Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions apps/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,17 @@ path from the built artifact. It does not run Next.js or a search backend.

Edit MDX in `content/docs/` and sidebar order in each `meta.json`. The build
checks local HTML links and assets, and emits a browser-searchable index at
`search.json`. Fonts come from the installed `@bloxwap/font` package (and `@bloxwap/hig-og-fonts` for social cards), so builds do not fetch Google Fonts.
`search.json`. Fonts come from the installed `@bloxwap/font` package (and `@bloxwap/hig/og-fonts` for social cards), so builds do not fetch Google Fonts.

The presentation follows the Bloxwap HIG and consumes it as packages, not copies:

- `@bloxwap/hig-tokens`, `@bloxwap/hig-contracts` and `@bloxwap/hig-react` (plus the build-time
`@bloxwap/hig-og-fonts`) are pinned to published npm version `0.1.0`; the lockfile and [release record](../../docs/hig-adoption/registry-release.json) retain their registry integrity hashes.
`app/global.css` imports `@bloxwap/hig-tokens/css` and `@bloxwap/hig-react/css` and maps HIG roles
onto Fumadocs; it does not declare palette values.
- `@bloxwap/hig` is pinned to published npm version `0.2.0`. The former `@bloxwap/hig-tokens`,
`@bloxwap/hig-contracts`, `@bloxwap/hig-react` and `@bloxwap/hig-og-fonts` packages 404; their
surfaces are subpath exports of this one package. The lockfiles and [release record](../../docs/hig-adoption/registry-release.json) retain the registry integrity hash.
`app/global.css` imports `@bloxwap/hig/tokens/css` and `@bloxwap/hig/react/css` and maps HIG roles
onto Fumadocs; it does not declare palette values. Next lists `@bloxwap/hig` in `transpilePackages`.
- Radius, type, motion and density roles (`--hig-radius-*`, `--hig-type-*`, `--hig-motion-*`,
`--hig-density-*`) and the font roles come from `@bloxwap/hig-tokens/css`; the docs declare none.
`--hig-density-*`) and the font roles come from `@bloxwap/hig/tokens/css`; the docs declare none.
`npm run lint` (`scripts/hig-tokens.mjs --check`) fails on registry version or integrity changes, local role declarations
or retired role names, copied palettes, `.btn`-style primitive copies or font binaries in the docs source.
- Buttons, install tabs/copy, code-window copy, callouts, search error/empty states and the playground
Expand Down Expand Up @@ -94,8 +95,9 @@ chrome's `--cts-font`/`--cts-mono` at Bloxwap Sans/Mono, and sets the canvas
`theme.fontFamily` to Bloxwap Sans and `monoFamily`/`scaleFontFamily` (status
line, drawing labels and scales) to Bloxwap Mono. The standalone playground and
SDK presets retain their existing font defaults.
Social cards use the static Bloxwap Sans faces shipped in `@bloxwap/hig-og-fonts/fonts` (OFL), a
build-time devDependency; Satori cannot read the variable WOFF2 in `@bloxwap/font`.
Social cards use the static Bloxwap Sans faces shipped in `@bloxwap/hig/og-fonts/fonts` (OFL);
Satori cannot read the variable WOFF2 in `@bloxwap/font`. `lib/og-card.tsx` loads them with
`readOgFont()` from `@bloxwap/hig/og-fonts/node`.

HIG presentation, registry integrity, validation, exceptions and upgrade/rollback
instructions are recorded in [the adoption report](../../docs/hig-adoption/README.md).
8 changes: 4 additions & 4 deletions apps/docs/app/global.css
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
@import '@bloxwap/hig-tokens/css';
@import '@bloxwap/hig-react/css/base';
@import '@bloxwap/hig-react/css';
@import '@bloxwap/hig/tokens/css';
@import '@bloxwap/hig/react/css/base';
@import '@bloxwap/hig/react/css';

