# Bloxwap HIG 0.2.0 ## ADR 001: platform renderers, shared contracts Source: docs/architecture/001-packages.md # ADR 001: platform renderers, shared contracts Status: accepted for candidate 0.1.0. Issues #4, #5. Evidence: `evidence/upstream-inventory.json`. ```mermaid graph TD Tokens[HIG tokens] --> Web[HIG React] Tokens --> Native[HIG Native] Contracts[HIG contracts] --> Web Contracts --> Native Contracts --> Feedback[optional feedback adapters] Web --> Docs[docs and consumer fixtures] Native --> Expo[Expo gallery] ``` HIG publishes one package, `@bloxwap/hig`; each area below is a subpath entry of it, built from its own `packages/hig/` source folder. `@bloxwap/hig/tokens` owns palette, density, units and generation; `@bloxwap/hig/contracts` owns platform-neutral types and formatting. `@bloxwap/hig/react` and `@bloxwap/hig/native` own renderers. `@bloxwap/hig/chart` owns the web adapter for `@bloxwap/chart` (`PriceChart`, `chartTheme`), and `@bloxwap/chart` is the package's only required peer (#96). No other entry imports `@bloxwap/chart`, and the peer is never optional: an optional peer on it made npm 11 drop chart's workspace copy when that repository regenerated its lockfile. `@bloxwap/hig/feedback` owns optional adapter entry points. `@bloxwap/hig/native-fonts` ships the static native TTFs (#64), and `@bloxwap/hig/og-fonts` ships the five byte-identical Latin faces that Satori/`next/og` cards need (#99). Both are generated from pinned `@bloxwap/font` sources by `npm run generate:fonts`, and their `fonts/` folders are the only places in the package allowed to contain font binaries. Web entries never resolve them. Base controls never load audio, haptics, charts, Expo or another platform. Each component has an individual export; CSS is explicit and side-effectful. Plain HTML consumers use tokens/CSS alone. Candidate validation matrix: Node 22.22+, TypeScript 5.9, React/DOM 19.1, RN 0.81.5 with Expo 54 and RN 0.83.10 with Expo 55 (React 19.1.0 and 19.2.0 respectively, #68), Vite 6.4. Next 16.4.0 webpack is verified by M5 production/packed consumer checks. NativeWind styling remains outside the supported StyleSheet consumer path. Use StyleSheet on RN and plain CSS on web; Tailwind may refer to `var(--hig-*)` without scanning HIG sources. The pinned monorepo source now documents web/native rem roots of 16; see source reconciliation. Never infer native sizes by replacing `rem` with `dp`. Prefer built-in semantic controls; modal/disclosure behavior must retain keyboard, focus restoration and descriptions. The `@bloxwap/hig/chart` entry imports the chart SDK only on the client. Libraries retain upstream licenses; extracted tokens and mark have commit provenance, no vendor engine source is copied. Font binaries remain owned by @bloxwap/font (OFL-1.1). Deprecation plan: inventory all live references, map variants to intent/density, run migration diff and consumer validation, switch a pilot behind a host flag, then delete unused copies. App comfortable targets and Pro compact rows are intentional. HIG cannot retire owning-repository copies within this hig-only change. API/token/behavior changes follow semver; breaking accessibility behavior counts as breaking. Support upgrades require web, SSR and Metro evidence with a documented rollback before changing the matrix. ## Attribution and provenance Source: docs/architecture/attribution.md # Attribution and provenance HIG code: MIT. @radix-ui/react-dialog, dropdown-menu, popover, tooltip: MIT, consumed as dependencies without copied engine code. @bloxwap/chart/sfx/haptic: independent upstream packages; optional integration only (chart through the separate `@bloxwap/hig/chart` adapter). @bloxwap/font: OFL-1.1, binaries stay in its pinned package. Brand SVG/geometry derives from Font docs, exact upstream commit in `evidence/upstream-inventory.json`; do not imply arbitrary trademark relicensing. Utility CheckIcon is original simple geometry. Full icons catalogs are excluded. ## Generated favicon and social preview `npm run generate` derives unchanged SVG favicons and a 1200×630 SVG social preview from the approved mark. Source SHA256 and derivative paths are in `planning/brand-artifacts.json`; lint rejects stale outputs. The derived preview declares its lineage and omits the source credential metadata; unchanged source/favicons retain it. Hosts whose social crawlers require raster images must render a PNG from this same SVG instead of redrawing the mark. This preview uses a generic font rather than embedding the full font asset. ## Candidate platform support · 0.1.0 Source: docs/architecture/platform-matrix.md # Candidate platform support · 0.1.0 | Area | DOM | RN | Evidence | | --- | --- | --- | --- | | Actions, surfaces, text, fields | Semantic DOM elements | Pressable/View/Text/TextInput | behavior tests, Vite/SSR/Metro | | Native text scaling and script tracking | Browser zoom; no cap | `Text`/`Heading`/`NumberReadout` `maxFontSizeMultiplier` (prop or HIGProvider default); Korean/Chinese runs at companion spacing via `script` and `letterSpacingByScript` | native Jest (#67); iOS Simulator AX5: capped readout 48pt vs 86pt prose; Android emulator 200% (non-linear scaling stays under the 2x cap) (`evidence/native-sim/issue-67-71/`) | | Native colour roles | CSS severity/data attributes | `Switch` `tone`, `Alert` `severity`/`tone` take token roles only; raw colours fall back. The Android Switch thumb uses the `text` token instead of the app theme accent | native Jest (#67); iOS Simulator and Android emulator captures (`evidence/native-sim/issue-67-71/`) | | Native colour space | CSS sRGB with P3 in `@supports` | `HIGProvider colorSpace` `srgb` (default) or `display-p3`; every component paints from `useColors()`; iOS `display-p3` uses `nativeColors.iosDisplayP3`, Android and web stay sRGB; no raw colours | native Jest (#93: P3 values for action/warning/negative/info, direct-import guard); iOS Simulator capture as a Display P3 host (`evidence/native-sim/issue-93/`); physical display pending (#55) | | Selection/range | Built-in input/select | OS Switch/community Slider, radio/check Pressables, Modal chooser; Android Slider adds `ACTION_SET_PROGRESS` through the autolinked `HIGAdjustableView` (plain View without native linking) | contract tests; Android emulator UiAutomation (#71): set-progress listed, 80 applied once, snapped/clamped, absent and rejected while disabled; iOS Simulator increment/decrement unchanged (`evidence/native-sim/issue-67-71/`) | | Dialog/sheet/menu | Radix scoped primitives; optional Vaul GestureSheet with fixed snap points or opt-in dynamic content sizing | Modal and explicit close/back; optional Gorhom GestureSheet with the same sizing alternatives, rejected-close retry and accessibility escape | DOM nested focus/Escape plus real Chromium/WebKit sheet drag, scroll and snap checks; mocked native focus/back/detents plus separate packed Android/iOS gesture entry; physical gesture/screen-reader certification is not claimed | | Tooltip/popover | Radix hover/focus/anchored | persistent HelpText/modal | no RN hover claim | | Navigation/search | href/callback adapters | host route/back callbacks | tab/search/disclosure contracts | | Financial lists/table | semantic table/list; optional per-place animated readout | FlatList labeled rows; optional Reanimated/Worklets per-place readout, static `data` remains engine-free | formatting and contract tests; mixed-bidi/native static fallbacks and physical-device/RTL observations remain limited | | Chart | `@bloxwap/hig/chart`: lazy @bloxwap/chart canvas (required peer) | numeric/list summary only | typed theme; physical gesture evidence pending | | Brand mark | individual SVG renderer/asset | optional react-native-svg | pinned geometry; device comparison pending | | React Native versions | — | RN 0.81.5 / Expo 54 / React 19.1.0 and RN 0.83.10 / Expo 55 / React 19.2.0 and RN 0.88.1 / Expo 58 / React 19.3.0; peer ranges `react-native` `^0.81.5 \|\| ^0.83.10 \|\| ^0.88.0`, SDK-matched Expo, SVG, Slider, Reanimated and Worklets peers | CI packed Metro exports and native-ref typechecks for all three (`smoke:packed-native`, `--profile rn-0.83`, `--profile rn-0.88`); iOS Simulator and Android emulator gallery, interaction and GestureSheet runs on both with the same outcomes; differences tracked in #86–#89 (`evidence/native-sim/rn-0.83/`) | | Fonts | pinned Sans/Mono asset URLs | hash-pinned static faces, typed manifest, explicit weight registration and host script fallback | Expo-web loading/byte/hash gates; no physical native-font certification | SSR: client components retain use-client boundaries in emitted JS. Importing a button needs React/tokens/contracts only at runtime. CSS is explicitly imported. Native API never imports DOM, Web Audio or chart. Native target size remains comfortable even when host density is compact; Pro density is a DOM exception, not a reason to shrink phone targets. Optional peer packages: none for chart, whose `@bloxwap/hig/chart` entry relies on the package's one required peer, `@bloxwap/chart` (#96); community Slider for native selection entry; react-native-svg for native brand entry. Vaul is isolated behind the web gesture-sheet entry. Gorhom and Gesture Handler are isolated behind the native gesture-sheet entry; Reanimated and Worklets are isolated behind the native gesture-sheet and animated-number entries. Base imports, including ordinary native `data` readouts, do not load them. No root barrel loads every component. DOM overlays are one scoped family entry and carry Radix MIT attribution; Radix peers are optional caret ranges and overlay-free web recipes have their own entries. ESM is supported; CommonJS consumer support is not claimed. RN 0.88 compatibility is verified by independent packed TypeScript and Android/iOS Metro consumers. Existing simulator/emulator interaction reports cover RN 0.81 and 0.83 only; no RN 0.88 physical or simulator interaction result is inferred from a bundle export. ## CI runners · HIG 0.1.0 Source: docs/ci-runners.md # CI runners · HIG 0.1.0 GitHub Actions jobs run on self-hosted Apple silicon Mac minis, except for the `publish` job in `publish.yml`: ```yaml runs-on: [self-hosted, macOS, ARM64] ``` Only that trusted-publishing job uses GitHub-hosted `ubuntu-24.04`. All other jobs remain self-hosted and do not call `apt`, `sudo`, or Playwright `--with-deps`. Scratch directories use `$RUNNER_TEMP`. Jobs that run npm/Python/Playwright export `TMPDIR=$RUNNER_TEMP` through `GITHUB_ENV` (the `runner` context is not available in workflow or job `env`). ## Required on every Mac mini - macOS on Apple silicon, with the Actions runner registered using the labels `self-hosted`, `macOS`, and `ARM64` - Git - Python 3.10+ with the standard-library `venv` module (`fonttools` 4.61.1 requires `>=3.10`). GitHub's `actions/setup-python` macOS builds mkdir `/Users/runner/hostedtoolcache` and fail on these minis (`Permission denied`). CI therefore installs relocatable CPython 3.12 into `$RUNNER_TEMP` with a pinned `uv` 0.13.1 binary (`scripts/ci-setup-python.sh`). The login-shell Python does not have to be 3.12. Cache misses create `$RUNNER_TEMP/hig-fonttools` and install `fonttools[woff]==4.61.1` and `brotli==1.2.1` there; they do not write to system site-packages - Network access to npm, GitHub, and Playwright browser downloads - Homebrew `gnu-tar` on **every** mini so `gtar` is on the runner service `PATH` or at `$(brew --prefix)/opt/gnu-tar/bin`. `actions/upload-pages-artifact` invokes `gtar` on macOS (`--hard-dereference` is a GNU option). Install with `brew install gnu-tar`. One machine having it is not enough. - Disk for npm caches, Playwright browsers at `~/Library/Caches/ms-playwright`, packed-consumer trees under `$RUNNER_TEMP`, and generated font faces Node 24.22.1 is installed per self-hosted job by `actions/setup-node`. It does not need to be the login-shell default. On the minis, `setup-node` must not set `cache: npm` (and must not rely on v5+ automatic npm caching): the minis already keep `~/.npm` on disk, and GitHub's cache store has saved 1.1–1.3 GB archives at about 1 MB/s, producing 27-minute `Post setup-node` tails. Quality jobs reuse generated native/OG font faces (~4 MB) from an on-disk cache on each mini (`scripts/ci-native-fonts.sh`, under `~/Library/Caches/bloxwap-hig/native-fonts`), not from `actions/cache`. The publication job uses no Actions caches and generates fonts every time with `bash scripts/ci-generate-fonts.sh`. ## Playwright Mac mini jobs that need browsers run `npx playwright install chromium webkit` with no `--with-deps`. Browsers stay on disk at `~/Library/Caches/ms-playwright` across jobs on a mini; do not save that folder with `actions/cache` (it stalls 11+ minutes because the directory is shared). Chromium and WebKit must launch headless as the runner user. Playwright Firefox is not installed or launched. ## Visual baselines Catalog Button/Field/Input states and Slider focus captures live under `tests/visual-baselines//` (`linux` or `darwin`). Geometry changes still fail; pixel changes still fail above 2% after a 24-channel-value antialias tolerance. Missing Darwin images fail closed: review the uploaded capture and commit it with `HIG_UPDATE_VISUALS=1`. Do not copy a Linux baseline or raise the thresholds to hide an OS difference. ## Not required for these workflows The `native` job exports Android and iOS Metro bundles (`smoke:packed-native` for RN 0.81.5 / Expo 54 and RN 0.83.10 / Expo 55). It does not boot simulators. Xcode, an iOS Simulator runtime, JDK 17, and the Android SDK are only for local `scripts/native-sim/` gallery runs, which these workflows do not execute. ## Jobs ### HIG candidate quality and dry-run artifacts (`quality.yml`) | Job | Role | | --- | --- | | `paths-filter` | Same PR path filter as before; docs-only PRs skip the heavy jobs | | `quality` | `check`, examples, quality gates, SSR and packed smokes, Playwright packed-next/css, release dry-run. Review artifacts upload only when the job fails; `publish.yml` rebuilds instead of downloading them | | `native` | Packed native Metro export for both supported RN/Expo profiles | | `browser` | Chromium and WebKit browser, visual, and hydration gates. Captures upload only when the job fails | | `candidate` | Aggregator so the required check reports on docs-only PRs | ### Documentation (`docs.yml`) | Job | Role | | --- | --- | | `build` | Static docs build and typecheck. GitHub Pages artifact only on `main` (same condition as deploy); pull requests still build and validate | | `deploy` | GitHub Pages deploy from `main` | ### Registry publication (`publish.yml`) The `publish` job is the only GitHub-hosted runner exception. [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/) requires a supported hosted runner and npm CLI >=11.5.1. It uses `ubuntu-24.04`, Node 24.22.1, and explicitly installs the repository's pinned npm@11.6.1 globally on the ephemeral runner. It uses pinned `actions/setup-python` with Python 3.12 and the portable `ci-generate-fonts.sh` venv, plus `npx playwright install --with-deps chromium` for registry consumers. `prerequisites` only runs `gh` and `git` checks and requests no OIDC token, so it runs on the minis. A published GitHub release with a `v*` tag or a manual dispatch with an existing tag starts the workflow. `prerequisites` requires configured reviewers on `npm-release` and a successful `quality.yml` run for the resolved tag commit, with manual dispatch using that same release tag as its workflow ref. `publish` needs both the unchanged self-hosted `security` scan and `prerequisites`, waits for the `npm-release` environment approval, checks out `refs/tags/` without persisted credentials, checks that its SHA still matches the prerequisite result, and re-runs the release gates. It publishes verified tarballs with `--provenance` using OIDC. The token fallback has been removed; a missing or mismatched Trusted Publisher must fail publication. Registry checks verify every tarball hash and require source-linked provenance for packages published by this run; already published versions are skipped for resumability. All eight `@bloxwap/hig-*` Trusted Publishers are configured for repository `bloxwap/hig`, workflow `publish.yml`, environment `npm-release`. The 0.1.2 publication records npm's GitHub Actions trusted-publisher identity for each package, separately from its signed provenance. No workflow step references the organization publishing token. The shared organization secret remains available to other repositories. Required environment reviewers and exact-source quality gates remain mandatory. See the [release policy](governance/release.md) and `evidence/release/registry-0.1.2/` for verification. ## Actions & surfaces · HIG 0.1.0 Source: docs/components/actions-surfaces.md # Actions & surfaces · HIG 0.1.0 Buttons, typography, and layout primitives for clear actions and related content. Includes `Button`, `IconButton`, `Link`, `Text`, `Heading`, `Stack`, `Inline`, `Surface`, `Card`, and `Separator`. Issue #13. Entry: `button / surface`. Import platform entries from `@bloxwap/hig/react/button` or `@bloxwap/hig/native/button` according to applicability below. Import `@bloxwap/hig/react/css` once on web; tokens CSS is a separate explicit import. ## API and anatomy | Component | Key props and behavior | | --- | --- | | `Button`, `IconButton` | `children`, `intent`, `busy`, `busyLabel`, `disabled`, `fullWidth`, `suppressFeedback`, and forwarded ref. Web uses `onClick`; native uses `onPress`. The busy suffix (` · Working`) comes from HIGProvider `messages.busy` or `busyLabel`; `''` hides it while busy state stays exposed. Native: the label and suffix render through HIG `Text` (HIGProvider or `maxFontSizeMultiplier` prop font-scale cap, registered family for the weight, per-script letter spacing), and a busy Button with text children sets its accessible name ([localization rule](../foundations/localization.md#built-in-component-strings)); an explicit `accessibilityLabel` wins. | | `Link` | Platform link behavior; host applications own routing. | | `Text`, `Heading` | Platform text props. Native adds `maxFontSizeMultiplier` (font-scale cap for dense numeric readouts, also on `NumberReadout`; HIGProvider sets a default) and `script` (`latin`, `ko`, `zh`; detected from string children). Korean and Chinese runs use the companion face's own spacing (`scriptLetterSpacing`, 0em) instead of Latin tracking; HIGProvider `letterSpacingByScript` overrides it per script in em. | | `Stack`, `Inline`, `Surface`, `Card` | Platform layout props. | ## When to use and examples Use one primary review action. Do: busy protects host requests and displays the translated busy label (`Working` in English). Don’t: use destructive red for every sell button. ## States, interaction and accessibility DOM semantic button/anchor, forwarded focus, target min-height tied to density (web: 44px comfortable, 36px compact); RN target always at least 48. Multiline labels wrap; RN custom children render in a View with text nodes styled individually. State review: rest; keyboard focus/hover (DOM); accepted press and release/cancel; selected/checked where applicable; disabled; busy; inline error/recovery; empty/loading; long translations/large text; reduced motion; sound and haptics disabled. No host action occurs on mere focus. Disabled/busy targets suppress accepted activation. Core has no required feedback backend; the optional provider callback follows the action, honors cancellation and isolates errors. ## Platform mapping and limits RN external Link uses Linking; routes belong to host. Native fontFamily only after registration. Decorative SVG hidden; IconButton label is mandatory. ## Validation and citation See `tests/components.dom.test.tsx`, `tests/native/components.test.cjs`, consumer fixtures and milestone evidence. Physical hardware, assistive-technology and baseline product comparisons are unperformed unless linked explicitly. Cite HIG 0.1.0 with this chapter anchor and a commit permalink, not a mutable preview branch. Preserve business behavior behind a host rollback flag during adoption. Native `Button contentMode="custom"` renders host Text/View children directly, preserving layouts with separate identity and trailing nodes. The root retains minimum target, disabled/busy, propagation, ref and accepted-feedback behavior. Hosts own visible busy content in this mode; `busyLabel` still controls the accessible busy suffix. Keep child content noninteractive and give icon-only actions an accessible label. Native `Link` accepts a forwarded View ref, root Pressable props and `contentMode="custom"` for host label/icon nodes. Required `href` retains its destination. Without `onPress`, it opens through Linking and reports failures to `onError`; a supplied `onPress` replaces that navigation so host WebBrowser/route handlers run exactly once. Disabled links reject activation. The root always has link semantics. ## Charts · HIG 0.1.0 Source: docs/components/chart.md # Charts · HIG 0.1.0 Web price charts with text summaries and native readout alternatives. Includes web-only `PriceChart` and `chartTheme`. Native uses readouts, lists, and text summaries. Issues #20 and #96. Entry: `@bloxwap/hig/chart`. Import `PriceChart` and `chartTheme` from `@bloxwap/hig/chart` for the DOM canvas integration only. Native consumers use `@bloxwap/hig/native/data` for summary/readout/list alternatives. Import `@bloxwap/hig/react/css` once on web; tokens CSS is a separate explicit import. ```sh npm install @bloxwap/hig @bloxwap/chart@^0.4.0 ``` `@bloxwap/hig/chart` needs `react` and `@bloxwap/chart` `^0.3.0 || ^0.4.0` (0.4.0 is the tested baseline), and uses `HIGProvider` from `@bloxwap/hig/react/provider` for the localized fallback. `@bloxwap/chart` is the only required peer of `@bloxwap/hig`; no other entry imports it, and there is no `./react/chart` entry. ## API and anatomy | Component | Key props and behavior | | --- | --- | | `PriceChart` | `data`, required `summary`, `height`, and chart SDK props. | | `chartTheme` | Token-based colors in sRGB for canvas compatibility. | ## When to use and examples Do: provide host-generated price/trend summary and lazy-load chart on demand. Don’t: play effects on ticks or present web canvas as RN. ## States, interaction and accessibility @bloxwap/chart/react loads only on PriceChart render, in Suspense. Token theme covers surface/text/border/grid/crosshair/up/down/fonts; engine owns gestures and cleanup. State review: rest; keyboard focus/hover (DOM); accepted press and release/cancel; selected/checked where applicable; disabled; busy; inline error/recovery; empty/loading; long translations/large text; reduced motion; sound and haptics disabled. No host action occurs on mere focus. Disabled/busy targets suppress accepted activation. Core has no required feedback backend; the optional provider callback follows the action, honors cancellation and isolates errors. ## Moved from `@bloxwap/hig/react/chart` (#96) Until #96 the adapter was the `./chart` entry of `@bloxwap/hig/react`, which declared `@bloxwap/chart` as an optional peer. npm 11.6 drops a workspace copy of `@bloxwap/chart` from a lockfile regenerated from scratch without `legacy-peer-deps` whenever an installed package declares it as an optional peer. bloxwap/chart hit this: its `packages/chart` workspace disappeared and `apps/docs` lost `@bloxwap/chart`. Repacking hig-react without that peer kept the link. A required peer does not trigger it, so the adapter moved to its own entry and `@bloxwap/chart` became a required peer. Since 0.2.0 that entry is `@bloxwap/hig/chart` inside the single `@bloxwap/hig` package, whose only required peer is still `@bloxwap/chart`. `npm run smoke:packed-chart-workspace` builds a chart-shaped workspace and regenerates its lockfile from scratch with strict peers, under both the ambient npm and the pinned npm 11.6.1. It fails if the link is lost (see the release chapter). Migration: replace `@bloxwap/hig/react/chart` with `@bloxwap/hig/chart` in imports. `PriceChart` props and `chartTheme` values are unchanged. ```diff -import {PriceChart, chartTheme} from '@bloxwap/hig/react/chart'; +import {PriceChart, chartTheme} from '@bloxwap/hig/chart'; ``` Consumers: an org-wide GitHub code search (default branches) on 2026-10-16 found no import of `@bloxwap/hig/react/chart` outside this repository. bloxwap/chart, sfx and haptic import only other hig-react entries, and chart builds its own theme from `@bloxwap/hig/tokens`. The candidate was never published. The release chapter normally keeps a deprecated export working for a minor release, but that is skipped here: there are no consumers to migrate, and a working shim would need the optional peer that causes the bug. Hosts that only use other hig-react entries change nothing. bloxwap/chart can drop the `legacy-peer-deps` regeneration note in `vendor/bloxwap-hig/README.md` at its next HIG refresh. ## Platform mapping and limits RN canvas is unsupported in this candidate. Use NumberReadout/List plus host summaries. A native chart engine/WebView bridge needs a separate supported target and device lifecycle review, rather than an invented integration. ## Validation and citation See `tests/components.dom.test.tsx`, `tests/native/components.test.cjs`, consumer fixtures and milestone evidence. Physical hardware, assistive-technology and baseline product comparisons are unperformed unless linked explicitly. Cite HIG 0.1.0 with this chapter anchor and a commit permalink, not a mutable preview branch. Preserve business behavior behind a host rollback flag during adoption. ## Fields & inputs · HIG 0.1.0 Source: docs/components/fields.md # Fields & inputs · HIG 0.1.0 Labeled inputs for text, search, and amounts, with visible help and validation. Includes `Field`, `Label`, `Input`, `TextArea`, `AmountInput`, and `SearchInput`. Issue #14. Entry: `field`. Import platform entries from `@bloxwap/hig/react/field` or `@bloxwap/hig/native/field` according to applicability below. Import `@bloxwap/hig/react/css` once on web; tokens CSS is a separate explicit import. ## API and anatomy | Component | Key props and behavior | | --- | --- | | `Field` | `label`, `id` (web), `description`, `error`, `required`, and `disabled`. | | `Input`, `TextArea` | Platform input props and forwarded refs. | | `AmountInput` | Text input with decimal keyboard hints; `value` stays a raw string. | ## When to use and examples Do: label Amount (BTC), describe available funds, keep 0,001 unchanged until host validates locale. Don’t: placeholder-only labels, Number(raw) on every keystroke, or strip IME text. ## States, interaction and accessibility Single-line inputs are 48-unit pills on the surface fill with a subtle border; focus thickens the border to a neutral ring and invalid inputs take the negative border. TextArea keeps a rounded rectangle. DOM label/id plus help/error aria-describedby; invalid state is explicit. Field required propagates to Input/TextArea for browser required semantics and form validation; explicit input props can override required. A disabled Field keeps its controls disabled even if a child requests otherwise. Native amount keyboardType and search returnKeyType remain host-overridable defaults for locale/negative-entry and submit flows. RN visible label plus accessibilityLabel/Hint; errors stay visible and live. Secure input and autofill remain host-controlled native props. State review: rest; keyboard focus/hover (DOM); accepted press and release/cancel; selected/checked where applicable; disabled; busy; inline error/recovery; empty/loading; long translations/large text; reduced motion; sound and haptics disabled. No host action occurs on mere focus. Disabled/busy targets suppress accepted activation. Core has no required feedback backend; the optional provider callback follows the action, honors cancellation and isolates errors. ## Platform mapping and limits RN decimal-pad has locale/platform differences; no implicit money parser. Host owns safe area and keyboard avoidance: on Android 15 edge-to-edge (Expo 54) the window no longer resizes for the IME, so wrap scrollable forms in `KeyboardAvoidingView behavior='padding'` on Android and use `automaticallyAdjustKeyboardInsets` on the iOS ScrollView, as the native gallery does. iOS Simulator and Android emulator runs show the AmountInput decimal pad and the focused field kept above the keyboard (`evidence/native-sim/`); physical IME composition and hardware-keyboard behavior remain device observations. ## Validation and citation See `tests/components.dom.test.tsx`, `tests/native/components.test.cjs`, consumer fixtures and milestone evidence. Physical hardware, assistive-technology and baseline product comparisons are unperformed unless linked explicitly. Cite HIG 0.1.0 with this chapter anchor and a commit permalink, not a mutable preview branch. Preserve business behavior behind a host rollback flag during adoption. ## Financial data · HIG 0.1.0 Source: docs/components/financial-data.md # Financial data · HIG 0.1.0 Readable numbers, metrics, lists, and tables for host-provided financial data. Includes `NumberReadout`, `AnimatedNumberReadout`, `Usd`, `Percent`, `Metric`, `Stat`, `Row`, and `List`. `Table` is web-only; native uses labeled lists. Issue #19. Entry: `data`. Import platform entries from `@bloxwap/hig/react/data` or `@bloxwap/hig/native/data` according to applicability below. Import `@bloxwap/hig/react/css` once on web; tokens CSS is a separate explicit import. ## API and anatomy | Component | Key props and behavior | | --- | --- | | Readouts | `value` accepts a number, `null`, or `undefined`. Locale comes from the provider; `options`, `stale`, and `label` control presentation. | | `List` | `items`, `keyFor`, `render`, `empty`, `label`, `loading`, `error`, `onRetry`, and `onLoadMore`. | | `Table` (web) | `caption`, `rows`, `columns`, and `keyFor`; optional host sorting callbacks. Column `align` accepts logical `start`, `center`, or `end`. `rowProps(row, index)` forwards row highlights, styles, data/ARIA attributes and events. `captionHidden`, `stickyHeader`, `loading`, and `state` support dense product tables. | Loading keeps existing rows visible, suppresses a misleading empty message, and disables pagination and retry until ready. Error text remains visible with an optional host retry action. Native lists use FlatList virtualization; stable host keys preserve identity during live updates. Native readouts, including `Usd`, `Percent`, and `AnimatedNumberReadout`, accept `tone="text" | "positive" | "negative" | "muted"`, `testID`, and a constrained typography `style`. The style accepts `fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, and `textAlign`, including registered styles and arrays. The tone resolves through the provider palette; it is explicit rather than inferred from the sign. Colour and tabular numeric glyphs stay controlled by the readout. Font scale follows HIG Text and the provider; `maxFontSizeMultiplier` can override its cap. For a branded 20 px equity pill, use `` with a registered host font. For signed PnL, add `tone={pnl < 0 ? 'negative' : 'positive'}` and `options={{signDisplay:'always'}}`. The same props on `AnimatedNumberReadout` style and identify its single final accessible reading; per-place motion is decorative. `NumberProps` on both platforms includes optional `prefix` and `suffix` display text around the canonical Intl-formatted value. The host owns the affixes and their localization. Do not use them to replace Intl currency, compact-notation, or sign formatting, and never parse the resulting display string back into a transaction value. Native readouts also accept `formattedValue` for a host's existing static display format, such as ``. The host owns this opaque display text and its localization; never parse it for submission or calculations. A finite numeric `value` is still required: `null`, `undefined`, `NaN`, and infinities display the canonical unknown dash even if text is supplied. An empty string falls back to the usual locale and `options` formatting. Stale text, the accessible label, semantic tone, and tabular typography still apply to the resulting reading. `AnimatedNumberReadout` keeps opaque `formattedValue` output static rather than guessing how to split it into animated places. Web tables keep a visible caption and ordinary headers by default. Set `captionHidden` when a visible section already names the table: the caption remains its accessible name. `stickyHeader` pins header cells within a bounded scroll region; give the region a height through `style` or `className`. `tableClassName` styles the table itself. Sorting still belongs to the host, and sort buttons retain native keyboard activation and `aria-sort`. ```tsx row.id} rowProps={row=>({className:row.current?'current-market':undefined,'aria-current':row.current?'true':undefined})} columns={[ {key:'market',label:'Market',render:row=>row.symbol}, {key:'balance',label:'Balance',align:'end',render:row=>}, ]} loading={refreshing} state={error?
{error}
:undefined}/> ``` The `state` slot is content for one cell spanning all columns; it can contain an empty explanation, loading skeleton, or error and retry UI. Pass content rather than `tr`/`td` elements. Existing data rows remain visible. Without a custom state, `loading` exposes the provider's `loading` message and `aria-busy`; an empty, idle table shows `noRows`. `state={null}` suppresses that fallback. Give custom errors an alert role and custom loading content a status role when an announcement is appropriate. `rowProps` cannot replace the row's native role or HIG's cells with `children`/HTML. Row callbacks do not add a selection keyboard model; use native controls inside cells for actions. ## When to use and examples Do: show — for unknown and 0 for zero; mark stale. Don’t: turn a formatted number into order quantity or announce every live tick. Locale display, USD vs USDC, signed PnL and compact notation follow the [localization guide](../foundations/localization.md). Vocabulary follows the [glossary](../foundations/glossary.md). ## States, interaction and accessibility `NumberReadout`, `Usd` and `Percent` are static and tabular. `AnimatedNumberReadout` keeps the same final reading while adding decorative per-place motion, with static reduced-motion and unsupported-layout fallbacks. Percent uses percent points (5 is 5%), with localized percent placement/digits and a single native text readout. Intl options control precision/trailing zeros/sign; percent style is enforced. Unknown/nonfinite values stay —. DOM table caption/column headers/aria-sort and scroll region; RN FlatList labeled-row alternative with stable keys. State review: rest; keyboard focus/hover (DOM); accepted press and release/cancel; selected/checked where applicable; disabled; busy; inline error/recovery; empty/loading; long translations/large text; reduced motion; sound and haptics disabled. No host action occurs on mere focus. Disabled/busy targets suppress accepted activation. Core has no required feedback backend; the optional provider callback follows the action, honors cancellation and isolates errors. ## Platform mapping and limits Numbers are display-only IEEE numeric inputs, no decimal arithmetic or hidden parser. Exact financial strings remain host-owned. No heavy DataTable engine; optional virtualization/sorting belongs to host. Native list device performance pending. ## Validation and citation See `tests/animated-number-model.test.ts`, `tests/animated-number.dom.test.tsx`, `tests/data.dom.test.tsx`, `tests/native/animated-number.test.cjs`, `tests/native/readout-styles.test.cjs`, and `scripts/pro-data-browser-smoke.mjs`, plus consumer fixtures and milestone evidence. Chromium/WebKit checks cover real per-place motion, interrupted updates, Arabic RTL digits, canonical final output and reduced motion. Android RN 0.83 and iOS RN 0.88 simulator matrices in `evidence/native-number-flow/` verify localized final labels, rapid bursts, large text and static fallbacks. iOS Hermes did not apply `nu-arab` to an English locale tag in the runtime check, so localized reel coverage uses `ar-EG` in LTR instead. VoiceOver/TalkBack speech, physical devices, animation smoothness and exhaustive visual bidi review remain unclaimed. Cite HIG 0.1.0 with this chapter anchor and a commit permalink, not a mutable preview branch. Preserve business behavior behind a host rollback flag during adoption. ## Optional number motion `AnimatedNumberReadout` lives in the separate `animated-number` entry on both platforms. It moves changed numeric places rather than fading the whole reading or interpolating financial values. The final visible output remains the canonical `Intl.NumberFormat` result used by `NumberReadout`, including the host's `prefix`, `suffix`, locale, options and stale text. Localized digits, grouping and decimal marks, currency tokens, and scientific-notation tokens are preserved; motion never assumes ASCII digits or a decimal point. The optional `trend` prop controls directional digit motion: `1` always moves upward, `-1` always moves downward, and `0` follows each changed digit’s own increase or decrease. A callback receives `(previousValue, newValue)` and returns one of those three values. When omitted, a finite numeric increase or decrease supplies the direction. This is a presentation hint, not a price signal or a replacement for an explicit semantic tone. Updates are latest-value-wins: a newer value supersedes pending motion and settles on the newest final reading, including during rapid updates. Both platforms expose one accessible final reading rather than separate characters, and neither adds a live region or announces every tick. Pending visual motion is canceled on replacement and unmount. Reduced motion and unknown/non-finite values use the static final reading. A platform that cannot safely measure or lay out individual places also uses the static result. Native opaque `formattedValue` remains static. Unsafe mixed-bidi tokenization and unsupported native text layouts may likewise fall back to the intact static reading; this is not a claim of physical-device or RTL certification. The native animated entry uses the optional Reanimated and Worklets peers. Install SDK-compatible versions when using that entry; the ordinary native `data` entry and static readouts import no animation engine. Example: `import {AnimatedNumberReadout} from '@bloxwap/hig/react/animated-number'` (or `hig-native/animated-number`), then ``. The gallery manifest contains runnable examples for both renderers. ## Navigation · HIG 0.1.0 Source: docs/components/navigation.md # Navigation · HIG 0.1.0 Tabs, disclosure, search, and navigation that keep the current location clear. Includes `Tabs`, `Accordion`, `Pagination`, `Navigation`, and `Search`. `Breadcrumb` is web-only; native alternatives include `BackButton` and `BottomNavigation`. Issue #17. Entry: `navigation`. Import platform entries from `@bloxwap/hig/react/navigation` or `@bloxwap/hig/native/navigation` according to applicability below. Import `@bloxwap/hig/react/css` once on web; tokens CSS is a separate explicit import. ## API and anatomy | Component | Key props and behavior | | --- | --- | | `Tabs` | `label`, `items`, `value`, `defaultValue`, and `onValueChange`. Items include panel content and optional `disabled`. | | `Navigation` | Web links use `href`; native destinations use host callbacks. | | `Search` | `query`, `results`, `loading`, and `error`. The host supplies the search index and router. | ## When to use and examples Do: expose current location and keep deep-link ownership with host. Don’t: replace native back with a browser history dependency. ## States, interaction and accessibility Tabs use selected states and one active panel; DOM Arrow/Home/End keys, RTL-aware arrow mapping. Only the selected tab is in the Tab order. The active DOM panel is a tab stop only when its first content is not focusable. When the panel starts with a focusable element, such as the `InstallCommand` `pre`, that element is the single stop after the tab list. Panels that start with text keep `tabIndex=0` so their content stays reachable. Server and hydration markup render every panel with `tabIndex=0`; on the client a layout effect corrects the shown panel before paint, in the same commit that shows it, so a panel that starts with a focusable element is never an extra stop after a tab switch. Accordion uses details/summary on DOM and expanded Pressable on RN. Search results are visible action lists; keyboard shortcuts belong to host. State review: rest; keyboard focus/hover (DOM); accepted press and release/cancel; selected/checked where applicable; disabled; busy; inline error/recovery; empty/loading; long translations/large text; reduced motion; sound and haptics disabled. No host action occurs on mere focus. Disabled/busy targets suppress accepted activation. Core has no required feedback backend; the optional provider callback follows the action, honors cancellation and isolates errors. ## Platform mapping and limits Breadcrumb is DOM-specific; native host routes plus BackButton are supported alternatives. No fuzzy-search engine, command palette shortcut hijacking or native hardware-keyboard certification. ## Validation and citation See `tests/components.dom.test.tsx`, `tests/native/components.test.cjs`, consumer fixtures and milestone evidence. Physical hardware, assistive-technology and baseline product comparisons are unperformed unless linked explicitly. Cite HIG 0.1.0 with this chapter anchor and a commit permalink, not a mutable preview branch. Preserve business behavior behind a host rollback flag during adoption. ## Overlays · HIG 0.1.0 Source: docs/components/overlays.md # Overlays · HIG 0.1.0 Dialogs, sheets, menus, and contextual help with explicit dismissal and focus behavior. Includes `Dialog`, `AlertDialog`, `Sheet`, and `Menu`. `Popover` and `Tooltip` are web-only; native help uses persistent `HelpText`. Issue #16. Entry: `overlay`. Import platform entries from `@bloxwap/hig/react/overlay` or `@bloxwap/hig/native/overlay` according to applicability below. Import `@bloxwap/hig/react/css` once on web; tokens CSS is a separate explicit import. ## API and anatomy | Component | Key props and behavior | | --- | --- | | `Dialog` | Controlled `open` and `onOpenChange`, plus `title`, `description`, and `children`. The web `trigger` is one element that accepts a ref. `closeLabel` overrides the Close button (`Sheet`, web `Popover`, native `Menu` and `GestureSheet` too); web `AlertDialog` takes `cancelLabel`. Defaults come from HIGProvider [`messages`](../foundations/localization.md#built-in-component-strings). | | Web host composition | `title` remains required; `titleHidden` keeps its accessible name when the host renders a visible heading in the card. `description` is optional; `descriptionHidden` keeps supplied supporting text available to assistive technology. `contentRef` reaches the actual modal element. `contentProps` accepts layout classes/styles, DOM attributes and Radix focus/Escape/outside events; HIG retains ownership of the dialog id, role, naming associations, locale, direction, children and mounting behavior. Owned props and `dangerouslySetInnerHTML` are filtered at runtime for JavaScript callers too; the caller's object is left unchanged. | | Web close control | Omit `closeButton` for the current translated action, pass a ref-capable button element for a custom control, or pass `null` when the host supplies its own explicit dismissal action. Custom controls are composed with Radix Close; disabled buttons and `event.preventDefault()` retain their normal behavior. `closeLabel`/`cancelLabel` label only the default action. | | Optional web `GestureSheet` | A separate Vaul modal root owns real drag, optional fixed snap points, scroll handoff and focus scope. The Dialog title/description, trigger, close control and `contentRef` contracts apply. For fixed detents, `backendProps` retains typed Vaul flags and snap-point options. For opt-in content sizing, use top-level `enableDynamicSizing` with optional `maxDynamicContentSize` and omit Vaul snap props. `contentProps` retains host layout, DOM data attributes, focus/Escape/outside events and pointer capture events. `handleProps`/`handleRef` customize the actual backend handle; `handle={false}` omits it. `overlayProps` observes outside-pointer actions or retains host overlay styling. | | Native focus targets | `initialFocusRef` and `returnFocusRef` accept live focus targets. Initial focus defaults to the title; the host supplies the trigger ref (Menu and Select pass their own trigger). | | `Menu` | `label` and items with `label`, optional `disabled`, and `onSelect`. | | Optional native `GestureSheet` | `contentMode='default'` scrolls the title, optional description, children and Close action. `contentMode='custom'` provides an unpadded content container; fixed mode is full-height while dynamic mode reports intrinsic content height. The host owns custom header, back/close actions, safe-area content padding and scrolling. `title` remains required as the accessible name. Pass `titleRef` attached to the caller's accessible heading to focus it on open; without it, the sheet focuses its visually hidden title. | Native `Dialog` and its aliases support `contentMode='custom'`: host content/actions, required `title`, one HIG Modal owning Back, accessibility escape and focus. `titleRef` targets the host heading (otherwise hidden); `initialFocusRef` overrides it, `returnFocusRef` targets the trigger. Rejected close retains focus. `stageComponent` accepts `GestureHandlerRootView`; `stageStyle`/`stageTestID` configure it. `backdrop` supplies the scrim/action; `contentStyle`/`testID` configure the surface. Preserve accessible descendants and close policy; never nest Modals. `animationType` defaults to `none`; reduced motion forces `none`. Animated iOS focus return waits for [iOS-only `onDismiss`](https://reactnative.dev/docs/modal#ondismiss); Android restores next frame. Keep mounted with `open={false}` through dismissal. Reopen/unmount cancels restoration. A web card can keep its own visible heading and close geometry without replacing the modal's focus scope: ```tsx { if (!next && pending) return; setOpen(next); }} title={t('reviewTitle')} titleHidden description={t('reviewDescription')} descriptionHidden contentRef={panelRef} contentProps={{ className: 'host-review-card', style: { maxWidth: 420 } }} closeButton={} >

