diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index ebcaaf6..3b20e48 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -29,6 +29,7 @@ jobs: - run: npm run smoke:packed - run: npm run smoke:packed-native - run: npm run smoke:packed-boundaries + - run: npm run smoke:packed-fumadocs - run: npm run smoke:packed-examples - run: npx playwright install --with-deps chromium firefox webkit - run: npm run smoke:packed-next diff --git a/apps/docs/public/llms.txt b/apps/docs/public/llms.txt index ccbf8ee..43b196a 100644 --- a/apps/docs/public/llms.txt +++ b/apps/docs/public/llms.txt @@ -60,7 +60,7 @@ Source: docs/architecture/platform-matrix.md 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: chart only for chart entry; community Slider for native selection entry; react-native-svg for native brand entry. Gorhom/Reanimated/Worklets/Gesture Handler are isolated behind the optional gesture-sheet entry; base imports do not load them. No root barrel loads every component. DOM overlays are one scoped family entry and carry Radix MIT attribution. ESM is supported; CommonJS consumer support is not claimed. +Optional peer packages: chart only for chart entry; community Slider for native selection entry; react-native-svg for native brand entry. Gorhom/Reanimated/Worklets/Gesture Handler are isolated behind the optional gesture-sheet entry; base imports 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. ## Actions & surfaces · HIG 0.1.0 @@ -308,7 +308,7 @@ 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. Import `@bloxwap/hig-react/css` once on web; tokens CSS is a separate explicit import. +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 @@ -1226,6 +1226,10 @@ These are review candidates, not published npm packages. Use local dry-run tarba Run `npm ci && npm run build && npm run smoke:packed`. The smoke script installs tarballs into an independent temporary consumer and builds the fixture. For a host app install tokens, contracts and React tarballs plus React/DOM 19.2. Import `@bloxwap/hig-tokens/css` and `@bloxwap/hig-react/css` once; import individual components such as `@bloxwap/hig-react/button`. Pin @bloxwap/font 0.1.2 and choose only the required families/scripts using asset URL @font-face rules named `'Bloxwap Sans'` and `'Bloxwap Mono'`; HIG controls read `--hig-font-sans`/`--hig-font-mono`, which fall back to those literal families. The included fixture has Sans and Mono only, and its browser smoke checks that `.hig-button` and `.hig-number` render loaded Bloxwap faces. Never copy font binaries or component folders. +### 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. + ## SSR and Next client boundaries Run `npm run smoke:ssr`, `npm run build:next` and `npm run smoke:packed-next`. Token/formatting imports and button/provider SSR are deterministic. Components with hooks carry `use client` in their emitted modules. In Next, import HIG CSS in the app layout and put interactive example state in a client component; keep wallet/network logic with the host. Next's published Bloxwap font loaders remain owned upstream. diff --git a/apps/docs/public/markdown/architecture/platform-matrix.md b/apps/docs/public/markdown/architecture/platform-matrix.md index bd9f40f..9965cf9 100644 --- a/apps/docs/public/markdown/architecture/platform-matrix.md +++ b/apps/docs/public/markdown/architecture/platform-matrix.md @@ -16,4 +16,4 @@ 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: chart only for chart entry; community Slider for native selection entry; react-native-svg for native brand entry. Gorhom/Reanimated/Worklets/Gesture Handler are isolated behind the optional gesture-sheet entry; base imports do not load them. No root barrel loads every component. DOM overlays are one scoped family entry and carry Radix MIT attribution. ESM is supported; CommonJS consumer support is not claimed. +Optional peer packages: chart only for chart entry; community Slider for native selection entry; react-native-svg for native brand entry. Gorhom/Reanimated/Worklets/Gesture Handler are isolated behind the optional gesture-sheet entry; base imports 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. diff --git a/apps/docs/public/markdown/components/recipes.md b/apps/docs/public/markdown/components/recipes.md index 6aeef21..1836cce 100644 --- a/apps/docs/public/markdown/components/recipes.md +++ b/apps/docs/public/markdown/components/recipes.md @@ -4,7 +4,7 @@ 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. Import `@bloxwap/hig-react/css` once on web; tokens CSS is a separate explicit import. +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 diff --git a/apps/docs/public/markdown/installation.md b/apps/docs/public/markdown/installation.md index 0a20afb..7d05395 100644 --- a/apps/docs/public/markdown/installation.md +++ b/apps/docs/public/markdown/installation.md @@ -6,6 +6,10 @@ These are review candidates, not published npm packages. Use local dry-run tarba Run `npm ci && npm run build && npm run smoke:packed`. The smoke script installs tarballs into an independent temporary consumer and builds the fixture. For a host app install tokens, contracts and React tarballs plus React/DOM 19.2. Import `@bloxwap/hig-tokens/css` and `@bloxwap/hig-react/css` once; import individual components such as `@bloxwap/hig-react/button`. Pin @bloxwap/font 0.1.2 and choose only the required families/scripts using asset URL @font-face rules named `'Bloxwap Sans'` and `'Bloxwap Mono'`; HIG controls read `--hig-font-sans`/`--hig-font-mono`, which fall back to those literal families. The included fixture has Sans and Mono only, and its browser smoke checks that `.hig-button` and `.hig-number` render loaded Bloxwap faces. Never copy font binaries or component folders. +### 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. + ## SSR and Next client boundaries Run `npm run smoke:ssr`, `npm run build:next` and `npm run smoke:packed-next`. Token/formatting imports and button/provider SSR are deterministic. Components with hooks carry `use client` in their emitted modules. In Next, import HIG CSS in the app layout and put interactive example state in a client component; keep wallet/network logic with the host. Next's published Bloxwap font loaders remain owned upstream. diff --git a/apps/docs/src/content.json b/apps/docs/src/content.json index 241c99d..7151873 100644 --- a/apps/docs/src/content.json +++ b/apps/docs/src/content.json @@ -27,7 +27,7 @@ "route": "guides/architecture/platform-matrix", "title": "Candidate platform support · 0.1.0", "source": "docs/architecture/platform-matrix.md", - "html": "

