openvibers/openvibe.sdk Commits
openvibe-sdk/themes: the scheme display preference (manual | auto) and each season's light half (#81)
Changes
@@@ -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 {@@@ -742,6 +742,7 @@"DISPLAY_OPTIONS": {"object": ["motion",++ "scheme","seasonal","text"]@@@ -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 {@@@ -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');@@@ -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[];
@@ -1995,7 +1995,7 @@ function (module, exports) {
@@ -1995,7 +1995,7 @@ function (module, exports) {
* const theme = await themes.get('nord'); // null when missing
* const theme = await themes.get('nord'); // null when missing
* const mine = await themes.active(); // { theme_id, custom_variables, display } (guests: theme_id null)
* 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.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
* 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):
* 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) {
@@ -2013,7 +2013,8 @@ function (module, exports) {
const { isOpenVibeError } = __load(3);
const { isOpenVibeError } = __load(3);
const enc = encodeURIComponent;
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). */
/** 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',
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',
'--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 } = {}) {
@@ -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; }
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 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;
let reduced = false;
try { reduced = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { reduced = false; }
try { reduced = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { reduced = false; }
return {
return {
@@ -742,6 +742,7 @@
@@ -742,6 +742,7 @@
"DISPLAY_OPTIONS": {
"DISPLAY_OPTIONS": {
"object": [
"object": [
"motion",
"motion",
+ "scheme",
"seasonal",
"seasonal",
"text"
"text"
]
]
@@ -11,7 +11,7 @@
@@ -11,7 +11,7 @@
* const theme = await themes.get('nord'); // null when missing
* const theme = await themes.get('nord'); // null when missing
* const mine = await themes.active(); // { theme_id, custom_variables, display } (guests: theme_id null)
* 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.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
* 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):
* On a page (the theme loader every OpenVibe site runs, openvibe-sdk/frame, paints the theme; this follows it):
@@ -29,7 +29,8 @@
@@ -29,7 +29,8 @@
const { isOpenVibeError } = require('./core/errors');
const { isOpenVibeError } = require('./core/errors');
const enc = encodeURIComponent;
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). */
/** 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',
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',
'--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 } = {}) {
@@ -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; }
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 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;
let reduced = false;
try { reduced = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { reduced = false; }
try { reduced = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { reduced = false; }
return {
return {
@@ -21,14 +21,14 @@ run([
@@ -21,14 +21,14 @@ run([
await t.catalog({ mode: 'dark' });
await t.catalog({ mode: 'dark' });
await t.active();
await t.active();
await t.set('vibe', { custom: { '--accent': '#ff5c8a' } });
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' });
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]);
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, [
assert.deepEqual(got, [
['GET', '/api/themes?mode=dark', null],
['GET', '/api/themes?mode=dark', null],
['GET', '/api/themes/me/active', null],
['GET', '/api/themes/me/active', null],
['PUT', '/api/themes/me', { theme_id: 'vibe', custom_variables: { '--accent': '#ff5c8a' } }],
['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],
['GET', '/api/themes/seasons?date=2026-10-25', null],
]);
]);
assert.equal(srv.requests[0].headers.authorization, 'Bearer user-jwt');
assert.equal(srv.requests[0].headers.authorization, 'Bearer user-jwt');
@@ -2,7 +2,8 @@ import type { OpenVibeClient } from './core';
@@ -2,7 +2,8 @@ import type { OpenVibeClient } from './core';
type Opts = { signal?: AbortSignal };
type Opts = { signal?: AbortSignal };
export type ThemeVariables = Record<string, string>;
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 {
export interface Theme {
id: string; name: string; slug: string; description?: string; mode: 'dark' | 'light'; variables: ThemeVariables;
id: string; name: string; slug: string; description?: string; mode: 'dark' | 'light'; variables: ThemeVariables;
preview_colors?: string[] | null; tags?: string[]; is_builtin?: number; downloads?: number;
preview_colors?: string[] | null; tags?: string[]; is_builtin?: number; downloads?: number;
@@ -10,7 +11,7 @@ export interface Theme {
@@ -10,7 +11,7 @@ export interface Theme {
season?: { id: string; name: string; from: string; to: string };
season?: { id: string; name: string; from: string; to: string };
[field: string]: unknown;
[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 ActiveTheme { theme_id: string | null; custom_variables?: ThemeVariables | null; display?: DisplayPrefs | null; guest?: boolean }
export interface ThemesClient {
export interface ThemesClient {
catalog(query?: { mode?: 'dark' | 'light'; search?: string; sort?: 'downloads' | 'rating'; limit?: number }, opts?: Opts): Promise<{ themes: Theme[] }>;
catalog(query?: { mode?: 'dark' | 'light'; search?: string; sort?: 'downloads' | 'rating'; limit?: number }, opts?: Opts): Promise<{ themes: Theme[] }>;
@@ -28,7 +29,7 @@ export interface ThemeSnapshot {
@@ -28,7 +29,7 @@ export interface ThemeSnapshot {
id: string | null;
id: string | null;
mode: 'dark' | 'light' | string;
mode: 'dark' | 'light' | string;
tokens: Record<string, 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;
season: { id: string } | null;
seasonMark: string | null;
seasonMark: string | null;
}
}
@@ -38,5 +39,5 @@ export function readTheme(opts?: { root?: Element; names?: readonly string[] }):
@@ -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 applyTheme(vars: ThemeVariables, opts?: { root?: HTMLElement }): HTMLElement | null;
export function seasonOn(date: Date | string | number | null | undefined, seasons: Season[]): Season | null;
export function seasonOn(date: Date | string | number | null | undefined, seasons: Season[]): Season | null;
export function onColor(hex: string): '#0b0d10' | '#ffffff' | 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[];
export const TOKENS: readonly string[];