@theme inline {
--font-sans: var(--font-bloxwap-sans), ui-rounded, system-ui, sans-serif;
Expand Down Expand Up @@ -151,7 +151,7 @@ html > body[data-scroll-locked] {
}

/*
* HIG controls (@bloxwap/hig-react/css) in the docs shell. HIG reads --hig-font-sans/--hig-font-mono, which
* HIG controls (@bloxwap/hig/react/css) in the docs shell. HIG reads --hig-font-sans/--hig-font-mono, which
* resolve to the @bloxwap/font Next loader variables, so no family bridge is needed. Links and icons share the
* button. Dense docs regions opt into HIG compact density on fine pointers; coarse pointers keep 48px targets.
*/
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/components/callout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ReactNode } from 'react';
import { Callout as HigCallout } from '@bloxwap/hig-react/status';
import { Callout as HigCallout } from '@bloxwap/hig/react/status';

type FumadocsType = 'info' | 'note' | 'idea' | 'success' | 'warn' | 'warning' | 'error';
const severity = { info: 'info', note: 'note', idea: 'tip', success: 'success', warn: 'warning', warning: 'warning', error: 'error' } as const;
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/components/chart-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import { useEffect, useRef, useState, type ReactNode } from 'react';
import { ArrowUpRight, Maximize2, Minimize2 } from 'lucide-react';
import { Button } from '@bloxwap/hig-react/button';
import { Spinner } from '@bloxwap/hig-react/status';
import { Button } from '@bloxwap/hig/react/button';
import { Spinner } from '@bloxwap/hig/react/status';
import { assetUrl } from '@/lib/site';

/**
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/components/copy-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useEffect, useState } from 'react';
import { Check, Copy } from 'lucide-react';
import { IconButton } from '@bloxwap/hig-react/button';
import { IconButton } from '@bloxwap/hig/react/button';
import { useClipboard } from '@/lib/clipboard';

/** HIG icon button that copies `text`, with a check mark, a screen-reader status and visible failure recovery. */
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/components/hero-code.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import type { ReactNode } from 'react';
import { CodeWindow } from '@bloxwap/hig-react/recipes/code-window';
import { CodeWindow } from '@bloxwap/hig/react/recipes/code-window';
import { CopyButton } from './copy-button';