Candidate platform support · 0.1.0

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
AreaDOMRNEvidence
Actions, surfaces, text, fieldsSemantic DOM elementsPressable/View/Text/TextInputbehavior tests, Vite/SSR/Metro
Native text scaling and script trackingBrowser zoom; no capText/Heading/NumberReadout maxFontSizeMultiplier (prop or HIGProvider default); Korean/Chinese runs at companion spacing via script and letterSpacingByScriptnative Jest (#67); iOS AX5 and Android 200% gallery captures pending
Native colour rolesCSS severity/data attributesSwitch tone, Alert severity/tone take token roles only; raw colours fall backnative Jest (#67)
Selection/rangeBuilt-in input/selectOS 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; module compiled in a headless Android release build (#71); emulator set-progress run pending
Dialog/sheet/menuRadix scoped primitivesModal and explicit close/back; optional Gorhom GestureSheet entryDOM nested focus/Escape; mocked native focus/back/detents plus separate packed Android/iOS gesture entry
Tooltip/popoverRadix hover/focus/anchoredpersistent HelpText/modalno RN hover claim
Navigation/searchhref/callback adaptershost route/back callbackstab/search/disclosure contracts
Financial lists/tablesemantic table/listFlatList labeled rowsformatting and contract tests
Chartoptional lazy @bloxwap/chart canvasnumeric/list summary onlytyped theme; physical gesture evidence pending
Brand markindividual SVG renderer/assetoptional react-native-svgpinned geometry; device comparison pending
Fontspinned Sans/Mono asset URLshash-pinned static faces, typed manifest, explicit weight registration and host script fallbackExpo-web loading/byte/hash gates; no physical native-font certification
\n

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.

\n

Optional peer packages: chart only for chart entry; community Slider for native selection entry; react-native-svg for native brand entry. Gorhom/Reanimated/Worklets/Gesture Handler are isolated behind the optional gesture-sheet entry; base imports do not load them. No root barrel loads every component. DOM overlays are one scoped family entry and carry Radix MIT attribution. ESM is supported; CommonJS consumer support is not claimed.

\n", + "html": "

Candidate platform support · 0.1.0

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
AreaDOMRNEvidence
Actions, surfaces, text, fieldsSemantic DOM elementsPressable/View/Text/TextInputbehavior tests, Vite/SSR/Metro
Native text scaling and script trackingBrowser zoom; no capText/Heading/NumberReadout maxFontSizeMultiplier (prop or HIGProvider default); Korean/Chinese runs at companion spacing via script and letterSpacingByScriptnative Jest (#67); iOS AX5 and Android 200% gallery captures pending
Native colour rolesCSS severity/data attributesSwitch tone, Alert severity/tone take token roles only; raw colours fall backnative Jest (#67)
Selection/rangeBuilt-in input/selectOS 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; module compiled in a headless Android release build (#71); emulator set-progress run pending
Dialog/sheet/menuRadix scoped primitivesModal and explicit close/back; optional Gorhom GestureSheet entryDOM nested focus/Escape; mocked native focus/back/detents plus separate packed Android/iOS gesture entry
Tooltip/popoverRadix hover/focus/anchoredpersistent HelpText/modalno RN hover claim
Navigation/searchhref/callback adaptershost route/back callbackstab/search/disclosure contracts
Financial lists/tablesemantic table/listFlatList labeled rowsformatting and contract tests
Chartoptional lazy @bloxwap/chart canvasnumeric/list summary onlytyped theme; physical gesture evidence pending
Brand markindividual SVG renderer/assetoptional react-native-svgpinned geometry; device comparison pending
Fontspinned Sans/Mono asset URLshash-pinned static faces, typed manifest, explicit weight registration and host script fallbackExpo-web loading/byte/hash gates; no physical native-font certification
\n

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.

\n

Optional peer packages: chart only for chart entry; community Slider for native selection entry; react-native-svg for native brand entry. Gorhom/Reanimated/Worklets/Gesture Handler are isolated behind the optional gesture-sheet entry; base imports 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.

\n", "toc": [], "section": "architecture" }, @@ -213,7 +213,7 @@ "route": "guides/components/recipes", "title": "Recipes · HIG 0.1.0", "source": "docs/components/recipes.md", - "html": "

Recipes · HIG 0.1.0

\n

Composed patterns for balances, markets, trade review, and documentation.

\n

Includes MarketRow, BalanceCard, TradeEntry, ActivityList, InstallCommand, CodeWindow, and MarketingHero.

\n

Issue #26. Entry: recipes. Import platform entries from @bloxwap/hig-react/recipes or @bloxwap/hig-native/recipes according to applicability below. Import @bloxwap/hig-react/css once on web; tokens CSS is a separate explicit import.

\n

API and anatomy

\n

All data and actions are supplied by the host application.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
RecipeKey props and behavior
TradeEntryRaw amount, onAmountChange, onConfirm, available, busy, and error; opens review before submission.
InstallCommandcommand and onCopy; accepts an asynchronous host clipboard adapter.
\n

When to use and examples

\n

Do: compose primitives and explicit outcomes in comfortable app or compact Pro context. Don’t: put wallet/network/SDK services in core packages.

\n

States, interaction and accessibility

\n

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.

\n

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.

\n

Platform mapping and limits

\n

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.

\n

Validation and citation

\n

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.

\n", + "html": "

Recipes · HIG 0.1.0

\n

Composed patterns for balances, markets, trade review, and documentation.

\n

Includes MarketRow, BalanceCard, TradeEntry, ActivityList, InstallCommand, CodeWindow, and MarketingHero.

\n

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.

\n

API and anatomy

\n

All data and actions are supplied by the host application.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
RecipeKey props and behavior
TradeEntryRaw amount, onAmountChange, onConfirm, available, busy, and error; opens review before submission.
InstallCommandcommand and onCopy; accepts an asynchronous host clipboard adapter.
\n

When to use and examples

\n

Do: compose primitives and explicit outcomes in comfortable app or compact Pro context. Don’t: put wallet/network/SDK services in core packages.

\n

States, interaction and accessibility

\n

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.

\n

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.

\n

Platform mapping and limits

\n

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.

\n

Validation and citation

\n

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.

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

Installation and citation · HIG 0.1.0

\n

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

\n

React / Vite

\n

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

\n

SSR and Next client boundaries

\n

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

\n

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

\n

Expo / React Native

\n

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

\n

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

\n

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

\n

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

\n

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

\n

Tokens only / plain CSS / Tailwind

\n

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

\n

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

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

Installation and citation · HIG 0.1.0

\n

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

\n

React / Vite

\n

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

\n

Radix overlay peers and Fumadocs

\n

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.

\n

SSR and Next client boundaries

\n

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

\n

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

\n

Expo / React Native

\n

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

\n

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

\n

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

\n

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

\n

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

\n

Tokens only / plain CSS / Tailwind

\n

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

\n

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

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
RolesExampleValues
Color--hig-surface, --hig-actionsRGB, then OKLCH/P3 in @supports
Space--hig-space-mdxs–xxxl, section, in rem
Font--hig-font-sans, --hig-font-mono@bloxwap/font Next variable, then literal family
Radius--hig-radius-md, --hig-radius-pillsm md lg xl in rem; pill 9999px
Type--hig-type-body-size, --hig-type-body-line-height, --hig-type-body-weight, --hig-type-bodycaption label body title display; the bare role is a font shorthand in Sans
Motion--hig-motion-duration-base, --hig-motion-easing-standardfast base slow ms; 0ms under prefers-reduced-motion: reduce or [data-hig-motion='reduced']
Density--hig-density-control, --hig-density-compact-gapcontrol target gap per mode; the unprefixed roles follow [data-hig-density='compact']
\n
@import '@bloxwap/hig-tokens/css';\n.card { border-radius: var(--hig-radius-lg); padding: var(--hig-space-lg); font: var(--hig-type-body); }\n.card h2 { font: var(--hig-type-title); }\n.card button { min-height: var(--hig-density-control); transition: background var(--hig-motion-duration-fast) var(--hig-motion-easing-standard); }\n``` Tailwind may reference these values through host configuration; HIG does not require content scans or a shadcn registry. Native uses dp and explicit hex/channel maps; do not feed CSS functions or rename rem to dp. The audited monorepo web/native rem roots are both 16; HIG supports StyleSheet rather than claiming a verified NativeWind consumer. See the source-reconciliation chapter for native font generation and registration.\n\n## Optional chart, sound and haptics\n\nChart is DOM-only and lazy; install @bloxwap/chart 0.3.0 for that entry and supply a textual summary. Native uses labeled readouts/lists. Feedback install paths, preferences, iOS physical-tap limitations and Expo backends are in the feedback chapters. Keep essential outcome text available with both channels off.\n\n## Stable citations and upgrades\n\nCite “Bloxwap HIG 0.1.0 candidate, chapter/anchor” plus an immutable GitHub commit permalink. Branch and docs preview links are mutable. Upgrade tokens, contracts, renderer, docs/examples and semantic cue mapping together; compare API behavior and adopt behind a rollback flag. Token appearance and accessibility behavior changes require semver review. No publication, release or tag is performed by this implementation.\n\nThe native fixture pins React **19.1.0**, matching the RN 0.81.5 renderer exactly. Web/Next/Fumadocs fixtures use React 19.2.0. Metro explicitly resolves every native `react`/`react/*` import to the native fixture's version so shared-workspace code cannot load web React. Quality and packed-native checks compare the installed version to RN's actual renderer guard; a successful Metro bundle alone does not prove runtime compatibility.\n
\n", "toc": [ { "label": "React / Vite", @@ -2571,7 +2571,7 @@ "source": "docs/components/recipes.md", "api": { "react": { - "entry": "recipes", + "entry": "recipes/market-row", "signature": "export declare function MarketRow({ symbol, name, price, change, onSelect }: {\n symbol: string;\n name: string;\n price: number | null;\n change: number | null;\n onSelect: () => void;\n}): import(\"react/jsx-runtime\").JSX.Element;" }, "native": { @@ -2597,7 +2597,7 @@ "source": "docs/components/recipes.md", "api": { "react": { - "entry": "recipes", + "entry": "recipes/balance-card", "signature": "export declare function BalanceCard({ label, balance, stale }: {\n label: string;\n balance: number | null;\n stale?: boolean;\n}): import(\"react/jsx-runtime\").JSX.Element;" }, "native": { @@ -2623,7 +2623,7 @@ "source": "docs/components/recipes.md", "api": { "react": { - "entry": "recipes", + "entry": "recipes/trade-entry", "signature": "export declare function TradeEntry({ asset, amount, onAmountChange, onConfirm, busy, error, available }: {\n asset: string;\n amount: string;\n onAmountChange: (v: string) => void;\n onConfirm: () => void;\n busy?: boolean;\n error?: string;\n available: string;\n}): import(\"react/jsx-runtime\").JSX.Element;" }, "native": { @@ -2647,7 +2647,7 @@ "source": "docs/components/recipes.md", "api": { "react": { - "entry": "recipes", + "entry": "recipes/install-command", "signature": "export declare function InstallCommand({ command, onCopy }: {\n command: string;\n onCopy: () => Promise | void;\n}): import(\"react/jsx-runtime\").JSX.Element;" }, "native": { @@ -2671,7 +2671,7 @@ "source": "docs/components/recipes.md", "api": { "react": { - "entry": "recipes", + "entry": "recipes/code-window", "signature": "export declare function CodeWindow({ language, children }: React.PropsWithChildren<{\n language: string;\n}>): import(\"react/jsx-runtime\").JSX.Element;" }, "native": { @@ -2695,7 +2695,7 @@ "source": "docs/components/recipes.md", "api": { "react": { - "entry": "recipes", + "entry": "recipes/marketing-hero", "signature": "export declare function MarketingHero({ title, description, href, cta }: {\n title: string;\n description: string;\n href: string;\n cta: string;\n}): import(\"react/jsx-runtime\").JSX.Element;" }, "native": { @@ -2720,7 +2720,7 @@ "source": "docs/components/recipes.md", "api": { "react": { - "entry": "recipes", + "entry": "recipes/activity-list", "signature": "export declare function ActivityList({ items }: {\n items: readonly {\n id: string;\n label: string;\n status: string;\n }[];\n}): import(\"react/jsx-runtime\").JSX.Element;" }, "native": { diff --git a/docs/architecture/platform-matrix.md b/docs/architecture/platform-matrix.md index bd9f40f..9965cf9 100644 --- a/docs/architecture/platform-matrix.md +++ b/docs/architecture/platform-matrix.md @@ -16,4 +16,4 @@ 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: chart only for chart entry; community Slider for native selection entry; react-native-svg for native brand entry. Gorhom/Reanimated/Worklets/Gesture Handler are isolated behind the optional gesture-sheet entry; base imports do not load them. No root barrel loads every component. DOM overlays are one scoped family entry and carry Radix MIT attribution. ESM is supported; CommonJS consumer support is not claimed. +Optional peer packages: chart only for chart entry; community Slider for native selection entry; react-native-svg for native brand entry. Gorhom/Reanimated/Worklets/Gesture Handler are isolated behind the optional gesture-sheet entry; base imports 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. diff --git a/docs/components/recipes.md b/docs/components/recipes.md index 6aeef21..1836cce 100644 --- a/docs/components/recipes.md +++ b/docs/components/recipes.md @@ -4,7 +4,7 @@ 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. Import `@bloxwap/hig-react/css` once on web; tokens CSS is a separate explicit import. +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 diff --git a/docs/installation.md b/docs/installation.md index 0a20afb..7d05395 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -6,6 +6,10 @@ These are review candidates, not published npm packages. Use local dry-run tarba Run `npm ci && npm run build && npm run smoke:packed`. The smoke script installs tarballs into an independent temporary consumer and builds the fixture. For a host app install tokens, contracts and React tarballs plus React/DOM 19.2. Import `@bloxwap/hig-tokens/css` and `@bloxwap/hig-react/css` once; import individual components such as `@bloxwap/hig-react/button`. Pin @bloxwap/font 0.1.2 and choose only the required families/scripts using asset URL @font-face rules named `'Bloxwap Sans'` and `'Bloxwap Mono'`; HIG controls read `--hig-font-sans`/`--hig-font-mono`, which fall back to those literal families. The included fixture has Sans and Mono only, and its browser smoke checks that `.hig-button` and `.hig-number` render loaded Bloxwap faces. Never copy font binaries or component folders. +### 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. + ## SSR and Next client boundaries Run `npm run smoke:ssr`, `npm run build:next` and `npm run smoke:packed-next`. Token/formatting imports and button/provider SSR are deterministic. Components with hooks carry `use client` in their emitted modules. In Next, import HIG CSS in the app layout and put interactive example state in a client component; keep wallet/network logic with the host. Next's published Bloxwap font loaders remain owned upstream. diff --git a/evidence/packed-smoke.json b/evidence/packed-smoke.json index 1c17279..cd8e00c 100644 --- a/evidence/packed-smoke.json +++ b/evidence/packed-smoke.json @@ -4,6 +4,7 @@ "@bloxwap/hig-contracts", "@bloxwap/hig-feedback", "@bloxwap/hig-native", + "@bloxwap/hig-native-fonts", "@bloxwap/hig-react", "@bloxwap/hig-tokens" ], diff --git a/evidence/quality-report.json b/evidence/quality-report.json index 2899f2e..b0b07d5 100644 --- a/evidence/quality-report.json +++ b/evidence/quality-report.json @@ -8,8 +8,9 @@ "contracts": 6, "feedback": 17, "native": 38, - "react": 27, - "tokens": 6 + "native-fonts": 4, + "react": 41, + "tokens": 5 }, "sizes": { "button": { @@ -32,7 +33,7 @@ }, "nativeSwitch": { "raw": 3747, - "gzip": 1850, + "gzip": 1849, "inputs": [ "packages/tokens/dist/index.js", "packages/contracts/dist/index.js", @@ -53,11 +54,11 @@ ] }, "docsJS": { - "raw": 2193927, - "gzip": 609406 + "raw": 2205989, + "gzip": 615153 }, "docsCSS": { - "raw": 108549 + "raw": 110489 }, "fonts": { "raw": 592796 diff --git a/package-lock.json b/package-lock.json index 22b8554..6cd177e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21894,10 +21894,10 @@ }, "peerDependencies": { "@bloxwap/chart": "^0.3.0", - "@radix-ui/react-dialog": "1.1.15", - "@radix-ui/react-dropdown-menu": "2.1.16", - "@radix-ui/react-popover": "1.1.15", - "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-tooltip": "^1.2.8", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/package.json b/package.json index dea1573..751d1ee 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "release:dry-run": "node scripts/release-dry-run.mjs", "smoke:packed-examples": "node scripts/packed-examples-smoke.mjs", "smoke:packed-boundaries": "node scripts/packed-boundaries-smoke.mjs", + "smoke:packed-fumadocs": "node scripts/packed-fumadocs-smoke.mjs", "smoke:release-artifact": "node scripts/release-artifact-smoke.mjs", "generate:fonts": "python3 scripts/generate-native-fonts.py", "generate:glossary": "node scripts/generate-glossary.mjs" diff --git a/packages/react/package.json b/packages/react/package.json index 9c9f13f..8738b83 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -56,6 +56,34 @@ "types": "./dist/recipes.d.ts", "import": "./dist/recipes.js" }, + "./recipes/market-row": { + "types": "./dist/recipes/market-row.d.ts", + "import": "./dist/recipes/market-row.js" + }, + "./recipes/balance-card": { + "types": "./dist/recipes/balance-card.d.ts", + "import": "./dist/recipes/balance-card.js" + }, + "./recipes/trade-entry": { + "types": "./dist/recipes/trade-entry.d.ts", + "import": "./dist/recipes/trade-entry.js" + }, + "./recipes/install-command": { + "types": "./dist/recipes/install-command.d.ts", + "import": "./dist/recipes/install-command.js" + }, + "./recipes/code-window": { + "types": "./dist/recipes/code-window.d.ts", + "import": "./dist/recipes/code-window.js" + }, + "./recipes/marketing-hero": { + "types": "./dist/recipes/marketing-hero.d.ts", + "import": "./dist/recipes/marketing-hero.js" + }, + "./recipes/activity-list": { + "types": "./dist/recipes/activity-list.d.ts", + "import": "./dist/recipes/activity-list.js" + }, "./chart": { "types": "./dist/chart.d.ts", "import": "./dist/chart.js" @@ -70,10 +98,10 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "@bloxwap/chart": "^0.3.0", - "@radix-ui/react-dialog": "1.1.15", - "@radix-ui/react-dropdown-menu": "2.1.16", - "@radix-ui/react-popover": "1.1.15", - "@radix-ui/react-tooltip": "1.2.8" + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-tooltip": "^1.2.8" }, "peerDependenciesMeta": { "@bloxwap/chart": { diff --git a/packages/react/src/dialog.tsx b/packages/react/src/dialog.tsx new file mode 100644 index 0000000..8257eea --- /dev/null +++ b/packages/react/src/dialog.tsx @@ -0,0 +1,9 @@ +"use client"; +import React from 'react'; +import * as Modal from '@radix-ui/react-dialog'; +import {Button} from './button.js'; +export interface DialogProps {open:boolean;onOpenChange:(open:boolean)=>void;title:string;description:string;trigger?:React.ReactNode;children:React.ReactNode} +export function Dialog({open,onOpenChange,title,description,trigger,children}:DialogProps){return {trigger&&{trigger}}{title}{description}{children};} +export const Sheet=Dialog; +/** Critical confirmation stays open on outside interaction and requires an explicit decision. */ +export function AlertDialog({open,onOpenChange,title,description,trigger,children}:DialogProps){return {trigger&&{trigger}}e.preventDefault()} className='hig-dialog'>{title}{description}{children};} diff --git a/packages/react/src/overlay.tsx b/packages/react/src/overlay.tsx index a965e7f..b9178fc 100644 --- a/packages/react/src/overlay.tsx +++ b/packages/react/src/overlay.tsx @@ -1,16 +1,11 @@ "use client"; import React,{useId,useRef} from 'react'; import {useHIG} from './provider.js'; -import * as Modal from '@radix-ui/react-dialog'; import * as Drop from '@radix-ui/react-dropdown-menu'; import * as Pop from '@radix-ui/react-popover'; import * as Tip from '@radix-ui/react-tooltip'; import {Button} from './button.js'; -export interface DialogProps {open:boolean;onOpenChange:(open:boolean)=>void;title:string;description:string;trigger?:React.ReactNode;children:React.ReactNode} -export function Dialog({open,onOpenChange,title,description,trigger,children}:DialogProps){return {trigger&&{trigger}}{title}{description}{children};} -export const Sheet=Dialog; -/** Critical confirmation stays open on outside interaction and requires an explicit decision. */ -export function AlertDialog({open,onOpenChange,title,description,trigger,children}:DialogProps){return {trigger&&{trigger}}e.preventDefault()} className='hig-dialog'>{title}{description}{children};} +export {Dialog,Sheet,AlertDialog,type DialogProps} from './dialog.js'; export function Menu({label,items}: {label:string;items:readonly {label:string;disabled?:boolean;onSelect:(event:Event)=>void}[]}){const {feedback}=useHIG();const id=useId();const sequence=useRef(0);const origin=useRef<'pointer'|'keyboard'>('keyboard');return {items.map((item,i)=>{origin.current='pointer';}} onKeyDown={()=>{origin.current='keyboard';}} onSelect={event=>{if(item.disabled)return;item.onSelect(event);if(!event.defaultPrevented)try{feedback?.emit({id:`${id}/${++sequence.current}`,source:id,intent:'selection',origin:origin.current});}catch{/* Optional feedback cannot alter the action. */}}}>{item.label})};} export function Popover({label,children}:React.PropsWithChildren<{label:string}>){return {children};} export function Tooltip({content,children}:React.PropsWithChildren<{content:string}>){return {children}{content};} diff --git a/packages/react/src/recipes.tsx b/packages/react/src/recipes.tsx index c0c2075..7402a84 100644 --- a/packages/react/src/recipes.tsx +++ b/packages/react/src/recipes.tsx @@ -1,16 +1,9 @@ "use client"; -import React,{useState} from 'react'; -import {Button,Link} from './button.js'; -import {Surface,Stack,Heading,Text} from './surface.js'; -import {Field,AmountInput} from './field.js'; -import {Dialog} from './overlay.js'; -import {Row,Usd,Percent} from './data.js'; -import {Alert} from './status.js'; -export function MarketRow({symbol,name,price,change,onSelect}: {symbol:string;name:string;price:number|null;change:number|null;onSelect:()=>void}){return ;} -export function BalanceCard({label,balance,stale}: {label:string;balance:number|null;stale?:boolean}){return {label};} -/** Host validates and submits. Opening review never submits a transaction. */ -export function TradeEntry({asset,amount,onAmountChange,onConfirm,busy,error,available}: {asset:string;amount:string;onAmountChange:(v:string)=>void;onConfirm:()=>void;busy?:boolean;error?:string;available:string}){const [review,setReview]=useState(false);return Review {asset} orderonAmountChange(e.target.value)} readOnly={busy}/>{error&&

{error}

}
;} -export function InstallCommand({command,onCopy}: {command:string;onCopy:()=>Promise|void}){const [status,setStatus]=useState('');return
{command}

{status}

;} -export function CodeWindow({language,children}:React.PropsWithChildren<{language:string}>){return {language}
{children}
;} -export function MarketingHero({title,description,href,cta}: {title:string;description:string;href:string;cta:string}){return
{title}{description}{cta}
;} -export function ActivityList({items}: {items:readonly {id:string;label:string;status:string}[]}){return items.length?:No activity yet;} +/** Aggregate entry. TradeEntry needs @radix-ui/react-dialog; import single recipes from `@bloxwap/hig-react/recipes/` to avoid overlay peers. */ +export {MarketRow} from './recipes/market-row.js'; +export {BalanceCard} from './recipes/balance-card.js'; +export {TradeEntry} from './recipes/trade-entry.js'; +export {InstallCommand} from './recipes/install-command.js'; +export {CodeWindow} from './recipes/code-window.js'; +export {MarketingHero} from './recipes/marketing-hero.js'; +export {ActivityList} from './recipes/activity-list.js'; diff --git a/packages/react/src/recipes/activity-list.tsx b/packages/react/src/recipes/activity-list.tsx new file mode 100644 index 0000000..6be0db8 --- /dev/null +++ b/packages/react/src/recipes/activity-list.tsx @@ -0,0 +1,4 @@ +"use client"; +import React from 'react'; +import {Text} from '../surface.js'; +export function ActivityList({items}: {items:readonly {id:string;label:string;status:string}[]}){return items.length?
    {items.map(i=>
  • {i.label} — {i.status}
  • )}
:No activity yet;} diff --git a/packages/react/src/recipes/balance-card.tsx b/packages/react/src/recipes/balance-card.tsx new file mode 100644 index 0000000..51367e4 --- /dev/null +++ b/packages/react/src/recipes/balance-card.tsx @@ -0,0 +1,5 @@ +"use client"; +import React from 'react'; +import {Surface,Text} from '../surface.js'; +import {Usd} from '../data.js'; +export function BalanceCard({label,balance,stale}: {label:string;balance:number|null;stale?:boolean}){return {label};} diff --git a/packages/react/src/recipes/code-window.tsx b/packages/react/src/recipes/code-window.tsx new file mode 100644 index 0000000..4b9c13d --- /dev/null +++ b/packages/react/src/recipes/code-window.tsx @@ -0,0 +1,4 @@ +"use client"; +import React from 'react'; +import {Surface,Text} from '../surface.js'; +export function CodeWindow({language,children}:React.PropsWithChildren<{language:string}>){return {language}
{children}
;} diff --git a/packages/react/src/recipes/install-command.tsx b/packages/react/src/recipes/install-command.tsx new file mode 100644 index 0000000..a76bc76 --- /dev/null +++ b/packages/react/src/recipes/install-command.tsx @@ -0,0 +1,5 @@ +"use client"; +import React,{useState} from 'react'; +import {Button} from '../button.js'; +import {Surface} from '../surface.js'; +export function InstallCommand({command,onCopy}: {command:string;onCopy:()=>Promise|void}){const [status,setStatus]=useState('');return
{command}

{status}

;} diff --git a/packages/react/src/recipes/market-row.tsx b/packages/react/src/recipes/market-row.tsx new file mode 100644 index 0000000..4dfa8f1 --- /dev/null +++ b/packages/react/src/recipes/market-row.tsx @@ -0,0 +1,5 @@ +"use client"; +import React from 'react'; +import {Button} from '../button.js'; +import {Row,Usd,Percent} from '../data.js'; +export function MarketRow({symbol,name,price,change,onSelect}: {symbol:string;name:string;price:number|null;change:number|null;onSelect:()=>void}){return ;} diff --git a/packages/react/src/recipes/marketing-hero.tsx b/packages/react/src/recipes/marketing-hero.tsx new file mode 100644 index 0000000..7473621 --- /dev/null +++ b/packages/react/src/recipes/marketing-hero.tsx @@ -0,0 +1,5 @@ +"use client"; +import React from 'react'; +import {Link} from '../button.js'; +import {Heading,Text} from '../surface.js'; +export function MarketingHero({title,description,href,cta}: {title:string;description:string;href:string;cta:string}){return
{title}{description}{cta}
;} diff --git a/packages/react/src/recipes/trade-entry.tsx b/packages/react/src/recipes/trade-entry.tsx new file mode 100644 index 0000000..7e392ce --- /dev/null +++ b/packages/react/src/recipes/trade-entry.tsx @@ -0,0 +1,9 @@ +"use client"; +import React,{useState} from 'react'; +import {Button} from '../button.js'; +import {Surface,Stack,Heading} from '../surface.js'; +import {Field,AmountInput} from '../field.js'; +import {Dialog} from '../dialog.js'; +import {Alert} from '../status.js'; +/** Host validates and submits. Opening review never submits a transaction. */ +export function TradeEntry({asset,amount,onAmountChange,onConfirm,busy,error,available}: {asset:string;amount:string;onAmountChange:(v:string)=>void;onConfirm:()=>void;busy?:boolean;error?:string;available:string}){const [review,setReview]=useState(false);return Review {asset} orderonAmountChange(e.target.value)} readOnly={busy}/>{error&&

{error}

}
;} diff --git a/planning/api-manifest.json b/planning/api-manifest.json index 271271b..1a60cce 100644 --- a/planning/api-manifest.json +++ b/planning/api-manifest.json @@ -130,10 +130,6 @@ "entry": "navigation", "signature": "export declare function Search({ query, onQueryChange, results, loading, error }: {\n query: string;\n onQueryChange: (s: string) => void;\n results: readonly {\n id: string;\n label: string;\n onSelect: () => void;\n }[];\n loading?: boolean;\n error?: string;\n}): import(\"react/jsx-runtime\").JSX.Element;" }, - "DialogProps": { - "entry": "overlay", - "signature": "export interface DialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title: string;\n description: string;\n trigger?: React.ReactNode;\n children: React.ReactNode;\n}" - }, "Dialog": { "entry": "overlay", "signature": "export declare function Dialog({ open, onOpenChange, title, description, trigger, children }: DialogProps): import(\"react/jsx-runtime\").JSX.Element;" @@ -146,6 +142,10 @@ "entry": "overlay", "signature": "export declare function AlertDialog({ open, onOpenChange, title, description, trigger, children }: DialogProps): import(\"react/jsx-runtime\").JSX.Element;" }, + "DialogProps": { + "entry": "overlay", + "signature": "export interface DialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title: string;\n description: string;\n trigger?: React.ReactNode;\n children: React.ReactNode;\n}" + }, "Menu": { "entry": "overlay", "signature": "export declare function Menu({ label, items }: {\n label: string;\n items: readonly {\n label: string;\n disabled?: boolean;\n onSelect: (event: Event) => void;\n }[];\n}): import(\"react/jsx-runtime\").JSX.Element;" @@ -175,31 +175,31 @@ "signature": "export declare function useControllable(value: T | undefined, initial: T, onChange?: (value: T) => void, intent?: FeedbackIntent): [T, (next: T) => void];" }, "MarketRow": { - "entry": "recipes", + "entry": "recipes/market-row", "signature": "export declare function MarketRow({ symbol, name, price, change, onSelect }: {\n symbol: string;\n name: string;\n price: number | null;\n change: number | null;\n onSelect: () => void;\n}): import(\"react/jsx-runtime\").JSX.Element;" }, "BalanceCard": { - "entry": "recipes", + "entry": "recipes/balance-card", "signature": "export declare function BalanceCard({ label, balance, stale }: {\n label: string;\n balance: number | null;\n stale?: boolean;\n}): import(\"react/jsx-runtime\").JSX.Element;" }, "TradeEntry": { - "entry": "recipes", + "entry": "recipes/trade-entry", "signature": "export declare function TradeEntry({ asset, amount, onAmountChange, onConfirm, busy, error, available }: {\n asset: string;\n amount: string;\n onAmountChange: (v: string) => void;\n onConfirm: () => void;\n busy?: boolean;\n error?: string;\n available: string;\n}): import(\"react/jsx-runtime\").JSX.Element;" }, "InstallCommand": { - "entry": "recipes", + "entry": "recipes/install-command", "signature": "export declare function InstallCommand({ command, onCopy }: {\n command: string;\n onCopy: () => Promise | void;\n}): import(\"react/jsx-runtime\").JSX.Element;" }, "CodeWindow": { - "entry": "recipes", + "entry": "recipes/code-window", "signature": "export declare function CodeWindow({ language, children }: React.PropsWithChildren<{\n language: string;\n}>): import(\"react/jsx-runtime\").JSX.Element;" }, "MarketingHero": { - "entry": "recipes", + "entry": "recipes/marketing-hero", "signature": "export declare function MarketingHero({ title, description, href, cta }: {\n title: string;\n description: string;\n href: string;\n cta: string;\n}): import(\"react/jsx-runtime\").JSX.Element;" }, "ActivityList": { - "entry": "recipes", + "entry": "recipes/activity-list", "signature": "export declare function ActivityList({ items }: {\n items: readonly {\n id: string;\n label: string;\n status: string;\n }[];\n}): import(\"react/jsx-runtime\").JSX.Element;" }, "Choice": { @@ -512,38 +512,34 @@ "entry": "recipes", "signature": "export declare function ActivityList({ items }: {\n items: readonly {\n id: string;\n label: string;\n status: string;\n }[];\n}): import(\"react/jsx-runtime\").JSX.Element;" }, - "accentRoles": { - "entry": "roles", - "signature": "export declare const accentRoles: readonly [\"action\", \"positive\", \"negative\", \"warning\", \"info\", \"buy\", \"sell\"];" - }, - "AccentRole": { - "entry": "roles", - "signature": "export type AccentRole = typeof accentRoles[number];" - }, - "roleColor": { - "entry": "roles", - "signature": "export declare function roleColor(role: unknown, fallback: AccentRole): string;" - }, "Select": { "entry": "select", "signature": "export declare function Select(props: ChoicesProps): import(\"react/jsx-runtime\").JSX.Element;" }, "Choice": { - "entry": "selection-types", + "entry": "selection", "signature": "export interface Choice {\n value: string;\n label: string;\n disabled?: boolean;\n}" }, "CheckedProps": { - "entry": "selection-types", + "entry": "selection", "signature": "export interface CheckedProps {\n label: string;\n checked?: boolean;\n defaultChecked?: boolean;\n disabled?: boolean;\n onCheckedChange?: (v: boolean) => void;\n}" }, "ChoicesProps": { - "entry": "selection-types", + "entry": "selection", "signature": "export interface ChoicesProps {\n label: string;\n options: readonly Choice[];\n value?: string;\n defaultValue?: string;\n disabled?: boolean;\n onValueChange?: (v: string) => void;\n}" }, + "Switch": { + "entry": "switch", + "signature": "export declare function Switch({ label, checked, defaultChecked, disabled, tone, onCheckedChange }: SwitchProps): import(\"react/jsx-runtime\").JSX.Element;" + }, "Slider": { "entry": "slider", "signature": "export declare function Slider({ label, value, defaultValue, min, max, step, disabled, onValueChange }: {\n label: string;\n value?: number;\n defaultValue?: number;\n min?: number;\n max?: number;\n step?: number;\n disabled?: boolean;\n onValueChange?: (v: number) => void;\n}): import(\"react/jsx-runtime\").JSX.Element;" }, + "AccentRole": { + "entry": "status", + "signature": "export type AccentRole = typeof accentRoles[number];" + }, "Badge": { "entry": "status", "signature": "export declare function Badge({ children }: React.PropsWithChildren): import(\"react/jsx-runtime\").JSX.Element;" @@ -636,9 +632,13 @@ "entry": "switch", "signature": "export interface SwitchProps extends CheckedProps {\n tone?: AccentRole;\n}" }, - "Switch": { - "entry": "switch", - "signature": "export declare function Switch({ label, checked, defaultChecked, disabled, tone, onCheckedChange }: SwitchProps): import(\"react/jsx-runtime\").JSX.Element;" + "accentRoles": { + "entry": "roles", + "signature": "export declare const accentRoles: readonly [\"action\", \"positive\", \"negative\", \"warning\", \"info\", \"buy\", \"sell\"];" + }, + "roleColor": { + "entry": "roles", + "signature": "export declare function roleColor(role: unknown, fallback: AccentRole): string;" }, "plainText": { "entry": "text-content", diff --git a/scripts/generate-docs.mjs b/scripts/generate-docs.mjs index f4c400e..5a92d1a 100644 --- a/scripts/generate-docs.mjs +++ b/scripts/generate-docs.mjs @@ -2,6 +2,7 @@ import {readFileSync,writeFileSync,mkdirSync,readdirSync,rmSync} from 'node:fs'; import {execFileSync} from 'node:child_process'; import {marked} from 'marked'; import ts from 'typescript'; +import {posix} from 'node:path'; import {exampleCode,examples} from './example-code.mjs'; execFileSync(process.execPath,['scripts/generate-glossary.mjs'],{stdio:'inherit'}); mkdirSync('apps/docs/public',{recursive:true}); @@ -14,7 +15,9 @@ const slug=s=>s.toLowerCase().replace(/<[^>]*>/g,'').replace(/[^a-z0-9]+/g,'-'). function html(markdown){return marked.parse(markdown,{async:false}).replace(/(.*?)<\/h\1>/g,(_,n,text)=>`${text}`);} const sourcefiles=files('docs'); for(const file of sourcefiles){const markdown=readFileSync(file,'utf8');const title=markdown.match(/^# (.+)/)?.[1]??file;const route='guides/'+file.slice(5,-3);pages.push({route,title,source:file,markdown,html:html(markdown),toc:[...markdown.matchAll(/^## (.+)$/gm)].map(m=>({label:m[1],id:slug(m[1])})),section:file.split('/')[1]});} -const api={};for(const platform of ['react','native']){api[platform]={};for(const file of readdirSync(`packages/${platform}/dist`).filter(f=>f.endsWith('.d.ts'))){const text=readFileSync(`packages/${platform}/dist/${file}`,'utf8'),parsed=ts.createSourceFile(file,text,ts.ScriptTarget.Latest,true);for(const statement of parsed.statements){if(!statement.modifiers?.some(m=>m.kind===ts.SyntaxKind.ExportKeyword))continue;const names=ts.isVariableStatement(statement)?statement.declarationList.declarations.map(d=>d.name.getText(parsed)):statement.name?[statement.name.getText(parsed)]:[];for(const name of names)api[platform][name]={entry:file.slice(0,-5),signature:statement.getText(parsed)};}}} +// A declaration belongs to its own file when that file is a public entry, otherwise to the public entry that re-exports it. +function declarations(path){const parsed=ts.createSourceFile(path,readFileSync(path,'utf8'),ts.ScriptTarget.Latest,true);const found=[];for(const statement of parsed.statements){if(ts.isExportDeclaration(statement)&&statement.moduleSpecifier&&statement.exportClause&&ts.isNamedExports(statement.exportClause)){const target=declarations(posix.join(posix.dirname(path),statement.moduleSpecifier.text.replace(/\.js$/,'.d.ts')));for(const element of statement.exportClause.elements){const source=(element.propertyName??element.name).getText(parsed);found.push(...target.filter(d=>d.name===source).map(d=>({...d,name:element.name.getText(parsed)})));}continue;}if(!statement.modifiers?.some(m=>m.kind===ts.SyntaxKind.ExportKeyword))continue;const names=ts.isVariableStatement(statement)?statement.declarationList.declarations.map(d=>d.name.getText(parsed)):statement.name?[statement.name.getText(parsed)]:[];for(const name of names)found.push({name,signature:statement.getText(parsed),origin:path});}return found;} +const api={};for(const platform of ['react','native']){api[platform]={};const exported=Object.values(JSON.parse(readFileSync(`packages/${platform}/package.json`,'utf8')).exports).map(t=>t?.types).filter(t=>t?.startsWith('./dist/')).map(t=>t.slice(7));const top=readdirSync(`packages/${platform}/dist`).filter(f=>f.endsWith('.d.ts')),files=[...new Set([...top.filter(f=>exported.includes(f)),...exported,...top])];for(const file of files)for(const {name,signature,origin} of declarations(`packages/${platform}/dist/${file}`)){const own=origin.slice(`packages/${platform}/dist/`.length);api[platform][name]??={entry:(exported.includes(own)?own:file).slice(0,-5),signature};}} const familySource={brand:'docs/foundations/platform.md',provider:'docs/architecture/platform-matrix.md'}; const catalog=examples.map(e=>{const source=familySource[e.family]??`docs/components/${e.family}.md`;const chapter=readFileSync(source,'utf8');const webCode=Object.fromEntries(e.states.map(s=>[s,exampleCode(e.name,'react','comfortable',s)]));const nativeCode=Object.fromEntries(e.states.map(s=>[s,exampleCode(e.name,'native','comfortable',s)]));const route=`components/${e.slug}`;pages.push({route,title:e.name,source,toc:[],section:'components',component:e.name});return {...e,route,source,code:{react:webCode,native:nativeCode},api:{react:api.react[e.name]??null,native:api.native[e.name]??null}};}); const codes=Object.fromEntries(catalog.map(e=>[e.name,e.code]));writeFileSync('apps/docs/src/code.json',JSON.stringify(codes,null,2)+'\n');for(const e of catalog){delete e.code;delete e.web;delete e.native;} diff --git a/scripts/packed-boundaries-smoke.mjs b/scripts/packed-boundaries-smoke.mjs index d12f7e9..01e4253 100644 --- a/scripts/packed-boundaries-smoke.mjs +++ b/scripts/packed-boundaries-smoke.mjs @@ -3,6 +3,7 @@ import {mkdtempSync,writeFileSync,existsSync} from 'node:fs'; import {tmpdir} from 'node:os'; import {join,resolve} from 'node:path'; import assert from 'node:assert/strict'; +import {build} from 'esbuild'; const directory=mkdtempSync(join(tmpdir(),'hig-boundaries-')); writeFileSync(join(directory,'package.json'),JSON.stringify({private:true,type:'module'})); const tarball=name=>resolve(`artifacts/bloxwap-hig-${name}-0.1.0.tgz`); @@ -14,4 +15,17 @@ execFileSync('npm',['install','--ignore-scripts','--no-audit','--no-fund',tarbal for(const peer of ['@radix-ui/react-dialog','@radix-ui/react-dropdown-menu','react-native','@bloxwap/chart','@bloxwap/sfx','@bloxwap/haptic'])assert.equal(existsSync(join(directory,'node_modules',peer)),false,`button unexpectedly installs ${peer}`); writeFileSync(join(directory,'button.mjs'),"import React from 'react';import {renderToString} from 'react-dom/server';import {Button} from '@bloxwap/hig-react/button';if(!renderToString(React.createElement(Button,null,'Review')).includes('Review'))throw Error('SSR failed');"); execFileSync(process.execPath,[join(directory,'button.mjs')],{cwd:directory,stdio:'inherit'}); -console.log('Clean packed boundaries: tokens/feedback root without React or device engines; Button SSR without Radix, native, chart or feedback engines'); +// Recipes without overlays must resolve, render and bundle with no Radix peer installed (#63). +const overlayFree={'install-command':['InstallCommand',{command:'npm i @bloxwap/hig-react',onCopy:()=>{}}],'code-window':['CodeWindow',{language:'bash',children:'npm ci'}],'market-row':['MarketRow',{symbol:'BTC',name:'Bitcoin',price:1,change:1,onSelect:()=>{}}],'balance-card':['BalanceCard',{label:'Balance',balance:1}],'marketing-hero':['MarketingHero',{title:'Trade',description:'Markets',href:'/',cta:'Start'}],'activity-list':['ActivityList',{items:[]}]}; +const bundle=contents=>build({stdin:{contents,resolveDir:directory,sourcefile:'entry.js'},absWorkingDir:directory,bundle:true,write:false,metafile:true,format:'esm',platform:'browser',logLevel:'silent'}); +for(const [entry,[name,props]] of Object.entries(overlayFree)){ + writeFileSync(join(directory,`${entry}.mjs`),`import React from 'react';import {renderToString} from 'react-dom/server';import {${name}} from '@bloxwap/hig-react/recipes/${entry}';if(!renderToString(React.createElement(${name},${JSON.stringify(props)})))throw Error('${name} SSR failed');`); + execFileSync(process.execPath,[join(directory,`${entry}.mjs`)],{cwd:directory,stdio:'inherit'}); + const result=await bundle(`export {${name}} from '@bloxwap/hig-react/recipes/${entry}';`); + const inputs=Object.keys(result.metafile.inputs); + assert.ok(result.outputFiles[0].contents.length>0); + assert.equal(inputs.some(p=>/radix|overlay|dialog/.test(p)),false,`recipes/${entry} pulls overlay code: ${inputs.filter(p=>/radix|overlay|dialog/.test(p))}`); +} +// Guard the guard: the overlay recipe still needs its Dialog peer, so the bundle must fail here. +await assert.rejects(bundle("export {TradeEntry} from '@bloxwap/hig-react/recipes/trade-entry';"),/@radix-ui\/react-dialog/,'trade-entry bundled without its dialog peer'); +console.log('Clean packed boundaries: tokens/feedback root without React or device engines; Button SSR and overlay-free recipe SSR/bundles without Radix, native, chart or feedback engines'); diff --git a/scripts/packed-fumadocs-smoke.mjs b/scripts/packed-fumadocs-smoke.mjs new file mode 100644 index 0000000..7eb77fe --- /dev/null +++ b/scripts/packed-fumadocs-smoke.mjs @@ -0,0 +1,48 @@ +import {execFileSync} from 'node:child_process'; +import {mkdtempSync,writeFileSync,readFileSync,readdirSync,existsSync,rmSync} from 'node:fs'; +import {tmpdir} from 'node:os'; +import {join,resolve,relative} from 'node:path'; +import assert from 'node:assert/strict'; +// #63: a packed hig-react consumer next to Fumadocs installs with strict peers and shares one Radix tree. +const fumadocs='16.17.3'; +const directory=mkdtempSync(join(tmpdir(),'hig-fumadocs-')); +const tarball=name=>resolve(`artifacts/bloxwap-hig-${name}-0.1.0.tgz`); +for(const name of ['react','tokens','contracts'])assert.ok(existsSync(tarball(name)),`Run smoke:packed first: missing ${tarball(name)}`); +writeFileSync(join(directory,'package.json'),JSON.stringify({private:true,type:'module',dependencies:{ + '@bloxwap/hig-react':`file:${tarball('react')}`,'@bloxwap/hig-tokens':`file:${tarball('tokens')}`,'@bloxwap/hig-contracts':`file:${tarball('contracts')}`, + 'fumadocs-ui':fumadocs,'fumadocs-core':fumadocs,next:'16.4.0',react:'19.2.0','react-dom':'19.2.0', + // Fumadocs supplies dialog and popover; the consumer adds the remaining optional HIG overlay peers at their current releases. + '@radix-ui/react-dropdown-menu':'^2.1.16','@radix-ui/react-tooltip':'^1.2.8'}},null,2)); +const env={...process.env,npm_config_legacy_peer_deps:'false',npm_config_strict_peer_deps:'true'}; +const npm=args=>execFileSync('npm',[...args,'--ignore-scripts','--no-audit','--no-fund'],{cwd:directory,stdio:'pipe',env}); +npm(['install']); +rmSync(join(directory,'node_modules'),{recursive:true,force:true}); +npm(['ci']); +function manifests(dir,found=[]){ + for(const entry of readdirSync(dir,{withFileTypes:true})){ + if(!entry.isDirectory())continue; + const path=join(dir,entry.name); + if(entry.name.startsWith('@')){manifests(path,found);continue;} + if(existsSync(join(path,'package.json')))found.push(path); + if(existsSync(join(path,'node_modules')))manifests(join(path,'node_modules'),found); + } + return found; +} +const radix=manifests(join(directory,'node_modules')).filter(p=>relative(directory,p).split('node_modules/').pop().startsWith('@radix-ui/')); +const copies=new Map(); +for(const path of radix){const name=JSON.parse(readFileSync(join(path,'package.json'),'utf8')).name;copies.set(name,[...(copies.get(name)??[]),relative(directory,path)]);} +const duplicated=[...copies].filter(([,paths])=>paths.length>1); +assert.deepEqual(duplicated,[],`Duplicated Radix packages: ${JSON.stringify(duplicated)}`); +const versions={}; +for(const name of ['@radix-ui/react-dialog','@radix-ui/react-popover','@radix-ui/react-dropdown-menu','@radix-ui/react-tooltip']){ + assert.deepEqual(copies.get(name),[`node_modules/${name}`],`${name} must be a single hoisted copy`); + versions[name]=JSON.parse(readFileSync(join(directory,'node_modules',name,'package.json'),'utf8')).version; +} +assert.equal(existsSync(join(directory,'node_modules/@bloxwap/hig-react/node_modules')),false,'hig-react nests its own dependency tree'); +writeFileSync(join(directory,'overlay.mjs'),`import React from 'react';import {renderToString} from 'react-dom/server'; +import {Menu,Popover,Tooltip,Dialog} from '@bloxwap/hig-react/overlay';import {TradeEntry} from '@bloxwap/hig-react/recipes'; +const h=React.createElement; +const html=renderToString(h('main',null,h(Menu,{label:'Actions',items:[{label:'Copy',onSelect(){}}]}),h(Popover,{label:'Details'},'Body'),h(Tooltip,{content:'Hint'},h('button',null,'Hover')),h(Dialog,{open:false,onOpenChange(){},title:'T',description:'D',trigger:h('button',null,'Open')},'Body'),h(TradeEntry,{asset:'BTC',amount:'1',onAmountChange(){},onConfirm(){},available:'2'}))); +for(const text of ['Actions','Details','Hover','Open','Review order'])if(!html.includes(text))throw Error('Missing '+text);`); +execFileSync(process.execPath,[join(directory,'overlay.mjs')],{cwd:directory,stdio:'inherit'}); +console.log(`Packed hig-react beside fumadocs-ui ${fumadocs}: strict-peer npm ci, ${radix.length} Radix packages with no duplicates; overlay SSR on ${Object.entries(versions).map(([n,v])=>`${n.replace('@radix-ui/react-','')}@${v}`).join(', ')}`); diff --git a/scripts/quality.mjs b/scripts/quality.mjs index e5d4d6d..14904d6 100644 --- a/scripts/quality.mjs +++ b/scripts/quality.mjs @@ -7,7 +7,7 @@ import {resolve,dirname} from 'node:path'; import {fileURLToPath} from 'node:url'; export function checkExports(directory){const pkg=JSON.parse(readFileSync(`${directory}/package.json`,'utf8'));const targets=[];function collect(value){if(typeof value==='string')targets.push(value);else if(value&&typeof value==='object')Object.values(value).forEach(collect);}collect(pkg.exports);for(const target of targets){if(target.includes('*'))continue;if(!existsSync(resolve(directory,target)))throw Error(`Invalid export ${pkg.name}: ${target}`);}return targets.length;} export function assertBudget(name,actual,limit){if(actual>limit)throw Error(`Budget regression ${name}: ${actual} > ${limit}`);} -export function checkPlatformImports(directory,platform){for(const file of readdirSync(`${directory}/dist`).filter(f=>f.endsWith('.js'))){const text=readFileSync(`${directory}/dist/${file}`,'utf8');const prohibited=platform==='web'?/from\s*['"](?:react-native|expo(?:-|\/|['"])|@bloxwap\/hig-native)/:/from\s*['"](?:react-dom|@bloxwap\/(?:chart|sfx|haptic)|@bloxwap\/hig-react)/;if(prohibited.test(text))throw Error(`Invalid ${platform} platform import: ${file}`);}} +export function checkPlatformImports(directory,platform){for(const file of readdirSync(`${directory}/dist`,{recursive:true}).filter(f=>f.endsWith('.js'))){const text=readFileSync(`${directory}/dist/${file}`,'utf8');const prohibited=platform==='web'?/from\s*['"](?:react-native|expo(?:-|\/|['"])|@bloxwap\/hig-native)/:/from\s*['"](?:react-dom|@bloxwap\/(?:chart|sfx|haptic)|@bloxwap\/hig-react)/;if(prohibited.test(text))throw Error(`Invalid ${platform} platform import: ${file}`);}} export function checkCueHashes(directory='packages/feedback/assets'){const manifest=JSON.parse(readFileSync(`${directory}/manifest.json`,'utf8'));for(const cue of manifest.cues){const bytes=readFileSync(`${directory}/${cue.file}`);if(createHash('sha256').update(bytes).digest('hex')!==cue.sha256)throw Error(`Native cue artifact drift: ${cue.file}`);}return manifest.cues.reduce((sum,c)=>sum+c.bytes,0);} export async function qualityReport(){ const nativeRuntime=verifyNativeReact('fixtures/native');verifyExpoNativeModules('fixtures/native');