@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 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';
@@ -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
- applyPlatformTokens(platform === 'auto' ? detectPlatform() : platform);
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', typeof profile.radiusCard === 'number' ? `${profile.radiusCard}px` : profile.radiusCard);
132
- root.style.setProperty('--platform-radius-pill', typeof profile.radiusPill === 'number' ? `${profile.radiusPill}px` : profile.radiusPill);
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
- if (mod.primary)
118
- root.style.setProperty('--primary', mod.primary);
119
- if (mod.accent)
120
- root.style.setProperty('--accent', mod.accent);
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
- if (typeof mod.radius === 'number')
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
- if (mod.fontHeading)
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
- if (mod.fontBody)
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
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "@hanzo/mod",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
+ "files": [
5
+ "dist"
6
+ ],
4
7
  "description": "Native OS look/feel customization, custom fonts, and mod themes for Hanzo.",
5
8
  "type": "module",
6
9
  "main": "./dist/index.js",
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
- })
@@ -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
- }
@@ -1,11 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "compilerOptions": {
4
- "noEmit": false,
5
- "declaration": true,
6
- "outDir": "./dist",
7
- "rootDir": "./src"
8
- },
9
- "include": ["src"],
10
- "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"]
11
- }
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
- }