@hanzo/mod 0.1.0 → 0.1.2
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/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/platform.d.ts +2 -0
- package/dist/platform.js +47 -3
- package/dist/theme.d.ts +8 -0
- package/dist/theme.js +190 -9
- package/package.json +4 -1
- package/src/font.ts +0 -69
- package/src/index.ts +0 -27
- package/src/mod.test.ts +0 -41
- package/src/platform.test.ts +0 -13
- package/src/platform.ts +0 -146
- package/src/theme.ts +0 -193
- package/tsconfig.build.json +0 -11
- package/tsconfig.json +0 -20
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { detectPlatform, getActivePlatform, setActivePlatform, applyPlatformTokens, PLATFORM_PROFILES, type PlatformId, type PlatformProfile, } from './platform.js';
|
|
2
|
-
export { exportMod, importMod, forkMod, applyMod, saveOrgMod, getOrgMod, BUILTIN_MODS, type ModTheme, } from './theme.js';
|
|
1
|
+
export { detectPlatform, getActivePlatform, setActivePlatform, applyPlatformTokens, clearPlatformTokens, PLATFORM_PROFILES, type PlatformId, type PlatformProfile, } from './platform.js';
|
|
2
|
+
export { exportMod, importMod, forkMod, applyMod, restoreActiveMod, clearActiveMod, saveOrgMod, getOrgMod, BUILTIN_MODS, type ModTheme, } from './theme.js';
|
|
3
3
|
export { injectCustomFont, removeCustomFont, applyFontFamily, type CustomFontOptions, } from './font.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { detectPlatform, getActivePlatform, setActivePlatform, applyPlatformTokens, PLATFORM_PROFILES, } from './platform.js';
|
|
2
|
-
export { exportMod, importMod, forkMod, applyMod, saveOrgMod, getOrgMod, BUILTIN_MODS, } from './theme.js';
|
|
1
|
+
export { detectPlatform, getActivePlatform, setActivePlatform, applyPlatformTokens, clearPlatformTokens, PLATFORM_PROFILES, } from './platform.js';
|
|
2
|
+
export { exportMod, importMod, forkMod, applyMod, restoreActiveMod, clearActiveMod, saveOrgMod, getOrgMod, BUILTIN_MODS, } from './theme.js';
|
|
3
3
|
export { injectCustomFont, removeCustomFont, applyFontFamily, } from './font.js';
|
package/dist/platform.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export declare const PLATFORM_PROFILES: Record<PlatformId, PlatformProfile>;
|
|
|
26
26
|
export declare function detectPlatform(userAgent?: string): PlatformId;
|
|
27
27
|
/** Reads the user's active platform preference (stored or detected) */
|
|
28
28
|
export declare function getActivePlatform(): PlatformId;
|
|
29
|
+
/** Clears inline platform token overrides from documentElement */
|
|
30
|
+
export declare function clearPlatformTokens(): void;
|
|
29
31
|
/** Sets the user's preferred platform customization */
|
|
30
32
|
export declare function setActivePlatform(platform: PlatformId | 'auto'): void;
|
|
31
33
|
/** Statically stamps the platform CSS custom properties onto documentElement */
|
package/dist/platform.js
CHANGED
|
@@ -101,6 +101,30 @@ export function getActivePlatform() {
|
|
|
101
101
|
}
|
|
102
102
|
return detectPlatform();
|
|
103
103
|
}
|
|
104
|
+
const PLATFORM_CSS_VARS = [
|
|
105
|
+
'--platform-font-sans',
|
|
106
|
+
'--platform-font-mono',
|
|
107
|
+
'--platform-radius-card',
|
|
108
|
+
'--platform-radius-pill',
|
|
109
|
+
'--glass-blur',
|
|
110
|
+
'--glass-saturate',
|
|
111
|
+
'--font-sans',
|
|
112
|
+
'--font-display',
|
|
113
|
+
'--font-mono',
|
|
114
|
+
'--pane-round',
|
|
115
|
+
'--radius-scale',
|
|
116
|
+
'--chrome-blur',
|
|
117
|
+
'--pane-blur',
|
|
118
|
+
];
|
|
119
|
+
/** Clears inline platform token overrides from documentElement */
|
|
120
|
+
export function clearPlatformTokens() {
|
|
121
|
+
if (typeof document === 'undefined')
|
|
122
|
+
return;
|
|
123
|
+
const root = document.documentElement;
|
|
124
|
+
for (const prop of PLATFORM_CSS_VARS) {
|
|
125
|
+
root.style.removeProperty(prop);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
104
128
|
/** Sets the user's preferred platform customization */
|
|
105
129
|
export function setActivePlatform(platform) {
|
|
106
130
|
if (typeof window === 'undefined')
|
|
@@ -116,7 +140,15 @@ export function setActivePlatform(platform) {
|
|
|
116
140
|
catch {
|
|
117
141
|
// ignore storage restrictions
|
|
118
142
|
}
|
|
119
|
-
|
|
143
|
+
if (platform === 'auto') {
|
|
144
|
+
clearPlatformTokens();
|
|
145
|
+
if (typeof document !== 'undefined') {
|
|
146
|
+
document.documentElement.setAttribute('data-platform', detectPlatform());
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
applyPlatformTokens(platform);
|
|
151
|
+
}
|
|
120
152
|
window.dispatchEvent(new CustomEvent('hanzo:mod:change', { detail: { platform } }));
|
|
121
153
|
}
|
|
122
154
|
/** Statically stamps the platform CSS custom properties onto documentElement */
|
|
@@ -125,11 +157,23 @@ export function applyPlatformTokens(platform) {
|
|
|
125
157
|
return;
|
|
126
158
|
const profile = PLATFORM_PROFILES[platform] ?? PLATFORM_PROFILES.ios;
|
|
127
159
|
const root = document.documentElement;
|
|
160
|
+
const radiusCardPx = typeof profile.radiusCard === 'number' ? `${profile.radiusCard}px` : profile.radiusCard;
|
|
161
|
+
const radiusPillPx = typeof profile.radiusPill === 'number' ? `${profile.radiusPill}px` : profile.radiusPill;
|
|
162
|
+
const radiusScale = typeof profile.radiusCard === 'number'
|
|
163
|
+
? String(Math.round((profile.radiusCard / 12) * 100) / 100)
|
|
164
|
+
: '1';
|
|
128
165
|
root.setAttribute('data-platform', profile.id);
|
|
129
166
|
root.style.setProperty('--platform-font-sans', profile.fontFamily);
|
|
130
167
|
root.style.setProperty('--platform-font-mono', profile.fontMono);
|
|
131
|
-
root.style.setProperty('--platform-radius-card',
|
|
132
|
-
root.style.setProperty('--platform-radius-pill',
|
|
168
|
+
root.style.setProperty('--platform-radius-card', radiusCardPx);
|
|
169
|
+
root.style.setProperty('--platform-radius-pill', radiusPillPx);
|
|
133
170
|
root.style.setProperty('--glass-blur', `${profile.glassBlur}px`);
|
|
134
171
|
root.style.setProperty('--glass-saturate', `${profile.glassSaturate}%`);
|
|
172
|
+
root.style.setProperty('--font-sans', `var(--font-arabic), ${profile.fontFamily}`);
|
|
173
|
+
root.style.setProperty('--font-display', `var(--font-arabic), ${profile.fontFamily}`);
|
|
174
|
+
root.style.setProperty('--font-mono', `var(--font-arabic), ${profile.fontMono}`);
|
|
175
|
+
root.style.setProperty('--pane-round', radiusCardPx);
|
|
176
|
+
root.style.setProperty('--radius-scale', radiusScale);
|
|
177
|
+
root.style.setProperty('--chrome-blur', `${profile.glassBlur}px`);
|
|
178
|
+
root.style.setProperty('--pane-blur', `${Math.round(profile.glassBlur * 1.5)}px`);
|
|
135
179
|
}
|
package/dist/theme.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ export interface ModTheme {
|
|
|
6
6
|
author?: string;
|
|
7
7
|
org?: string;
|
|
8
8
|
platform?: PlatformId;
|
|
9
|
+
theme?: 'light' | 'dark' | 'system';
|
|
10
|
+
background?: string;
|
|
11
|
+
surface?: string;
|
|
12
|
+
foreground?: string;
|
|
9
13
|
fontHeading?: string;
|
|
10
14
|
fontBody?: string;
|
|
11
15
|
fontMono?: string;
|
|
@@ -27,6 +31,10 @@ export declare function importMod(json: string): ModTheme;
|
|
|
27
31
|
export declare function forkMod(base: ModTheme, overrides: Partial<ModTheme>): ModTheme;
|
|
28
32
|
/** Applies a theme mod dynamically to the page */
|
|
29
33
|
export declare function applyMod(mod: ModTheme): void;
|
|
34
|
+
/** Restores and applies the persisted active mod (or org mod) if present */
|
|
35
|
+
export declare function restoreActiveMod(orgId?: string): ModTheme | null;
|
|
36
|
+
/** Clears the active mod from localStorage and removes inline mod overrides */
|
|
37
|
+
export declare function clearActiveMod(): void;
|
|
30
38
|
/** Saves an organization-scoped mod configuration */
|
|
31
39
|
export declare function saveOrgMod(orgId: string, mod: ModTheme): void;
|
|
32
40
|
/** Gets the active mod for an organization */
|
package/dist/theme.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PLATFORM_PROFILES, applyPlatformTokens } from './platform.js';
|
|
1
|
+
import { PLATFORM_PROFILES, applyPlatformTokens, clearPlatformTokens } from './platform.js';
|
|
2
2
|
export const BUILTIN_MODS = [
|
|
3
3
|
{
|
|
4
4
|
id: 'ios-liquid',
|
|
@@ -6,6 +6,8 @@ export const BUILTIN_MODS = [
|
|
|
6
6
|
description: 'Continuous squircles, high-specular liquid glass, and Apple SF Pro typography.',
|
|
7
7
|
author: 'Hanzo Design',
|
|
8
8
|
platform: 'ios',
|
|
9
|
+
theme: 'dark',
|
|
10
|
+
background: '#000000',
|
|
9
11
|
glassBlur: 24,
|
|
10
12
|
radius: 22,
|
|
11
13
|
density: 'comfortable',
|
|
@@ -16,6 +18,8 @@ export const BUILTIN_MODS = [
|
|
|
16
18
|
description: 'Google Material 3 tonal elevations, Roboto Sans, and smooth dynamic rounded cards.',
|
|
17
19
|
author: 'Hanzo Design',
|
|
18
20
|
platform: 'android',
|
|
21
|
+
theme: 'dark',
|
|
22
|
+
background: '#0b0e14',
|
|
19
23
|
glassBlur: 16,
|
|
20
24
|
radius: 16,
|
|
21
25
|
density: 'default',
|
|
@@ -26,6 +30,8 @@ export const BUILTIN_MODS = [
|
|
|
26
30
|
description: 'Translucent sidebars, vibrant visual effect filters, and clean desktop density.',
|
|
27
31
|
author: 'Hanzo Design',
|
|
28
32
|
platform: 'macos',
|
|
33
|
+
theme: 'dark',
|
|
34
|
+
background: '#09090b',
|
|
29
35
|
glassBlur: 20,
|
|
30
36
|
radius: 12,
|
|
31
37
|
density: 'compact',
|
|
@@ -36,6 +42,8 @@ export const BUILTIN_MODS = [
|
|
|
36
42
|
description: 'Microsoft Fluent Design Mica acrylic and Segoe UI Variable geometry.',
|
|
37
43
|
author: 'Hanzo Design',
|
|
38
44
|
platform: 'windows',
|
|
45
|
+
theme: 'dark',
|
|
46
|
+
background: '#0a0d12',
|
|
39
47
|
glassBlur: 18,
|
|
40
48
|
radius: 8,
|
|
41
49
|
density: 'default',
|
|
@@ -46,6 +54,8 @@ export const BUILTIN_MODS = [
|
|
|
46
54
|
description: 'High contrast deep OLED black with electric cyan and magenta accents.',
|
|
47
55
|
author: 'Hanzo Labs',
|
|
48
56
|
platform: 'ios',
|
|
57
|
+
theme: 'dark',
|
|
58
|
+
background: '#000000',
|
|
49
59
|
primary: '#00f0ff',
|
|
50
60
|
accent: '#ff007f',
|
|
51
61
|
glassBlur: 28,
|
|
@@ -58,12 +68,28 @@ export const BUILTIN_MODS = [
|
|
|
58
68
|
description: 'Pure arctic monochrome glass, high clarity, and minimalist typography.',
|
|
59
69
|
author: 'Hanzo Design',
|
|
60
70
|
platform: 'macos',
|
|
71
|
+
theme: 'dark',
|
|
72
|
+
background: '#090d16',
|
|
61
73
|
primary: '#e2e8f0',
|
|
62
74
|
accent: '#38bdf8',
|
|
63
75
|
glassBlur: 22,
|
|
64
76
|
radius: 14,
|
|
65
77
|
density: 'default',
|
|
66
78
|
},
|
|
79
|
+
{
|
|
80
|
+
id: 'studio-light',
|
|
81
|
+
name: 'Studio Paper (Light)',
|
|
82
|
+
description: 'Warm editorial paper ground, crisp ink typography, and subtle daylight glass.',
|
|
83
|
+
author: 'Hanzo Design',
|
|
84
|
+
platform: 'macos',
|
|
85
|
+
theme: 'light',
|
|
86
|
+
background: '#faf8f5',
|
|
87
|
+
primary: '#18181b',
|
|
88
|
+
accent: '#2563eb',
|
|
89
|
+
glassBlur: 16,
|
|
90
|
+
radius: 12,
|
|
91
|
+
density: 'default',
|
|
92
|
+
},
|
|
67
93
|
];
|
|
68
94
|
const ORG_MOD_KEY = (orgId) => `hanzo.mod.org.${orgId}`;
|
|
69
95
|
const ACTIVE_MOD_KEY = 'hanzo.mod.active';
|
|
@@ -84,6 +110,10 @@ export function importMod(json) {
|
|
|
84
110
|
author: parsed.author ? String(parsed.author).slice(0, 64) : undefined,
|
|
85
111
|
org: parsed.org,
|
|
86
112
|
platform: parsed.platform && parsed.platform in PLATFORM_PROFILES ? parsed.platform : undefined,
|
|
113
|
+
theme: parsed.theme === 'light' || parsed.theme === 'dark' || parsed.theme === 'system' ? parsed.theme : undefined,
|
|
114
|
+
background: parsed.background,
|
|
115
|
+
surface: parsed.surface,
|
|
116
|
+
foreground: parsed.foreground,
|
|
87
117
|
fontHeading: parsed.fontHeading,
|
|
88
118
|
fontBody: parsed.fontBody,
|
|
89
119
|
fontMono: parsed.fontMono,
|
|
@@ -106,28 +136,137 @@ export function forkMod(base, overrides) {
|
|
|
106
136
|
name: overrides.name || `${base.name} (Mod)`,
|
|
107
137
|
};
|
|
108
138
|
}
|
|
139
|
+
function modGroundInk(c) {
|
|
140
|
+
const m = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.exec(c.trim());
|
|
141
|
+
if (!m)
|
|
142
|
+
return '#fafafa';
|
|
143
|
+
const h = m[1].length === 3 ? m[1].replace(/./g, '$&$&') : m[1].slice(0, 6);
|
|
144
|
+
const [r, g, b] = [0, 2, 4].map((i) => {
|
|
145
|
+
const v = parseInt(h.slice(i, i + 2), 16) / 255;
|
|
146
|
+
return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
|
147
|
+
});
|
|
148
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.179 ? '#09090b' : '#fafafa';
|
|
149
|
+
}
|
|
150
|
+
const MOD_CSS_VARS = [
|
|
151
|
+
'--background',
|
|
152
|
+
'--foreground',
|
|
153
|
+
'--text-primary',
|
|
154
|
+
'--card',
|
|
155
|
+
'--surface-card',
|
|
156
|
+
'--card-foreground',
|
|
157
|
+
'--popover',
|
|
158
|
+
'--popover-foreground',
|
|
159
|
+
'--muted',
|
|
160
|
+
'--secondary',
|
|
161
|
+
'--secondary-hover',
|
|
162
|
+
'--secondary-foreground',
|
|
163
|
+
'--pane-floor',
|
|
164
|
+
'--pane-fill',
|
|
165
|
+
'--composer-fill',
|
|
166
|
+
'--composer-ground',
|
|
167
|
+
'--primary',
|
|
168
|
+
'--primary-foreground',
|
|
169
|
+
'--primary-hover',
|
|
170
|
+
'--accent',
|
|
171
|
+
'--accent-foreground',
|
|
172
|
+
'--accent-hover',
|
|
173
|
+
'--ring',
|
|
174
|
+
'--liquid-glass-bg',
|
|
175
|
+
'--glass-blur',
|
|
176
|
+
'--chrome-blur',
|
|
177
|
+
'--pane-blur',
|
|
178
|
+
'--radius',
|
|
179
|
+
'--pane-round',
|
|
180
|
+
'--radius-scale',
|
|
181
|
+
'--density',
|
|
182
|
+
'--font-heading',
|
|
183
|
+
'--font-display',
|
|
184
|
+
'--font-body',
|
|
185
|
+
'--font-sans',
|
|
186
|
+
'--font-mono',
|
|
187
|
+
];
|
|
109
188
|
/** Applies a theme mod dynamically to the page */
|
|
110
189
|
export function applyMod(mod) {
|
|
111
190
|
if (typeof document === 'undefined')
|
|
112
191
|
return;
|
|
113
192
|
const root = document.documentElement;
|
|
193
|
+
for (const prop of MOD_CSS_VARS) {
|
|
194
|
+
root.style.removeProperty(prop);
|
|
195
|
+
}
|
|
114
196
|
if (mod.platform) {
|
|
115
197
|
applyPlatformTokens(mod.platform);
|
|
116
198
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
|
|
199
|
+
const effectiveTheme = mod.theme ??
|
|
200
|
+
(mod.background ? (modGroundInk(mod.background) === '#09090b' ? 'light' : 'dark') : undefined);
|
|
201
|
+
if (effectiveTheme) {
|
|
202
|
+
const night = effectiveTheme === 'dark' ||
|
|
203
|
+
(effectiveTheme === 'system' && typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches);
|
|
204
|
+
root.classList.toggle('light', !night);
|
|
205
|
+
root.classList.toggle('dark', night);
|
|
206
|
+
root.classList.toggle('t_light', !night);
|
|
207
|
+
root.classList.toggle('t_dark', night);
|
|
208
|
+
root.style.colorScheme = night ? 'dark' : 'light';
|
|
209
|
+
root.setAttribute('data-scheme', effectiveTheme);
|
|
210
|
+
}
|
|
211
|
+
if (mod.background) {
|
|
212
|
+
const bg = mod.background.trim();
|
|
213
|
+
const fg = mod.foreground?.trim() || modGroundInk(bg);
|
|
214
|
+
const isLight = fg === '#09090b';
|
|
215
|
+
const mixTarget = isLight ? 'black' : 'white';
|
|
216
|
+
const surface = mod.surface?.trim() || `color-mix(in oklch, ${bg} 95%, ${mixTarget})`;
|
|
217
|
+
root.style.setProperty('--background', bg);
|
|
218
|
+
root.style.setProperty('--foreground', fg);
|
|
219
|
+
root.style.setProperty('--text-primary', fg);
|
|
220
|
+
root.style.setProperty('--card', surface);
|
|
221
|
+
root.style.setProperty('--surface-card', surface);
|
|
222
|
+
root.style.setProperty('--card-foreground', fg);
|
|
223
|
+
root.style.setProperty('--popover', `color-mix(in oklch, ${bg} 92%, ${mixTarget})`);
|
|
224
|
+
root.style.setProperty('--popover-foreground', fg);
|
|
225
|
+
root.style.setProperty('--muted', `color-mix(in oklch, ${bg} 92%, ${mixTarget})`);
|
|
226
|
+
root.style.setProperty('--secondary', `color-mix(in oklch, ${bg} 90%, ${mixTarget})`);
|
|
227
|
+
root.style.setProperty('--secondary-hover', `color-mix(in oklch, ${bg} 84%, ${mixTarget})`);
|
|
228
|
+
root.style.setProperty('--secondary-foreground', fg);
|
|
229
|
+
root.style.setProperty('--pane-floor', `color-mix(in oklch, ${bg} 95%, black)`);
|
|
230
|
+
root.style.setProperty('--pane-fill', `color-mix(in oklch, ${bg} 97%, ${mixTarget})`);
|
|
231
|
+
root.style.setProperty('--composer-fill', `color-mix(in oklch, ${bg} 90%, ${mixTarget})`);
|
|
232
|
+
root.style.setProperty('--composer-ground', `color-mix(in oklch, ${bg} 90%, ${mixTarget})`);
|
|
233
|
+
}
|
|
234
|
+
const accent = mod.accent || mod.primary;
|
|
235
|
+
if (accent) {
|
|
236
|
+
const fg = modGroundInk(accent);
|
|
237
|
+
const hover = `color-mix(in oklch, ${accent} 88%, white)`;
|
|
238
|
+
root.style.setProperty('--primary', mod.primary || accent);
|
|
239
|
+
root.style.setProperty('--primary-foreground', fg);
|
|
240
|
+
root.style.setProperty('--primary-hover', hover);
|
|
241
|
+
root.style.setProperty('--accent', accent);
|
|
242
|
+
root.style.setProperty('--accent-foreground', fg);
|
|
243
|
+
root.style.setProperty('--accent-hover', hover);
|
|
244
|
+
root.style.setProperty('--ring', accent);
|
|
245
|
+
}
|
|
121
246
|
if (mod.glassTint)
|
|
122
247
|
root.style.setProperty('--liquid-glass-bg', mod.glassTint);
|
|
123
|
-
if (typeof mod.glassBlur === 'number')
|
|
248
|
+
if (typeof mod.glassBlur === 'number') {
|
|
124
249
|
root.style.setProperty('--glass-blur', `${mod.glassBlur}px`);
|
|
125
|
-
|
|
250
|
+
root.style.setProperty('--chrome-blur', `${mod.glassBlur}px`);
|
|
251
|
+
root.style.setProperty('--pane-blur', `${Math.round(mod.glassBlur * 1.5)}px`);
|
|
252
|
+
}
|
|
253
|
+
if (typeof mod.radius === 'number') {
|
|
126
254
|
root.style.setProperty('--radius', `${mod.radius}px`);
|
|
127
|
-
|
|
255
|
+
root.style.setProperty('--pane-round', `${mod.radius}px`);
|
|
256
|
+
root.style.setProperty('--radius-scale', String(Math.round((mod.radius / 12) * 100) / 100));
|
|
257
|
+
}
|
|
258
|
+
if (mod.density) {
|
|
259
|
+
const d = mod.density === 'compact' ? '0.85' : mod.density === 'comfortable' ? '1.15' : '1';
|
|
260
|
+
root.style.setProperty('--density', d);
|
|
261
|
+
}
|
|
262
|
+
if (mod.fontHeading) {
|
|
128
263
|
root.style.setProperty('--font-heading', mod.fontHeading);
|
|
129
|
-
|
|
264
|
+
root.style.setProperty('--font-display', mod.fontHeading);
|
|
265
|
+
}
|
|
266
|
+
if (mod.fontBody) {
|
|
130
267
|
root.style.setProperty('--font-body', mod.fontBody);
|
|
268
|
+
root.style.setProperty('--font-sans', mod.fontBody);
|
|
269
|
+
}
|
|
131
270
|
if (mod.fontMono)
|
|
132
271
|
root.style.setProperty('--font-mono', mod.fontMono);
|
|
133
272
|
// Inject custom CSS if defined
|
|
@@ -151,6 +290,48 @@ export function applyMod(mod) {
|
|
|
151
290
|
// ignore
|
|
152
291
|
}
|
|
153
292
|
}
|
|
293
|
+
/** Restores and applies the persisted active mod (or org mod) if present */
|
|
294
|
+
export function restoreActiveMod(orgId) {
|
|
295
|
+
if (typeof window === 'undefined')
|
|
296
|
+
return null;
|
|
297
|
+
try {
|
|
298
|
+
const raw = (orgId ? window.localStorage.getItem(ORG_MOD_KEY(orgId)) : null) ||
|
|
299
|
+
window.localStorage.getItem(ACTIVE_MOD_KEY);
|
|
300
|
+
if (!raw)
|
|
301
|
+
return null;
|
|
302
|
+
const mod = JSON.parse(raw);
|
|
303
|
+
if (mod && typeof mod === 'object' && mod.name) {
|
|
304
|
+
applyMod(mod);
|
|
305
|
+
return mod;
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/** Clears the active mod from localStorage and removes inline mod overrides */
|
|
314
|
+
export function clearActiveMod() {
|
|
315
|
+
if (typeof window === 'undefined')
|
|
316
|
+
return;
|
|
317
|
+
try {
|
|
318
|
+
window.localStorage.removeItem(ACTIVE_MOD_KEY);
|
|
319
|
+
const styleEl = document.getElementById('hanzo-custom-mod-css');
|
|
320
|
+
if (styleEl)
|
|
321
|
+
styleEl.remove();
|
|
322
|
+
if (typeof document !== 'undefined') {
|
|
323
|
+
const root = document.documentElement;
|
|
324
|
+
for (const prop of MOD_CSS_VARS) {
|
|
325
|
+
root.style.removeProperty(prop);
|
|
326
|
+
}
|
|
327
|
+
clearPlatformTokens();
|
|
328
|
+
}
|
|
329
|
+
window.dispatchEvent(new CustomEvent('hanzo:mod:cleared'));
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
// ignore
|
|
333
|
+
}
|
|
334
|
+
}
|
|
154
335
|
/** Saves an organization-scoped mod configuration */
|
|
155
336
|
export function saveOrgMod(orgId, mod) {
|
|
156
337
|
if (typeof window === 'undefined')
|
package/package.json
CHANGED
package/src/font.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dynamic custom font loader and manager for organizations and designers.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export interface CustomFontOptions {
|
|
6
|
-
family: string
|
|
7
|
-
url: string
|
|
8
|
-
format?: 'woff2' | 'woff' | 'truetype' | 'opentype'
|
|
9
|
-
weights?: (number | string)[]
|
|
10
|
-
isStylesheet?: boolean
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const INJECTED_FONT_PREFIX = 'hanzo-font-'
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Injects a designer/organization custom font into document head dynamically.
|
|
17
|
-
* Supports direct webfont stylesheets (Google Fonts, Adobe Fonts, Typekit) or @font-face binaries.
|
|
18
|
-
*/
|
|
19
|
-
export function injectCustomFont(options: CustomFontOptions): void {
|
|
20
|
-
if (typeof document === 'undefined') return
|
|
21
|
-
|
|
22
|
-
const elementId = `${INJECTED_FONT_PREFIX}${options.family.toLowerCase().replace(/[^a-z0-9]/g, '-')}`
|
|
23
|
-
const existing = document.getElementById(elementId)
|
|
24
|
-
if (existing) existing.remove()
|
|
25
|
-
|
|
26
|
-
if (options.isStylesheet || options.url.endsWith('.css') || options.url.includes('fonts.googleapis.com')) {
|
|
27
|
-
const link = document.createElement('link')
|
|
28
|
-
link.id = elementId
|
|
29
|
-
link.rel = 'stylesheet'
|
|
30
|
-
link.href = options.url
|
|
31
|
-
document.head.appendChild(link)
|
|
32
|
-
} else {
|
|
33
|
-
const format = options.format || (options.url.endsWith('.woff2') ? 'woff2' : options.url.endsWith('.woff') ? 'woff' : 'truetype')
|
|
34
|
-
const weights = options.weights || [400, 500, 600, 700]
|
|
35
|
-
|
|
36
|
-
let fontFaceRules = ''
|
|
37
|
-
for (const w of weights) {
|
|
38
|
-
fontFaceRules += `
|
|
39
|
-
@font-face {
|
|
40
|
-
font-family: "${options.family}";
|
|
41
|
-
src: url("${options.url}") format("${format}");
|
|
42
|
-
font-weight: ${w};
|
|
43
|
-
font-style: normal;
|
|
44
|
-
font-display: swap;
|
|
45
|
-
}
|
|
46
|
-
`
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const style = document.createElement('style')
|
|
50
|
-
style.id = elementId
|
|
51
|
-
style.textContent = fontFaceRules
|
|
52
|
-
document.head.appendChild(style)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Removes an injected custom font stylesheet or style tag */
|
|
57
|
-
export function removeCustomFont(family: string): void {
|
|
58
|
-
if (typeof document === 'undefined') return
|
|
59
|
-
const elementId = `${INJECTED_FONT_PREFIX}${family.toLowerCase().replace(/[^a-z0-9]/g, '-')}`
|
|
60
|
-
const el = document.getElementById(elementId)
|
|
61
|
-
if (el) el.remove()
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Statically applies a font-family to CSS variables */
|
|
65
|
-
export function applyFontFamily(target: 'heading' | 'sans' | 'mono', fontFamily: string): void {
|
|
66
|
-
if (typeof document === 'undefined') return
|
|
67
|
-
const cssVar = target === 'heading' ? '--font-heading' : target === 'mono' ? '--font-mono' : '--font-sans'
|
|
68
|
-
document.documentElement.style.setProperty(cssVar, fontFamily)
|
|
69
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
detectPlatform,
|
|
3
|
-
getActivePlatform,
|
|
4
|
-
setActivePlatform,
|
|
5
|
-
applyPlatformTokens,
|
|
6
|
-
PLATFORM_PROFILES,
|
|
7
|
-
type PlatformId,
|
|
8
|
-
type PlatformProfile,
|
|
9
|
-
} from './platform'
|
|
10
|
-
|
|
11
|
-
export {
|
|
12
|
-
exportMod,
|
|
13
|
-
importMod,
|
|
14
|
-
forkMod,
|
|
15
|
-
applyMod,
|
|
16
|
-
saveOrgMod,
|
|
17
|
-
getOrgMod,
|
|
18
|
-
BUILTIN_MODS,
|
|
19
|
-
type ModTheme,
|
|
20
|
-
} from './theme'
|
|
21
|
-
|
|
22
|
-
export {
|
|
23
|
-
injectCustomFont,
|
|
24
|
-
removeCustomFont,
|
|
25
|
-
applyFontFamily,
|
|
26
|
-
type CustomFontOptions,
|
|
27
|
-
} from './font'
|
package/src/mod.test.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { detectPlatform, PLATFORM_PROFILES } from './platform'
|
|
3
|
-
import { BUILTIN_MODS, exportMod, forkMod, importMod } from './theme'
|
|
4
|
-
|
|
5
|
-
describe('mod system', () => {
|
|
6
|
-
it('detects platforms accurately from user agents', () => {
|
|
7
|
-
expect(detectPlatform('Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15')).toBe('ios')
|
|
8
|
-
expect(detectPlatform('Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36')).toBe('android')
|
|
9
|
-
expect(detectPlatform('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36')).toBe('macos')
|
|
10
|
-
expect(detectPlatform('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36')).toBe('windows')
|
|
11
|
-
expect(detectPlatform('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36')).toBe('linux')
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
it('provides platform profiles with correct typography and radius', () => {
|
|
15
|
-
expect(PLATFORM_PROFILES.ios.radiusCard).toBe(22)
|
|
16
|
-
expect(PLATFORM_PROFILES.ios.glassBlur).toBe(24)
|
|
17
|
-
expect(PLATFORM_PROFILES.android.radiusCard).toBe(16)
|
|
18
|
-
expect(PLATFORM_PROFILES.windows.radiusCard).toBe(8)
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it('serializes and deserializes mods cleanly', () => {
|
|
22
|
-
const base = BUILTIN_MODS[0]
|
|
23
|
-
const json = exportMod(base)
|
|
24
|
-
const imported = importMod(json)
|
|
25
|
-
expect(imported.name).toBe(base.name)
|
|
26
|
-
expect(imported.platform).toBe(base.platform)
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('forks mods with custom designer overrides', () => {
|
|
30
|
-
const base = BUILTIN_MODS[0]
|
|
31
|
-
const forked = forkMod(base, {
|
|
32
|
-
name: 'Acme Brand Theme',
|
|
33
|
-
primary: '#10b981',
|
|
34
|
-
fontHeading: 'Cabinet Grotesk',
|
|
35
|
-
})
|
|
36
|
-
expect(forked.name).toBe('Acme Brand Theme')
|
|
37
|
-
expect(forked.primary).toBe('#10b981')
|
|
38
|
-
expect(forked.fontHeading).toBe('Cabinet Grotesk')
|
|
39
|
-
expect(forked.platform).toBe('ios')
|
|
40
|
-
})
|
|
41
|
-
})
|
package/src/platform.test.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'vitest'
|
|
2
|
-
import { PLATFORM_PROFILES, type PlatformId } from './platform'
|
|
3
|
-
|
|
4
|
-
describe('@hanzo/mod platform profiles', () => {
|
|
5
|
-
test('defines profiles for all major platforms', () => {
|
|
6
|
-
const platforms: PlatformId[] = ['ios', 'android', 'macos', 'windows', 'linux']
|
|
7
|
-
for (const p of platforms) {
|
|
8
|
-
expect(PLATFORM_PROFILES[p]).toBeDefined()
|
|
9
|
-
expect(PLATFORM_PROFILES[p].id).toBe(p)
|
|
10
|
-
expect(PLATFORM_PROFILES[p].fontFamily).toBeDefined()
|
|
11
|
-
}
|
|
12
|
-
})
|
|
13
|
-
})
|
package/src/platform.ts
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Native OS platform detection and design system adaptors for Hanzo.
|
|
3
|
-
*
|
|
4
|
-
* Configures native look/feel tailored to the user's OS:
|
|
5
|
-
* - iOS: Continuous squircles, Liquid Glass blurs, SF Pro fonts, safe area insets, specular highlights
|
|
6
|
-
* - Android: Material 3 tokens, Roboto/Google Sans, tonal elevations, ripple curves
|
|
7
|
-
* - macOS: Vibrant translucent glass, compact desktop paddings
|
|
8
|
-
* - Windows: Fluent/Mica acrylic glass, sharp modern geometry
|
|
9
|
-
* - Linux: Clean system lines, flat modern edges
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
export type PlatformId = 'ios' | 'android' | 'macos' | 'windows' | 'linux'
|
|
13
|
-
|
|
14
|
-
export interface PlatformProfile {
|
|
15
|
-
id: PlatformId
|
|
16
|
-
name: string
|
|
17
|
-
fontFamily: string
|
|
18
|
-
fontMono: string
|
|
19
|
-
radiusCard: number | string
|
|
20
|
-
radiusPill: number | string
|
|
21
|
-
glassBlur: number
|
|
22
|
-
glassSaturate: number
|
|
23
|
-
specularBorder: boolean
|
|
24
|
-
touchFeedback: 'spring' | 'ripple' | 'subtle'
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const PLATFORM_PROFILES: Record<PlatformId, PlatformProfile> = {
|
|
28
|
-
ios: {
|
|
29
|
-
id: 'ios',
|
|
30
|
-
name: 'Apple iOS',
|
|
31
|
-
fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", sans-serif',
|
|
32
|
-
fontMono: '"SF Mono", ui-monospace, Menlo, monospace',
|
|
33
|
-
radiusCard: 22,
|
|
34
|
-
radiusPill: 9999,
|
|
35
|
-
glassBlur: 24,
|
|
36
|
-
glassSaturate: 190,
|
|
37
|
-
specularBorder: true,
|
|
38
|
-
touchFeedback: 'spring',
|
|
39
|
-
},
|
|
40
|
-
android: {
|
|
41
|
-
id: 'android',
|
|
42
|
-
name: 'Google Android',
|
|
43
|
-
fontFamily: 'Roboto, "Google Sans", system-ui, sans-serif',
|
|
44
|
-
fontMono: '"Roboto Mono", monospace',
|
|
45
|
-
radiusCard: 16,
|
|
46
|
-
radiusPill: 16,
|
|
47
|
-
glassBlur: 16,
|
|
48
|
-
glassSaturate: 130,
|
|
49
|
-
specularBorder: false,
|
|
50
|
-
touchFeedback: 'ripple',
|
|
51
|
-
},
|
|
52
|
-
macos: {
|
|
53
|
-
id: 'macos',
|
|
54
|
-
name: 'Apple macOS',
|
|
55
|
-
fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif',
|
|
56
|
-
fontMono: '"SF Mono", Menlo, monospace',
|
|
57
|
-
radiusCard: 12,
|
|
58
|
-
radiusPill: 9999,
|
|
59
|
-
glassBlur: 20,
|
|
60
|
-
glassSaturate: 160,
|
|
61
|
-
specularBorder: true,
|
|
62
|
-
touchFeedback: 'subtle',
|
|
63
|
-
},
|
|
64
|
-
windows: {
|
|
65
|
-
id: 'windows',
|
|
66
|
-
name: 'Microsoft Windows',
|
|
67
|
-
fontFamily: '"Segoe UI Variable", "Segoe UI", system-ui, sans-serif',
|
|
68
|
-
fontMono: '"Cascadia Code", Consolas, monospace',
|
|
69
|
-
radiusCard: 8,
|
|
70
|
-
radiusPill: 9999,
|
|
71
|
-
glassBlur: 18,
|
|
72
|
-
glassSaturate: 125,
|
|
73
|
-
specularBorder: false,
|
|
74
|
-
touchFeedback: 'subtle',
|
|
75
|
-
},
|
|
76
|
-
linux: {
|
|
77
|
-
id: 'linux',
|
|
78
|
-
name: 'Linux / Desktop',
|
|
79
|
-
fontFamily: 'system-ui, -apple-system, "Cantarell", sans-serif',
|
|
80
|
-
fontMono: '"Ubuntu Mono", monospace',
|
|
81
|
-
radiusCard: 10,
|
|
82
|
-
radiusPill: 9999,
|
|
83
|
-
glassBlur: 14,
|
|
84
|
-
glassSaturate: 110,
|
|
85
|
-
specularBorder: false,
|
|
86
|
-
touchFeedback: 'subtle',
|
|
87
|
-
},
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const STORAGE_KEY = 'hanzo.mod.platform'
|
|
91
|
-
|
|
92
|
-
/** Detects the current OS platform from user agent strings */
|
|
93
|
-
export function detectPlatform(userAgent?: string): PlatformId {
|
|
94
|
-
const ua = (userAgent ?? (typeof navigator !== 'undefined' ? navigator.userAgent : '')).toLowerCase()
|
|
95
|
-
if (/iphone|ipad|ipod/.test(ua) || (typeof navigator !== 'undefined' && navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) {
|
|
96
|
-
return 'ios'
|
|
97
|
-
}
|
|
98
|
-
if (/android/.test(ua)) return 'android'
|
|
99
|
-
if (/macintosh|mac os x/.test(ua)) return 'macos'
|
|
100
|
-
if (/windows|win32|win64/.test(ua)) return 'windows'
|
|
101
|
-
if (/linux|x11/.test(ua)) return 'linux'
|
|
102
|
-
return 'ios'
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Reads the user's active platform preference (stored or detected) */
|
|
106
|
-
export function getActivePlatform(): PlatformId {
|
|
107
|
-
if (typeof window === 'undefined') return 'ios'
|
|
108
|
-
try {
|
|
109
|
-
const kept = window.localStorage.getItem(STORAGE_KEY) as PlatformId | null
|
|
110
|
-
if (kept && kept in PLATFORM_PROFILES) return kept
|
|
111
|
-
} catch {
|
|
112
|
-
// ignore storage restrictions
|
|
113
|
-
}
|
|
114
|
-
return detectPlatform()
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** Sets the user's preferred platform customization */
|
|
118
|
-
export function setActivePlatform(platform: PlatformId | 'auto'): void {
|
|
119
|
-
if (typeof window === 'undefined') return
|
|
120
|
-
try {
|
|
121
|
-
if (platform === 'auto') {
|
|
122
|
-
window.localStorage.removeItem(STORAGE_KEY)
|
|
123
|
-
} else {
|
|
124
|
-
window.localStorage.setItem(STORAGE_KEY, platform)
|
|
125
|
-
}
|
|
126
|
-
} catch {
|
|
127
|
-
// ignore storage restrictions
|
|
128
|
-
}
|
|
129
|
-
applyPlatformTokens(platform === 'auto' ? detectPlatform() : platform)
|
|
130
|
-
window.dispatchEvent(new CustomEvent('hanzo:mod:change', { detail: { platform } }))
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Statically stamps the platform CSS custom properties onto documentElement */
|
|
134
|
-
export function applyPlatformTokens(platform: PlatformId): void {
|
|
135
|
-
if (typeof document === 'undefined') return
|
|
136
|
-
const profile = PLATFORM_PROFILES[platform] ?? PLATFORM_PROFILES.ios
|
|
137
|
-
const root = document.documentElement
|
|
138
|
-
|
|
139
|
-
root.setAttribute('data-platform', profile.id)
|
|
140
|
-
root.style.setProperty('--platform-font-sans', profile.fontFamily)
|
|
141
|
-
root.style.setProperty('--platform-font-mono', profile.fontMono)
|
|
142
|
-
root.style.setProperty('--platform-radius-card', typeof profile.radiusCard === 'number' ? `${profile.radiusCard}px` : profile.radiusCard)
|
|
143
|
-
root.style.setProperty('--platform-radius-pill', typeof profile.radiusPill === 'number' ? `${profile.radiusPill}px` : profile.radiusPill)
|
|
144
|
-
root.style.setProperty('--glass-blur', `${profile.glassBlur}px`)
|
|
145
|
-
root.style.setProperty('--glass-saturate', `${profile.glassSaturate}%`)
|
|
146
|
-
}
|
package/src/theme.ts
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { PLATFORM_PROFILES, type PlatformId, applyPlatformTokens } from './platform'
|
|
2
|
-
|
|
3
|
-
export interface ModTheme {
|
|
4
|
-
id: string
|
|
5
|
-
name: string
|
|
6
|
-
description?: string
|
|
7
|
-
author?: string
|
|
8
|
-
org?: string
|
|
9
|
-
platform?: PlatformId
|
|
10
|
-
fontHeading?: string
|
|
11
|
-
fontBody?: string
|
|
12
|
-
fontMono?: string
|
|
13
|
-
customFontUrl?: string
|
|
14
|
-
primary?: string
|
|
15
|
-
accent?: string
|
|
16
|
-
glassTint?: string
|
|
17
|
-
glassBlur?: number
|
|
18
|
-
radius?: number
|
|
19
|
-
density?: 'compact' | 'comfortable' | 'default'
|
|
20
|
-
customCss?: string
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export const BUILTIN_MODS: ModTheme[] = [
|
|
24
|
-
{
|
|
25
|
-
id: 'ios-liquid',
|
|
26
|
-
name: 'Apple iOS Liquid',
|
|
27
|
-
description: 'Continuous squircles, high-specular liquid glass, and Apple SF Pro typography.',
|
|
28
|
-
author: 'Hanzo Design',
|
|
29
|
-
platform: 'ios',
|
|
30
|
-
glassBlur: 24,
|
|
31
|
-
radius: 22,
|
|
32
|
-
density: 'comfortable',
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
id: 'android-material',
|
|
36
|
-
name: 'Android Material 3',
|
|
37
|
-
description: 'Google Material 3 tonal elevations, Roboto Sans, and smooth dynamic rounded cards.',
|
|
38
|
-
author: 'Hanzo Design',
|
|
39
|
-
platform: 'android',
|
|
40
|
-
glassBlur: 16,
|
|
41
|
-
radius: 16,
|
|
42
|
-
density: 'default',
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
id: 'macos-vibrant',
|
|
46
|
-
name: 'macOS Vibrant',
|
|
47
|
-
description: 'Translucent sidebars, vibrant visual effect filters, and clean desktop density.',
|
|
48
|
-
author: 'Hanzo Design',
|
|
49
|
-
platform: 'macos',
|
|
50
|
-
glassBlur: 20,
|
|
51
|
-
radius: 12,
|
|
52
|
-
density: 'compact',
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
id: 'windows-fluent',
|
|
56
|
-
name: 'Windows Mica Fluent',
|
|
57
|
-
description: 'Microsoft Fluent Design Mica acrylic and Segoe UI Variable geometry.',
|
|
58
|
-
author: 'Hanzo Design',
|
|
59
|
-
platform: 'windows',
|
|
60
|
-
glassBlur: 18,
|
|
61
|
-
radius: 8,
|
|
62
|
-
density: 'default',
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
id: 'cyber-zen',
|
|
66
|
-
name: 'Cyber Zen (OLED)',
|
|
67
|
-
description: 'High contrast deep OLED black with electric cyan and magenta accents.',
|
|
68
|
-
author: 'Hanzo Labs',
|
|
69
|
-
platform: 'ios',
|
|
70
|
-
primary: '#00f0ff',
|
|
71
|
-
accent: '#ff007f',
|
|
72
|
-
glassBlur: 28,
|
|
73
|
-
radius: 20,
|
|
74
|
-
density: 'comfortable',
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
id: 'nordic-frost',
|
|
78
|
-
name: 'Nordic Frost',
|
|
79
|
-
description: 'Pure arctic monochrome glass, high clarity, and minimalist typography.',
|
|
80
|
-
author: 'Hanzo Design',
|
|
81
|
-
platform: 'macos',
|
|
82
|
-
primary: '#e2e8f0',
|
|
83
|
-
accent: '#38bdf8',
|
|
84
|
-
glassBlur: 22,
|
|
85
|
-
radius: 14,
|
|
86
|
-
density: 'default',
|
|
87
|
-
},
|
|
88
|
-
]
|
|
89
|
-
|
|
90
|
-
const ORG_MOD_KEY = (orgId: string) => `hanzo.mod.org.${orgId}`
|
|
91
|
-
const ACTIVE_MOD_KEY = 'hanzo.mod.active'
|
|
92
|
-
|
|
93
|
-
/** Serializes a theme mod to JSON for exporting/sharing */
|
|
94
|
-
export function exportMod(mod: ModTheme): string {
|
|
95
|
-
return JSON.stringify(mod, null, 2)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Parses and validates an imported mod JSON string */
|
|
99
|
-
export function importMod(json: string): ModTheme {
|
|
100
|
-
const parsed = JSON.parse(json) as Partial<ModTheme>
|
|
101
|
-
if (!parsed || typeof parsed !== 'object' || !parsed.name) {
|
|
102
|
-
throw new Error('Invalid Hanzo mod: missing theme name')
|
|
103
|
-
}
|
|
104
|
-
return {
|
|
105
|
-
id: parsed.id || `mod-${Date.now()}`,
|
|
106
|
-
name: String(parsed.name).slice(0, 64),
|
|
107
|
-
description: parsed.description ? String(parsed.description).slice(0, 256) : undefined,
|
|
108
|
-
author: parsed.author ? String(parsed.author).slice(0, 64) : undefined,
|
|
109
|
-
org: parsed.org,
|
|
110
|
-
platform: parsed.platform && parsed.platform in PLATFORM_PROFILES ? parsed.platform : undefined,
|
|
111
|
-
fontHeading: parsed.fontHeading,
|
|
112
|
-
fontBody: parsed.fontBody,
|
|
113
|
-
fontMono: parsed.fontMono,
|
|
114
|
-
customFontUrl: parsed.customFontUrl,
|
|
115
|
-
primary: parsed.primary,
|
|
116
|
-
accent: parsed.accent,
|
|
117
|
-
glassTint: parsed.glassTint,
|
|
118
|
-
glassBlur: typeof parsed.glassBlur === 'number' ? parsed.glassBlur : undefined,
|
|
119
|
-
radius: typeof parsed.radius === 'number' ? parsed.radius : undefined,
|
|
120
|
-
density: parsed.density,
|
|
121
|
-
customCss: parsed.customCss,
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** Creates a new mod derived from an existing base theme */
|
|
126
|
-
export function forkMod(base: ModTheme, overrides: Partial<ModTheme>): ModTheme {
|
|
127
|
-
return {
|
|
128
|
-
...base,
|
|
129
|
-
...overrides,
|
|
130
|
-
id: overrides.id || `${base.id}-fork-${Date.now().toString(36)}`,
|
|
131
|
-
name: overrides.name || `${base.name} (Mod)`,
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** Applies a theme mod dynamically to the page */
|
|
136
|
-
export function applyMod(mod: ModTheme): void {
|
|
137
|
-
if (typeof document === 'undefined') return
|
|
138
|
-
const root = document.documentElement
|
|
139
|
-
|
|
140
|
-
if (mod.platform) {
|
|
141
|
-
applyPlatformTokens(mod.platform)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (mod.primary) root.style.setProperty('--primary', mod.primary)
|
|
145
|
-
if (mod.accent) root.style.setProperty('--accent', mod.accent)
|
|
146
|
-
if (mod.glassTint) root.style.setProperty('--liquid-glass-bg', mod.glassTint)
|
|
147
|
-
if (typeof mod.glassBlur === 'number') root.style.setProperty('--glass-blur', `${mod.glassBlur}px`)
|
|
148
|
-
if (typeof mod.radius === 'number') root.style.setProperty('--radius', `${mod.radius}px`)
|
|
149
|
-
if (mod.fontHeading) root.style.setProperty('--font-heading', mod.fontHeading)
|
|
150
|
-
if (mod.fontBody) root.style.setProperty('--font-body', mod.fontBody)
|
|
151
|
-
if (mod.fontMono) root.style.setProperty('--font-mono', mod.fontMono)
|
|
152
|
-
|
|
153
|
-
// Inject custom CSS if defined
|
|
154
|
-
let styleEl = document.getElementById('hanzo-custom-mod-css')
|
|
155
|
-
if (mod.customCss) {
|
|
156
|
-
if (!styleEl) {
|
|
157
|
-
styleEl = document.createElement('style')
|
|
158
|
-
styleEl.id = 'hanzo-custom-mod-css'
|
|
159
|
-
document.head.appendChild(styleEl)
|
|
160
|
-
}
|
|
161
|
-
styleEl.textContent = mod.customCss
|
|
162
|
-
} else if (styleEl) {
|
|
163
|
-
styleEl.remove()
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
window.localStorage.setItem(ACTIVE_MOD_KEY, JSON.stringify(mod))
|
|
168
|
-
window.dispatchEvent(new CustomEvent('hanzo:mod:applied', { detail: { mod } }))
|
|
169
|
-
} catch {
|
|
170
|
-
// ignore
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** Saves an organization-scoped mod configuration */
|
|
175
|
-
export function saveOrgMod(orgId: string, mod: ModTheme): void {
|
|
176
|
-
if (typeof window === 'undefined') return
|
|
177
|
-
try {
|
|
178
|
-
window.localStorage.setItem(ORG_MOD_KEY(orgId), JSON.stringify({ ...mod, org: orgId }))
|
|
179
|
-
} catch {
|
|
180
|
-
// ignore
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/** Gets the active mod for an organization */
|
|
185
|
-
export function getOrgMod(orgId: string): ModTheme | null {
|
|
186
|
-
if (typeof window === 'undefined') return null
|
|
187
|
-
try {
|
|
188
|
-
const raw = window.localStorage.getItem(ORG_MOD_KEY(orgId))
|
|
189
|
-
return raw ? (JSON.parse(raw) as ModTheme) : null
|
|
190
|
-
} catch {
|
|
191
|
-
return null
|
|
192
|
-
}
|
|
193
|
-
}
|
package/tsconfig.build.json
DELETED
package/tsconfig.json
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
-
"module": "esnext",
|
|
6
|
-
"moduleResolution": "bundler",
|
|
7
|
-
"jsx": "react-jsx",
|
|
8
|
-
"strict": false,
|
|
9
|
-
"strictNullChecks": false,
|
|
10
|
-
"noImplicitAny": false,
|
|
11
|
-
"skipLibCheck": true,
|
|
12
|
-
"esModuleInterop": true,
|
|
13
|
-
"resolveJsonModule": true,
|
|
14
|
-
"isolatedModules": true,
|
|
15
|
-
"noEmit": true,
|
|
16
|
-
"types": ["node", "react"]
|
|
17
|
-
},
|
|
18
|
-
"include": ["src"],
|
|
19
|
-
"exclude": ["node_modules", "dist"]
|
|
20
|
-
}
|