/*
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/components/install-command.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { Tabs } from '@bloxwap/hig-react/navigation';
import { InstallCommand as HigInstallCommand } from '@bloxwap/hig-react/recipes/install-command';
import { Tabs } from '@bloxwap/hig/react/navigation';
import { InstallCommand as HigInstallCommand } from '@bloxwap/hig/react/recipes/install-command';
import { useClipboard } from '@/lib/clipboard';

const managers = [
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/components/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
SearchDialogIcon, SearchDialogInput, SearchDialogList, SearchDialogOverlay,
type SharedProps,
} from 'fumadocs-ui/components/dialog/search';
import { Alert, EmptyState } from '@bloxwap/hig-react/status';
import { Alert, EmptyState } from '@bloxwap/hig/react/status';
import { assetUrl } from '@/lib/site';

function NoResults() {
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/lib/og-card.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { readOgFont } from '@bloxwap/hig-og-fonts/node';
import { tokens as hig } from '@bloxwap/hig-tokens';
import { readOgFont } from '@bloxwap/hig/og-fonts/node';
import { tokens as hig } from '@bloxwap/hig/tokens';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { ImageResponse } from 'next/og';
Expand Down
3 changes: 2 additions & 1 deletion apps/docs/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export default withMDX({
reactStrictMode: true,
// Resolve both this app and the local chart package from the workspace root.
turbopack: { root: new URL('../..', import.meta.url).pathname },
transpilePackages: ['@bloxwap/chart'],
// Unified HIG package. Subpath imports replace the retired @bloxwap/hig-* names.
transpilePackages: ['@bloxwap/chart', '@bloxwap/hig'],
// Type checking runs through tsrs in `npm run check` (prebuild), so Next skips its own tsc pass.
typescript: { ignoreBuildErrors: true },
});
5 changes: 1 addition & 4 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@
"dependencies": {
"@bloxwap/chart": "0.4.0",
"@bloxwap/font": "0.1.2",
"@bloxwap/hig-contracts": "0.1.0",
"@bloxwap/hig-react": "0.1.0",
"@bloxwap/hig-tokens": "0.1.0",
"@bloxwap/hig": "0.2.0",
"fumadocs-core": "16.17.7",
"fumadocs-mdx": "15.4.9",
"fumadocs-ui": "16.17.7",
Expand All @@ -29,7 +27,6 @@
"react-dom": "^19.3.1"
},
"devDependencies": {
"@bloxwap/hig-og-fonts": "0.1.0",
"@tailwindcss/postcss": "^4.3.3",
"@types/mdx": "^2.0.14",
"@types/node": "^24.19.2",
Expand Down
80 changes: 57 additions & 23 deletions apps/docs/scripts/hig-tokens.mjs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
// HIG adoption guard for the docs site. Tokens, roles, CSS and components come from published
// @bloxwap/hig-* registry packages. @bloxwap/hig-tokens/css emits the radius, type,
// motion and density roles, so this script only derives the canvas-compatible sRGB chart theme from the
// typed manifest, and its --check fails CI when copies of HIG sources or role declarations creep back in.
// HIG adoption guard for the docs site. Tokens, roles, CSS and components come from the published
// @bloxwap/hig package. @bloxwap/hig/tokens/css emits the radius, type, motion and density roles, so
// this script only derives the canvas-compatible sRGB chart theme from the typed manifest, and its
// --check fails CI when copies of HIG sources or role declarations creep back in.
import { readFile, readdir } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { createRequire } from 'node:module';
import assert from 'node:assert/strict';
import { tokens } from '@bloxwap/hig-tokens';
import { tokens } from '@bloxwap/hig/tokens';

const require = createRequire(import.meta.url);
export const manifest = tokens;
export const tokensCssPath = require.resolve('@bloxwap/hig-tokens/css');
export const tokensCssPath = require.resolve('@bloxwap/hig/tokens/css');
export const colors = Object.fromEntries(Object.entries(manifest.colors).map(([key, value]) => [key, value.srgb]));
/** CSS value of a role as a modern browser computes it from @bloxwap/hig-tokens/css. */
/** CSS value of a role as a modern browser computes it from @bloxwap/hig/tokens/css. */
export const cssColor = (role) => manifest.colors[role].oklch ?? manifest.colors[role].srgb;
export const chartTheme = {
theme: { background: colors.surface, textColor: colors.text, borderColor: colors.border, fontFamily: '"Bloxwap Sans", system-ui, sans-serif', monoFamily: '"Bloxwap Mono", monospace', scaleFontFamily: '"Bloxwap Mono", monospace' },
Expand All @@ -21,29 +21,63 @@ export const chartTheme = {
series: { upColor: colors.buy, downColor: colors.sell, wickUpColor: colors.buy, wickDownColor: colors.sell, borderUpColor: colors.buy, borderDownColor: colors.sell },
};

const retiredPackages = ['tokens', 'contracts', 'react', 'native', 'native-fonts', 'chart', 'feedback', 'og-fonts'].map((name) => `@bloxwap/hig-${name}`);
/** bun.lock is JSON with trailing commas. */
const parseLock = (text) => JSON.parse(text.replace(/,(\s*[}\]])/g, '$1'));

