diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 897c334c..2e5e61bf 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -70,6 +70,14 @@ jobs: working-directory: apps/docs run: bun run build + - name: Install browser for documentation checks + working-directory: apps/docs + run: bunx playwright install --with-deps chromium + + - name: Test production documentation interactions + working-directory: apps/docs + run: bun run test:browser + - name: Check documentation links uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2 with: diff --git a/.gitignore b/.gitignore index 0ad297e8..806fd60d 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ apps/docs/tsconfig.tsbuildinfo # a funded key must never be committable. .env .env.* + +apps/docs/test-results/ +apps/docs/playwright-report/ diff --git a/apps/docs/README.md b/apps/docs/README.md index c67e8f5f..0b0b8f9d 100644 --- a/apps/docs/README.md +++ b/apps/docs/README.md @@ -8,17 +8,18 @@ from the SDK package published from `packages/hyperliquid/dist`. ## Design system -The design follows the Bloxwap monorepo's documentation site at commit `15201923b`: +The shared presentation uses HIG 0.1.0 candidate at commit +`1a776b9326c6ba03702894943b3edbfd178c4b40` via the approved plain CSS distribution. -- `app/brand-tokens.css` is a verbatim snapshot of `packages/ui/src/styles/tokens.generated.css`. -- Fumadocs colors, typography, navigation, and forced dark mode follow `workers/docs/src/app/global.css` and its layouts. +- `@bloxwap/hig-docs-css` is a private local package; `vendor/hig/README.md` records provenance, hashes and update instructions. +- HIG tokens/CSS supply colors, controls, geometry and states; `app/global.css` adapts them to Fumadocs and the market playground. +- Fumadocs owns routing, navigation, search and MDX adapters; dark mode remains forced. - The app icon and favicon in `public/logos/` preserve the original Bloxwap artwork and metadata. -- Bloxwap Sans body text and Bloxwap Mono code use the self-hosted Next.js exports from `@bloxwap/font@0.1.2`. -- Space Grotesk Bold headings retain the local asset and OFL notice in `public/fonts/`. -- Static Open Graph font instances and their license/provenance are documented in `public/fonts/README.md`. +- Headings/body use Bloxwap Sans and code uses Bloxwap Mono from self-hosted `@bloxwap/font@0.1.2` Next.js exports. +- Static Open Graph font instances retain the license/provenance documented in `public/fonts/README.md`. -Update these snapshots from the monorepo when its design changes. Keep the artwork and font notices intact. -Building this site does not require a checkout of the monorepo or a request to a font CDN. +Read the [HIG migration evidence](design-review/hig/README.md) for validation, measurements, +exceptions and rollback. No monorepo checkout, font CDN or npm publication is required. The [historical design review](design-review/README.md) preserves the original findings and screenshots from 2026-09-26. @@ -55,6 +56,9 @@ NEXT_PUBLIC_BASE_PATH=/hyperliquid bun run docs:build NEXT_PUBLIC_BASE_PATH=/hyperliquid bun run docs:preview ``` +Run `bun run --cwd apps/docs test:browser` after the production build to test the static shell +and market playground interactions. Install Chromium with `bunx --cwd apps/docs playwright install chromium`. + Open `http://localhost:4173/hyperliquid/`. The preview serves only the static files in `apps/docs/out`. The build verifies every documentation page, internal link, asset, and anchor, plus the static JSON search index and `.nojekyll`. It also checks each page's social metadata and its distinct 1200 × 630 PNG card. diff --git a/apps/docs/app/brand-tokens.css b/apps/docs/app/brand-tokens.css deleted file mode 100644 index 1e180211..00000000 --- a/apps/docs/app/brand-tokens.css +++ /dev/null @@ -1,172 +0,0 @@ -/* GENERATED by @workspace/tokens (bun run gen:css) — do not edit by hand. */ - -/* sRGB fallback brand colors */ -:root { - --bloxwap-green: #00ff3f; - --bloxwap-blue: #35b5ff; - --bloxwap-purple: #b300ff; - --bloxwap-pink: #ff479c; - --bloxwap-yellow: #fffb38; - --bloxwap-gold: #ffc83d; - --bloxwap-black: #0a0a0a; -} - -.dark { - --bloxwap-green: #00ff3f; - --bloxwap-blue: #35b5ff; - --bloxwap-purple: #b300ff; - --bloxwap-pink: #ff479c; - --bloxwap-yellow: #fffb38; - --bloxwap-gold: #ffc83d; - --bloxwap-black: #0a0a0a; -} - -/* Display P3 wide-gamut brand colors — more vibrant on supported displays */ -@supports (color: color(display-p3 1 1 1)) { - :root { - --bloxwap-green: color(display-p3 0 1 0.24); - --bloxwap-blue: color(display-p3 0.18 0.72 1); - --bloxwap-purple: color(display-p3 0.7 0 1); - --bloxwap-pink: color(display-p3 1 0.24 0.62); - --bloxwap-yellow: color(display-p3 1 0.98 0.18); - --bloxwap-gold: color(display-p3 1 0.78 0.2); - --bloxwap-black: color(display-p3 0.04 0.04 0.04); - } - - .dark { - --bloxwap-green: color(display-p3 0 1 0.24); - --bloxwap-blue: color(display-p3 0.18 0.72 1); - --bloxwap-purple: color(display-p3 0.7 0 1); - --bloxwap-pink: color(display-p3 1 0.24 0.62); - --bloxwap-yellow: color(display-p3 1 0.98 0.18); - --bloxwap-gold: color(display-p3 1 0.78 0.2); - --bloxwap-black: color(display-p3 0.04 0.04 0.04); - } -} - -@theme { - --color-bloxwap-green: var(--bloxwap-green); - --color-bloxwap-blue: var(--bloxwap-blue); - --color-bloxwap-purple: var(--bloxwap-purple); - --color-bloxwap-pink: var(--bloxwap-pink); - --color-bloxwap-yellow: var(--bloxwap-yellow); - --color-bloxwap-gold: var(--bloxwap-gold); - --color-bloxwap-black: var(--bloxwap-black); -} - -/* Semantic (shadcn/Radix) palette */ -:root { - --radius: 0.625rem; - color-scheme: dark; - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); -} - -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); -} - -/* Type scale — additive: ONLY the steps Tailwind's own ladder stops short of */ -@theme { - --text-3xs: 0.625rem; - --text-3xs--line-height: 0.875rem; - --text-2xs: 0.6875rem; - --text-2xs--line-height: 1rem; -} diff --git a/apps/docs/app/global.css b/apps/docs/app/global.css index 751bdd70..7d8abb8c 100644 --- a/apps/docs/app/global.css +++ b/apps/docs/app/global.css @@ -1,7 +1,8 @@ @import "tailwindcss"; @import "fumadocs-ui/css/neutral.css"; @import "fumadocs-ui/css/preset.css"; -@import "./brand-tokens.css"; +@import "@bloxwap/hig-docs-css/tokens"; +@import "@bloxwap/hig-docs-css/styles"; @source "../node_modules/fumadocs-ui/dist/**/*.js"; @@ -10,36 +11,35 @@ --font-mono: var(--font-bloxwap-mono), ui-monospace, monospace; } -/* - * Semantic layer. Components below reference only these names, never the brand primitives in - * brand-tokens.css (generated by the monorepo's token package) or the two parallel palettes it - * aliases. The site is dark-only by brand decision (see components/provider.tsx), so this layer has - * a single mode. - * - * Bloxwap green is used at full brand intensity everywhere it appears, as text, line or fill; - * softening it drifts it toward mint and off-brand. The secondary syntax hues (pink, blue) are - * desaturated for code text — mixed toward the text color rather than faded toward the surface, - * which keeps them above 4.5:1 on the code window. - */ +/* Host aliases adapt Fumadocs and the SDK demo to the pinned HIG distribution. */ :root { - --color-surface: var(--bloxwap-black); - --color-surface-raised: var(--card); - --color-text: var(--foreground); - --color-text-muted: var(--muted-foreground); - /* Quietest gray that still clears WCAG AA 4.5:1 on the surface (bloxwap.com's footer base). */ - --color-text-quiet: #878d96; - --color-line: var(--border); - /* A fill one step above whatever overlay it sits in (dialogs use --popover, the same gray as - --color-surface-raised, so raised fills vanish inside them). */ - --color-fill-subtle: color-mix(in oklab, var(--popover) 88%, var(--color-text)); - --color-accent: var(--bloxwap-green); - --color-accent-on: var(--bloxwap-black); - --color-code-keyword: color-mix(in oklab, var(--bloxwap-pink) 70%, var(--color-text)); - --color-code-string: var(--color-accent); - --color-code-type: color-mix(in oklab, var(--bloxwap-blue) 60%, var(--color-text)); + --color-surface: var(--hig-background); + --color-surface-raised: var(--hig-surface); + --color-text: var(--hig-text); + --color-text-muted: var(--hig-muted); + --color-text-quiet: var(--hig-faint); + --color-line: var(--hig-border); + --color-fill-subtle: var(--hig-nested); + --color-accent: var(--hig-action); + --color-accent-on: var(--hig-onAction); + --color-code-keyword: var(--hig-warning); + --color-code-string: var(--hig-action); + --color-code-type: var(--hig-info); + --popover: var(--hig-surface); + --popover-foreground: var(--hig-text); + --muted: var(--hig-nested); + --radius: 10px; + --radius-md: 10px; + --radius-xl: 14px; --ease-out: cubic-bezier(0.2, 0, 0, 1); --duration-interactive: 160ms; } +@theme { + --text-3xs: 0.625rem; + --text-3xs--line-height: 0.875rem; + --text-2xs: 0.75rem; + --text-2xs--line-height: 1.125rem; +} #nd-sidebar { border-color: var(--color-line); @@ -165,7 +165,7 @@ textarea:focus-visible { } .hero h1 { margin: --spacing(7) 0 --spacing(5); - font-family: var(--font-docs-display), var(--font-sans); + font-family: var(--font-sans); font-size: clamp(50px, 5.5vw, 72px); line-height: 1.08; font-weight: 700; @@ -387,7 +387,7 @@ textarea:focus-visible { text-align: center; } .section-intro h2 { - font-family: var(--font-docs-display), var(--font-sans); + font-family: var(--font-sans); font-size: var(--text-3xl); font-weight: 700; line-height: 1.2; @@ -592,7 +592,7 @@ textarea:focus-visible { align-items: center; margin: --spacing(2) 0 --spacing(2); font-size: var(--text-xl); - font-family: var(--font-docs-display), var(--font-sans); + font-family: var(--font-sans); font-weight: 700; letter-spacing: -0.02em; } @@ -613,12 +613,9 @@ textarea:focus-visible { line-height: 1.7; } /* Site footer, ported from bloxwap.com and aligned to the header column like .landing. */ -/* Full-bleed so its fill hides the page scrolling beneath it; the padding keeps the content in the - header's column. */ +/* Footer follows the content so it cannot cover the playground or keyboard targets. */ .site-footer { - position: sticky; - bottom: 0; - z-index: 10; + position: static; width: 100%; padding: --spacing(8) max(--spacing(4), calc((100% - var(--fd-layout-width, 1400px)) / 2 + --spacing(4))); background: var(--color-surface); @@ -736,7 +733,7 @@ textarea:focus-visible { max-width: 73rem; } #nd-page h1 { - font-family: var(--font-docs-display), var(--font-sans); + font-family: var(--font-sans); font-size: clamp(1.875rem, 3vw, 2.5rem); font-weight: 700; letter-spacing: -0.035em; @@ -749,14 +746,14 @@ textarea:focus-visible { } /* Trust pages (about, contact, privacy, 404): a readable article column inside the landing width. */ .trust-page h1 { - font-family: var(--font-docs-display), var(--font-sans); + font-family: var(--font-sans); font-size: clamp(1.875rem, 3vw, 2.5rem); font-weight: 700; letter-spacing: -0.035em; line-height: 1.2; } .prose h2 { - font-family: var(--font-docs-display), var(--font-sans); + font-family: var(--font-sans); font-size: var(--text-2xl); font-weight: 700; letter-spacing: -0.02em; @@ -1013,7 +1010,9 @@ textarea:focus-visible { } } @media (prefers-reduced-motion: reduce) { - .hero-copy > *, + .hero-copy > .eyebrow, + .hero-copy > h1, + .hero-copy > :not(.eyebrow, h1), .hero .code-window, .search-skeleton > span { animation: none; @@ -1032,3 +1031,31 @@ textarea:focus-visible { transform: none; } } + +/* HIG comfortable controls, using the host font loader and Fumadocs adapters. */ +.hig-button { + font-family: var(--font-sans); +} +.install-tabs > button, +.playground-options button { + min-height: var(--hig-control); +} +.code-copy, +.install-copy { + min-width: 48px; + min-height: 48px; +} +.copy-feedback { + font-size: 0.75rem; + color: var(--hig-muted); +} +.copy-feedback[data-error="true"] { + color: var(--hig-negative); +} +.hig-shell { + min-height: 100vh; +} +.search-error { + padding: var(--hig-space-lg); + color: var(--hig-negative); +} diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index 9169739f..1b4da4e6 100644 --- a/apps/docs/app/layout.tsx +++ b/apps/docs/app/layout.tsx @@ -1,7 +1,7 @@ import type { Metadata } from "next"; import type { ReactNode } from "react"; import { Provider } from "@/components/provider"; -import { display, mono, sans } from "@/lib/fonts"; +import { mono, sans } from "@/lib/fonts"; import { homeDescription, siteUrl, socialMetadata } from "@/lib/social"; import "./global.css"; @@ -14,7 +14,7 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: ReactNode }) { return ( - + {children} diff --git a/apps/docs/components/copy-button.tsx b/apps/docs/components/copy-button.tsx index 407b6e49..32569275 100644 --- a/apps/docs/components/copy-button.tsx +++ b/apps/docs/components/copy-button.tsx @@ -1,23 +1,46 @@ "use client"; import { Check, Copy } from "lucide-react"; -import { useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; -/** Copies `text` to the clipboard and confirms with a check for two seconds. */ +/** Clipboard failure is visible; timers cannot update an unmounted control. */ export function CopyButton({ text, label, className }: { text: string; label: string; className?: string }) { - const [copied, setCopied] = useState(false); + const [status, setStatus] = useState<"idle" | "copied" | "error">("idle"); const resetTimer = useRef>(null); + // Reset confirmation when the command changes. + // biome-ignore lint/correctness/useExhaustiveDependencies: text intentionally resets the confirmation + useEffect(() => { + setStatus("idle"); + return () => { + if (resetTimer.current) clearTimeout(resetTimer.current); + }; + }, [text]); const copy = async () => { - await navigator.clipboard.writeText(text); - setCopied(true); + try { + await navigator.clipboard.writeText(text); + setStatus("copied"); + } catch { + setStatus("error"); + } if (resetTimer.current) clearTimeout(resetTimer.current); - resetTimer.current = setTimeout(() => setCopied(false), 2000); + resetTimer.current = setTimeout(() => setStatus("idle"), 2000); }; return ( - + <> + + + {status === "copied" ? "Copied" : status === "error" ? "Copy failed. Select and copy manually." : ""} + + ); } diff --git a/apps/docs/components/install-command.tsx b/apps/docs/components/install-command.tsx index 8fe96a43..7c92ceb8 100644 --- a/apps/docs/components/install-command.tsx +++ b/apps/docs/components/install-command.tsx @@ -1,7 +1,7 @@ "use client"; -import { Check, Copy } from "lucide-react"; -import { type CSSProperties, useRef, useState } from "react"; +import { type CSSProperties, useState } from "react"; +import { CopyButton } from "./copy-button"; const installers = [ { name: "bun", command: "bun add @bloxwap/hyperliquid" }, @@ -12,21 +12,10 @@ const installers = [ export function InstallCommand() { const [active, setActive] = useState(0); - const [copied, setCopied] = useState(false); - const resetTimer = useRef>(null); - - const copy = async () => { - await navigator.clipboard.writeText(installers[active].command); - setCopied(true); - if (resetTimer.current) clearTimeout(resetTimer.current); - resetTimer.current = setTimeout(() => setCopied(false), 2000); - }; - return (
-
@@ -35,29 +24,18 @@ export function InstallCommand() { ))} -
+
{installers[active].command} - +
); diff --git a/apps/docs/components/provider.tsx b/apps/docs/components/provider.tsx index b13a06b4..34a49974 100644 --- a/apps/docs/components/provider.tsx +++ b/apps/docs/components/provider.tsx @@ -4,16 +4,13 @@ import { RootProvider } from "fumadocs-ui/provider/next"; import type { ReactNode } from "react"; import SearchDialog from "./search"; -/** - * The docs are dark-only by brand decision, not by omission: Bloxwap's identity is built on neon - * accents over near-black, and the generated brand tokens in app/brand-tokens.css exist in that - * mode only. Offering light mode would first need light values from the brand source; the semantic - * color layer in app/global.css is where they would go, with no component changes. - */ +/** HIG dark shell; feedback remains muted and the OS reduced-motion setting is respected. */ export function Provider({ children }: { children: ReactNode }) { return ( - {children} +
+ {children} +
); } diff --git a/apps/docs/components/search.tsx b/apps/docs/components/search.tsx index f1aadf54..e77203b5 100644 --- a/apps/docs/components/search.tsx +++ b/apps/docs/components/search.tsx @@ -76,10 +76,16 @@ export default function DocsSearchDialog(props: SharedProps) { - (pending ? : props.onOpenChange(false)} />)} - /> + {query.error ? ( +

+ Search unavailable. Reload to retry, or use the navigation. +

+ ) : ( + (pending ? : props.onOpenChange(false)} />)} + /> + )} ); diff --git a/apps/docs/design-review/hig/README.md b/apps/docs/design-review/hig/README.md new file mode 100644 index 00000000..b80de547 --- /dev/null +++ b/apps/docs/design-review/hig/README.md @@ -0,0 +1,90 @@ +# HIG adoption evidence · 2026-10-13 + +Hyperliquid adopts the approved plain CSS path from +[bloxwap/hig #34](https://github.com/bloxwap/hig/issues/34) and the +[SDK migration kit](https://github.com/bloxwap/hig/blob/1a776b9326c6ba03702894943b3edbfd178c4b40/docs/migration/sdk-docs.md). + +Source baseline: Hyperliquid `4a312a211cd04e187d680111f47d2a1b1dea5ef1`. +HIG: 0.1.0 candidate, commit `1a776b9326c6ba03702894943b3edbfd178c4b40`. +Distribution provenance and SHA-256 hashes are in `../../vendor/hig/README.md`. +The candidate is a private local file package, without an npm publication. + +Fumadocs retains the page tree, mobile sidebar, search dialog/Orama index, +MDX code/callout adapters and all docs URLs. HIG supplies shared tokens and +CSS primitives; host aliases bridge the existing shell and SDK forms/states. +Bloxwap Sans/Mono remain pinned to 0.1.2, including script companions. +Space Grotesk and its loader/license are removed. Static OG font instances +remain necessary for Satori; their existing provenance/licenses are preserved. +The market playground keeps its SDK transport, feed, reconnect and cleanup logic. +Feedback is muted (no audio/haptic dependency); reduced motion uses OS CSS. + +## Measurement + +`before.json` and `after.json` count uncompressed exported CSS/JS, Next font +files, public TTFs and authored CSS. These are all export assets, not initial +page download sizes. Next's build cache can retain unused assets, so no +initial-load or tree-shaking claim is made from these totals. + +Before/after screenshots use Chromium, 1440×1000 desktop and 390×844 mobile, +with reduced motion enabled. Reproduce from the repository root against the +static preview with `node apps/docs/scripts/capture-hig.mjs before` (or `after`). +The baseline was rebuilt successfully after an initial transient Next.js +`Expected workStore to be initialized` prerender failure. + +## Validation + +Production build uses `NEXT_PUBLIC_BASE_PATH=/hyperliquid`, `output: export`, +trailing slashes and unoptimized images. Export verification covers all 13 docs +pages, 20 HTML files, 355 anchors, social cards, static search and agent mirrors. +The docs browser suite runs against this same export in CI. It covers keyboard +install selection, clipboard success/denial, search results/empty/offline states, +mobile sidebar/deep links, large text/RTL reflow, and the actual SDK subscription +path with deterministic WebSocket frames for payloads/asset changes/off/error/retry. +Mock frames prove host integration, not upstream availability or real market prices. +A separate unmocked browser session reached Live and received a BTC mid-price; +its timestamp and payload are recorded in `live-market.json`. +The footer now follows the content so it cannot cover the playground. +Reduced-motion selectors match the entrance-animation specificity so all hero +content respects the OS setting; the browser suite checks the computed animation. + +Local commands: `bun run check`, `bun run docs:check`, `bun run docs:build`, +`bun run build`, `bun run test:offline --timeout 30000`, `bun run --cwd apps/docs test`, and +`bun run --cwd apps/docs test:browser`. See the owning PR for final results. + +## Adoption matrix (owning repository scope) + +| Repository | Version / owning evidence | Status / remaining validation | +| --- | --- | --- | +| Hyperliquid | HIG 0.1.0 candidate; [PR #179](https://github.com/bloxwap/hyperliquid/pull/179) and committed evidence | Shared shell adoption; physical-device validation pending | +| Chart | Not changed by this PR | Owning PR/build/demo evidence required | +| Font | Not changed by this PR | Owning PR/build/tester evidence required | +| SFX | Not changed by this PR | Owning PR/build/sound-board evidence required | +| Haptic | Not changed by this PR | Owning PR/build/platform demo and physical tactile evidence required | + +Exceptions, review 2026-11-13: #32 has no npm release; replace the pinned local +CSS distribution when official packages ship. Fumadocs remains the host shell +adapter, rather than replacing its router/search with HIG's standalone catalog. +No physical devices, iOS hardware, VoiceOver/TalkBack, native tactile response, +or wide-gamut display fidelity can be certified on this Linux VM. Browser +viewport/RTL/font-size tests are explicitly browser checks. No production +settings, deployment steps, tags, releases or npm publications were changed. +HIG's upstream adoption ledger and issue checklist remain for its maintainers; +this PR only records the Hyperliquid row and does not claim all five migrated. + +Rollback: revert the owning squash commit to restore the baseline token CSS, +fonts and clipboard components. Docs content and SDK APIs have no migration diff. + +## Local results + +- Aggregate check, docs typecheck, lint and formatting: pass (existing lint warnings only). +- SDK build: pass, including published consumer/import identity verification. +- Offline SDK: 2,087 pass, 244 skip, zero failures; 76,722 assertions across 255 files. + Live endpoint tests are skipped by the suite's offline setting. The initial run + timed out in an existing HIP-3 schema snapshot at five seconds under VM memory + pressure; the complete rerun used Bun's 30-second timeout with unchanged assertions. +- Docs helpers: 10 pass, zero failures, 30 assertions. +- Production browser interactions: six pass, zero failures. +- Export CSS: 159,189 → 158,685 bytes. + JavaScript: 1,355,870 → 1,356,112 bytes. Public TTFs: 1,009,020 → 892,964 bytes. + Runtime dependencies: one private CSS package added, zero runtime JS packages added. + Development only: Playwright test runner and its two transitive packages added. diff --git a/apps/docs/design-review/hig/after-clients.png b/apps/docs/design-review/hig/after-clients.png new file mode 100644 index 00000000..9ff37450 Binary files /dev/null and b/apps/docs/design-review/hig/after-clients.png differ diff --git a/apps/docs/design-review/hig/after-home.png b/apps/docs/design-review/hig/after-home.png new file mode 100644 index 00000000..aa59777f Binary files /dev/null and b/apps/docs/design-review/hig/after-home.png differ diff --git a/apps/docs/design-review/hig/after-mobile.png b/apps/docs/design-review/hig/after-mobile.png new file mode 100644 index 00000000..3fc05ad4 Binary files /dev/null and b/apps/docs/design-review/hig/after-mobile.png differ diff --git a/apps/docs/design-review/hig/after.json b/apps/docs/design-review/hig/after.json new file mode 100644 index 00000000..60f5df2a --- /dev/null +++ b/apps/docs/design-review/hig/after.json @@ -0,0 +1,8 @@ +{ + "cssBytes": 158685, + "jsBytes": 1356112, + "fontBytes": 12965628, + "publicFontBytes": 892964, + "globalCssBytes": 26568, + "legacyTokenCssBytes": 0 +} diff --git a/apps/docs/design-review/hig/before-clients.png b/apps/docs/design-review/hig/before-clients.png new file mode 100644 index 00000000..6839b67b Binary files /dev/null and b/apps/docs/design-review/hig/before-clients.png differ diff --git a/apps/docs/design-review/hig/before-home.png b/apps/docs/design-review/hig/before-home.png new file mode 100644 index 00000000..3920e09d Binary files /dev/null and b/apps/docs/design-review/hig/before-home.png differ diff --git a/apps/docs/design-review/hig/before-mobile.png b/apps/docs/design-review/hig/before-mobile.png new file mode 100644 index 00000000..fcd8a428 Binary files /dev/null and b/apps/docs/design-review/hig/before-mobile.png differ diff --git a/apps/docs/design-review/hig/before.json b/apps/docs/design-review/hig/before.json new file mode 100644 index 00000000..a99cc9ad --- /dev/null +++ b/apps/docs/design-review/hig/before.json @@ -0,0 +1,8 @@ +{ + "cssBytes": 159189, + "jsBytes": 1355870, + "fontBytes": 12965628, + "publicFontBytes": 1009020, + "globalCssBytes": 26807, + "legacyTokenCssBytes": 5921 +} diff --git a/apps/docs/design-review/hig/live-market.json b/apps/docs/design-review/hig/live-market.json new file mode 100644 index 00000000..bc66eb20 --- /dev/null +++ b/apps/docs/design-review/hig/live-market.json @@ -0,0 +1,7 @@ +{ + "at": "2026-10-13T23:48:31.331Z", + "mocked": false, + "live": true, + "status": "Live", + "payload": "{\n \"coin\": \"BTC\",\n \"midPx\": \"83203.5\"\n}" +} diff --git a/apps/docs/design-review/hig/validation.txt b/apps/docs/design-review/hig/validation.txt new file mode 100644 index 00000000..26af5eac --- /dev/null +++ b/apps/docs/design-review/hig/validation.txt @@ -0,0 +1,69 @@ +Local Linux validation (Bun 1.4.2; Node 20.19.2 for the Playwright capture runner). +No physical-device certification is claimed. + +$ bun run check +ok src/signing/mod.ts 13 / 20 +ok src/api/info/client.ts 98 / 110 +ok src/api/info/_methods/allMids.ts 6 / 12 +ok src/api/exchange/_methods/order.ts 29 / 35 +ok src/api/subscription/_methods/allMids.ts 2 / 8 +ok src/api/explorer/_methods/explorerBlock.ts 2 / 12 +ok src/actions/order.ts 30 / 35 +All 9 import graph budgets are within limits, and 5 operation entry points are narrower than their clients and barrels. + +$ bun run docs:check +$ bun run check:content && fumadocs-mdx && next typegen && tsc --noEmit +$ bun run scripts/check-content.ts && bun run scripts/lint-markdown.ts +Fumadocs navigation covers all 13 pages; page metadata and local Markdown links are valid. +markdownlint: 18 files, no errors +[MDX] generated files in 33.61029500000018ms +[MDX] generated files in 13.30009199999995ms +Generating route types... +✓ Types generated successfully + +$ NEXT_PUBLIC_BASE_PATH=/hyperliquid bun run docs:build +└ ○ /sitemap.xml + + +○ (Static) prerendered as static content +● (SSG) prerendered as static HTML (uses generateStaticParams) + +Exported 13 Markdown mirrors. +Verified 13 docs pages, 20 HTML files, 1168 internal links/assets, 355 anchors, 14 unique social cards, static search, 22 agent-readiness artifacts, and .nojekyll (base path: /hyperliquid). + +$ bun run build +$ cd packages/hyperliquid && bun run build +$ bun run .dev/build/build.ts +Published consumer imports, identities, nonce sharing, signing ownership, and error types passed. +Published consumer imports, identities, nonce sharing, signing ownership, and error types passed. +Built @bloxwap/hyperliquid@0.6.0 into dist/ (313 JavaScript files, 314 declaration files rewritten). + +$ bun run test:offline --timeout 30000 +(skip) userTwapSliceFills +(skip) webData3 + + 2087 pass + 244 skip + 0 fail + 76722 expect() calls +Ran 2331 tests across 255 files. [113.53s] + +$ bun run --cwd apps/docs test +(pass) homeStructuredData > describes the SDK as free, MIT-licensed software published by the organization [0.10ms] +(pass) homeStructuredData > gives the organization a contact point and shares its @id with the root site [0.03ms] +(pass) homeStructuredData > serializes safely inside a script element [0.16ms] + + 10 pass + 0 fail + 30 expect() calls +Ran 10 tests across 1 file. [20.00ms] + +$ bun run --cwd apps/docs test:browser + ✓ 1 e2e/shell.spec.ts:3:1 › static shell, keyboard install and copy success/failure (902ms) + ✓ 2 e2e/shell.spec.ts:44:1 › search keyboard, results, empty state and deep link (2.6s) + ✓ 3 e2e/shell.spec.ts:57:1 › market SDK subscription, payload, asset change and off (1.3s) + ✓ 4 e2e/shell.spec.ts:85:1 › mobile navigation, large text and RTL reflow (1.3s) + ✓ 5 e2e/shell.spec.ts:99:1 › market loading, server rejection and retry (936ms) + ✓ 6 e2e/shell.spec.ts:128:1 › search offline error is visible (1.1s) + + 6 passed (10.4s) diff --git a/apps/docs/e2e/shell.spec.ts b/apps/docs/e2e/shell.spec.ts new file mode 100644 index 00000000..87a522f1 --- /dev/null +++ b/apps/docs/e2e/shell.spec.ts @@ -0,0 +1,134 @@ +import { expect, test } from "@playwright/test"; + +test("static shell, keyboard install and copy success/failure", async ({ page }) => { + const errors: string[] = []; + page.on("pageerror", (error) => errors.push(error.message)); + await page.addInitScript(() => { + Object.defineProperty(navigator, "clipboard", { + value: { + writeText: async (text: string) => { + (window as unknown as { copied: string }).copied = text; + }, + }, + configurable: true, + }); + }); + await page.goto("/hyperliquid/"); + await expect(page.locator(".hig-shell")).toHaveAttribute("data-hig-density", "comfortable"); + const npm = page.getByRole("button", { name: "npm", exact: true }); + await npm.focus(); + await page.keyboard.press("Enter"); + await expect(npm).toHaveAttribute("aria-pressed", "true"); + await page.getByRole("button", { name: "Copy install command" }).click(); + await expect(page.locator(".install-command [role=status]")).toHaveText("Copied"); + expect(await page.evaluate(() => (window as unknown as { copied: string }).copied)).toBe( + "npm install @bloxwap/hyperliquid", + ); + await page.evaluate(() => { + navigator.clipboard.writeText = async () => { + throw new Error("Denied"); + }; + }); + await page.getByRole("button", { name: "yarn", exact: true }).click(); + await page.getByRole("button", { name: "Copy install command" }).click(); + await expect(page.locator(".install-command [role=status]")).toContainText("Copy failed"); + expect(await page.locator(".hero-description").evaluate((element) => getComputedStyle(element).animationName)).toBe( + "none", + ); + const footer = await page.locator(".site-footer").boundingBox(); + const playground = await page.locator(".playground-section").boundingBox(); + expect(footer?.y).toBeGreaterThan((playground?.y ?? 0) + (playground?.height ?? 0)); + expect(errors).toEqual([]); +}); + +test("search keyboard, results, empty state and deep link", async ({ page }) => { + await page.goto("/hyperliquid/docs/clients/"); + await page.keyboard.press("Control+k"); + const input = page.getByRole("dialog").locator("input"); + await input.fill("transport"); + await expect(page.getByRole("dialog").getByRole("option").first()).toBeVisible(); + await input.fill("zzzzdoesnotexistzzzz"); + await expect(page.getByText("No matches. Try a method name", { exact: false })).toBeVisible(); + await page.keyboard.press("Escape"); + await expect(page.getByRole("dialog")).toBeHidden(); + await expect(page.locator("h1")).toContainText("Clients"); +}); + +test("market SDK subscription, payload, asset change and off", async ({ page }) => { + await page.routeWebSocket("wss://api.hyperliquid.xyz/ws", (socket) => { + const interval = setInterval( + () => socket.send(JSON.stringify({ channel: "allMids", data: { mids: { BTC: "65000", SOL: "150" } } })), + 250, + ); + socket.onClose(() => clearInterval(interval)); + socket.onMessage((raw) => { + const message = JSON.parse(String(raw)); + if (message.method === "unsubscribe") + socket.send(JSON.stringify({ channel: "subscriptionResponse", data: message })); + if (message.method === "subscribe") { + socket.send(JSON.stringify({ channel: "subscriptionResponse", data: message })); + socket.send(JSON.stringify({ channel: "allMids", data: { mids: { BTC: "65000", SOL: "150" } } })); + } + }); + }); + await page.goto("/hyperliquid/"); + await expect(page.locator(".playground-status")).toHaveAttribute("data-status", "off"); + await page.getByRole("button", { name: "Mid price", exact: true }).click(); + await expect(page.locator(".playground-status")).toHaveAttribute("data-status", "live"); + await expect(page.locator(".playground-json")).toContainText("65000"); + await page.getByRole("button", { name: "Solana", exact: true }).click(); + await expect(page.locator(".playground-json")).toContainText("150"); + await page.getByRole("button", { name: "Off", exact: true }).click(); + await expect(page.locator(".playground-status")).toHaveAttribute("data-status", "off"); +}); + +test("mobile navigation, large text and RTL reflow", async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto("/hyperliquid/docs/clients/"); + await page.getByRole("button", { name: "Open Sidebar" }).click(); + await expect(page.getByRole("link", { name: "Connect to Hyperliquid", exact: true }).last()).toBeVisible(); + await page.getByRole("link", { name: "Connect to Hyperliquid", exact: true }).last().click(); + await expect(page).toHaveURL(/\/docs\/transports\//); + await page.evaluate(() => { + document.documentElement.dir = "rtl"; + document.documentElement.style.fontSize = "20px"; + }); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); +}); + +test("market loading, server rejection and retry", async ({ page }) => { + let subscriptions = 0; + let rejectSubscription = () => {}; + await page.routeWebSocket("wss://api.hyperliquid.xyz/ws", (socket) => { + socket.onMessage((raw) => { + const message = JSON.parse(String(raw)); + if (message.method !== "subscribe") return; + subscriptions++; + if (subscriptions === 1) { + rejectSubscription = () => + socket.send( + JSON.stringify({ channel: "error", data: `Invalid subscription ${JSON.stringify(message.subscription)}` }), + ); + } else { + socket.send(JSON.stringify({ channel: "subscriptionResponse", data: message })); + socket.send(JSON.stringify({ channel: "allMids", data: { mids: { BTC: "65000" } } })); + } + }); + }); + await page.goto("/hyperliquid/"); + await page.getByRole("button", { name: "Mid price", exact: true }).click(); + await expect(page.locator(".playground-status")).toHaveAttribute("data-status", /connecting|reconnecting|waiting/); + await expect.poll(() => subscriptions).toBe(1); + rejectSubscription(); + await expect(page.locator(".playground-status")).toHaveAttribute("data-status", "error"); + await page.getByRole("button", { name: "Retry", exact: true }).click(); + await expect(page.locator(".playground-status")).toHaveAttribute("data-status", "live"); +}); + +test("search offline error is visible", async ({ page }) => { + await page.route("**/search-index.json", (route) => route.abort()); + await page.goto("/hyperliquid/docs/clients/"); + await page.keyboard.press("Control+k"); + await page.getByRole("dialog").locator("input").fill("transport"); + await expect(page.getByRole("alert")).toContainText("Search unavailable"); +}); diff --git a/apps/docs/lib/fonts.ts b/apps/docs/lib/fonts.ts index 8014411b..42bf2098 100644 --- a/apps/docs/lib/fonts.ts +++ b/apps/docs/lib/fonts.ts @@ -1,11 +1,2 @@ -import localFont from "next/font/local"; - export { BloxwapSans as sans } from "@bloxwap/font/sans"; export { BloxwapMono as mono } from "@bloxwap/font/mono"; - -export const display = localFont({ - src: "../public/fonts/SpaceGrotesk-Bold.ttf", - weight: "700", - variable: "--font-docs-display", - display: "swap", -}); diff --git a/apps/docs/package.json b/apps/docs/package.json index ff307823..3984c3c9 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -8,10 +8,12 @@ "build": "fumadocs-mdx && next build && bun run scripts/export-markdown.ts && bun run scripts/verify-export.ts", "check": "bun run check:content && fumadocs-mdx && next typegen && tsc --noEmit", "test": "bun test lib/", - "check:content": "bun run scripts/check-content.ts && bun run scripts/lint-markdown.ts" + "check:content": "bun run scripts/check-content.ts && bun run scripts/lint-markdown.ts", + "test:browser": "playwright test" }, "dependencies": { "@bloxwap/font": "0.1.2", + "@bloxwap/hig-docs-css": "file:vendor/hig", "@bloxwap/hyperliquid": "workspace:*", "animejs": "4.4.0", "fumadocs-core": "16.15.14", @@ -24,15 +26,16 @@ "rehype-raw": "7.0.0" }, "devDependencies": { - "@types/bun": "1.3.14", + "@playwright/test": "1.64.0", "@tailwindcss/postcss": "4.3.3", + "@types/bun": "1.3.14", "@types/mdx": "2.0.14", "@types/node": "26.1.1", "@types/react": "19.3.0", "@types/react-dom": "19.3.0", + "markdownlint": "0.41.1", "postcss": "8.5.28", "tailwindcss": "4.3.3", - "typescript": "5.9.3", - "markdownlint": "0.41.1" + "typescript": "5.9.3" } } diff --git a/apps/docs/playwright.config.ts b/apps/docs/playwright.config.ts new file mode 100644 index 00000000..c0529cf9 --- /dev/null +++ b/apps/docs/playwright.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "@playwright/test"; + +export default defineConfig({ + testDir: "./e2e", + workers: 1, + timeout: 120_000, + expect: { timeout: 30_000 }, + use: { baseURL: "http://127.0.0.1:4173", reducedMotion: "reduce" }, + webServer: { + command: "bun run preview", + url: "http://127.0.0.1:4173/hyperliquid/", + env: { NEXT_PUBLIC_BASE_PATH: "/hyperliquid" }, + reuseExistingServer: false, + }, +}); diff --git a/apps/docs/public/fonts/README.md b/apps/docs/public/fonts/README.md index 73bbd33a..05cdd0bc 100644 --- a/apps/docs/public/fonts/README.md +++ b/apps/docs/public/fonts/README.md @@ -3,7 +3,7 @@ Web body text uses Bloxwap Sans and code uses Bloxwap Mono from the exact `@bloxwap/font@0.1.2` dependency. The Next.js exports self-host variable weights 100–900, italics, and script companions without requests to Google Fonts. -Space Grotesk Bold remains the heading face, with its adjacent OFL notice. +Headings use Bloxwap Sans, following HIG typography roles. `BloxwapSans-Bold.ttf` (700) and `BloxwapSans-Black.ttf` (900) are static instances of `fonts/BloxwapSans/BloxwapSans-Variable.woff2` from the published diff --git a/apps/docs/public/fonts/SpaceGrotesk-Bold.ttf b/apps/docs/public/fonts/SpaceGrotesk-Bold.ttf deleted file mode 100644 index f8eb245d..00000000 Binary files a/apps/docs/public/fonts/SpaceGrotesk-Bold.ttf and /dev/null differ diff --git a/apps/docs/public/fonts/SpaceGrotesk-OFL.txt b/apps/docs/public/fonts/SpaceGrotesk-OFL.txt deleted file mode 100644 index d5666d70..00000000 --- a/apps/docs/public/fonts/SpaceGrotesk-OFL.txt +++ /dev/null @@ -1,93 +0,0 @@ -Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/docs/scripts/capture-hig.mjs b/apps/docs/scripts/capture-hig.mjs new file mode 100644 index 00000000..e170da8a --- /dev/null +++ b/apps/docs/scripts/capture-hig.mjs @@ -0,0 +1,16 @@ +import { chromium } from "@playwright/test"; +const browser = await chromium.launch({ headless: true }); +const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, reducedMotion: "reduce" }); +for (const [name, path] of [ + ["home", ""], + ["clients", "docs/clients/"], +]) { + await page.goto("http://127.0.0.1:4173/hyperliquid/" + path); + await page.evaluate(() => document.fonts.ready); + await page.screenshot({ path: `apps/docs/design-review/hig/${process.argv[2]}-${name}.png`, fullPage: true }); +} +await page.setViewportSize({ width: 390, height: 844 }); +await page.goto("http://127.0.0.1:4173/hyperliquid/docs/clients/"); +await page.evaluate(() => document.fonts.ready); +await page.screenshot({ path: `apps/docs/design-review/hig/${process.argv[2]}-mobile.png`, fullPage: true }); +await browser.close(); diff --git a/apps/docs/vendor/hig/LICENSE b/apps/docs/vendor/hig/LICENSE new file mode 100644 index 00000000..daec4c13 --- /dev/null +++ b/apps/docs/vendor/hig/LICENSE @@ -0,0 +1,5 @@ +MIT License + +Copyright (c) 2026 Bloxwap + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to inclusion of this notice. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. diff --git a/apps/docs/vendor/hig/README.md b/apps/docs/vendor/hig/README.md new file mode 100644 index 00000000..c56c67d9 --- /dev/null +++ b/apps/docs/vendor/hig/README.md @@ -0,0 +1,18 @@ +# HIG plain CSS distribution + +Pinned candidate: HIG 0.1.0, bloxwap/hig commit +`1a776b9326c6ba03702894943b3edbfd178c4b40` (main, 2026-10-13). + +The migration kit permits the plain CSS distribution for shared presentation. +No npm release exists; this private local package distributes the two upstream +CSS files unchanged, with their MIT license. It has no runtime dependencies. + +- `tokens.css`: `packages/tokens/generated/tokens.css`, SHA-256 + `1c60086af561070fdc1ee6056d6bc43bd8eed78614c318d466ee3850dc317609` +- `styles.css`: `packages/react/src/styles.css`, SHA-256 + `f0ef3dd51ae6a67a34bf32fc20a4949c5a39ec5b0de4ad2c3813a28c2cdfad49` + +Update from a reviewed HIG commit, replace both files byte for byte, update +these hashes/version and rerun the production export and browser tests. +Do not hand-edit this distribution. Consumer aliases live in app/global.css. +Replace this local distribution with official package imports after #32 ships. diff --git a/apps/docs/vendor/hig/package.json b/apps/docs/vendor/hig/package.json new file mode 100644 index 00000000..5a4632d8 --- /dev/null +++ b/apps/docs/vendor/hig/package.json @@ -0,0 +1,11 @@ +{ + "name": "@bloxwap/hig-docs-css", + "version": "0.1.0", + "private": true, + "license": "MIT", + "exports": { + "./tokens": "./tokens.css", + "./styles": "./styles.css" + }, + "sideEffects": ["*.css"] +} diff --git a/apps/docs/vendor/hig/styles.css b/apps/docs/vendor/hig/styles.css new file mode 100644 index 00000000..68367571 --- /dev/null +++ b/apps/docs/vendor/hig/styles.css @@ -0,0 +1,45 @@ +:root { --hig-control: 48px; --hig-gap: 12px; } +[data-hig-density='compact'] { --hig-control: 36px; --hig-gap: 8px; } +* { box-sizing: border-box; } +.hig-button { min-height: var(--hig-control); max-width:100%; padding:8px 20px; border:1px solid transparent; border-radius:9999px; font:600 1rem/1.5 'Bloxwap Sans',system-ui,sans-serif; background:var(--hig-action); color:var(--hig-onAction); cursor:pointer; overflow-wrap:anywhere; } +.hig-button[data-intent='secondary'],.hig-button[data-intent='ghost'] { background:var(--hig-nested);color:var(--hig-text); } +.hig-button[data-intent='destructive'] { background:var(--hig-negative);color:var(--hig-onAction); } +.hig-button[data-full] { width:100%; } +.hig-button:disabled { opacity:.5;cursor:default; } +.hig-button:hover:not(:disabled) { filter:brightness(.9); } +.hig-button:active:not(:disabled) { transform:scale(.98); } +:focus-visible { outline:2px solid var(--hig-focus);outline-offset:3px; } +.hig-stack,.hig-field { display:flex;flex-direction:column;gap:var(--hig-gap);min-width:0; } +.hig-inline { display:flex;flex-wrap:wrap;align-items:center;gap:var(--hig-gap); } +.hig-surface { padding:20px;border-radius:14px;background:var(--hig-surface);min-width:0; } +.hig-text { font-size:1rem;line-height:1.5; } +.hig-heading { line-height:1.25; } +.hig-input { width:100%;min-height:var(--hig-control);border-radius:10px;border:1px solid var(--hig-muted);padding:12px;background:var(--hig-nested);color:var(--hig-text);font:inherit; } +.hig-field label,.hig-group legend { font-weight:600; } +.hig-field p { margin:0;color:var(--hig-muted); } +.hig-field [role='alert'] { color:var(--hig-negative); } +.hig-choice { display:inline-flex;align-items:center;min-height:var(--hig-control);gap:8px;padding:4px 8px; } +.hig-choice input { accent-color:var(--hig-action);width:20px;height:20px; } +.hig-group { border:0;padding:0; } +.hig-link { color:var(--hig-action);text-decoration:underline;text-underline-offset:4px;overflow-wrap:anywhere; } +.hig-separator { border:0;border-top:1px solid var(--hig-border);width:100%; } +.hig-backdrop { position:fixed;inset:0;background:#000b;z-index:40; } +.hig-dialog { position:fixed;inset-block-start:50%;inset-inline-start:50%;transform:translate(-50%,-50%);max-width:min(560px,calc(100vw - 32px));width:100%;max-height:calc(100dvh - 32px);overflow:auto;padding:24px;border-radius:14px;background:var(--hig-surface);color:var(--hig-text);z-index:41;display:flex;flex-direction:column;gap:12px; } +.hig-popover { max-width:min(360px,calc(100vw - 32px));background:var(--hig-nested);color:var(--hig-text);padding:12px;border-radius:14px;z-index:50; } +.hig-menu-item { min-height:48px;padding:12px;border-radius:8px;cursor:pointer; } +.hig-menu-item[data-highlighted] { background:var(--hig-surface);outline:2px solid var(--hig-focus); } +.hig-menu-item[data-disabled] { opacity:.5; } +.hig-number { font-family:'Bloxwap Mono',monospace;font-variant-numeric:tabular-nums; } +.hig-row { display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;padding-block:12px; } +.hig-list { list-style:none;padding:0;margin:0; } +.hig-table-scroll { overflow-x:auto; } +table { border-collapse:collapse;width:100%; }th,td { padding:12px;text-align:start;border-bottom:1px solid var(--hig-border); } +.hig-badge { padding:4px 10px;border-radius:9999px;background:var(--hig-nested); } +.hig-avatar { display:inline-grid;place-items:center;width:40px;height:40px;border-radius:9999px;background:var(--hig-nested); } +.hig-skeleton { min-height:48px;background:var(--hig-nested);border-radius:10px; } +[data-severity='error'] { border-inline-start:3px solid var(--hig-negative); } +[data-severity='warning'] { border-inline-start:3px solid var(--hig-warning); } +@media(prefers-reduced-motion:reduce) { .hig-button:active:not(:disabled) {transform:none;} } +[data-hig-motion='reduced'] .hig-button:active:not(:disabled) {transform:none;} +.hig-button[role='tab'][aria-selected='false'] { background:var(--hig-nested); color:var(--hig-text); } +input[type='range'] { accent-color:var(--hig-action); min-height:48px; width:100%; } diff --git a/apps/docs/vendor/hig/tokens.css b/apps/docs/vendor/hig/tokens.css new file mode 100644 index 00000000..3b269981 --- /dev/null +++ b/apps/docs/vendor/hig/tokens.css @@ -0,0 +1,35 @@ +:root { color-scheme: dark; + --hig-background: #0a0a0a; + --hig-surface: #171717; + --hig-nested: #262626; + --hig-text: #fafafa; + --hig-muted: #a1a1a1; + --hig-faint: #858585; + --hig-border: #3a3a3a; + --hig-action: #00ff3f; + --hig-onAction: #0a0a0a; + --hig-focus: #00ff3f; + --hig-buy: #00ff3f; + --hig-sell: #ff5c5c; + --hig-positive: #00ff3f; + --hig-negative: #ff5c5c; + --hig-warning: #f5b300; + --hig-info: #35b5ff; + --hig-space-xs: 0.25rem; + --hig-space-sm: 0.5rem; + --hig-space-md: 0.75rem; + --hig-space-lg: 1rem; + --hig-space-xl: 1.25rem; + --hig-space-xxl: 1.5rem; + --hig-space-xxxl: 2rem; + --hig-space-section: 3rem; +} +@supports (color: color(display-p3 1 1 1)) { :root { + --hig-background: color(display-p3 0.04 0.04 0.04); + --hig-action: color(display-p3 0 1 0.24); + --hig-onAction: color(display-p3 0.04 0.04 0.04); + --hig-focus: color(display-p3 0 1 0.24); + --hig-buy: color(display-p3 0 1 0.24); + --hig-positive: color(display-p3 0 1 0.24); + --hig-info: color(display-p3 0.18 0.72 1); +} } diff --git a/biome.jsonc b/biome.jsonc index 73940f30..34f5f83d 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -35,7 +35,11 @@ "!apps/docs/next-env.d.ts", // Copied brand assets retain their original formatting and provenance metadata. "!apps/docs/public/logos", - "!apps/docs/app/brand-tokens.css" + "!apps/docs/vendor/hig", + "!.hig-source", + "!.hig-artifacts", + "!apps/docs/test-results", + "!apps/docs/playwright-report" ] }, "formatter": { diff --git a/bun.lock b/bun.lock index f9ec46fc..4bd3cd11 100644 --- a/bun.lock +++ b/bun.lock @@ -12,6 +12,7 @@ "name": "@bloxwap/hyperliquid-docs", "dependencies": { "@bloxwap/font": "0.1.2", + "@bloxwap/hig-docs-css": "file:vendor/hig", "@bloxwap/hyperliquid": "workspace:*", "animejs": "4.4.0", "fumadocs-core": "16.15.14", @@ -24,6 +25,7 @@ "rehype-raw": "7.0.0", }, "devDependencies": { + "@playwright/test": "1.64.0", "@tailwindcss/postcss": "4.3.3", "@types/bun": "1.3.14", "@types/mdx": "2.0.14", @@ -38,7 +40,7 @@ }, "packages/hyperliquid": { "name": "@bloxwap/hyperliquid", - "version": "0.5.0", + "version": "0.6.0", "dependencies": { "@noble/hashes": "2.2.0", "valibot": "1.4.2", @@ -272,6 +274,8 @@ "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], + "@playwright/test": ["@playwright/test@1.64.0", "", { "dependencies": { "playwright": "1.64.0" }, "bin": { "playwright": "cli.js" } }, "sha512-oTN9FNYuN6bqQMwiho6jJKLCG2/QOYCixGXuCjRmzOq23eODeukuY2XiAVoVpAECbuezMt1MHQjAiQxK8F+Y+g=="], + "@radix-ui/number": ["@radix-ui/number@1.1.3", "", {}, "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.7", "", {}, "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q=="], @@ -810,6 +814,10 @@ "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + "playwright": ["playwright@1.64.0", "", { "dependencies": { "playwright-core": "1.64.0" }, "bin": { "playwright": "cli.js" } }, "sha512-kVzTnYFYEyQ9RGzBjNCd2+yEqWmVbxl3AHPynv3qQ6JTjv4a+kwRcnqQwIpuP3NnrnIrE/Q6wWAIAvMfIE2/Yg=="], + + "playwright-core": ["playwright-core@1.64.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-T9r+MZkTECl2+oUcZ26YgZzTrYidTsIhYYkJix+6iDOymOb1FB2RLLSOxRGnZGv5DStt+aduIVbt+E8WygXYpg=="], + "postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="], "property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="], @@ -958,6 +966,8 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@bloxwap/hyperliquid-docs/@bloxwap/hig-docs-css": ["@bloxwap/hig-docs-css@file:apps/docs/vendor/hig", {}], + "@noble/curves/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], "@scure/bip32/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],