@hanzo/mod
Five platform look profiles (iOS, Android, macOS, Windows, Linux), theme mods as JSON, and web fonts, written as CSS custom properties on the page's root element and kept in this browser's localStorage. No server, no account sync, no mod loader.
@hanzo/mod is a small browser library with no dependencies. It does three
things, all on the page it runs in:
- Platform profiles. Five sets of font stacks, corner radii and glass values, named after an OS. It picks one from the user agent or keeps the one chosen, and writes it as CSS custom properties.
- Theme mods. A mod is a JSON object: colours, fonts, a corner radius, glass values, custom CSS. It applies one, serializes it, parses and checks one, or derives a new one from another. Six are built in.
- Fonts. It loads a web font by stylesheet or font-file URL and points the heading, body or mono face at a family.
Everything it does is a write to document.documentElement (CSS custom
properties and a data-platform attribute), a <link> or <style> element
in the page's head, or a localStorage key, and it dispatches two events on
window (hanzo:mod:change, hanzo:mod:applied). It makes no request of its own:
the only network traffic is the font or stylesheet URL you hand it. On a server
(no window, no document) the functions that touch the page do nothing,
getActivePlatform answers ios and getOrgMod answers null.
- npm:
@hanzo/mod, 0.1.0 - Source:
hanzoai/app, directorypkg/mod - License: Apache-2.0 OR MIT
- Peer:
react19 or later is declared; no module imports it
@hanzo/app depends on it, and its Settings panel is the one place it is used today (below).
Install
pnpm add @hanzo/modExample
From the Settings panel in @hanzo/app (pkg/app/src/look.tsx), reduced to
the calls:
import { setActivePlatform, BUILTIN_MODS, applyMod, importMod } from '@hanzo/mod'
// A platform: kept under localStorage hanzo.mod.platform, then written to <html>.
// 'auto' forgets the choice and follows the user agent.
setActivePlatform('macos')
// A built-in mod, or one pasted as JSON.
applyMod(BUILTIN_MODS[5])
applyMod(importMod(pasted))A web font, loaded by its stylesheet and made the body face:
import { injectCustomFont, applyFontFamily } from '@hanzo/mod'
injectCustomFont({
family: 'Inter',
url: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;600',
isStylesheet: true,
})
applyFontFamily('sans', 'Inter, sans-serif')For a stylesheet, the face is named by the stylesheet itself (Google's CSS
above declares Inter); family only names the element injectCustomFont
adds, so it can be replaced or removed.
Exported API
Platform profiles:
| Export | What it does |
|---|---|
PLATFORM_PROFILES | For ios, android, macos, windows and linux: a name, a font stack, a mono stack, card and pill radii, glass blur (px) and saturation (%), specularBorder and touchFeedback. |
detectPlatform(userAgent?) | Reads the user agent (default navigator.userAgent) and returns a PlatformId. An iPad that reports MacIntel with touch points counts as ios; a user agent that matches nothing returns ios. |
getActivePlatform() | The id kept under localStorage key hanzo.mod.platform, else detectPlatform(). |
setActivePlatform(id) | Keeps id under hanzo.mod.platform, or removes the key for 'auto'; calls applyPlatformTokens; dispatches hanzo:mod:change on window. |
applyPlatformTokens(id) | Sets data-platform and --platform-font-sans, --platform-font-mono, --platform-radius-card, --platform-radius-pill, --glass-blur and --glass-saturate on <html>. |
Theme mods:
| Export | What it does |
|---|---|
ModTheme | The type: id, name, and optional description, author, org, platform, fontHeading, fontBody, fontMono, customFontUrl, primary, accent, glassTint, glassBlur, radius, density, customCss. |
BUILTIN_MODS | Six mods: ios-liquid, android-material, macos-vibrant, windows-fluent, cyber-zen, nordic-frost. |
applyMod(mod) | Applies the mod's platform profile when it names one; sets --primary, --accent, --liquid-glass-bg (from glassTint), --glass-blur, --radius, --font-heading, --font-body, --font-mono for the fields the mod has; puts customCss in <style id="hanzo-custom-mod-css">, or removes that element when the mod has none; keeps the mod under hanzo.mod.active; dispatches hanzo:mod:applied. |
exportMod(mod) | The mod as indented JSON. |
importMod(json) | Parses JSON into a ModTheme. Throws Invalid Hanzo mod: missing theme name when there is no name. Cuts name and author to 64 characters and description to 256, drops a platform it does not know and a glassBlur or radius that is not a number, gives a missing id one made from the time, and passes every other field through as given. |
forkMod(base, overrides) | base with overrides on top, under a new id (<base id>-fork-<time>) and the name <base name> (Mod), unless the overrides set them. |
saveOrgMod(orgId, mod) | Writes the mod, with org set to orgId, to localStorage key hanzo.mod.org.<orgId> in this browser. |
getOrgMod(orgId) | Reads that key back, or null. |
Fonts:
| Export | What it does |
|---|---|
CustomFontOptions | The type: family, url, and optional format (woff2, woff, truetype, opentype), weights, isStylesheet. |
injectCustomFont(options) | A URL ending in .css, on fonts.googleapis.com, or passed with isStylesheet becomes a <link rel="stylesheet">. Any other URL becomes one @font-face rule per weight (default 400, 500, 600, 700, font-display: swap) in a <style>. Replaces the element an earlier call made for the same family. |
removeCustomFont(family) | Removes that element. |
applyFontFamily(target, family) | Sets --font-heading, --font-sans or --font-mono on <html>, for target heading, sans or mono. |
What reads the properties
@hanzo/mod writes custom properties; a stylesheet decides what they change.
In what hanzo.ai ships (@hanzo/design 0.5.35, @hanzo/ui 8.29.26,
@hanzo/glass 0.1.0):
| Property | Written by | Read by |
|---|---|---|
--font-sans, --font-mono, --radius, --primary | applyFontFamily, applyMod | @hanzo/design and @hanzo/ui |
--accent | applyMod | @hanzo/ui |
--liquid-glass-bg | applyMod | @hanzo/glass |
data-platform, --platform-*, --glass-blur, --glass-saturate, --font-heading, --font-body | applyPlatformTokens, applyMod, applyFontFamily | None of them. A host that wants these reads them in its own CSS. |
So on hanzo.ai a platform profile alone changes nothing on screen: its radii
and font stacks go to --platform-*, which no Hanzo stylesheet reads. A mod's
own radius, colours and mono face do.
In hanzo.ai's Settings
Settings → General on hanzo.ai draws Look from @hanzo/app/look. Below the
appearance controls (theme, type, accent, which @hanzo/appearance keeps with
the person's IAM account), it has four @hanzo/mod controls:
- Native Platform Look & Feel: Auto and the five platforms, through
setActivePlatform. - Organization Theme Mods: one button per built-in mod, through
applyMod. The choice is not saved for the organization: the panel does not callsaveOrgMod. - Custom Typography: a family name is applied to the heading and body faces
with
applyFontFamily. A stylesheet URL is loaded, but the face applied is namedCustomBrandFont, which a stylesheet such as Google Fonts' does not declare, so text falls back to the browser's generic sans-serif. - Fork & Share Theme Mods: pasted JSON goes through
importModandapplyMod. Export JSON copies the first built-in mod (ios-liquid), not the one in effect.
A mod or a font applied there lasts until the page reloads. The chosen platform is kept, and the panel shows it as chosen on the next visit, but its properties are not written to the page again.
What it does not do
- No server and no account sync. Every key is in this browser's
localStorage.saveOrgModdoes not reach another browser, another device, or another member of the organization. - No re-apply on load.
applyModkeeps the mod underhanzo.mod.active, and nothing in the package reads that key back; the kept platform is not written to the page again either. A host that wants a choice to outlive a reload callsapplyModandapplyPlatformTokens(getActivePlatform())at start-up itself. - No reset.
applyModsets only the fields a mod has. A colour or radius an earlier mod set stays until the page reloads. - Not every field is applied.
density,customFontUrl,description,authorandorgtravel in a mod's JSON;applyModreads none of them. - No sanitizing.
importModpasses colours andcustomCssthrough as given, andapplyModputscustomCsson the page as it is. CSS can load remote URLs, so apply only mods you trust. - No native look. The profiles are font stacks and numbers for web CSS. It draws no native controls, and there is no native mobile shell on @hanzo/app.
- No mod loader. HIP-0528 (a draft) describes mods as packages: a manifest, a registry, a loader, and an API for them. None of that is in 0.1.0; this package is the three groups above and nothing else.
Related
hanzoai/app: the source,pkg/mod@hanzo/modon npm- @hanzo/app: the app whose Settings panel uses it
- HIP-0528: the draft mod contract