@hanzo/mod 0.0.0-stage → 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/font.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Dynamic custom font loader and manager for organizations and designers.
3
+ */
4
+ export interface CustomFontOptions {
5
+ family: string;
6
+ url: string;
7
+ format?: 'woff2' | 'woff' | 'truetype' | 'opentype';
8
+ weights?: (number | string)[];
9
+ isStylesheet?: boolean;
10
+ }
11
+ /**
12
+ * Injects a designer/organization custom font into document head dynamically.
13
+ * Supports direct webfont stylesheets (Google Fonts, Adobe Fonts, Typekit) or @font-face binaries.
14
+ */
15
+ export declare function injectCustomFont(options: CustomFontOptions): void;
16
+ /** Removes an injected custom font stylesheet or style tag */
17
+ export declare function removeCustomFont(family: string): void;
18
+ /** Statically applies a font-family to CSS variables */
19
+ export declare function applyFontFamily(target: 'heading' | 'sans' | 'mono', fontFamily: string): void;
package/dist/font.js ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Dynamic custom font loader and manager for organizations and designers.
3
+ */
4
+ const INJECTED_FONT_PREFIX = 'hanzo-font-';
5
+ /**
6
+ * Injects a designer/organization custom font into document head dynamically.
7
+ * Supports direct webfont stylesheets (Google Fonts, Adobe Fonts, Typekit) or @font-face binaries.
8
+ */
9
+ export function injectCustomFont(options) {
10
+ if (typeof document === 'undefined')
11
+ return;
12
+ const elementId = `${INJECTED_FONT_PREFIX}${options.family.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
13
+ const existing = document.getElementById(elementId);
14
+ if (existing)
15
+ existing.remove();
16
+ if (options.isStylesheet || options.url.endsWith('.css') || options.url.includes('fonts.googleapis.com')) {
17
+ const link = document.createElement('link');
18
+ link.id = elementId;
19
+ link.rel = 'stylesheet';
20
+ link.href = options.url;
21
+ document.head.appendChild(link);
22
+ }
23
+ else {
24
+ const format = options.format || (options.url.endsWith('.woff2') ? 'woff2' : options.url.endsWith('.woff') ? 'woff' : 'truetype');
25
+ const weights = options.weights || [400, 500, 600, 700];
26
+ let fontFaceRules = '';
27
+ for (const w of weights) {
28
+ fontFaceRules += `
29
+ @font-face {
30
+ font-family: "${options.family}";
31
+ src: url("${options.url}") format("${format}");
32
+ font-weight: ${w};
33
+ font-style: normal;
34
+ font-display: swap;
35
+ }
36
+ `;
37
+ }
38
+ const style = document.createElement('style');
39
+ style.id = elementId;
40
+ style.textContent = fontFaceRules;
41
+ document.head.appendChild(style);
42
+ }
43
+ }
44
+ /** Removes an injected custom font stylesheet or style tag */
45
+ export function removeCustomFont(family) {
46
+ if (typeof document === 'undefined')
47
+ return;
48
+ const elementId = `${INJECTED_FONT_PREFIX}${family.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
49
+ const el = document.getElementById(elementId);
50
+ if (el)
51
+ el.remove();
52
+ }
53
+ /** Statically applies a font-family to CSS variables */
54
+ export function applyFontFamily(target, fontFamily) {
55
+ if (typeof document === 'undefined')
56
+ return;
57
+ const cssVar = target === 'heading' ? '--font-heading' : target === 'mono' ? '--font-mono' : '--font-sans';
58
+ document.documentElement.style.setProperty(cssVar, fontFamily);
59
+ }
@@ -0,0 +1,3 @@
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
+ export { injectCustomFont, removeCustomFont, applyFontFamily, type CustomFontOptions, } from './font.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
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
+ export { injectCustomFont, removeCustomFont, applyFontFamily, } from './font.js';
@@ -0,0 +1,34 @@
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
+ export type PlatformId = 'ios' | 'android' | 'macos' | 'windows' | 'linux';
12
+ export interface PlatformProfile {
13
+ id: PlatformId;
14
+ name: string;
15
+ fontFamily: string;
16
+ fontMono: string;
17
+ radiusCard: number | string;
18
+ radiusPill: number | string;
19
+ glassBlur: number;
20
+ glassSaturate: number;
21
+ specularBorder: boolean;
22
+ touchFeedback: 'spring' | 'ripple' | 'subtle';
23
+ }
24
+ export declare const PLATFORM_PROFILES: Record<PlatformId, PlatformProfile>;
25
+ /** Detects the current OS platform from user agent strings */
26
+ export declare function detectPlatform(userAgent?: string): PlatformId;
27
+ /** Reads the user's active platform preference (stored or detected) */
28
+ export declare function getActivePlatform(): PlatformId;
29
+ /** Clears inline platform token overrides from documentElement */
30
+ export declare function clearPlatformTokens(): void;
31
+ /** Sets the user's preferred platform customization */
32
+ export declare function setActivePlatform(platform: PlatformId | 'auto'): void;
33
+ /** Statically stamps the platform CSS custom properties onto documentElement */
34
+ export declare function applyPlatformTokens(platform: PlatformId): void;
@@ -0,0 +1,179 @@
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
+ export const PLATFORM_PROFILES = {
12
+ ios: {
13
+ id: 'ios',
14
+ name: 'Apple iOS',
15
+ fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", sans-serif',
16
+ fontMono: '"SF Mono", ui-monospace, Menlo, monospace',
17
+ radiusCard: 22,
18
+ radiusPill: 9999,
19
+ glassBlur: 24,
20
+ glassSaturate: 190,
21
+ specularBorder: true,
22
+ touchFeedback: 'spring',
23
+ },
24
+ android: {
25
+ id: 'android',
26
+ name: 'Google Android',
27
+ fontFamily: 'Roboto, "Google Sans", system-ui, sans-serif',
28
+ fontMono: '"Roboto Mono", monospace',
29
+ radiusCard: 16,
30
+ radiusPill: 16,
31
+ glassBlur: 16,
32
+ glassSaturate: 130,
33
+ specularBorder: false,
34
+ touchFeedback: 'ripple',
35
+ },
36
+ macos: {
37
+ id: 'macos',
38
+ name: 'Apple macOS',
39
+ fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif',
40
+ fontMono: '"SF Mono", Menlo, monospace',
41
+ radiusCard: 12,
42
+ radiusPill: 9999,
43
+ glassBlur: 20,
44
+ glassSaturate: 160,
45
+ specularBorder: true,
46
+ touchFeedback: 'subtle',
47
+ },
48
+ windows: {
49
+ id: 'windows',
50
+ name: 'Microsoft Windows',
51
+ fontFamily: '"Segoe UI Variable", "Segoe UI", system-ui, sans-serif',
52
+ fontMono: '"Cascadia Code", Consolas, monospace',
53
+ radiusCard: 8,
54
+ radiusPill: 9999,
55
+ glassBlur: 18,
56
+ glassSaturate: 125,
57
+ specularBorder: false,
58
+ touchFeedback: 'subtle',
59
+ },
60
+ linux: {
61
+ id: 'linux',
62
+ name: 'Linux / Desktop',
63
+ fontFamily: 'system-ui, -apple-system, "Cantarell", sans-serif',
64
+ fontMono: '"Ubuntu Mono", monospace',
65
+ radiusCard: 10,
66
+ radiusPill: 9999,
67
+ glassBlur: 14,
68
+ glassSaturate: 110,
69
+ specularBorder: false,
70
+ touchFeedback: 'subtle',
71
+ },
72
+ };
73
+ const STORAGE_KEY = 'hanzo.mod.platform';
74
+ /** Detects the current OS platform from user agent strings */
75
+ export function detectPlatform(userAgent) {
76
+ const ua = (userAgent ?? (typeof navigator !== 'undefined' ? navigator.userAgent : '')).toLowerCase();
77
+ if (/iphone|ipad|ipod/.test(ua) || (typeof navigator !== 'undefined' && navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) {
78
+ return 'ios';
79
+ }
80
+ if (/android/.test(ua))
81
+ return 'android';
82
+ if (/macintosh|mac os x/.test(ua))
83
+ return 'macos';
84
+ if (/windows|win32|win64/.test(ua))
85
+ return 'windows';
86
+ if (/linux|x11/.test(ua))
87
+ return 'linux';
88
+ return 'ios';
89
+ }
90
+ /** Reads the user's active platform preference (stored or detected) */
91
+ export function getActivePlatform() {
92
+ if (typeof window === 'undefined')
93
+ return 'ios';
94
+ try {
95
+ const kept = window.localStorage.getItem(STORAGE_KEY);
96
+ if (kept && kept in PLATFORM_PROFILES)
97
+ return kept;
98
+ }
99
+ catch {
100
+ // ignore storage restrictions
101
+ }
102
+ return detectPlatform();
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
+ }
128
+ /** Sets the user's preferred platform customization */
129
+ export function setActivePlatform(platform) {
130
+ if (typeof window === 'undefined')
131
+ return;
132
+ try {
133
+ if (platform === 'auto') {
134
+ window.localStorage.removeItem(STORAGE_KEY);
135
+ }
136
+ else {
137
+ window.localStorage.setItem(STORAGE_KEY, platform);
138
+ }
139
+ }
140
+ catch {
141
+ // ignore storage restrictions
142
+ }
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
+ }
152
+ window.dispatchEvent(new CustomEvent('hanzo:mod:change', { detail: { platform } }));
153
+ }
154
+ /** Statically stamps the platform CSS custom properties onto documentElement */
155
+ export function applyPlatformTokens(platform) {
156
+ if (typeof document === 'undefined')
157
+ return;
158
+ const profile = PLATFORM_PROFILES[platform] ?? PLATFORM_PROFILES.ios;
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';
165
+ root.setAttribute('data-platform', profile.id);
166
+ root.style.setProperty('--platform-font-sans', profile.fontFamily);
167
+ root.style.setProperty('--platform-font-mono', profile.fontMono);
168
+ root.style.setProperty('--platform-radius-card', radiusCardPx);
169
+ root.style.setProperty('--platform-radius-pill', radiusPillPx);
170
+ root.style.setProperty('--glass-blur', `${profile.glassBlur}px`);
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`);
179
+ }
@@ -0,0 +1,41 @@
1
+ import { type PlatformId } from './platform.js';
2
+ export interface ModTheme {
3
+ id: string;
4
+ name: string;
5
+ description?: string;
6
+ author?: string;
7
+ org?: string;
8
+ platform?: PlatformId;
9
+ theme?: 'light' | 'dark' | 'system';
10
+ background?: string;
11
+ surface?: string;
12
+ foreground?: string;
13
+ fontHeading?: string;
14
+ fontBody?: string;
15
+ fontMono?: string;
16
+ customFontUrl?: string;
17
+ primary?: string;
18
+ accent?: string;
19
+ glassTint?: string;
20
+ glassBlur?: number;
21
+ radius?: number;
22
+ density?: 'compact' | 'comfortable' | 'default';
23
+ customCss?: string;
24
+ }
25
+ export declare const BUILTIN_MODS: ModTheme[];
26
+ /** Serializes a theme mod to JSON for exporting/sharing */
27
+ export declare function exportMod(mod: ModTheme): string;
28
+ /** Parses and validates an imported mod JSON string */
29
+ export declare function importMod(json: string): ModTheme;
30
+ /** Creates a new mod derived from an existing base theme */
31
+ export declare function forkMod(base: ModTheme, overrides: Partial<ModTheme>): ModTheme;
32
+ /** Applies a theme mod dynamically to the page */
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;
38
+ /** Saves an organization-scoped mod configuration */
39
+ export declare function saveOrgMod(orgId: string, mod: ModTheme): void;
40
+ /** Gets the active mod for an organization */
41
+ export declare function getOrgMod(orgId: string): ModTheme | null;
package/dist/theme.js ADDED
@@ -0,0 +1,357 @@
1
+ import { PLATFORM_PROFILES, applyPlatformTokens, clearPlatformTokens } from './platform.js';
2
+ export const BUILTIN_MODS = [
3
+ {
4
+ id: 'ios-liquid',
5
+ name: 'Apple iOS Liquid',
6
+ description: 'Continuous squircles, high-specular liquid glass, and Apple SF Pro typography.',
7
+ author: 'Hanzo Design',
8
+ platform: 'ios',
9
+ theme: 'dark',
10
+ background: '#000000',
11
+ glassBlur: 24,
12
+ radius: 22,
13
+ density: 'comfortable',
14
+ },
15
+ {
16
+ id: 'android-material',
17
+ name: 'Android Material 3',
18
+ description: 'Google Material 3 tonal elevations, Roboto Sans, and smooth dynamic rounded cards.',
19
+ author: 'Hanzo Design',
20
+ platform: 'android',
21
+ theme: 'dark',
22
+ background: '#0b0e14',
23
+ glassBlur: 16,
24
+ radius: 16,
25
+ density: 'default',
26
+ },
27
+ {
28
+ id: 'macos-vibrant',
29
+ name: 'macOS Vibrant',
30
+ description: 'Translucent sidebars, vibrant visual effect filters, and clean desktop density.',
31
+ author: 'Hanzo Design',
32
+ platform: 'macos',
33
+ theme: 'dark',
34
+ background: '#09090b',
35
+ glassBlur: 20,
36
+ radius: 12,
37
+ density: 'compact',
38
+ },
39
+ {
40
+ id: 'windows-fluent',
41
+ name: 'Windows Mica Fluent',
42
+ description: 'Microsoft Fluent Design Mica acrylic and Segoe UI Variable geometry.',
43
+ author: 'Hanzo Design',
44
+ platform: 'windows',
45
+ theme: 'dark',
46
+ background: '#0a0d12',
47
+ glassBlur: 18,
48
+ radius: 8,
49
+ density: 'default',
50
+ },
51
+ {
52
+ id: 'cyber-zen',
53
+ name: 'Cyber Zen (OLED)',
54
+ description: 'High contrast deep OLED black with electric cyan and magenta accents.',
55
+ author: 'Hanzo Labs',
56
+ platform: 'ios',
57
+ theme: 'dark',
58
+ background: '#000000',
59
+ primary: '#00f0ff',
60
+ accent: '#ff007f',
61
+ glassBlur: 28,
62
+ radius: 20,
63
+ density: 'comfortable',
64
+ },
65
+ {
66
+ id: 'nordic-frost',
67
+ name: 'Nordic Frost',
68
+ description: 'Pure arctic monochrome glass, high clarity, and minimalist typography.',
69
+ author: 'Hanzo Design',
70
+ platform: 'macos',
71
+ theme: 'dark',
72
+ background: '#090d16',
73
+ primary: '#e2e8f0',
74
+ accent: '#38bdf8',
75
+ glassBlur: 22,
76
+ radius: 14,
77
+ density: 'default',
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
+ },
93
+ ];
94
+ const ORG_MOD_KEY = (orgId) => `hanzo.mod.org.${orgId}`;
95
+ const ACTIVE_MOD_KEY = 'hanzo.mod.active';
96
+ /** Serializes a theme mod to JSON for exporting/sharing */
97
+ export function exportMod(mod) {
98
+ return JSON.stringify(mod, null, 2);
99
+ }
100
+ /** Parses and validates an imported mod JSON string */
101
+ export function importMod(json) {
102
+ const parsed = JSON.parse(json);
103
+ if (!parsed || typeof parsed !== 'object' || !parsed.name) {
104
+ throw new Error('Invalid Hanzo mod: missing theme name');
105
+ }
106
+ return {
107
+ id: parsed.id || `mod-${Date.now()}`,
108
+ name: String(parsed.name).slice(0, 64),
109
+ description: parsed.description ? String(parsed.description).slice(0, 256) : undefined,
110
+ author: parsed.author ? String(parsed.author).slice(0, 64) : undefined,
111
+ org: parsed.org,
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,
117
+ fontHeading: parsed.fontHeading,
118
+ fontBody: parsed.fontBody,
119
+ fontMono: parsed.fontMono,
120
+ customFontUrl: parsed.customFontUrl,
121
+ primary: parsed.primary,
122
+ accent: parsed.accent,
123
+ glassTint: parsed.glassTint,
124
+ glassBlur: typeof parsed.glassBlur === 'number' ? parsed.glassBlur : undefined,
125
+ radius: typeof parsed.radius === 'number' ? parsed.radius : undefined,
126
+ density: parsed.density,
127
+ customCss: parsed.customCss,
128
+ };
129
+ }
130
+ /** Creates a new mod derived from an existing base theme */
131
+ export function forkMod(base, overrides) {
132
+ return {
133
+ ...base,
134
+ ...overrides,
135
+ id: overrides.id || `${base.id}-fork-${Date.now().toString(36)}`,
136
+ name: overrides.name || `${base.name} (Mod)`,
137
+ };
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
+ ];
188
+ /** Applies a theme mod dynamically to the page */
189
+ export function applyMod(mod) {
190
+ if (typeof document === 'undefined')
191
+ return;
192
+ const root = document.documentElement;
193
+ for (const prop of MOD_CSS_VARS) {
194
+ root.style.removeProperty(prop);
195
+ }
196
+ if (mod.platform) {
197
+ applyPlatformTokens(mod.platform);
198
+ }
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
+ }
246
+ if (mod.glassTint)
247
+ root.style.setProperty('--liquid-glass-bg', mod.glassTint);
248
+ if (typeof mod.glassBlur === 'number') {
249
+ root.style.setProperty('--glass-blur', `${mod.glassBlur}px`);
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') {
254
+ root.style.setProperty('--radius', `${mod.radius}px`);
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) {
263
+ root.style.setProperty('--font-heading', mod.fontHeading);
264
+ root.style.setProperty('--font-display', mod.fontHeading);
265
+ }
266
+ if (mod.fontBody) {
267
+ root.style.setProperty('--font-body', mod.fontBody);
268
+ root.style.setProperty('--font-sans', mod.fontBody);
269
+ }
270
+ if (mod.fontMono)
271
+ root.style.setProperty('--font-mono', mod.fontMono);
272
+ // Inject custom CSS if defined
273
+ let styleEl = document.getElementById('hanzo-custom-mod-css');
274
+ if (mod.customCss) {
275
+ if (!styleEl) {
276
+ styleEl = document.createElement('style');
277
+ styleEl.id = 'hanzo-custom-mod-css';
278
+ document.head.appendChild(styleEl);
279
+ }
280
+ styleEl.textContent = mod.customCss;
281
+ }
282
+ else if (styleEl) {
283
+ styleEl.remove();
284
+ }
285
+ try {
286
+ window.localStorage.setItem(ACTIVE_MOD_KEY, JSON.stringify(mod));
287
+ window.dispatchEvent(new CustomEvent('hanzo:mod:applied', { detail: { mod } }));
288
+ }
289
+ catch {
290
+ // ignore
291
+ }
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
+ }
335
+ /** Saves an organization-scoped mod configuration */
336
+ export function saveOrgMod(orgId, mod) {
337
+ if (typeof window === 'undefined')
338
+ return;
339
+ try {
340
+ window.localStorage.setItem(ORG_MOD_KEY(orgId), JSON.stringify({ ...mod, org: orgId }));
341
+ }
342
+ catch {
343
+ // ignore
344
+ }
345
+ }
346
+ /** Gets the active mod for an organization */
347
+ export function getOrgMod(orgId) {
348
+ if (typeof window === 'undefined')
349
+ return null;
350
+ try {
351
+ const raw = window.localStorage.getItem(ORG_MOD_KEY(orgId));
352
+ return raw ? JSON.parse(raw) : null;
353
+ }
354
+ catch {
355
+ return null;
356
+ }
357
+ }
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "name": "@hanzo/mod",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.2",
4
+ "files": [
5
+ "dist"
6
+ ],
7
+ "description": "Native OS look/feel customization, custom fonts, and mod themes for Hanzo.",
8
+ "type": "module",
9
+ "main": "./dist/index.js",
10
+ "module": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "license": "Apache-2.0 OR MIT",
20
+ "devDependencies": {
21
+ "@types/node": "26.5.0",
22
+ "@types/react": "19.2.18",
23
+ "react": "19.2.8",
24
+ "typescript": "7.0.2",
25
+ "vitest": "4.1.11"
26
+ },
27
+ "peerDependencies": {
28
+ "react": ">=19.0.0"
29
+ },
30
+ "scripts": {
31
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && node ../app/scripts/specifiers.mjs ./dist",
32
+ "typecheck": "tsc --noEmit",
33
+ "test": "vitest run"
34
+ }
6
35
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.