From d42d1ffd2cbb392c655fd6dd5a23f3f75eb70e52 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Thu, 15 Oct 2026 08:25:34 -0700 Subject: [PATCH 1/4] feat(tokens): native fonts package, token font variables and radius/type/motion/density roles (#64 #65 #66) Co-Authored-By: Claude Opus 5.5 --- .github/workflows/quality.yml | 2 +- .gitignore | 2 +- apps/docs/public/llms.txt | 60 +++++-- .../public/markdown/foundations/platform.md | 6 +- .../foundations/source-reconciliation.md | 2 +- .../markdown/governance/new-repository.md | 2 +- .../public/markdown/governance/quality.md | 4 +- apps/docs/public/markdown/installation.md | 29 ++- apps/docs/public/markdown/migration/native.md | 17 +- docs/foundations/platform.md | 6 +- docs/foundations/source-reconciliation.md | 2 +- docs/governance/new-repository.md | 2 +- docs/governance/quality.md | 4 +- docs/installation.md | 29 ++- docs/migration/native.md | 17 +- fixtures/native/App.tsx | 2 +- fixtures/native/gesture-sheet.tsx | 2 +- fixtures/native/package.json | 1 + fixtures/next/src/app/client.tsx | 3 +- fixtures/next/src/app/layout.tsx | 4 +- package-lock.json | 10 ++ package.json | 2 +- packages/native-fonts/LICENSE | 5 + packages/native-fonts/README.md | 17 ++ .../assets => native-fonts}/fonts/OFL.txt | 0 .../fonts/manifest.json | 0 packages/native-fonts/package.json | 26 +++ packages/native-fonts/src/index.ts | 167 ++++++++++++++++++ packages/native-fonts/tsconfig.json | 12 ++ packages/react/src/styles.css | 4 +- packages/tokens/generated/manifest.json | 26 +++ packages/tokens/generated/tokens.css | 43 +++++ packages/tokens/package.json | 3 +- packages/tokens/source.json | 26 +++ packages/tokens/src/index.ts | 26 +++ planning/budgets.json | 2 +- scripts/browser-smoke.mjs | 5 +- scripts/build.mjs | 2 +- scripts/check-drift.mjs | 3 +- scripts/font-check.mjs | 17 ++ scripts/generate-native-fonts-index.mjs | 7 + scripts/generate-native-fonts.py | 4 +- scripts/generate.mjs | 19 ++ scripts/next-browser-smoke.mjs | 9 +- scripts/packed-native-smoke.mjs | 14 +- scripts/packed-next-smoke.mjs | 11 +- scripts/packed-smoke.mjs | 2 +- scripts/quality.mjs | 6 +- scripts/release-dry-run.mjs | 7 +- scripts/source-metro-baseline.mjs | 2 +- tests/quality.test.ts | 1 + tests/tokens.test.ts | 39 ++++ 52 files changed, 652 insertions(+), 61 deletions(-) create mode 100644 packages/native-fonts/LICENSE create mode 100644 packages/native-fonts/README.md rename packages/{tokens/assets => native-fonts}/fonts/OFL.txt (100%) rename packages/{tokens/assets => native-fonts}/fonts/manifest.json (100%) create mode 100644 packages/native-fonts/package.json create mode 100644 packages/native-fonts/src/index.ts create mode 100644 packages/native-fonts/tsconfig.json create mode 100644 scripts/font-check.mjs create mode 100644 scripts/generate-native-fonts-index.mjs diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index ccca6f4..ebcaaf6 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -30,10 +30,10 @@ jobs: - run: npm run smoke:packed-native - run: npm run smoke:packed-boundaries - run: npm run smoke:packed-examples + - run: npx playwright install --with-deps chromium firefox webkit - run: npm run smoke:packed-next env: NEXT_TELEMETRY_DISABLED: '1' - - run: npx playwright install --with-deps chromium firefox webkit - name: Browser, visual and hydration gates run: node scripts/run-browser-gates.mjs - run: npm run release:dry-run -- --channel candidate diff --git a/.gitignore b/.gitignore index 3364b74..3ac2db5 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,7 @@ apps/docs/content/docs/ apps/docs/out/ *.tsbuildinfo .local/ -packages/tokens/assets/fonts/*.ttf +packages/native-fonts/fonts/*.ttf .validation-tmp/ *.log diff --git a/apps/docs/public/llms.txt b/apps/docs/public/llms.txt index c3d7a73..ccbf8ee 100644 --- a/apps/docs/public/llms.txt +++ b/apps/docs/public/llms.txt @@ -825,15 +825,15 @@ Dark is the only audited mode. Source values retain the commit-pinned monorepo a ## Typography (#7) -Pin @bloxwap/font 0.1.2. Vite: import `@bloxwap/font/css`; Next: upstream Sans/Mono loaders own loading and script fallback. Sans for prose/controls; Mono for code and tabular readouts; Pixel for a short nonessential accent. Body 16/24, label 14/20, caption 12/18, title 24/30, display 48/52 are candidate role mappings based on SDK scale. Web rem uses 16px root; RN sizes are explicit dp and allow font scaling. Do not disable Dynamic Type or fixed-height clip translated labels. Run the pinned native font generator and register its static TTF faces from the optional tokens/fonts export; the generated manifest records weight/family/source hashes. Published WOFF2/Next loaders are not directly native assets. **Do** wait for registration before applying a custom native family; **don't** invent a family string and claim the font rendered. Arabic/Hebrew/CJK companions are owned upstream; load required scripts, test mixed-script punctuation and RTL. Large-text and device font evidence is pending. +Pin @bloxwap/font 0.1.2. Vite: import `@bloxwap/font/css`; Next: upstream Sans/Mono loaders own loading and script fallback. Sans for prose/controls; Mono for code and tabular readouts; Pixel for a short nonessential accent. Body 16/24, label 14/20, caption 12/18, title 24/30, display 48/52 are candidate role mappings based on SDK scale. Web CSS exposes the roles as `--hig-type--size`, `-line-height`, `-weight` and a `--hig-type-` font shorthand, and the families as `--hig-font-sans`/`--hig-font-mono`. Those resolve the `@bloxwap/font` Next loader variables first and fall back to the literal `'Bloxwap Sans'`/`'Bloxwap Mono'` families, so HIG controls render the loaded face in both Next and plain `@font-face` hosts. Web rem uses 16px root; RN sizes are explicit dp and allow font scaling. Do not disable Dynamic Type or fixed-height clip translated labels. Run the pinned native font generator and register its static TTF faces from `@bloxwap/hig-native-fonts`; its typed manifest records weight/family/source hashes. Web hosts never install that package. Published WOFF2/Next loaders are not directly native assets. **Do** wait for registration before applying a custom native family; **don't** invent a family string and claim the font rendered. Arabic/Hebrew/CJK companions are owned upstream; load required scripts, test mixed-script punctuation and RTL. Large-text and device font evidence is pending. ## Geometry and layout (#8) -Spaces 4/8/12/16/20/24/32/48; radius 6/8/10/14 and pill. Comfortable control/target 48; compact 36, reserved for pointer/keyboard dense layouts. Painted icons can be 16/20/24 while targets stay 48 on touch. Never expand adjacent invisible hit areas so they overlap. Use logical inline spacing, wrapping labels, safe-area insets, keyboard avoidance and a scrollable native sheet; preserve portrait/tablet rotation without locking orientation. Pro tables may scroll with column headers; app amount entry reflows. Elevation uses nested fills rather than unverified shadows. Layer order: document, sticky nav, contextual popover, modal, notification. Browser top layer handles modal stacking; native Modal remains platform-owned. +Spaces 4/8/12/16/20/24/32/48; radius 6/8/10/14 and pill (`--hig-radius-sm`…`--hig-radius-xl`, `--hig-radius-pill`). Comfortable control/target 48 and gap 12; compact 36 and gap 8, reserved for pointer/keyboard dense layouts. CSS exposes each mode as `--hig-density--`, and `--hig-density-control`, `--hig-density-target` and `--hig-density-gap` follow the nearest `[data-hig-density]`, which the React provider sets. Painted icons can be 16/20/24 while targets stay 48 on touch. Never expand adjacent invisible hit areas so they overlap. Use logical inline spacing, wrapping labels, safe-area insets, keyboard avoidance and a scrollable native sheet; preserve portrait/tablet rotation without locking orientation. Pro tables may scroll with column headers; app amount entry reflows. Elevation uses nested fills rather than unverified shadows. Layer order: document, sticky nav, contextual popover, modal, notification. Browser top layer handles modal stacking; native Modal remains platform-owned. ## Motion and gestures (#9) -Fast 180ms, base 260ms, slow 320ms; web easing `cubic-bezier(.22,1,.36,1)`. Press response ends on release/cancel; action executes on accepted activation, never press-in. Drag/scroll cancellation clears visual and feedback state. Interrupt transitions from current state instead of queuing. Reduced motion uses immediate state changes, static skeletons and unanimated numeric readouts. Outcome messages remain explicit. **Do** animate deliberate disclosure once; **don't** animate or play sound for every live-data update. Native candidate uses static transitions until cancellation/device animation evidence exists; motion preference is independent from sound/haptics. +Fast 180ms, base 260ms, slow 320ms; web easing `cubic-bezier(.22,1,.36,1)`. CSS exposes `--hig-motion-duration-` and `--hig-motion-easing-standard`. The durations become 0ms under `prefers-reduced-motion: reduce` and inside `[data-hig-motion='reduced']`, which the React provider sets when the host or OS asks for reduced motion. Press response ends on release/cancel; action executes on accepted activation, never press-in. Drag/scroll cancellation clears visual and feedback state. Interrupt transitions from current state instead of queuing. Reduced motion uses immediate state changes, static skeletons and unanimated numeric readouts. Outcome messages remain explicit. **Do** animate deliberate disclosure once; **don't** animate or play sound for every live-data update. Native candidate uses static transitions until cancellation/device animation evidence exists; motion preference is independent from sound/haptics. ## Accessibility and localization (#10) @@ -905,7 +905,7 @@ Source `spacing.ts` and `type.ts` explicitly set both rem roots to 16; native sp ## Native font generation and registration -Install `fonttools[woff]==4.61.1` and `brotli==1.2.0`, then run `npm run generate:fonts` before native consumer commands. `scripts/generate-native-fonts.py` derives static faces exclusively from the installed `@bloxwap/font@0.1.2` variable WOFF2 files, verifying all four input hashes before writing. It adapts the monorepo's generator and preserves its naming/weight metadata. The generated manifest contains input/output SHA256, PostScript names, Expo registration keys, weights, scripts and byte counts; OFL attribution ships beside the faces. Generated TTF files are build outputs, not per-repository committed binary copies. Local tarballs include them through `@bloxwap/hig-tokens/fonts/*`; consumers register only needed faces. +Install `fonttools[woff]==4.61.1` and `brotli==1.2.0`, then run `npm run generate:fonts` before native consumer commands. `scripts/generate-native-fonts.py` derives static faces exclusively from the installed `@bloxwap/font@0.1.2` variable WOFF2 files, verifying all four input hashes before writing. It adapts the monorepo's generator and preserves its naming/weight metadata. The generated manifest contains input/output SHA256, PostScript names, Expo registration keys, weights, scripts and byte counts; OFL attribution ships beside the faces. Generated TTF files are build outputs, not per-repository committed binary copies. Local tarballs include them through `@bloxwap/hig-native-fonts/fonts/*` (moved out of the web tokens package in #64); consumers register only needed faces. The Expo fixture registers Sans Regular/SemiBold and Mono Regular through `expo-font`, waits for loading, exposes failure visibly and passes explicit 400/600 family keys to the native provider. Text, headings and buttons select registered static weight families, avoiding an invalid variable family name or synthesized weight. Expo web uses feedback.web.ts browser adapters; Android/iOS retain the native backend. Korean/Simplified Chinese Regular/SemiBold faces can be registered by host locale policy; other scripts use an explicit host/system fallback. Pixel remains a scoped web accent, not a native body-font requirement. Font registration in Expo web and Metro builds does not certify native rasterization, native Dynamic Type, or multilingual physical-device layout. @@ -1063,7 +1063,7 @@ Add or update the repository's row in `planning/adoption.json` (a PR to bloxwap/ | Applicability | Install | Never add | | --- | --- | --- | | Product runtime UI (web) | `@bloxwap/hig-tokens`, `-contracts`, `-react`; `@bloxwap/font` | Copied component folders or token CSS | -| Product runtime UI (React Native) | `@bloxwap/hig-tokens`, `-contracts`, `-native`; optional `-feedback` plus Expo peers | DOM renderer imports, font binaries outside the manifest | +| Product runtime UI (React Native) | `@bloxwap/hig-tokens`, `-contracts`, `-native`, `-native-fonts`; optional `-feedback` plus Expo peers | DOM renderer imports, font binaries outside the manifest | | Docs or marketing site | `@bloxwap/hig-tokens`, `-react` (CSS and the controls in use); `@bloxwap/font` | A local `.btn` layer, a palette copy, `vendor/hig` | | Brand or content only | Nothing at runtime; follow the principles, content and brand chapters | React, RN or audio dependencies | | No UI | Nothing; README and content conventions only | Any HIG runtime dependency | @@ -1155,8 +1155,10 @@ Budgets are regression limits from the initial candidate measurements, not claim | Native Switch gzip (React/RN external) | 1,383 B; 1,849 B after #67 | 2,200 B | Raised for #67: the shared Text now carries the font-scale cap and Korean/Chinese script tracking, Switch the token-role track. ~19% headroom; verifies no Slider/SVG/overlay/feedback imports | | Platform-neutral feedback contract gzip | 962 B | 1,200 B | ~25% headroom without device engines | | Docs JS gzip, all lazy chunks | 204,877 B | 240,000 B | ~17% headroom; includes chart/code examples, not initial-load-only | -| Docs CSS | 17,419 B | 22,000 B | ~26% headroom for responsive/documentation states | +| Docs CSS | 110,618 B (with token roles, #66) | 115,000 B | ~4% headroom; raised from 110,000 B when `@bloxwap/hig-tokens/css` gained radius/type/motion/density/font roles | | Sans + Mono assets | 592,796 B | 600,000 B | Pins two approved faces; new script/face requires explicit review | +| Native fixture faces from `@bloxwap/hig-native-fonts` (Sans 400/600, Mono 400) | 1,278,880 B | 1,400,000 B | Only the three registered faces; optional CJK and other weights stay unloaded. Every generated face hash is checked | +| Font binaries in web/shared tarballs (tokens, contracts, react, feedback, native) | 0 files | 0 files | Native faces ship only in `@bloxwap/hig-native-fonts` (#64); `release:dry-run` fails otherwise and records `packedSizes` | | Native four-cue WAV assets | 103,618 B | 110,000 B | ~6% headroom; no full engine/catalog assets | The actual build report is `evidence/quality-report.json`. Bundles use explicit external React/RN peers, so figures measure HIG runtime contribution, not total installed/Metro app size. Production app/Pro/native before/after deltas require owning-repository builds and reviewed baselines; do not invent a percentage improvement. Export/dependency graph reports and packed tests enforce separation, while owning migrations must measure complete apps with equal data/toolchain. @@ -1222,11 +1224,13 @@ These are review candidates, not published npm packages. Use local dry-run tarba ## React / Vite -Run `npm ci && npm run build && npm run smoke:packed`. The smoke script installs tarballs into an independent temporary consumer and builds the fixture. For a host app install tokens, contracts and React tarballs plus React/DOM 19.2. Import `@bloxwap/hig-tokens/css` and `@bloxwap/hig-react/css` once; import individual components such as `@bloxwap/hig-react/button`. Pin @bloxwap/font 0.1.2 and choose only the required families/scripts using asset URL @font-face rules. The included fixture has Sans and Mono only. Never copy font binaries or component folders. +Run `npm ci && npm run build && npm run smoke:packed`. The smoke script installs tarballs into an independent temporary consumer and builds the fixture. For a host app install tokens, contracts and React tarballs plus React/DOM 19.2. Import `@bloxwap/hig-tokens/css` and `@bloxwap/hig-react/css` once; import individual components such as `@bloxwap/hig-react/button`. Pin @bloxwap/font 0.1.2 and choose only the required families/scripts using asset URL @font-face rules named `'Bloxwap Sans'` and `'Bloxwap Mono'`; HIG controls read `--hig-font-sans`/`--hig-font-mono`, which fall back to those literal families. The included fixture has Sans and Mono only, and its browser smoke checks that `.hig-button` and `.hig-number` render loaded Bloxwap faces. Never copy font binaries or component folders. ## SSR and Next client boundaries -Run `npm run smoke:ssr`, `npm run build:next` and `npm run smoke:packed-next`. Token/formatting imports and button/provider SSR are deterministic. Components with hooks carry `use client` in their emitted modules. In Next, import HIG CSS in the app layout and put interactive example state in a client component; keep wallet/network logic with the host. Next's published Bloxwap font loaders remain owned upstream. Next 16.4.0 (webpack), React 19.2 and the published Bloxwap Sans Next loader are verified in the dedicated production fixture; other Next versions require host checks. Verify hydration, stable IDs and StrictMode before adopting a host version. +Run `npm run smoke:ssr`, `npm run build:next` and `npm run smoke:packed-next`. Token/formatting imports and button/provider SSR are deterministic. Components with hooks carry `use client` in their emitted modules. In Next, import HIG CSS in the app layout and put interactive example state in a client component; keep wallet/network logic with the host. Next's published Bloxwap font loaders remain owned upstream. + +Fonts in Next: put the `@bloxwap/font` loader variables on ``, for example ``. The loaders register hashed family names, so HIG never matches a literal `'Bloxwap Sans'` there. Instead `--hig-font-sans` and `--hig-font-mono` in `@bloxwap/hig-tokens/css` resolve `var(--font-bloxwap-sans)` and `var(--font-bloxwap-mono)` first and fall back to the literal families. No bridge CSS is needed. If a host keeps its own variable names, override the roles on `:root` after importing the tokens CSS: `:root { --hig-font-sans: var(--my-sans), 'Bloxwap Sans', system-ui, sans-serif; }`. The variables must be defined on `` (not ``), because the roles resolve on `:root`. `npm run smoke:packed-next` serves the packed consumer and checks, in Chromium, that `.hig-button` and `.hig-number` compute to a loaded Bloxwap face (`document.fonts.check` plus the `FontFace` status). Next 16.4.0 (webpack), React 19.2 and the published Bloxwap Sans Next loader are verified in the dedicated production fixture; other Next versions require host checks. Verify hydration, stable IDs and StrictMode before adopting a host version. ## Expo / React Native @@ -1238,11 +1242,30 @@ Install Expo native peers with `npx expo install` (for example `expo-asset ~12.0 Simulator/emulator verification: `scripts/native-sim/` builds the packed candidate gallery as real Release apps and drives them with XCUITest (iOS Simulator) and UiAutomation instrumentation, uiautomator and TalkBack (Android emulator). Results and residual physical-hardware limits are in `evidence/native-sim/`. -WOFF2 is not a native font asset. `@bloxwap/hig-tokens/fonts/*` ships 15 static Bloxwap Sans/Mono TTFs generated reproducibly from pinned Font 0.1.2 sources, plus a typed FontManifest. Register them with expo-font before setting the native provider fontFamily, as the gallery does. Rasterization on physical displays remains part of #55. Moving these faces out of the web tokens package is tracked in #64. New repositories follow `docs/governance/new-repository.md`. +WOFF2 is not a native font asset. `@bloxwap/hig-native-fonts` ships 15 static Bloxwap Sans/Mono (plus Korean and Simplified Chinese Sans) TTFs generated reproducibly from pinned Font 0.1.2 sources, with a typed `fontManifest` export and `@bloxwap/hig-native-fonts/manifest`. Install it alongside the tokens, contracts and native tarballs, then `require('@bloxwap/hig-native-fonts/fonts/BloxwapSans.ttf')` and register the faces with expo-font before setting the native provider fontFamily, as the gallery does. `@bloxwap/hig-tokens` no longer contains font binaries (#64); see [native font migration](migration/native.md#native-fonts-moved-out-of-hig-tokens). Rasterization on physical displays remains part of #55. New repositories follow `docs/governance/new-repository.md`. ## Tokens only / plain CSS / Tailwind -Install the tokens tarball alone and import CSS variables or typed tokens. No React/RN/audio peer is required. Plain HTML uses semantic `--hig-*` roles. Tailwind may reference these values through host configuration; HIG does not require content scans or a shadcn registry. Native uses dp and explicit hex/channel maps; do not feed CSS functions or rename rem to dp. The audited monorepo web/native rem roots are both 16; HIG supports StyleSheet rather than claiming a verified NativeWind consumer. See the source-reconciliation chapter for native font generation and registration. +Install the tokens tarball alone and import CSS variables or typed tokens. No React/RN/audio peer is required. Plain HTML uses semantic `--hig-*` roles. + +`@bloxwap/hig-tokens/css` defines every role a plain CSS host needs, so nobody has to read the manifest or generate role files: + +| Roles | Example | Values | +| --- | --- | --- | +| Color | `--hig-surface`, `--hig-action` | sRGB, then OKLCH/P3 in `@supports` | +| Space | `--hig-space-md` | `xs`–`xxxl`, `section`, in rem | +| Font | `--hig-font-sans`, `--hig-font-mono` | `@bloxwap/font` Next variable, then literal family | +| Radius | `--hig-radius-md`, `--hig-radius-pill` | `sm` `md` `lg` `xl` in rem; `pill` 9999px | +| Type | `--hig-type-body-size`, `--hig-type-body-line-height`, `--hig-type-body-weight`, `--hig-type-body` | `caption` `label` `body` `title` `display`; the bare role is a `font` shorthand in Sans | +| Motion | `--hig-motion-duration-base`, `--hig-motion-easing-standard` | `fast` `base` `slow` ms; 0ms under `prefers-reduced-motion: reduce` or `[data-hig-motion='reduced']` | +| Density | `--hig-density-control`, `--hig-density-compact-gap` | `control` `target` `gap` per mode; the unprefixed roles follow `[data-hig-density='compact']` | + +```css +@import '@bloxwap/hig-tokens/css'; +.card { border-radius: var(--hig-radius-lg); padding: var(--hig-space-lg); font: var(--hig-type-body); } +.card h2 { font: var(--hig-type-title); } +.card button { min-height: var(--hig-density-control); transition: background var(--hig-motion-duration-fast) var(--hig-motion-easing-standard); } +``` Tailwind may reference these values through host configuration; HIG does not require content scans or a shadcn registry. Native uses dp and explicit hex/channel maps; do not feed CSS functions or rename rem to dp. The audited monorepo web/native rem roots are both 16; HIG supports StyleSheet rather than claiming a verified NativeWind consumer. See the source-reconciliation chapter for native font generation and registration. ## Optional chart, sound and haptics @@ -1266,7 +1289,7 @@ Pilot: font registration → dp/sRGB token mapping → amount/selection/review s Checklist: - [ ] Record actual RN/Expo/Metro/autolinking versions, source commit, old ui-native/token/font/audio wrappers, screenshots and native bundle/assets/latency baseline. -- [ ] Obtain approved pinned static TTF faces and upstream manifest or supply reviewed typed FontManifest; register before setting family. Do not treat WOFF2 or an invented native family as success. +- [ ] Register the pinned static TTF faces from `@bloxwap/hig-native-fonts` and its typed `fontManifest`; register before setting family. Do not treat WOFF2 or an invented native family as success. - [ ] Map dp and legacy NativeWind rem roots explicitly. Keep Dynamic Type, 48 targets, logical RTL layout, safe areas, keyboard avoidance, native back and labelled fields/errors. - [ ] Preview native import codemods, using individual switch/checkbox/range entries; wire optional SVG/Slider/Expo audio/haptic peers only where used. Retain platform-specific descriptions and host callbacks. - [ ] Preserve raw financial strings, order/network logic, stable list identity and quiet live numbers. Use labelled list/readout native chart alternative unless a real supported native engine is reviewed. @@ -1277,6 +1300,21 @@ Checklist: - [ ] Remove duplicated source/assets/backend wrappers after device builds confirm references and autolinking are gone; quantify before/after sizes with equal toolchain/data. - [ ] Link owning rollout PR and remaining version/exception work into the matrix. +## Native fonts moved out of hig-tokens + +Issue #64. Up to `0.1.0-candidate.sha7c67a07c`, `@bloxwap/hig-tokens` shipped the 15 static TTFs under `@bloxwap/hig-tokens/fonts/*`. Web-only consumers never served them, but they made up most of the tarball. They now live in `@bloxwap/hig-native-fonts`. The bytes, file names, registration keys and SHA-256 hashes are unchanged. + +| Before | After | +| --- | --- | +| `require('@bloxwap/hig-tokens/fonts/BloxwapSans.ttf')` | `require('@bloxwap/hig-native-fonts/fonts/BloxwapSans.ttf')` | +| `@bloxwap/hig-tokens/fonts/manifest.json` | `@bloxwap/hig-native-fonts/manifest`, or the typed `fontManifest` export | +| `@bloxwap/hig-tokens/fonts/OFL.txt` | `@bloxwap/hig-native-fonts/fonts/OFL.txt` | + +- **Expo / React Native:** add the `bloxwap-hig-native-fonts-*.tgz` tarball next to tokens, contracts and native (with a matching `overrides` entry when vendoring), then replace the import prefix. `nativeFont(fontManifest, 'BloxwapSans', 600)` from `@bloxwap/hig-contracts` returns the registration key. Hashes you already pinned stay valid. +- **Satori / OG images in web repositories:** these also read static TTFs (bloxwap/chart's OG card is one). Install `@bloxwap/hig-native-fonts` as a build-time dependency for that script only, or read the TTF from `@bloxwap/font` if it ships one. Never copy the faces into the repository. +- **Web-only hosts:** remove nothing. They keep loading fonts from `@bloxwap/font`, and their tokens tarball shrinks. `npm run release:dry-run` fails if any web or shared tarball contains `.ttf`, `.otf` or `.woff*` files, and it records the packed sizes in `packedSizes` in the release report. +- **Coordination:** monorepo#4540 loads the faces from `@bloxwap/hig-tokens`. Rebase it on a candidate that includes this change, and switch it to the new prefix when it does. + Rollback: host feature flag restores old renderer/backends and previous lockfile. Persisted business data and feedback choices retain old-compatible fields during staged rollout; avoid one-way preference deletion. Keep old static assets until both physical platforms pass. diff --git a/apps/docs/public/markdown/foundations/platform.md b/apps/docs/public/markdown/foundations/platform.md index 4fb9cb3..81c7b42 100644 --- a/apps/docs/public/markdown/foundations/platform.md +++ b/apps/docs/public/markdown/foundations/platform.md @@ -6,15 +6,15 @@ Dark is the only audited mode. Source values retain the commit-pinned monorepo a ## Typography (#7) -Pin @bloxwap/font 0.1.2. Vite: import `@bloxwap/font/css`; Next: upstream Sans/Mono loaders own loading and script fallback. Sans for prose/controls; Mono for code and tabular readouts; Pixel for a short nonessential accent. Body 16/24, label 14/20, caption 12/18, title 24/30, display 48/52 are candidate role mappings based on SDK scale. Web rem uses 16px root; RN sizes are explicit dp and allow font scaling. Do not disable Dynamic Type or fixed-height clip translated labels. Run the pinned native font generator and register its static TTF faces from the optional tokens/fonts export; the generated manifest records weight/family/source hashes. Published WOFF2/Next loaders are not directly native assets. **Do** wait for registration before applying a custom native family; **don't** invent a family string and claim the font rendered. Arabic/Hebrew/CJK companions are owned upstream; load required scripts, test mixed-script punctuation and RTL. Large-text and device font evidence is pending. +Pin @bloxwap/font 0.1.2. Vite: import `@bloxwap/font/css`; Next: upstream Sans/Mono loaders own loading and script fallback. Sans for prose/controls; Mono for code and tabular readouts; Pixel for a short nonessential accent. Body 16/24, label 14/20, caption 12/18, title 24/30, display 48/52 are candidate role mappings based on SDK scale. Web CSS exposes the roles as `--hig-type--size`, `-line-height`, `-weight` and a `--hig-type-` font shorthand, and the families as `--hig-font-sans`/`--hig-font-mono`. Those resolve the `@bloxwap/font` Next loader variables first and fall back to the literal `'Bloxwap Sans'`/`'Bloxwap Mono'` families, so HIG controls render the loaded face in both Next and plain `@font-face` hosts. Web rem uses 16px root; RN sizes are explicit dp and allow font scaling. Do not disable Dynamic Type or fixed-height clip translated labels. Run the pinned native font generator and register its static TTF faces from `@bloxwap/hig-native-fonts`; its typed manifest records weight/family/source hashes. Web hosts never install that package. Published WOFF2/Next loaders are not directly native assets. **Do** wait for registration before applying a custom native family; **don't** invent a family string and claim the font rendered. Arabic/Hebrew/CJK companions are owned upstream; load required scripts, test mixed-script punctuation and RTL. Large-text and device font evidence is pending. ## Geometry and layout (#8) -Spaces 4/8/12/16/20/24/32/48; radius 6/8/10/14 and pill. Comfortable control/target 48; compact 36, reserved for pointer/keyboard dense layouts. Painted icons can be 16/20/24 while targets stay 48 on touch. Never expand adjacent invisible hit areas so they overlap. Use logical inline spacing, wrapping labels, safe-area insets, keyboard avoidance and a scrollable native sheet; preserve portrait/tablet rotation without locking orientation. Pro tables may scroll with column headers; app amount entry reflows. Elevation uses nested fills rather than unverified shadows. Layer order: document, sticky nav, contextual popover, modal, notification. Browser top layer handles modal stacking; native Modal remains platform-owned. +Spaces 4/8/12/16/20/24/32/48; radius 6/8/10/14 and pill (`--hig-radius-sm`…`--hig-radius-xl`, `--hig-radius-pill`). Comfortable control/target 48 and gap 12; compact 36 and gap 8, reserved for pointer/keyboard dense layouts. CSS exposes each mode as `--hig-density--`, and `--hig-density-control`, `--hig-density-target` and `--hig-density-gap` follow the nearest `[data-hig-density]`, which the React provider sets. Painted icons can be 16/20/24 while targets stay 48 on touch. Never expand adjacent invisible hit areas so they overlap. Use logical inline spacing, wrapping labels, safe-area insets, keyboard avoidance and a scrollable native sheet; preserve portrait/tablet rotation without locking orientation. Pro tables may scroll with column headers; app amount entry reflows. Elevation uses nested fills rather than unverified shadows. Layer order: document, sticky nav, contextual popover, modal, notification. Browser top layer handles modal stacking; native Modal remains platform-owned. ## Motion and gestures (#9) -Fast 180ms, base 260ms, slow 320ms; web easing `cubic-bezier(.22,1,.36,1)`. Press response ends on release/cancel; action executes on accepted activation, never press-in. Drag/scroll cancellation clears visual and feedback state. Interrupt transitions from current state instead of queuing. Reduced motion uses immediate state changes, static skeletons and unanimated numeric readouts. Outcome messages remain explicit. **Do** animate deliberate disclosure once; **don't** animate or play sound for every live-data update. Native candidate uses static transitions until cancellation/device animation evidence exists; motion preference is independent from sound/haptics. +Fast 180ms, base 260ms, slow 320ms; web easing `cubic-bezier(.22,1,.36,1)`. CSS exposes `--hig-motion-duration-` and `--hig-motion-easing-standard`. The durations become 0ms under `prefers-reduced-motion: reduce` and inside `[data-hig-motion='reduced']`, which the React provider sets when the host or OS asks for reduced motion. Press response ends on release/cancel; action executes on accepted activation, never press-in. Drag/scroll cancellation clears visual and feedback state. Interrupt transitions from current state instead of queuing. Reduced motion uses immediate state changes, static skeletons and unanimated numeric readouts. Outcome messages remain explicit. **Do** animate deliberate disclosure once; **don't** animate or play sound for every live-data update. Native candidate uses static transitions until cancellation/device animation evidence exists; motion preference is independent from sound/haptics. ## Accessibility and localization (#10) diff --git a/apps/docs/public/markdown/foundations/source-reconciliation.md b/apps/docs/public/markdown/foundations/source-reconciliation.md index 795c55a..6f4f136 100644 --- a/apps/docs/public/markdown/foundations/source-reconciliation.md +++ b/apps/docs/public/markdown/foundations/source-reconciliation.md @@ -14,7 +14,7 @@ Source `spacing.ts` and `type.ts` explicitly set both rem roots to 16; native sp ## Native font generation and registration -Install `fonttools[woff]==4.61.1` and `brotli==1.2.0`, then run `npm run generate:fonts` before native consumer commands. `scripts/generate-native-fonts.py` derives static faces exclusively from the installed `@bloxwap/font@0.1.2` variable WOFF2 files, verifying all four input hashes before writing. It adapts the monorepo's generator and preserves its naming/weight metadata. The generated manifest contains input/output SHA256, PostScript names, Expo registration keys, weights, scripts and byte counts; OFL attribution ships beside the faces. Generated TTF files are build outputs, not per-repository committed binary copies. Local tarballs include them through `@bloxwap/hig-tokens/fonts/*`; consumers register only needed faces. +Install `fonttools[woff]==4.61.1` and `brotli==1.2.0`, then run `npm run generate:fonts` before native consumer commands. `scripts/generate-native-fonts.py` derives static faces exclusively from the installed `@bloxwap/font@0.1.2` variable WOFF2 files, verifying all four input hashes before writing. It adapts the monorepo's generator and preserves its naming/weight metadata. The generated manifest contains input/output SHA256, PostScript names, Expo registration keys, weights, scripts and byte counts; OFL attribution ships beside the faces. Generated TTF files are build outputs, not per-repository committed binary copies. Local tarballs include them through `@bloxwap/hig-native-fonts/fonts/*` (moved out of the web tokens package in #64); consumers register only needed faces. The Expo fixture registers Sans Regular/SemiBold and Mono Regular through `expo-font`, waits for loading, exposes failure visibly and passes explicit 400/600 family keys to the native provider. Text, headings and buttons select registered static weight families, avoiding an invalid variable family name or synthesized weight. Expo web uses feedback.web.ts browser adapters; Android/iOS retain the native backend. Korean/Simplified Chinese Regular/SemiBold faces can be registered by host locale policy; other scripts use an explicit host/system fallback. Pixel remains a scoped web accent, not a native body-font requirement. Font registration in Expo web and Metro builds does not certify native rasterization, native Dynamic Type, or multilingual physical-device layout. diff --git a/apps/docs/public/markdown/governance/new-repository.md b/apps/docs/public/markdown/governance/new-repository.md index fa07510..95ee497 100644 --- a/apps/docs/public/markdown/governance/new-repository.md +++ b/apps/docs/public/markdown/governance/new-repository.md @@ -9,7 +9,7 @@ Add or update the repository's row in `planning/adoption.json` (a PR to bloxwap/ | Applicability | Install | Never add | | --- | --- | --- | | Product runtime UI (web) | `@bloxwap/hig-tokens`, `-contracts`, `-react`; `@bloxwap/font` | Copied component folders or token CSS | -| Product runtime UI (React Native) | `@bloxwap/hig-tokens`, `-contracts`, `-native`; optional `-feedback` plus Expo peers | DOM renderer imports, font binaries outside the manifest | +| Product runtime UI (React Native) | `@bloxwap/hig-tokens`, `-contracts`, `-native`, `-native-fonts`; optional `-feedback` plus Expo peers | DOM renderer imports, font binaries outside the manifest | | Docs or marketing site | `@bloxwap/hig-tokens`, `-react` (CSS and the controls in use); `@bloxwap/font` | A local `.btn` layer, a palette copy, `vendor/hig` | | Brand or content only | Nothing at runtime; follow the principles, content and brand chapters | React, RN or audio dependencies | | No UI | Nothing; README and content conventions only | Any HIG runtime dependency | diff --git a/apps/docs/public/markdown/governance/quality.md b/apps/docs/public/markdown/governance/quality.md index d68b95d..f6a3add 100644 --- a/apps/docs/public/markdown/governance/quality.md +++ b/apps/docs/public/markdown/governance/quality.md @@ -13,8 +13,10 @@ Budgets are regression limits from the initial candidate measurements, not claim | Native Switch gzip (React/RN external) | 1,383 B; 1,849 B after #67 | 2,200 B | Raised for #67: the shared Text now carries the font-scale cap and Korean/Chinese script tracking, Switch the token-role track. ~19% headroom; verifies no Slider/SVG/overlay/feedback imports | | Platform-neutral feedback contract gzip | 962 B | 1,200 B | ~25% headroom without device engines | | Docs JS gzip, all lazy chunks | 204,877 B | 240,000 B | ~17% headroom; includes chart/code examples, not initial-load-only | -| Docs CSS | 17,419 B | 22,000 B | ~26% headroom for responsive/documentation states | +| Docs CSS | 110,618 B (with token roles, #66) | 115,000 B | ~4% headroom; raised from 110,000 B when `@bloxwap/hig-tokens/css` gained radius/type/motion/density/font roles | | Sans + Mono assets | 592,796 B | 600,000 B | Pins two approved faces; new script/face requires explicit review | +| Native fixture faces from `@bloxwap/hig-native-fonts` (Sans 400/600, Mono 400) | 1,278,880 B | 1,400,000 B | Only the three registered faces; optional CJK and other weights stay unloaded. Every generated face hash is checked | +| Font binaries in web/shared tarballs (tokens, contracts, react, feedback, native) | 0 files | 0 files | Native faces ship only in `@bloxwap/hig-native-fonts` (#64); `release:dry-run` fails otherwise and records `packedSizes` | | Native four-cue WAV assets | 103,618 B | 110,000 B | ~6% headroom; no full engine/catalog assets | The actual build report is `evidence/quality-report.json`. Bundles use explicit external React/RN peers, so figures measure HIG runtime contribution, not total installed/Metro app size. Production app/Pro/native before/after deltas require owning-repository builds and reviewed baselines; do not invent a percentage improvement. Export/dependency graph reports and packed tests enforce separation, while owning migrations must measure complete apps with equal data/toolchain. diff --git a/apps/docs/public/markdown/installation.md b/apps/docs/public/markdown/installation.md index 867e965..0a20afb 100644 --- a/apps/docs/public/markdown/installation.md +++ b/apps/docs/public/markdown/installation.md @@ -4,11 +4,13 @@ These are review candidates, not published npm packages. Use local dry-run tarba ## React / Vite -Run `npm ci && npm run build && npm run smoke:packed`. The smoke script installs tarballs into an independent temporary consumer and builds the fixture. For a host app install tokens, contracts and React tarballs plus React/DOM 19.2. Import `@bloxwap/hig-tokens/css` and `@bloxwap/hig-react/css` once; import individual components such as `@bloxwap/hig-react/button`. Pin @bloxwap/font 0.1.2 and choose only the required families/scripts using asset URL @font-face rules. The included fixture has Sans and Mono only. Never copy font binaries or component folders. +Run `npm ci && npm run build && npm run smoke:packed`. The smoke script installs tarballs into an independent temporary consumer and builds the fixture. For a host app install tokens, contracts and React tarballs plus React/DOM 19.2. Import `@bloxwap/hig-tokens/css` and `@bloxwap/hig-react/css` once; import individual components such as `@bloxwap/hig-react/button`. Pin @bloxwap/font 0.1.2 and choose only the required families/scripts using asset URL @font-face rules named `'Bloxwap Sans'` and `'Bloxwap Mono'`; HIG controls read `--hig-font-sans`/`--hig-font-mono`, which fall back to those literal families. The included fixture has Sans and Mono only, and its browser smoke checks that `.hig-button` and `.hig-number` render loaded Bloxwap faces. Never copy font binaries or component folders. ## SSR and Next client boundaries -Run `npm run smoke:ssr`, `npm run build:next` and `npm run smoke:packed-next`. Token/formatting imports and button/provider SSR are deterministic. Components with hooks carry `use client` in their emitted modules. In Next, import HIG CSS in the app layout and put interactive example state in a client component; keep wallet/network logic with the host. Next's published Bloxwap font loaders remain owned upstream. Next 16.4.0 (webpack), React 19.2 and the published Bloxwap Sans Next loader are verified in the dedicated production fixture; other Next versions require host checks. Verify hydration, stable IDs and StrictMode before adopting a host version. +Run `npm run smoke:ssr`, `npm run build:next` and `npm run smoke:packed-next`. Token/formatting imports and button/provider SSR are deterministic. Components with hooks carry `use client` in their emitted modules. In Next, import HIG CSS in the app layout and put interactive example state in a client component; keep wallet/network logic with the host. Next's published Bloxwap font loaders remain owned upstream. + +Fonts in Next: put the `@bloxwap/font` loader variables on ``, for example ``. The loaders register hashed family names, so HIG never matches a literal `'Bloxwap Sans'` there. Instead `--hig-font-sans` and `--hig-font-mono` in `@bloxwap/hig-tokens/css` resolve `var(--font-bloxwap-sans)` and `var(--font-bloxwap-mono)` first and fall back to the literal families. No bridge CSS is needed. If a host keeps its own variable names, override the roles on `:root` after importing the tokens CSS: `:root { --hig-font-sans: var(--my-sans), 'Bloxwap Sans', system-ui, sans-serif; }`. The variables must be defined on `` (not ``), because the roles resolve on `:root`. `npm run smoke:packed-next` serves the packed consumer and checks, in Chromium, that `.hig-button` and `.hig-number` compute to a loaded Bloxwap face (`document.fonts.check` plus the `FontFace` status). Next 16.4.0 (webpack), React 19.2 and the published Bloxwap Sans Next loader are verified in the dedicated production fixture; other Next versions require host checks. Verify hydration, stable IDs and StrictMode before adopting a host version. ## Expo / React Native @@ -20,11 +22,30 @@ Install Expo native peers with `npx expo install` (for example `expo-asset ~12.0 Simulator/emulator verification: `scripts/native-sim/` builds the packed candidate gallery as real Release apps and drives them with XCUITest (iOS Simulator) and UiAutomation instrumentation, uiautomator and TalkBack (Android emulator). Results and residual physical-hardware limits are in `evidence/native-sim/`. -WOFF2 is not a native font asset. `@bloxwap/hig-tokens/fonts/*` ships 15 static Bloxwap Sans/Mono TTFs generated reproducibly from pinned Font 0.1.2 sources, plus a typed FontManifest. Register them with expo-font before setting the native provider fontFamily, as the gallery does. Rasterization on physical displays remains part of #55. Moving these faces out of the web tokens package is tracked in #64. New repositories follow `docs/governance/new-repository.md`. +WOFF2 is not a native font asset. `@bloxwap/hig-native-fonts` ships 15 static Bloxwap Sans/Mono (plus Korean and Simplified Chinese Sans) TTFs generated reproducibly from pinned Font 0.1.2 sources, with a typed `fontManifest` export and `@bloxwap/hig-native-fonts/manifest`. Install it alongside the tokens, contracts and native tarballs, then `require('@bloxwap/hig-native-fonts/fonts/BloxwapSans.ttf')` and register the faces with expo-font before setting the native provider fontFamily, as the gallery does. `@bloxwap/hig-tokens` no longer contains font binaries (#64); see [native font migration](migration/native.md#native-fonts-moved-out-of-hig-tokens). Rasterization on physical displays remains part of #55. New repositories follow `docs/governance/new-repository.md`. ## Tokens only / plain CSS / Tailwind -Install the tokens tarball alone and import CSS variables or typed tokens. No React/RN/audio peer is required. Plain HTML uses semantic `--hig-*` roles. Tailwind may reference these values through host configuration; HIG does not require content scans or a shadcn registry. Native uses dp and explicit hex/channel maps; do not feed CSS functions or rename rem to dp. The audited monorepo web/native rem roots are both 16; HIG supports StyleSheet rather than claiming a verified NativeWind consumer. See the source-reconciliation chapter for native font generation and registration. +Install the tokens tarball alone and import CSS variables or typed tokens. No React/RN/audio peer is required. Plain HTML uses semantic `--hig-*` roles. + +`@bloxwap/hig-tokens/css` defines every role a plain CSS host needs, so nobody has to read the manifest or generate role files: + +| Roles | Example | Values | +| --- | --- | --- | +| Color | `--hig-surface`, `--hig-action` | sRGB, then OKLCH/P3 in `@supports` | +| Space | `--hig-space-md` | `xs`–`xxxl`, `section`, in rem | +| Font | `--hig-font-sans`, `--hig-font-mono` | `@bloxwap/font` Next variable, then literal family | +| Radius | `--hig-radius-md`, `--hig-radius-pill` | `sm` `md` `lg` `xl` in rem; `pill` 9999px | +| Type | `--hig-type-body-size`, `--hig-type-body-line-height`, `--hig-type-body-weight`, `--hig-type-body` | `caption` `label` `body` `title` `display`; the bare role is a `font` shorthand in Sans | +| Motion | `--hig-motion-duration-base`, `--hig-motion-easing-standard` | `fast` `base` `slow` ms; 0ms under `prefers-reduced-motion: reduce` or `[data-hig-motion='reduced']` | +| Density | `--hig-density-control`, `--hig-density-compact-gap` | `control` `target` `gap` per mode; the unprefixed roles follow `[data-hig-density='compact']` | + +```css +@import '@bloxwap/hig-tokens/css'; +.card { border-radius: var(--hig-radius-lg); padding: var(--hig-space-lg); font: var(--hig-type-body); } +.card h2 { font: var(--hig-type-title); } +.card button { min-height: var(--hig-density-control); transition: background var(--hig-motion-duration-fast) var(--hig-motion-easing-standard); } +``` Tailwind may reference these values through host configuration; HIG does not require content scans or a shadcn registry. Native uses dp and explicit hex/channel maps; do not feed CSS functions or rename rem to dp. The audited monorepo web/native rem roots are both 16; HIG supports StyleSheet rather than claiming a verified NativeWind consumer. See the source-reconciliation chapter for native font generation and registration. ## Optional chart, sound and haptics diff --git a/apps/docs/public/markdown/migration/native.md b/apps/docs/public/markdown/migration/native.md index 913f53d..4265712 100644 --- a/apps/docs/public/markdown/migration/native.md +++ b/apps/docs/public/markdown/migration/native.md @@ -6,7 +6,7 @@ Pilot: font registration → dp/sRGB token mapping → amount/selection/review s Checklist: - [ ] Record actual RN/Expo/Metro/autolinking versions, source commit, old ui-native/token/font/audio wrappers, screenshots and native bundle/assets/latency baseline. -- [ ] Obtain approved pinned static TTF faces and upstream manifest or supply reviewed typed FontManifest; register before setting family. Do not treat WOFF2 or an invented native family as success. +- [ ] Register the pinned static TTF faces from `@bloxwap/hig-native-fonts` and its typed `fontManifest`; register before setting family. Do not treat WOFF2 or an invented native family as success. - [ ] Map dp and legacy NativeWind rem roots explicitly. Keep Dynamic Type, 48 targets, logical RTL layout, safe areas, keyboard avoidance, native back and labelled fields/errors. - [ ] Preview native import codemods, using individual switch/checkbox/range entries; wire optional SVG/Slider/Expo audio/haptic peers only where used. Retain platform-specific descriptions and host callbacks. - [ ] Preserve raw financial strings, order/network logic, stable list identity and quiet live numbers. Use labelled list/readout native chart alternative unless a real supported native engine is reviewed. @@ -17,4 +17,19 @@ Checklist: - [ ] Remove duplicated source/assets/backend wrappers after device builds confirm references and autolinking are gone; quantify before/after sizes with equal toolchain/data. - [ ] Link owning rollout PR and remaining version/exception work into the matrix. +## Native fonts moved out of hig-tokens + +Issue #64. Up to `0.1.0-candidate.sha7c67a07c`, `@bloxwap/hig-tokens` shipped the 15 static TTFs under `@bloxwap/hig-tokens/fonts/*`. Web-only consumers never served them, but they made up most of the tarball. They now live in `@bloxwap/hig-native-fonts`. The bytes, file names, registration keys and SHA-256 hashes are unchanged. + +| Before | After | +| --- | --- | +| `require('@bloxwap/hig-tokens/fonts/BloxwapSans.ttf')` | `require('@bloxwap/hig-native-fonts/fonts/BloxwapSans.ttf')` | +| `@bloxwap/hig-tokens/fonts/manifest.json` | `@bloxwap/hig-native-fonts/manifest`, or the typed `fontManifest` export | +| `@bloxwap/hig-tokens/fonts/OFL.txt` | `@bloxwap/hig-native-fonts/fonts/OFL.txt` | + +- **Expo / React Native:** add the `bloxwap-hig-native-fonts-*.tgz` tarball next to tokens, contracts and native (with a matching `overrides` entry when vendoring), then replace the import prefix. `nativeFont(fontManifest, 'BloxwapSans', 600)` from `@bloxwap/hig-contracts` returns the registration key. Hashes you already pinned stay valid. +- **Satori / OG images in web repositories:** these also read static TTFs (bloxwap/chart's OG card is one). Install `@bloxwap/hig-native-fonts` as a build-time dependency for that script only, or read the TTF from `@bloxwap/font` if it ships one. Never copy the faces into the repository. +- **Web-only hosts:** remove nothing. They keep loading fonts from `@bloxwap/font`, and their tokens tarball shrinks. `npm run release:dry-run` fails if any web or shared tarball contains `.ttf`, `.otf` or `.woff*` files, and it records the packed sizes in `packedSizes` in the release report. +- **Coordination:** monorepo#4540 loads the faces from `@bloxwap/hig-tokens`. Rebase it on a candidate that includes this change, and switch it to the new prefix when it does. + Rollback: host feature flag restores old renderer/backends and previous lockfile. Persisted business data and feedback choices retain old-compatible fields during staged rollout; avoid one-way preference deletion. Keep old static assets until both physical platforms pass. diff --git a/docs/foundations/platform.md b/docs/foundations/platform.md index 4fb9cb3..81c7b42 100644 --- a/docs/foundations/platform.md +++ b/docs/foundations/platform.md @@ -6,15 +6,15 @@ Dark is the only audited mode. Source values retain the commit-pinned monorepo a ## Typography (#7) -Pin @bloxwap/font 0.1.2. Vite: import `@bloxwap/font/css`; Next: upstream Sans/Mono loaders own loading and script fallback. Sans for prose/controls; Mono for code and tabular readouts; Pixel for a short nonessential accent. Body 16/24, label 14/20, caption 12/18, title 24/30, display 48/52 are candidate role mappings based on SDK scale. Web rem uses 16px root; RN sizes are explicit dp and allow font scaling. Do not disable Dynamic Type or fixed-height clip translated labels. Run the pinned native font generator and register its static TTF faces from the optional tokens/fonts export; the generated manifest records weight/family/source hashes. Published WOFF2/Next loaders are not directly native assets. **Do** wait for registration before applying a custom native family; **don't** invent a family string and claim the font rendered. Arabic/Hebrew/CJK companions are owned upstream; load required scripts, test mixed-script punctuation and RTL. Large-text and device font evidence is pending. +Pin @bloxwap/font 0.1.2. Vite: import `@bloxwap/font/css`; Next: upstream Sans/Mono loaders own loading and script fallback. Sans for prose/controls; Mono for code and tabular readouts; Pixel for a short nonessential accent. Body 16/24, label 14/20, caption 12/18, title 24/30, display 48/52 are candidate role mappings based on SDK scale. Web CSS exposes the roles as `--hig-type--size`, `-line-height`, `-weight` and a `--hig-type-` font shorthand, and the families as `--hig-font-sans`/`--hig-font-mono`. Those resolve the `@bloxwap/font` Next loader variables first and fall back to the literal `'Bloxwap Sans'`/`'Bloxwap Mono'` families, so HIG controls render the loaded face in both Next and plain `@font-face` hosts. Web rem uses 16px root; RN sizes are explicit dp and allow font scaling. Do not disable Dynamic Type or fixed-height clip translated labels. Run the pinned native font generator and register its static TTF faces from `@bloxwap/hig-native-fonts`; its typed manifest records weight/family/source hashes. Web hosts never install that package. Published WOFF2/Next loaders are not directly native assets. **Do** wait for registration before applying a custom native family; **don't** invent a family string and claim the font rendered. Arabic/Hebrew/CJK companions are owned upstream; load required scripts, test mixed-script punctuation and RTL. Large-text and device font evidence is pending. ## Geometry and layout (#8) -Spaces 4/8/12/16/20/24/32/48; radius 6/8/10/14 and pill. Comfortable control/target 48; compact 36, reserved for pointer/keyboard dense layouts. Painted icons can be 16/20/24 while targets stay 48 on touch. Never expand adjacent invisible hit areas so they overlap. Use logical inline spacing, wrapping labels, safe-area insets, keyboard avoidance and a scrollable native sheet; preserve portrait/tablet rotation without locking orientation. Pro tables may scroll with column headers; app amount entry reflows. Elevation uses nested fills rather than unverified shadows. Layer order: document, sticky nav, contextual popover, modal, notification. Browser top layer handles modal stacking; native Modal remains platform-owned. +Spaces 4/8/12/16/20/24/32/48; radius 6/8/10/14 and pill (`--hig-radius-sm`…`--hig-radius-xl`, `--hig-radius-pill`). Comfortable control/target 48 and gap 12; compact 36 and gap 8, reserved for pointer/keyboard dense layouts. CSS exposes each mode as `--hig-density--`, and `--hig-density-control`, `--hig-density-target` and `--hig-density-gap` follow the nearest `[data-hig-density]`, which the React provider sets. Painted icons can be 16/20/24 while targets stay 48 on touch. Never expand adjacent invisible hit areas so they overlap. Use logical inline spacing, wrapping labels, safe-area insets, keyboard avoidance and a scrollable native sheet; preserve portrait/tablet rotation without locking orientation. Pro tables may scroll with column headers; app amount entry reflows. Elevation uses nested fills rather than unverified shadows. Layer order: document, sticky nav, contextual popover, modal, notification. Browser top layer handles modal stacking; native Modal remains platform-owned. ## Motion and gestures (#9) -Fast 180ms, base 260ms, slow 320ms; web easing `cubic-bezier(.22,1,.36,1)`. Press response ends on release/cancel; action executes on accepted activation, never press-in. Drag/scroll cancellation clears visual and feedback state. Interrupt transitions from current state instead of queuing. Reduced motion uses immediate state changes, static skeletons and unanimated numeric readouts. Outcome messages remain explicit. **Do** animate deliberate disclosure once; **don't** animate or play sound for every live-data update. Native candidate uses static transitions until cancellation/device animation evidence exists; motion preference is independent from sound/haptics. +Fast 180ms, base 260ms, slow 320ms; web easing `cubic-bezier(.22,1,.36,1)`. CSS exposes `--hig-motion-duration-` and `--hig-motion-easing-standard`. The durations become 0ms under `prefers-reduced-motion: reduce` and inside `[data-hig-motion='reduced']`, which the React provider sets when the host or OS asks for reduced motion. Press response ends on release/cancel; action executes on accepted activation, never press-in. Drag/scroll cancellation clears visual and feedback state. Interrupt transitions from current state instead of queuing. Reduced motion uses immediate state changes, static skeletons and unanimated numeric readouts. Outcome messages remain explicit. **Do** animate deliberate disclosure once; **don't** animate or play sound for every live-data update. Native candidate uses static transitions until cancellation/device animation evidence exists; motion preference is independent from sound/haptics. ## Accessibility and localization (#10) diff --git a/docs/foundations/source-reconciliation.md b/docs/foundations/source-reconciliation.md index 795c55a..6f4f136 100644 --- a/docs/foundations/source-reconciliation.md +++ b/docs/foundations/source-reconciliation.md @@ -14,7 +14,7 @@ Source `spacing.ts` and `type.ts` explicitly set both rem roots to 16; native sp ## Native font generation and registration -Install `fonttools[woff]==4.61.1` and `brotli==1.2.0`, then run `npm run generate:fonts` before native consumer commands. `scripts/generate-native-fonts.py` derives static faces exclusively from the installed `@bloxwap/font@0.1.2` variable WOFF2 files, verifying all four input hashes before writing. It adapts the monorepo's generator and preserves its naming/weight metadata. The generated manifest contains input/output SHA256, PostScript names, Expo registration keys, weights, scripts and byte counts; OFL attribution ships beside the faces. Generated TTF files are build outputs, not per-repository committed binary copies. Local tarballs include them through `@bloxwap/hig-tokens/fonts/*`; consumers register only needed faces. +Install `fonttools[woff]==4.61.1` and `brotli==1.2.0`, then run `npm run generate:fonts` before native consumer commands. `scripts/generate-native-fonts.py` derives static faces exclusively from the installed `@bloxwap/font@0.1.2` variable WOFF2 files, verifying all four input hashes before writing. It adapts the monorepo's generator and preserves its naming/weight metadata. The generated manifest contains input/output SHA256, PostScript names, Expo registration keys, weights, scripts and byte counts; OFL attribution ships beside the faces. Generated TTF files are build outputs, not per-repository committed binary copies. Local tarballs include them through `@bloxwap/hig-native-fonts/fonts/*` (moved out of the web tokens package in #64); consumers register only needed faces. The Expo fixture registers Sans Regular/SemiBold and Mono Regular through `expo-font`, waits for loading, exposes failure visibly and passes explicit 400/600 family keys to the native provider. Text, headings and buttons select registered static weight families, avoiding an invalid variable family name or synthesized weight. Expo web uses feedback.web.ts browser adapters; Android/iOS retain the native backend. Korean/Simplified Chinese Regular/SemiBold faces can be registered by host locale policy; other scripts use an explicit host/system fallback. Pixel remains a scoped web accent, not a native body-font requirement. Font registration in Expo web and Metro builds does not certify native rasterization, native Dynamic Type, or multilingual physical-device layout. diff --git a/docs/governance/new-repository.md b/docs/governance/new-repository.md index fa07510..95ee497 100644 --- a/docs/governance/new-repository.md +++ b/docs/governance/new-repository.md @@ -9,7 +9,7 @@ Add or update the repository's row in `planning/adoption.json` (a PR to bloxwap/ | Applicability | Install | Never add | | --- | --- | --- | | Product runtime UI (web) | `@bloxwap/hig-tokens`, `-contracts`, `-react`; `@bloxwap/font` | Copied component folders or token CSS | -| Product runtime UI (React Native) | `@bloxwap/hig-tokens`, `-contracts`, `-native`; optional `-feedback` plus Expo peers | DOM renderer imports, font binaries outside the manifest | +| Product runtime UI (React Native) | `@bloxwap/hig-tokens`, `-contracts`, `-native`, `-native-fonts`; optional `-feedback` plus Expo peers | DOM renderer imports, font binaries outside the manifest | | Docs or marketing site | `@bloxwap/hig-tokens`, `-react` (CSS and the controls in use); `@bloxwap/font` | A local `.btn` layer, a palette copy, `vendor/hig` | | Brand or content only | Nothing at runtime; follow the principles, content and brand chapters | React, RN or audio dependencies | | No UI | Nothing; README and content conventions only | Any HIG runtime dependency | diff --git a/docs/governance/quality.md b/docs/governance/quality.md index d68b95d..f6a3add 100644 --- a/docs/governance/quality.md +++ b/docs/governance/quality.md @@ -13,8 +13,10 @@ Budgets are regression limits from the initial candidate measurements, not claim | Native Switch gzip (React/RN external) | 1,383 B; 1,849 B after #67 | 2,200 B | Raised for #67: the shared Text now carries the font-scale cap and Korean/Chinese script tracking, Switch the token-role track. ~19% headroom; verifies no Slider/SVG/overlay/feedback imports | | Platform-neutral feedback contract gzip | 962 B | 1,200 B | ~25% headroom without device engines | | Docs JS gzip, all lazy chunks | 204,877 B | 240,000 B | ~17% headroom; includes chart/code examples, not initial-load-only | -| Docs CSS | 17,419 B | 22,000 B | ~26% headroom for responsive/documentation states | +| Docs CSS | 110,618 B (with token roles, #66) | 115,000 B | ~4% headroom; raised from 110,000 B when `@bloxwap/hig-tokens/css` gained radius/type/motion/density/font roles | | Sans + Mono assets | 592,796 B | 600,000 B | Pins two approved faces; new script/face requires explicit review | +| Native fixture faces from `@bloxwap/hig-native-fonts` (Sans 400/600, Mono 400) | 1,278,880 B | 1,400,000 B | Only the three registered faces; optional CJK and other weights stay unloaded. Every generated face hash is checked | +| Font binaries in web/shared tarballs (tokens, contracts, react, feedback, native) | 0 files | 0 files | Native faces ship only in `@bloxwap/hig-native-fonts` (#64); `release:dry-run` fails otherwise and records `packedSizes` | | Native four-cue WAV assets | 103,618 B | 110,000 B | ~6% headroom; no full engine/catalog assets | The actual build report is `evidence/quality-report.json`. Bundles use explicit external React/RN peers, so figures measure HIG runtime contribution, not total installed/Metro app size. Production app/Pro/native before/after deltas require owning-repository builds and reviewed baselines; do not invent a percentage improvement. Export/dependency graph reports and packed tests enforce separation, while owning migrations must measure complete apps with equal data/toolchain. diff --git a/docs/installation.md b/docs/installation.md index 867e965..0a20afb 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -4,11 +4,13 @@ These are review candidates, not published npm packages. Use local dry-run tarba ## React / Vite -Run `npm ci && npm run build && npm run smoke:packed`. The smoke script installs tarballs into an independent temporary consumer and builds the fixture. For a host app install tokens, contracts and React tarballs plus React/DOM 19.2. Import `@bloxwap/hig-tokens/css` and `@bloxwap/hig-react/css` once; import individual components such as `@bloxwap/hig-react/button`. Pin @bloxwap/font 0.1.2 and choose only the required families/scripts using asset URL @font-face rules. The included fixture has Sans and Mono only. Never copy font binaries or component folders. +Run `npm ci && npm run build && npm run smoke:packed`. The smoke script installs tarballs into an independent temporary consumer and builds the fixture. For a host app install tokens, contracts and React tarballs plus React/DOM 19.2. Import `@bloxwap/hig-tokens/css` and `@bloxwap/hig-react/css` once; import individual components such as `@bloxwap/hig-react/button`. Pin @bloxwap/font 0.1.2 and choose only the required families/scripts using asset URL @font-face rules named `'Bloxwap Sans'` and `'Bloxwap Mono'`; HIG controls read `--hig-font-sans`/`--hig-font-mono`, which fall back to those literal families. The included fixture has Sans and Mono only, and its browser smoke checks that `.hig-button` and `.hig-number` render loaded Bloxwap faces. Never copy font binaries or component folders. ## SSR and Next client boundaries -Run `npm run smoke:ssr`, `npm run build:next` and `npm run smoke:packed-next`. Token/formatting imports and button/provider SSR are deterministic. Components with hooks carry `use client` in their emitted modules. In Next, import HIG CSS in the app layout and put interactive example state in a client component; keep wallet/network logic with the host. Next's published Bloxwap font loaders remain owned upstream. Next 16.4.0 (webpack), React 19.2 and the published Bloxwap Sans Next loader are verified in the dedicated production fixture; other Next versions require host checks. Verify hydration, stable IDs and StrictMode before adopting a host version. +Run `npm run smoke:ssr`, `npm run build:next` and `npm run smoke:packed-next`. Token/formatting imports and button/provider SSR are deterministic. Components with hooks carry `use client` in their emitted modules. In Next, import HIG CSS in the app layout and put interactive example state in a client component; keep wallet/network logic with the host. Next's published Bloxwap font loaders remain owned upstream. + +Fonts in Next: put the `@bloxwap/font` loader variables on ``, for example ``. The loaders register hashed family names, so HIG never matches a literal `'Bloxwap Sans'` there. Instead `--hig-font-sans` and `--hig-font-mono` in `@bloxwap/hig-tokens/css` resolve `var(--font-bloxwap-sans)` and `var(--font-bloxwap-mono)` first and fall back to the literal families. No bridge CSS is needed. If a host keeps its own variable names, override the roles on `:root` after importing the tokens CSS: `:root { --hig-font-sans: var(--my-sans), 'Bloxwap Sans', system-ui, sans-serif; }`. The variables must be defined on `` (not ``), because the roles resolve on `:root`. `npm run smoke:packed-next` serves the packed consumer and checks, in Chromium, that `.hig-button` and `.hig-number` compute to a loaded Bloxwap face (`document.fonts.check` plus the `FontFace` status). Next 16.4.0 (webpack), React 19.2 and the published Bloxwap Sans Next loader are verified in the dedicated production fixture; other Next versions require host checks. Verify hydration, stable IDs and StrictMode before adopting a host version. ## Expo / React Native @@ -20,11 +22,30 @@ Install Expo native peers with `npx expo install` (for example `expo-asset ~12.0 Simulator/emulator verification: `scripts/native-sim/` builds the packed candidate gallery as real Release apps and drives them with XCUITest (iOS Simulator) and UiAutomation instrumentation, uiautomator and TalkBack (Android emulator). Results and residual physical-hardware limits are in `evidence/native-sim/`. -WOFF2 is not a native font asset. `@bloxwap/hig-tokens/fonts/*` ships 15 static Bloxwap Sans/Mono TTFs generated reproducibly from pinned Font 0.1.2 sources, plus a typed FontManifest. Register them with expo-font before setting the native provider fontFamily, as the gallery does. Rasterization on physical displays remains part of #55. Moving these faces out of the web tokens package is tracked in #64. New repositories follow `docs/governance/new-repository.md`. +WOFF2 is not a native font asset. `@bloxwap/hig-native-fonts` ships 15 static Bloxwap Sans/Mono (plus Korean and Simplified Chinese Sans) TTFs generated reproducibly from pinned Font 0.1.2 sources, with a typed `fontManifest` export and `@bloxwap/hig-native-fonts/manifest`. Install it alongside the tokens, contracts and native tarballs, then `require('@bloxwap/hig-native-fonts/fonts/BloxwapSans.ttf')` and register the faces with expo-font before setting the native provider fontFamily, as the gallery does. `@bloxwap/hig-tokens` no longer contains font binaries (#64); see [native font migration](migration/native.md#native-fonts-moved-out-of-hig-tokens). Rasterization on physical displays remains part of #55. New repositories follow `docs/governance/new-repository.md`. ## Tokens only / plain CSS / Tailwind -Install the tokens tarball alone and import CSS variables or typed tokens. No React/RN/audio peer is required. Plain HTML uses semantic `--hig-*` roles. Tailwind may reference these values through host configuration; HIG does not require content scans or a shadcn registry. Native uses dp and explicit hex/channel maps; do not feed CSS functions or rename rem to dp. The audited monorepo web/native rem roots are both 16; HIG supports StyleSheet rather than claiming a verified NativeWind consumer. See the source-reconciliation chapter for native font generation and registration. +Install the tokens tarball alone and import CSS variables or typed tokens. No React/RN/audio peer is required. Plain HTML uses semantic `--hig-*` roles. + +`@bloxwap/hig-tokens/css` defines every role a plain CSS host needs, so nobody has to read the manifest or generate role files: + +| Roles | Example | Values | +| --- | --- | --- | +| Color | `--hig-surface`, `--hig-action` | sRGB, then OKLCH/P3 in `@supports` | +| Space | `--hig-space-md` | `xs`–`xxxl`, `section`, in rem | +| Font | `--hig-font-sans`, `--hig-font-mono` | `@bloxwap/font` Next variable, then literal family | +| Radius | `--hig-radius-md`, `--hig-radius-pill` | `sm` `md` `lg` `xl` in rem; `pill` 9999px | +| Type | `--hig-type-body-size`, `--hig-type-body-line-height`, `--hig-type-body-weight`, `--hig-type-body` | `caption` `label` `body` `title` `display`; the bare role is a `font` shorthand in Sans | +| Motion | `--hig-motion-duration-base`, `--hig-motion-easing-standard` | `fast` `base` `slow` ms; 0ms under `prefers-reduced-motion: reduce` or `[data-hig-motion='reduced']` | +| Density | `--hig-density-control`, `--hig-density-compact-gap` | `control` `target` `gap` per mode; the unprefixed roles follow `[data-hig-density='compact']` | + +```css +@import '@bloxwap/hig-tokens/css'; +.card { border-radius: var(--hig-radius-lg); padding: var(--hig-space-lg); font: var(--hig-type-body); } +.card h2 { font: var(--hig-type-title); } +.card button { min-height: var(--hig-density-control); transition: background var(--hig-motion-duration-fast) var(--hig-motion-easing-standard); } +``` Tailwind may reference these values through host configuration; HIG does not require content scans or a shadcn registry. Native uses dp and explicit hex/channel maps; do not feed CSS functions or rename rem to dp. The audited monorepo web/native rem roots are both 16; HIG supports StyleSheet rather than claiming a verified NativeWind consumer. See the source-reconciliation chapter for native font generation and registration. ## Optional chart, sound and haptics diff --git a/docs/migration/native.md b/docs/migration/native.md index 913f53d..4265712 100644 --- a/docs/migration/native.md +++ b/docs/migration/native.md @@ -6,7 +6,7 @@ Pilot: font registration → dp/sRGB token mapping → amount/selection/review s Checklist: - [ ] Record actual RN/Expo/Metro/autolinking versions, source commit, old ui-native/token/font/audio wrappers, screenshots and native bundle/assets/latency baseline. -- [ ] Obtain approved pinned static TTF faces and upstream manifest or supply reviewed typed FontManifest; register before setting family. Do not treat WOFF2 or an invented native family as success. +- [ ] Register the pinned static TTF faces from `@bloxwap/hig-native-fonts` and its typed `fontManifest`; register before setting family. Do not treat WOFF2 or an invented native family as success. - [ ] Map dp and legacy NativeWind rem roots explicitly. Keep Dynamic Type, 48 targets, logical RTL layout, safe areas, keyboard avoidance, native back and labelled fields/errors. - [ ] Preview native import codemods, using individual switch/checkbox/range entries; wire optional SVG/Slider/Expo audio/haptic peers only where used. Retain platform-specific descriptions and host callbacks. - [ ] Preserve raw financial strings, order/network logic, stable list identity and quiet live numbers. Use labelled list/readout native chart alternative unless a real supported native engine is reviewed. @@ -17,4 +17,19 @@ Checklist: - [ ] Remove duplicated source/assets/backend wrappers after device builds confirm references and autolinking are gone; quantify before/after sizes with equal toolchain/data. - [ ] Link owning rollout PR and remaining version/exception work into the matrix. +## Native fonts moved out of hig-tokens + +Issue #64. Up to `0.1.0-candidate.sha7c67a07c`, `@bloxwap/hig-tokens` shipped the 15 static TTFs under `@bloxwap/hig-tokens/fonts/*`. Web-only consumers never served them, but they made up most of the tarball. They now live in `@bloxwap/hig-native-fonts`. The bytes, file names, registration keys and SHA-256 hashes are unchanged. + +| Before | After | +| --- | --- | +| `require('@bloxwap/hig-tokens/fonts/BloxwapSans.ttf')` | `require('@bloxwap/hig-native-fonts/fonts/BloxwapSans.ttf')` | +| `@bloxwap/hig-tokens/fonts/manifest.json` | `@bloxwap/hig-native-fonts/manifest`, or the typed `fontManifest` export | +| `@bloxwap/hig-tokens/fonts/OFL.txt` | `@bloxwap/hig-native-fonts/fonts/OFL.txt` | + +- **Expo / React Native:** add the `bloxwap-hig-native-fonts-*.tgz` tarball next to tokens, contracts and native (with a matching `overrides` entry when vendoring), then replace the import prefix. `nativeFont(fontManifest, 'BloxwapSans', 600)` from `@bloxwap/hig-contracts` returns the registration key. Hashes you already pinned stay valid. +- **Satori / OG images in web repositories:** these also read static TTFs (bloxwap/chart's OG card is one). Install `@bloxwap/hig-native-fonts` as a build-time dependency for that script only, or read the TTF from `@bloxwap/font` if it ships one. Never copy the faces into the repository. +- **Web-only hosts:** remove nothing. They keep loading fonts from `@bloxwap/font`, and their tokens tarball shrinks. `npm run release:dry-run` fails if any web or shared tarball contains `.ttf`, `.otf` or `.woff*` files, and it records the packed sizes in `packedSizes` in the release report. +- **Coordination:** monorepo#4540 loads the faces from `@bloxwap/hig-tokens`. Rebase it on a candidate that includes this change, and switch it to the new prefix when it does. + Rollback: host feature flag restores old renderer/backends and previous lockfile. Persisted business data and feedback choices retain old-compatible fields during staged rollout; avoid one-way preference deletion. Keep old static assets until both physical platforms pass. diff --git a/fixtures/native/App.tsx b/fixtures/native/App.tsx index 883bebc..0c787ce 100644 --- a/fixtures/native/App.tsx +++ b/fixtures/native/App.tsx @@ -43,4 +43,4 @@ function Gallery(){ const storage=jsonStorage(AsyncStorage); // Metro asset dependencies must be statically analyzable. // eslint-disable-next-line @typescript-eslint/no-require-imports -export default function App(){const [loaded,error]=useFonts({BloxwapSansRegular:require('@bloxwap/hig-tokens/fonts/BloxwapSans.ttf'),BloxwapSansSemiBold:require('@bloxwap/hig-tokens/fonts/BloxwapSans-SemiBold.ttf'),BloxwapMonoRegular:require('@bloxwap/hig-tokens/fonts/BloxwapMono.ttf')});if(error)return Font loading failed: {error.message};if(!loaded)return Loading Bloxwap fonts…;return ;} +export default function App(){const [loaded,error]=useFonts({BloxwapSansRegular:require('@bloxwap/hig-native-fonts/fonts/BloxwapSans.ttf'),BloxwapSansSemiBold:require('@bloxwap/hig-native-fonts/fonts/BloxwapSans-SemiBold.ttf'),BloxwapMonoRegular:require('@bloxwap/hig-native-fonts/fonts/BloxwapMono.ttf')});if(error)return Font loading failed: {error.message};if(!loaded)return Loading Bloxwap fonts…;return ;} diff --git a/fixtures/native/gesture-sheet.tsx b/fixtures/native/gesture-sheet.tsx index 707df86..3b35274 100644 --- a/fixtures/native/gesture-sheet.tsx +++ b/fixtures/native/gesture-sheet.tsx @@ -13,7 +13,7 @@ import {Text} from '@bloxwap/hig-native/surface'; export default function GestureSheetExample(){ // Metro requires static asset references. // eslint-disable-next-line @typescript-eslint/no-require-imports - const [loaded,error]=useFonts({BloxwapSansRegular:require('@bloxwap/hig-tokens/fonts/BloxwapSans.ttf'),BloxwapSansSemiBold:require('@bloxwap/hig-tokens/fonts/BloxwapSans-SemiBold.ttf')}); + const [loaded,error]=useFonts({BloxwapSansRegular:require('@bloxwap/hig-native-fonts/fonts/BloxwapSans.ttf'),BloxwapSansSemiBold:require('@bloxwap/hig-native-fonts/fonts/BloxwapSans-SemiBold.ttf')}); const [open,setOpen]=useState(false),trigger=useRef(null); if(error)return Font loading failed: {error.message};if(!loaded)return Loading Bloxwap fonts…; return Optional native gesture-sheet fixtureOrientation and keyboard behavior are delegated to the native backend and require runtime observation.; diff --git a/fixtures/native/package.json b/fixtures/native/package.json index 0b0768b..9098958 100644 --- a/fixtures/native/package.json +++ b/fixtures/native/package.json @@ -11,6 +11,7 @@ "@bloxwap/hig-contracts": "0.1.0", "@bloxwap/hig-feedback": "0.1.0", "@bloxwap/hig-native": "0.1.0", + "@bloxwap/hig-native-fonts": "0.1.0", "@bloxwap/hig-tokens": "0.1.0", "@bloxwap/sfx": "0.1.0", "@expo/metro-runtime": "~6.1.2", diff --git a/fixtures/next/src/app/client.tsx b/fixtures/next/src/app/client.tsx index ec53847..4c9f584 100644 --- a/fixtures/next/src/app/client.tsx +++ b/fixtures/next/src/app/client.tsx @@ -3,4 +3,5 @@ import React,{useState} from 'react'; import {HIGProvider} from '@bloxwap/hig-react/provider'; import {Button} from '@bloxwap/hig-react/button'; import {Field,AmountInput} from '@bloxwap/hig-react/field'; -export function Client(){const [amount,setAmount]=useState('0.01');const [count,setCount]=useState(0);return setAmount(e.target.value)}/>

Accepted: {count}

;} +import {NumberReadout} from '@bloxwap/hig-react/data'; +export function Client(){const [amount,setAmount]=useState('0.01');const [count,setCount]=useState(0);return setAmount(e.target.value)}/>