{t('reviewTitle')}

{reviewContent}
``` Here `t`, `panelRef`, the card content and pending state belong to the host. The visible heading remains a real heading; the hidden Radix title supplies the modal's accessible name. An omitted description removes its ARIA association instead of referencing a missing element. `contentProps.onOpenAutoFocus` and `onCloseAutoFocus` can prevent the default focus choice; the host then supplies the intended focus target. Other content events retain Radix cancellation semantics. Controlled `onOpenChange` can reject a pending wallet close without unmounting the dialog or moving focus back to the trigger. `AlertDialog` continues to prevent outside-pointer dismissal even when a host observes that event. These web composition props apply to `Dialog`, `AlertDialog` and the static `Sheet` alias. They do not add drag gestures, detents or product keyboard heuristics to the static sheet. For a native card stack, import `GestureSheet` from `@bloxwap/hig/native/gesture-sheet` and render the caller's cards directly: ```tsx const titleRef = useRef(null); {card.title} {card.content} ``` Here `Text` and `View` are React Native types/components, and the host supplies the card state and its own accessible back/close labels. Custom content can use its own Gorhom scrollable or list without an outer scroll view. `snapPoints` may change while open: the current detent index is retained, or clamped to the highest remaining index if its detent was removed. Detent changes keep the sheet mounted and do not repeat open focus. Keep at least one sorted snap point; `initialIndex` applies to the next presentation. Focus return, Android Back, reduced-motion and dismissible backdrop behavior apply in both content modes. For content-sized native cards, set `enableDynamicSizing` and omit `snapPoints`. This chooses one backend-measured detent rather than adding a competing fixed point. In custom mode HIG uses Gorhom's `BottomSheetView` to report intrinsic layout; it keeps a plain full-height `View` in fixed mode so nested scrollable registration stays with the caller. Choose the sizing mode for the presentation and avoid `flex: 1` or a full-height constraint on content that should determine its own height. Bound a long card's own scroll view. Default anatomy continues to use the backend scroll view. Mixed headers/footers and nested registered Gorhom scrollables compete for the backend's content-height measurement: their scroll content can replace the whole card height. Use host-measured fixed snap points for that composition. A content-sized card can instead bound a plain React Native `ScrollView` and set `enableContentPanningGesture={false}`, retaining the handle gesture and its own scroll gesture. This follows [Gorhom dynamic sizing](https://gorhom.dev/react-native-bottom-sheet/dynamic-sizing). ```tsx {contentSizedCard} ``` `maxDynamicContentSize` is an optional finite positive ceiling in native points; the backend container height is the default. The dynamic detent uses index `0`. Content growth, shrinkage and ceiling changes keep the presentation and card state, and height updates do not repeat open focus. Reduced motion applies HIG's no-animation backend policy; compositions that cannot safely report intrinsic height use the fixed-size alternative described above. A fixed sheet still requires a nonempty `snapPoints` list. `enableContentPanningGesture` passes through to the backend, allowing hosts with an independently scrolling list to disable content panning without changing their close/Back actions. Close, Android Back and iOS accessibility escape use the same controlled close request. If the host rejects the request and controlled `open` remains true, HIG retries presentation after host state settles, retaining the card and its state instead of leaving a backend-dismissed but logically open sheet. Rejected closes and interrupted reopenings do not return focus or call `onDismiss`; hosts may use that settled callback to clear retained state only after an accepted close. `onHardwareBackPress` can consume Android Back (`true`) or defer to the host's nested-card dispatcher (`false`); omitting it keeps the default dismissal behavior. Accessibility escape is a platform contract, not a physical VoiceOver certification. `topInset` and `bottomInset` default to safe-area values; use `0` for an intentional flush edge. `handleComponent` (including `null` for an overlay handle), `handleStyle`, `handleIndicatorStyle`, `backgroundComponent` and `backgroundStyle` retain host geometry and decoration. The backend measures the rendered handle. Use `useColors()` in custom backgrounds. `modalRef` exposes the optional backend's imperative motion methods; hosts must keep controlled `open` in sync with `present()`/`dismiss()`. Focus return and settled `onDismiss` remain controlled by the HIG wrapper. `animateOnMount`, `animationConfigs` and `backdropComponent` retain host entry motion and backdrop styling; the reduced-motion mapping still applies. A custom backdrop must honor the host dismissal policy. Keyboard policies are typed backend options: `keyboardBehavior`, `keyboardBlurBehavior` and `android_keyboardInputMode`. Defaults remain HIG's `interactive`, `restore` and `adjustResize`. A host retaining an existing Gorhom policy can explicitly pass `none` for blur and `adjustPan` on Android. See [Gorhom keyboard handling](https://gorhom.dev/react-native-bottom-sheet/keyboard-handling). For a screen-level drawer that must prepare and retain its native children while closed, choose `presentation='inline'`. HIG uses the public persistent `BottomSheet` backend; the default remains `presentation='modal'` with `BottomSheetModal`. Inline content stays mounted at index `-1`, hidden from accessibility and touch, and registers no Back listener while closed. Opening and reopening keep child/controller/list state. Rejected closes restore the last open detent; accepted closes return focus and call `onDismiss` once after settling. Use `sheetRef` for the inline backend's actual `expand`, `close` and `snapToIndex` methods; `modalRef` is only available in modal mode and exposes `present`/`dismiss`. Imperative calls must keep controlled `open` in sync. `onChange` observes backend index/position/type after HIG updates its index, focus and close request; it cannot replace those handlers. This lets a host change its animation policy after the opening detent settles. `containerStyle` retains sibling overlay order. Hosts own screen-level backdrop/container layering for inline presentation. No modal portal provider is required for inline sheets. ## Optional web gesture backend Install `vaul ^1.1.2` and import `GestureSheet` from `@bloxwap/hig/react/gesture-sheet`. Core `overlay`, Button and overlay-free recipes do not import or install Vaul. The backend supplies its own motion CSS when loaded; HIG's explicitly imported component CSS supplies scoped surface, text, geometry and reduced-motion rules. See the official [Vaul anatomy and API](https://vaul.emilkowal.ski/api) and the verified [1.1.2 source](https://github.com/emilkowalski/vaul/blob/v1.1.2/src/index.tsx). ```tsx import {GestureSheet} from '@bloxwap/hig/react/gesture-sheet'; { if (next || !pending) setOpen(next); }} title={t('markets')} titleHidden contentMode='custom' contentRef={panelRef} contentProps={{ className: 'host-market-card', style: { bottom: viewportBottom, maxHeight: availableHeight } }} backendProps={{ handleOnly: false, repositionInputs: false, shouldScaleBackground: true, dismissible: !pending }} handleProps={{ style: { width: 100 }, preventCycle: true }} closeButton={null} >

