@hanzo/mod 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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, PLATFORM_PROFILES, type PlatformId, type PlatformProfile, } from './platform.js';
2
+ export { exportMod, importMod, forkMod, applyMod, 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, PLATFORM_PROFILES, } from './platform.js';
2
+ export { exportMod, importMod, forkMod, applyMod, saveOrgMod, getOrgMod, BUILTIN_MODS, } from './theme.js';
3
+ export { injectCustomFont, removeCustomFont, applyFontFamily, } from './font.js';
@@ -0,0 +1,32 @@
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
+ /** Sets the user's preferred platform customization */
30
+ export declare function setActivePlatform(platform: PlatformId | 'auto'): void;
31
+ /** Statically stamps the platform CSS custom properties onto documentElement */
32
+ export declare function applyPlatformTokens(platform: PlatformId): void;
@@ -0,0 +1,135 @@
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
+ /** Sets the user's preferred platform customization */
105
+ export function setActivePlatform(platform) {
106
+ if (typeof window === 'undefined')
107
+ return;
108
+ try {
109
+ if (platform === 'auto') {
110
+ window.localStorage.removeItem(STORAGE_KEY);
111
+ }
112
+ else {
113
+ window.localStorage.setItem(STORAGE_KEY, platform);
114
+ }
115
+ }
116
+ catch {
117
+ // ignore storage restrictions
118
+ }
119
+ applyPlatformTokens(platform === 'auto' ? detectPlatform() : platform);
120
+ window.dispatchEvent(new CustomEvent('hanzo:mod:change', { detail: { platform } }));
121
+ }
122
+ /** Statically stamps the platform CSS custom properties onto documentElement */
123
+ export function applyPlatformTokens(platform) {
124
+ if (typeof document === 'undefined')
125
+ return;
126
+ const profile = PLATFORM_PROFILES[platform] ?? PLATFORM_PROFILES.ios;
127
+ const root = document.documentElement;
128
+ root.setAttribute('data-platform', profile.id);
129
+ root.style.setProperty('--platform-font-sans', profile.fontFamily);
130
+ 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);
133
+ root.style.setProperty('--glass-blur', `${profile.glassBlur}px`);
134
+ root.style.setProperty('--glass-saturate', `${profile.glassSaturate}%`);
135
+ }
@@ -0,0 +1,33 @@
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
+ fontHeading?: string;
10
+ fontBody?: string;
11
+ fontMono?: string;
12
+ customFontUrl?: string;
13
+ primary?: string;
14
+ accent?: string;
15
+ glassTint?: string;
16
+ glassBlur?: number;
17
+ radius?: number;
18
+ density?: 'compact' | 'comfortable' | 'default';
19
+ customCss?: string;
20
+ }
21
+ export declare const BUILTIN_MODS: ModTheme[];
22
+ /** Serializes a theme mod to JSON for exporting/sharing */
23
+ export declare function exportMod(mod: ModTheme): string;
24
+ /** Parses and validates an imported mod JSON string */
25
+ export declare function importMod(json: string): ModTheme;
26
+ /** Creates a new mod derived from an existing base theme */
27
+ export declare function forkMod(base: ModTheme, overrides: Partial<ModTheme>): ModTheme;
28
+ /** Applies a theme mod dynamically to the page */
29
+ export declare function applyMod(mod: ModTheme): void;
30
+ /** Saves an organization-scoped mod configuration */
31
+ export declare function saveOrgMod(orgId: string, mod: ModTheme): void;
32
+ /** Gets the active mod for an organization */
33
+ export declare function getOrgMod(orgId: string): ModTheme | null;
package/dist/theme.js ADDED
@@ -0,0 +1,176 @@
1
+ import { PLATFORM_PROFILES, applyPlatformTokens } 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
+ glassBlur: 24,
10
+ radius: 22,
11
+ density: 'comfortable',
12
+ },
13
+ {
14
+ id: 'android-material',
15
+ name: 'Android Material 3',
16
+ description: 'Google Material 3 tonal elevations, Roboto Sans, and smooth dynamic rounded cards.',
17
+ author: 'Hanzo Design',
18
+ platform: 'android',
19
+ glassBlur: 16,
20
+ radius: 16,
21
+ density: 'default',
22
+ },
23
+ {
24
+ id: 'macos-vibrant',
25
+ name: 'macOS Vibrant',
26
+ description: 'Translucent sidebars, vibrant visual effect filters, and clean desktop density.',
27
+ author: 'Hanzo Design',
28
+ platform: 'macos',
29
+ glassBlur: 20,
30
+ radius: 12,
31
+ density: 'compact',
32
+ },
33
+ {
34
+ id: 'windows-fluent',
35
+ name: 'Windows Mica Fluent',
36
+ description: 'Microsoft Fluent Design Mica acrylic and Segoe UI Variable geometry.',
37
+ author: 'Hanzo Design',
38
+ platform: 'windows',
39
+ glassBlur: 18,
40
+ radius: 8,
41
+ density: 'default',
42
+ },
43
+ {
44
+ id: 'cyber-zen',
45
+ name: 'Cyber Zen (OLED)',
46
+ description: 'High contrast deep OLED black with electric cyan and magenta accents.',
47
+ author: 'Hanzo Labs',
48
+ platform: 'ios',
49
+ primary: '#00f0ff',
50
+ accent: '#ff007f',
51
+ glassBlur: 28,
52
+ radius: 20,
53
+ density: 'comfortable',
54
+ },
55
+ {
56
+ id: 'nordic-frost',
57
+ name: 'Nordic Frost',
58
+ description: 'Pure arctic monochrome glass, high clarity, and minimalist typography.',
59
+ author: 'Hanzo Design',
60
+ platform: 'macos',
61
+ primary: '#e2e8f0',
62
+ accent: '#38bdf8',
63
+ glassBlur: 22,
64
+ radius: 14,
65
+ density: 'default',
66
+ },
67
+ ];
68
+ const ORG_MOD_KEY = (orgId) => `hanzo.mod.org.${orgId}`;
69
+ const ACTIVE_MOD_KEY = 'hanzo.mod.active';
70
+ /** Serializes a theme mod to JSON for exporting/sharing */
71
+ export function exportMod(mod) {
72
+ return JSON.stringify(mod, null, 2);
73
+ }
74
+ /** Parses and validates an imported mod JSON string */
75
+ export function importMod(json) {
76
+ const parsed = JSON.parse(json);
77
+ if (!parsed || typeof parsed !== 'object' || !parsed.name) {
78
+ throw new Error('Invalid Hanzo mod: missing theme name');
79
+ }
80
+ return {
81
+ id: parsed.id || `mod-${Date.now()}`,
82
+ name: String(parsed.name).slice(0, 64),
83
+ description: parsed.description ? String(parsed.description).slice(0, 256) : undefined,
84
+ author: parsed.author ? String(parsed.author).slice(0, 64) : undefined,
85
+ org: parsed.org,
86
+ platform: parsed.platform && parsed.platform in PLATFORM_PROFILES ? parsed.platform : undefined,
87
+ fontHeading: parsed.fontHeading,
88
+ fontBody: parsed.fontBody,
89
+ fontMono: parsed.fontMono,
90
+ customFontUrl: parsed.customFontUrl,
91
+ primary: parsed.primary,
92
+ accent: parsed.accent,
93
+ glassTint: parsed.glassTint,
94
+ glassBlur: typeof parsed.glassBlur === 'number' ? parsed.glassBlur : undefined,
95
+ radius: typeof parsed.radius === 'number' ? parsed.radius : undefined,
96
+ density: parsed.density,
97
+ customCss: parsed.customCss,
98
+ };
99
+ }
100
+ /** Creates a new mod derived from an existing base theme */
101
+ export function forkMod(base, overrides) {
102
+ return {
103
+ ...base,
104
+ ...overrides,
105
+ id: overrides.id || `${base.id}-fork-${Date.now().toString(36)}`,
106
+ name: overrides.name || `${base.name} (Mod)`,
107
+ };
108
+ }
109
+ /** Applies a theme mod dynamically to the page */
110
+ export function applyMod(mod) {
111
+ if (typeof document === 'undefined')
112
+ return;
113
+ const root = document.documentElement;
114
+ if (mod.platform) {
115
+ applyPlatformTokens(mod.platform);
116
+ }
117
+ if (mod.primary)
118
+ root.style.setProperty('--primary', mod.primary);
119
+ if (mod.accent)
120
+ root.style.setProperty('--accent', mod.accent);
121
+ if (mod.glassTint)
122
+ root.style.setProperty('--liquid-glass-bg', mod.glassTint);
123
+ if (typeof mod.glassBlur === 'number')
124
+ root.style.setProperty('--glass-blur', `${mod.glassBlur}px`);
125
+ if (typeof mod.radius === 'number')
126
+ root.style.setProperty('--radius', `${mod.radius}px`);
127
+ if (mod.fontHeading)
128
+ root.style.setProperty('--font-heading', mod.fontHeading);
129
+ if (mod.fontBody)
130
+ root.style.setProperty('--font-body', mod.fontBody);
131
+ if (mod.fontMono)
132
+ root.style.setProperty('--font-mono', mod.fontMono);
133
+ // Inject custom CSS if defined
134
+ let styleEl = document.getElementById('hanzo-custom-mod-css');
135
+ if (mod.customCss) {
136
+ if (!styleEl) {
137
+ styleEl = document.createElement('style');
138
+ styleEl.id = 'hanzo-custom-mod-css';
139
+ document.head.appendChild(styleEl);
140
+ }
141
+ styleEl.textContent = mod.customCss;
142
+ }
143
+ else if (styleEl) {
144
+ styleEl.remove();
145
+ }
146
+ try {
147
+ window.localStorage.setItem(ACTIVE_MOD_KEY, JSON.stringify(mod));
148
+ window.dispatchEvent(new CustomEvent('hanzo:mod:applied', { detail: { mod } }));
149
+ }
150
+ catch {
151
+ // ignore
152
+ }
153
+ }
154
+ /** Saves an organization-scoped mod configuration */
155
+ export function saveOrgMod(orgId, mod) {
156
+ if (typeof window === 'undefined')
157
+ return;
158
+ try {
159
+ window.localStorage.setItem(ORG_MOD_KEY(orgId), JSON.stringify({ ...mod, org: orgId }));
160
+ }
161
+ catch {
162
+ // ignore
163
+ }
164
+ }
165
+ /** Gets the active mod for an organization */
166
+ export function getOrgMod(orgId) {
167
+ if (typeof window === 'undefined')
168
+ return null;
169
+ try {
170
+ const raw = window.localStorage.getItem(ORG_MOD_KEY(orgId));
171
+ return raw ? JSON.parse(raw) : null;
172
+ }
173
+ catch {
174
+ return null;
175
+ }
176
+ }
package/package.json CHANGED
@@ -1,6 +1,32 @@
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.0",
4
+ "description": "Native OS look/feel customization, custom fonts, and mod themes for Hanzo.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "license": "Apache-2.0 OR MIT",
17
+ "devDependencies": {
18
+ "@types/node": "26.5.0",
19
+ "@types/react": "19.2.18",
20
+ "react": "19.2.8",
21
+ "typescript": "7.0.2",
22
+ "vitest": "4.1.11"
23
+ },
24
+ "peerDependencies": {
25
+ "react": ">=19.0.0"
26
+ },
27
+ "scripts": {
28
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && node ../app/scripts/specifiers.mjs ./dist",
29
+ "typecheck": "tsc --noEmit",
30
+ "test": "vitest run"
31
+ }
6
32
  }
package/src/font.ts ADDED
@@ -0,0 +1,69 @@
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 ADDED
@@ -0,0 +1,27 @@
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'
@@ -0,0 +1,41 @@
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
+ })
@@ -0,0 +1,13 @@
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
+ })
@@ -0,0 +1,146 @@
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 ADDED
@@ -0,0 +1,193 @@
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
+ }
@@ -0,0 +1,11 @@
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 ADDED
@@ -0,0 +1,20 @@
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
+ }
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.