@bunnyland/ui-web 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +211 -0
  3. package/assets/bunnyland-api.js +345 -0
  4. package/assets/bunnyland-play.js +1223 -0
  5. package/assets/bunnyland-ui.css +1634 -0
  6. package/assets/bunnyland-ui.js +795 -0
  7. package/dist/admin-widgets.d.ts +31 -0
  8. package/dist/admin-widgets.d.ts.map +1 -0
  9. package/dist/admin-widgets.js +100 -0
  10. package/dist/admin-widgets.js.map +1 -0
  11. package/dist/api.d.ts +37 -0
  12. package/dist/api.d.ts.map +1 -0
  13. package/dist/api.js +125 -0
  14. package/dist/api.js.map +1 -0
  15. package/dist/index.d.ts +7 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +7 -0
  18. package/dist/play.d.ts +265 -0
  19. package/dist/play.d.ts.map +1 -0
  20. package/dist/play.js +806 -0
  21. package/dist/play.js.map +1 -0
  22. package/dist/player-widgets.d.ts +11 -0
  23. package/dist/player-widgets.d.ts.map +1 -0
  24. package/dist/player-widgets.js +21 -0
  25. package/dist/player-widgets.js.map +1 -0
  26. package/dist/preact/auth.d.ts +37 -0
  27. package/dist/preact/auth.d.ts.map +1 -0
  28. package/dist/preact/components.d.ts +58 -0
  29. package/dist/preact/components.d.ts.map +1 -0
  30. package/dist/preact/theme.d.ts +16 -0
  31. package/dist/preact/theme.d.ts.map +1 -0
  32. package/dist/preact.d.ts +4 -0
  33. package/dist/preact.d.ts.map +1 -0
  34. package/dist/preact.js +488 -0
  35. package/dist/preact.js.map +1 -0
  36. package/dist/theme.d.ts +31 -0
  37. package/dist/theme.d.ts.map +1 -0
  38. package/dist/theme.js +165 -0
  39. package/dist/theme.js.map +1 -0
  40. package/dist/widgets.d.ts +7 -0
  41. package/dist/widgets.d.ts.map +1 -0
  42. package/dist/widgets.js +31 -0
  43. package/dist/widgets.js.map +1 -0
  44. package/docs/admin/custom-web-themes.md +112 -0
  45. package/docs/developer/design-language.md +116 -0
  46. package/package.json +101 -0
  47. package/src/admin-widgets.ts +189 -0
  48. package/src/api.ts +193 -0
  49. package/src/index.ts +6 -0
  50. package/src/play.ts +1151 -0
  51. package/src/player-widgets.ts +29 -0
  52. package/src/preact/auth.tsx +338 -0
  53. package/src/preact/components.tsx +249 -0
  54. package/src/preact/theme.tsx +72 -0
  55. package/src/preact.ts +3 -0
  56. package/src/theme.ts +225 -0
  57. package/src/widgets.ts +41 -0