Accepted: {count}

Mark price

;} diff --git a/fixtures/next/src/app/layout.tsx b/fixtures/next/src/app/layout.tsx index 93fa6fb..a4bd364 100644 --- a/fixtures/next/src/app/layout.tsx +++ b/fixtures/next/src/app/layout.tsx @@ -2,5 +2,7 @@ import React from 'react'; import '@bloxwap/hig-tokens/css'; import '@bloxwap/hig-react/css'; import {BloxwapSans} from '@bloxwap/font/sans'; +import {BloxwapMono} from '@bloxwap/font/mono'; export const metadata={icons:{icon:'/favicon.svg'},title:'HIG Next consumer',description:'Packed-style client/SSR component boundaries'}; -export default function Layout({children}:React.PropsWithChildren){return {children};} +// The loader variables sit on so --hig-font-sans/--hig-font-mono resolve to the hashed Next families (#65). +export default function Layout({children}:React.PropsWithChildren){return {children};} diff --git a/package-lock.json b/package-lock.json index 41fe8fd..22b8554 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1200,6 +1200,7 @@ "@bloxwap/hig-contracts": "0.1.0", "@bloxwap/hig-feedback": "0.1.0", "@bloxwap/hig-native": "0.1.0", + "@bloxwap/hig-native-fonts": "0.1.0", "@bloxwap/hig-tokens": "0.1.0", "@bloxwap/sfx": "0.1.0", "@expo/metro-runtime": "~6.1.2", @@ -2848,6 +2849,10 @@ "resolved": "packages/native", "link": true }, + "node_modules/@bloxwap/hig-native-fonts": { + "resolved": "packages/native-fonts", + "link": true + }, "node_modules/@bloxwap/hig-react": { "resolved": "packages/react", "link": true @@ -21874,6 +21879,11 @@ } } }, + "packages/native-fonts": { + "name": "@bloxwap/hig-native-fonts", + "version": "0.1.0", + "license": "MIT" + }, "packages/react": { "name": "@bloxwap/hig-react", "version": "0.1.0", diff --git a/package.json b/package.json index 87a06ab..dea1573 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "node": ">=22.22" }, "scripts": { - "generate": "node scripts/generate.mjs && node scripts/generate-brand.mjs", + "generate": "node scripts/generate.mjs && node scripts/generate-native-fonts-index.mjs && node scripts/generate-brand.mjs", "build": "npm run generate && node scripts/build.mjs && npm run build:docs", "typecheck": "tsc --noEmit && npm run check -w hig-docs", "lint": "eslint . && node scripts/check-drift.mjs", diff --git a/packages/native-fonts/LICENSE b/packages/native-fonts/LICENSE new file mode 100644 index 0000000..daec4c1 --- /dev/null +++ b/packages/native-fonts/LICENSE @@ -0,0 +1,5 @@ +MIT License + +Copyright (c) 2026 Bloxwap + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to inclusion of this notice. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. diff --git a/packages/native-fonts/README.md b/packages/native-fonts/README.md new file mode 100644 index 0000000..df5bca3 --- /dev/null +++ b/packages/native-fonts/README.md @@ -0,0 +1,17 @@ +# @bloxwap/hig-native-fonts + +Version 0.1.0 candidate. Static Bloxwap Sans/Mono (and Korean/Simplified Chinese Sans) TTF faces for React Native and Expo hosts, derived reproducibly from pinned `@bloxwap/font` 0.1.2 by `npm run generate:fonts`. + +Web hosts never need this package: load fonts through `@bloxwap/font` and use `@bloxwap/hig-tokens/css`. + +```tsx +import {useFonts} from 'expo-font'; +import {fontManifest} from '@bloxwap/hig-native-fonts'; + +const [loaded] = useFonts({ + BloxwapSansRegular: require('@bloxwap/hig-native-fonts/fonts/BloxwapSans.ttf'), + BloxwapSansSemiBold: require('@bloxwap/hig-native-fonts/fonts/BloxwapSans-SemiBold.ttf'), +}); +``` + +`fontManifest` (also `@bloxwap/hig-native-fonts/manifest`) lists every face with its registration key, PostScript name, weight, script, byte count and SHA-256, plus the hashes of the `@bloxwap/font` sources. It satisfies the `FontManifest` contract in `@bloxwap/hig-contracts`, so `nativeFont(fontManifest, 'BloxwapSans', 600)` resolves a registered family. OFL attribution ships in `fonts/OFL.txt`. diff --git a/packages/tokens/assets/fonts/OFL.txt b/packages/native-fonts/fonts/OFL.txt similarity index 100% rename from packages/tokens/assets/fonts/OFL.txt rename to packages/native-fonts/fonts/OFL.txt diff --git a/packages/tokens/assets/fonts/manifest.json b/packages/native-fonts/fonts/manifest.json similarity index 100% rename from packages/tokens/assets/fonts/manifest.json rename to packages/native-fonts/fonts/manifest.json diff --git a/packages/native-fonts/package.json b/packages/native-fonts/package.json new file mode 100644 index 0000000..289fa26 --- /dev/null +++ b/packages/native-fonts/package.json @@ -0,0 +1,26 @@ +{ + "name": "@bloxwap/hig-native-fonts", + "version": "0.1.0", + "type": "module", + "license": "MIT", + "files": [ + "dist", + "fonts", + "README.md", + "LICENSE" + ], + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./manifest": "./fonts/manifest.json", + "./fonts/*": "./fonts/*" + }, + "repository": { + "type": "git", + "url": "https://github.com/bloxwap/hig", + "directory": "packages/native-fonts" + } +} diff --git a/packages/native-fonts/src/index.ts b/packages/native-fonts/src/index.ts new file mode 100644 index 0000000..22c87ce --- /dev/null +++ b/packages/native-fonts/src/index.ts @@ -0,0 +1,167 @@ +// Generated by npm run generate from fonts/manifest.json (written by npm run generate:fonts). +export interface NativeFontFace { readonly family: string; readonly weight: number; readonly asset: string; readonly script: string; readonly registration: string; readonly postscript: string; readonly bytes: number } +export interface NativeFontManifest { readonly package: string; readonly version: string; readonly sources: Readonly>; readonly faces: ReadonlyArray; readonly outputs: Readonly> } +export const fontManifest = { + "package": "@bloxwap/font", + "version": "0.1.2", + "sources": { + "fonts/BloxwapSans/BloxwapSans-Variable.woff2": "96f6be58023b1d22152c797336282a4330a47ab4a3c0ab87451be887903746b7", + "fonts/BloxwapMono/BloxwapMono-Variable.woff2": "fa66e41575807c3d260d8f5b493be349cac127836b0174976c7d4876237d2632", + "fonts/BloxwapSansKR/BloxwapSansKR-Variable.woff2": "ac41500ba7546e204d07499266cadb848e24c0e9935c2616124aedcfba5a9737", + "fonts/BloxwapSansSC/BloxwapSansSC-Variable.woff2": "c0244998e334bbd997dc9acfaf3f10bce169facfec1cf2bbcfc4db38c7122893" + }, + "faces": [ + { + "family": "BloxwapSans", + "weight": 400, + "asset": "BloxwapSans.ttf", + "script": "latin", + "registration": "BloxwapSansRegular", + "postscript": "BloxwapSans-Regular", + "bytes": 446440 + }, + { + "family": "BloxwapSans", + "weight": 500, + "asset": "BloxwapSans-Medium.ttf", + "script": "latin", + "registration": "BloxwapSansMedium", + "postscript": "BloxwapSans-Medium", + "bytes": 447024 + }, + { + "family": "BloxwapSans", + "weight": 600, + "asset": "BloxwapSans-SemiBold.ttf", + "script": "latin", + "registration": "BloxwapSansSemiBold", + "postscript": "BloxwapSans-SemiBold", + "bytes": 447104 + }, + { + "family": "BloxwapSans", + "weight": 700, + "asset": "BloxwapSans-Bold.ttf", + "script": "latin", + "registration": "BloxwapSansBold", + "postscript": "BloxwapSans-Bold", + "bytes": 447388 + }, + { + "family": "BloxwapSans", + "weight": 800, + "asset": "BloxwapSans-ExtraBold.ttf", + "script": "latin", + "registration": "BloxwapSansExtraBold", + "postscript": "BloxwapSans-ExtraBold", + "bytes": 446820 + }, + { + "family": "BloxwapSans", + "weight": 900, + "asset": "BloxwapSans-Black.ttf", + "script": "latin", + "registration": "BloxwapSansBlack", + "postscript": "BloxwapSans-Black", + "bytes": 445912 + }, + { + "family": "BloxwapMono", + "weight": 400, + "asset": "BloxwapMono.ttf", + "script": "latin", + "registration": "BloxwapMonoRegular", + "postscript": "BloxwapMono-Regular", + "bytes": 385336 + }, + { + "family": "BloxwapMono", + "weight": 500, + "asset": "BloxwapMono-Medium.ttf", + "script": "latin", + "registration": "BloxwapMonoMedium", + "postscript": "BloxwapMono-Medium", + "bytes": 385628 + }, + { + "family": "BloxwapMono", + "weight": 600, + "asset": "BloxwapMono-SemiBold.ttf", + "script": "latin", + "registration": "BloxwapMonoSemiBold", + "postscript": "BloxwapMono-SemiBold", + "bytes": 385372 + }, + { + "family": "BloxwapMono", + "weight": 700, + "asset": "BloxwapMono-Bold.ttf", + "script": "latin", + "registration": "BloxwapMonoBold", + "postscript": "BloxwapMono-Bold", + "bytes": 384964 + }, + { + "family": "BloxwapMono", + "weight": 800, + "asset": "BloxwapMono-ExtraBold.ttf", + "script": "latin", + "registration": "BloxwapMonoExtraBold", + "postscript": "BloxwapMono-ExtraBold", + "bytes": 384608 + }, + { + "family": "BloxwapSansKR", + "weight": 400, + "asset": "BloxwapSansKR.ttf", + "script": "korean", + "registration": "BloxwapSansKRRegular", + "postscript": "BloxwapSansKR-Regular", + "bytes": 1294968 + }, + { + "family": "BloxwapSansKR", + "weight": 600, + "asset": "BloxwapSansKR-SemiBold.ttf", + "script": "korean", + "registration": "BloxwapSansKRSemiBold", + "postscript": "BloxwapSansKR-SemiBold", + "bytes": 1295432 + }, + { + "family": "BloxwapSansSC", + "weight": 400, + "asset": "BloxwapSansSC.ttf", + "script": "simplified-chinese", + "registration": "BloxwapSansSCRegular", + "postscript": "BloxwapSansSC-Regular", + "bytes": 2422672 + }, + { + "family": "BloxwapSansSC", + "weight": 600, + "asset": "BloxwapSansSC-SemiBold.ttf", + "script": "simplified-chinese", + "registration": "BloxwapSansSCSemiBold", + "postscript": "BloxwapSansSC-SemiBold", + "bytes": 2422976 + } + ], + "outputs": { + "BloxwapSans.ttf": "778520981b42b408c19e43fc6d62e513ff0c47bd9870d62159e2e74cda2f79f5", + "BloxwapSans-Medium.ttf": "add342361cd444c59fb05bb072ac71b32ed27d2fd2f9a364bddb6a930c6bd6bd", + "BloxwapSans-SemiBold.ttf": "7257cb505ec841dae8f5ff3160c629d08003bfd31b9d94334dd4a31798444c8c", + "BloxwapSans-Bold.ttf": "13e10451ecec4e33733b196454f1232a16d34d5014dce66daaee4d0c58600e1b", + "BloxwapSans-ExtraBold.ttf": "83cd52444039e62c49d691103fb8ab6329f3135ec8c80e8348e448d03a05cb0a", + "BloxwapSans-Black.ttf": "a99894c158cff3cff828b7d17ec12f075352848843944a730caa9d64c0362ea2", + "BloxwapMono.ttf": "6469eea2fc3fbad052301a6e0febf0182aef280b888acbbf1390c623ff168b65", + "BloxwapMono-Medium.ttf": "f969fd1e4d3b81a7fac108a21a8130b7618f04c39ce7fd5e598fb70fd1230b1f", + "BloxwapMono-SemiBold.ttf": "88389198a85cb655404e67f3c9ae683b61ee1d85e57f5222d0228833f6c7f7b2", + "BloxwapMono-Bold.ttf": "3246a540d8bd8d8dda8ee965d0e1f38b9e00cb373cc1756500a052631d01623a", + "BloxwapMono-ExtraBold.ttf": "89cc3282a7781f08b66d72c0d1b105fdfaa1531e48cfcc50c6eeb5b2b5db64bd", + "BloxwapSansKR.ttf": "d8eae9352bd2d4ae09eeecdb3ffd9cf9c914982f24db3ff816bb16d8ea329c1e", + "BloxwapSansKR-SemiBold.ttf": "0897de89055aa2d30722a5bee8a97443a373c5105b726846508477ae49a03e8c", + "BloxwapSansSC.ttf": "1a6802ac902328f3cd5040d7400a2e9e46f15871fa6ea2c9d8138dfffb8333cb", + "BloxwapSansSC-SemiBold.ttf": "9af551f298cc13fdffe4f58e7373099f1a84d3caca86a67399acf2cc35970d4b" + } +} as const satisfies NativeFontManifest; diff --git a/packages/native-fonts/tsconfig.json b/packages/native-fonts/tsconfig.json new file mode 100644 index 0000000..41c6617 --- /dev/null +++ b/packages/native-fonts/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "rootDir": "src", + "outDir": "dist" + }, + "include": [ + "src" + ] +} diff --git a/packages/react/src/styles.css b/packages/react/src/styles.css index c83af26..65a91fb 100644 --- a/packages/react/src/styles.css +++ b/packages/react/src/styles.css @@ -1,7 +1,7 @@ :root { --hig-control: 48px; --hig-gap: 12px; } [data-hig-density='compact'] { --hig-control: 36px; --hig-gap: 8px; } * { box-sizing: border-box; } -.hig-button { min-height: var(--hig-control); max-width:100%; padding:8px 20px; border:1px solid transparent; border-radius:9999px; font:600 1rem/1.5 'Bloxwap Sans',system-ui,sans-serif; background:var(--hig-action); color:var(--hig-onAction); cursor:pointer; overflow-wrap:anywhere; } +.hig-button { min-height: var(--hig-control); max-width:100%; padding:8px 20px; border:1px solid transparent; border-radius:9999px; font:600 1rem/1.5 var(--hig-font-sans,'Bloxwap Sans',system-ui,sans-serif); background:var(--hig-action); color:var(--hig-onAction); cursor:pointer; overflow-wrap:anywhere; } .hig-button[data-intent='secondary'],.hig-button[data-intent='ghost'] { background:var(--hig-nested);color:var(--hig-text); } .hig-button[data-intent='destructive'] { background:var(--hig-negative);color:var(--hig-onAction); } .hig-button[data-full] { width:100%; } @@ -29,7 +29,7 @@ .hig-menu-item { min-height:48px;padding:12px;border-radius:8px;cursor:pointer; } .hig-menu-item[data-highlighted] { background:var(--hig-surface);outline:2px solid var(--hig-focus); } .hig-menu-item[data-disabled] { opacity:.5; } -.hig-number { font-family:'Bloxwap Mono',monospace;font-variant-numeric:tabular-nums; } +.hig-number { font-family:var(--hig-font-mono,'Bloxwap Mono',monospace);font-variant-numeric:tabular-nums; } .hig-row { display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;padding-block:12px; } .hig-list { list-style:none;padding:0;margin:0; } .hig-table-scroll { overflow-x:auto; } diff --git a/packages/tokens/generated/manifest.json b/packages/tokens/generated/manifest.json index d17d0db..f9cbc93 100644 --- a/packages/tokens/generated/manifest.json +++ b/packages/tokens/generated/manifest.json @@ -126,6 +126,14 @@ "base": 260, "slow": 320 }, + "easing": { + "standard": [ + 0.22, + 1, + 0.36, + 1 + ] + }, "type": { "caption": { "size": 12, @@ -152,5 +160,23 @@ "lineHeight": 52, "weight": 600 } + }, + "font": { + "sans": { + "family": "Bloxwap Sans", + "loaderVariable": "--font-bloxwap-sans", + "fallback": [ + "system-ui", + "sans-serif" + ] + }, + "mono": { + "family": "Bloxwap Mono", + "loaderVariable": "--font-bloxwap-mono", + "fallback": [ + "ui-monospace", + "monospace" + ] + } } } diff --git a/packages/tokens/generated/tokens.css b/packages/tokens/generated/tokens.css index 32f91a7..dde0154 100644 --- a/packages/tokens/generated/tokens.css +++ b/packages/tokens/generated/tokens.css @@ -23,7 +23,50 @@ --hig-space-xxl: 1.5rem; --hig-space-xxxl: 2rem; --hig-space-section: 3rem; + --hig-font-sans: var(--font-bloxwap-sans, 'Bloxwap Sans'), system-ui, sans-serif; + --hig-font-mono: var(--font-bloxwap-mono, 'Bloxwap Mono'), ui-monospace, monospace; + --hig-radius-sm: 0.375rem; + --hig-radius-md: 0.5rem; + --hig-radius-lg: 0.625rem; + --hig-radius-xl: 0.875rem; + --hig-radius-pill: 9999px; + --hig-type-caption-size: 0.75rem; + --hig-type-caption-line-height: 1.125rem; + --hig-type-caption-weight: 500; + --hig-type-caption: 500 0.75rem/1.125rem var(--hig-font-sans); + --hig-type-label-size: 0.875rem; + --hig-type-label-line-height: 1.25rem; + --hig-type-label-weight: 600; + --hig-type-label: 600 0.875rem/1.25rem var(--hig-font-sans); + --hig-type-body-size: 1rem; + --hig-type-body-line-height: 1.5rem; + --hig-type-body-weight: 400; + --hig-type-body: 400 1rem/1.5rem var(--hig-font-sans); + --hig-type-title-size: 1.5rem; + --hig-type-title-line-height: 1.875rem; + --hig-type-title-weight: 600; + --hig-type-title: 600 1.5rem/1.875rem var(--hig-font-sans); + --hig-type-display-size: 3rem; + --hig-type-display-line-height: 3.25rem; + --hig-type-display-weight: 600; + --hig-type-display: 600 3rem/3.25rem var(--hig-font-sans); + --hig-motion-duration-fast: 180ms; + --hig-motion-duration-base: 260ms; + --hig-motion-duration-slow: 320ms; + --hig-motion-easing-standard: cubic-bezier(0.22, 1, 0.36, 1); + --hig-density-comfortable-control: 3rem; + --hig-density-comfortable-target: 3rem; + --hig-density-comfortable-gap: 0.75rem; + --hig-density-compact-control: 2.25rem; + --hig-density-compact-target: 2.25rem; + --hig-density-compact-gap: 0.5rem; + --hig-density-control: var(--hig-density-comfortable-control); + --hig-density-target: var(--hig-density-comfortable-target); + --hig-density-gap: var(--hig-density-comfortable-gap); } +[data-hig-density='compact'] { --hig-density-control: var(--hig-density-compact-control); --hig-density-target: var(--hig-density-compact-target); --hig-density-gap: var(--hig-density-compact-gap); } +@media (prefers-reduced-motion: reduce) { :root { --hig-motion-duration-fast: 0ms; --hig-motion-duration-base: 0ms; --hig-motion-duration-slow: 0ms; } } +[data-hig-motion='reduced'] { --hig-motion-duration-fast: 0ms; --hig-motion-duration-base: 0ms; --hig-motion-duration-slow: 0ms; } @supports (color: oklch(0.5 0 0)) { :root { --hig-background: oklch(0.145 0 0); --hig-surface: oklch(0.205 0 0); diff --git a/packages/tokens/package.json b/packages/tokens/package.json index 260c73d..63127be 100644 --- a/packages/tokens/package.json +++ b/packages/tokens/package.json @@ -19,8 +19,7 @@ }, "./css": "./dist/tokens.css", "./manifest": "./dist/manifest.json", - "./mark.svg": "./assets/bloxwap-icon.svg", - "./fonts/*": "./assets/fonts/*" + "./mark.svg": "./assets/bloxwap-icon.svg" }, "repository": { "type": "git", diff --git a/packages/tokens/source.json b/packages/tokens/source.json index d17d0db..f9cbc93 100644 --- a/packages/tokens/source.json +++ b/packages/tokens/source.json @@ -126,6 +126,14 @@ "base": 260, "slow": 320 }, + "easing": { + "standard": [ + 0.22, + 1, + 0.36, + 1 + ] + }, "type": { "caption": { "size": 12, @@ -152,5 +160,23 @@ "lineHeight": 52, "weight": 600 } + }, + "font": { + "sans": { + "family": "Bloxwap Sans", + "loaderVariable": "--font-bloxwap-sans", + "fallback": [ + "system-ui", + "sans-serif" + ] + }, + "mono": { + "family": "Bloxwap Mono", + "loaderVariable": "--font-bloxwap-mono", + "fallback": [ + "ui-monospace", + "monospace" + ] + } } } diff --git a/packages/tokens/src/index.ts b/packages/tokens/src/index.ts index f3d740b..09e6603 100644 --- a/packages/tokens/src/index.ts +++ b/packages/tokens/src/index.ts @@ -127,6 +127,14 @@ export const tokens = { "base": 260, "slow": 320 }, + "easing": { + "standard": [ + 0.22, + 1, + 0.36, + 1 + ] + }, "type": { "caption": { "size": 12, @@ -153,6 +161,24 @@ export const tokens = { "lineHeight": 52, "weight": 600 } + }, + "font": { + "sans": { + "family": "Bloxwap Sans", + "loaderVariable": "--font-bloxwap-sans", + "fallback": [ + "system-ui", + "sans-serif" + ] + }, + "mono": { + "family": "Bloxwap Mono", + "loaderVariable": "--font-bloxwap-mono", + "fallback": [ + "ui-monospace", + "monospace" + ] + } } } as const; // Small native bindings keep a basic control from retaining color-space/provenance metadata. diff --git a/planning/budgets.json b/planning/budgets.json index 9ffb39b..579b06c 100644 --- a/planning/budgets.json +++ b/planning/budgets.json @@ -15,7 +15,7 @@ "gzip": 650000 }, "docsCSS": { - "raw": 110000 + "raw": 115000 }, "fonts": { "raw": 600000 diff --git a/scripts/browser-smoke.mjs b/scripts/browser-smoke.mjs index 3e4b286..a52ec35 100644 --- a/scripts/browser-smoke.mjs +++ b/scripts/browser-smoke.mjs @@ -2,6 +2,7 @@ import {chromium,firefox,webkit} from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; import {writeFileSync} from 'node:fs'; import assert from 'node:assert/strict'; +import {assertLoadedFace} from './font-check.mjs'; const stage=process.argv[2]??'m2'; const engine=process.env.HIG_BROWSER??'chromium';const launcher={chromium,firefox,webkit}[engine];if(!launcher)throw Error('Unsupported browser'); const browser=await launcher.launch({...(engine==='chromium'&&process.env.HIG_CHROME?{executablePath:process.env.HIG_CHROME}:{}),headless:true,args:engine==='chromium'?['--no-sandbox','--disable-dev-shm-usage']:[]}); @@ -11,6 +12,8 @@ const context=await browser.newContext({viewport:{width:390,height:844},reducedM const page=await context.newPage();page.setDefaultTimeout(120000); const errors=[];page.on('pageerror',e=>errors.push(e.message)); await page.goto(process.env.HIG_PREVIEW_URL??'http://localhost:4173',{waitUntil:'networkidle'}); +// Plain CSS host: @font-face registers the literal families the token font roles fall back to (#65). +await assertLoadedFace(page,'.hig-button',/^Bloxwap Sans$/);await assertLoadedFace(page,'.hig-number',/^Bloxwap Mono$/); await page.getByRole('button',{name:'Review order',exact:true}).click(); assert.equal(await page.getByRole('dialog').count(),1); await page.keyboard.press('Escape'); @@ -43,7 +46,7 @@ await page.screenshot({path:`evidence/${label}-web-mobile.png`,fullPage:true}); await page.setViewportSize({width:1440,height:1000}); await page.getByLabel('Pro',{exact:true}).check(); await page.screenshot({path:`evidence/${label}-web-pro.png`,fullPage:true}); -writeFileSync(`evidence/${label}-browser.json`,JSON.stringify({version:'0.1.0',browser:await browser.version(),platform:`Linux ${engine} headless; no physical-device claim`,viewport:[390,844],checks:['no page errors','modal Escape','keyboard tabs','no horizontal viewport overflow','zero axe WCAG findings'],violations},null,2)+'\n'); +writeFileSync(`evidence/${label}-browser.json`,JSON.stringify({version:'0.1.0',browser:await browser.version(),platform:`Linux ${engine} headless; no physical-device claim`,viewport:[390,844],checks:['no page errors','plain @font-face Bloxwap Sans/Mono loaded on .hig-button/.hig-number','modal Escape','keyboard tabs','no horizontal viewport overflow','zero axe WCAG findings'],violations},null,2)+'\n'); console.log(`Browser ${stage}: interaction/accessibility/screenshots passed`); }finally{await browser.close();} diff --git a/scripts/build.mjs b/scripts/build.mjs index d4b0519..372b9d7 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -1,6 +1,6 @@ import {execFileSync} from 'node:child_process'; import {mkdirSync,copyFileSync,existsSync} from 'node:fs'; -for(const p of ['contracts','tokens','feedback','react','native']){if(!existsSync(`packages/${p}/tsconfig.json`))continue;execFileSync('npx',['tsc','-p',`packages/${p}/tsconfig.json`],{stdio:'inherit'});} +for(const p of ['contracts','tokens','native-fonts','feedback','react','native']){if(!existsSync(`packages/${p}/tsconfig.json`))continue;execFileSync('npx',['tsc','-p',`packages/${p}/tsconfig.json`],{stdio:'inherit'});} mkdirSync('packages/tokens/dist',{recursive:true}); for(const p of ['tokens.css','manifest.json'])copyFileSync(`packages/tokens/generated/${p}`,`packages/tokens/dist/${p}`); if(existsSync('packages/react/src/styles.css'))copyFileSync('packages/react/src/styles.css','packages/react/dist/styles.css'); diff --git a/scripts/check-drift.mjs b/scripts/check-drift.mjs index b5c9d44..f64544b 100644 --- a/scripts/check-drift.mjs +++ b/scripts/check-drift.mjs @@ -3,6 +3,7 @@ import {readFileSync} from 'node:fs'; import {fileURLToPath} from 'node:url'; function check(paths,generator){const before=paths.map(p=>readFileSync(p));execFileSync(process.execPath,[fileURLToPath(new URL(generator,import.meta.url))]);paths.forEach((p,i)=>{if(!readFileSync(p).equals(before[i]))throw new Error(`Stale generated artifact: ${p}`);});} check(['packages/tokens/src/index.ts','packages/tokens/generated/tokens.css','packages/tokens/generated/manifest.json','evidence/contrast.json'],'./generate.mjs'); +check(['packages/native-fonts/src/index.ts'],'./generate-native-fonts-index.mjs'); check(['apps/docs/public/favicon.svg','fixtures/next/public/favicon.svg','apps/docs/public/og.svg','apps/docs/public/og.png','planning/brand-artifacts.json'],'./generate-brand.mjs'); check(['docs/foundations/glossary.md','apps/docs/public/glossary.json'],'./generate-glossary.mjs'); -console.log('Token, brand and glossary generation drift passed'); +console.log('Token, native font manifest, brand and glossary generation drift passed'); diff --git a/scripts/font-check.mjs b/scripts/font-check.mjs new file mode 100644 index 0000000..c590e8e --- /dev/null +++ b/scripts/font-check.mjs @@ -0,0 +1,17 @@ +import assert from 'node:assert/strict'; +// #65: resolve the element's computed first family, load it and require a loaded FontFace of that family. +// document.fonts.check alone also returns true when no face matches, so the FontFace status is asserted too. +export async function assertLoadedFace(page,selector,expected){ + const result=await page.evaluate(async selector=>{ + await document.fonts.ready;const element=document.querySelector(selector);if(!element)return {missing:true}; + const style=getComputedStyle(element),family=style.fontFamily.split(',')[0].trim().replace(/^["']|["']$/g,''); + const spec=`${style.fontStyle} ${style.fontWeight} ${style.fontSize} "${family}"`;await document.fonts.load(spec,element.textContent||'0'); + const faces=[...document.fonts].filter(face=>face.family.replace(/^["']|["']$/g,'')===family); + return {fontFamily:style.fontFamily,family,spec,check:document.fonts.check(spec,element.textContent||'0'),loaded:faces.some(face=>face.status==='loaded')}; + },selector); + assert.ok(!result.missing,`${selector} renders`); + assert.match(result.family,expected,`${selector} uses a Bloxwap face: ${result.fontFamily}`); + assert.equal(result.check,true,`document.fonts.check(${result.spec})`); + assert.equal(result.loaded,true,`${selector} face ${result.family} is loaded, not a system fallback`); + return result; +} diff --git a/scripts/generate-native-fonts-index.mjs b/scripts/generate-native-fonts-index.mjs new file mode 100644 index 0000000..7aa85bf --- /dev/null +++ b/scripts/generate-native-fonts-index.mjs @@ -0,0 +1,7 @@ +import {readFileSync,writeFileSync} from 'node:fs'; +const fontManifest=JSON.parse(readFileSync('packages/native-fonts/fonts/manifest.json','utf8')); +writeFileSync('packages/native-fonts/src/index.ts',`// Generated by npm run generate from fonts/manifest.json (written by npm run generate:fonts). +export interface NativeFontFace { readonly family: string; readonly weight: number; readonly asset: string; readonly script: string; readonly registration: string; readonly postscript: string; readonly bytes: number } +export interface NativeFontManifest { readonly package: string; readonly version: string; readonly sources: Readonly>; readonly faces: ReadonlyArray; readonly outputs: Readonly> } +export const fontManifest = ${JSON.stringify(fontManifest,null,2)} as const satisfies NativeFontManifest; +`); diff --git a/scripts/generate-native-fonts.py b/scripts/generate-native-fonts.py index f388905..df1bf24 100644 --- a/scripts/generate-native-fonts.py +++ b/scripts/generate-native-fonts.py @@ -57,7 +57,7 @@ def instance(source, family, weight): package_dir = args.package_dir if package_dir is None: # Match workspace/hoisted installs by looking up ancestor node_modules. - for base in (ROOT / 'packages/tokens', ROOT / 'packages', ROOT): + for base in (ROOT / 'packages/native-fonts', ROOT / 'packages', ROOT): candidate = base / 'node_modules/@bloxwap/font' if (candidate / 'package.json').is_file(): package_dir = candidate @@ -72,7 +72,7 @@ def instance(source, family, weight): actual = hashlib.sha256((pkg / relative).read_bytes()).hexdigest() if actual != expected: raise SystemExit(f'Published {PACKAGE_VERSION} source SHA256 mismatch: {relative}: {actual}') -native = ROOT / 'packages/tokens/assets/fonts' +native = ROOT / 'packages/native-fonts/fonts' native.mkdir(parents=True, exist_ok=True) manifest = {'package': '@bloxwap/font', 'version': PACKAGE_VERSION, 'sources': SOURCE_SHA256, 'faces': [], 'outputs': {}} for family, weights, script in [('BloxwapSans', [400,500,600,700,800,900], 'latin'), ('BloxwapMono', [400,500,600,700,800], 'latin'), ('BloxwapSansKR', [400,600], 'korean'), ('BloxwapSansSC', [400,600], 'simplified-chinese')]: diff --git a/scripts/generate.mjs b/scripts/generate.mjs index a8d7b8c..763cd17 100644 --- a/scripts/generate.mjs +++ b/scripts/generate.mjs @@ -21,10 +21,29 @@ const p3=Object.entries(tokens.colors).filter(([,v])=>v.p3).map(([k,v])=>` --hi const oklch=Object.entries(tokens.colors).filter(([,v])=>v.oklch).map(([k,v])=>` --hig-${k}: ${v.oklch};`).join('\n'); const sourceVars=Object.entries(upstream.semanticOklch).map(([k,v])=>` --hig-source-${k}: ${v};`).join('\n'); const space=Object.entries(tokens.space).map(([k,v])=>` --hig-space-${k}: ${v/16}rem;`).join('\n'); +const rem=px=>`${px/16}rem`; +// Font roles prefer the @bloxwap/font Next loader variables when a host sets them on , then the literal families plain @font-face hosts register (#65). +const fonts=Object.entries(tokens.font).map(([k,v])=>` --hig-font-${k}: var(${v.loaderVariable}, '${v.family}'), ${v.fallback.join(', ')};`).join('\n'); +const radius=Object.entries(tokens.radius).map(([k,v])=>` --hig-radius-${k}: ${v>=9999?`${v}px`:rem(v)};`).join('\n'); +const type=Object.entries(tokens.type).map(([k,v])=>[` --hig-type-${k}-size: ${rem(v.size)};`,` --hig-type-${k}-line-height: ${rem(v.lineHeight)};`,` --hig-type-${k}-weight: ${v.weight};`,` --hig-type-${k}: ${v.weight} ${rem(v.size)}/${rem(v.lineHeight)} var(--hig-font-sans);`].join('\n')).join('\n'); +const durations=Object.entries(tokens.motion).map(([k,v])=>` --hig-motion-duration-${k}: ${v}ms;`).join('\n'); +const reducedDurations=Object.keys(tokens.motion).map(k=>`--hig-motion-duration-${k}: 0ms;`).join(' '); +const easing=Object.entries(tokens.easing).map(([k,v])=>` --hig-motion-easing-${k}: cubic-bezier(${v.join(', ')});`).join('\n'); +const densityMetrics=Object.keys(tokens.density.comfortable); +const density=[...Object.entries(tokens.density).flatMap(([mode,metrics])=>Object.entries(metrics).map(([k,v])=>` --hig-density-${mode}-${k}: ${rem(v)};`)),...densityMetrics.map(k=>` --hig-density-${k}: var(--hig-density-comfortable-${k});`)].join('\n'); writeFileSync('packages/tokens/generated/tokens.css',`:root { color-scheme: dark; ${vars} ${space} +${fonts} +${radius} +${type} +${durations} +${easing} +${density} } +[data-hig-density='compact'] { ${densityMetrics.map(k=>`--hig-density-${k}: var(--hig-density-compact-${k});`).join(' ')} } +@media (prefers-reduced-motion: reduce) { :root { ${reducedDurations} } } +[data-hig-motion='reduced'] { ${reducedDurations} } @supports (color: oklch(0.5 0 0)) { :root { ${oklch} ${sourceVars} diff --git a/scripts/next-browser-smoke.mjs b/scripts/next-browser-smoke.mjs index 3b71dc3..f67f18a 100644 --- a/scripts/next-browser-smoke.mjs +++ b/scripts/next-browser-smoke.mjs @@ -1,6 +1,8 @@ import {chromium} from '@playwright/test'; import assert from 'node:assert/strict'; import {writeFileSync} from 'node:fs'; +import {assertLoadedFace} from './font-check.mjs'; +const stage=process.argv[2]==='packed'?'packed':'m5'; const browser=await chromium.launch({...(process.env.HIG_CHROME?{executablePath:process.env.HIG_CHROME}:{}),headless:true,args:['--no-sandbox','--disable-dev-shm-usage']}); try{ const page=await browser.newPage({viewport:{width:390,height:844}});page.setDefaultTimeout(120000);const errors=[]; @@ -10,8 +12,9 @@ try{ assert.equal(await page.getByLabel('Amount (BTC)').inputValue(),'0,00001'); await page.getByRole('button',{name:'Review order'}).focus();await page.keyboard.press('Enter'); assert.equal(await page.getByRole('status').textContent(),'Accepted: 1'); + const fonts={button:await assertLoadedFace(page,'.hig-button',/Bloxwap_?\s?Sans/i),number:await assertLoadedFace(page,'.hig-number',/Bloxwap_?\s?Mono/i)}; assert.deepEqual(errors,[]); - await page.screenshot({path:'evidence/m5-next-hydration.png',fullPage:true}); - writeFileSync('evidence/m5-next-browser.json',JSON.stringify({version:'0.1.0',framework:'Next 16.4.0 webpack',checks:['production server/client boundary','no hydration/page/console errors','raw locale amount retained','keyboard accepted action once','Bloxwap Sans loader compiled'],browser:await browser.version()},null,2)+'\n'); - console.log('Next hydration/keyboard/raw-amount browser check passed'); + await page.screenshot({path:`evidence/${stage}-next-hydration.png`,fullPage:true}); + writeFileSync(`evidence/${stage}-next-browser.json`,JSON.stringify({version:'0.1.0',framework:'Next 16.4.0 webpack',consumer:stage==='packed'?'packed tarballs in a clean temporary app':'workspace fixture',checks:['production server/client boundary','no hydration/page/console errors','raw locale amount retained','keyboard accepted action once','Bloxwap Sans/Mono Next loaders compiled','.hig-button/.hig-number computed font is a loaded Bloxwap face (document.fonts.check + FontFace status)'],fonts:Object.fromEntries(Object.entries(fonts).map(([k,v])=>[k,{fontFamily:v.fontFamily,check:v.check,loaded:v.loaded}])),browser:await browser.version()},null,2)+'\n'); + console.log(`Next (${stage}) hydration/keyboard/raw-amount/font browser check passed`); }finally{await browser.close();} diff --git a/scripts/packed-native-smoke.mjs b/scripts/packed-native-smoke.mjs index 383eb31..5418c10 100644 --- a/scripts/packed-native-smoke.mjs +++ b/scripts/packed-native-smoke.mjs @@ -1,6 +1,8 @@ import {verifyNativeReact,verifyExpoNativeModules} from './native-compatibility.mjs'; import {execFileSync} from 'node:child_process'; -import {mkdtempSync,writeFileSync,readFileSync,cpSync,readdirSync} from 'node:fs'; +import {mkdtempSync,writeFileSync,readFileSync,cpSync,readdirSync,statSync} from 'node:fs'; +import {createHash} from 'node:crypto'; +import assert from 'node:assert/strict'; import {join,resolve} from 'node:path'; import {tmpdir} from 'node:os'; const dir=mkdtempSync(join(tmpdir(),'hig-native-consumer-')); @@ -13,6 +15,16 @@ const tarballs=readdirSync('artifacts').filter(n=>n.endsWith('.tgz')&&!n.include execFileSync('npm',['install','--no-audit','--no-fund',...tarballs],{cwd:dir,stdio:'pipe'}); console.log('Native runtime versions',verifyNativeReact(dir));console.log('Expo native modules',verifyExpoNativeModules(dir)); execFileSync('npm',['run','bundle','--','--max-workers','2'],{cwd:dir,stdio:'inherit'}); +// #64: native faces come from @bloxwap/hig-native-fonts with the generator's hashes; the web tokens package carries none. +const sha=bytes=>createHash('sha256').update(bytes).digest('hex'); +const fontRoot=join(dir,'node_modules/@bloxwap/hig-native-fonts/fonts'),installedFonts=JSON.parse(readFileSync(join(fontRoot,'manifest.json'),'utf8')),sourceFonts=JSON.parse(readFileSync('packages/native-fonts/fonts/manifest.json','utf8')); +assert.deepEqual(installedFonts.outputs,sourceFonts.outputs); +for(const [asset,hash] of Object.entries(installedFonts.outputs))assert.equal(sha(readFileSync(join(fontRoot,asset))),hash,`Packed native font hash ${asset}`); +function walk(path){return readdirSync(path,{withFileTypes:true}).flatMap(e=>e.isDirectory()?walk(join(path,e.name)):[join(path,e.name)]);} +const exported=new Set(walk(join(dir,'dist')).filter(p=>statSync(p).size>100000).map(p=>sha(readFileSync(p)))); +for(const asset of ['BloxwapSans.ttf','BloxwapSans-SemiBold.ttf','BloxwapMono.ttf'])assert.ok(exported.has(installedFonts.outputs[asset]),`Metro export registers ${asset} from @bloxwap/hig-native-fonts`); +assert.equal(walk(join(dir,'node_modules/@bloxwap/hig-tokens')).filter(p=>/\.(?:ttf|otf|woff2?)$/i.test(p)).length,0,'Web tokens package ships no font binaries'); +console.log('Packed native fonts: hashes match the generator manifest and Metro bundles the registered faces'); writeFileSync(join(dir,'index.js'),readFileSync('fixtures/native/index-gesture.js')); execFileSync('npm',['run','bundle','--','--max-workers','2','--output-dir','dist-gesture'],{cwd:dir,stdio:'inherit'}); console.log('Packed optional gesture-sheet consumer exported Android/iOS with real Gorhom/Reanimated/Worklets peers'); diff --git a/scripts/packed-next-smoke.mjs b/scripts/packed-next-smoke.mjs index 9c803ba..a651d27 100644 --- a/scripts/packed-next-smoke.mjs +++ b/scripts/packed-next-smoke.mjs @@ -1,4 +1,5 @@ -import {execFileSync} from 'node:child_process'; +import {execFileSync,spawn} from 'node:child_process'; +import {setTimeout as delay} from 'node:timers/promises'; import {mkdtempSync,writeFileSync,readFileSync,cpSync,readdirSync} from 'node:fs'; import {join,resolve} from 'node:path'; import {tmpdir} from 'node:os'; @@ -13,4 +14,12 @@ const tarballs=readdirSync('artifacts').filter(n=>n.endsWith('.tgz')&&!n.include execFileSync('npm',['install','--ignore-scripts','--no-audit','--no-fund',...tarballs],{cwd:dir,stdio:'pipe'}); execFileSync('npm',['run','build'],{cwd:dir,stdio:'inherit',env:{...process.env,NEXT_TELEMETRY_DISABLED:'1'}}); const html=readFileSync(join(dir,'.next/server/app/index.html'),'utf8');assert.match(html,/Server display:/);assert.match(html,/Review order/);const font=html.match(/href="([^"]+\.woff2)"[^>]*as="font"/);assert.ok(font,'Next preloads a font');const emitted=readFileSync(join(dir,'.next',font[1].replace(/^\/_next\//,'')));const published=readFileSync(join(dir,'node_modules/@bloxwap/font/fonts/BloxwapSans/BloxwapSans-Variable.woff2'));assert.ok(emitted.equals(published),'Next emitted the pinned published Sans font bytes'); +// #65: serve the packed consumer and require loaded Bloxwap faces on HIG controls in a real browser. +const port=process.env.HIG_PACKED_NEXT_PORT??'4180',url=`http://127.0.0.1:${port}`; +const server=spawn(process.execPath,[join(dir,'node_modules/next/dist/bin/next'),'start','--hostname','127.0.0.1','--port',port],{cwd:dir,stdio:'inherit',env:{...process.env,NEXT_TELEMETRY_DISABLED:'1'}}); +try{ + let up=false;for(let i=0;i<120&&!up;i++){if(server.exitCode!==null)throw Error('Packed Next server exited');try{up=(await fetch(url)).ok;}catch{/* Server may still be starting. */}if(!up)await delay(500);} + assert.ok(up,'Packed Next server started'); + execFileSync(process.execPath,['scripts/next-browser-smoke.mjs','packed'],{stdio:'inherit',env:{...process.env,HIG_NEXT_URL:url}}); +}finally{server.kill('SIGTERM');} console.log(`Packed Next ${manifest.dependencies.next}: production SSR/client/font build from ${tarballs.length} independent tarballs passed`); diff --git a/scripts/packed-smoke.mjs b/scripts/packed-smoke.mjs index 7a58a98..8bb52f7 100644 --- a/scripts/packed-smoke.mjs +++ b/scripts/packed-smoke.mjs @@ -9,7 +9,7 @@ const tarballs=[]; for(const manifest of manifests){ const name=manifest.name.replace('@bloxwap/hig-',''); const pack=JSON.parse(execFileSync('npm',['pack',`./packages/${name}`,'--json','--ignore-scripts','--pack-destination',resolve('artifacts')],{encoding:'utf8'})); - if(name!=='native')tarballs.push(resolve('artifacts',pack[0].filename)); + if(name!=='native'&&name!=='native-fonts')tarballs.push(resolve('artifacts',pack[0].filename)); } writeFileSync(join(dir,'package.json'),JSON.stringify({private:true,type:'module',devDependencies:{vite:'6.4.1'},dependencies:{...JSON.parse(readFileSync('fixtures/web/package.json','utf8')).dependencies,'@bloxwap/haptic':'0.1.0','@bloxwap/sfx':'0.1.0','@bloxwap/font':'0.1.2',react:'19.1.0','react-dom':'19.1.0'}},null,2)); const consumerManifest=JSON.parse(readFileSync(join(dir,'package.json'),'utf8'));for(const key of Object.keys(consumerManifest.dependencies))if(key.startsWith('@bloxwap/hig-'))delete consumerManifest.dependencies[key];writeFileSync(join(dir,'package.json'),JSON.stringify(consumerManifest)); diff --git a/scripts/quality.mjs b/scripts/quality.mjs index 79e1d7d..e5d4d6d 100644 --- a/scripts/quality.mjs +++ b/scripts/quality.mjs @@ -25,9 +25,9 @@ export async function qualityReport(){ sizes.docsJS={raw:assets.filter(a=>a.name.endsWith('.js')).reduce((s,a)=>s+a.bytes.length,0),gzip:assets.filter(a=>a.name.endsWith('.js')).reduce((s,a)=>s+gzipSync(a.bytes).length,0)}; sizes.docsCSS={raw:assets.filter(a=>a.name.endsWith('.css')).reduce((s,a)=>s+a.bytes.length,0)}; sizes.fonts={raw:assets.filter(a=>a.name.endsWith('.woff2')).reduce((s,a)=>s+a.bytes.length,0)}; - const fontManifest=JSON.parse(readFileSync('packages/tokens/assets/fonts/manifest.json','utf8')); - for(const [asset,hash] of Object.entries(fontManifest.outputs)){if(createHash('sha256').update(readFileSync(`packages/tokens/assets/fonts/${asset}`)).digest('hex')!==hash)throw Error(`Native font drift: ${asset}`);} - sizes.nativeFonts={raw:['BloxwapSans.ttf','BloxwapSans-SemiBold.ttf','BloxwapMono.ttf'].reduce((sum,asset)=>sum+readFileSync(`packages/tokens/assets/fonts/${asset}`).length,0),scope:'Only three explicitly registered fixture faces; optional CJK/other weight assets remain unloaded'}; + const fontManifest=JSON.parse(readFileSync('packages/native-fonts/fonts/manifest.json','utf8')); + for(const [asset,hash] of Object.entries(fontManifest.outputs)){if(createHash('sha256').update(readFileSync(`packages/native-fonts/fonts/${asset}`)).digest('hex')!==hash)throw Error(`Native font drift: ${asset}`);} + sizes.nativeFonts={raw:['BloxwapSans.ttf','BloxwapSans-SemiBold.ttf','BloxwapMono.ttf'].reduce((sum,asset)=>sum+readFileSync(`packages/native-fonts/fonts/${asset}`).length,0),scope:'Only three explicitly registered fixture faces; optional CJK/other weight assets remain unloaded'}; sizes.nativeCues={raw:checkCueHashes()}; for(const name of ['docsJS','docsCSS','fonts','nativeFonts','nativeCues']){const rule=budgets[name];for(const metric of Object.keys(rule))assertBudget(`${name}.${metric}`,sizes[name][metric],rule[metric]);} const report={version:'0.1.0',nativeRuntime,exports,sizes,baseline:'HIG candidate measurements; representative authenticated source React/Metro baselines in evidence/issue-1-18; deployed production deltas not claimed',physicalDevices:'pending; API/browser/Metro checks are not tactile or VoiceOver/TalkBack proof'}; diff --git a/scripts/release-dry-run.mjs b/scripts/release-dry-run.mjs index 68863a4..4ad5ba2 100644 --- a/scripts/release-dry-run.mjs +++ b/scripts/release-dry-run.mjs @@ -22,9 +22,14 @@ for(const name of readdirSync('packages')){ const packed=JSON.parse(execFileSync('npm',['pack',stage,'--ignore-scripts','--json','--pack-destination',output],{encoding:'utf8'}))[0];const bytes=readFileSync(join(output,packed.filename)); artifacts.push({package:manifest.name,version:artifactVersion,file:packed.filename,bytes:bytes.length,sha256:createHash('sha256').update(bytes).digest('hex'),files:packed.files.map(f=>f.path),dependencies:manifest.dependencies,optionalPeers:Object.keys(manifest.peerDependenciesMeta??{})}); } +// Web consumers load fonts from @bloxwap/font; native faces ship only in @bloxwap/hig-native-fonts (#64). +const fontFile=/\.(?:ttf|otf|woff2?)$/i; +for(const artifact of artifacts.filter(a=>a.package!=='@bloxwap/hig-native-fonts'))if(artifact.files.some(f=>fontFile.test(f)))throw Error(`Font binary in web/shared package ${artifact.package}`); +const tokensArtifact=artifacts.find(a=>a.package==='@bloxwap/hig-tokens'),nativeFontsArtifact=artifacts.find(a=>a.package==='@bloxwap/hig-native-fonts'); +const packedSizes={webTokens:{file:tokensArtifact.file,bytes:tokensArtifact.bytes,fontFiles:0},nativeFonts:{file:nativeFontsArtifact.file,bytes:nativeFontsArtifact.bytes,fontFiles:nativeFontsArtifact.files.filter(f=>fontFile.test(f)).length}}; const blockers=JSON.parse(readFileSync('planning/release-blockers.json','utf8')); cpSync('apps/docs/dist',join(output,'docs'),{recursive:true}); -const release={dryRun:true,published:false,tagsCreated:false,releasesCreated:false,channel,sourceCommit:commit,sourceVersion,artifactVersion,docsGuidelineVersion:sourceVersion,versionPolicy:'Prerelease package metadata references the same source/contract/docs candidate version and commit; no source versions or git refs are mutated.',artifacts,quality:report.sizes,publishable:false,blockers,rollback:'No deployment/publication occurred. Owners retain previous lockfile/artifacts and pilot flags; use docs/governance/release.md for rollout rollback.'}; +const release={dryRun:true,published:false,tagsCreated:false,releasesCreated:false,channel,sourceCommit:commit,sourceVersion,artifactVersion,docsGuidelineVersion:sourceVersion,versionPolicy:'Prerelease package metadata references the same source/contract/docs candidate version and commit; no source versions or git refs are mutated.',artifacts,packedSizes,quality:report.sizes,publishable:false,blockers,rollback:'No deployment/publication occurred. Owners retain previous lockfile/artifacts and pilot flags; use docs/governance/release.md for rollout rollback.'}; writeFileSync(join(output,'report.json'),JSON.stringify(release,null,2)+'\n'); writeFileSync('evidence/release-dry-run.json',JSON.stringify(release,null,2)+'\n'); writeFileSync(join(output,'CHANGELOG.md'),`# ${artifactVersion} (${channel}, dry run)\n\nSource: ${commit}; HIG contract/docs ${sourceVersion}.\n\nSemantic tokens and separate renderers; optional feedback; docs/playground/gallery; migration/quality governance. No npm publication, release, tag or deployment.\n\nStable publication is blocked by task policy and pending source/device/adoption evidence.\n`); diff --git a/scripts/source-metro-baseline.mjs b/scripts/source-metro-baseline.mjs index 020c2e3..b564111 100644 --- a/scripts/source-metro-baseline.mjs +++ b/scripts/source-metro-baseline.mjs @@ -13,7 +13,7 @@ writeFileSync(join(directory,'app.json'),JSON.stringify({expo:{name:'HIG source copyFileSync('babel.config.cjs',join(directory,'babel.config.cjs')); if(!existsSync(join(directory,'node_modules')))symlinkSync(resolve('node_modules'),join(directory,'node_modules'),'dir'); writeFileSync(join(directory,'metro.config.cjs'),`const {getDefaultConfig}=require('expo/metro-config');const path=require('node:path');const config=getDefaultConfig(__dirname);config.watchFolders=[${JSON.stringify(root)}];config.resolver.nodeModulesPaths=[${JSON.stringify(resolve('node_modules'))}];config.resolver.resolveRequest=(context,name,platform)=>{if(name==='@workspace/tokens'||name.startsWith('@workspace/tokens/'))return {type:'sourceFile',filePath:path.join(${JSON.stringify(resolve('.local/monorepo/packages/tokens/src'))},(name==='@workspace/tokens'?'index':name.slice('@workspace/tokens/'.length))+'.ts')};return context.resolveRequest(context,name,platform);};module.exports=config;`); -const faces=`{'Bloxwap Sans 400':require('@bloxwap/hig-tokens/fonts/BloxwapSans.ttf'),'Bloxwap Sans 600':require('@bloxwap/hig-tokens/fonts/BloxwapSans-SemiBold.ttf')}`; +const faces=`{'Bloxwap Sans 400':require('@bloxwap/hig-native-fonts/fonts/BloxwapSans.ttf'),'Bloxwap Sans 600':require('@bloxwap/hig-native-fonts/fonts/BloxwapSans-SemiBold.ttf')}`; const results=[]; function files(path){return readdirSync(path,{withFileTypes:true}).flatMap(e=>e.isDirectory()?files(join(path,e.name)):[join(path,e.name)]);} for(const stage of ['before','hig']){ diff --git a/tests/quality.test.ts b/tests/quality.test.ts index e7c40dd..c54d3d5 100644 --- a/tests/quality.test.ts +++ b/tests/quality.test.ts @@ -12,3 +12,4 @@ it('fails a deliberate budget regression',()=>{expect(()=>assertBudget('button', it('fails cross-platform imports instead of silently skipping native checks',()=>{const directory=mkdtempSync(join(tmpdir(),'hig-invalid-platform-'));mkdirSync(join(directory,'dist'));writeFileSync(join(directory,'dist','button.js'),"import {View} from 'react-native';");expect(()=>checkPlatformImports(directory,'web')).toThrow('Invalid web platform import');}); it('rejects copied tokens/obsolete imports and expired exceptions',()=>{const copied="import { Button } from '@workspace/ui/button'; const green='#00ff3f';";expect(driftViolations(copied,'surface.tsx',[])).toHaveLength(2);expect(driftViolations(copied,'surface.tsx',[{path:'surface.tsx',rule:'copied-palette',owner:'maintainer',reason:'approved',expires:'2000-01-01'}])).toHaveLength(2);}); it('a deliberately stale generated token artifact fails the real anti-drift command',()=>{const directory=mkdtempSync(join(tmpdir(),'hig-stale-token-'));mkdirSync(join(directory,'packages/tokens'),{recursive:true});writeFileSync(join(directory,'packages/tokens/source.json'),readFileSync('packages/tokens/source.json'));writeFileSync(join(directory,'packages/tokens/upstream-palette.json'),readFileSync('packages/tokens/upstream-palette.json'));const generator=resolve('scripts/generate.mjs');expect(spawnSync(process.execPath,[generator],{cwd:directory}).status).toBe(0);writeFileSync(join(directory,'packages/tokens/generated/tokens.css'),'stale');const check=spawnSync(process.execPath,[resolve('scripts/check-drift.mjs')],{cwd:directory,encoding:'utf8'});expect(check.status).not.toBe(0);expect(check.stderr).toContain('Stale generated artifact');}); +it('a stale radius/type/motion/density role fails the real anti-drift command (#66)',()=>{const directory=mkdtempSync(join(tmpdir(),'hig-stale-role-'));mkdirSync(join(directory,'packages/tokens'),{recursive:true});writeFileSync(join(directory,'packages/tokens/source.json'),readFileSync('packages/tokens/source.json'));writeFileSync(join(directory,'packages/tokens/upstream-palette.json'),readFileSync('packages/tokens/upstream-palette.json'));const generator=resolve('scripts/generate.mjs');expect(spawnSync(process.execPath,[generator],{cwd:directory}).status).toBe(0);const css=join(directory,'packages/tokens/generated/tokens.css');writeFileSync(css,readFileSync(css,'utf8').replace('--hig-motion-duration-fast: 180ms;','--hig-motion-duration-fast: 200ms;'));const check=spawnSync(process.execPath,[resolve('scripts/check-drift.mjs')],{cwd:directory,encoding:'utf8'});expect(check.status).not.toBe(0);expect(check.stderr).toContain('Stale generated artifact');}); diff --git a/tests/tokens.test.ts b/tests/tokens.test.ts index ce8bbe7..9d0218f 100644 --- a/tests/tokens.test.ts +++ b/tests/tokens.test.ts @@ -2,6 +2,8 @@ import {describe,it,expect} from 'vitest'; import {tokens,nativeColors,withAlpha} from '../packages/tokens/src/index'; import {formatNumber,nativeFont} from '../packages/contracts/src/index'; import contrast from '../evidence/contrast.json'; +import {fontManifest} from '../packages/native-fonts/src/index'; +import {readFileSync,existsSync} from 'node:fs'; describe('platform contracts',()=>{ it('keeps direction aliases',()=>{expect(tokens.colors.buy.srgb).toBe(tokens.colors.positive.srgb);expect(tokens.colors.sell.srgb).toBe('#ff5c5c');}); it('keeps CSS out of RN',()=>{for(const color of Object.values(nativeColors.android))expect(color).toMatch(/^#[0-9a-f]{6}$/);expect(nativeColors.iosP3.action.components).toEqual([0,1,.24]);}); @@ -10,3 +12,40 @@ describe('platform contracts',()=>{ it('preserves unknown/zero/locale',()=>{expect(formatNumber(null)).toBe('—');expect(formatNumber(0)).toBe('0');expect(formatNumber(1234.5,'de-DE')).toBe('1.234,5');}); it('requires registered native faces',()=>{expect(()=>nativeFont({version:'0.1.2',faces:[]},'Bloxwap Sans',400)).toThrow();}); }); +describe('token CSS roles (#65, #66)',()=>{ + const css=readFileSync('packages/tokens/generated/tokens.css','utf8'); + const root=css.slice(0,css.indexOf('}')); + it('defines radius, type, motion and density roles from source.json',()=>{ + for(const [k,v] of Object.entries(tokens.radius))expect(root).toContain(`--hig-radius-${k}: ${v>=9999?`${v}px`:`${v/16}rem`};`); + for(const [k,v] of Object.entries(tokens.type)){expect(root).toContain(`--hig-type-${k}-size: ${v.size/16}rem;`);expect(root).toContain(`--hig-type-${k}-line-height: ${v.lineHeight/16}rem;`);expect(root).toContain(`--hig-type-${k}-weight: ${v.weight};`);expect(root).toContain(`--hig-type-${k}: ${v.weight} ${v.size/16}rem/${v.lineHeight/16}rem var(--hig-font-sans);`);} + for(const [k,v] of Object.entries(tokens.motion))expect(root).toContain(`--hig-motion-duration-${k}: ${v}ms;`); + expect(root).toContain('--hig-motion-easing-standard: cubic-bezier(0.22, 1, 0.36, 1);'); + for(const [mode,metrics] of Object.entries(tokens.density))for(const [k,v] of Object.entries(metrics))expect(root).toContain(`--hig-density-${mode}-${k}: ${v/16}rem;`); + expect(css).toContain("[data-hig-density='compact'] { --hig-density-control: var(--hig-density-compact-control);"); + }); + it('zeroes motion durations for reduced motion',()=>{ + const reduced=Object.keys(tokens.motion).map(k=>`--hig-motion-duration-${k}: 0ms;`).join(' '); + expect(css).toContain(`@media (prefers-reduced-motion: reduce) { :root { ${reduced} } }`); + expect(css).toContain(`[data-hig-motion='reduced'] { ${reduced} }`); + }); + it('exposes font roles that prefer the @bloxwap/font Next variables, then literal families',()=>{ + expect(root).toContain("--hig-font-sans: var(--font-bloxwap-sans, 'Bloxwap Sans'), system-ui, sans-serif;"); + expect(root).toContain("--hig-font-mono: var(--font-bloxwap-mono, 'Bloxwap Mono'), ui-monospace, monospace;"); + const controls=readFileSync('packages/react/src/styles.css','utf8'); + expect(controls).toContain("font:600 1rem/1.5 var(--hig-font-sans,'Bloxwap Sans',system-ui,sans-serif)"); + expect(controls).toContain("font-family:var(--hig-font-mono,'Bloxwap Mono',monospace)"); + expect(controls).not.toMatch(/font(?:-family)?:[^;{}]*?(?{ + it('keeps font binaries out of the web tokens package',()=>{ + const pkg=JSON.parse(readFileSync('packages/tokens/package.json','utf8')); + expect(Object.keys(pkg.exports).some(k=>k.startsWith('./fonts'))).toBe(false); + expect(existsSync('packages/tokens/assets/fonts')).toBe(false); + }); + it('ships the typed manifest that resolves registered faces',()=>{ + expect(fontManifest).toEqual(JSON.parse(readFileSync('packages/native-fonts/fonts/manifest.json','utf8'))); + expect(nativeFont(fontManifest,'BloxwapSans',600)).toBe('BloxwapSansSemiBold'); + expect(Object.keys(fontManifest.outputs)).toHaveLength(15); + }); +}); From 07669e08c775d3672093c7898e42e5b5ad3f4ec5 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Thu, 15 Oct 2026 08:38:43 -0700 Subject: [PATCH 2/4] evidence: packed Next font browser check (#65) Co-Authored-By: Claude Opus 5.5 --- evidence/packed-next-browser.json | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 evidence/packed-next-browser.json diff --git a/evidence/packed-next-browser.json b/evidence/packed-next-browser.json new file mode 100644 index 0000000..88da319 --- /dev/null +++ b/evidence/packed-next-browser.json @@ -0,0 +1,26 @@ +{ + "version": "0.1.0", + "framework": "Next 16.4.0 webpack", + "consumer": "packed tarballs in a clean temporary app", + "checks": [ + "production server/client boundary", + "no hydration/page/console errors", + "raw locale amount retained", + "keyboard accepted action once", + "Bloxwap Sans/Mono Next loaders compiled", + ".hig-button/.hig-number computed font is a loaded Bloxwap face (document.fonts.check + FontFace status)" + ], + "fonts": { + "button": { + "fontFamily": "BloxwapSans, \"Bloxwap Sans Scripts\", ui-rounded, system-ui, sans-serif, system-ui, sans-serif", + "check": true, + "loaded": true + }, + "number": { + "fontFamily": "BloxwapMono, \"Bloxwap Mono Scripts\", ui-monospace, SFMono-Regular, Menlo, monospace, ui-monospace, monospace", + "check": true, + "loaded": true + } + }, + "browser": "141.0.7390.37" +} From a356fcc10bc1404f842f9ed20ad72d5227df9570 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Thu, 15 Oct 2026 08:40:06 -0700 Subject: [PATCH 3/4] docs: drop the HIG font bridge now that token font roles resolve the Next loader (#65) Co-Authored-By: Claude Opus 5.5 --- apps/docs/app/global.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/docs/app/global.css b/apps/docs/app/global.css index a5d1e7b..cb082b4 100644 --- a/apps/docs/app/global.css +++ b/apps/docs/app/global.css @@ -17,5 +17,3 @@ .brand-name small{font-size:.7rem;font-weight:400;color:var(--hig-muted)} .docs-version{padding-top:.75rem;font-size:.7rem;color:var(--hig-muted)} -.hig-button { font-family:var(--font-bloxwap-sans),system-ui,sans-serif; } -.hig-number { font-family:var(--font-bloxwap-mono),monospace; } From 360ce2844b71f5b38b27a5238e9c5fa22339d568 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Thu, 15 Oct 2026 09:07:31 -0700 Subject: [PATCH 4/4] docs: regenerate content after rebase Co-Authored-By: Claude Opus 5.5 --- apps/docs/src/content.json | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/docs/src/content.json b/apps/docs/src/content.json index 65ed034..241c99d 100644 --- a/apps/docs/src/content.json +++ b/apps/docs/src/content.json @@ -403,7 +403,7 @@ "route": "guides/foundations/platform", "title": "Foundation and platform rules · HIG 0.1.0", "source": "docs/foundations/platform.md", - "html": "

