openvibers/openvibe.sdk Commits

openvibe-sdk/themes: the scheme display preference (manual | auto) and each season's light half (#81)

OP OpenVibers committed 19 days ago · f326d6370c · 5 files, +16 −12

Changes

ddiff --git a/browser/openvibe-sdk.mjs b/browser/openvibe-sdk.mjs
iindex a4085f2..d5d780e 100644
---- a/browser/openvibe-sdk.mjs
++++ b/browser/openvibe-sdk.mjs
@@@ -1995,7 +1995,7 @@ function (module, exports) {
* const theme = await themes.get('nord'); // null when missing
* const mine = await themes.active(); // { theme_id, custom_variables, display } (guests: theme_id null)
* await themes.set('nord'); // or set('vibe', { custom: { '--accent': '#ff5c8a' } })
-- * await themes.setDisplay({ motion: 'reduced', text: '112', seasonal: 'off' });
++ * await themes.setDisplay({ motion: 'reduced', text: '112', seasonal: 'off', scheme: 'auto' });
* const { seasons, now } = await themes.seasons(); // what stands in for the default theme when
*
* On a page (the theme loader every OpenVibe site runs, openvibe-sdk/frame, paints the theme; this follows it):
@@@ -2013,7 +2013,8 @@ function (module, exports) {
const { isOpenVibeError } = __load(3);
const enc = encodeURIComponent;
--const DISPLAY_OPTIONS = Object.freeze({ motion: ['auto', 'reduced'], text: ['100', '112', '125'], seasonal: ['on', 'off'] });
++// scheme: 'manual' (the picked theme as it is) or 'auto' (its light or dark half, as the device is set).
++const DISPLAY_OPTIONS = Object.freeze({ motion: ['auto', 'reduced'], text: ['100', '112', '125'], seasonal: ['on', 'off'], scheme: ['manual', 'auto'] });
/** The tokens every OpenVibe theme sets (the shared names apps style with). */
const TOKENS = Object.freeze(['--bg-primary', '--bg-secondary', '--bg-tertiary', '--bg-card', '--bg-hover', '--text-primary', '--text-secondary', '--text-muted',
'--accent', '--accent-light', '--accent-dark', '--accent-strong', '--on-accent', '--on-accent-strong', '--accent-glow', '--border', '--border-light',
@@@ -2095,7 +2096,7 @@ function readTheme({ root, names = TOKENS } = {}) {
for (const n of names) { const v = cs.getPropertyValue(n).trim(); if (v) tokens[n] = v; }
}
const raw = (L && L.display && typeof L.display.get === 'function') ? L.display.get() : {};
-- const display = { motion: 'auto', text: '100', seasonal: 'on', ...cleanDisplay(raw) };
++ const display = { motion: 'auto', text: '100', seasonal: 'on', scheme: 'manual', ...cleanDisplay(raw) };
let reduced = false;
try { reduced = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { reduced = false; }
return {
ddiff --git a/docs/api-surface.json b/docs/api-surface.json
iindex e6d3dec..9655b7a 100644
---- a/docs/api-surface.json
++++ b/docs/api-surface.json
@@@ -742,6 +742,7 @@
"DISPLAY_OPTIONS": {
"object": [
"motion",
++ "scheme",
"seasonal",
"text"
]
ddiff --git a/src/themes.js b/src/themes.js
iindex 9c37fad..547bac7 100644
---- a/src/themes.js
++++ b/src/themes.js
@@@ -11,7 +11,7 @@
* const theme = await themes.get('nord'); // null when missing
* const mine = await themes.active(); // { theme_id, custom_variables, display } (guests: theme_id null)
* await themes.set('nord'); // or set('vibe', { custom: { '--accent': '#ff5c8a' } })
-- * await themes.setDisplay({ motion: 'reduced', text: '112', seasonal: 'off' });
++ * await themes.setDisplay({ motion: 'reduced', text: '112', seasonal: 'off', scheme: 'auto' });
* const { seasons, now } = await themes.seasons(); // what stands in for the default theme when
*
* On a page (the theme loader every OpenVibe site runs, openvibe-sdk/frame, paints the theme; this follows it):
@@@ -29,7 +29,8 @@
const { isOpenVibeError } = require('./core/errors');
const enc = encodeURIComponent;
--const DISPLAY_OPTIONS = Object.freeze({ motion: ['auto', 'reduced'], text: ['100', '112', '125'], seasonal: ['on', 'off'] });
++// scheme: 'manual' (the picked theme as it is) or 'auto' (its light or dark half, as the device is set).
++const DISPLAY_OPTIONS = Object.freeze({ motion: ['auto', 'reduced'], text: ['100', '112', '125'], seasonal: ['on', 'off'], scheme: ['manual', 'auto'] });
/** The tokens every OpenVibe theme sets (the shared names apps style with). */
const TOKENS = Object.freeze(['--bg-primary', '--bg-secondary', '--bg-tertiary', '--bg-card', '--bg-hover', '--text-primary', '--text-secondary', '--text-muted',
'--accent', '--accent-light', '--accent-dark', '--accent-strong', '--on-accent', '--on-accent-strong', '--accent-glow', '--border', '--border-light',
@@@ -111,7 +112,7 @@ function readTheme({ root, names = TOKENS } = {}) {
for (const n of names) { const v = cs.getPropertyValue(n).trim(); if (v) tokens[n] = v; }
}
const raw = (L && L.display && typeof L.display.get === 'function') ? L.display.get() : {};
-- const display = { motion: 'auto', text: '100', seasonal: 'on', ...cleanDisplay(raw) };
++ const display = { motion: 'auto', text: '100', seasonal: 'on', scheme: 'manual', ...cleanDisplay(raw) };
let reduced = false;
try { reduced = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { reduced = false; }
return {
ddiff --git a/test/themes.test.js b/test/themes.test.js
iindex 7aaeb01..ab905f5 100644
---- a/test/themes.test.js
++++ b/test/themes.test.js
@@@ -21,14 +21,14 @@ run([
await t.catalog({ mode: 'dark' });
await t.active();
await t.set('vibe', { custom: { '--accent': '#ff5c8a' } });
-- await t.setDisplay({ motion: 'reduced', text: '125', seasonal: 'off', colour: 'red', 'text-extra': 1 });
++ await t.setDisplay({ motion: 'reduced', text: '125', seasonal: 'off', scheme: 'auto', colour: 'red', 'text-extra': 1 });
await t.seasons({ date: '2026-10-25' });
const got = srv.requests.slice(2).map((r) => [r.method, r.url, r.body && String(r.body).length ? JSON.parse(String(r.body)) : null]);
assert.deepEqual(got, [
['GET', '/api/themes?mode=dark', null],
['GET', '/api/themes/me/active', null],
['PUT', '/api/themes/me', { theme_id: 'vibe', custom_variables: { '--accent': '#ff5c8a' } }],
-- ['PUT', '/api/themes/me/display', { motion: 'reduced', text: '125', seasonal: 'off' }],
++ ['PUT', '/api/themes/me/display', { motion: 'reduced', text: '125', seasonal: 'off', scheme: 'auto' }],
['GET', '/api/themes/seasons?date=2026-10-25', null],
]);
assert.equal(srv.requests[0].headers.authorization, 'Bearer user-jwt');
ddiff --git a/types/themes.d.ts b/types/themes.d.ts
iindex 702b755..8f49d6c 100644
---- a/types/themes.d.ts
++++ b/types/themes.d.ts
@@@ -2,7 +2,8 @@ import type { OpenVibeClient } from './core';
type Opts = { signal?: AbortSignal };
export type ThemeVariables = Record<string, string>;
--export interface Season { id: string; name: string; theme: string; from: string; to: string }
++/** theme: the dark half; light: its light half (Hayride for Halloween). */
++export interface Season { id: string; name: string; theme: string; light?: string; from: string; to: string }
export interface Theme {
id: string; name: string; slug: string; description?: string; mode: 'dark' | 'light'; variables: ThemeVariables;
preview_colors?: string[] | null; tags?: string[]; is_builtin?: number; downloads?: number;
@@@ -10,7 +11,7 @@ export interface Theme {
season?: { id: string; name: string; from: string; to: string };
[field: string]: unknown;
}
--export interface DisplayPrefs { motion?: 'auto' | 'reduced'; text?: '100' | '112' | '125'; seasonal?: 'on' | 'off' }
++export interface DisplayPrefs { motion?: 'auto' | 'reduced'; text?: '100' | '112' | '125'; seasonal?: 'on' | 'off'; scheme?: 'manual' | 'auto' }
export interface ActiveTheme { theme_id: string | null; custom_variables?: ThemeVariables | null; display?: DisplayPrefs | null; guest?: boolean }
export interface ThemesClient {
catalog(query?: { mode?: 'dark' | 'light'; search?: string; sort?: 'downloads' | 'rating'; limit?: number }, opts?: Opts): Promise<{ themes: Theme[] }>;
@@@ -28,7 +29,7 @@ export interface ThemeSnapshot {
id: string | null;
mode: 'dark' | 'light' | string;
tokens: Record<string, string>;
-- display: { motion: 'auto' | 'reduced'; text: '100' | '112' | '125'; seasonal: 'on' | 'off'; textScale: number; calm: boolean };
++ display: { motion: 'auto' | 'reduced'; text: '100' | '112' | '125'; seasonal: 'on' | 'off'; scheme: 'manual' | 'auto'; textScale: number; calm: boolean };
season: { id: string } | null;
seasonMark: string | null;
}
@@@ -38,5 +39,5 @@ export function readTheme(opts?: { root?: Element; names?: readonly string[] }):
export function applyTheme(vars: ThemeVariables, opts?: { root?: HTMLElement }): HTMLElement | null;
export function seasonOn(date: Date | string | number | null | undefined, seasons: Season[]): Season | null;
export function onColor(hex: string): '#0b0d10' | '#ffffff' | null;
--export const DISPLAY_OPTIONS: Readonly<{ motion: string[]; text: string[]; seasonal: string[] }>;
++export const DISPLAY_OPTIONS: Readonly<{ motion: string[]; text: string[]; seasonal: string[]; scheme: string[] }>;
export const TOKENS: readonly string[];