@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.
- package/LICENSE +661 -0
- package/README.md +211 -0
- package/assets/bunnyland-api.js +345 -0
- package/assets/bunnyland-play.js +1223 -0
- package/assets/bunnyland-ui.css +1634 -0
- package/assets/bunnyland-ui.js +795 -0
- package/dist/admin-widgets.d.ts +31 -0
- package/dist/admin-widgets.d.ts.map +1 -0
- package/dist/admin-widgets.js +100 -0
- package/dist/admin-widgets.js.map +1 -0
- package/dist/api.d.ts +37 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +125 -0
- package/dist/api.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/play.d.ts +265 -0
- package/dist/play.d.ts.map +1 -0
- package/dist/play.js +806 -0
- package/dist/play.js.map +1 -0
- package/dist/player-widgets.d.ts +11 -0
- package/dist/player-widgets.d.ts.map +1 -0
- package/dist/player-widgets.js +21 -0
- package/dist/player-widgets.js.map +1 -0
- package/dist/preact/auth.d.ts +37 -0
- package/dist/preact/auth.d.ts.map +1 -0
- package/dist/preact/components.d.ts +58 -0
- package/dist/preact/components.d.ts.map +1 -0
- package/dist/preact/theme.d.ts +16 -0
- package/dist/preact/theme.d.ts.map +1 -0
- package/dist/preact.d.ts +4 -0
- package/dist/preact.d.ts.map +1 -0
- package/dist/preact.js +488 -0
- package/dist/preact.js.map +1 -0
- package/dist/theme.d.ts +31 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +165 -0
- package/dist/theme.js.map +1 -0
- package/dist/widgets.d.ts +7 -0
- package/dist/widgets.d.ts.map +1 -0
- package/dist/widgets.js +31 -0
- package/dist/widgets.js.map +1 -0
- package/docs/admin/custom-web-themes.md +112 -0
- package/docs/developer/design-language.md +116 -0
- package/package.json +101 -0
- package/src/admin-widgets.ts +189 -0
- package/src/api.ts +193 -0
- package/src/index.ts +6 -0
- package/src/play.ts +1151 -0
- package/src/player-widgets.ts +29 -0
- package/src/preact/auth.tsx +338 -0
- package/src/preact/components.tsx +249 -0
- package/src/preact/theme.tsx +72 -0
- package/src/preact.ts +3 -0
- package/src/theme.ts +225 -0
- 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"}
|
package/dist/widgets.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/widgets.ts
|
|
2
|
+
function e(e) {
|
|
3
|
+
return String(e).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\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
|
+
}
|