Foundation and platform rules · HIG 0.1.0

\n

Color (#6)

\n

Dark is the only audited mode. Source values retain the commit-pinned monorepo authored OKLCH and brand channels; see source reconciliation. Use surface/text/action/focus/buy/sell/warning roles, never infer intent from a hue. Web emits sRGB then P3 in @supports. RN uses explicit hex values; iOS Display-P3 hosts use the generated iosDisplayP3 channel hex map; hosts using sRGB retain ios/Android values. Android and third-party images use sRGB. evidence/contrast.json records all 33 approved normal-text pairings. Faint text is restricted to background/surface: faint on nested fails normal-text AA, so use muted on nested instead. Border is decorative; meaningful control edges/focus must use tested text or focus tones. Do use onAction on green fills; don't put white body text on green.

\n

Typography (#7)

\n

Pin @bloxwap/font 0.1.2. Vite: import @bloxwap/font/css; Next: upstream Sans/Mono loaders own loading and script fallback. Sans for prose/controls; Mono for code and tabular readouts; Pixel for a short nonessential accent. Body 16/24, label 14/20, caption 12/18, title 24/30, display 48/52 are candidate role mappings based on SDK scale. Web rem uses 16px root; RN sizes are explicit dp and allow font scaling. Do not disable Dynamic Type or fixed-height clip translated labels. Run the pinned native font generator and register its static TTF faces from the optional tokens/fonts export; the generated manifest records weight/family/source hashes. Published WOFF2/Next loaders are not directly native assets. Do wait for registration before applying a custom native family; don't invent a family string and claim the font rendered. Arabic/Hebrew/CJK companions are owned upstream; load required scripts, test mixed-script punctuation and RTL. Large-text and device font evidence is pending.

\n

Geometry and layout (#8)

\n

Spaces 4/8/12/16/20/24/32/48; radius 6/8/10/14 and pill. Comfortable control/target 48; compact 36, reserved for pointer/keyboard dense layouts. Painted icons can be 16/20/24 while targets stay 48 on touch. Never expand adjacent invisible hit areas so they overlap. Use logical inline spacing, wrapping labels, safe-area insets, keyboard avoidance and a scrollable native sheet; preserve portrait/tablet rotation without locking orientation. Pro tables may scroll with column headers; app amount entry reflows. Elevation uses nested fills rather than unverified shadows. Layer order: document, sticky nav, contextual popover, modal, notification. Browser top layer handles modal stacking; native Modal remains platform-owned.

\n

Motion and gestures (#9)

\n

Fast 180ms, base 260ms, slow 320ms; web easing cubic-bezier(.22,1,.36,1). Press response ends on release/cancel; action executes on accepted activation, never press-in. Drag/scroll cancellation clears visual and feedback state. Interrupt transitions from current state instead of queuing. Reduced motion uses immediate state changes, static skeletons and unanimated numeric readouts. Outcome messages remain explicit. Do animate deliberate disclosure once; don't animate or play sound for every live-data update. Native candidate uses static transitions until cancellation/device animation evidence exists; motion preference is independent from sound/haptics.

\n

Accessibility and localization (#10)

\n

Baseline: WCAG 2.2 AA, Recommendation 12 December 2024. Normal text 4.5:1; large text and meaningful non-text UI 3:1. Touch baseline 48 logical units exceeds the web minimum. Provide visible focus, semantic labels/descriptions, logical keyboard order and non-drag alternatives. Native roles/state/value map to VoiceOver/TalkBack. No tooltip-only critical text. Restore trigger focus on modal dismissal; Escape/native back closes topmost cancellable overlay. Host owns safe-area, locale, parsing, route and keyboard adapters. Intl display is not money arithmetic. Vocabulary is locked in the trading glossary (docs/foundations/glossary.json) before any locale translation. Number, currency, date, leverage, percent and RTL formatting is in the localization guide.

\n

Component review checklist: keyboard completion; labeled controls/errors; busy/disabled; focus restoration; narrow viewport; 200% text; long translations; Arabic/Hebrew RTL; mixed scripts; reduced motion; sound off; haptics off; unknown/stale/error values; VoiceOver/TalkBack on physical devices. Automated results and physical observations must be separate. Exception requires owner, affected surface, reason, alternative and expiry. No observed device evidence means pending, never pass.

\n

Brand and icons (#11)

\n

Approved mark is copied verbatim from the pinned Font docs SVG, including embedded provenance. Import @bloxwap/hig-tokens/mark.svg on web; both web and RN geometry renderers are available from their brand entries. Keep aspect ratio and clear space of one quarter mark width. Decorative marks/icons are hidden from accessibility; icon-only actions require an explicit accessible name and 48-unit target. Use currentColor for utility icons, never tint the approved brand mark arbitrarily. Directional arrows mirror in RTL; brand, check and financial direction do not. Icons repository contains a large catalog; do not import it wholesale. Only reviewed single glyphs belong in runtime exports. Favicon/OG derivatives have source provenance and drift checks. Physical device visual comparison remains unperformed.

\n

Font-loading note: @bloxwap/font/css references all supported families/scripts, which may increase deployed asset bytes even with on-demand browser loading. The Latin web fixture instead uses two explicit @font-face rules with package asset URLs (Sans and Mono). Add only the scripts/italics used by each consumer. Do not assume a Latin-only fixture certifies mixed-script rendering.

\n", + "html": "

Foundation and platform rules · HIG 0.1.0

\n

Color (#6)

\n

Dark is the only audited mode. Source values retain the commit-pinned monorepo authored OKLCH and brand channels; see source reconciliation. Use surface/text/action/focus/buy/sell/warning roles, never infer intent from a hue. Web emits sRGB then P3 in @supports. RN uses explicit hex values; iOS Display-P3 hosts use the generated iosDisplayP3 channel hex map; hosts using sRGB retain ios/Android values. Android and third-party images use sRGB. evidence/contrast.json records all 33 approved normal-text pairings. Faint text is restricted to background/surface: faint on nested fails normal-text AA, so use muted on nested instead. Border is decorative; meaningful control edges/focus must use tested text or focus tones. Do use onAction on green fills; don't put white body text on green.

\n

Typography (#7)

\n

Pin @bloxwap/font 0.1.2. Vite: import @bloxwap/font/css; Next: upstream Sans/Mono loaders own loading and script fallback. Sans for prose/controls; Mono for code and tabular readouts; Pixel for a short nonessential accent. Body 16/24, label 14/20, caption 12/18, title 24/30, display 48/52 are candidate role mappings based on SDK scale. Web CSS exposes the roles as --hig-type-<role>-size, -line-height, -weight and a --hig-type-<role> font shorthand, and the families as --hig-font-sans/--hig-font-mono. Those resolve the @bloxwap/font Next loader variables first and fall back to the literal 'Bloxwap Sans'/'Bloxwap Mono' families, so HIG controls render the loaded face in both Next and plain @font-face hosts. Web rem uses 16px root; RN sizes are explicit dp and allow font scaling. Do not disable Dynamic Type or fixed-height clip translated labels. Run the pinned native font generator and register its static TTF faces from @bloxwap/hig-native-fonts; its typed manifest records weight/family/source hashes. Web hosts never install that package. Published WOFF2/Next loaders are not directly native assets. Do wait for registration before applying a custom native family; don't invent a family string and claim the font rendered. Arabic/Hebrew/CJK companions are owned upstream; load required scripts, test mixed-script punctuation and RTL. Large-text and device font evidence is pending.

\n

Geometry and layout (#8)

\n

Spaces 4/8/12/16/20/24/32/48; radius 6/8/10/14 and pill (--hig-radius-sm…--hig-radius-xl, --hig-radius-pill). Comfortable control/target 48 and gap 12; compact 36 and gap 8, reserved for pointer/keyboard dense layouts. CSS exposes each mode as --hig-density-<mode>-<control|target|gap>, and --hig-density-control, --hig-density-target and --hig-density-gap follow the nearest [data-hig-density], which the React provider sets. Painted icons can be 16/20/24 while targets stay 48 on touch. Never expand adjacent invisible hit areas so they overlap. Use logical inline spacing, wrapping labels, safe-area insets, keyboard avoidance and a scrollable native sheet; preserve portrait/tablet rotation without locking orientation. Pro tables may scroll with column headers; app amount entry reflows. Elevation uses nested fills rather than unverified shadows. Layer order: document, sticky nav, contextual popover, modal, notification. Browser top layer handles modal stacking; native Modal remains platform-owned.

\n

Motion and gestures (#9)

\n

Fast 180ms, base 260ms, slow 320ms; web easing cubic-bezier(.22,1,.36,1). CSS exposes --hig-motion-duration-<fast|base|slow> and --hig-motion-easing-standard. The durations become 0ms under prefers-reduced-motion: reduce and inside [data-hig-motion='reduced'], which the React provider sets when the host or OS asks for reduced motion. Press response ends on release/cancel; action executes on accepted activation, never press-in. Drag/scroll cancellation clears visual and feedback state. Interrupt transitions from current state instead of queuing. Reduced motion uses immediate state changes, static skeletons and unanimated numeric readouts. Outcome messages remain explicit. Do animate deliberate disclosure once; don't animate or play sound for every live-data update. Native candidate uses static transitions until cancellation/device animation evidence exists; motion preference is independent from sound/haptics.

\n

Accessibility and localization (#10)

\n

Baseline: WCAG 2.2 AA, Recommendation 12 December 2024. Normal text 4.5:1; large text and meaningful non-text UI 3:1. Touch baseline 48 logical units exceeds the web minimum. Provide visible focus, semantic labels/descriptions, logical keyboard order and non-drag alternatives. Native roles/state/value map to VoiceOver/TalkBack. No tooltip-only critical text. Restore trigger focus on modal dismissal; Escape/native back closes topmost cancellable overlay. Host owns safe-area, locale, parsing, route and keyboard adapters. Intl display is not money arithmetic. Vocabulary is locked in the trading glossary (docs/foundations/glossary.json) before any locale translation. Number, currency, date, leverage, percent and RTL formatting is in the localization guide.

\n

Component review checklist: keyboard completion; labeled controls/errors; busy/disabled; focus restoration; narrow viewport; 200% text; long translations; Arabic/Hebrew RTL; mixed scripts; reduced motion; sound off; haptics off; unknown/stale/error values; VoiceOver/TalkBack on physical devices. Automated results and physical observations must be separate. Exception requires owner, affected surface, reason, alternative and expiry. No observed device evidence means pending, never pass.

\n

Brand and icons (#11)

\n

Approved mark is copied verbatim from the pinned Font docs SVG, including embedded provenance. Import @bloxwap/hig-tokens/mark.svg on web; both web and RN geometry renderers are available from their brand entries. Keep aspect ratio and clear space of one quarter mark width. Decorative marks/icons are hidden from accessibility; icon-only actions require an explicit accessible name and 48-unit target. Use currentColor for utility icons, never tint the approved brand mark arbitrarily. Directional arrows mirror in RTL; brand, check and financial direction do not. Icons repository contains a large catalog; do not import it wholesale. Only reviewed single glyphs belong in runtime exports. Favicon/OG derivatives have source provenance and drift checks. Physical device visual comparison remains unperformed.

\n

Font-loading note: @bloxwap/font/css references all supported families/scripts, which may increase deployed asset bytes even with on-demand browser loading. The Latin web fixture instead uses two explicit @font-face rules with package asset URLs (Sans and Mono). Add only the scripts/italics used by each consumer. Do not assume a Latin-only fixture certifies mixed-script rendering.

\n", "toc": [ { "label": "Color (#6)", @@ -469,7 +469,7 @@ "route": "guides/foundations/source-reconciliation", "title": "Pinned source reconciliation · HIG 0.1.0", "source": "docs/foundations/source-reconciliation.md", - "html": "

Pinned source reconciliation · HIG 0.1.0

\n

Issues #2, #6–#9, #11–#18. See evidence/issue-1-18/monorepo-sha.txt for the exact monorepo commit and monorepo-source-fetch.json for all 129 audited paths. This supersedes the earlier 404 source-access limitation; it does not turn old unperformed product/device checks into passes.

\n

The audit covers tokens, shared web/native primitives, plain-ui and app/Pro-owned controls. monorepo-audit.json records byte-identical duplicate groups, source bytes, repository asset totals and explicit source component dispositions. The existing catalog/platform matrix defines the HIG destination families. The authenticated organization inventory now includes monorepo, .github-private, taptradetv and tapsiren; their complete observed trees and explicit applicability decisions are recorded in new-repositories.json and planning/adoption.json. Named owner approval remains an organizational blocker, not a fabricated assignment. Confetti, signal strength, animated number backends and overlay layering internals stay optional host integrations; they are not silently added to base imports. Migration owners remain the owning product teams; HIG cannot claim their source forks have been deleted.

\n

Tokens and platform units

\n

packages/tokens/upstream-palette.json preserves the complete authored semantic OKLCH palette and brand sRGB/P3 channels. sourceColors exports web strings plus converted Android sRGB and iOS Display-P3 channel values; generated CSS exposes authored --hig-source-* variables. The HIG surface, nested, text and muted roles now retain their corresponding authored OKLCH values and tested sRGB fallback. Brand green and blue preserve upstream P3 channels. HIG focus green, market sell red, warning gold, opaque border and faint text are intentional intent/accessibility mappings rather than mislabeled exact source copies; direction remains distinct from destructive intent.

\n

Native views default to sRGB and use nativeColors.android/ios. A host that explicitly configures RN iOS default color space as Display-P3 must use nativeColors.iosDisplayP3 throughout, including converted third-party sRGB assets. Never send CSS functions to native views or use P3 channel hex in an sRGB host. These are mathematical conversion checks, not physical wide-gamut display certification.

\n

Source spacing.ts and type.ts explicitly set both rem roots to 16; native spacing md=12 and base type=16 match HIG. Radius 6/8/10/14 and motion 180/260/320 ms also match. HIG caption line-height 18, title 30 and display 52 intentionally provide readable role leading versus the stock utility steps. Comfortable 48-unit native targets and compact 36-pixel desktop targets remain distinct; compact native buttons still retain 48-unit hit targets. Source native sheets use optional Gorhom/Reanimated gesture backends, 320 ms card transitions and separate 500 ms opening motion. HIG's basic Sheet stays a static, scrollable Modal alternative. The isolated gesture-sheet entry now supplies an optional Gorhom adapter with detents, cancellation delegation, native back, safe-area/keyboard configuration, reduced-motion mapping and focus requests. Its mocked lifecycle and real Metro imports are software-verified; actual worklet, pan interruption and assistive-technology behavior still require a booted native target.

\n

Native font generation and registration

\n

Install fonttools[woff]==4.61.1 and brotli==1.2.0, then run npm run generate:fonts before native consumer commands. scripts/generate-native-fonts.py derives static faces exclusively from the installed @bloxwap/font@0.1.2 variable WOFF2 files, verifying all four input hashes before writing. It adapts the monorepo's generator and preserves its naming/weight metadata. The generated manifest contains input/output SHA256, PostScript names, Expo registration keys, weights, scripts and byte counts; OFL attribution ships beside the faces. Generated TTF files are build outputs, not per-repository committed binary copies. Local tarballs include them through @bloxwap/hig-tokens/fonts/*; consumers register only needed faces.

\n

The Expo fixture registers Sans Regular/SemiBold and Mono Regular through expo-font, waits for loading, exposes failure visibly and passes explicit 400/600 family keys to the native provider. Text, headings and buttons select registered static weight families, avoiding an invalid variable family name or synthesized weight. Expo web uses feedback.web.ts browser adapters; Android/iOS retain the native backend. Korean/Simplified Chinese Regular/SemiBold faces can be registered by host locale policy; other scripts use an explicit host/system fallback. Pixel remains a scoped web accent, not a native body-font requirement. Font registration in Expo web and Metro builds does not certify native rasterization, native Dynamic Type, or multilingual physical-device layout.

\n

Observation boundaries

\n

Headless browser synthetic composition checks preserve raw negative/decimal locale strings without formatting or precision loss; real on-screen IME composition is a separate device check. Browser 200% root text, narrow width and RTL checks exercise DOM reflow, not OS text scaling. Jest inspects RN roles, states and handlers with native modules mocked; Expo web exercises RN web rendering; neither is TalkBack. Android emulator installation, startup and any observed OS semantics are reported separately in evidence/issue-1-18/. Physical iOS/Android, tactile output and real VoiceOver/TalkBack observations remain unperformed on this Linux VM.

\n

The root build now compiles contracts/tokens before feedback and platform renderers, so clean builds cannot rely on stale dist output. The expanded browser flow runs in Chromium/Firefox/WebKit under 320-pixel width, translated RTL tabs and 200% root text. It checks required validation, synthetic composition/raw precision, keyboard range/radio/menu/search, nested focus restoration, persistence/retry and zero automated axe WCAG findings. The Expo-web gallery separately checks registered faces, Modal picker, labels and range keyboard values. The native Slider web backend needs explicit ARIA min/max/now and keyboard actions; native OS builds retain adjustable role/state/value semantics. Checkbox/radio/tab web aliases likewise preserve native-renderer state in RN web. None of these observations is labeled as TalkBack. Quality gates verify every generated native face hash and cap the three selected fixture faces at 1.4 MB; optional CJK/other weight faces are available but are not imported by the default fixture.

\n", + "html": "

Pinned source reconciliation · HIG 0.1.0

\n

Issues #2, #6–#9, #11–#18. See evidence/issue-1-18/monorepo-sha.txt for the exact monorepo commit and monorepo-source-fetch.json for all 129 audited paths. This supersedes the earlier 404 source-access limitation; it does not turn old unperformed product/device checks into passes.

\n

The audit covers tokens, shared web/native primitives, plain-ui and app/Pro-owned controls. monorepo-audit.json records byte-identical duplicate groups, source bytes, repository asset totals and explicit source component dispositions. The existing catalog/platform matrix defines the HIG destination families. The authenticated organization inventory now includes monorepo, .github-private, taptradetv and tapsiren; their complete observed trees and explicit applicability decisions are recorded in new-repositories.json and planning/adoption.json. Named owner approval remains an organizational blocker, not a fabricated assignment. Confetti, signal strength, animated number backends and overlay layering internals stay optional host integrations; they are not silently added to base imports. Migration owners remain the owning product teams; HIG cannot claim their source forks have been deleted.

\n

Tokens and platform units

\n

packages/tokens/upstream-palette.json preserves the complete authored semantic OKLCH palette and brand sRGB/P3 channels. sourceColors exports web strings plus converted Android sRGB and iOS Display-P3 channel values; generated CSS exposes authored --hig-source-* variables. The HIG surface, nested, text and muted roles now retain their corresponding authored OKLCH values and tested sRGB fallback. Brand green and blue preserve upstream P3 channels. HIG focus green, market sell red, warning gold, opaque border and faint text are intentional intent/accessibility mappings rather than mislabeled exact source copies; direction remains distinct from destructive intent.

\n

Native views default to sRGB and use nativeColors.android/ios. A host that explicitly configures RN iOS default color space as Display-P3 must use nativeColors.iosDisplayP3 throughout, including converted third-party sRGB assets. Never send CSS functions to native views or use P3 channel hex in an sRGB host. These are mathematical conversion checks, not physical wide-gamut display certification.

\n

Source spacing.ts and type.ts explicitly set both rem roots to 16; native spacing md=12 and base type=16 match HIG. Radius 6/8/10/14 and motion 180/260/320 ms also match. HIG caption line-height 18, title 30 and display 52 intentionally provide readable role leading versus the stock utility steps. Comfortable 48-unit native targets and compact 36-pixel desktop targets remain distinct; compact native buttons still retain 48-unit hit targets. Source native sheets use optional Gorhom/Reanimated gesture backends, 320 ms card transitions and separate 500 ms opening motion. HIG's basic Sheet stays a static, scrollable Modal alternative. The isolated gesture-sheet entry now supplies an optional Gorhom adapter with detents, cancellation delegation, native back, safe-area/keyboard configuration, reduced-motion mapping and focus requests. Its mocked lifecycle and real Metro imports are software-verified; actual worklet, pan interruption and assistive-technology behavior still require a booted native target.

\n

Native font generation and registration

\n

Install fonttools[woff]==4.61.1 and brotli==1.2.0, then run npm run generate:fonts before native consumer commands. scripts/generate-native-fonts.py derives static faces exclusively from the installed @bloxwap/font@0.1.2 variable WOFF2 files, verifying all four input hashes before writing. It adapts the monorepo's generator and preserves its naming/weight metadata. The generated manifest contains input/output SHA256, PostScript names, Expo registration keys, weights, scripts and byte counts; OFL attribution ships beside the faces. Generated TTF files are build outputs, not per-repository committed binary copies. Local tarballs include them through @bloxwap/hig-native-fonts/fonts/* (moved out of the web tokens package in #64); consumers register only needed faces.

\n

The Expo fixture registers Sans Regular/SemiBold and Mono Regular through expo-font, waits for loading, exposes failure visibly and passes explicit 400/600 family keys to the native provider. Text, headings and buttons select registered static weight families, avoiding an invalid variable family name or synthesized weight. Expo web uses feedback.web.ts browser adapters; Android/iOS retain the native backend. Korean/Simplified Chinese Regular/SemiBold faces can be registered by host locale policy; other scripts use an explicit host/system fallback. Pixel remains a scoped web accent, not a native body-font requirement. Font registration in Expo web and Metro builds does not certify native rasterization, native Dynamic Type, or multilingual physical-device layout.

\n

Observation boundaries

\n

Headless browser synthetic composition checks preserve raw negative/decimal locale strings without formatting or precision loss; real on-screen IME composition is a separate device check. Browser 200% root text, narrow width and RTL checks exercise DOM reflow, not OS text scaling. Jest inspects RN roles, states and handlers with native modules mocked; Expo web exercises RN web rendering; neither is TalkBack. Android emulator installation, startup and any observed OS semantics are reported separately in evidence/issue-1-18/. Physical iOS/Android, tactile output and real VoiceOver/TalkBack observations remain unperformed on this Linux VM.

\n

The root build now compiles contracts/tokens before feedback and platform renderers, so clean builds cannot rely on stale dist output. The expanded browser flow runs in Chromium/Firefox/WebKit under 320-pixel width, translated RTL tabs and 200% root text. It checks required validation, synthetic composition/raw precision, keyboard range/radio/menu/search, nested focus restoration, persistence/retry and zero automated axe WCAG findings. The Expo-web gallery separately checks registered faces, Modal picker, labels and range keyboard values. The native Slider web backend needs explicit ARIA min/max/now and keyboard actions; native OS builds retain adjustable role/state/value semantics. Checkbox/radio/tab web aliases likewise preserve native-renderer state in RN web. None of these observations is labeled as TalkBack. Quality gates verify every generated native face hash and cap the three selected fixture faces at 1.4 MB; optional CJK/other weight faces are available but are not imported by the default fixture.

\n", "toc": [ { "label": "Tokens and platform units", @@ -548,7 +548,7 @@ "route": "guides/governance/new-repository", "title": "New repository setup and upgrades · HIG 0.1.0", "source": "docs/governance/new-repository.md", - "html": "

New repository setup and upgrades · HIG 0.1.0

\n

Issue #36. These steps adopt HIG in a new or existing Bloxwap repository without copying component folders, palettes, fonts or feedback engines. The five SDK docs sites followed this path: bloxwap/hyperliquid#180, bloxwap/chart#53, bloxwap/font#11, bloxwap/sfx#50 and bloxwap/haptic#26.

\n

1. Pick applicability

\n

Add or update the repository's row in planning/adoption.json (a PR to bloxwap/hig) with one of these applicability values:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
ApplicabilityInstallNever add
Product runtime UI (web)@bloxwap/hig-tokens, -contracts, -react; @bloxwap/fontCopied component folders or token CSS
Product runtime UI (React Native)@bloxwap/hig-tokens, -contracts, -native; optional -feedback plus Expo peersDOM renderer imports, font binaries outside the manifest
Docs or marketing site@bloxwap/hig-tokens, -react (CSS and the controls in use); @bloxwap/fontA local .btn layer, a palette copy, vendor/hig
Brand or content onlyNothing at runtime; follow the principles, content and brand chaptersReact, RN or audio dependencies
No UINothing; README and content conventions onlyAny HIG runtime dependency
\n

2. Install versioned packages

\n

HIG is not on npm yet (#56). Until it is, consume the packed candidate:

\n
git clone https://github.com/bloxwap/hig && cd hig && git checkout <commit>\nnpm ci && npm run generate:fonts && npm run build\nnpm run release:dry-run -- --channel candidate   # local tarballs only; no publish, tag or release\n
\n

generate:fonts needs Python with fonttools[woff]==4.61.1 and brotli==1.2.0. In the consuming repository:

\n
    \n
  • Copy only the needed artifacts/release-candidate/bloxwap-hig-*.tgz files into vendor/bloxwap-hig/.
  • \n
  • Record their SHA-256 hashes and the source commit in a README or provenance.json.
  • \n
  • Depend on them with file: dependencies, adding overrides so HIG's internal dependencies resolve to the same files.
  • \n
\n

Never copy packages/*/src, dist folders, tokens.css, palettes, fonts or feedback engines. When #56 publishes, swap the file: dependencies for registry versions and delete vendor/bloxwap-hig/.

\n

Then follow the installation chapter for your platform: import @bloxwap/hig-tokens/css and @bloxwap/hig-react/css once, use per-component imports, load fonts through @bloxwap/font, and on native register fonts from the HIG font manifest.

\n

3. Add the scoped drift check to CI

\n

Create hig-drift.json in the repository root:

\n
{\n  "paths": ["apps/docs/app", "apps/docs/components", "apps/docs/lib", "apps/docs/styles"],\n  "canonical": [],\n  "obsoleteImports": [],\n  "exceptions": []\n}\n
\n

Run the check from a pinned HIG checkout. The script uses only Node built-ins and needs no install:

\n
- uses: actions/checkout@v4\n  with: {repository: bloxwap/hig, ref: <same commit as the vendored tarballs>, path: .hig}\n- run: node .hig/scripts/drift.mjs --root . --config hig-drift.json\n
\n
    \n
  • Scope: only paths you have migrated. Widen paths as more surfaces move.
  • \n
  • canonical: use it only for assets the repository owns. For example, bloxwap/font owns its web font files.
  • \n
  • Exceptions: record any other exception with an owner, a reason and an expiry no more than 180 days ahead, following the contribution chapter's exception policy.
  • \n
\n

First run against current main heads on 2026-10-15, scanning apps/docs (automated, local):

\n
    \n
  • hyperliquid and haptic pass.
  • \n
  • sfx flags the brand-mark fill in lib/og-card.tsx. That needs an exception like HIG's own brand marks.
  • \n
  • chart flags styles/hig-roles.generated.css, tracked upstream in #66.
  • \n
  • font flags its own font binaries, which belong under canonical.
  • \n
  • agent-cli flags vendor/hig/tokens.css, its import, and committed TTFs (#58).
  • \n
\n

4. Validate and record

\n
    \n
  • Build the site or app and run the repository's lint, typecheck, test and browser checks, plus keyboard, 390px mobile, large text and RTL checks.
  • \n
  • Measure CSS, JS and font bytes and the dependency count before and after.
  • \n
  • Link the merged PR, the HIG version and any dated exceptions in the matrix row.
  • \n
  • Label evidence as automated, browser, iOS Simulator, Android emulator or physical device.
  • \n
\n

Upgrading HIG

\n
    \n
  1. Read the changelog in the dry-run artifact, or the npm release once #56 is done, together with the semver section of the release chapter.
  2. \n
  3. Upgrade tokens, contracts, renderer and feedback together. Never mix candidate commits. Recompute the vendored hashes.
  4. \n
  5. Update the drift checkout ref to the same commit and run the check. Remove exceptions whose need has gone.
  6. \n
  7. Re-run the validation in step 4 and update the matrix row's higVersion and PR links.
  8. \n
  9. Roll back by reverting the upgrade PR, which restores the previous tarballs and lockfile. Product pilots also keep their host feature flag (for example NEXT_PUBLIC_HIG_PILOT=0).
  10. \n
\n", + "html": "

New repository setup and upgrades · HIG 0.1.0

\n

Issue #36. These steps adopt HIG in a new or existing Bloxwap repository without copying component folders, palettes, fonts or feedback engines. The five SDK docs sites followed this path: bloxwap/hyperliquid#180, bloxwap/chart#53, bloxwap/font#11, bloxwap/sfx#50 and bloxwap/haptic#26.

\n

1. Pick applicability

\n

Add or update the repository's row in planning/adoption.json (a PR to bloxwap/hig) with one of these applicability values:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
ApplicabilityInstallNever add
Product runtime UI (web)@bloxwap/hig-tokens, -contracts, -react; @bloxwap/fontCopied component folders or token CSS
Product runtime UI (React Native)@bloxwap/hig-tokens, -contracts, -native, -native-fonts; optional -feedback plus Expo peersDOM renderer imports, font binaries outside the manifest
Docs or marketing site@bloxwap/hig-tokens, -react (CSS and the controls in use); @bloxwap/fontA local .btn layer, a palette copy, vendor/hig
Brand or content onlyNothing at runtime; follow the principles, content and brand chaptersReact, RN or audio dependencies
No UINothing; README and content conventions onlyAny HIG runtime dependency
\n

2. Install versioned packages

\n

HIG is not on npm yet (#56). Until it is, consume the packed candidate:

\n
git clone https://github.com/bloxwap/hig && cd hig && git checkout <commit>\nnpm ci && npm run generate:fonts && npm run build\nnpm run release:dry-run -- --channel candidate   # local tarballs only; no publish, tag or release\n
\n

generate:fonts needs Python with fonttools[woff]==4.61.1 and brotli==1.2.0. In the consuming repository:

\n
    \n
  • Copy only the needed artifacts/release-candidate/bloxwap-hig-*.tgz files into vendor/bloxwap-hig/.
  • \n
  • Record their SHA-256 hashes and the source commit in a README or provenance.json.
  • \n
  • Depend on them with file: dependencies, adding overrides so HIG's internal dependencies resolve to the same files.
  • \n
\n

Never copy packages/*/src, dist folders, tokens.css, palettes, fonts or feedback engines. When #56 publishes, swap the file: dependencies for registry versions and delete vendor/bloxwap-hig/.

\n

Then follow the installation chapter for your platform: import @bloxwap/hig-tokens/css and @bloxwap/hig-react/css once, use per-component imports, load fonts through @bloxwap/font, and on native register fonts from the HIG font manifest.

\n

3. Add the scoped drift check to CI

\n

Create hig-drift.json in the repository root:

\n
{\n  "paths": ["apps/docs/app", "apps/docs/components", "apps/docs/lib", "apps/docs/styles"],\n  "canonical": [],\n  "obsoleteImports": [],\n  "exceptions": []\n}\n
\n

Run the check from a pinned HIG checkout. The script uses only Node built-ins and needs no install:

\n
- uses: actions/checkout@v4\n  with: {repository: bloxwap/hig, ref: <same commit as the vendored tarballs>, path: .hig}\n- run: node .hig/scripts/drift.mjs --root . --config hig-drift.json\n
\n
    \n
  • Scope: only paths you have migrated. Widen paths as more surfaces move.
  • \n
  • canonical: use it only for assets the repository owns. For example, bloxwap/font owns its web font files.
  • \n
  • Exceptions: record any other exception with an owner, a reason and an expiry no more than 180 days ahead, following the contribution chapter's exception policy.
  • \n
\n

First run against current main heads on 2026-10-15, scanning apps/docs (automated, local):

\n
    \n
  • hyperliquid and haptic pass.
  • \n
  • sfx flags the brand-mark fill in lib/og-card.tsx. That needs an exception like HIG's own brand marks.
  • \n
  • chart flags styles/hig-roles.generated.css, tracked upstream in #66.
  • \n
  • font flags its own font binaries, which belong under canonical.
  • \n
  • agent-cli flags vendor/hig/tokens.css, its import, and committed TTFs (#58).
  • \n
\n

4. Validate and record

\n
    \n
  • Build the site or app and run the repository's lint, typecheck, test and browser checks, plus keyboard, 390px mobile, large text and RTL checks.
  • \n
  • Measure CSS, JS and font bytes and the dependency count before and after.
  • \n
  • Link the merged PR, the HIG version and any dated exceptions in the matrix row.
  • \n
  • Label evidence as automated, browser, iOS Simulator, Android emulator or physical device.
  • \n
\n

Upgrading HIG

\n
    \n
  1. Read the changelog in the dry-run artifact, or the npm release once #56 is done, together with the semver section of the release chapter.
  2. \n
  3. Upgrade tokens, contracts, renderer and feedback together. Never mix candidate commits. Recompute the vendored hashes.
  4. \n
  5. Update the drift checkout ref to the same commit and run the check. Remove exceptions whose need has gone.
  6. \n
  7. Re-run the validation in step 4 and update the matrix row's higVersion and PR links.
  8. \n
  9. Roll back by reverting the upgrade PR, which restores the previous tarballs and lockfile. Product pilots also keep their host feature flag (for example NEXT_PUBLIC_HIG_PILOT=0).
  10. \n
\n", "toc": [ { "label": "1. Pick applicability", @@ -577,7 +577,7 @@ "route": "guides/governance/quality", "title": "Quality, budgets and evidence · HIG 0.1.0", "source": "docs/governance/quality.md", - "html": "

Quality, budgets and evidence · HIG 0.1.0

\n

Issues #31–#32. npm run check executes DOM and native suites with test counts. Native module mocks demonstrate roles/state/callback/lifecycle contracts; real packed Expo/Metro consumers demonstrate platform resolution. Neither is physical output or assistive-technology certification.

\n

Gates inspect actual generated package exports, platform imports, tokens/cue hashes, scoped anti-drift exceptions, packed consumer installs, every copyable example, SSR/hydration/StrictMode, docs static route/base paths, visual/browser states and measured gzip/assets. Negative tests deliberately introduce invalid exports, stale token output, budget regression, cross-platform imports and copied/obsolete tokens to prove failure paths.

\n

Budgets are regression limits from the initial candidate measurements, not claims of production savings:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
MetricMeasured initialLimitReason
Individual DOM Button gzip (React external)706 B850 B~20% API/behavior fix headroom, overlay/audio/chart excluded
Individual AmountInput gzip325 B400 BSmall labelled primitive, ~23% headroom
Native Switch gzip (React/RN external)1,383 B; 1,849 B after #672,200 BRaised for #67: the shared Text now carries the font-scale cap and Korean/Chinese script tracking, Switch the token-role track. ~19% headroom; verifies no Slider/SVG/overlay/feedback imports
Platform-neutral feedback contract gzip962 B1,200 B~25% headroom without device engines
Docs JS gzip, all lazy chunks204,877 B240,000 B~17% headroom; includes chart/code examples, not initial-load-only
Docs CSS17,419 B22,000 B~26% headroom for responsive/documentation states
Sans + Mono assets592,796 B600,000 BPins two approved faces; new script/face requires explicit review
Native four-cue WAV assets103,618 B110,000 B~6% headroom; no full engine/catalog assets
\n

The actual build report is evidence/quality-report.json. Bundles use explicit external React/RN peers, so figures measure HIG runtime contribution, not total installed/Metro app size. Production app/Pro/native before/after deltas require owning-repository builds and reviewed baselines; do not invent a percentage improvement. Export/dependency graph reports and packed tests enforce separation, while owning migrations must measure complete apps with equal data/toolchain.

\n

Exceptions require path/rule, owner, reason and expiry. Approved brand geometry is exempt from literal-palette detection until review; unrelated primitive/palette copies fail. New repositories should run scoped gates on migrated surfaces and keep dated residuals rather than blanket exemptions.

\n

Physical release review remains pending: iPhone Safari, Android Chrome, native iOS and Android, VoiceOver/TalkBack, large text/RTL, color/font fidelity, actual SFX/haptic output/latency/settings/interruption/background behavior. Coordinate upstream haptic#9. Release artifacts cannot change those “pending” rows into passes.

\n

Versioned visual regressions

\n

The catalog browser gate captures and runs axe on every shipped state. Button, Field and Input additionally compare eight versioned mobile/reduced-motion screenshots in tests/visual-baselines/. Geometry changes fail; pixel changes above 2% fail after a 24-channel-value antialias tolerance. tests/visual.test.ts deliberately changes geometry and pixels to prove the gate fails. Baselines record current candidate rendering, not approved product parity or native-device fidelity. Review an intentional change using CI captures, then run HIG_UPDATE_VISUALS=1 node scripts/catalog-browser-smoke.mjs against the production docs preview and commit the reviewed baseline images.

\n", + "html": "

Quality, budgets and evidence · HIG 0.1.0

\n

Issues #31–#32. npm run check executes DOM and native suites with test counts. Native module mocks demonstrate roles/state/callback/lifecycle contracts; real packed Expo/Metro consumers demonstrate platform resolution. Neither is physical output or assistive-technology certification.

\n

Gates inspect actual generated package exports, platform imports, tokens/cue hashes, scoped anti-drift exceptions, packed consumer installs, every copyable example, SSR/hydration/StrictMode, docs static route/base paths, visual/browser states and measured gzip/assets. Negative tests deliberately introduce invalid exports, stale token output, budget regression, cross-platform imports and copied/obsolete tokens to prove failure paths.

\n

Budgets are regression limits from the initial candidate measurements, not claims of production savings:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
MetricMeasured initialLimitReason
Individual DOM Button gzip (React external)706 B850 B~20% API/behavior fix headroom, overlay/audio/chart excluded
Individual AmountInput gzip325 B400 BSmall labelled primitive, ~23% headroom
Native Switch gzip (React/RN external)1,383 B; 1,849 B after #672,200 BRaised for #67: the shared Text now carries the font-scale cap and Korean/Chinese script tracking, Switch the token-role track. ~19% headroom; verifies no Slider/SVG/overlay/feedback imports
Platform-neutral feedback contract gzip962 B1,200 B~25% headroom without device engines
Docs JS gzip, all lazy chunks204,877 B240,000 B~17% headroom; includes chart/code examples, not initial-load-only
Docs CSS110,618 B (with token roles, #66)115,000 B~4% headroom; raised from 110,000 B when @bloxwap/hig-tokens/css gained radius/type/motion/density/font roles
Sans + Mono assets592,796 B600,000 BPins two approved faces; new script/face requires explicit review
Native fixture faces from @bloxwap/hig-native-fonts (Sans 400/600, Mono 400)1,278,880 B1,400,000 BOnly the three registered faces; optional CJK and other weights stay unloaded. Every generated face hash is checked
Font binaries in web/shared tarballs (tokens, contracts, react, feedback, native)0 files0 filesNative faces ship only in @bloxwap/hig-native-fonts (#64); release:dry-run fails otherwise and records packedSizes
Native four-cue WAV assets103,618 B110,000 B~6% headroom; no full engine/catalog assets
\n

The actual build report is evidence/quality-report.json. Bundles use explicit external React/RN peers, so figures measure HIG runtime contribution, not total installed/Metro app size. Production app/Pro/native before/after deltas require owning-repository builds and reviewed baselines; do not invent a percentage improvement. Export/dependency graph reports and packed tests enforce separation, while owning migrations must measure complete apps with equal data/toolchain.

\n

Exceptions require path/rule, owner, reason and expiry. Approved brand geometry is exempt from literal-palette detection until review; unrelated primitive/palette copies fail. New repositories should run scoped gates on migrated surfaces and keep dated residuals rather than blanket exemptions.

\n

Physical release review remains pending: iPhone Safari, Android Chrome, native iOS and Android, VoiceOver/TalkBack, large text/RTL, color/font fidelity, actual SFX/haptic output/latency/settings/interruption/background behavior. Coordinate upstream haptic#9. Release artifacts cannot change those “pending” rows into passes.

\n

Versioned visual regressions

\n

The catalog browser gate captures and runs axe on every shipped state. Button, Field and Input additionally compare eight versioned mobile/reduced-motion screenshots in tests/visual-baselines/. Geometry changes fail; pixel changes above 2% fail after a 24-channel-value antialias tolerance. tests/visual.test.ts deliberately changes geometry and pixels to prove the gate fails. Baselines record current candidate rendering, not approved product parity or native-device fidelity. Review an intentional change using CI captures, then run HIG_UPDATE_VISUALS=1 node scripts/catalog-browser-smoke.mjs against the production docs preview and commit the reviewed baseline images.

\n", "toc": [ { "label": "Versioned visual regressions", @@ -598,7 +598,7 @@ "route": "guides/installation", "title": "Installation and citation · HIG 0.1.0", "source": "docs/installation.md", - "html": "

Installation and citation · HIG 0.1.0

\n

These are review candidates, not published npm packages. Use local dry-run tarballs or CI artifact downloads; do not assume @bloxwap/hig-* is available on the registry. Published-style imports in examples are tested against packed candidates. Version references identify a candidate until release gates are complete.

\n

React / Vite

\n

Run npm ci && npm run build && npm run smoke:packed. The smoke script installs tarballs into an independent temporary consumer and builds the fixture. For a host app install tokens, contracts and React tarballs plus React/DOM 19.2. Import @bloxwap/hig-tokens/css and @bloxwap/hig-react/css once; import individual components such as @bloxwap/hig-react/button. Pin @bloxwap/font 0.1.2 and choose only the required families/scripts using asset URL @font-face rules. The included fixture has Sans and Mono only. Never copy font binaries or component folders.

\n

SSR and Next client boundaries

\n

Run npm run smoke:ssr, npm run build:next and npm run smoke:packed-next. Token/formatting imports and button/provider SSR are deterministic. Components with hooks carry use client in their emitted modules. In Next, import HIG CSS in the app layout and put interactive example state in a client component; keep wallet/network logic with the host. Next's published Bloxwap font loaders remain owned upstream. Next 16.4.0 (webpack), React 19.2 and the published Bloxwap Sans Next loader are verified in the dedicated production fixture; other Next versions require host checks. Verify hydration, stable IDs and StrictMode before adopting a host version.

\n

Expo / React Native

\n

Run npm run smoke:packed-native after packed artifacts are built, or npm run start -w hig-native-fixture for the device gallery. Matrix: React 19.1, RN 0.81.5, Expo 54, Metro as locked. Native consumers install tokens/contracts/native tarballs; optional feedback installs its tarball and explicit Expo backend peers. Slider and SVG peers are only needed by their family entries. Import @bloxwap/hig-native/button, not the DOM renderer. Dynamic Type remains enabled; native targets stay 48. Keyboard/safe-area/back behavior belongs to the native host and its supported OS.

\n

Install Expo native peers with npx expo install (for example expo-asset ~12.0.13 and expo-constants ~18.0.14 alongside expo-font/expo-audio). Expo 54 autolinking links the copy resolved from the app root; a hoisted SDK 57 expo-asset pulled by expo-audio's * peer compiles away and the app fails at launch with Cannot find native module 'ExpoAsset' (observed on the iOS Simulator). verifyExpoNativeModules in scripts/native-compatibility.mjs guards the fixture and packed consumer. HIGProvider follows the OS layout direction (I18nManager.isRTL) unless direction is passed.

\n

@bloxwap/hig-native ships one small Android library (android/, com.bloxwap.hig.HIGPackage) that React Native and Expo autolinking pick up in development and release builds (npx expo prebuild, npx expo run:android). It adds Android set-progress to Slider; nothing else depends on it, and in Expo Go the Slider falls back to increment/decrement only. iOS needs no native code.

\n

Simulator/emulator verification: scripts/native-sim/ builds the packed candidate gallery as real Release apps and drives them with XCUITest (iOS Simulator) and UiAutomation instrumentation, uiautomator and TalkBack (Android emulator). Results and residual physical-hardware limits are in evidence/native-sim/.

\n

WOFF2 is not a native font asset. @bloxwap/hig-tokens/fonts/* ships 15 static Bloxwap Sans/Mono TTFs generated reproducibly from pinned Font 0.1.2 sources, plus a typed FontManifest. Register them with expo-font before setting the native provider fontFamily, as the gallery does. Rasterization on physical displays remains part of #55. Moving these faces out of the web tokens package is tracked in #64. New repositories follow docs/governance/new-repository.md.

\n

Tokens only / plain CSS / Tailwind

\n

Install the tokens tarball alone and import CSS variables or typed tokens. No React/RN/audio peer is required. Plain HTML uses semantic --hig-* roles. Tailwind may reference these values through host configuration; HIG does not require content scans or a shadcn registry. Native uses dp and explicit hex/channel maps; do not feed CSS functions or rename rem to dp. The audited monorepo web/native rem roots are both 16; HIG supports StyleSheet rather than claiming a verified NativeWind consumer. See the source-reconciliation chapter for native font generation and registration.

\n

Optional chart, sound and haptics

\n

Chart is DOM-only and lazy; install @bloxwap/chart 0.3.0 for that entry and supply a textual summary. Native uses labeled readouts/lists. Feedback install paths, preferences, iOS physical-tap limitations and Expo backends are in the feedback chapters. Keep essential outcome text available with both channels off.

\n

Stable citations and upgrades

\n

Cite “Bloxwap HIG 0.1.0 candidate, chapter/anchor” plus an immutable GitHub commit permalink. Branch and docs preview links are mutable. Upgrade tokens, contracts, renderer, docs/examples and semantic cue mapping together; compare API behavior and adopt behind a rollback flag. Token appearance and accessibility behavior changes require semver review. No publication, release or tag is performed by this implementation.

\n

The native fixture pins React 19.1.0, matching the RN 0.81.5 renderer exactly. Web/Next/Fumadocs fixtures use React 19.2.0. Metro explicitly resolves every native react/react/* import to the native fixture's version so shared-workspace code cannot load web React. Quality and packed-native checks compare the installed version to RN's actual renderer guard; a successful Metro bundle alone does not prove runtime compatibility.

\n", + "html": "

Installation and citation · HIG 0.1.0

\n

These are review candidates, not published npm packages. Use local dry-run tarballs or CI artifact downloads; do not assume @bloxwap/hig-* is available on the registry. Published-style imports in examples are tested against packed candidates. Version references identify a candidate until release gates are complete.

\n

React / Vite

\n

Run npm ci && npm run build && npm run smoke:packed. The smoke script installs tarballs into an independent temporary consumer and builds the fixture. For a host app install tokens, contracts and React tarballs plus React/DOM 19.2. Import @bloxwap/hig-tokens/css and @bloxwap/hig-react/css once; import individual components such as @bloxwap/hig-react/button. Pin @bloxwap/font 0.1.2 and choose only the required families/scripts using asset URL @font-face rules named 'Bloxwap Sans' and 'Bloxwap Mono'; HIG controls read --hig-font-sans/--hig-font-mono, which fall back to those literal families. The included fixture has Sans and Mono only, and its browser smoke checks that .hig-button and .hig-number render loaded Bloxwap faces. Never copy font binaries or component folders.

\n

SSR and Next client boundaries

\n

Run npm run smoke:ssr, npm run build:next and npm run smoke:packed-next. Token/formatting imports and button/provider SSR are deterministic. Components with hooks carry use client in their emitted modules. In Next, import HIG CSS in the app layout and put interactive example state in a client component; keep wallet/network logic with the host. Next's published Bloxwap font loaders remain owned upstream.

\n

Fonts in Next: put the @bloxwap/font loader variables on <html>, for example <html className={${BloxwapSans.variable} ${BloxwapMono.variable}}>. The loaders register hashed family names, so HIG never matches a literal 'Bloxwap Sans' there. Instead --hig-font-sans and --hig-font-mono in @bloxwap/hig-tokens/css resolve var(--font-bloxwap-sans) and var(--font-bloxwap-mono) first and fall back to the literal families. No bridge CSS is needed. If a host keeps its own variable names, override the roles on :root after importing the tokens CSS: :root { --hig-font-sans: var(--my-sans), 'Bloxwap Sans', system-ui, sans-serif; }. The variables must be defined on <html> (not <body>), because the roles resolve on :root. npm run smoke:packed-next serves the packed consumer and checks, in Chromium, that .hig-button and .hig-number compute to a loaded Bloxwap face (document.fonts.check plus the FontFace status). Next 16.4.0 (webpack), React 19.2 and the published Bloxwap Sans Next loader are verified in the dedicated production fixture; other Next versions require host checks. Verify hydration, stable IDs and StrictMode before adopting a host version.

\n

Expo / React Native

\n

Run npm run smoke:packed-native after packed artifacts are built, or npm run start -w hig-native-fixture for the device gallery. Matrix: React 19.1, RN 0.81.5, Expo 54, Metro as locked. Native consumers install tokens/contracts/native tarballs; optional feedback installs its tarball and explicit Expo backend peers. Slider and SVG peers are only needed by their family entries. Import @bloxwap/hig-native/button, not the DOM renderer. Dynamic Type remains enabled; native targets stay 48. Keyboard/safe-area/back behavior belongs to the native host and its supported OS.

\n

Install Expo native peers with npx expo install (for example expo-asset ~12.0.13 and expo-constants ~18.0.14 alongside expo-font/expo-audio). Expo 54 autolinking links the copy resolved from the app root; a hoisted SDK 57 expo-asset pulled by expo-audio's * peer compiles away and the app fails at launch with Cannot find native module 'ExpoAsset' (observed on the iOS Simulator). verifyExpoNativeModules in scripts/native-compatibility.mjs guards the fixture and packed consumer. HIGProvider follows the OS layout direction (I18nManager.isRTL) unless direction is passed.

\n

@bloxwap/hig-native ships one small Android library (android/, com.bloxwap.hig.HIGPackage) that React Native and Expo autolinking pick up in development and release builds (npx expo prebuild, npx expo run:android). It adds Android set-progress to Slider; nothing else depends on it, and in Expo Go the Slider falls back to increment/decrement only. iOS needs no native code.

\n

Simulator/emulator verification: scripts/native-sim/ builds the packed candidate gallery as real Release apps and drives them with XCUITest (iOS Simulator) and UiAutomation instrumentation, uiautomator and TalkBack (Android emulator). Results and residual physical-hardware limits are in evidence/native-sim/.

\n

WOFF2 is not a native font asset. @bloxwap/hig-native-fonts ships 15 static Bloxwap Sans/Mono (plus Korean and Simplified Chinese Sans) TTFs generated reproducibly from pinned Font 0.1.2 sources, with a typed fontManifest export and @bloxwap/hig-native-fonts/manifest. Install it alongside the tokens, contracts and native tarballs, then require('@bloxwap/hig-native-fonts/fonts/BloxwapSans.ttf') and register the faces with expo-font before setting the native provider fontFamily, as the gallery does. @bloxwap/hig-tokens no longer contains font binaries (#64); see native font migration. Rasterization on physical displays remains part of #55. New repositories follow docs/governance/new-repository.md.

\n

Tokens only / plain CSS / Tailwind

\n

Install the tokens tarball alone and import CSS variables or typed tokens. No React/RN/audio peer is required. Plain HTML uses semantic --hig-* roles.

\n

@bloxwap/hig-tokens/css defines every role a plain CSS host needs, so nobody has to read the manifest or generate role files:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
RolesExampleValues
Color--hig-surface, --hig-actionsRGB, then OKLCH/P3 in @supports
Space--hig-space-mdxs–xxxl, section, in rem
Font--hig-font-sans, --hig-font-mono@bloxwap/font Next variable, then literal family
Radius--hig-radius-md, --hig-radius-pillsm md lg xl in rem; pill 9999px
Type--hig-type-body-size, --hig-type-body-line-height, --hig-type-body-weight, --hig-type-bodycaption label body title display; the bare role is a font shorthand in Sans
Motion--hig-motion-duration-base, --hig-motion-easing-standardfast base slow ms; 0ms under prefers-reduced-motion: reduce or [data-hig-motion='reduced']
Density--hig-density-control, --hig-density-compact-gapcontrol target gap per mode; the unprefixed roles follow [data-hig-density='compact']
\n
@import '@bloxwap/hig-tokens/css';\n.card { border-radius: var(--hig-radius-lg); padding: var(--hig-space-lg); font: var(--hig-type-body); }\n.card h2 { font: var(--hig-type-title); }\n.card button { min-height: var(--hig-density-control); transition: background var(--hig-motion-duration-fast) var(--hig-motion-easing-standard); }\n``` Tailwind may reference these values through host configuration; HIG does not require content scans or a shadcn registry. Native uses dp and explicit hex/channel maps; do not feed CSS functions or rename rem to dp. The audited monorepo web/native rem roots are both 16; HIG supports StyleSheet rather than claiming a verified NativeWind consumer. See the source-reconciliation chapter for native font generation and registration.\n\n## Optional chart, sound and haptics\n\nChart is DOM-only and lazy; install @bloxwap/chart 0.3.0 for that entry and supply a textual summary. Native uses labeled readouts/lists. Feedback install paths, preferences, iOS physical-tap limitations and Expo backends are in the feedback chapters. Keep essential outcome text available with both channels off.\n\n## Stable citations and upgrades\n\nCite “Bloxwap HIG 0.1.0 candidate, chapter/anchor” plus an immutable GitHub commit permalink. Branch and docs preview links are mutable. Upgrade tokens, contracts, renderer, docs/examples and semantic cue mapping together; compare API behavior and adopt behind a rollback flag. Token appearance and accessibility behavior changes require semver review. No publication, release or tag is performed by this implementation.\n\nThe native fixture pins React **19.1.0**, matching the RN 0.81.5 renderer exactly. Web/Next/Fumadocs fixtures use React 19.2.0. Metro explicitly resolves every native `react`/`react/*` import to the native fixture's version so shared-workspace code cannot load web React. Quality and packed-native checks compare the installed version to RN's actual renderer guard; a successful Metro bundle alone does not prove runtime compatibility.\n
\n", "toc": [ { "label": "React / Vite", @@ -631,8 +631,13 @@ "route": "guides/migration/native", "title": "Native application pilot (#35)", "source": "docs/migration/native.md", - "html": "

Native application pilot (#35)

\n

Owner: bloxwap/monorepo maintainers (named assignment in #57). Status on 2026-10-15: monorepo#4540 migrates the app's settings, entry, logout, sound card and demo number to HIG 0.1.0-candidate.sha7c67a07c, with one HIG feedback controller and 15 duplicate fonts removed. It was verified on the iOS Simulator and Android emulator, but it is not merged yet (it needs the required human signoff). Physical-device certification is tracked in #55 and React Native 0.83 certification in #68. No tactile, audible or VoiceOver observation is claimed.

\n

Pilot: font registration → dp/sRGB token mapping → amount/selection/review sheet → visible async outcome → independent feedback settings. The Expo gallery proves candidate packaging and contract examples, not the owning app's native build.

\n

Checklist:

\n
    \n
  • Record actual RN/Expo/Metro/autolinking versions, source commit, old ui-native/token/font/audio wrappers, screenshots and native bundle/assets/latency baseline.
  • \n
  • Obtain approved pinned static TTF faces and upstream manifest or supply reviewed typed FontManifest; register before setting family. Do not treat WOFF2 or an invented native family as success.
  • \n
  • Map dp and legacy NativeWind rem roots explicitly. Keep Dynamic Type, 48 targets, logical RTL layout, safe areas, keyboard avoidance, native back and labelled fields/errors.
  • \n
  • Preview native import codemods, using individual switch/checkbox/range entries; wire optional SVG/Slider/Expo audio/haptic peers only where used. Retain platform-specific descriptions and host callbacks.
  • \n
  • Preserve raw financial strings, order/network logic, stable list identity and quiet live numbers. Use labelled list/readout native chart alternative unless a real supported native engine is reviewed.
  • \n
  • Read old sound/haptic settings with a host migration adapter before writing v1 key. Keep explicit false choices, volume and motion independent; handle corrupt/missing storage conservatively.
  • \n
  • Hook AppState/interruption/background and dispose cached players/listeners. Test muted channels, missing assets, backend rejection, repeated values, busy/disabled, canceled scroll/drag and unsupported hardware.
  • \n
  • Build the real app for iOS and Android. Observe large text, long translations, Arabic/Hebrew RTL, VoiceOver/TalkBack, orientation, keyboard/sheets, reduced motion and every feedback combination on devices.
  • \n
  • Record actual device/OS/settings and audible/tactile output, latency, silent-mode/interruption/background behavior. Reuse upstream haptic#9 review; simulator/mocks remain API evidence only.
  • \n
  • Remove duplicated source/assets/backend wrappers after device builds confirm references and autolinking are gone; quantify before/after sizes with equal toolchain/data.
  • \n
  • Link owning rollout PR and remaining version/exception work into the matrix.
  • \n
\n

Rollback: host feature flag restores old renderer/backends and previous lockfile. Persisted business data and feedback choices retain old-compatible fields during staged rollout; avoid one-way preference deletion. Keep old static assets until both physical platforms pass.

\n", - "toc": [], + "html": "

Native application pilot (#35)

\n

Owner: bloxwap/monorepo maintainers (named assignment in #57). Status on 2026-10-15: monorepo#4540 migrates the app's settings, entry, logout, sound card and demo number to HIG 0.1.0-candidate.sha7c67a07c, with one HIG feedback controller and 15 duplicate fonts removed. It was verified on the iOS Simulator and Android emulator, but it is not merged yet (it needs the required human signoff). Physical-device certification is tracked in #55 and React Native 0.83 certification in #68. No tactile, audible or VoiceOver observation is claimed.

\n

Pilot: font registration → dp/sRGB token mapping → amount/selection/review sheet → visible async outcome → independent feedback settings. The Expo gallery proves candidate packaging and contract examples, not the owning app's native build.

\n

Checklist:

\n
    \n
  • Record actual RN/Expo/Metro/autolinking versions, source commit, old ui-native/token/font/audio wrappers, screenshots and native bundle/assets/latency baseline.
  • \n
  • Register the pinned static TTF faces from @bloxwap/hig-native-fonts and its typed fontManifest; register before setting family. Do not treat WOFF2 or an invented native family as success.
  • \n
  • Map dp and legacy NativeWind rem roots explicitly. Keep Dynamic Type, 48 targets, logical RTL layout, safe areas, keyboard avoidance, native back and labelled fields/errors.
  • \n
  • Preview native import codemods, using individual switch/checkbox/range entries; wire optional SVG/Slider/Expo audio/haptic peers only where used. Retain platform-specific descriptions and host callbacks.
  • \n
  • Preserve raw financial strings, order/network logic, stable list identity and quiet live numbers. Use labelled list/readout native chart alternative unless a real supported native engine is reviewed.
  • \n
  • Read old sound/haptic settings with a host migration adapter before writing v1 key. Keep explicit false choices, volume and motion independent; handle corrupt/missing storage conservatively.
  • \n
  • Hook AppState/interruption/background and dispose cached players/listeners. Test muted channels, missing assets, backend rejection, repeated values, busy/disabled, canceled scroll/drag and unsupported hardware.
  • \n
  • Build the real app for iOS and Android. Observe large text, long translations, Arabic/Hebrew RTL, VoiceOver/TalkBack, orientation, keyboard/sheets, reduced motion and every feedback combination on devices.
  • \n
  • Record actual device/OS/settings and audible/tactile output, latency, silent-mode/interruption/background behavior. Reuse upstream haptic#9 review; simulator/mocks remain API evidence only.
  • \n
  • Remove duplicated source/assets/backend wrappers after device builds confirm references and autolinking are gone; quantify before/after sizes with equal toolchain/data.
  • \n
  • Link owning rollout PR and remaining version/exception work into the matrix.
  • \n
\n

Native fonts moved out of hig-tokens

\n

Issue #64. Up to 0.1.0-candidate.sha7c67a07c, @bloxwap/hig-tokens shipped the 15 static TTFs under @bloxwap/hig-tokens/fonts/*. Web-only consumers never served them, but they made up most of the tarball. They now live in @bloxwap/hig-native-fonts. The bytes, file names, registration keys and SHA-256 hashes are unchanged.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
BeforeAfter
require('@bloxwap/hig-tokens/fonts/BloxwapSans.ttf')require('@bloxwap/hig-native-fonts/fonts/BloxwapSans.ttf')
@bloxwap/hig-tokens/fonts/manifest.json@bloxwap/hig-native-fonts/manifest, or the typed fontManifest export
@bloxwap/hig-tokens/fonts/OFL.txt@bloxwap/hig-native-fonts/fonts/OFL.txt
\n
    \n
  • Expo / React Native: add the bloxwap-hig-native-fonts-*.tgz tarball next to tokens, contracts and native (with a matching overrides entry when vendoring), then replace the import prefix. nativeFont(fontManifest, 'BloxwapSans', 600) from @bloxwap/hig-contracts returns the registration key. Hashes you already pinned stay valid.
  • \n
  • Satori / OG images in web repositories: these also read static TTFs (bloxwap/chart's OG card is one). Install @bloxwap/hig-native-fonts as a build-time dependency for that script only, or read the TTF from @bloxwap/font if it ships one. Never copy the faces into the repository.
  • \n
  • Web-only hosts: remove nothing. They keep loading fonts from @bloxwap/font, and their tokens tarball shrinks. npm run release:dry-run fails if any web or shared tarball contains .ttf, .otf or .woff* files, and it records the packed sizes in packedSizes in the release report.
  • \n
  • Coordination: monorepo#4540 loads the faces from @bloxwap/hig-tokens. Rebase it on a candidate that includes this change, and switch it to the new prefix when it does.
  • \n
\n

Rollback: host feature flag restores old renderer/backends and previous lockfile. Persisted business data and feedback choices retain old-compatible fields during staged rollout; avoid one-way preference deletion. Keep old static assets until both physical platforms pass.

\n", + "toc": [ + { + "label": "Native fonts moved out of hig-tokens", + "id": "native-fonts-moved-out-of-hig-tokens" + } + ], "section": "migration" }, {