{t('markets')}

{hostHeaderAndScrollableContent}
``` `contentMode='default'` provides padded, scrolling anatomy. `custom` leaves padding and scrolling with the host. The required accessible title remains available when hidden; render a real visible heading when the host card needs one. `closeButton={null}` means the host must retain an explicit accessible close/Back action. `handleOnly=true` requires the actual handle for dragging. Handle styles, classes and capture events are customizable; HIG filters replacements for the backend's handle pointer/click handlers, hidden state and reserved `data-vaul-*` attributes. Frozen caller prop objects remain unchanged. Backend defaults remain Vaul's: `handleOnly=false`, `repositionInputs=true`, `shouldScaleBackground=false`, `dismissible=true` and `autoFocus=false`. Retain existing host flags explicitly. With `autoFocus=false`, hosts may use `contentProps.onOpenAutoFocus` to focus their heading/search control without requesting a software keyboard automatically. `shouldScaleBackground=true` requires the host's `[data-vaul-drawer-wrapper]` around the page. Physical `backendProps.direction` is independent of the provider's text direction. Fixed snap points retain Vaul's viewport geometry. The modal root stays controlled and modal; there is no second nested HIG Dialog root. For content-sized sheets, set top-level `enableDynamicSizing` and omit `backendProps.snapPoints`, `snapPoint`, `activeSnapPoint`, `setActiveSnapPoint`, `fadeFromIndex`, and `snapToSequentialPoint`. Dynamic sizing and those Vaul snap props are mutually exclusive; fixed snapping remains the other explicit mode. `maxDynamicContentSize` is an optional finite positive content-height ceiling in CSS pixels; the viewport ceiling still applies. Content growth and shrinkage update the open sheet smoothly without remounting its children or repeating open focus. Reduced motion applies the new size without height animation, and unavailable measurement or animation capabilities use a static final size. Dynamic height applies to top/bottom sheets; other physical directions retain their static layout. The controlled dynamic example in the catalog uses real React and React Native content expansion/collapse. The host can reject `onOpenChange(false)` while pending; HIG restores Vaul's last resting motion styles after a rejected drag without remounting card state. Use `dismissible=false` as well when pending should prevent dismissal attempts altogether. Backend `onAnimationEnd` is observed only when its reported state still matches controlled `open`, so rejected closes and stale opposite-state callbacks cannot clear retained card state. Programmatic state changes keep the backend's callback behavior. Reduced motion removes the sheet/overlay animation and transition; drag and fixed snap selection still work. Viewport constraints, editable-input blur policy, safe-area padding and product-specific keyboard accessory reserves stay in the host. `scripts/gesture-sheet-browser-smoke.mjs` exercises the real Vaul backend in Chromium and WebKit: drag/scroll handoff, handle-only drag, controlled veto with retained input identity, nondismissible state, snap expansion, dynamic growth/shrink and capped scrolling, rapid resize interruption, focus/node identity, focus trap/Escape/return, viewport resizing, host styles/refs and reduced motion. Evidence in `evidence/web-gesture-sheet/` records the exact source baseline and dirty state. Desktop viewport resizing does not certify physical mobile software-keyboard behavior; that remains a host/device observation. ## When to use and examples Do: repeat the order amount and give an explicit cancel action. Don’t: hide essential terms in hover help or execute confirm on open. ## States, interaction and accessibility DOM Radix focus scopes restore trigger focus, trap modal focus, handle Escape and scroll locks. AlertDialog blocks outside-click dismissal. Menus use roving keyboard focus. Native Modal owns back and keyboard avoidance with scrollable content. Native overlays follow the host orientation: React Native's iOS Modal defaults `supportedOrientations` to portrait, so HIG passes every orientation and opening a Dialog, AlertDialog, Sheet, Menu or Select never rotates a landscape host back to portrait (observed on the iOS Simulator, `evidence/native-sim/ios/orientation/`). Hosts still decide which orientations the app supports. State review: rest; keyboard focus/hover (DOM); accepted press and release/cancel; selected/checked where applicable; disabled; busy; inline error/recovery; empty/loading; long translations/large text; reduced motion; sound and haptics disabled. No host action occurs on mere focus. Disabled/busy targets suppress accepted activation. Core has no required feedback backend; the optional provider callback follows the action, honors cancellation and isolates errors. ## Platform mapping and limits Basic Sheet uses static Modal layout. The separate optional `@bloxwap/hig/native/gesture-sheet` entry provides GestureSheet with sorted snapPoints, initialIndex, dismissible, safe-area/keyboard configuration, reduced-motion mapping, native back, scoped listener cleanup and controlled dismissal deduplication. Rejected controlled closes re-present after host state settles. See `fixtures/native/gesture-sheet.tsx` and `index-gesture.js` for the real separate consumer; packed smoke compiles both Android/iOS entry points. Hosts provide GestureHandlerRootView, SafeAreaProvider and BottomSheetModalProvider. Required optional peers are Gorhom 5.2.14, Reanimated 4.1.1, Worklets 0.5.1 and Gesture Handler 2.28.0, pinned to Expo 54 bundledNativeModules rather than the newer monorepo native runtime. Backend configuration follows [Gorhom usage](https://gorhom.dev/react-native-bottom-sheet/modal/usage) and [props](https://gorhom.dev/react-native-bottom-sheet/props). Mocked wiring and Metro compilation are not gesture-detent or TalkBack certification. With TalkBack on the Android emulator, opening Dialog/AlertDialog/Sheet/Menu moves accessibility focus to the heading and Back closes the overlay; without a `returnFocusRef` TalkBack falls back to the first screen element, so hosts should pass the trigger. VoiceOver does not run in the iOS Simulator, so VoiceOver focus restoration remains a physical-device observation (`evidence/native-sim/`). Native help is persistent text; no hover tooltip or anchored popover claim. ## Validation and citation See `tests/gesture-sheet.dom.test.tsx`, `tests/native/gesture-sheet-dynamic.test.cjs`, consumer fixtures and milestone evidence. Real Chromium/WebKit cases cover fixed detents and dynamic growth/shrink; `evidence/web-gesture-sheet/` records the exact inputs. RN 0.88 iOS Simulator checks in `evidence/native-number-flow/ios-inline/` verify short→tall→restored geometry in normal and reduced motion, plus retained inline state and rejected/accepted dismissal. Physical hardware, VoiceOver/TalkBack speech, software-keyboard behavior and baseline product comparisons are unperformed unless linked explicitly. Cite HIG 0.1.0 with this chapter anchor and a commit permalink, not a mutable preview branch. Preserve business behavior behind a host rollback flag during adoption. ## Recipes · HIG 0.1.0 Source: docs/components/recipes.md # Recipes · HIG 0.1.0 Composed patterns for balances, markets, trade review, and documentation. Includes `MarketRow`, `BalanceCard`, `TradeEntry`, `ActivityList`, `InstallCommand`, `CodeWindow`, and `MarketingHero`. Issue #26. Entry: `recipes`. Import platform entries from `@bloxwap/hig/react/recipes` or `@bloxwap/hig/native/recipes` according to applicability below. On web, each recipe also has its own entry, for example `@bloxwap/hig/react/recipes/install-command`, `recipes/code-window`, `recipes/market-row`, `recipes/balance-card`, `recipes/marketing-hero` and `recipes/activity-list`. These need no Radix peer; `smoke:packed-boundaries` renders and bundles them with no Radix installed (#63). `recipes/trade-entry` and the aggregate `recipes` entry need `@radix-ui/react-dialog` for the confirm step. Import `@bloxwap/hig/react/css` once on web; tokens CSS is a separate explicit import. ## API and anatomy All data and actions are supplied by the host application. | Recipe | Key props and behavior | | --- | --- | | `TradeEntry` | Raw `amount`, `onAmountChange`, `onConfirm`, `available`, `busy`, and `error`; opens review before submission. | | `InstallCommand` | `command` and `onCopy`; accepts an asynchronous host clipboard adapter. | | `MarketRow` (web) | `symbol`, `name`, `price`, `change`, and `onSelect`; `priceOptions` controls Intl precision and currency/decimal presentation; `changeOptions` controls percent-point precision/signs. `mode='presentation'` lets a host option own selection, with optional `identity`, `icon`, `favorite`, and formatted `volume` slots. | Web `MarketRow` keeps its name button and USD formatting by default. A command palette that already owns option selection uses presentation mode to avoid a second action and tab stop: ```tsx openMarket(market.symbol)}> {market.symbol} {market.name}} icon={} favorite={} volume={} style={{gridTemplateColumns:'minmax(0,1fr) 12ch 8ch 10ch'}}/> ``` Presentation mode uses a grid with logical numeric alignment and no block padding. The host can set matching column widths on each row through `style`/`className`, its compact height and the surrounding keyboard/option semantics. The number face inherits from that row. Slots render host content; keep decorative icons/indicators hidden from assistive technology, or give interactive controls their own names and deliberate keyboard behavior. Presentation rows accept no `onSelect` and emit no action feedback. Tiny prices keep the requested precision; unknown values remain —. Volume formatting and units stay host-owned. The optional `identity` slot belongs to presentation mode; action mode keeps its named HIG button. Hosts that use `toFixed` rounding should pass their already-rounded percent-point value plus explicit `changeOptions` to preserve binary rounding, trailing digits, signs and negative zero. Options use the same contract as `Percent`: the input is percent points, and the rendered style remains percent. ## When to use and examples Do: compose primitives and explicit outcomes in comfortable app or compact Pro context. Don’t: put wallet/network/SDK services in core packages. ## States, interaction and accessibility Native CodeWindow and InstallCommand render code in the provider's `monoFontFamily` (for example the registered `BloxwapMonoRegular`; platform monospace otherwise) and keep code left-to-right inside RTL roots. Copy failures leave a manual-selection recovery message. Trade confirm remains host-controlled; no demo sends a transaction. Web/native fixtures show balance, market, entry and feedback preference previews. State review: rest; keyboard focus/hover (DOM); accepted press and release/cancel; selected/checked where applicable; disabled; busy; inline error/recovery; empty/loading; long translations/large text; reduced motion; sound and haptics disabled. No host action occurs on mere focus. Disabled/busy targets suppress accepted activation. Core has no required feedback backend; the optional provider callback follows the action, honors cancellation and isolates errors. ## Platform mapping and limits M2 supplies visual recipes; M3 adds semantic feedback composition because #26 depends on #25. Product visual fidelity is provisional until inaccessible monorepo audit. Account/positions variants can compose these primitives without service coupling. ## Validation and citation See `tests/components.dom.test.tsx`, `tests/pro-data.dom.test.tsx`, `tests/pro-data-typecheck.tsx`, `tests/native/components.test.cjs`, consumer fixtures and milestone evidence. Physical hardware, assistive-technology and baseline product comparisons are unperformed unless linked explicitly. Cite HIG 0.1.0 with this chapter anchor and a commit permalink, not a mutable preview branch. Preserve business behavior behind a host rollback flag during adoption. ## Selection controls · HIG 0.1.0 Source: docs/components/selection.md # Selection controls · HIG 0.1.0 Controls for choosing options, changing preferences, and adjusting values. Includes `Checkbox`, `RadioGroup`, `Switch`, `SegmentedControl`, `Select`, and `Slider`. See the navigation guide for `Tabs`. Issue #15. Entry: `selection`. Import platform entries from `@bloxwap/hig/react/selection` or `@bloxwap/hig/native/selection` according to applicability below. Import `@bloxwap/hig/react/css` once on web; tokens CSS is a separate explicit import. ## API and anatomy | Control | Key props and behavior | | --- | --- | | Checkbox and switch | `checked`, `defaultChecked`, and `onCheckedChange`. Native `Switch` also takes `tone`, a token role (`action` default, `positive`, `negative`, `warning`, `info`, `buy`, `sell`) for the on track; raw colours are not accepted and fall back to `action`. On Android the thumb uses the `text` token, so the app theme accent never shows. | | Choice controls | `value`, `defaultValue`, and `onValueChange`. Options have `value`, `label`, and optional `disabled`. Native options also accept `description` (a second line) and `lang` (the label's BCP-47 language). | | `SegmentedControl` | Equal-width segments with a shared selection pill on web and native. Accepted selection changes slide the pill using the base motion token (260ms); repeated or rejected changes do not move it. Initial placement and reduced motion are immediate. No additional animation peer is required. | | `Select` | A custom styled combobox/listbox on web. Arrow keys, Home/End, typeahead, Enter/Space, Escape and Tab support keyboard selection; disabled options cannot be selected. The popup escapes clipped containers while inheriting HIG theme and density. | | `Slider` | `min`, `max`, `step`, and `value`; a visible `label` is required. Web uses the product slider’s white 20 × 14px oval thumb and 6px track. Native keeps the community backend’s platform thumb shape with HIG colors. The DOM keyboard focus ring is drawn inside the 48px range box, so it never paints over the label or the next control at comfortable or compact density. Native increment/decrement actions use HIGProvider `messages.increase`/`messages.decrease`. | ## When to use and examples Do: control selection from accepted host state. Don’t: emit a success cue on every render or repeated slider value. Native language pickers can keep the native name and a translated description in one radio row: ```tsx ``` Register each face before passing its name. `Text` chooses `fontFamiliesByScript` by the label's glyphs, independently of the app locale; `lang` supplies `accessibilityLanguage` and resolves shared CJK punctuation. Descriptions use the app language. A script map selects the requested static weight, or its nearest registered weight; the existing `fontFamilies`/`fontFamily` remain the fallback. Explicit text `script` and style `fontFamily` override detection and registered family selection respectively. Native radios expose the label, supporting description as an accessibility hint, checked/disabled state and the explicit `activate` accessibility action; disabled rows reject both touch and accessibility activation. Per-option language is passed to React Native's `accessibilityLanguage`; pronunciation support depends on the platform and installed voices. ## States, interaction and accessibility Checkboxes, radio controls and sliders retain native HTML keyboard semantics. Web Select renders HIG options in a custom popup; focus stays on its labeled combobox, which announces the active option. Escape dismisses without changing selection, and Tab dismisses while moving focus onward. RN Switch renders the OS track inside one labeled 48dp switch row (the row is the accessibility element and touch target); checkbox/radio use labeled Pressables with checked state. RN slider uses the optional community backend for the visual track and touch drag; a plain 48dp adjustable View owns the label, value and increment/decrement actions (5% steps) on iOS and Android because the backend's iOS Fabric view ignores accessibility increments and its Android SeekBar node measures 18dp. On Android the wrapper is the package's autolinked `HIGAdjustableView`, which also lists `ACTION_SET_PROGRESS` for Voice Access and Switch Access: a requested value snaps to `step`, clamps to `min`/`max`, updates the control once and is rejected while disabled. React Native's own `accessibilityActions` cannot express set-progress (custom names get synthetic ids and the progress argument is dropped). Without native linking (Expo Go) the wrapper is a plain View and set-progress is absent. The backend's native track height (18dp Android, 40pt iOS) still bounds direct touch drag; hosts needing a larger drag area should pad around the control. Select opens labeled native Modal choices and returns focus to its trigger. State review: rest; keyboard focus/hover (DOM); accepted press and release/cancel; selected/checked where applicable; disabled; busy; inline error/recovery; empty/loading; long translations/large text; reduced motion; sound and haptics disabled. No host action occurs on mere focus. Disabled/busy targets suppress accepted activation. Core has no required feedback backend; the optional provider callback follows the action, honors cancellation and isolates errors. ## Platform mapping and limits SegmentedControl uses single-choice radio semantics on both platforms. Use it for a short set of related choices; use `RadioGroup` when options need longer descriptions. Web retains native radio keyboard behavior and a visible focus ring. Native segments keep 48dp minimum touch targets, wrapping labels, per-option language and accessibility activation. The decorative pill is hidden from accessibility and does not intercept taps. Its position follows RTL direction and available width; native layout changes snap into place, and rapid selections interrupt the current animation. `HIGProvider reducedMotion` and the OS motion preference disable movement. A controlled pill follows the accepted `value`, including programmatic changes. An absent value has no pill. Slider accessibility increment/decrement, set-progress and disabled rejection are verified on the iOS Simulator and Android emulator (`evidence/native-sim/`); tactile drag feel and haptic rate remain physical-device work; the slider backend is an optional native peer. Earlier native SegmentedControl gallery captures show the previous radio-list rendering and do not validate the new animation on physical devices. ## Validation and citation See `tests/select.dom.test.tsx`, `tests/components.dom.test.tsx`, `tests/segmented-control.dom.test.tsx`, `tests/native/components.test.cjs`, `tests/native/segmented-control.test.cjs`, `tests/native/language-options.test.cjs`, consumer fixtures and milestone evidence. `node scripts/select-browser-smoke.mjs` verifies the custom dropdown in Chromium/WebKit at mobile and desktop sizes, including RTL, clipped containers, keyboard selection and modal focus. `node scripts/segmented-control-browser-smoke.mjs` checks real animation frames, interruption, RTL, responsive alignment and reduced motion in Chromium/WebKit for React DOM and React Native Web; it does not certify iOS/Android rendering. Physical hardware, assistive-technology and baseline product comparisons are unperformed unless linked explicitly. Cite HIG 0.1.0 with this chapter anchor and a commit permalink, not a mutable preview branch. Preserve business behavior behind a host rollback flag during adoption. `SwitchRow` from `@bloxwap/hig/native/switch` (also `selection`) wraps an existing host row in one switch target. It takes the same required `label`, `checked` / `defaultChecked`, `disabled` and `onCheckedChange` contract as `Switch`, plus required noninteractive `children` and root Pressable props. Keep the visible label, description and track noninteractive: the root owns activation and hides the content from accessibility to avoid duplicate targets. Put the description in `accessibilityHint`. Root `style` supports pressed callbacks; `accessibilityState` preserves `busy` while HIG owns checked/disabled state. Disable the row explicitly during pending requests; busy alone is descriptive. The Expo web row supports Space, Enter and pointer activation. Tree shaking an ESM import of ordinary `Switch` removes this optional anatomy. ## Status & feedback · HIG 0.1.0 Source: docs/components/status.md # Status & feedback · HIG 0.1.0 Visible progress, notifications, and recoverable states that keep people informed. Includes `Badge`, `Alert`, `Callout`, `Toast`, `Progress`, `Spinner`, `Skeleton`, `EmptyState`, `Avatar`, and `StatusIndicator`. Issue #18. Entry: `status`. Import platform entries from `@bloxwap/hig/react/status` or `@bloxwap/hig/native/status` according to applicability below. Import `@bloxwap/hig/react/css` once on web; tokens CSS is a separate explicit import. ## API and anatomy | Component | Key props and behavior | | --- | --- | | `Badge` | Quiet label; web adds explicit `tone` (`neutral`, `positive`, `negative`, `warning`, or `info`), standard span props and a forwarded span ref. The neutral default retains its appearance. Tone uses provider token roles and never infers meaning from text or a number's sign. Badge adds no live region; the host owns announcements. | | `Alert` | `title`, `severity`, and an optional recovery `action`. Severity sets the glowing stroke from a token role on web and native — a 2px tone border on all sides with a matching `0 0 24px` glow at 55% on a translucent (95%) surface, 16px radius: `default` → neutral (`border` token stroke, soft white 20px glow), info → `info`, success → `positive`, warning → `warning`, error → `negative`. On native, `tone` swaps in another token role for an approved product accent, never a raw colour. | | `Callout` | Same API as `Alert`, for static guidance. It adds the documentation severities `note` (`info` role) and `tip` (`positive` role), so SDK docs do not need their own stroke colours. Pass `announce={false}` for callouts present at page load. `title` accepts a ReactNode, such as text with a `` span; on web it renders inside `` and should stay inline. On native it renders inside the `Heading`, so pass a string or inline `Text`; its visible text is announced. | | `Toast` | `notices` with stable `id`, `message`, `severity`, and optional `action`; `onDismiss` handles dismissal. Toast cards reuse the Alert glowing stroke at a pill (999) radius. Native notices also accept `description` and `closable` (default true when `onDismiss` is supplied). Native `maxHeight` bounds a scrolling viewport; it defaults to 60% of the window height and accepts dp or a parent percentage. `dismissLabel` overrides visible Dismiss text; its accessible name contains that text. | | `Progress` | `label`, `value`, and `max`. | | `EmptyState` | `title`, `description`, and an optional `action`. | Native `EmptyState` keeps the default `Surface`, `Heading` and body-text anatomy. Its `description` is optional; omitted or empty text leaves no blank description node. Pass decorative `illustration` content before the heading; HIG hides the whole illustration subtree from VoiceOver/TalkBack navigation while keeping the heading and action accessible. The host controls the illustration's dimensions and spacing. Root `ViewProps`, including `style`, `testID` and layout events, pass to the Surface. `titleStyle` and `descriptionStyle` reach the actual text nodes; `maxFontSizeMultiplier` caps both, or inherits the provider policy when omitted. Text keeps the provider palette and heading semantics unless the host explicitly styles it. ```tsx } style={{ flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: 'transparent', padding: 0, gap: 12 }} titleStyle={{ fontSize: 20, lineHeight: 28, textAlign: 'center' }} maxFontSizeMultiplier={2} action={} /> ``` The mark and clear-filter action remain host-owned. `tests/native/empty-state.test.cjs` verifies default anatomy, omitted descriptions, decorative accessibility metadata, real text/root props and accepted action delivery; these wiring checks are not physical-device accessibility certification. ## When to use and examples Do: keep critical failure inline with Retry; use toast for copied acknowledgement. Don’t: dismiss critical text on a timer or rely on audio for success. ## States, interaction and accessibility Notifications deduplicate ids and preserve every message until explicit dismissal, including bursts larger than three. Host delivery queues may limit arrivals, but the renderer never silently removes an undismissed message. A single coalesced announcement reports each changed burst, prioritizing errors; unchanged rerenders do not repeat it. DOM and Expo web use one live region while every notification remains visible. Native Android uses explicit announcement delivery, and inline Android status uses live-region metadata; iOS announces changed inline content explicitly. Actual VoiceOver/TalkBack timing remains unobserved. Skeleton and spinner are static under candidate motion policy. State review: rest; keyboard focus/hover (DOM); accepted press and release/cancel; selected/checked where applicable; disabled; busy; inline error/recovery; empty/loading; long translations/large text; reduced motion; sound and haptics disabled. No host action occurs on mere focus. Disabled/busy targets suppress accepted activation. Core has no required feedback backend; the optional provider callback follows the action, honors cancellation and isolates errors. ## Platform mapping and limits Toast is an inline notification stack, no mandatory portal/timer backend. Host owns delivery ordering/persistence. On native, messages use body-size semibold text and descriptions use a separate body-size line; neither truncates or disables text scaling. The stack scrolls within `maxHeight`, including nested host scroll views. A notice with `closable={false}` has no Dismiss action; host recovery actions remain available. Without `onDismiss`, notices have no Dismiss buttons. No notice expires automatically: remove it from the controlled `notices` array when its host lifetime ends; keep failures with recovery actions until explicitly resolved. VoiceOver/TalkBack observations are pending, automated role/state tests are not device evidence. ```tsx // Native: compact notices with large-text scrolling and a persistent failure. ``` ## Validation and citation See `tests/components.dom.test.tsx`, `tests/native/components.test.cjs`, consumer fixtures and milestone evidence. Physical hardware, assistive-technology and baseline product comparisons are unperformed unless linked explicitly. Cite HIG 0.1.0 with this chapter anchor and a commit permalink, not a mutable preview branch. Preserve business behavior behind a host rollback flag during adoption. ## Optional adapters and installation · HIG 0.1.0 Source: docs/feedback/adapters.md # Optional adapters and installation · HIG 0.1.0 Issues #22–#24. Base imports do not load adapters. Individual `web-sound`, `browser-haptic`, `web-haptic`, `native` and `react` entries preserve dependency boundaries. The composite `web` entry needs the Haptic package; sound-only consumers should import `web-sound`. ## Web sound Install `@bloxwap/hig/feedback@0.1.0 @bloxwap/sfx@^0.2.0`. `createWebSoundAdapter()` loads SFX only for enabled requests after an accepted interaction. It calls the documented unlock/play APIs, passes per-call volume and uses the same presets as native. New requests replace pending startup, so delayed loading does not create a backlog. It avoids upstream global enabled/volume stores because HIG preferences are root-scoped. `@bloxwap/sfx/react` owns global preference helpers; using those alongside HIG would create competing preference owners. HIG passes one adapter-local `AbortSignal` to each play. Muting, backgrounding, explicit cancellation and disposal abort that root's plays; the next accepted request gets a fresh signal. This stops buffered and live voices while other roots and host-owned voices continue. Generations still discard superseded startup/unlock requests. HIG reference-counts default adapters and disposes the shared engine after the last user. A host that also owns that engine should inject/own its lifecycle. ## Browser and physical-tap haptics `createBrowserHapticAdapter()` maps accepted semantic events to browser vibration when `navigator.vibrate` exists. Its existing default and `{vibrate}` injection retain their patterns; neither requires importing Haptic. Device intensity/pattern quality varies and may be suppressed. iOS WebKit without vibration cannot provide imperative async outcome feedback; the adapter reports unavailable and remains silent. The optional `ImperativeHapticBackend` injection accepts the `canTriggerHaptic` and `triggerHaptic` functions from `@bloxwap/haptic` (`^0.2.0 || ^0.3.0`): `createBrowserHapticAdapter({canTriggerHaptic, triggerHaptic})`. HIG probes capability at request time, so activation and document visibility are current, and delegates the semantic cue to upstream's presets. Rejection/failure cannot change the primary action. `observed` stays false. The adapter adds no eager import, helper DOM or physical-tap simulation. The imperative API reports iOS unavailable, including during transient activation. It cannot replace eligible physical targets or express iOS async outcome patterns. Neither the browser Vibration API nor this upstream API provides a per-root cancellation handle: `cancel()`/`dispose()` retain the existing `navigator.vibrate(0)` stop when available, which is document-wide. This is distinct from SFX's per-root voice cancellation. For approved iOS physical action targets, install `@bloxwap/haptic@^0.3.0` and wrap a single ref-capable HIG button in `HapticFeedbackTarget eligible`. The wrapper uses upstream `HapticTarget`, preserves the target/handler/ref, enables only the physical-tap route and disables debug audio/helper switch visibility. Thus browser vibration comes from the semantic adapter and is not duplicated by the wrapper. HIG preferences still gate the wrapper. **Do** pre-approve the target with `eligible` after disabled/busy/policy checks. **Don't** wrap an action whose eligibility is decided only by an async click callback, or promise a programmable iOS success/error pattern. The OS physical tick occurs before delegated handlers; a later preventDefault cannot retract that tick. That cancellation limitation, physical-tap intensity limits and device/VoiceOver checks remain upstream [haptic#9](https://github.com/bloxwap/haptic/issues/9) / [haptic#7](https://github.com/bloxwap/haptic/issues/7). The missing imperative iOS API for semantic controllers is [haptic#27](https://github.com/bloxwap/haptic/issues/27) (upstream follow-up to #70). No wrapper is required for basic actions. ## Native audio Install the feedback package plus `expo-audio` (`~1.1.1` on Expo SDK 54, `~55.0.18` on SDK 55) only in the native host setup. `expoAudioBackend(Audio)` supplies a thin playback bridge; `createNativeSoundAdapter(backend, assets)` accepts Metro asset ids. The Expo gallery imports only the four committed WAV cues. Configure ambient/silent-mode behavior with `playsInSilentMode:false`, no background playback and `doNotMix` interruption mode. Host AppState calls `setBackground`; backgrounding pauses cached players. Call `preload()` at startup: it configures the audio session and caches players once. After that, `play()` dispatches synchronously, with no awaited bridge round trip before `player.play()`. It stops only the previous cue, issues `seekTo(0)` without awaiting it (native players apply calls in order) and plays. A cue requested before preload settles waits for it, and only the newest such request plays. Concurrency is limited to one active cue, volume/mute are honored, missing cues are silent, and disposal pauses/removes every cached player. Playback completion: HIG does not listen to `playbackStatusUpdate` and never relies on `didJustFinish`. On Android, `expo-audio` 1.1 (Expo SDK 54) reports `didJustFinish` twice for one finished cue; SDK 55 (`expo-audio` 55.0) reports it once (#89). In the Android emulator run, SDK 54 had 4 `play()` calls, 4 playing starts and 8 `didJustFinish` statuses: the same finish reported twice, not a second play (`evidence/native-sim/issue-86-88-89/`). In `expo-audio` 1.1, every status sent while the player is ended carries `didJustFinish: true`, and two listeners send one when a cue ends. A host or future adapter that acts on completion must de-duplicate it, for example by ignoring a `didJustFinish` that has no `play()` or `playing: true` status since the last one. Device timing for both SDKs is tracked in #55. Regenerate with `npm run generate:cues`. `assets/manifest.json` pins package/recipe/export flags, 48kHz mono PCM16, durations, peaks/RMS, bytes and reviewed hashes. Noise recipes vary; do not assert regenerated WAV bytes are deterministic. Inspect duration/envelope/level tolerances and review the actual artifact hashes. Offline generation dependencies are dev-only; Metro never imports them or browser synthesis. ## Native haptics Install `expo-haptics` (`~15.0.7` on Expo SDK 54, `~55.0.18` on SDK 55) in the native host. `expoHapticBackend(Haptics)` bridges selection, impact light/medium/heavy and notification success/warning/error. `createNativeHapticAdapter` reports API availability separately from observation. Unsupported hardware, backgrounding, preferences and failed backend calls are no-ops that do not change primary actions. Android notification/impact approximation and OS suppression must be observed on devices before support is certified. ## Simulated lifecycle regression evidence The SFX tests use the lockfile's published `@bloxwap/sfx` package with its recording AudioContext fake to cover buffered/live root isolation, controller mute/background, cancellation during unlock, reuse and last-default-user engine disposal. Haptic tests use the lockfile's published `@bloxwap/haptic` capability/trigger functions with fake browser globals, cover changing activation/visibility, rejection, SSR and iOS unavailability, and preserve the released fallback. These tests request no physical sound or haptics. The automated tests inject native player and haptic backends. Pending configure/seek requests are superseded by the newest request; cancellation, backgrounding, zero volume and disposal stop pending playback. Failed configuration can retry. Explicit impact intent reaches the native backend even for an otherwise silent press preset. Pause/remove failures cannot prevent other cached players from being released. A dispatch test drives 400 cues through `FeedbackController` with simulated slow bridge calls (configure 60 ms, seek 40 ms). It asserts that every cue reaches `player.play()` inside `emit()` and that p95 stays under one 16 ms frame. The native gallery stamps each emit and logs `dispatch`/`p95` on every play for simulator and emulator runs. These tests establish dispatch/lifecycle behavior, not observed sound, tactile output, acoustic latency (#55) or OS audio-session behavior. Menu disclosure and dismissal are intentionally silent. Web/native Menu commits emit a single selection intent after the host handler; a host can cancel that effect with the selection event's `preventDefault()`. Native accordion disclosure emits one silent disclosure intent rather than duplicate activation/selection effects. The Chromium and WebKit fixture also checks switch, range, menu selection and canceled menu actions with injected adapters. ## Semantic feedback · HIG 0.1.0 Source: docs/feedback/contract.md # Semantic feedback · HIG 0.1.0 Issues #21, #25. Types in `@bloxwap/hig/contracts/feedback` and `@bloxwap/hig/feedback` import without DOM, RN, React, audio, Expo or hardware APIs. Base renderers accept an optional `FeedbackSink`; no provider or feedback backend is required to perform an action. | Intent | Timing | Sound | Haptic | Cooldown | | --- | --- | --- | --- | --- | | press/release | gesture phases; visual response | silent | none | none | | cancel | discarded gesture/action; clears pending work | silent | cancel where possible | none | | activate | accepted button click/keyboard/native press, after host handler | tick | selection | 80ms | | selection/toggle | accepted state commit, never a render/repeated/rejected value | tick/toggle | selection | 80ms | | disclosure | accepted open/close | silent | none | 120ms | | success | host operation completion with stable operation id | success | notification success | 400ms | | warning | host outcome | silent | notification warning | 400ms | | error | host operation failure with visual recovery text | error | notification error | 400ms | | notification | nonessential transient acknowledgement | silent | none | 800ms | The exported `presets` table is authoritative. Each event has id, source, intent and origin. An async outcome uses the host operation id, not the button's interaction id; an accepted click does not imply order success. Optional native impact overrides map light/medium/heavy; browser physical-tap intensity cannot be programmed. Live price ticks and general navigation are silent. **Do** emit a success event after the host request resolves, alongside visible success text. **Don't** play a success sound on press-in, for a rejected controlled value, or on every market render. Busy/disabled/suppressed/cancelled events are rejected. Buttons compose the host handler before activation, honor default prevention, preserve refs and block nested activation propagation. Choice controls emit on committed changes only. Sliders use semantic steps and controller cooldown; no effect queue accumulates. A host cancellation before activation suppresses the request; a physical cue already delivered by the OS cannot be recalled. Preference precedence: host policy may forbid a channel; persisted user choices enable sound/haptics independently; component suppression can disable its request; OS/browser/backend capability decides whether output is attempted. Volume is clamped 0–1. Motion is a separate setting and OS reduced motion remains respected. Defaults are both channels off. No-feedback, sound-only, haptics-only, both, and host-injected adapter configurations use the same actions. Root/local `FeedbackProvider`s hold independent preference/controller state; a nested button emits one request. Deduplication uses source/id/intent; host stable outcome ids suppress repeated notifications. Cooldowns drop excess requests, never delay primary actions or queue a backlog. Seen/cadence maps are bounded. Exceptions thrown/rejected by backends and storage are caught. Disposal is idempotent and late startup/preload completions cannot revive disposed players. Capability `available` is API routing, while `observed` records actual physical evidence and remains false in this candidate. ## Preferences, preview and device review · HIG 0.1.0 Source: docs/feedback/preferences.md # Preferences, preview and device review · HIG 0.1.0 `FeedbackProvider` accepts a stable `createController(preferences)` factory and optional storage adapter. `useFeedback` exposes preferences, update, sink and controller. `FeedbackPreferencesControl` is a render-prop control so DOM and RN use their own shipped Switch/Slider instead of importing another platform. Use a stable factory; mount/replay creates fresh adapters and unmount disposes them. React StrictMode lifecycle is tested. `jsonStorage` supports browser Storage or native AsyncStorage-shaped interfaces. It uses a versioned key; read legacy saved choices explicitly with a host adapter before writing the new key. `migratePreferences` preserves explicit false sound/haptic choices, legacy soundEnabled/hapticsEnabled, clamped volume and independent reducedMotion/motion. Corrupt/missing storage falls back to muted defaults. A delayed saved read cannot overwrite a newer user choice. Storage rejection leaves the live UI usable. Web and native fixtures include real independent sound, haptics, volume and motion controls, entry/selection/success preview and visible state. The web fixture persists localStorage; the native gallery persists AsyncStorage. Both wire lifecycle background state. Sound and haptic preferences are never encoded in wallet/trade logic. Native gallery sound import/package/Metro success is automated evidence, not audible/tactile observation. | Platform | Routing | Evidence and acceptance | | --- | --- | --- | | Desktop Chrome/Firefox/WebKit | Web Audio after gesture; vibration only when API exists | Automated interaction; no hardware assertion | | iPhone Safari | optional pre-approved physical target; async haptic adapter unavailable | Owner-attested under #55; no recorded physical tap, VoiceOver or settings/OS observation | | Android browser | browser vibration patterns when allowed | Owner-attested under #55; no recorded device output/intensity observation | | Native iOS/Android | Expo audio and selection/impact/notification bridges | Owner-attested under #55; no recorded physical output, acoustic latency or interruption observation | | Simulator / Jest mocks | API/contract behavior only | never tactile certification | HIG's physical acceptance uses the [2026-10-19 owner attestation](https://github.com/bloxwap/hig/blob/main/evidence/physical/owner-attestation-2026-10-19.md), approved in [PR #131](https://github.com/bloxwap/hig/pull/131), with review due **2027-01-15**. This is a dated owner-approved exception for #55, not a recorded device run. A contradicting physical result voids the relevant exception. Upstream [haptic#9](https://github.com/bloxwap/haptic/issues/9) remains its own physical-certification tracker. A recorded device acceptance run must include HIG/package versions, device, OS/browser, date, observer and actual observations: default/muted channels; each independent combination; volume/silent-mode; latency; interruptions; background/foreground; repeated/nested activation; cancelled scroll/drag; disabled/busy; slider repeated values; iOS preset limits; accessibility helper tab/VoiceOver order; visible recovery with feedback off. Attach video/measurements where useful. Reuse upstream haptic certification rather than declaring simulator passes as hardware proof. Owner: HIG and owning app/haptic maintainers; Joe Blau accepted the named adoption ownership in #57. ## Trading glossary · HIG 0.2.0 Source: docs/foundations/glossary.md # Trading glossary · HIG 0.2.0 Issue #46. Locked before any locale translation. Machine-readable source: `docs/foundations/glossary.json` (also exported as `/hig/glossary.json`). Formatting rules: [localization guide](localization.md). Translation agents **must** consume the JSON. The rendered page is a view of that source. Do not invent terms, back-port product UI copy, or fill pending locales. ## Locales | Locale | Status | Role | | --- | --- | --- | | `en-US` | approved | Canonical English. HIG defaultEnvironment and authored documentation locale. | | `es` | todo | No HIG-approved glossary translation. Do not guess. Observed: apps/app/messages/es.json in the product inventory. | | `fr` | todo | No HIG-approved glossary translation. Do not guess. Observed: apps/app/messages/fr.json in the product inventory. | | `ko` | todo | No HIG-approved glossary translation. Do not guess. Observed: apps/app/messages/ko.json in the product inventory. | | `ko-KR` | todo | No HIG-approved glossary translation. Do not guess. Observed: Google Play store-screenshot locale in the product inventory. | | `zh-CN` | todo | No HIG-approved glossary translation. Do not guess. Observed: apps/app/messages/zh-CN.json in the product inventory. | | `zh-Hans` | todo | No HIG-approved glossary translation. Do not guess. Observed: App Store screenshot locale in the product inventory. | | `de-DE` | not a translation locale | HIG Intl number-format fixture only. Not an approved translation locale. | | `ar-EG` | not a translation locale | HIG Intl percent/numeral fixture only. RTL is a layout direction, not a glossary locale. | `en-US` is the only approved HIG glossary locale (`defaultEnvironment.locale`). Product message catalogs and store screenshots listed above are inventory observations, not approvals. ## Change process 1. Edit English fields in `docs/foundations/glossary.json` only. Do not hand-edit this Markdown. 2. Run `node scripts/generate-glossary.mjs` (also invoked by docs generation) and commit the JSON and Markdown together. 3. A term is locked when its English definition, usage notes, do/don't examples and `untranslated` flag are reviewed in this repository. Do not start translations before that lock. 4. Promote a pending locale only by setting `status: approved` and supplying `translations[locale]` for **every** term. Incomplete locales stay `todo`. 5. Never guess a translation. Product/marketing strings are not an approved glossary source. 6. Changing a locked definition is a breaking content change for translation agents. Call out term ids in the PR. Tickers, brand names and `untranslatedForms` never enter translation files. 7. `de-DE` and `ar-EG` remain Intl fixtures, not glossary locales. This chapter blocks locale translation work until the JSON lock is adopted. ## Terms ### Perpetual (perp) Term id: `perp`. Aliases: `perps`, `perpetual futures`, `perpetual contract`. **Untranslated:** keep the listed short form in every locale. Untranslated forms: `perp`, `perps`. **Definition:** A derivative that tracks an asset’s price with no fixed expiry. Funding payments help keep the contract aligned with its underlying market. Stocks, commodities, indices and pre-IPO listings on Bloxwap are perps, not ownership of the underlying asset. **Usage:** Prefer “perp” in dense UI (market row, ticket). Use “perpetual” once in explanatory copy, then the short form. Never imply the trader owns the tracked stock, commodity or token. **Do:** Label a Bitcoin market “BTC perp”. Say “open a perp position” rather than “buy Bitcoin” when the instrument is a perpetual. **Don't:** Don’t call a perp a spot purchase, share, or tokenized stock certificate. Don’t invent an expiry date for a perp. See also: `funding`, `position`, `leverage`. ### Leverage Term id: `leverage`. Aliases: `lev`. **Untranslated:** no — translate in approved locales. **Definition:** Position size relative to the margin supporting it. At 10×, 100 USDC of margin supports 1,000 USDC of notional exposure. Leverage magnifies both gains and losses. Market-specific limits are set by Hyperliquid, not by HIG. **Usage:** Display as a multiplier with the multiplication sign (`10×`), never as a percent (`1000%`). Keep the `×` suffix untranslated. Limits and available leverage are host-supplied. **Do:** Show selected leverage as `10×` next to size. Repeat the selected leverage on the review step before the host submits. **Don't:** Don’t write `10x leverage = safe`. Don’t display leverage as `+1000%` or reuse percent styling. See also: `margin`, `notional`, `liquidation-price`. ### Margin Term id: `margin`. Aliases: `collateral`, `cross margin`, `isolated margin`. **Untranslated:** no — translate in approved locales. Untranslated forms: `USDC`. **Definition:** USDC collateral used to open and maintain a leveraged position. If collateral falls below the maintenance requirement, Hyperliquid can liquidate the position. With cross margin, liquidation can consume the whole trading account, not only one position’s margin. **Usage:** Bloxwap trading collateral is USDC. Say “margin” for trading collateral; do not call it a deposit held by Bloxwap. Isolated vs cross is a host/protocol mode, not a HIG component. **Do:** Label available collateral “Available margin (USDC)”. Explain that cross margin can consume the trading account, not only the row’s margin. **Don't:** Don’t call USDC margin a bank deposit or a Bloxwap-custodied balance. Don’t display unknown margin as `0`. See also: `usdc`, `leverage`, `liquidation-price`, `position`. ### Position Term id: `position`. Aliases: `open position`, `exposure`. **Untranslated:** no — translate in approved locales. **Definition:** Open long or short exposure in one market. A position has size, side, margin, leverage, entry, mark, notional, unrealized PnL and an estimated liquidation price. Closing requires a host-submitted order; HIG never executes a trade. **Usage:** One row per market/side as the host provides. Don’t merge unrelated markets into a single position label. Size stays a raw decimal string in business logic. **Do:** Title a row “BTC long” with size, leverage and PnL as separate readouts. Keep last-known position data and mark it stale when the host is offline. **Don't:** Don’t say “position closed” until the host confirms the close fill. Don’t treat a formatted size as the order quantity. See also: `long`, `short`, `notional`, `pnl`. ### Liquidation price (liq. price) Term id: `liquidation-price`. Aliases: `liq price`, `liquidation`, `estimated liquidation price`. **Untranslated:** no — translate in approved locales. **Definition:** The estimated mark price at which the position no longer meets its maintenance margin and Hyperliquid may force-close it. The figure is an estimate; auto-deleveraging can also close positions, including profitable ones. **Usage:** Always present it as estimated and host-computed. Higher leverage generally leaves less room for an adverse move. Unknown estimates render as —. **Do:** Label the readout “Est. liquidation price”. Keep the estimate visible next to leverage when the trader changes size. **Don't:** Don’t call the estimate a guaranteed stop. Don’t omit “estimated” on first display in a risk-critical surface. See also: `margin`, `leverage`, `stop`. ### Funding Term id: `funding`. Aliases: `funding rate`, `funding payment`. **Untranslated:** no — translate in approved locales. **Definition:** Periodic payments between long and short holders of a perpetual that help keep the contract aligned with its underlying market. Positive funding generally means longs pay shorts; negative funding means shorts pay longs. Funding is separate from exchange and builder fees. **Usage:** Show the rate and the next payment time. Compute from UTC; display in the user’s locale with an explicit time zone. Hyperliquid funding is hourly; the interval is protocol-owned, not a HIG constant to restyle as a calendar expiry. **Do:** Label “Funding +0.0100% · next 16:00 UTC”. Use a duration countdown (“in 12 min”) rather than a zone-less clock. **Don't:** Don’t mix funding into the builder-fee line as if it were the same charge. Don’t show a next-funding time without a time zone. See also: `perp`, `pnl`, `long`, `short`. ### Profit and loss (PnL) Term id: `pnl`. Aliases: `P&L`, `unrealized PnL`, `realized PnL`, `net PnL`. **Untranslated:** keep the listed short form in every locale. Untranslated forms: `PnL`. **Definition:** Profit and loss on a position or account. Unrealized PnL marks an open position; realized PnL is locked in after closes. Net figures may include trading fees and available funding; the host must say which components are included. **Usage:** Keep the abbreviation PnL in every locale. Pair it with a signed, locale-formatted amount. Color is not a substitute for the sign. Zero is `0`, not empty; unknown is —. **Do:** Show `PnL +$12.34` with `signDisplay: 'exceptZero'`. Name the readout “Unrealized PnL” or “Net PnL” according to host data. **Don't:** Don’t translate PnL into an unverified local acronym. Don’t announce every live PnL tick to a screen reader. See also: `position`, `notional`, `funding`. ### Long Term id: `long`. Aliases: `UP`. **Untranslated:** no — translate in approved locales. **Definition:** A position that profits if the market price rises. Opening long buys the perp; closing long sells it. Product chrome may label the action UP; the canonical term remains long. **Usage:** Map UP/DOWN product labels to long/short in copy, APIs and accessibility names. Buy/sell color means direction, never safety. Don’t leave the canonical term untranslated just because the button says UP. **Do:** Accessible name: “Long BTC”. Visible tap label may stay “UP” where the product uses it. Pair long with a buy-direction token, not a success/destructive intent. **Don't:** Don’t say “buy Bitcoin” when the action opens a BTC perp long. Don’t use destructive red for every long close. See also: `short`, `position`, `perp`. ### Short Term id: `short`. Aliases: `DOWN`. **Untranslated:** no — translate in approved locales. **Definition:** A position that profits if the market price falls. Opening short sells the perp; closing short buys it back. Product chrome may label the action DOWN; the canonical term remains short. **Usage:** Same mapping rules as long. Short is a market direction, not a destructive action. Closing a short is not “selling the asset”. **Do:** Accessible name: “Short ETH”. Visible tap label may stay “DOWN” where the product uses it. State “close short” for an exit, not “buy ETH”. **Don't:** Don’t describe a short as borrowing stock or delivering the underlying. Don’t treat DOWN as a glossary synonym that replaces short in docs or APIs. See also: `long`, `position`, `perp`. ### Stop Term id: `stop`. Aliases: `stop loss`, `stop-loss`, `SL`, `stop order`. **Untranslated:** keep the listed short form in every locale. Untranslated forms: `SL`. **Definition:** A reduce-risk order that submits a close when the market reaches a trigger the trader set. It is not a guaranteed fill at that price and it is not the protocol liquidation price. **Usage:** Label the control “Stop”. If a compact badge uses SL, keep SL untranslated and expose “Stop” to assistive technology. Host owns trigger parsing and submission. **Do:** Place stop beside take profit as optional risk controls on an open position. Confirm the trigger price as a raw string before the host submits. **Don't:** Don’t present a stop as insurance against liquidation. Don’t reuse the estimated liquidation price as a silent stop. See also: `take-profit`, `liquidation-price`, `position`. ### Take profit Term id: `take-profit`. Aliases: `TP`, `take-profit`. **Untranslated:** keep the listed short form in every locale. Untranslated forms: `TP`. **Definition:** An order that submits a close when the market reaches a favorable trigger the trader set. Like a stop, it is not a guaranteed fill at that price. **Usage:** Spell “take profit” in sentences. Compact badges may use TP; keep TP untranslated. Pair with stop; neither replaces a review step. **Do:** Label the field “Take profit”. Show both trigger and current mark so the trader can see distance. **Don't:** Don’t hide take profit behind an unlabeled TP icon. Don’t treat a take-profit fill as realized until the host confirms it. See also: `stop`, `pnl`, `position`. ### Notional Term id: `notional`. Aliases: `notional value`, `position value`. **Untranslated:** no — translate in approved locales. **Definition:** Absolute position value in quote terms, typically mark × size. Notional is the exposure, not the margin posted. At 10×, 100 USDC margin corresponds to 1,000 USDC notional. **Usage:** Use “notional” in Pro/details; “position value” is allowed in first-run copy if it still means mark × size. Never display notional as if it were withdrawable balance. **Do:** Show “Notional 1,000.00 USDC” separately from “Margin 100.00 USDC”. Keep notional host-computed; HIG only formats the number. **Don't:** Don’t label margin as notional or notional as available cash. Don’t round notional for display and feed that figure back into an order. See also: `leverage`, `margin`, `position`. ### Builder fee Term id: `builder-fee`. Aliases: `builder-fee`, `interface fee`. **Untranslated:** no — translate in approved locales. Untranslated forms: `bps`. **Definition:** A fee added by an interface that routes orders to Hyperliquid, charged on each fill in addition to the exchange fee. Live rates, Genesis discounts and rebate math belong to the host fee schedule, not this glossary. **Usage:** Spell “builder fee”. Quote basis points as `bps` (untranslated) with a percent companion (`5 bps (0.05%)`) when showing a rate. Fees apply on open and close fills; funding is separate. **Do:** Itemize “Builder fee” and “Exchange fee” on the review step. Point at the host fee schedule for the current rate rather than hard-coding it in a component. **Don't:** Don’t call the builder fee a spread, a spread markup, or Hyperliquid’s exchange fee. Don’t bake a candidate rate into HIG component copy. See also: `funding`, `pnl`. ### USDC Term id: `usdc`. Aliases: `USD Coin`. **Untranslated:** yes — keep English. Untranslated forms: `USDC`. **Definition:** A stablecoin designed to track the US dollar. Bloxwap uses USDC for trading collateral and settlement. It is not the same ticker as USD for display purposes. **Usage:** Always keep `USDC` untranslated. Format the amount with the locale’s decimal/grouping rules, then place the ticker `USDC` — never a `$` or `currency: 'USD'` style. See the localization guide. **Do:** Display `1,234.50 USDC`. Use the Usd readout only for actual USD, not for USDC collateral. **Don't:** Don’t localize USDC to “USD”, “US$”, or a local currency name. Don’t pass `currency: 'USDC'` to Intl; it is not a reliable ISO 4217 code. See also: `margin`, `notional`, `pnl`. ## Numbers, currency, dates and built-in strings · HIG 0.1.0 Source: docs/foundations/localization.md # Numbers, currency, dates and built-in strings · HIG 0.1.0 Issue #47. Vocabulary is locked in the [trading glossary](glossary.md); agents consume [`docs/foundations/glossary.json`](glossary.json). Machine-readable format profiles: [`docs/foundations/localization.json`](localization.json). Host owns locale selection, parsing and time zone; HIG formats display only. `HIGProvider` locale is a BCP 47 tag (`en-US` default). Direction (`ltr`/`rtl`) is independent. `de-DE` and `ar-EG` are Intl fixtures already covered by tests, not approved translation locales. ## Shared helpers Use the display helpers from `@bloxwap/hig/contracts` and the data readouts. Do not hand-roll separators, currency symbols, compact suffixes, the leverage `×` or zone labels. Every helper returns `—` for `null`, `undefined` and non-finite input. ```ts import {formatNumber, formatLeverage, formatTicker, formatZonedTime} from '@bloxwap/hig/contracts'; formatNumber(value, locale, options); // Intl.NumberFormat formatLeverage(10, locale); // '10×' (decimal style forced) formatTicker(12.34, 'USDC', locale, {signDisplay: 'exceptZero'}); // '+12.34 USDC' (no-break space) formatZonedTime(instant, locale, 'UTC'); // '4:00 PM UTC' / '16:00 UTC' ``` | Need | API | | --- | --- | | Decimal/grouped numbers | `formatNumber` / `NumberReadout` + host `minimumFractionDigits`/`maximumFractionDigits` | | USD | `Usd` (`style:'currency', currency:'USD'`) | | USDC | `formatTicker(amount, 'USDC', locale, options)` — decimal amount plus untranslated ticker, never `$` | | Leverage | `formatLeverage(value, locale)` — `10×`, never percent | | Percent points | `Percent` (5 means 5%; do not pass a ratio) | | Signed PnL | `signDisplay:'exceptZero'` on `Usd`/`formatNumber`/`formatTicker` | | Compact totals | `notation:'compact', compactDisplay:'short'` | | Funding / expiry clocks | `formatZonedTime(instant, locale, timeZone)` — `Intl.DateTimeFormat` with the zone name always shown | | Other dates | `Intl.DateTimeFormat` | | Funding countdown | `Intl.RelativeTimeFormat` on a duration, not a zone-less clock | Hermes in RN 0.81.5 and 0.83.10 provides `Intl`. Don’t polyfill, don’t concatenate `'$'+value`, and don’t parse a formatted string back into an order. ## Numbers Grouping and decimal marks come from the locale: `1,234.5` in `en-US`, `1.234,5` in `de-DE`, Eastern Arabic numerals in `ar-EG`. Precision is host-supplied (tick size, `szDecimals`). Generic USD balances may use 2 fraction digits; prices, sizes, funding rates and fees do not share one precision table. **Do** keep `0` for zero and — for unknown/non-finite; mark stale. **Don’t** use a rounded display value as transaction input, strip unfamiliar separators, or replace Eastern Arabic digits with Latin ones unless a dual readout is explicit. Readout `prefix` and `suffix` are host-localized display strings, not parsers or substitutes for Intl currency, sign, percent, or compact formatting. Animated readouts generate digit glyphs from the active numbering system and always settle on the same canonical Intl reading as the static component. ## Currency, signed PnL and compact notation USD uses ISO `currency:'USD'`. Intl places the symbol (`$1,234.50` vs `1.234,50 $`). USDC is a ticker, not a reliable ISO 4217 code: use `formatTicker(amount, 'USDC', locale)`, which formats the number without `style:'currency'` and appends `USDC` after a no-break space. Never show USDC as `$` or `USD`. Signed PnL always includes a sign except at zero (`+$12.34`, `-$12.34`). Color is not a substitute for the sign. Don’t use accounting parentheses. Compact notation is locale-owned. `1.2K` / `3.4M` is the `en-US` short form; `de-DE` yields `3,4 Mio.`, `ja-JP` may use `万`. Never hard-code `K`/`M`. Compact is for overview totals, not order tickets or review amounts. USDC PnL follows the same sign rule: `+12.34 USDC`. Tabular nums on every readout. ## Dates, times and funding `en-US` clocks are 12-hour (`4:00 PM`). Many other locales resolve `h23`. Use `Intl.DateTimeFormat` (`hourCycle` from the locale unless the host overrides). Store instants in UTC. Funding and expiries: compute the next payment from UTC (Hyperliquid funding is hourly; the interval is protocol-owned). Display the instant with an explicit zone (`16:00 UTC` or a named device zone) using `formatZonedTime`. Pair a duration countdown (“in 12 min”) with that labeled clock. Perps have no expiry — don’t render an expiry field on a perp. **Don’t** show a funding time without a zone or treat a local wall clock as UTC. ## Leverage and percentages Leverage is a multiplier with the untranslated `×` suffix (`10×`, via `formatLeverage`), never `1000%` or percent styling. Percentages use the glossary’s rate terms (funding, builder fee as `5 bps (0.05%)`) and `Percent` for percent-point values (`+2.5%`, `-2.5%`). Keep `bps` untranslated. ## RTL and text expansion Logical inline spacing, wrapping labels, no fixed-height clipping of translated text, Dynamic Type stays on. Plan for ~30–40% expansion (German/Finnish) and 200% text. Directional arrows mirror in RTL; brand, check marks and financial direction (`+`/`−`, buy/sell) do not. Preserve bidi isolates that Intl inserts around signed currency in Arabic. Native per-place motion uses the intact static reading whenever separate reels cannot preserve that bidi layout; it never reverses formatted parts. Mixed-script punctuation needs the required font scripts; a Latin-only fixture does not certify them. Long translations, Arabic/Hebrew RTL and large text remain part of the [platform checklist](platform.md#accessibility-and-localization-10). Physical-device observations are still pending. ### Native direction from the host locale Issue #86. On native, `HIGProvider` follows `I18nManager.isRTL` when `direction` is unset. React Native 0.82 and later, including 0.83.10, do not report an Android per-app RTL locale through `I18nManager` (Android 13+ `cmd locale set-app-locales ar-EG`, or a per-app language picker). `I18nManager.isRTL` stays `false`, so an Arabic or Hebrew user would get a left-to-right HIG tree. RN 0.81.5 and iOS are not affected. Hosts that lay out right to left pass the locale's direction: ```tsx import {getLocales} from 'expo-localization'; import {HIGProvider, localeDirection} from '@bloxwap/hig/native/provider'; const direction = localeDirection(getLocales()[0]); // 'rtl' for ar-EG; undefined keeps the I18nManager default {children}; ``` `localeDirection` (also exported from `@bloxwap/hig/contracts`) returns expo-localization's `textDirection` when present. Otherwise it reads the tag's script, then its language: `ar_EG`, `he-IL` and `az-Arab-IR` give `rtl`; `en-US`, `ar-Latn` and `en-u-nu-arab` give `ltr`. It accepts BCP 47 tags, Android/Java identifiers and objects with `textDirection`/`languageTag`, and returns `undefined` for no language (`''`, `und`). Without expo-localization, React Native's `I18nManager.getConstants().localeIdentifier` is the app's configuration locale on Android (from the RN 0.83 source; the gallery verifies the expo-localization path). - **Don't** pass a locale-derived direction in an app that disallows RTL (`I18nManager.allowRTL(false)`, or `android:supportsRtl="false"`). Leave `direction` unset so HIG follows the OS layout. HIG never derives direction on its own for this reason. - **Don't** expect `I18nManager.isRTL` to change on RN 0.83 Android. Native views HIG does not draw keep the host's direction. Upstream: [react/react-native#53417](https://github.com/react/react-native/pull/53417) (0.82.0) made `I18nUtil.isDevicePreferredLanguageRTL` read `Locale.getAvailableLocales()[0]` instead of the app's locale. The bug is reported in [#54938](https://github.com/react/react-native/issues/54938) and fixed by [#57635](https://github.com/react/react-native/pull/57635), first released in 0.88.0. 0.83.10, 0.84.1, 0.85.3, 0.86.3 and 0.87.1 still have the bug. The native gallery passes `localeDirection(getLocales()[0])`, and its `os-arabic` Android emulator step asserts `rtl` on RN 0.81.5 and 0.83.10 (`evidence/native-sim/issue-86-88-89/`). A system-wide Arabic locale and physical devices are not yet tested (#55). ## Built-in component strings Issue #62. HIG components render a small amount of their own copy: the Close button on overlays, the Toast Dismiss button, the Button busy suffix, loading and empty states, and the recipe labels. None of it is hard-coded in `packages/hig/react/src` or `packages/hig/native/src`. Every string comes from a typed `HIGMessages` catalog (`@bloxwap/hig/contracts/messages`). Hosts translate it through `HIGProvider messages`. A few components also take a per-instance prop (`closeLabel`, `cancelLabel`, `dismissLabel`, `busyLabel`). HIG ships the English (`en-US`) defaults only (`defaultMessages`). It does not ship or guess translations. The host's message catalog owns French, Korean and other copy, under the same rule as the [glossary](glossary.md): an unapproved locale is never filled by guessing. ```tsx import {HIGProvider} from '@bloxwap/hig/react/provider'; // or '@bloxwap/hig/native/provider' import type {HIGMessages} from '@bloxwap/hig/contracts/messages'; const fr: Partial = { close: t('hig.close'), // from the host catalog, e.g. 'Fermer' dismiss: t('hig.dismiss'), dismissNotice: (message, dismiss) => `${dismiss} : ${message}`, busy: t('hig.busy'), }; … // one instance // no visible suffix; busy state stays exposed ``` Resolution order, per key: the component prop, then the nearest `HIGProvider messages`, then any enclosing `HIGProvider messages`, then English. A nested provider for density or direction keeps the host's translations. `undefined` keys and values of the wrong type (a string where a function is expected, or the reverse) are ignored, so a partial catalog never blanks a label. `useMessages()` returns the complete resolved catalog; `useHIG().messages` holds only the host overrides. Each English default is also a named export of `@bloxwap/hig/contracts/messages`, and a component falls back per key (`messages.close ?? en.close`), so importing one component bundles only the copy it renders. Rules: - **Visible text and accessible names come from the same key.** The Close, Dismiss and busy text is the button content, so it is also the accessible name. The Toast dismiss name is `dismissNotice(message, dismiss)`. A translation must keep the `dismiss` argument in it, so voice-control users can say the label they see. `stale` and `required` are appended to both the visible text and the accessible name. - **A busy Button keeps its name (#94).** When a native Button has text children and is busy, HIG sets its accessible name, because React Native Android otherwise announces only the busy state. On iOS the name is the visible `{text} · {busy}` (the text alone when `busy` is `''`). On Android the name is `{text}`: Android appends its own localized busy state description, so the suffix would be spoken twice where the host's `busy` matches it (Korean `처리 중`). An explicit `accessibilityLabel` always wins. - **Slider actions are labeled (#87).** The native Slider's increment and decrement accessibility actions carry `increase` and `decrease`. React Native 0.83 Android otherwise lists them with an empty label. - **Close means dismissing an overlay.** `close` closes a dialog or sheet. It is not the glossary sense of closing a position (an exit order). Translate it with the platform's dismiss word, never with the trading term for closing a position. - **Functions receive formatted arguments.** `pageOf` and `progressValue` receive numbers already formatted for the provider locale (Eastern Arabic digits in `ar-EG`). `resultCount` receives the raw count and the locale, so it can pick a plural form with `Intl.PluralRules`. Translations may reorder arguments. Component code never concatenates words. - **RTL is a direction, not a translation.** Messages render inside the provider's `dir` (web) or `writingDirection` (native). The Unicode bidi algorithm places the ` · ` busy separator and parentheses. Do not add direction marks to messages. - **Trading words stay with the host.** Default messages contain no glossary term. The TradeEntry recipe copy (`reviewAssetOrder`, `confirmOrder`, …) is generic order-review copy. The host supplies the asset ticker, which stays untranslated. | Key | English default | Where | Prop override | | --- | --- | --- | --- | | `close` | Close | Dialog, Sheet, Popover (web), Menu and GestureSheet (native) close button | `closeLabel` | | `cancel` | Cancel | AlertDialog (web) decline button | `cancelLabel` | | `dismiss` | Dismiss | Toast dismiss button text | `dismissLabel` | | `dismissNotice` `(message, dismiss)` | {dismiss} {message} | Toast dismiss accessible name; must contain the `dismiss` argument | — | | `busy` | Working | Button busy suffix, shown as ` · {busy}`; `''` hides it | `busyLabel` | | `notifications` | Notifications | Toast region name (web) | — | | `loading` | Loading | Spinner label (web appends `…`) | `label` | | `loadingContent` | Loading content | Skeleton label | `label` | | `loadingItems` | Loading items | List loading row | — | | `loadingChart` | Loading chart… | PriceChart fallback (web, `@bloxwap/hig/chart`) | — | | `noItems` | No items | List empty text | `empty` | | `noRows` | No rows | Table empty text (web) | — | | `retry` | Retry | List retry button | — | | `loadMore` | Load more | List load-more button | — | | `stale` | (stale) | NumberReadout stale marker (visible and accessible) | — | | `required` | (required) | Field required marker (visible and accessible) | — | | `breadcrumb` | Breadcrumb | Breadcrumb landmark name (web) | — | | `pagination` | Pagination | Pagination landmark name (web) | — | | `previous` | Previous | Pagination previous button | — | | `next` | Next | Pagination next button | — | | `pageOf` `(page, total)` | Page {page} of {total} | Pagination position; numbers arrive locale-formatted | — | | `search` | Search | Search field label | — | | `searching` | Searching… | Search in-progress status | — | | `noResults` | No results | Search empty status | — | | `resultCount` `(count, locale)` | {count} result / {count} results | Search result count (native); use `Intl.PluralRules` for other plural forms | — | | `back` | Back | BackButton (native) | — | | `menuDescription` | Available actions | Menu sheet description (native) | — | | `choose` | Choose | Select trigger before a choice (native) | — | | `chooseOne` | Choose one option. | Select sheet description (native) | — | | `inProgress` | In progress | Indeterminate Progress value text (native) | — | | `progressValue` `(value, max)` | {value} of {max} | Determinate Progress value text (native); numbers arrive locale-formatted | — | | `copied` | Copied | InstallCommand success status | — | | `copyFailed` | Copy failed. Select the command and copy manually. | InstallCommand failure status | — | | `copyInstallCommand` | Copy install command | InstallCommand button | — | | `reviewAssetOrder` `(asset)` | Review {asset} order | TradeEntry heading | — | | `amountInAsset` `(asset)` | Amount ({asset}) | TradeEntry amount label | — | | `availableAmount` `(available)` | Available: {available} | TradeEntry amount description | — | | `reviewOrder` | Review order | TradeEntry review button | — | | `confirmOrder` | Confirm order | TradeEntry dialog title and confirm button | — | | `confirmOrderDescription` `(amount, asset)` | Amount: {amount} {asset}. Verify the host-provided terms before submission. | TradeEntry dialog description | — | | `orderUnavailable` | Order unavailable | TradeEntry error title (web) | — | | `noActivity` | No activity yet | ActivityList empty text | — | | `statusOnline` | online | StatusIndicator `online` | — | | `statusOffline` | offline | StatusIndicator `offline` | — | | `statusStale` | stale | StatusIndicator `stale` | — | | `increase` | Increase | Slider increment accessibility action label (native; listed by Android accessibility services) | — | | `decrease` | Decrease | Slider decrement accessibility action label (native) | — | `npm run lint` runs `scripts/check-built-in-strings.mjs`, and so does `tests/built-in-strings.test.ts`. The check fails when a component under `packages/{react,native}/src` adds a user-visible or accessible literal. That covers JSX text, a visible or accessibility attribute (`aria-label`, `accessibilityLabel`, `title`, `label`, `description`, …), a label-like default prop and a `set…()` status string. To add copy, add a key to `HIGMessages`, read it with `useMessages()`, and add a row to the table above. The test checks that every key is in this table with its English default. ## Validation See `tests/tokens.test.ts`, `tests/data.dom.test.tsx`, `tests/glossary.test.ts`, `tests/built-in-strings.test.ts`, `tests/messages.dom.test.tsx`, `tests/native/messages.test.cjs`, `docs/foundations/localization.json` and the data chapter. Cite HIG 0.1.0 with this chapter anchor and a commit permalink. The native Button's busy suffix starts with the separator (`· Working`); the row supplies its visual gap. On iOS its accessible name remains `Action · Working`. Android speaks the action name and its localized busy state once. ## Native runtime differences · HIG 0.1.0 Source: docs/foundations/native-runtime-differences.md # Native runtime differences · HIG 0.1.0 Follow-ups #86–#89 and #92–#94 distinguish implementation checks from physical-device certification in #55. The supported packed consumers remain React Native 0.81.5 / Expo 54 and React Native 0.83.10 / Expo 55. Historical simulator/emulator captures remain in `evidence/native-sim/`; their results are not overwritten by software tests. ## Android locale direction RN 0.83's `I18nManager.isRTL` can report false for an Arabic app locale. The regression is [upstream #54938](https://github.com/react/react-native/issues/54938), introduced by [#53417](https://github.com/react/react-native/pull/53417) and fixed by [#57635](https://github.com/react/react-native/pull/57635), with older-Android handling in [#58723](https://github.com/react/react-native/pull/58723). HIG defaults direction to `I18nManager.isRTL`. Hosts that support RTL pass `direction={localeDirection(getLocales()[0])}`, using `localeDirection` from `@bloxwap/hig/native/provider` and their localization adapter (for example Expo Localization). This handles an Arabic per-app locale even when RN's OS diagnostic still reports false. Hosts that disallow RTL leave direction unset. Formatting locale remains independent, with `en-US` as the default. The Android `os-arabic` harness asserts the explicit host direction. Historical local fallback measurements predate the merged public API. ## Slider action labels RN 0.83 exposes an empty accessibility action label when the host omits it. HIG supplies `messages.increase` and `messages.decrease` for both actions, with English Increase/Decrease defaults and host translation overrides. Disabled adjustments remain rejected. Automated labels and state checks do not establish what a person hears through TalkBack; physical reference flows remain in #55. ## iOS Dynamic Type audit The historical RN 0.81.5 / Expo 54 run recorded 68 TextInput Dynamic Type heuristic findings. The RN 0.83.10 / Expo 55 run recorded none, while the large-text captures and text-field counts matched. The note in `evidence/native-sim/native-sim.json` applies to RN 0.81.5 only. Source comparison of the release tags establishes that `RCTUITextField.mm` is byte-identical and the Fabric text-input view has no new UIFontMetrics/scaling behavior. The font utility changed to honor default font handlers in [React Native #53755](https://github.com/react/react-native/pull/53755). That refactor has not been isolated as the cause of the audit change. We therefore do not claim a UIFontMetrics fix or a physical accessibility pass. Root-cause attribution for #88 and device VoiceOver/largest-size checks remain open. ## Audio completion diagnostics Expo Audio 1.1's Android playback status derives `didJustFinish` from the player's ended state. Both the playback-state listener and status polling can report that state. The SDK 54 captures show two completion callbacks after one recorded `play:tick`; SDK 55 shows one. A completion callback is not an extra play. Only the gallery recorder listens to `playbackStatusUpdate`. The HIG adapter dispatches preloaded players without waiting for or replaying completion events. `tests/native-verification.test.ts` injects two completion callbacks and asserts exactly one backend `play()` call while preserving both raw diagnostic events. No runtime completion de-duplication is needed unless the adapter later depends on completion. Dispatch timing and audible latency remain distinct; speaker, silent-mode and interruption observations remain in #55. ## Native text and color follow-ups Native Button uses HIG Text for both its label and localized busy suffix, preserving font registration, script spacing and scale caps. Its busy accessible name includes the visible label, with explicit host labels taking precedence. Mixed ReactNode status titles concatenate visible text without inserting extra spaces. `HIGProvider colorSpace='display-p3'` selects the canonical P3 table on iOS. Android and web resolve to sRGB. Native controls and host-drawn views read the resolved table through `useColors()`; nested providers inherit the selected space. These are channel-selection and semantic tests, not comparisons of physical displays. ## Foundation and platform rules · HIG 0.1.0 Source: docs/foundations/platform.md # Foundation and platform rules · HIG 0.1.0 ## Color (#6) Dark is the only audited mode. Source values retain the commit-pinned monorepo authored OKLCH and brand channels; see [source reconciliation](source-reconciliation.md). 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 from HIG's own tables, selected once per app by `HIGProvider colorSpace` (#93); see the native colour space table below. 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. ### Primary brand colour Set `primaryColor` on `HIGProvider` to apply another brand's accent. For example, Tap Trade TV can use a red primary colour: ```tsx import {HIGProvider} from '@bloxwap/hig/react/provider'; // Native: import {HIGProvider} from '@bloxwap/hig/native/provider'; ``` The red above is an example; supply the brand's exact hex. Both providers accept opaque sRGB `#RGB` or `#RRGGBB` values. Other formats, including transparent colours and CSS variables, throw a descriptive error. Omit the prop at the root to retain the existing Bloxwap green and wide-gamut defaults. Nested providers inherit the primary colour unless they supply another one. Separate roots can use different brands; changing or removing the prop updates mounted components. The override sets `action` and `focus`, plus `onAction` to black or white for the higher text contrast (at least 4.5:1 on the primary fill). Buttons, links, selection controls, progress indicators, action tones, and chart crosshair labels follow the accent. Success/error/warning/info and buy/sell roles keep their existing meanings and colours. Destructive buttons keep their dark label on the negative fill. The approved Bloxwap mark retains its original colours; another brand supplies its own logo. Web applies scoped CSS variables on the provider wrapper and on HIG portalled overlays, including Dialog, GestureSheet, Menu, Popover, and Tooltip. Import the normal token and component CSS. `useThemeStyle()` from the web provider entry returns the primary variables for a host-owned portal's root. Both platforms export `useColors()` for host-drawn content (web returns sRGB values for canvas). Native converts the sRGB input when an iOS provider selects `display-p3`; Android and react-native-web keep sRGB input channels. A nested provider can change colour space while inheriting the brand accent. The [default palette contrast report](../../evidence/contrast.json) covers the default theme. Custom accents also need review against the surfaces where they appear, particularly link text and focus rings; automatic label selection only guarantees contrast on the primary fill. Choose an accent with enough contrast for the dark surfaces your product uses. Tokens-only hosts can call `resolvePrimaryColor` from `@bloxwap/hig/tokens` and apply the resulting `action`, `focus`, and `onAction` roles to a scoped element. Keep these three roles together so primary labels remain readable. ### Native colour space React Native reads a hex colour's channels in the app's default colour space. That is sRGB unless the iOS host overrides `defaultColorSpace()` in its React Native factory delegate to return Display P3. If it does, `#ff479c` is read as P3 channels and paints a more saturated pink than the token. Set `colorSpace` on the root `HIGProvider` to match the host. Every HIG native component (Text, Surface, Separator, Button, Link, Field, Switch, Slider, Alert, Badge, Spinner, Skeleton, Dialog and GestureSheet) then paints from that table through `useColors()`, with the primary brand override applied if present. The Bloxwap mark uses `useBrandColors()` to retain its approved colours in the same colour space. Hosts choose a table and can configure the primary accent through `primaryColor`. Nested providers inherit both settings, and a test fails if a component imports `nativeSrgb` or paints a raw hex value. | `HIGProvider colorSpace` | iOS | Android | react-native-web | | --- | --- | --- | --- | | `srgb` (default) | `nativeSrgb` | `nativeSrgb` | `nativeSrgb` | | `display-p3` | `nativeDisplayP3` (= `nativeColors.iosDisplayP3`) | `nativeSrgb` (HIG publishes no Android P3 table) | `nativeSrgb` | | Role | `nativeSrgb` | `iosDisplayP3` | | --- | --- | --- | | action / positive / buy / focus | `#00ff3f` | `#00ff3d` | | warning | `#fffb38` | `#fffa2e` | | negative / sell | `#ff479c` | `#ff3d9e` | | info | `#35b5ff` | `#2eb8ff` | Neutral roles (background, surface, nested, text, muted, faint, border, onAction) have the same hex in both tables. Use `useColors()` to read the resolved table for host-drawn views next to HIG components. Evidence is native Jest (#93) and iOS Simulator captures of the gallery built as a Display P3 host (`evidence/native-sim/issue-93/`). Those captures predate the pink/neon-yellow tone adoption: in them, `display-p3` paints warning and negative as exactly the then-current sRGB tokens (`#f5b300`, `#ff5c5c`), while `srgb` on the same host paints them oversaturated (`#ffaf00`, `#ff4a54`); the mechanism is unchanged. Action and info are wider than sRGB, so the screenshot clips them. Physical display comparison is tracked in #55. ## Typography (#7) Pin @bloxwap/font 0.1.4. Vite: import `@bloxwap/font/css`, which loads unicode-range subsets (latin, cyrillic, greek, other) so a page fetches only the scripts it renders; 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 (`--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)`. 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 segmented selection uses an interruptible core Animated transform with the same timing and easing; physical-device animation feel remains unverified. Motion preference is independent from sound/haptics. ## Accessibility and localization (#10) Baseline: [WCAG 2.2 AA, Recommendation 12 December 2024](https://www.w3.org/TR/2024/REC-WCAG22-20241212/). 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](glossary.md) (`docs/foundations/glossary.json`) before any locale translation. Number, currency, date, leverage, percent and RTL formatting is in the [localization guide](localization.md). 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. ## Brand and icons (#11) 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. 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. ## Native text in multiple scripts Register each required companion face with the host font loader, then pass its registration name through `HIGProvider fontFamiliesByScript`, for example `{latin: {400: 'BloxwapSansRegular'}, ko: {400: 'BloxwapSansKRRegular', 600: 'BloxwapSansKRSemiBold'}, zh: {400: 'BloxwapSansSCRegular'}}`. The provider's `fontFamilies` remains the fallback. Nested providers inherit the script map when it is omitted. HIG Text detects Hangul or Han in each run even when the provider locale is English. Shared CJK punctuation follows the run's `accessibilityLanguage`, then the provider locale; `script` can select a face explicitly. If a script map has fewer weights, Text chooses its nearest registered weight. Explicit `style.fontFamily` wins. Hosts load these optional faces themselves; declaring a map does not load a font. Native RadioGroup options accept `lang` for the spoken label and `description` for its translated second line. ## Principles and content · HIG 0.1.0 Source: docs/foundations/principles.md # Principles and content · HIG 0.1.0 Issues #2, #3. Rules are candidate guidance anchored to SDK source and the public marketing surface; the pinned monorepo audit and live-origin snapshots are in `evidence/issue-1-18/`. Do not represent example trading actions as live product behavior. ## Clear action hierarchy Offer one primary action per decision. In an amount-entry recipe, **do** label the next step “Review order” and show the asset and amount before host submission; **don't** say “Go” or execute a trade when a field receives focus. Secondary actions navigate or cancel. Destructive intent means irreversible removal/cancellation; buy/sell colors mean market direction and never imply safety. Bind pending state to the host request, block duplicate activation, retain a progress label. Disabled means unavailable; busy means an action is running. Give the reason nearby. ## Touch before density Use comfortable 48-unit controls for mobile trade forms. **Do** stack amount, available balance, validation and primary action at narrow widths; **don't** squeeze a desktop order panel into a phone. Pro can use compact 36-unit controls with keyboard focus and separated targets. SDK code windows scroll horizontally; surrounding prose reflows. Marketing CTA and SDK install controls retain explicit labels, as observed in `bloxwap.com` and `font/apps/docs/app/global.css`. ## State tells the truth Resting, hover, focus, pressed, selected, busy, disabled, empty, error, success, offline and stale are distinct states. **Do** retain the last value with a timestamp and “Data unavailable — retry”; **don't** display unknown as zero, silently imply a completed order, or announce every price tick. Errors name the failed action and recovery. Confirmations repeat host-provided consequences and offer cancel before commit. Essential information remains visible and accessible with feedback muted. ## Content and precision Use sentence case and concrete verbs (“Copy install command”). **Do** display currency, precision and units supplied by the host; **don't** use a rounded display number as transaction input. Keep raw decimal strings in business logic. A percent component expects percent points, not an implicit ratio. Use localized display with explicit parsing policy; do not strip unfamiliar separators and submit a changed amount. Code uses Mono, body Sans, Pixel only as a decorative brand accent. ## Composition and progressive disclosure Show common actions first, advanced options on deliberate disclosure. **Do** give a native sheet a visible close action and let Android back dismiss; **don't** rely on hover tooltips on touch. Navigation remains host-owned: HIG does not choose routes, wallets, order types, or trading outcomes. Use status/callout for persistent critical errors; toast only for nonessential acknowledgements. ## Concrete source and pattern references The preceding rules map to Bloxwap-owned examples, without treating host demo outcomes as submitted trades: - Clear action hierarchy: [pinned Bloxwap source](https://github.com/bloxwap/monorepo/blob/d47cef4c40363184bdfda8d8c263ead108ef9ff6/packages/ui-native/src/components/button.tsx), [HIG pattern](../components/actions-surfaces.md). Source primary/secondary/destructive variants, busy activation policy and semibold labels. - Touch before density: [pinned Bloxwap source](https://github.com/bloxwap/monorepo/blob/d47cef4c40363184bdfda8d8c263ead108ef9ff6/packages/tokens/src/spacing.ts), [HIG pattern](../components/recipes.md). Source spacing aliases and 16-unit root; marketing CTA before-capture is before-bloxwap.com.png. - State tells the truth: [pinned Bloxwap source](https://github.com/bloxwap/monorepo/blob/d47cef4c40363184bdfda8d8c263ead108ef9ff6/packages/ui-native/src/components/toast.tsx), [HIG pattern](../components/status.md). Source notification severity and persistent readable outcomes; HIG preserves undismissed bursts. - Content and precision: [pinned Bloxwap source](https://github.com/bloxwap/monorepo/blob/d47cef4c40363184bdfda8d8c263ead108ef9ff6/packages/ui-native/src/components/number-format.ts), [HIG pattern](../components/fields.md). Source display formatting stays separate from raw amount input and host arithmetic. - Composition and progressive disclosure: [pinned Bloxwap source](https://github.com/bloxwap/monorepo/blob/d47cef4c40363184bdfda8d8c263ead108ef9ff6/packages/ui-native/src/components/sheet.tsx), [HIG pattern](../components/overlays.md). Source back/dismissal and safe-area behavior; basic HIG Modal alternative has an explicit close action. ## Pinned source reconciliation · HIG 0.1.0 Source: docs/foundations/source-reconciliation.md # Pinned source reconciliation · HIG 0.1.0 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. 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. Joe Blau accepted the recorded source and applicability decisions on 2026-10-27; see evidence/adoption/owner-approval-2026-10-27.md. 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. ## Tokens and platform units `packages/hig/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. Native views default to sRGB and use `nativeColors.android`/`ios`. A host that explicitly configures RN iOS default color space as Display-P3 sets `HIGProvider colorSpace='display-p3'`, so HIG components paint `nativeColors.iosDisplayP3` (also exported alone as `nativeDisplayP3`). It reads `useColors()` for its own views and converts third-party sRGB assets (#93). 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. 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. ## Native font generation and registration Install `fonttools[woff]==4.61.1` and `brotli==1.2.1`, 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.4` variable WOFF2 files, verifying all four input hashes before writing. Those four sources are byte-identical to 0.1.1 and 0.1.2, so the derived faces, their SHA-256 and their 0.1.1 unique IDs are unchanged. 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. ## Observation boundaries 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. 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. ## Organization adoption and governance (#36) Source: docs/governance/adoption.md # Organization adoption and governance (#36) The active-repository matrix is `planning/adoption.json`, refreshed on 2026-10-27 using the repository inventory in `planning/org-inventory-2026-10-15.json`: the 15 repositories visible to `gh repo list bloxwap`, none archived. The 2026-10-13 inventory is kept for history. Each row records: - applicability - owner (Joe Blau, `joeblau`, accepts the recorded rows and exceptions in the [2026-10-27 decision record](../../evidence/adoption/owner-approval-2026-10-27.md)) - HIG version - merged and open PRs - validation - status - remaining work - dated exceptions, each with scope, reason, owner, an expiry no more than 180 days ahead, and a tracking issue `tests/docs.test.ts` fails when a visible repository has no row or when a row is missing these fields. Product surfaces (bloxwap.app, bloxwap.pro, bloxwap.com, the React Native app) and the five SDK docs sites have their own rows under `surfaces`. ## Status · 2026-10-27 All 15 inventoried repositories have applicability decisions and named owner acceptance. All ten consumer repositories have merged their registry migrations; the exact versions, PRs and validation remain in the matrix. | Group | Repositories | State | | --- | --- | --- | | Canonical | hig | Coordinated 0.1.2 release, then the single `@bloxwap/hig` 0.2.0 package; RN 0.81, 0.83 and 0.88 packed-consumer profiles | | SDK docs migrated | hyperliquid, chart, font, sfx, haptic | Published 0.1.0 registry packages; candidate archives and the previously tracked peer/font/role workarounds retired | | Product | monorepo | Registry migration #4737 and the HIG 0.1.2 / RN 0.88 upgrade #4826 merged. Owner decisions accepted in HIG #57. Real wallet acceptance and rollback retirement remain #110 | | Docs site migrated | agent-cli | Registry migration #68 merged; scoped drift checks retained | | Product, partly migrated | taptradetv | Registry migration #253 merged and deployed. Sponsors and host retain their explicitly recorded applicability scopes; this does not claim every host primitive migrated | | Public pages migrated | tapsiren, bloxwap.github.io | Registry migrations #430 and #36 merged. Pages builds the tested artifact; generated runtime assets are no longer committed | | No HIG runtime | icons, dimension-adapters, .github, .github-private | Applicability accepted; no runtime dependency required | Owner acceptance records product decisions and the six-locale glossary exception through 2027-01-15. Physical-device claims retain the separate dated owner attestation. Neither supplies new wallet, native-speaker or hardware observations. Historical audit snapshots retain their original dates; current status is in `planning/adoption.json`. ## Governance - **Ownership, review, admission, removal, platform support and product exceptions:** see the contribution chapter (`docs/governance/contributing.md`). - **Release, semver and deprecation:** see the release chapter (`docs/governance/release.md`). - **Setting up and upgrading a repository** without copying component folders, palettes, fonts or feedback engines: see `docs/governance/new-repository.md`. - **Anti-drift:** `scripts/drift.mjs` enforces it here and in adopting repositories with `--root` and a `hig-drift.json`. ## Completion review - Every issue needs an owning implementation and validation link, or an explicit linked follow-up. See `planning/issue-coverage.md` for the per-issue state. - Catalog entries need docs, code, platform and test coverage. - Source and bundle deltas must be measured in the owning repository. - A matrix row is "migrated" only when its owning PR is merged and its validation is linked. Open PRs are recorded as in progress, never as migrated. - #57 still has no owner approvals; named-owner and native-speaker decisions are never inferred from checks or merges. - Physical hardware results are never inferred from simulator, emulator or browser evidence. ## Contribution, ownership and admission · HIG 0.1.0 Source: docs/governance/contributing.md # Contribution, ownership and admission · HIG 0.1.0 Issue #36. This chapter sets out how HIG stays the one maintained design system for Bloxwap. It covers who owns what, how changes are reviewed, how components enter and leave the catalog, which platforms are supported, and how a product can justify an exception. Release, semver and deprecation rules are in the release chapter. Setting up and upgrading a repository is covered in the new-repository chapter. ## Canonical ownership | Area | Canonical owner | Others may | | --- | --- | --- | | Semantic tokens (colour roles, space, radius, type, motion, density), native colour spaces, contrast pairs | `bloxwap/hig` → `@bloxwap/hig/tokens` | Map roles to host variables with `var()`. They may not redefine literal values. | | Component contracts, DOM and React Native renderers, recipes, the semantic feedback contract | `bloxwap/hig` → `@bloxwap/hig/contracts`, `-react`, `-native`, `-feedback` | Compose components and override layout in context. They may not fork a primitive's paint or geometry. | | Font sources, families and web loaders | `bloxwap/font` → `@bloxwap/font` | HIG generates pinned native faces from Font releases. Consumers install fonts and never commit binaries. | | Sound recipes and the Web Audio engine | `bloxwap/sfx` | HIG owns the cue mapping and preferences, and exports approved WAV cues for native. | | Haptic presets and the web engine | `bloxwap/haptic` | HIG owns the RN adapter and the capability matrix. | | Chart engine | `bloxwap/chart` | HIG owns chart theming and the accessible non-canvas alternative. | | Product business logic, routing and data | The owning product repository | HIG components never import wallet, network or trading code. | | Brand marks | `bloxwap/hig` brand component (pinned from the Font SVG) | Use the component. Exact mark fills need a dated exception. | The organization adoption matrix (`planning/adoption.json`) records the owner, applicability, HIG version, merged PRs, status and dated exceptions for each repository and product surface. ## Contribution and review criteria Every HIG pull request must: 1. **Link its issue** (`Part of #N` or `Closes #N`) and state its intent, the states it affects and its platform mapping. 2. **Change source and generated artifacts together**, then run `npm ci && npm run check` (build, lint, generation drift, typecheck, web and native tests). Also run the smokes relevant to the change: `smoke:ssr`, `smoke:packed`, `smoke:packed-native`, `smoke:packed-boundaries`, `smoke:packed-examples`, `examples:check` and `quality`. 3. **Cover behaviour on both renderers**, or record the platform decision and its supported alternative. Include accessible names, roles and states, keyboard and focus, native back, large text and RTL, and muted operation (sound and haptics off) without losing essential feedback. 4. **Label evidence precisely**: automated, browser, iOS Simulator, Android emulator or physical device. A mock, simulator or emulator never proves physical sound, haptics, VoiceOver or display output. 5. **List the checks that were not run**, upstream gaps, rollout and rollback. Breaking changes follow the release chapter. 6. **Pass the scoped anti-drift check** (`node scripts/drift.mjs`, run by `npm run quality` and tests). New exceptions follow the exception policy below. A maintainer reviews against the same list. A PR that only updates screenshots or baselines must say why the visual change is intended. ## Component admission A component or recipe enters the catalog only when all of the following hold: - It is used repeatedly in at least two Bloxwap surfaces, or one product surface plus an SDK docs site, and the inventory (`planning/catalog.json`) cites that usage. - It has a minimal API that composes existing primitives, with no wallet, network or trading dependencies and no unrelated imports. The per-entry export and boundary smokes enforce this. - It has a React DOM renderer and either a React Native renderer or a documented native alternative, and the platform matrix is updated. - It has a complete state matrix: focus, pressed, busy/disabled, error, empty, long text, large text, reduced motion, and feedback disabled where relevant. It also has catalog examples that typecheck in the packed consumers. - It is documented: a usage chapter, an API reference, accessibility notes and a migration note for each fork it replaces. ## Component removal 1. Inventory every consumer with org-wide code search and the adoption matrix. 2. Deprecate the component in a minor release. Name the replacement, provide a codemod or checklist, and keep the export working for at least one minor release and 30 days. 3. Open removal PRs in each consuming repository, or record dated exceptions for them. 4. Remove the component in the next major release and record the change in the changelog and the matrix. ## Platform support policy | Platform | Supported (tested in CI or recorded evidence) | Notes | | --- | --- | --- | | React DOM | React 19.2; Vite, SSR and Next 16.4 (webpack) packed consumers; Chromium, Firefox and WebKit | Other bundler or Next versions need checks in the host | | React Native | RN 0.81.5 with Expo 54 and React 19.1.0; RN 0.83.10 with Expo 55 and React 19.2.0 (#68); Metro; packed Android/iOS exports in CI for both; iOS Simulator (iOS 26.5) and Android emulator (API 35) gallery runs for both | RN 0.82 and 0.84+ are not supported. On RN 0.83 Android a per-app RTL locale does not reach `I18nManager.isRTL`, so RTL hosts pass `direction={localeDirection(getLocales()[0])}` ([localization](../foundations/localization.md#native-direction-from-the-host-locale), #86). Physical devices are pending (#55) | | Tokens only | Plain CSS, Tailwind through host config, typed tokens | No React, RN or audio peers | | Feedback | Web: `@bloxwap/sfx` and `@bloxwap/haptic`. Native: Expo audio and haptics adapters | Essential outcomes must never depend on sound or haptics | Support only changes in a minor or major release. Each change updates `docs/architecture/platform-matrix.md`, the packed smokes and this table together. ## Justified product exceptions A product may differ from HIG only through a recorded exception. Silent forks are not allowed. - **Scope:** an exact path or folder prefix, and a single anti-drift rule (`obsolete-import`, `copied-palette`, `copied-token-definition`, `copied-primitive-css` or `copied-font-binary`). - **Required fields:** `owner` (an accountable role or person), `reason` (the user or product need, or the rollback dependency) and `expires` (`YYYY-MM-DD`, at most 180 days ahead). - **Where:** HIG's own exceptions go in `planning/drift-exceptions.json`. An adopting repository keeps its exceptions in its own `hig-drift.json`. The adoption matrix links to them. - **Review:** the check fails on expired exceptions and on exceptions without an owner or reason, and warns about unused ones. Renewing an exception needs a new reason and owner approval. Repeated exceptions for the same need are a signal to propose a HIG change through the admission criteria. Intentional density differences, such as compact Pro versus the comfortable touch app, are HIG density modes. They are not exceptions. ## Scoped anti-drift checks `scripts/drift.mjs` has no dependencies and checks only files tracked in git under the configured `paths`. Paths listed under `canonical` are exempt from the copy rules. | Rule | Fails when | | --- | --- | | `obsolete-import` | Code imports a retired fork (`@workspace/tokens`, `@workspace/ui`, `@workspace/ui-native`, plus repository-specific `obsoleteImports`) or a copied `vendor/hig/` or `styles/hig/` folder | | `copied-palette` | Code hard-codes the Bloxwap brand hexes owned by `@bloxwap/hig/tokens` | | `copied-token-definition` | A stylesheet assigns a literal value to a `--hig-*` token. Remapping one role to another with `var()` is allowed | | `copied-primitive-css` | A bare `.hig-*` selector sets the primitive's paint or geometry (background, border, radius or min-height). Contextual layout overrides are allowed | | `copied-font-binary` | A `.ttf`, `.otf` or `.woff*` file is committed outside a canonical font owner | Generation drift (stale tokens, brand or glossary outputs) is checked separately by `scripts/check-drift.mjs` in `npm run lint`. ## New repository setup and upgrades · HIG 0.1.2 Source: docs/governance/new-repository.md # New repository setup and upgrades · HIG 0.1.2 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. ## 1. Pick applicability Add or update the repository's row in `planning/adoption.json` (a PR to bloxwap/hig) with one of these applicability values: | Applicability | Install | Never add | | --- | --- | --- | | Product runtime UI (web) | `@bloxwap/hig/tokens`, `-contracts`, `-react`; `@bloxwap/font`; `-chart` with `@bloxwap/chart` only for `PriceChart` (#96) | Copied component folders or token CSS | | 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`; `-og-fonts` as a build-time dev dependency for OG cards only | A local `.btn` layer, a palette copy, `vendor/hig`, `-native-fonts` for OG cards | | 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 | ## 2. Install versioned packages Install the required packages at the same exact published version. For a web consumer: ```sh npm install --save-exact @bloxwap/hig-contracts@0.1.2 @bloxwap/hig-tokens@0.1.2 @bloxwap/hig-react@0.1.2 ``` Native consumers install `@bloxwap/hig-native` and `@bloxwap/hig-native-fonts` instead of the DOM renderer; add feedback only where needed and satisfy the supported profile's peers. Keep all HIG dependencies and overrides aligned. Commit the lockfile and record the release, source commit and exact registry integrity values in the owning evidence manifest. Building candidates requires `npm run generate:fonts` with Python packages `fonttools[woff]==4.61.1` and `brotli==1.2.1`. Registry consumers use the packaged fonts without generating them. For unpublished changes only, follow the [release dry-run procedure](release.md) and consume its exact packed candidate archives. Never copy `packages/*/src`, `dist` folders, token CSS, palettes, fonts or feedback engines. Replace candidate `file:` dependencies with registry versions and remove obsolete archives when adopting a published release. Retain provenance manifests and historical validation records. Then follow the installation chapter for your platform: import `@bloxwap/hig/tokens/css` and `@bloxwap/hig/react/css` once (add `@bloxwap/hig/react/css/base` first only when HIG owns the whole page), use per-component imports, load fonts through `@bloxwap/font`, and on native register fonts from the HIG font manifest. ## 3. Add the scoped drift check to CI Create `hig-drift.json` in the repository root: ```json { "paths": ["apps/docs/app", "apps/docs/components", "apps/docs/lib", "apps/docs/styles"], "canonical": [], "obsoleteImports": [], "exceptions": [] } ``` Run the check from a pinned HIG checkout. The script uses only Node built-ins and needs no install: ```yaml - uses: actions/checkout@v4 with: {repository: bloxwap/hig, ref: , path: .hig} - run: node .hig/scripts/drift.mjs --root . --config hig-drift.json ``` - **Scope:** only paths you have migrated. Widen `paths` as more surfaces move. - **`canonical`:** use it only for assets the repository owns. For example, `bloxwap/font` owns its web font files. - **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. First run against current `main` heads on 2026-10-15, scanning `apps/docs` (automated, local): - hyperliquid and haptic pass. - sfx flags the brand-mark fill in `lib/og-card.tsx`. That needs an exception like HIG's own brand marks. - chart flagged `styles/hig-roles.generated.css` (#66). [chart#56](https://github.com/bloxwap/chart/pull/56) deleted it. - font flags its own font binaries, which belong under `canonical`. - agent-cli flagged `vendor/hig/tokens.css`, its import, and committed TTFs (#58). [agent-cli#47](https://github.com/bloxwap/agent-cli/pull/47) removed them, and its drift check now passes with no exceptions. ## 4. Validate and record - 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. - Measure CSS, JS and font bytes and the dependency count before and after. - Link the merged PR, the HIG version and any dated exceptions in the matrix row. - Label evidence as automated, browser, iOS Simulator, Android emulator or physical device. ## Upgrading HIG 1. Read the release changelog (or the dry-run changelog for an unpublished candidate), together with the semver section of the release chapter. 2. Upgrade tokens, contracts, renderer and feedback together. Never mix release versions or candidate commits. Verify the registry integrity values or candidate archive hashes. 3. Update the drift checkout `ref` to the same commit and run the check. Remove exceptions whose need has gone. 4. Re-run the validation in step 4 and update the matrix row's `higVersion` and PR links. 5. Roll back by reverting the upgrade PR, which restores the previous exact versions and lockfile. Product pilots also keep their host feature flag (for example `NEXT_PUBLIC_HIG_PILOT=0`). ## Quality, budgets and evidence · HIG 0.1.0 Source: docs/governance/quality.md # Quality, budgets and evidence · HIG 0.1.0 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. 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. Budgets are regression limits from the initial candidate measurements, not claims of production savings: | Metric | Measured initial | Limit | Reason | | --- | --- | --- | --- | | Individual DOM Button gzip (React external) | 706 B | 850 B | ~20% API/behavior fix headroom, overlay/audio/chart excluded | | Individual AmountInput gzip | 325 B | 400 B | Small labelled primitive, ~23% headroom | | Native Switch gzip (React/RN external) | 1,383 B; 1,870 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 and the Android thumb token. ~18% 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 | 124,111 B (with resizable drawer and per-place number motion) | 125,000 B | Tight headroom against the current Next CSS output; the gesture example remains lazy-loaded and Vaul injects its own runtime CSS rather than adding a second bundled stylesheet | | 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`. The committed copy, the release report and the browser acceptance files were copied from a Linux CI run (`evidence/ci-refresh.json`, #98), not regenerated locally. GitHub Actions now runs on self-hosted macOS ARM64 Mac minis; see [CI runners](../ci-runners.md). Failed quality and browser jobs upload fresh copies for review; green runs skip the upload. 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. 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. 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. ## Versioned visual regressions 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//` (`linux` or `darwin`), and Slider compares two keyboard-focused captures (comfortable and compact density). Geometry changes fail; pixel changes above 2% fail after a 24-channel-value antialias tolerance on every OS. `tests/visual.test.ts` deliberately changes geometry and pixels to prove the gate fails. Baselines record current candidate rendering on that OS, not approved product parity or native-device fidelity. Do not reuse another OS image or raise the thresholds to hide a runner difference. 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 for that platform. ## Release candidates, semver and rollback · HIG 0.1.2 Source: docs/governance/release.md # Release candidates, semver and rollback · HIG 0.1.2 Issue #32. The local release automation is **dry-run only**. It never invokes npm publication, GitHub release or git tag creation. Unsupported flags fail before artifact creation. The dry run does not request or use npm credentials; registry publication is confined to the maintainer-controlled workflow described below. ```sh npm ci npm run check npm run examples:check npm run quality npm run generate:fonts # Python with fonttools[woff]==4.61.1 and brotli==1.2.1 npm run smoke:packed npm run smoke:packed-og npm run smoke:packed-native npm run smoke:packed-next npm run smoke:packed-css npm run smoke:packed-boundaries npm run smoke:packed-chart-workspace npm run release:dry-run -- --channel candidate npm run release:dry-run -- --channel stable-simulation ``` Candidate mode stages `-candidate.sha` package metadata and matching internal dependency versions. Stable-simulation stages the coordinated source version. Runtime contract/docs source version and source commit are recorded explicitly; neither source files nor git refs are version-mutated by the dry run. Each dry run rebuilds packages and docs before staging so ignored output cannot retain an older API. Both produce local tarballs, checked export/types/CSS/assets/files/dependency manifests, hashes, changelog, copied docs artifact and provenance report. `npm pack --ignore-scripts` packs already validated outputs, never publishes. Physical claims remain pending in every report. The release dry run, `npm run quality`, `smoke:packed-native` and `smoke:packed-og` first check that the generated TTF faces exist and match their manifests (#98). If any face is missing or stale, they stop before building anything. The message names `npm run generate:fonts` and its Python pins and lists the affected files, instead of failing with a raw `ENOENT`. `packedSizes` in the report records the `@bloxwap/hig` tarball size and, for the `tokens` (no fonts), `native-fonts` and `og-fonts` entries, their file count, unpacked bytes, gzip-9 bytes and font-file count. The dry run fails if a font binary sits outside `native-fonts/fonts/` or `og-fonts/fonts/`, or if the `og-fonts` entry reaches 1,000,000 B compressed (#99). `smoke:packed-og` builds the documented `app/og/route.tsx` handler with Turbopack (the Next 16 default) and with `--webpack`. It serves each build with `next start`, and also as moved standalone output. Every face's SHA-256 has to match, so a loader that bundles only one face fails here. The chart adapter ships as the `@bloxwap/hig/chart` entry (#96). The dry run fails unless `@bloxwap/hig` declares `@bloxwap/chart` as a required peer and never as an optional peer or dependency. The result is recorded in `chartAdapter` in the report. `npm run quality` applies the same rule to the source manifest and checks that no built `dist` file outside `chart/` imports `@bloxwap/chart`. `smoke:packed-boundaries` checks that the packed manifest has no `./react/chart` entry, that a React button bundle pulls no `@bloxwap/chart` code, and that `PriceChart` renders on the server with its required peer installed. `smoke:packed-chart-workspace` builds a host shaped like bloxwap/chart: an npm workspace whose `packages/chart` provides `@bloxwap/chart` 0.4.0, plus `apps/docs`, which depends on it and on the packed `@bloxwap/hig` through a `file:` spec. The smoke deletes `package-lock.json` and `node_modules` and installs with `legacy-peer-deps=false` and `strict-peer-deps=true`. It then asserts four things. The lockfile keeps `packages/chart` and the `node_modules/@bloxwap/chart` link. `npm ls` is clean. Node and esbuild resolve `@bloxwap/chart` (including the `@bloxwap/hig/chart` entry's lazy `@bloxwap/chart/react`) from the app into the workspace. The host's `HIGProvider` messages reach `PriceChart`. It repeats the checks after `npm ci` and requires a second `npm install` to leave the lockfile byte-identical. Every scenario runs twice: once with the ambient npm (in CI, the npm bundled with Node 24.22.1) and once with the npm pinned in `packageManager` (11.6.1, the version chart reported), which is installed into a scratch prefix. A control run repacks `@bloxwap/hig` with the old optional peer and records in `evidence/packed-chart-workspace-smoke.json` whether each npm still drops the workspace. CI checks out PR source, installs locked peers, executes web/native/SSR/Metro/example/visual/accessibility/budget gates and uploads review artifacts. A provenance record identifies source commit, package/contract/docs versions and hashes; it is not an npm registry provenance attestation or a release. Artifact inspection checks exports, licenses, optional peers and only approved assets. No example app implementation is included in runtime packages. Publication gates are tracked in `planning/release-blockers.json`. The owner explicitly authorized the first coordinated npm publication, including the customizable primary-colour provider, on 2026-10-23; see [the release authorization](../../evidence/release/owner-authorization-2026-10-23.md) and [PR #141](https://github.com/bloxwap/hig/pull/141). Releases 0.1.0 and 0.1.1 used an organization token. Release 0.1.2 uses npm trusted publishing; subsequent workflow executions have no token fallback. Released SFX/Haptic 0.2.0 backends satisfy their setup gates. Physical-device and native-font acceptance use the existing dated owner attestation from PR #131. Source-parity and named adoption decisions are accepted in #57. Owning-product rollout and six non-English glossary entries retain scoped exceptions through 2027-01-15; real wallet acceptance and rollback retirement remain open in #110. These exceptions do not turn unobserved product or physical checks into passes. The immutable-tag, package-set, tarball-hash and CI checks still apply. Semver: - **Major:** breaking changes to token meaning or appearance, typography scale, layout, accessibility or action behaviour, or semantic feedback mapping. - **Minor:** compatible additions. - **Patch:** verified fixes. Deprecation names the alternative, a migration codemod or checklist, and the owning consumers from `planning/adoption.json`. A deprecated export keeps working for at least one minor release and 30 days, and is removed only in the next major release after consumer PRs are merged or have dated exceptions (see the contribution chapter). Keep code/examples/API/citations at the same source commit; separate package prerelease identifiers are explicit in artifact metadata. Rollback for an owning pilot: keep the previous package artifacts and lockfile, disable the host feature flag, restore prior renderer/backend imports and rebuild. Keep saved feedback/business data backward-compatible; never erase prior choices. Remove old assets only after production and physical-device proof. A future published rollback would require separate release authorization and package provenance; this task performs none. ## Maintainer-controlled registry publication (#56) The local dry-run command remains unable to publish, create tags, or create releases. In the separate `publish.yml` workflow, the GitHub-hosted `ubuntu-24.04` `publish` job is the explicit exception to the self-hosted runner policy; `prerequisites` runs on the minis. Publishing a GitHub release with a `v*` tag triggers it; maintainers can also dispatch it manually with the existing tag as both the workflow ref and input, for example `gh workflow run publish.yml --ref v0.1.2 -f tag=v0.1.2`. It validates the tag and checks out `refs/tags/` so every gate runs against that tag commit. Before publication, `prerequisites` fails unless the `npm-release` environment has required reviewers and `quality.yml` succeeded for this exact tag commit. Manual dispatch rejects a branch ref because the OIDC provenance source must identify the release tag, then resolves that tag SHA. The `publish` job needs both the unchanged self-hosted `security` release scan and these prerequisites, waits for `npm-release` environment approval, and verifies that the checked-out tag still matches the approved SHA. Checkout does not persist credentials. The workflow re-runs `npm run check`, `npm run quality`, the stable-simulation release dry run, and `node scripts/verify-release.mjs` in that order. The tag, source commit and coordinated version must match; the release must contain exactly the `@bloxwap/hig` package and its tarball hash must match. Every gate in `planning/release-blockers.json` must be `cleared`, `satisfied`, or have a valid `approved-exception` with a named approver, approval link and unexpired review date. Expired or undocumented exceptions and uncommitted source changes fail. It never creates the tag or GitHub release for the maintainer. An npm owner must configure a **Trusted Publisher for `@bloxwap/hig`** with repository **bloxwap/hig**, workflow **publish.yml**, and environment **npm-release**. The GitHub `npm-release` environment is configured with `joeblau` as required reviewer. The eight 0.1.x `@bloxwap/hig-*` packages had their trusted publishers configured and read back using authenticated owner access on 2026-10-27, and 0.1.2 was published through them. `@bloxwap/hig` 0.2.0 was published manually by the owner, so its registry metadata names no `trustedPublisher`; the first workflow release of `@bloxwap/hig` must confirm its npm setting. Changing this workflow alone does not configure npm. The token-published [0.1.0](https://github.com/bloxwap/hig/releases/tag/v0.1.0) and [0.1.1](https://github.com/bloxwap/hig/releases/tag/v0.1.1) releases predate this migration; their exact source commits are the recorded legacy exceptions below. Publication uses `contents: read`, `id-token: write`, OIDC trusted publishing and `npm publish --provenance`. The transitional `NPM_TOKEN` fallback is removed. Release 0.1.2's registry metadata identifies GitHub Actions and each package's configured `trustedPublisher` ID; signed provenance is verified separately. Provenance alone does not identify the authentication method. The shared organization secret is not deleted because other repositories use it; this workflow no longer references it. The ephemeral hosted runner uses Node 24.22.1 and explicitly installs npm@11.6.1 (the repository's `packageManager` pin), satisfying the [npm trusted-publishing CLI minimum of 11.5.1](https://docs.npmjs.com/trusted-publishers/). Python 3.12 comes from pinned `actions/setup-python`; `ci-generate-fonts.sh` installs pinned fonttools and brotli into a temporary venv. Playwright installs Chromium with `--with-deps` on Ubuntu. The shared minis retain their existing setup policy. Only after those gates does the workflow publish the exact verified `@bloxwap/hig` tarball. An already published version is logged and skipped so an interrupted run can resume. Successful publishes are recorded in `artifacts/published-packages.txt`; the workflow passes this file as `HIG_PROVENANCE_PACKAGES_FILE` to `smoke:registry`. Registry smoke always checks every tarball's SHA-256 against the verified artifact, checks the SLSA statement's exact source commit, workflow, tag, package and SHA-512 digest, and runs `npm audit signatures` to verify the registry signatures and provenance bundles. Provenance remains required when an interrupted publication resumes and skips existing packages, and when the smoke runs without a publish manifest. The only legacy exemptions are token-published 0.1.0 at source `bdf8d07014fea5e69ea51a061b82707661a9d701` and 0.1.1 at source `6f4dd060e0985be644d255154aa7a0277cdde27f`; a supplied missing or invalid manifest fails closed. The manifest is uploaded with the verification artifacts. Registry smoke then runs independent web/SSR, Next hydration/font, and all three supported native Metro/Expo profiles against registry versions. Newly published exact versions can briefly return npm metadata `E404` while registry visibility catches up. That lookup retries at most thirty-one times, ten seconds apart (five minutes of bounded visibility retries), with a 15-second limit per npm command. Other npm errors, malformed metadata, unavailable tarballs, hash mismatches and consumer failures still fail normally. An exhausted metadata-visibility retry requires investigation; once the version is visible, rerun the unchanged release tag. A published package defect requires a corrected new version. Automation never unpublishes or overwrites a registry version, and a skipped version with different bytes still fails the registry hash check. ## Installation and citation · HIG 0.1.1 Source: docs/installation.md # Installation and citation · HIG 0.1.1 HIG ships as one npm package, `@bloxwap/hig`. Every surface is a subpath export of it: tokens, contracts, React and React Native components, the chart adapter, feedback, native fonts and OG fonts. Local dry-run tarballs and CI artifacts remain available for testing unreleased changes. Registry releases verify the exact package hash and independent web, Next and native consumers. ```sh npm install @bloxwap/hig@0.2.0 ``` | Entry | Use | | --- | --- | | `@bloxwap/hig/tokens`, `@bloxwap/hig/tokens/css` | Semantic tokens and the role CSS | | `@bloxwap/hig/contracts` | Platform-neutral types, formatting, feedback, announcements and messages | | `@bloxwap/hig/react/`, `@bloxwap/hig/react/css` | Web components and their scoped CSS | | `@bloxwap/hig/native/` | React Native components | | `@bloxwap/hig/chart` | `PriceChart` and `chartTheme` for `@bloxwap/chart` | | `@bloxwap/hig/feedback`, `@bloxwap/hig/feedback/` | Optional sound and haptic feedback | | `@bloxwap/hig/native-fonts` | Static native TTFs and their manifest | | `@bloxwap/hig/og-fonts`, `@bloxwap/hig/og-fonts/node` | Satori / `next/og` card faces | Every renderer, device and overlay peer is optional, so install only the peers of the entries you import. The one required peer is `@bloxwap/chart` `^0.3.0` (see [Optional chart, sound and haptics](#optional-chart-sound-and-haptics)). ### Moving from the 0.1 packages HIG 0.1 published eight packages: `@bloxwap/hig-tokens`, `-contracts`, `-react`, `-native`, `-chart`, `-feedback`, `-native-fonts` and `-og-fonts`. From 0.2.0 they are deprecated on npm in favour of `@bloxwap/hig`. Replace them with the one dependency and change the hyphen after `hig` to a slash in every import. Entry names and exports are unchanged: | 0.1 import | 0.2 import | | --- | --- | | `@bloxwap/hig-tokens/css` | `@bloxwap/hig/tokens/css` | | `@bloxwap/hig-react/button` | `@bloxwap/hig/react/button` | | `@bloxwap/hig-native/provider` | `@bloxwap/hig/native/provider` | | `@bloxwap/hig-chart` | `@bloxwap/hig/chart` | | `@bloxwap/hig-og-fonts/node` | `@bloxwap/hig/og-fonts/node` | In Next, list `'@bloxwap/hig'` in `transpilePackages` or `serverExternalPackages` instead of the old package names. ## 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 `@bloxwap/hig` plus compatible React/DOM peers. 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.4 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. ### Component CSS and the opt-in base `@bloxwap/hig/react/css` styles HIG components only. Every rule is scoped to a HIG class, for example `.hig-button`, `.hig-table` cells, `.hig-range` or `.hig-surface[data-severity]`. The box-sizing and focus ring rules use `:where()` on HIG classes, so they keep the specificity of a bare element rule and host CSS can still override them. Loading the file has no effect on plain `table`, `th`, `td`, `input[type=range]`, `[data-severity]` or the focus outline of a non-HIG element. Hosts that load HIG on some routes only can rely on this: Next.js keeps a route's stylesheet after a client navigation, and the scoped file then has no side effects on the other routes (#95). The page-wide element defaults are a separate, opt-in entry, `@bloxwap/hig/react/css/base`: | Rule | Effect | | --- | --- | | `* { box-sizing: border-box }` | Border-box sizing on every element | | `:focus-visible` | The 2px `--hig-focus` ring with a 3px offset on every focusable element | | `table`, `th`, `td` | Collapsed borders, full width, 12px cells with a `--hig-border` rule | | `input[type='range']` | `--hig-action` accent, 48px minimum height, full width | Import the base only when HIG owns the whole page, and import it before the component CSS: ```ts import '@bloxwap/hig/tokens/css'; import '@bloxwap/hig/react/css/base'; // optional: page-wide element defaults import '@bloxwap/hig/react/css'; ``` These are the rules that `@bloxwap/hig/react/css` applied to the whole page before #95, so a host that adds the base sees no change. The HIG docs site and the web and Next fixtures import it. HIG components look the same with or without the base. `npm run smoke:packed-react-css` checks this. It installs the packed tokens, contracts and React tarballs into an empty consumer and renders Table, Slider, Button and Alert to static markup. Then, in Chromium, Firefox and WebKit, it compares three script-free pages: tokens only, tokens plus `@bloxwap/hig/react/css`, and tokens plus the base and the component CSS. The plain table, cells, range, box-sizing, `[data-severity]` and keyboard focus outline must compute the same with the component CSS as with tokens only. The base page must change them, which proves the check can see a leak. The components must compute the same with and without the base. `npm run smoke:packed-boundaries` also checks that both entries resolve through package exports in a bundler and that the component CSS has no selector outside HIG classes. ### Radix overlay peers and Fumadocs The four Radix packages are optional peers with caret ranges: `@radix-ui/react-dialog ^1.1.15`, `@radix-ui/react-popover ^1.1.15`, `@radix-ui/react-dropdown-menu ^2.1.16` and `@radix-ui/react-tooltip ^1.2.8`. Install them only for `@bloxwap/hig/react/overlay`, `@bloxwap/hig/react/recipes` or `@bloxwap/hig/react/recipes/trade-entry` (dialog only). Hosts that already use Radix, such as Fumadocs, share their copy: `npm run smoke:packed-fumadocs` installs the packed React tarball next to fumadocs-ui 16.17.3 with strict peers and `npm ci`, asserts every `@radix-ui/*` package appears once, and renders the overlays on the resolved current releases. The workspace fixtures keep testing the lower bounds. Do not set `legacy-peer-deps` for HIG. The separate optional `@bloxwap/hig/react/gesture-sheet` entry requires `vaul ^1.1.2`; Vaul brings its Radix Dialog dependency. Ordinary overlay and action entries do not load Vaul. Retain the host's existing input repositioning, handle-only, background scaling and viewport layout flags; see [gesture sheet composition](components/overlays.md#optional-web-gesture-backend). `npm run smoke:packed-boundaries` verifies both absence in core consumers and resolution of the gesture entry after the explicit backend install. ## Brand accent Pass `primaryColor` to the web or native `HIGProvider` to theme primary actions for another brand, for example `` for a red Tap Trade TV theme. Use the brand's opaque sRGB hex (`#RGB` or `#RRGGBB`). HIG selects readable primary button text and inherits the accent through nested providers. Omit it to keep Bloxwap green. See [primary brand colour](foundations/platform.md#primary-brand-colour) for portal styling, native colour spaces, and contrast guidance. ## 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 (or in the route layouts that use HIG; the component CSS is scoped, see above) 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. ## Open Graph card fonts Satori, `next/og` `ImageResponse` and `@vercel/og` read TTF/OTF but not WOFF2, and `@bloxwap/font` ships only WOFF2. Docs and marketing sites that render OG cards install `@bloxwap/hig/og-fonts` as a build-time dev dependency. Do not use `@bloxwap/hig/native-fonts` for this (#99). The package holds five static Latin faces: Bloxwap Sans 400, 500, 700 and 900, and Bloxwap Mono 500. These are the faces the chart, sfx, agent-cli and tapsiren cards use. It packs to about 0.84 MB; the native fonts package is 4.4 MB. `npm run generate:fonts` copies the five faces byte for byte from `@bloxwap/hig/native-fonts`, so each SHA-256 in its manifest equals the native manifest entry. The release dry run records the packed size in `packedSizes.ogFonts` and fails at 1,000,000 B. Node runtime (route handlers with `export const runtime = 'nodejs'`, or static OG export scripts): ```tsx import {ImageResponse} from 'next/og'; import {loadOgFonts} from '@bloxwap/hig/og-fonts/node'; const fonts = await loadOgFonts(); // {name, data, weight, style}[]; throws if any TTF's SHA-256 differs from the manifest export function GET() { return new ImageResponse(
Bloxwap
, {width: 1200, height: 630, fonts}); } ``` This works in a Next 16 route handler with the default Turbopack build and with `next build --webpack`. Turbopack copies the five faces into `.next/server`, so `output: 'standalone'` works even after you move it. webpack reads the faces from `node_modules/@bloxwap/hig/og-fonts/fonts` at the absolute path it had at build time. If a webpack standalone build runs from another directory, add `serverExternalPackages: ['@bloxwap/hig']` to `next.config`. Earlier 0.1.0 candidate builds made each face's path from a template URL, and under Turbopack every face read the same file. The route then failed with `BloxwapSans.ttf: SHA-256 f969fd1e… does not match the manifest`. Pass a subset if you need fewer faces: `loadOgFonts(ogFonts.filter(f => f.weight >= 700))`, with `ogFonts` imported from `@bloxwap/hig/og-fonts`. Edge and Workers runtimes cannot read `node_modules` at request time. Copy `@bloxwap/hig/og-fonts/fonts/` into a gitignored build folder at build time. Before you write each file, check it against `fontManifest.outputs[asset]` (from the package root, or as JSON at `@bloxwap/hig/og-fonts/manifest`), as tapsiren's `copy-og-fonts.mjs` does. Plain Node ESM scripts outside Next import `next/og.js`, because `next` has no exports map. `npm run smoke:packed-og` installs the packed tarball next to `next@16.4.0` in a clean consumer. It hashes every exported TTF against both manifests and renders 1200×630 PNG cards through `ImageResponse`: each Sans weight must render differently, and none may match the default font. It also requires `readOgFont` to reject a tampered face. It then builds an `app/og/route.tsx` handler with the code above and checks it four ways: Turbopack and `--webpack`, each under `next start`; Turbopack `output: 'standalone'`; and webpack standalone with `serverExternalPackages`. Each standalone build is moved to another directory with the consumer's `node_modules` hidden. Every face must come back with its own SHA-256, and every card must match the plain Node render byte for byte. Results go to `evidence/packed-og-smoke.json`. ## Expo / React Native Run `npm run smoke:packed-native` after packed artifacts are built, or `npm run start -w hig-native-fixture` for the device gallery. Matrix: RN 0.81.5 with Expo SDK 54 and React 19.1.0, and RN 0.83.10 with Expo SDK 55 and React 19.2.0 (#68), Metro as locked. `npm run smoke:packed-native -- --profile rn-0.83` installs the same fixture with the SDK 55 versions from `fixtures/native/profiles.json`; CI exports both consumers for Android and iOS. `@bloxwap/hig/native` declares `react-native` `^0.81.5 || ^0.83.10`, so RN 0.82 and 0.84+ are outside the tested range. Native consumers install the tokens/contracts/native packages; optional feedback installs its package 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. The optional `gesture-sheet` entry uses Gorhom Bottom Sheet, Gesture Handler, Safe Area Context, Reanimated and Worklets, with `GestureHandlerRootView`, `SafeAreaProvider` and `BottomSheetModalProvider` supplied by the host. The optional `animated-number` entry uses Reanimated and Worklets for per-place motion; ordinary `data` readouts remain engine-free. Install only the peers for the entries used, at versions compatible with the host's Expo/RN SDK. Install Expo native peers with `npx expo install` (for example, on SDK 54, `expo-asset ~12.0.13` and `expo-constants ~18.0.14` alongside `expo-font`/`expo-audio`; on SDK 55, `expo-asset ~55.0.20` and `expo-constants ~55.0.17`). 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. On RN 0.83 Android, a per-app RTL locale (for example Arabic set in the system's app language settings) does not set `I18nManager.isRTL`, so pass `direction` explicitly there (#86). `@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. 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). `prepare-app.sh rn-0.83` builds the RN 0.83 / Expo 55 gallery, and `compare-runs.mjs` compares it with an RN 0.81.5 run of the same candidate. Results and residual physical-hardware limits are in `evidence/native-sim/` (RN 0.83: `evidence/native-sim/rn-0.83/`). 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 packages, 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 package 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); } ``` `npm run smoke:packed-css` proves this path. It installs only the packed `@bloxwap/hig` tarball into an empty consumer, links `@bloxwap/hig/tokens/css` from a script-free HTML page whose own stylesheet uses nothing but the roles, and checks the computed radius, type, motion and density values in Chromium, Firefox and WebKit, including 0ms durations under `prefers-reduced-motion: reduce` and `[data-hig-motion='reduced']` and the compact values under `[data-hig-density='compact']`. It fails if the page requests anything other than the HTML, its stylesheet and the token CSS, so the manifest is never read. 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 Chart is DOM-only and lazy. The adapter is the `@bloxwap/hig/chart` entry (#96): install `@bloxwap/chart` `^0.3.0` and supply a textual summary. `@bloxwap/chart` is the only required peer of `@bloxwap/hig`, and no other entry imports it. It is never optional, so a host with a workspace `@bloxwap/chart` (like bloxwap/chart) can regenerate its lockfile from scratch with strict peers and keep the workspace link. See the [charts chapter](components/chart.md). 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. ## Stable citations and upgrades Cite “Bloxwap HIG 0.1.1, 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. The GitHub release links the immutable source tag and the registry validation workflow. The native fixture pins React **19.1.0**, matching the RN 0.81.5 renderer exactly; the RN 0.83 profile pins **19.2.0**, matching the RN 0.83.10 renderer. 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 the version RN's Paper and Fabric renderers are built for (RN 0.83 no longer throws on a mismatch, so the check reads the renderer build itself); a successful Metro bundle alone does not prove runtime compatibility. The packed check also fails when a declared `@bloxwap/hig/native` or `@bloxwap/hig/feedback` peer range rejects a version the consumer installed. ## Native application pilot (#35) Source: docs/migration/native.md # Native application pilot (#35) Owner: Joe Blau (accepted in #57). Status on 2026-10-15: [monorepo#4540](https://github.com/bloxwap/monorepo/pull/4540) migrates the app's settings, entry, logout, sound card and demo number to HIG `0.1.0-candidate.shac32771dc`, with one HIG feedback controller and 15 duplicate fonts removed. It loads the faces from `@bloxwap/hig/native-fonts` (#64) and passes translated `messages` (#62). It also drops the local Switch, Alert and Text workarounds (#67), and passes `direction` to `HIGProvider` on Android (#86). It was verified on the iOS Simulator and Android emulator and merged as `2f636e8fe` on 2026-10-16. Physical-device certification retains the dated owner-attested exception in #55. Subsequent native rollout and registry adoption are merged in monorepo#4620/#4629/#4737. Merged [monorepo#4826](https://github.com/bloxwap/monorepo/pull/4826) adopts HIG 0.1.2 on RN 0.88 / Expo 58 and removes the temporary ref cast, closing #4776. Workarounds the app can drop at its next candidate refresh (a candidate built from `74f4f6a` or later; tracked in #115): - **Busy Button name (#94):** delete the `HigActionButton` wrapper's `higButtonAccessibilityLabel`. Button now sets the name itself: `{label} · {busy}` on iOS and `{label}` on Android, where React Native adds its own busy word, so Korean no longer reads `처리 중` twice. - **Display P3 (#93):** set `colorSpace='display-p3'` on the root `HIGProvider` and drop the local `nativeColors.iosDisplayP3` lookup for the Button fill (see [Native colour space](#native-colour-space-93)). - **Button text (#92):** the label and busy suffix now follow the provider `maxFontSizeMultiplier` and use the 600 face. Re-capture the large-text and fr/ko screens. - **Direction (#86):** `localeDirection` from `@bloxwap/hig/native/provider` can replace the app's own direction lookup. HIG is certified on React Native 0.83.10 / Expo SDK 55 (#68). No tactile, audible or VoiceOver observation is claimed. 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. Checklist: - [ ] Record actual RN/Expo/Metro/autolinking versions, source commit, old ui-native/token/font/audio wrappers, screenshots and native bundle/assets/latency baseline. - [ ] 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. - [ ] On RN 0.83, pass `HIGProvider direction={localeDirection(getLocales()[0])}` if the app supports RTL: an Android per-app Arabic or Hebrew locale does not reach `I18nManager.isRTL` ([localization](../foundations/localization.md#native-direction-from-the-host-locale), #86). - [ ] 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. - [ ] 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. - [ ] 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. - [ ] 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. - [ ] 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. - [ ] 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 (merged) already loads the faces from `@bloxwap/hig/native-fonts`. ## Native colour space (#93) Up to `0.1.0-candidate.shaac27cf7`, HIG native components always painted `nativeSrgb` hex values. On an iOS host whose React Native default colour space is Display P3, those values were read as P3 channels, so the Switch track, Alert accent, Text, Surface and the other components painted warning, negative and info more saturated than the token. HIG now selects one of its own tables through the provider: ```tsx // iOS hosts whose app delegate returns .displayP3 from defaultColorSpace() (bloxwap/monorepo apps/mobile/plugins/with-ios-display-p3.js). {app} ``` - **Display P3 hosts** set `colorSpace='display-p3'` on the root `HIGProvider`. iOS then paints `nativeColors.iosDisplayP3`, and Android and web keep `nativeSrgb`, so the prop can be set unconditionally. Nested providers inherit it. - **Host-drawn colour** next to HIG components: drop local `nativeColors.iosDisplayP3` lookups and per-platform selection and read `useColors()` from `@bloxwap/hig/native/provider`. Delete wrappers that only existed to pass a P3 colour into Button `style`. - **sRGB hosts** change nothing. The default is `srgb` and renders the same values as before. - Raw colours are still not accepted. `colorSpace` takes only `'srgb'` or `'display-p3'`, and any other value falls back to sRGB. 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. ## App, Pro and marketing pilots (#33) Source: docs/migration/products.md # App, Pro and marketing pilots (#33) Owner: Joe Blau, accepted in [HIG #57](https://github.com/bloxwap/hig/issues/57). The initial pilot and follow-ups are merged; the combined rollout is [monorepo#4619](https://github.com/bloxwap/monorepo/pull/4619), and [#4826](https://github.com/bloxwap/monorepo/pull/4826) upgrades the registry consumers to HIG 0.1.2 with the compact Pro stats geometry preserved. Marketing font cleanup is merged in [#4598](https://github.com/bloxwap/monorepo/pull/4598). `planning/adoption.json` records the current versions, rollback flags and remaining real wallet acceptance under HIG #110. The checklist below preserves the adoption procedure. | Surface | Pilot and HIG mapping | Preserve | Owner work remaining | | --- | --- | --- | --- | | bloxwap.app | Amount/field, review action, sheet/confirmation, account/feedback settings; comfortable 48 targets | Raw amount/precision, wallet/network logic, busy protection, sheet descriptions, saved preferences and physical iOS tap eligibility | Audit actual primitives/brand OKLCH, review import preview, migrate a host-flagged pilot, validate production/mobile/keyboard/feedback screenshots | | bloxwap.pro | Compact nav/order entry, market rows, numbers/table and optional chart | Keyboard order, desktop density, sort/precision, chart pan/zoom/crosshair, live updates remain quiet | Confirm legacy variants, wire host router and chart theme, compare dense before/after captures and per-component/representative bundles | | bloxwap.com | Hero/CTA/features, typography and plain CSS/tokens where appropriate | Marketing content/links, metadata, static hosting and responsive CTA access | Replace copied CSS/fonts only after consumers removed; build public site and compare representative pages/asset bytes | Checklist: - [ ] Pin HIG candidate artifacts/contract version and record source commit/baseline screenshots, CSS/font/duplicate/bundle bytes. - [ ] Run import-only codemod preview; inspect every diff. Do map brand→primary and danger→destructive after intent review; don't blindly map sell to danger or apply shadcn variants unchanged. - [ ] Preserve existing raw financial/business state; host validation still owns parsing and submission. No request on focus/open. - [ ] Wire explicit CSS/fonts/provider, comfortable app versus compact Pro, labelled fields and confirmation title/description. - [ ] Bridge legacy feedback preferences explicitly; never replace saved false choices with new enabled defaults. Physical tick must be pre-approved; async iOS outcomes can be silent. - [ ] Build the owning production app; verify keyboard, mobile, long labels, 200% text, RTL, stale/offline/error, muted channels and busy protection. - [ ] Compare before/after at the same viewport/data/toolchain; record measured source/asset/bundle delta without guessed improvements. - [ ] Remove old copies/dependencies only after reference search and build/device proof. Link owning PR and residual surface work in adoption matrix. ## Partial adoption: scoped component CSS and the opt-in base (#95) `@bloxwap/hig/react/css` now styles HIG components only, so a product can load it on the routes it migrates and leave the rest of the site alone. Next.js keeps a route's stylesheet after a client navigation. Before #95 the file also set page-wide rules (`* { box-sizing }`, a global `:focus-visible` ring, `table`/`th`/`td` and `input[type=range]` defaults), and these reached routes that never imported HIG. Those rules are now the opt-in entry `@bloxwap/hig/react/css/base` (see [installation](../installation.md#component-css-and-the-opt-in-base)). - **Whole-page adopters** (bloxwap.app, bloxwap.pro, docs sites): add `import '@bloxwap/hig/react/css/base'` before `@bloxwap/hig/react/css` to keep the current look of plain tables, ranges and focus rings. HIG components need no change. - **Partial adopters**: import only `@bloxwap/hig/react/css` in the HIG routes, and delete any host reset that undid the old global rules. taptradetv loads HIG only on `/sponsors` ([taptradetv#188](https://github.com/bloxwap/taptradetv/pull/188), #59). The registry refresh in [taptradetv#253](https://github.com/bloxwap/taptradetv/pull/253) includes #95 and removes the obsolete `th, td` reset in `apps/web/app/sponsors/sponsors-hig.css`. The `/schedule` calendar then keeps its own cell styles after a navigation from `/sponsors`, and non-HIG routes keep the host's focus ring. - Before removing a reset, verify with a client navigation from a HIG route to a non-HIG route in a production build. Compare the computed styles of tables, ranges and focus rings, not only screenshots. Rollback: keep the previous artifact lockfile and original UI behind a host feature flag. Revert the pilot switch/lockfile; keep raw settings/business data unchanged. Do not remove legacy files until the pilot is certified. The adoption matrix, not this kit, records which surfaces are migrated. ## Five SDK documentation pilots (#34) Source: docs/migration/sdk-docs.md # Five SDK documentation pilots (#34) Owner: Joe Blau (accepted in #57), with each SDK repository retaining implementation ownership. Preserve each interactive SDK demo and its host APIs; HIG replaces shared presentation only. As of 2026-10-27, all five docs sites install published HIG 0.1.0: [hyperliquid#217](https://github.com/bloxwap/hyperliquid/pull/217), [chart#78](https://github.com/bloxwap/chart/pull/78), [font#39](https://github.com/bloxwap/font/pull/39), [sfx#80](https://github.com/bloxwap/sfx/pull/80) and [haptic#49](https://github.com/bloxwap/haptic/pull/49). The earlier candidate refreshes removed the legacy peer installs, nested Radix trees, font bridges and generated role workaround. Exact source, validation, historical measurements and residual exceptions remain in the owning PRs and `planning/adoption.json`. | Repository | Shared presentation | Distinctive feature to preserve | Remaining owner validation | | --- | --- | --- | --- | | hyperliquid | Font/tokens, shell/nav/search, code/install/copy/callout/forms/states | Market playground and live SDK request handling | Production/static export, query/deep links, market demo errors/loading, copy/search/mobile keyboard | | chart | Same shell/controls plus HIG chart theme | Chart preview, drawings/controls, pan/zoom/crosshair and WASM/worker paths | Canvas/text summary, gesture responsiveness, base assets, chart-specific demos remain optional | | font | Type roles, common controls/geometry and code windows | Type tester, glyph coverage, variable axes and locale companions | Font licensing/provenance, subsets/bytes, scripts/RTL/large text and tester functionality | | sfx | Controls/presets/settings/docs states | Sound board and recipe editor/export | Trusted audio unlock, mute/volume, edited recipes, native WAV command docs and cleanup | | haptic | Common controls and explicit capability labels | Physical-target/platform demo and certification references | Physical iOS/Android/VoiceOver evidence remains upstream haptic#9; no API-call-as-tactile-proof claims | Docs sites that render Markdown tables or plain form controls relied on the page-wide rules that `@bloxwap/hig/react/css` set before #95. On a candidate that contains #95, add `@import '@bloxwap/hig/react/css/base';` before `@import '@bloxwap/hig/react/css';` to keep the same look, as the HIG docs site does. See [installation](../installation.md#component-css-and-the-opt-in-base). Per-repo checklist: - [ ] Pin source commit, HIG version and staged tarballs; capture before pages and duplicate CSS/primitive/font/audio/dependency/bundle baseline. - [ ] Inventory each existing apps/docs shell, static base path, metadata, search/index adapter, CSS/Next font loader and copy/install control. - [ ] Preview import mapping; preserve API props, router/search and SDK-specific business/demo logic. Plain HTML surfaces can use tokens/CSS alone. - [ ] Migrate one shared shell/control page behind a rollback flag; preserve Fumadocs conventions or host equivalent and existing deep-link/base URLs. - [ ] Execute that repo's production/static build and distinctive demo. Verify asset paths, keyboard/search/copy/mobile nav, loading/error/offline states, large text/RTL, and muted feedback. - [ ] Compare equivalent screenshots and measured CSS/font/runtime bytes. Remove old files only after references are gone and tests pass. - [ ] Update five separate adoption rows with owning PR URL, validated version, current gaps and rollback commit. Do not combine them into an unsupported “all migrated” claim. Rollback: restore previous package lock/imports and shell flag per repo. Retain SDK demos and deep links. New shared HIG adoption must not change published SDK API examples. ## Compact source comparison The October 14 review of the five local checkout revisions found the following presentation baseline. These are source line counts, including product-specific styles, comments and formatting; they are not an estimate of removable CSS. | Site revision | Global CSS lines | Migration difference | | --- | ---: | --- | | [Hyperliquid 53159f1](https://github.com/bloxwap/hyperliquid/tree/53159f1/apps/docs) | 1,034 | Semantic color aliases and 160 ms interaction timing. | | [Chart 8c71c71](https://github.com/bloxwap/chart/tree/8c71c71/apps/docs) | 440 | Nunito/Maple Mono remain; preserve chart previews. | | [Font b406391](https://github.com/bloxwap/font/tree/b406391/apps/docs) | 703 | Hex neutrals, specimen typography and wrapping buttons. | | [SFX 217eacb](https://github.com/bloxwap/sfx/tree/217eacb/apps/docs) | 509 | Preserve sound-board and playback behavior. | | [Haptic b546223](https://github.com/bloxwap/haptic/tree/b546223/apps/docs) | 416 | Preserve platform capability and feedback demos. | Hyperliquid, Chart and SFX have byte-identical 172-line token snapshots; Haptic's declarations match with a different provenance comment. Chart, SFX and Haptic also repeat the same button and search recipes. Use the existing HIG token/component packages for shared behavior, and keep the written guide compact with **rule → token or recipe → exception** rather than duplicating the five implementations. ## OG card fonts: switch to `@bloxwap/hig/og-fonts` (#99) Since #64, chart, sfx, agent-cli and tapsiren vendor the whole `@bloxwap/hig/native-fonts` tarball (4,420,375 B, 15 faces) as a build-time dev dependency, but their Satori/`next/og` cards read only two to four TTFs. Switch each repository at its next HIG candidate refresh: | Repository | OG card file | Faces it reads | Change | | --- | --- | --- | --- | | chart | `apps/docs/lib/og-card.tsx` | Sans 700, 900 | Replace the `node_modules/@bloxwap/hig/native-fonts/fonts` path with `loadOgFonts(ogFonts.filter(f => f.weight >= 700))` | | sfx | `apps/docs/lib/og-card.tsx` | Sans 700, 900 | Replace both `hig.resolve('@bloxwap/hig/native-fonts/fonts/…')` reads with the same `loadOgFonts` call | | agent-cli | `apps/docs/lib/og-card.tsx` | Sans 700, 900 | Same as sfx | | tapsiren | `workers/tapsiren/scripts/copy-og-fonts.mjs` | Sans 400, 500, 700; Mono 500 | Keep the build-time copy (the card runs on Workers). Resolve `@bloxwap/hig/og-fonts/manifest` and `@bloxwap/hig/og-fonts/fonts/` instead of the native package; the SHA-256 values do not change | In each repository: 1. Vendor `bloxwap-hig-og-fonts-.tgz` from `artifacts/release-candidate/` (about 0.84 MB packed; the exact size is in `packedSizes.ogFonts` of that candidate's release report). Record its SHA-256 next to the other tarballs in `vendor/bloxwap-hig/`. 2. Replace the `@bloxwap/hig/native-fonts` dev dependency with `@bloxwap/hig/og-fonts`, regenerate the lockfile, and delete the native fonts tarball unless the repository also ships a React Native app. 3. Import the fonts from `@bloxwap/hig/og-fonts/node` (`loadOgFonts`, `readOgFont`). These check every SHA-256 against the package manifest. They work with the default Turbopack `next build` and with `next build --webpack`. If the docs app uses webpack with `output: 'standalone'` and the build moves, add `serverExternalPackages: ['@bloxwap/hig']`. Vendor a candidate that includes the per-face asset URL fix. Under Turbopack, an earlier `0.1.0` tarball returns 500 with `SHA-256 … does not match the manifest`. An edge host checks `fontManifest.outputs` itself, as tapsiren does. Font names (`'Bloxwap Sans'`, `'Bloxwap Mono'`) and weights stay the same. 4. Rebuild the OG cards and compare them with the previous images. The faces are byte-identical copies, so the cards should not change. Record the vendored-byte delta in the repository's adoption evidence and update its row in `planning/adoption.json`. See [Open Graph card fonts](../installation.md#open-graph-card-fonts) for the full API and the edge-runtime path. ## Chart adapter: `@bloxwap/hig/chart` (#96) `@bloxwap/hig/react` no longer has a `./chart` entry or an optional `@bloxwap/chart` peer. `PriceChart` and `chartTheme` moved unchanged to `@bloxwap/hig/chart`, which requires `@bloxwap/chart` `^0.3.0` as a peer. The optional peer made npm drop chart's `packages/chart` workspace when `package-lock.json` was regenerated from scratch without `legacy-peer-deps`. This reproduces on npm 10.9.9, 11.6.1, 11.21.1 and 12.2.0. A required peer does not trigger it. | Repository | Uses `@bloxwap/hig/react/chart`? | Change at the next HIG refresh | | --- | --- | --- | | chart | No. The docs theme comes from `@bloxwap/hig/tokens` (`apps/docs/scripts/hig-tokens.mjs`) | Vendor the new hig-react tarball. Remove the `legacy-peer-deps` regeneration note from `vendor/bloxwap-hig/README.md`. A from-scratch `npm install` then keeps `@bloxwap/chart -> ./packages/chart`. Vendor `@bloxwap/hig/chart` only if the docs adopt `PriceChart` | | sfx, haptic, hyperliquid, font | No | None beyond the usual tarball refresh | A host that does import the entry changes one specifier and adds the package: ```diff -import {PriceChart} from '@bloxwap/hig/react/chart'; +import {PriceChart} from '@bloxwap/hig/chart'; ``` The candidate was never published, and a GitHub code search on 2026-10-16 found no consumer outside this repository, so no deprecated shim is kept (see [charts](../components/chart.md#moved-from-bloxwap-hig-react-chart-96)). `npm run smoke:packed-chart-workspace` reproduces chart's layout: an npm workspace whose `packages/chart` provides `@bloxwap/chart`, and an app that depends on the packed hig-react and hig-chart. It deletes the lockfile, reinstalls with strict peers and fails if the workspace link is lost. ## Documentation website and migration shell · HIG 0.1.0 Source: docs/website.md # Documentation website and migration shell · HIG 0.1.0 HIG uses Next.js static export and Fumadocs, matching the sibling SDK sites. SFX and Haptic pin `fumadocs-core`/`fumadocs-ui` 16.17.1, `fumadocs-mdx` 15.4.7 and Next 16.4.0; HIG uses those versions. Font uses 16.15.17/15.4.5/16.3.6 and Chart 16.15.14/15.4.5/16.3.6. React 19.2 satisfies the current Fumadocs peer contract. `docs/` Markdown chapters are the single normative source. The trading glossary is the exception: `docs/foundations/glossary.json` is authored and `docs/foundations/glossary.md` is generated. `scripts/generate-docs.mjs` generates the ignored `apps/docs/content/docs` collection, `meta.json` navigation, Markdown downloads, catalog data and `llms.txt`. Do not edit generated guides. Component reference pages derive from published package declarations and `planning/examples.json`; they link to the normative family guide. `npm run generate:examples` emits runnable web/native previews; `npm run examples:check` compiles all code variants. `apps/docs/source.config.ts` defines the MDX collection; `lib/source.ts` supplies the page tree. `app/docs/[[...slug]]` renders Fumadocs titles, TOCs, code, navigation and source/edit links. `components/provider.tsx` and `search.tsx` provide the dark theme and static Orama search at `search.json`. Fonts, palette and preview controls import the shipped Bloxwap font and HIG packages. Fumadocs owns the responsive navigation, accessible search dialog and document shell. Run `npm ci`, then `bun docs:dev` or `npm run docs:dev`. Turbo's terminal UI prepares package declarations, examples and guides before starting Next at `http://localhost:3905/`, with documents under `/docs/`. `npm run dev:docs` is an alias. Use `bun docs:dev --ui=stream` for plain terminal logs. `bun docs:check` and `bun docs:build` also prepare their required package outputs. `npm run build:docs` generates the collection, builds Next and copies its static export to `apps/docs/dist`. Preview with `bun docs:preview` at `http://localhost:4174/hig/`. The static build defaults to `/hig`, while development defaults to the host root; override `NEXT_PUBLIC_BASE_PATH` without a trailing slash and `HIG_DOCS_ORIGIN` for another host. Every canonical document lives under `/docs/`. Explicit static redirect documents retain the previous `/guides/*`, `/components/*` and `/catalog/` URLs, including their query and fragment. Sitemap, Markdown and LLM exports ship in the same artifact. No deployment is part of the build. The homepage retains the bounded playground: App/Pro/Docs presets, supported states, locale/direction contracts, independent sound/haptics/motion settings, volume, matching Get Code, and validated share configuration. Native examples are real RN components in the Expo gallery. Device screenshots, VoiceOver/TalkBack and physical feedback observations remain pending. Live examples use sample host-owned actions and never submit orders or call a wallet. ## Reusing the shell for issue #34 Each SDK keeps its own content and distinctive demo: Hyperliquid market playground, Chart preview, Font type tester, SFX sound board, Haptic interaction demos. Start with HIG's `next.config.mjs`, PostCSS/source config, `lib/source.ts`, root/docs layouts, provider/search and MDX component mapping. Set the product title, repository, base path and port. Copy the shell code as a starting template, then import the versioned HIG token CSS, React CSS and font assets rather than copying palette values or primitive implementations. Keep the SDK's existing `content/docs` as its normative collection, or generate it from one existing Markdown source as HIG does; never maintain two authored guides. Place SDK demos behind a client boundary like `components/demo.tsx`; keep documentation server-rendered and searchable. Compose HIG install/copy/code/callout controls in those demos. Preserve each SDK's static search, llms.txt, sitemap, metadata and legacy URLs. Match the production base path during browser tests. Run build, lint, typecheck, example checks and the docs gates, capture desktop/mobile pages and the distinctive demo, and record exceptions and package versions in the #34 migration matrix. This HIG change provides the shell; it does not claim the five separate SDK migrations are complete. ## Validation `npm run check` includes the Next build and docs typecheck. CI runs production browser gates against the exported site, including the playground, search/navigation, legacy deep links, code tabs, all component states, runtime errors and axe checks. Screenshots and browser reports are uploaded with the static export as review artifacts. Publication, tagging and deployment remain separate operations. The separate Documentation workflow builds and typechecks the static site for pull requests and pushes to `main`. After a successful build on `main`, it publishes `apps/docs/out` to [GitHub Pages](https://bloxwap.github.io/hig/) using the `/hig` base path. Pages uses GitHub Actions as its publishing source. This publishes the candidate documentation; package releases and tags remain separate operations. The workflow can also be run manually from `main` to redeploy. The Fumadocs/Next framework changes the whole-export asset baseline: approximately 587 KB gzip of JavaScript and 95 KB CSS across all split routes (previous Vite budgets: 240 KB gzip and 22 KB). The new whole-export ceilings are 650 KB gzip and 110 KB CSS. This measures every chunk, not one page load. Font loading remains Latin Sans/Mono from upstream package files through `next/font/local`, avoiding the all-script loader’s roughly 13 MB export; the existing 600 KB font ceiling is retained.