package/dist/theme.js ADDED
@@ -0,0 +1,165 @@
1
+ import { escapeHtml as e, storageGet as t, storageSet as n } from "./widgets.js";
2
+ //#region src/theme.ts
3
+ var r = "bunnyland.theme", i = "bl-theme-", a = "bunnyland.color-scheme", o = "bl-color-scheme-", s = "purple-blue", c = "bunnyland:themechange", l = [
4
+ {
5
+ value: "purple-blue",
6
+ label: "Purple / Blue"
7
+ },
8
+ {
9
+ value: "candy",
10
+ label: "Candy Pink / Cyan"
11
+ },
12
+ {
13
+ value: "earth",
14
+ label: "Earth Green / Gold"
15
+ },
16
+ {
17
+ value: "ocean",
18
+ label: "Ocean Teal / Coral"
19
+ },
20
+ {
21
+ value: "sunset",
22
+ label: "Sunset Orange / Plum"
23
+ },
24
+ {
25
+ value: "high-contrast",
26
+ label: "High Contrast"
27
+ }
28
+ ], u = l.map((e) => ({ ...e })), d = {
29
+ anime: "candy",
30
+ "anime-dark": "candy-dark",
31
+ "anime-light": "candy-light",
32
+ dark: "purple-blue-dark",
33
+ light: "purple-blue-light"
34
+ }, f = [
35
+ {
36
+ value: "auto",
37
+ label: "Auto (System)"
38
+ },
39
+ {
40
+ value: "dark",
41
+ label: "Dark"
42
+ },
43
+ {
44
+ value: "light",
45
+ label: "Light"
46
+ }
47
+ ], p = /^[a-z0-9][a-z0-9-]*$/, m = /* @__PURE__ */ new Set(), h = /* @__PURE__ */ new Set();
48
+ function g(e) {
49
+ let t = d[String(e || "purple-blue").trim()] || String(e || "purple-blue").trim();
50
+ if (v(t)) return {
51
+ theme: t,
52
+ scheme: null
53
+ };
54
+ let n = t.match(/^(.*)-(dark|light)$/);
55
+ return n && v(n[1]) ? {
56
+ theme: n[1],
57
+ scheme: n[2]
58
+ } : {
59
+ theme: t,
60
+ scheme: null
61
+ };
62
+ }
63
+ function _(e) {
64
+ return g(e).theme;
65
+ }
66
+ function v(e) {
67
+ return u.some((t) => t.value === e);
68
+ }
69
+ function y(e) {
70
+ let t = String(e?.value || "").trim();
71
+ return p.test(t) ? {
72
+ value: t,
73
+ label: String(e?.label || t).trim() || t
74
+ } : null;
75
+ }
76
+ function b(t) {
77
+ let n = D();
78
+ t.innerHTML = F().map((t) => `
79
+ <option value="${e(t.value)}">${e(t.label)}</option>
80
+ `).join(""), t.value = n;
81
+ }
82
+ function x() {
83
+ for (let e of m) b(e);
84
+ }
85
+ function S(e) {
86
+ return e === "dark" || e === "light" ? e : "auto";
87
+ }
88
+ function C(e) {
89
+ e.innerHTML = f.map((e) => `
90
+ <option value="${e.value}">${e.label}</option>
91
+ `).join(""), e.value = O();
92
+ }
93
+ function w() {
94
+ for (let e of h) C(e);
95
+ }
96
+ function T(e = globalThis.location?.search || "") {
97
+ let t = new URLSearchParams(e).get("theme");
98
+ if (!t) return null;
99
+ let n = _(t);
100
+ return v(n) ? n : null;
101
+ }
102
+ function E(e) {
103
+ let t = _(e);
104
+ return v(t) ? t : s;
105
+ }
106
+ function D(e = document.documentElement) {
107
+ return E(e.dataset.theme);
108
+ }
109
+ function O(e = document.documentElement) {
110
+ return S(e.dataset.colorScheme);
111
+ }
112
+ function k(e) {
113
+ typeof e.dispatchEvent == "function" && typeof CustomEvent == "function" && e.dispatchEvent(new CustomEvent(c, { detail: {
114
+ colorScheme: O(e),
115
+ theme: D(e)
116
+ } }));
117
+ }
118
+ function A(e, t, a) {
119
+ for (let e of [...t.classList]) e.startsWith("bl-theme-") && t.classList.remove(e);
120
+ return t.classList.add(`${i}${e}`), t.dataset.theme = e, a && n(r, e), x(), k(t), e;
121
+ }
122
+ function j(e, t, r) {
123
+ for (let e of [...t.classList]) e.startsWith("bl-color-scheme-") && t.classList.remove(e);
124
+ return e !== "auto" && t.classList.add(`${o}${e}`), t.dataset.colorScheme = e, r && n(a, e), w(), k(t), e;
125
+ }
126
+ function M(e, t = document.documentElement) {
127
+ return j(S(e), t, !0);
128
+ }
129
+ function N(e, t = document.documentElement) {
130
+ let n = g(e);
131
+ return n.scheme && j(n.scheme, t, !0), A(E(n.theme), t, !0);
132
+ }
133
+ function P(e = document.documentElement, n = s, i = globalThis.location?.search || "") {
134
+ let o = new URLSearchParams(i).get("theme"), c = T(i), l = t(r), u = g(c && o || l || n || "purple-blue"), d = t(a);
135
+ return j(u.scheme || S(d), e, !!(u.scheme || d)), A(E(u.theme), e, !!(c || l && v(u.theme)));
136
+ }
137
+ function F() {
138
+ return u.map((e) => ({ ...e }));
139
+ }
140
+ function I() {
141
+ return f.map((e) => ({ ...e }));
142
+ }
143
+ function L(e) {
144
+ let t = y(e);
145
+ if (!t) return null;
146
+ let n = u.findIndex((e) => e.value === t.value);
147
+ return n === -1 ? u.push(t) : u[n] = t, x(), { ...t };
148
+ }
149
+ function R(e) {
150
+ return Array.isArray(e) ? e.map((e) => L(e)).filter((e) => e !== null) : [];
151
+ }
152
+ function z(e) {
153
+ return e ? (m.add(e), b(e), e.value = P(), e.addEventListener("change", () => N(e.value)), { setValue(t) {
154
+ e.value = E(t), N(e.value);
155
+ } }) : null;
156
+ }
157
+ function B(e) {
158
+ return e ? (h.add(e), C(e), e.addEventListener("change", () => M(e.value)), { setValue(t) {
159
+ e.value = S(t), M(e.value);
160
+ } }) : null;
161
+ }
162
+ //#endregion
163
+ export { o as COLOR_SCHEME_CLASS_PREFIX, a as COLOR_SCHEME_KEY, s as DEFAULT_THEME, l as DEFAULT_THEME_OPTIONS, c as THEME_CHANGE_EVENT, i as THEME_CLASS_PREFIX, r as THEME_KEY, u as THEME_OPTIONS, B as bindColorSchemeSelect, z as bindThemeSelect, I as colorSchemeOptions, O as currentColorScheme, D as currentTheme, P as initTheme, E as normalizeTheme, L as registerThemeOption, R as registerThemeOptions, M as setColorScheme, N as setTheme, T as themeFromSearch, F as themeOptions };
164
+
165
+ //# sourceMappingURL=theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.js","names":[],"sources":["../src/theme.ts"],"sourcesContent":["import { escapeHtml, storageGet, storageSet } from './widgets';\n\nexport const THEME_KEY = 'bunnyland.theme';\nexport const THEME_CLASS_PREFIX = 'bl-theme-';\nexport const COLOR_SCHEME_KEY = 'bunnyland.color-scheme';\nexport const COLOR_SCHEME_CLASS_PREFIX = 'bl-color-scheme-';\nexport const DEFAULT_THEME = 'purple-blue';\nexport const THEME_CHANGE_EVENT = 'bunnyland:themechange';\n\nexport interface ThemeOption {\n value: string;\n label: string;\n}\n\nexport type ColorScheme = 'auto' | 'dark' | 'light';\n\nexport const DEFAULT_THEME_OPTIONS: ThemeOption[] = [\n { value: 'purple-blue', label: 'Purple / Blue' },\n { value: 'candy', label: 'Candy Pink / Cyan' },\n { value: 'earth', label: 'Earth Green / Gold' },\n { value: 'ocean', label: 'Ocean Teal / Coral' },\n { value: 'sunset', label: 'Sunset Orange / Plum' },\n { value: 'high-contrast', label: 'High Contrast' },\n];\n\nexport const THEME_OPTIONS: ThemeOption[] = DEFAULT_THEME_OPTIONS.map(option => ({ ...option }));\n\nconst THEME_ALIASES: Record<string, string> = {\n anime: 'candy',\n 'anime-dark': 'candy-dark',\n 'anime-light': 'candy-light',\n dark: 'purple-blue-dark',\n light: 'purple-blue-light',\n};\n\nconst COLOR_SCHEME_OPTIONS: ThemeOption[] = [\n { value: 'auto', label: 'Auto (System)' },\n { value: 'dark', label: 'Dark' },\n { value: 'light', label: 'Light' },\n];\n\nconst THEME_VALUE_PATTERN = /^[a-z0-9][a-z0-9-]*$/;\nconst boundThemeSelects = new Set<HTMLSelectElement>();\nconst boundColorSchemeSelects = new Set<HTMLSelectElement>();\n\nfunction parseThemeSelection(name: string | null | undefined): { scheme: ColorScheme | null; theme: string } {\n const raw = THEME_ALIASES[String(name || DEFAULT_THEME).trim()] || String(name || DEFAULT_THEME).trim();\n if (isKnownTheme(raw)) return { theme: raw, scheme: null };\n const match = raw.match(/^(.*)-(dark|light)$/);\n if (match && isKnownTheme(match[1])) return { theme: match[1], scheme: match[2] as ColorScheme };\n return { theme: raw, scheme: null };\n}\n\nfunction normalizeThemeValue(name: string | null | undefined): string {\n return parseThemeSelection(name).theme;\n}\n\nfunction isKnownTheme(name: string): boolean {\n return THEME_OPTIONS.some(option => option.value === name);\n}\n\nfunction sanitizeThemeOption(option: ThemeOption): ThemeOption | null {\n const value = String(option?.value || '').trim();\n if (!THEME_VALUE_PATTERN.test(value)) return null;\n const label = String(option?.label || value).trim() || value;\n return { value, label };\n}\n\nfunction renderThemeSelect(select: HTMLSelectElement): void {\n const theme = currentTheme();\n select.innerHTML = themeOptions().map(option => `\n <option value=\"${escapeHtml(option.value)}\">${escapeHtml(option.label)}</option>\n `).join('');\n select.value = theme;\n}\n\nfunction refreshThemeSelects(): void {\n for (const select of boundThemeSelects) renderThemeSelect(select);\n}\n\nfunction normalizeColorSchemeValue(name: string | null | undefined): ColorScheme {\n return name === 'dark' || name === 'light' ? name : 'auto';\n}\n\nfunction renderColorSchemeSelect(select: HTMLSelectElement): void {\n select.innerHTML = COLOR_SCHEME_OPTIONS.map(option => `\n <option value=\"${option.value}\">${option.label}</option>\n `).join('');\n select.value = currentColorScheme();\n}\n\nfunction refreshColorSchemeSelects(): void {\n for (const select of boundColorSchemeSelects) renderColorSchemeSelect(select);\n}\n\nexport function themeFromSearch(search = globalThis.location?.search || ''): string | null {\n const requested = new URLSearchParams(search).get('theme');\n if (!requested) return null;\n const theme = normalizeThemeValue(requested);\n return isKnownTheme(theme) ? theme : null;\n}\n\nexport function normalizeTheme(name: string | null | undefined): string {\n const theme = normalizeThemeValue(name);\n return isKnownTheme(theme) ? theme : DEFAULT_THEME;\n}\n\nexport function currentTheme(root: HTMLElement = document.documentElement): string {\n return normalizeTheme(root.dataset.theme);\n}\n\nexport function currentColorScheme(root: HTMLElement = document.documentElement): ColorScheme {\n return normalizeColorSchemeValue(root.dataset.colorScheme);\n}\n\nfunction dispatchThemeChange(root: HTMLElement): void {\n if (typeof root.dispatchEvent === 'function' && typeof CustomEvent === 'function') {\n root.dispatchEvent(new CustomEvent(THEME_CHANGE_EVENT, {\n detail: { colorScheme: currentColorScheme(root), theme: currentTheme(root) },\n }));\n }\n}\n\nfunction applyTheme(theme: string, root: HTMLElement, persist: boolean): string {\n for (const className of [...root.classList]) {\n if (className.startsWith(THEME_CLASS_PREFIX)) root.classList.remove(className);\n }\n root.classList.add(`${THEME_CLASS_PREFIX}${theme}`);\n root.dataset.theme = theme;\n if (persist) storageSet(THEME_KEY, theme);\n refreshThemeSelects();\n dispatchThemeChange(root);\n return theme;\n}\n\nfunction applyColorScheme(scheme: ColorScheme, root: HTMLElement, persist: boolean): ColorScheme {\n for (const className of [...root.classList]) {\n if (className.startsWith(COLOR_SCHEME_CLASS_PREFIX)) root.classList.remove(className);\n }\n if (scheme !== 'auto') root.classList.add(`${COLOR_SCHEME_CLASS_PREFIX}${scheme}`);\n root.dataset.colorScheme = scheme;\n if (persist) storageSet(COLOR_SCHEME_KEY, scheme);\n refreshColorSchemeSelects();\n dispatchThemeChange(root);\n return scheme;\n}\n\nexport function setColorScheme(name: string, root: HTMLElement = document.documentElement): ColorScheme {\n return applyColorScheme(normalizeColorSchemeValue(name), root, true);\n}\n\nexport function setTheme(name: string, root: HTMLElement = document.documentElement): string {\n const selection = parseThemeSelection(name);\n if (selection.scheme) applyColorScheme(selection.scheme, root, true);\n const theme = normalizeTheme(selection.theme);\n return applyTheme(theme, root, true);\n}\n\nexport function initTheme(\n root: HTMLElement = document.documentElement,\n defaultTheme = DEFAULT_THEME,\n search = globalThis.location?.search || '',\n): string {\n const linkedValue = new URLSearchParams(search).get('theme');\n const linkedTheme = themeFromSearch(search);\n const stored = storageGet(THEME_KEY);\n const selection = parseThemeSelection((linkedTheme && linkedValue) || stored || defaultTheme || DEFAULT_THEME);\n const storedScheme = storageGet(COLOR_SCHEME_KEY);\n const scheme = selection.scheme || normalizeColorSchemeValue(storedScheme);\n applyColorScheme(scheme, root, Boolean(selection.scheme || storedScheme));\n const theme = normalizeTheme(selection.theme);\n return applyTheme(theme, root, Boolean(linkedTheme || (stored && isKnownTheme(selection.theme))));\n}\n\nexport function themeOptions(): ThemeOption[] {\n return THEME_OPTIONS.map(option => ({ ...option }));\n}\n\nexport function colorSchemeOptions(): ThemeOption[] {\n return COLOR_SCHEME_OPTIONS.map(option => ({ ...option }));\n}\n\nexport function registerThemeOption(option: ThemeOption): ThemeOption | null {\n const theme = sanitizeThemeOption(option);\n if (!theme) return null;\n const index = THEME_OPTIONS.findIndex(existing => existing.value === theme.value);\n if (index === -1) THEME_OPTIONS.push(theme);\n else THEME_OPTIONS[index] = theme;\n refreshThemeSelects();\n return { ...theme };\n}\n\nexport function registerThemeOptions(options: ThemeOption[] | null | undefined): ThemeOption[] {\n if (!Array.isArray(options)) return [];\n return options\n .map(option => registerThemeOption(option))\n .filter((option): option is ThemeOption => option !== null);\n}\n\nexport function bindThemeSelect(select: HTMLSelectElement | null): { setValue: (value: string) => void } | null {\n if (!select) return null;\n boundThemeSelects.add(select);\n renderThemeSelect(select);\n select.value = initTheme();\n select.addEventListener('change', () => setTheme(select.value));\n return {\n setValue(value: string): void {\n select.value = normalizeTheme(value);\n setTheme(select.value);\n },\n };\n}\n\nexport function bindColorSchemeSelect(select: HTMLSelectElement | null): { setValue: (value: string) => void } | null {\n if (!select) return null;\n boundColorSchemeSelects.add(select);\n renderColorSchemeSelect(select);\n select.addEventListener('change', () => setColorScheme(select.value));\n return {\n setValue(value: string): void {\n select.value = normalizeColorSchemeValue(value);\n setColorScheme(select.value);\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export declare function escapeHtml(value: unknown): string;
2
+ export declare function cloneJson<T>(value: T): T;
3
+ export declare function storageGet(key: string): string | null;
4
+ export declare function storageSet(key: string, value: string): void;
5
+ export declare function storageRemove(key: string): void;
6
+ export declare function normalizeTags(value: unknown): string[];
7
+ //# sourceMappingURL=widgets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"widgets.d.ts","sourceRoot":"","sources":["../src/widgets.ts"],"names":[],"mappings":"AAAA,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAMjD;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAExC;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMrD;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAM3D;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAM/C;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAItD"}
@@ -0,0 +1,31 @@
1
+ //#region src/widgets.ts
2
+ function e(e) {
3
+ return String(e).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4
+ }
5
+ function t(e) {
6
+ return JSON.parse(JSON.stringify(e));
7
+ }
8
+ function n(e) {
9
+ try {
10
+ return localStorage.getItem(e);
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+ function r(e, t) {
16
+ try {
17
+ localStorage.setItem(e, t);
18
+ } catch {}
19
+ }
20
+ function i(e) {
21
+ try {
22
+ localStorage.removeItem(e);
23
+ } catch {}
24
+ }
25
+ function a(e) {
26
+ return Array.isArray(e) ? e.map((e) => String(e)).filter(Boolean) : typeof e == "string" ? e.split(",").map((e) => e.trim()).filter(Boolean) : [];
27
+ }
28
+ //#endregion
29
+ export { t as cloneJson, e as escapeHtml, a as normalizeTags, n as storageGet, i as storageRemove, r as storageSet };
30
+
31
+ //# sourceMappingURL=widgets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"widgets.js","names":[],"sources":["../src/widgets.ts"],"sourcesContent":["export function escapeHtml(value: unknown): string {\n return String(value)\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n}\n\nexport function cloneJson<T>(value: T): T {\n return JSON.parse(JSON.stringify(value)) as T;\n}\n\nexport function storageGet(key: string): string | null {\n try {\n return localStorage.getItem(key);\n } catch (_err) {\n return null;\n }\n}\n\nexport function storageSet(key: string, value: string): void {\n try {\n localStorage.setItem(key, value);\n } catch (_err) {\n // Preferences are best-effort; UI should keep working without storage.\n }\n}\n\nexport function storageRemove(key: string): void {\n try {\n localStorage.removeItem(key);\n } catch (_err) {\n // Best-effort storage cleanup only.\n }\n}\n\nexport function normalizeTags(value: unknown): string[] {\n if (Array.isArray(value)) return value.map(tag => String(tag)).filter(Boolean);\n if (typeof value === 'string') return value.split(',').map(tag => tag.trim()).filter(Boolean);\n return [];\n}\n"],"mappings":";AAAA,SAAgB,EAAW,GAAwB;CACjD,OAAO,OAAO,CAAK,CAAC,CACjB,QAAQ,MAAM,OAAO,CAAC,CACtB,QAAQ,MAAM,MAAM,CAAC,CACrB,QAAQ,MAAM,MAAM,CAAC,CACrB,QAAQ,MAAM,QAAQ;AAC3B;AAEA,SAAgB,EAAa,GAAa;CACxC,OAAO,KAAK,MAAM,KAAK,UAAU,CAAK,CAAC;AACzC;AAEA,SAAgB,EAAW,GAA4B;CACrD,IAAI;EACF,OAAO,aAAa,QAAQ,CAAG;CACjC,QAAe;EACb,OAAO;CACT;AACF;AAEA,SAAgB,EAAW,GAAa,GAAqB;CAC3D,IAAI;EACF,aAAa,QAAQ,GAAK,CAAK;CACjC,QAAe,CAEf;AACF;AAEA,SAAgB,EAAc,GAAmB;CAC/C,IAAI;EACF,aAAa,WAAW,CAAG;CAC7B,QAAe,CAEf;AACF;AAEA,SAAgB,EAAc,GAA0B;CAGtD,OAFI,MAAM,QAAQ,CAAK,IAAU,EAAM,KAAI,MAAO,OAAO,CAAG,CAAC,CAAC,CAAC,OAAO,OAAO,IACzE,OAAO,KAAU,WAAiB,EAAM,MAAM,GAAG,CAAC,CAAC,KAAI,MAAO,EAAI,KAAK,CAAC,CAAC,CAAC,OAAO,OAAO,IACrF,CAAC;AACV"}
@@ -0,0 +1,112 @@
1
+ # Custom web themes
2
+
3
+ Server admins can add deployment-specific themes to Bunnyland browser clients without
4
+ patching `@bunnyland/ui-web`. A custom theme has two parts:
5
+
6
+ 1. a theme option in the client `config.json`, so the theme appears in selectors;
7
+ 2. CSS for the matching root class, so the theme changes colors.
8
+
9
+ ## Add theme options
10
+
11
+ Static Bunnyland browser clients read `config.json` from the site root. Add a `themes`
12
+ array with one object per custom theme:
13
+
14
+ ```json
15
+ {
16
+ "serverUrl": "/api/",
17
+ "autoConnect": true,
18
+ "discordUrl": "https://discord.gg/example",
19
+ "theme": "server-night",
20
+ "themes": [
21
+ { "value": "server-night", "label": "Server Night" },
22
+ { "value": "festival-day", "label": "Festival Day" }
23
+ ]
24
+ }
25
+ ```
26
+
27
+ `value` becomes the class suffix in `bl-theme-<value>`, so use lowercase letters,
28
+ numbers, and hyphens. `label` is the human-readable selector text.
29
+
30
+ `theme` is the deployment default. It is applied when a visitor does not already have a
31
+ saved theme preference. A link with `?theme=<value>` overrides both the deployment default
32
+ and the visitor's saved preference, then saves that linked theme for future pages.
33
+
34
+ When using the Bunnyland web Docker image, set `BUNNYLAND_WEB_THEMES` to the JSON array and
35
+ `BUNNYLAND_WEB_THEME` to the default value:
36
+
37
+ ```sh
38
+ BUNNYLAND_WEB_THEME='server-night'
39
+ BUNNYLAND_WEB_THEMES='[{"value":"server-night","label":"Server Night"}]'
40
+ ```
41
+
42
+ Restart the frontend container after changing the variable.
43
+
44
+ ## Serve the CSS
45
+
46
+ Registering a theme only adds it to the list. The colors come from CSS variables on the
47
+ matching root class:
48
+
49
+ ```css
50
+ :root.bl-theme-server-night {
51
+ color-scheme: dark;
52
+
53
+ --bl-bg: #101218;
54
+ --bl-bg-strong: #171b25;
55
+ --bl-bg-deep: #080a0f;
56
+ --bl-bg-row: #151923;
57
+ --bl-bg-subtle: #1d2330;
58
+ --bl-surface: #242b38;
59
+ --bl-surface-hover: #30394a;
60
+ --bl-border: #30394a;
61
+ --bl-border-muted: #202633;
62
+ --bl-border-control: #465267;
63
+ --bl-border-strong: #5d6a80;
64
+
65
+ --bl-text: #edf2ff;
66
+ --bl-text-soft: #c5cde0;
67
+ --bl-text-muted: #7f8aa3;
68
+ --bl-text-dim: #9aa4ba;
69
+ --bl-text-inverse: #ffffff;
70
+ --bl-accent: #74c7ec;
71
+ --bl-accent-strong: #a6e3a1;
72
+ --bl-secondary: #cba6f7;
73
+ --bl-ok: #a6e3a1;
74
+ --bl-error: #f38ba8;
75
+ --bl-warn: #f9e2af;
76
+ --bl-info: #89dceb;
77
+
78
+ --bl-shadow-popover: 0 6px 18px rgba(0, 0, 0, 0.55);
79
+ --bl-shadow-text: 0 1px 2px rgba(0, 0, 0, 0.6);
80
+ --bl-overlay: rgba(8, 10, 15, 0.74);
81
+ --bl-overlay-strong: rgba(8, 10, 15, 0.88);
82
+ --bl-overlay-solid: rgba(16, 18, 24, 0.96);
83
+ --bl-accent-wash: rgba(116, 199, 236, 0.14);
84
+ --bl-accent-wash-strong: rgba(116, 199, 236, 0.28);
85
+ --bl-secondary-wash: rgba(203, 166, 247, 0.22);
86
+ --bl-secondary-wash-strong: rgba(203, 166, 247, 0.34);
87
+ }
88
+ ```
89
+
90
+ Place that CSS after `assets/bunnyland-ui.css`, or in a separate stylesheet loaded after
91
+ it. Existing pages can keep using `bindThemeSelect()` and the client menu; the shared
92
+ theme helpers refresh selectors after custom themes are registered.
93
+
94
+ This example is intentionally dark-only. A custom theme that supports automatic appearance
95
+ must also define its light tokens inside `@media (prefers-color-scheme: light)` and under a
96
+ `.bl-color-scheme-light` override; exclude `.bl-color-scheme-dark` from its media-query
97
+ selector. Built-in palettes already provide both modes.
98
+
99
+ ## Validate
100
+
101
+ After deployment:
102
+
103
+ 1. Open `/config.json` and confirm the `themes` array is valid JSON.
104
+ 2. Open a browser client and check that the custom label appears in the theme selector.
105
+ 3. Select the theme and confirm the page root has `data-theme="<value>"` and the
106
+ `bl-theme-<value>` class.
107
+ 4. Open a link such as `/?theme=server-night` and confirm the linked theme is selected.
108
+ 5. Check at least one admin page and one player page, because both use the shared theme
109
+ variables but have different layouts.
110
+
111
+ If the option appears but colors do not change, the CSS did not load after
112
+ `bunnyland-ui.css` or the CSS class does not match the configured theme value.
@@ -0,0 +1,116 @@
1
+ # Bunnyland web design language
2
+
3
+ This document is the canonical design contract for Bunnyland browser clients. The shared
4
+ tokens, native helpers, and Preact components live in `@bunnyland/ui-web`; application
5
+ repositories should consume them rather than copy their behavior.
6
+
7
+ ## Principles
8
+
9
+ 1. **Dense, readable tools.** Bunnyland clients are information-rich editors and play
10
+ surfaces. Prefer compact controls, visible labels, and stable layouts over decorative
11
+ whitespace.
12
+ 2. **State changes are local.** Put interactive state in the smallest component that owns
13
+ it. A changed field must not recreate unrelated lists, toolbars, canvases, or focused
14
+ controls.
15
+ 3. **The server remains authoritative.** Components display typed projections and schema
16
+ data. They do not infer hidden world state or duplicate server rules.
17
+ 4. **Native browser behavior first.** Use semantic buttons, labels, inputs, dialogs, and
18
+ links. Preact coordinates those elements; it does not replace their accessibility.
19
+ 5. **Imperative renderers are islands.** Three.js and LiteGraph retain ownership of their
20
+ canvases. Preact owns their surrounding controls and uses refs/effects for lifecycle.
21
+
22
+ ## Foundations
23
+
24
+ Import the shared stylesheet once per page:
25
+
26
+ ```ts
27
+ import '@bunnyland/ui-web/assets/bunnyland-ui.css';
28
+ ```
29
+
30
+ Colors, spacing, typography, radii, overlays, and semantic states derive from `--bl-*`
31
+ custom properties. Application CSS may compose these tokens but must not hard-code a
32
+ parallel palette.
33
+
34
+ The six built-in palettes each provide dark and light tokens. They follow
35
+ `prefers-color-scheme` unless the user forces an appearance in the client menu. Deployment
36
+ themes register a validated option and provide a matching `:root.bl-theme-<value>` token
37
+ block. Palette choice is stored under `bunnyland.theme`; an Auto, Dark, or Light override is
38
+ stored under `bunnyland.color-scheme`. Both are reflected on the root element and broadcast
39
+ with the `bunnyland:themechange` event.
40
+
41
+ Full-screen applications use a flex column rooted at `body > #app`. The body uses the
42
+ dynamic viewport height, with a `100vh` fallback, while the application work surface and
43
+ nested panes use `min-width: 0` and `min-height: 0`. The document stays fixed; pane bodies
44
+ or an explicit stacked-pane work surface own scrolling. Document-style pages such as a
45
+ welcome screen may opt back into normal page scrolling.
46
+
47
+ At phone widths, toolbars wrap, fixed-width controls stay within the viewport, and
48
+ multi-pane work surfaces switch to a single-column grid or vertical flex stack. Keep every
49
+ workflow pane reachable unless product requirements explicitly make it desktop-only.
50
+ Prefer CSS Grid, Flexbox, `minmax()`, and `clamp()` over viewport-aware component state.
51
+
52
+ ## Shared Preact elements
53
+
54
+ Import Preact components from the dedicated entry point:
55
+
56
+ ```tsx
57
+ import {
58
+ Button,
59
+ EmptyState,
60
+ Field,
61
+ Pane,
62
+ Pill,
63
+ SearchSelect,
64
+ StatusText,
65
+ TagEditor,
66
+ ThemeSelect,
67
+ Toolbar,
68
+ ToolbarBrand,
69
+ ToolbarRow,
70
+ } from '@bunnyland/ui-web/preact';
71
+ ```
72
+
73
+ Applications import directly from `preact` and `preact/hooks`; do not add React or
74
+ `preact/compat`. `preact` is a peer dependency, which keeps one runtime in the consuming
75
+ Vite application. Standalone consumers install a published package version or the CI-built
76
+ `artifacts/bunnyland-ui-web-<version>.tgz` rather than referencing a sibling checkout.
77
+
78
+ - `Toolbar`, `ToolbarRow`, and `ToolbarBrand` establish consistent application chrome.
79
+ - `Pane` owns a titled scroll/layout region and an optional tools slot.
80
+ - `Field` connects a visible label, control, and optional description.
81
+ - `Button` provides semantic `primary`, `secondary`, `danger`, and `quiet` variants.
82
+ - `StatusText`, `Pill`, and `EmptyState` express small, stable display states.
83
+ - `SearchSelect` and `TagEditor` are controlled components; their parent owns committed
84
+ values while each component keeps only its transient input draft.
85
+ - `ThemeSelect` and `useTheme` share the same imperative theme contract used by legacy
86
+ pages during migration.
87
+
88
+ ## Component and state boundaries
89
+
90
+ - Key entity, component, edge, action, event, and queue rows with durable domain IDs.
91
+ - Never key mutable lists by their display position when an ID exists.
92
+ - Preserve object identity for unchanged records. Update nested data with structural
93
+ sharing so child components can avoid work.
94
+ - Keep live transport state, filters, selected IDs, field drafts, and large projections in
95
+ separate state owners.
96
+ - Do not place an entire world snapshot in a context consumed by every component.
97
+ - Effects that open timers, sockets, observers, or renderer instances must return cleanup.
98
+ - Use refs for mutable renderer instances and DOM focus/selection, not as a second state
99
+ store.
100
+
101
+ ## Interaction states
102
+
103
+ Every asynchronous operation exposes an idle, working, success, and failure state without
104
+ moving the primary controls. Disabled controls remain visible and explain their prerequisite
105
+ through adjacent text or a title. Destructive actions use the danger treatment and require
106
+ confirmation when recovery is not immediate.
107
+
108
+ Filtering must preserve input focus and selection. Live refreshes must preserve unchanged
109
+ row nodes, expanded details, scroll position, and active form drafts.
110
+
111
+ ## Verification
112
+
113
+ For each migrated surface, browser tests should assert behavior through roles, labels, and
114
+ stable test IDs. Performance regressions should additionally verify that representative
115
+ unchanged rows remain the same DOM nodes after a live update and that focused inputs are not
116
+ replaced while typing.
package/package.json ADDED
@@ -0,0 +1,101 @@
1
+ {
2
+ "name": "@bunnyland/ui-web",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "Shared web UI helpers, themes, and widgets for Bunnyland browser clients.",
6
+ "license": "AGPL-3.0-or-later",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/thalismind/bunnyland-ui-web.git"
10
+ },
11
+ "homepage": "https://github.com/thalismind/bunnyland-ui-web#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/thalismind/bunnyland-ui-web/issues"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public",
17
+ "provenance": true,
18
+ "registry": "https://registry.npmjs.org/"
19
+ },
20
+ "sideEffects": [
21
+ "./assets/*.css",
22
+ "./assets/*.js"
23
+ ],
24
+ "main": "./dist/index.js",
25
+ "module": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "files": [
28
+ "assets",
29
+ "docs",
30
+ "dist",
31
+ "src"
32
+ ],
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "./api": {
39
+ "types": "./dist/api.d.ts",
40
+ "default": "./dist/api.js"
41
+ },
42
+ "./play": {
43
+ "types": "./dist/play.d.ts",
44
+ "default": "./dist/play.js"
45
+ },
46
+ "./theme": {
47
+ "types": "./dist/theme.d.ts",
48
+ "default": "./dist/theme.js"
49
+ },
50
+ "./widgets": {
51
+ "types": "./dist/widgets.d.ts",
52
+ "default": "./dist/widgets.js"
53
+ },
54
+ "./player-widgets": {
55
+ "types": "./dist/player-widgets.d.ts",
56
+ "default": "./dist/player-widgets.js"
57
+ },
58
+ "./admin-widgets": {
59
+ "types": "./dist/admin-widgets.d.ts",
60
+ "default": "./dist/admin-widgets.js"
61
+ },
62
+ "./preact": {
63
+ "types": "./dist/preact.d.ts",
64
+ "default": "./dist/preact.js"
65
+ },
66
+ "./assets/bunnyland-ui.css": "./assets/bunnyland-ui.css",
67
+ "./assets/bunnyland-ui.js": "./assets/bunnyland-ui.js",
68
+ "./assets/bunnyland-api.js": "./assets/bunnyland-api.js",
69
+ "./assets/bunnyland-play.js": "./assets/bunnyland-play.js"
70
+ },
71
+ "scripts": {
72
+ "build": "tsc -p tsconfig.json && vite build",
73
+ "lint": "eslint src test storybook/preact-story.tsx scripts vite.config.ts vite.storybook.config.ts vitest.config.ts",
74
+ "test": "node --test && vitest run",
75
+ "test:coverage": "node --test --experimental-test-coverage",
76
+ "test:components": "vitest run",
77
+ "check": "npm run lint && npm run build && npm run test:coverage && npm run test:components",
78
+ "check:release-tag": "node scripts/check-release-tag.mjs",
79
+ "prepack": "npm run build",
80
+ "pack:artifact": "node scripts/package-artifact.mjs",
81
+ "storybook:bundle": "vite build --config vite.storybook.config.ts",
82
+ "storybook": "npm run build && npm run storybook:bundle && node scripts/build-storybook.mjs"
83
+ },
84
+ "peerDependencies": {
85
+ "preact": "^10.29.0"
86
+ },
87
+ "devDependencies": {
88
+ "@eslint/js": "^10.0.1",
89
+ "@preact/preset-vite": "^2.10.0",
90
+ "@testing-library/preact": "^3.2.0",
91
+ "eslint": "^10.5.0",
92
+ "globals": "^17.6.0",
93
+ "jsdom": "^29.1.1",
94
+ "playwright": "^1.61.1",
95
+ "preact": "^10.29.0",
96
+ "typescript": "^5.6.0",
97
+ "typescript-eslint": "^8.49.0",
98
+ "vite": "^8.1.0",
99
+ "vitest": "^4.0.0"
100
+ }
101
+ }