if (process.argv.includes('--check')) {
const release = JSON.parse(await readFile(new URL('../../../docs/hig-adoption/registry-release.json', import.meta.url), 'utf8'));
const lock = JSON.parse(await readFile(new URL('../../../package-lock.json', import.meta.url), 'utf8'));
const docsPackage = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
for (const name of ['tokens', 'contracts', 'react', 'og-fonts']) {
const packageName = `@bloxwap/hig-${name}`;
const field = name === 'og-fonts' ? 'devDependencies' : 'dependencies';
assert.equal(docsPackage[field][packageName], release.version, `${packageName} must use the coordinated registry version`);
const locked = lock.packages[`node_modules/${packageName}`];
assert.equal(locked.version, release.version, `${packageName} lockfile version drifted`);
assert.equal(locked.resolved, `https://registry.npmjs.org/${packageName}/-/hig-${name}-${release.version}.tgz`, `${packageName} must resolve from npm`);
assert.equal(locked.resolved, release.packages[packageName].resolved);
assert.equal(locked.integrity, release.packages[packageName].integrity, `${packageName} registry integrity drifted`);
assert.equal(JSON.parse(await readFile(new URL(`../../../node_modules/${packageName}/package.json`, import.meta.url), 'utf8')).version, release.version, `Installed ${packageName} is not ${release.version}; run npm ci`);
const root = new URL('../../../', import.meta.url);
const release = JSON.parse(await readFile(new URL('docs/hig-adoption/registry-release.json', root), 'utf8'));
const lock = JSON.parse(await readFile(new URL('package-lock.json', root), 'utf8'));
const bunLock = parseLock(await readFile(new URL('bun.lock', root), 'utf8'));
const rootPackage = JSON.parse(await readFile(new URL('package.json', root), 'utf8'));
const manifests = [new URL('package.json', root)];
for (const pattern of rootPackage.workspaces) {
const dir = new URL(pattern.replace('*', ''), root);
for (const entry of await readdir(dir, { withFileTypes: true })) {
const manifestUrl = new URL(`${entry.name}/package.json`, dir);
if (entry.isDirectory() && existsSync(manifestUrl)) manifests.push(manifestUrl);
}
}
for (const manifestUrl of manifests) {
const manifestJson = JSON.parse(await readFile(manifestUrl, 'utf8'));
for (const name of retiredPackages) {
assert.equal(manifestJson.dependencies?.[name], undefined, `${name} is retired; depend on @bloxwap/hig (${manifestUrl.pathname})`);
assert.equal(manifestJson.devDependencies?.[name], undefined, `${name} is retired; depend on @bloxwap/hig (${manifestUrl.pathname})`);
}
}
const packageName = '@bloxwap/hig';
const chartVersion = JSON.parse(await readFile(new URL('packages/chart/package.json', root), 'utf8')).version;
// Published 0.2.0 peers @bloxwap/chart ^0.3.0, which excludes this workspace's 0.4.0. The override
// keeps that required peer on the workspace package so install does not fetch a second chart.
assert.equal(rootPackage.overrides?.[packageName]?.['@bloxwap/chart'], chartVersion, 'root override must point the HIG chart peer at the workspace chart version');
assert.equal(bunLock.overrides?.[packageName]?.['@bloxwap/chart'], chartVersion, 'bun.lock override must match the workspace chart version');
const docsPackage = JSON.parse(await readFile(new URL('apps/docs/package.json', root), 'utf8'));
const pinned = release.packages[packageName];
assert.equal(docsPackage.dependencies[packageName], release.version, `${packageName} must use the coordinated registry version`);
assert.equal(docsPackage.devDependencies?.[packageName], undefined, `${packageName} is a direct dependency, not a second devDependency`);
const locked = lock.packages[`node_modules/${packageName}`];
assert.equal(locked.version, release.version, `${packageName} lockfile version drifted`);
assert.equal(locked.resolved, `https://registry.npmjs.org/@bloxwap/hig/-/hig-${release.version}.tgz`, `${packageName} must resolve from npm`);
assert.equal(locked.resolved, pinned.resolved);
assert.equal(locked.integrity, pinned.integrity, `${packageName} registry integrity drifted`);
const bunEntry = bunLock.packages[packageName];
assert.ok(bunEntry, `${packageName} is missing from bun.lock`);
assert.equal(bunEntry[0], `${packageName}@${release.version}`, `${packageName} bun.lock version drifted`);
assert.equal(bunEntry.at(-1), pinned.integrity, `${packageName} bun.lock integrity drifted`);
for (const name of retiredPackages) {
assert.equal(lock.packages[`node_modules/${name}`], undefined, `${name} remains in package-lock.json`);
assert.equal(bunLock.packages[name], undefined, `${name} remains in bun.lock`);
}
assert.ok(!existsSync(new URL('../styles/', import.meta.url)), 'styles/ is gone: radius/type/motion/density roles come from @bloxwap/hig-tokens/css');
assert.equal(JSON.parse(await readFile(new URL(`node_modules/${packageName}/package.json`, root), 'utf8')).version, release.version, `Installed ${packageName} is not ${release.version}; run bun install --frozen-lockfile`);
assert.ok(!existsSync(new URL('../styles/', import.meta.url)), 'styles/ is gone: radius/type/motion/density roles come from @bloxwap/hig/tokens/css');
// No copied palettes, role declarations, token snapshots, primitive classes or font binaries in the docs shell.
const forbidden = [
[/--(hig|bloxwap)-[\w-]+\s*:\s*(#|oklch\(|rgb|color\()/, 'copied palette declaration'],
[/--hig-(radius|type|motion|density|font|duration|control)-[\w-]+\s*:/, 'local HIG role declaration (use the @bloxwap/hig-tokens/css role)'],
[/--hig-(radius|type|motion|density|font|duration|control)-[\w-]+\s*:/, 'local HIG role declaration (use the @bloxwap/hig/tokens/css role)'],
[/var\(--hig-(duration|control)-|var\(--hig-type-[a-z]+-line\)/, 'retired generated role name (use --hig-motion-duration-*, --hig-density-*-control, --hig-type-*-line-height)'],
[/vendor\/hig\/|tokens\.generated\.css|@workspace\/(tokens|ui)/, 'obsolete token snapshot import'],
[/(^|[\s"'.])btn(--[\w-]+)?\b(?![-\w])/m, 'duplicate .btn primitive (use @bloxwap/hig-react Button or .hig-button)'],
[/(^|[\s"'.])btn(--[\w-]+)?\b(?![-\w])/m, 'duplicate .btn primitive (use @bloxwap/hig/react Button or .hig-button)'],
];
async function checkSurface(dir) {
for (const entry of await readdir(dir, { withFileTypes: true })) {
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/scripts/measure-hig.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const afterLock = lockPackages(await readFile('package-lock.json', 'utf8'));
const docsDeps = (text) => Object.keys({ ...JSON.parse(text).dependencies, ...JSON.parse(text).devDependencies });
const release = JSON.parse(await readFile('docs/hig-adoption/registry-release.json', 'utf8'));
// Generated token adapters: hig.generated.css (copied snapshot, step 1), hig-roles.generated.css (step 2), none since
// @bloxwap/hig-tokens/css emits the roles itself.
// @bloxwap/hig/tokens/css emits the roles itself.
const generated = (ref) => ['hig.generated.css', 'hig-roles.generated.css'].reduce((sum, file) => { try { return sum + Buffer.byteLength(git('show', `${ref}:apps/docs/styles/${file}`)); } catch { return sum; } }, 0);

console.log(JSON.stringify({
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/scripts/prepare-demo.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ await rm(output, { recursive: true, force: true });
await mkdir(new URL('demo/', output), { recursive: true });
await Promise.all([
cp(new URL('apps/playground/index.html', repository), new URL('demo/index.html', output)),
cp(tokensCssPath, new URL('assets/hig-tokens.css', output)), // @bloxwap/hig-tokens/css, unmodified
cp(tokensCssPath, new URL('assets/hig-tokens.css', output)), // @bloxwap/hig/tokens/css, unmodified
cp(new URL('packages/chart/dist/', repository), new URL('dist/', output), { recursive: true }),
]);

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/scripts/test-hig-browser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ try {
assert.deepEqual(presentation.legacy, []);
assert.match(presentation.motion, /^0(m?s)$/);
assert.equal(presentation.transform, 'none');
record('@bloxwap/hig-tokens palette, HIG controls in Bloxwap typography, no legacy aliases, reduced motion');
record('@bloxwap/hig/tokens palette, HIG controls in Bloxwap typography, no legacy aliases, reduced motion');
await page.getByRole('tab', { name: 'npm', exact: true }).click();
await page.getByRole('tab', { name: 'bun', exact: true }).click();
if (evidence) { await mkdir(evidence, { recursive: true }); await page.screenshot({ path: `${evidence}/after-home.png`, fullPage: true }); }
Expand Down
Loading