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
2 changes: 2 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:
- run: npm ci
- run: npm run docs:build
- run: npm run docs:check
- run: npm run lint
- run: npm run docs:test
- uses: actions/upload-pages-artifact@v5
with:
path: apps/docs/out
Expand Down
3 changes: 3 additions & 0 deletions apps/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,6 @@ chrome's `--cts-font`/`--cts-mono` at Bloxwap Sans/Mono, and sets the canvas
line, drawing labels and scales) to Bloxwap Mono. The standalone playground and
SDK presets retain their existing font defaults.
Static social-image font provenance is recorded in `fonts/README.md`.

HIG presentation, pinned candidate provenance, validation, exceptions and upgrade/rollback
instructions are recorded in [the adoption report](../../docs/hig-adoption/README.md).
121 changes: 52 additions & 69 deletions apps/docs/app/global.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
@import '../styles/tokens.generated.css';
@import '../vendor/hig/tokens.css';
@import '../styles/hig.generated.css';

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

#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;
font-weight: 600;
letter-spacing: -0.035em;
line-height: 1.2;
}

#nd-page > p {
font-size: 1.125rem;
line-height: 1.6;
font-size: var(--type-body-size);
line-height: var(--type-body-line);
}

.prose {
line-height: 1.75;
line-height: 1.5;
}

.prose h2 {
font-family: var(--font-docs-display), var(--font-sans);
font-family: var(--font-sans);
font-size: 1.375rem;
font-weight: 700;
font-weight: 600;
letter-spacing: -0.02em;
}

.prose h3 {
font-weight: 700;
font-weight: 600;
}

.prose a:not([data-card]):not(.not-prose a) {
Expand All @@ -109,7 +110,7 @@ html > body[data-scroll-locked] {

.prose [data-card='true'] h3 {
font-size: 1rem;
font-weight: 700;
font-weight: 600;
}

#nd-sidebar {
Expand Down Expand Up @@ -140,62 +141,14 @@ html > body[data-scroll-locked] {
border-radius: var(--radius-full);
transition: background-color var(--duration-fast) var(--ease-spring), color var(--duration-fast) var(--ease-spring);
font-size: 0.875rem;
font-weight: 700;
font-weight: 600;
}

.docs-app-link:hover {
background: var(--card);
color: var(--bloxwap-green);
}

/*
* Scale layer from the monorepo's plain-UI system (packages/plain-ui/scripts/gen-tokens-css.mjs,
* values from @workspace/tokens). The palette and radius steps come from tokens.generated.css;
* the shared --text-* names repeat Tailwind's identical rem values.
*/
:root {
--radius-2xl: 1rem;
--radius-full: 9999px;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 1.875rem;
--text-4xl: 2.25rem;
--text-5xl: 3rem;
--text-6xl: 3.75rem;
--space-xs: 0.25rem;
--space-sm: 0.5rem;
--space-md: 0.75rem;
--space-lg: 1rem;
--space-xl: 1.25rem;
--space-2xl: 1.5rem;
--space-3xl: 2rem;
--space-4xl: 2.5rem;
--space-5xl: 3rem;
--space-6xl: 4rem;
--duration-fast: 180ms;
--duration-base: 260ms;
--duration-slow: 320ms;
--ease-spring: cubic-bezier(0.22, 1, 0.36, 1);
--ease-standard: ease-out;
--icon-md: 16px;
--control-h: 3rem;
--control-h-sm: 2.25rem;
--control-h-md: 2.75rem;
--control-min-w: 6rem;
}

@media (prefers-reduced-motion: reduce) {
:root {
--duration-fast: 0ms;
--duration-base: 0ms;
--duration-slow: 0ms;
}
}

/*
* Buttons: plain-UI's .btn family (www button.tsx: pill, h-12, text-base semibold, pressed scale).
* The primary fill is brand green with black text, as in www's .nav-cta/.submit and the app's CTAs.
Expand All @@ -205,15 +158,16 @@ html > body[data-scroll-locked] {
align-items: center;
justify-content: center;
gap: var(--space-sm);
height: var(--control-h);
min-height: var(--control-h);
min-width: var(--control-min-w);
padding: 0 var(--space-xl);
border: 1px solid transparent;
border-radius: var(--radius-full);
background: var(--bloxwap-green);
color: var(--bloxwap-black);
font: 600 var(--text-base) / 1 var(--font-sans);
white-space: nowrap;
white-space: normal;
overflow-wrap: anywhere;
cursor: pointer;
text-decoration: none;
touch-action: manipulation;
Expand All @@ -240,7 +194,7 @@ html > body[data-scroll-locked] {
.brand img { flex: none; width: 28px; height: 28px; }
.brand-product { border-left: 1px solid var(--border); padding-left: var(--space-md); color: var(--muted-foreground); font-size: var(--text-sm); }
.hero h1, .principles h2, .start-grid h2, .guide-links h3, .not-found h1 {
font-family: var(--font-docs-display), var(--font-sans);
font-family: var(--font-sans);
}
/* Same box as the Fumadocs header (max-w --fd-layout-width, 16px padding), so content lines up with the nav. */
.home { width: 100%; max-width: var(--fd-layout-width, 1400px); padding: var(--space-5xl) var(--space-lg) 0; margin: 0 auto; }
Expand All @@ -249,9 +203,9 @@ html > body[data-scroll-locked] {
.hero-copy { min-width: 0; }
.eyebrow { display: flex; align-items: center; gap: var(--space-sm); color: var(--color-fd-muted-foreground); font: var(--text-2xs) / 1.5 var(--font-mono); letter-spacing: .09em; }
.status-dot { width: 6px; height: 6px; border-radius: var(--radius-full); background: var(--color-fd-primary); }
.hero h1 { margin: var(--space-xl) 0 0; font-size: clamp(var(--text-5xl), 4.4vw, 4rem); line-height: 1.07; font-weight: 700; letter-spacing: -.04em; }
.hero h1 { margin: var(--space-xl) 0 0; font-size: clamp(var(--text-5xl), 4.4vw, 4rem); line-height: 1.07; font-weight: 600; letter-spacing: -.04em; }
.hero h1 span { color: var(--color-fd-foreground); }
.hero-description { max-width: 35rem; margin-top: var(--space-xl); color: var(--color-fd-muted-foreground); line-height: 1.75; font-size: var(--text-lg); }
.hero-description { max-width: 35rem; margin-top: var(--space-xl); color: var(--color-fd-muted-foreground); line-height: 1.5; font-size: var(--text-lg); }
.hero-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-md); margin: var(--space-2xl) 0 var(--space-xl); }
.package-install { width: fit-content; max-width: 100%; margin-top: var(--space-2xl); }
.install-tabs { display: inline-flex; max-width: 100%; gap: var(--space-xs); padding: var(--space-xs); border-radius: var(--radius-full); background: var(--card); }
Expand All @@ -263,7 +217,7 @@ html > body[data-scroll-locked] {
.install-tabs button:hover { color: var(--foreground); }
.install-tabs button[aria-selected='true'] { background: var(--muted); color: var(--foreground); }
.install-command {
display: flex; align-items: center; gap: var(--space-lg); min-width: 0; margin-top: var(--space-md);
display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: var(--space-lg); min-width: 0; margin-top: var(--space-md);
padding: var(--space-xs) var(--space-xs) var(--space-xs) var(--space-xl); border-radius: var(--radius-full);
background: var(--card); font: 600 var(--text-sm) / 1.5 var(--font-mono);
}
Expand Down Expand Up @@ -330,7 +284,7 @@ button[data-search-full] kbd {
scroll-margin-top: 5rem;
}
.preview-intro { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-lg); margin-bottom: var(--space-2xl); }
.preview-intro h2 { margin: 0; font-size: var(--text-3xl); letter-spacing: -.04em; line-height: 1.2; font-weight: 700; }
.preview-intro h2 { margin: 0; font-size: var(--text-3xl); letter-spacing: -.04em; line-height: 1.2; font-weight: 600; }
.preview-intro > p:not(.eyebrow) { margin: 0; max-width: 25rem; color: var(--color-fd-muted-foreground); font-size: var(--text-sm); line-height: 1.7; }
.preview-intro--section { align-items: center; text-align: center; }
.preview-actions { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-xs); }
Expand Down Expand Up @@ -362,10 +316,10 @@ button[data-search-full] kbd {
padding: var(--space-xl);
}
.feature-number { color: var(--color-fd-primary); font: var(--text-2xs) var(--font-mono); }
.principles h2 { font-size: var(--text-base); font-weight: 700; margin: var(--space-md) 0 var(--space-sm); }
.principles h2 { font-size: var(--text-base); font-weight: 600; margin: var(--space-md) 0 var(--space-sm); }
.principles p { font-size: var(--text-sm); color: var(--color-fd-muted-foreground); line-height: 1.7; }
.start-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-6xl); padding: var(--space-5xl) 0 var(--space-6xl); }
.start-grid h2 { margin: var(--space-xl) 0; font-size: var(--text-3xl); letter-spacing: -.04em; line-height: 1.2; font-weight: 700; }
.start-grid h2 { margin: var(--space-xl) 0; font-size: var(--text-3xl); letter-spacing: -.04em; line-height: 1.2; font-weight: 600; }
.start-grid > div > p:last-child { color: var(--color-fd-muted-foreground); font-size: var(--text-sm); max-width: 25rem; line-height: 1.7; }
.guide-links { display: grid; gap: var(--space-md); }
.guide-links a {
Expand All @@ -376,7 +330,7 @@ button[data-search-full] kbd {
.guide-links a > span { font: var(--text-2xs) var(--font-mono); color: var(--color-fd-muted-foreground); }
.guide-links a > svg { flex: none; margin-left: auto; width: var(--icon-md); height: var(--icon-md); color: var(--color-fd-muted-foreground); transition: color var(--duration-fast) var(--ease-spring), transform var(--duration-fast) var(--ease-spring); }
.guide-links a:hover > svg { color: var(--color-fd-primary); transform: translateX(2px); }
.guide-links h3 { font-size: var(--text-sm); margin-bottom: var(--space-xs); font-weight: 700; transition: color var(--duration-fast) var(--ease-spring); }
.guide-links h3 { font-size: var(--text-sm); margin-bottom: var(--space-xs); font-weight: 600; transition: color var(--duration-fast) var(--ease-spring); }
.guide-links a:hover h3 { color: var(--color-fd-primary); }
.guide-links p { color: var(--color-fd-muted-foreground); font-size: var(--text-xs); }
/* Full-bleed rule; content inset to the header's box so both edges line up with the nav. */
Expand Down Expand Up @@ -438,3 +392,32 @@ button[data-search-full] kbd {
.start-grid { padding: var(--space-4xl) 0; }
.chart-frame { height: clamp(320px, 72svh, 600px); }
}

/* HIG type roles and adaptive targets; the dense chart keeps compact controls. */
body { font-size: var(--type-body-size); line-height: var(--type-body-line); }
.hero h1 { font-size: clamp(var(--type-title-size), 4.4vw, var(--type-display-size)); line-height: 1.0833; font-weight: var(--type-display-weight); }
#nd-page h1 { font-size: var(--type-title-size); line-height: var(--type-title-line); font-weight: var(--type-title-weight); }
.install-tabs button { min-height: var(--control-h-sm); }
.install-feedback, .copy-feedback { color: var(--destructive); font-size: var(--type-caption-size); }
.btn--icon { min-height: var(--control-h-sm); }
@media (pointer: coarse) {
.btn--sm, .btn--icon, .install-tabs button, .docs-app-link, #nd-page .edit-on-github { min-height: var(--control-h); }
.btn--icon { min-width: var(--control-h); }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0ms !important; transition-duration: 0ms !important; scroll-behavior: auto !important; }
.btn:active, .guide-links a:hover > svg { transform: none; }
}

@theme inline {
--color-background: var(--background); --color-foreground: var(--foreground);
--color-card: var(--card); --color-card-foreground: var(--card-foreground);
--color-muted: var(--muted); --color-muted-foreground: var(--muted-foreground);
--color-border: var(--border); --color-primary: var(--primary);
--color-bloxwap-green: var(--hig-action); --color-bloxwap-black: var(--hig-background);
--text-3xs: 0.625rem; --text-2xs: 0.6875rem;
}

.search-error { color: var(--destructive); padding: var(--space-lg); }

.install-command .copy-feedback { grid-column: 1 / -1; padding-inline-end: var(--space-lg); }
8 changes: 1 addition & 7 deletions apps/docs/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { Metadata } from 'next';
import localFont from 'next/font/local';
import { BloxwapSans as sans } from '@bloxwap/font/sans';
import { BloxwapMono as mono } from '@bloxwap/font/mono';
import type { ReactNode } from 'react';
Expand All @@ -8,11 +7,6 @@ import { basePath, siteUrl } from '@/lib/site';
import { homeSocial, socialImagePath, socialMetadata } from '@/lib/social';
import './global.css';

const display = localFont({
src: '../fonts/SpaceGrotesk-Bold.ttf', weight: '700',
variable: '--font-docs-display', display: 'swap',
});

export const metadata: Metadata = {
metadataBase: new URL(`${siteUrl}${basePath}/`),
title: { default: '@bloxwap/chart — Financial charts for your interface', template: '%s · @bloxwap/chart' },
Expand All @@ -22,7 +16,7 @@ export const metadata: Metadata = {
};

export default function RootLayout({ children }: { children: ReactNode }) {
return <html lang="en" suppressHydrationWarning className={`${sans.variable} ${mono.variable} ${display.variable} dark`}>
return <html lang="en" suppressHydrationWarning className={`${sans.variable} ${mono.variable} dark`}>
<body className="flex min-h-screen flex-col antialiased">
<link rel="service-desc" type="application/json" href={`${basePath}/openapi.json`} />
<Provider>{children}</Provider>
Expand Down
7 changes: 5 additions & 2 deletions apps/docs/components/chart-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import { assetUrl } from '@/lib/site';
export function ChartPreview({ children }: { children?: ReactNode }) {
const container = useRef<HTMLDivElement>(null);
const iframe = useRef<HTMLIFrameElement>(null);
const [loaded, setLoaded] = useState(false);
const [fullscreen, setFullscreen] = useState(false);
const [canFullscreen, setCanFullscreen] = useState(false);
// Name the file: `next dev` does not map directory URLs in public/ to index.html.
const demoUrl = assetUrl('/chart-demo/demo/index.html');
const demoUrl = assetUrl('/chart-demo/demo/index.html?lockedTheme=dark');

useEffect(() => {
setCanFullscreen(document.fullscreenEnabled);
Expand Down Expand Up @@ -68,8 +69,10 @@ export function ChartPreview({ children }: { children?: ReactNode }) {
</button>}
</div>
</div>
<p className="preview-summary">Interactive candlestick chart with price and volume readouts, interval selection, drawings, pan and zoom. Live market data may be unavailable; choose a synthetic market in the playground to explore offline.</p>
{!loaded && <p role="status">Loading chart playground…</p>}
<div ref={container} className="chart-preview">
<iframe ref={iframe} className="chart-frame" src={demoUrl} title="Interactive Bloxwap chart playground" allowFullScreen />
<iframe ref={iframe} className="chart-frame" src={demoUrl} onLoad={() => setLoaded(true)} title="Interactive Bloxwap chart playground" allowFullScreen />
</div>
</div>;
}
14 changes: 11 additions & 3 deletions apps/docs/components/copy-button.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
'use client';

import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Check, Copy } from 'lucide-react';

/** A quiet icon button that copies `text`, with a check mark and a screen-reader status on success. */
export function CopyButton({ text, label }: { text: string; label: string }) {
const pending = useRef(false);
const [busy, setBusy] = useState(false);
const [result, setResult] = useState<'copied' | 'failed' | null>(null);

useEffect(() => {
Expand All @@ -14,19 +16,25 @@ export function CopyButton({ text, label }: { text: string; label: string }) {
}, [result]);

async function copy() {
if (pending.current) return;
pending.current = true;
setBusy(true);
try {
await navigator.clipboard.writeText(text);
setResult('copied');
} catch {
setResult('failed');
} finally {
pending.current = false;
setBusy(false);
}
}

return <>
<button type="button" className="btn btn--ghost btn--icon" aria-label={label} title={result === 'copied' ? 'Copied!' : label} onClick={copy}>
<button type="button" className="btn btn--ghost btn--icon" aria-label={label} disabled={busy} aria-busy={busy} title={result === 'copied' ? 'Copied!' : label} onClick={copy}>
{result === 'copied' ? <Check aria-hidden="true" /> : <Copy aria-hidden="true" />}
</button>
<span role="status" className="sr-only">
<span role="status" className={result === 'failed' ? 'copy-feedback' : 'sr-only'}>
{result === 'copied' ? 'Code copied to clipboard.' : result === 'failed' ? 'Could not copy. Select the code to copy it manually.' : ''}
</span>
</>;
Expand Down
29 changes: 3 additions & 26 deletions apps/docs/components/install-command.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { useEffect, useId, useState } from 'react';
import { Check, Copy } from 'lucide-react';
import { useId, useState } from 'react';
import { CopyButton } from './copy-button';

const managers = [
{ name: 'bun', command: 'bun add @bloxwap/chart' },
Expand All @@ -13,28 +13,10 @@ const managers = [
export function InstallCommand() {
const id = useId();
const [selected, setSelected] = useState(0);
const [copyResult, setCopyResult] = useState<{ command: string; success: boolean } | null>(null);
const { command } = managers[selected];
const result = copyResult?.command === command ? copyResult : null;

useEffect(() => {
if (!copyResult) return;
const timer = window.setTimeout(() => setCopyResult(null), copyResult.success ? 2000 : 5000);
return () => window.clearTimeout(timer);
}, [copyResult]);

function select(index: number) {
setSelected(index);
setCopyResult(null);
}

async function copy() {
try {
await navigator.clipboard.writeText(command);
setCopyResult({ command, success: true });
} catch {
setCopyResult({ command, success: false });
}
}

return <div className="package-install">
Expand Down Expand Up @@ -64,13 +46,8 @@ export function InstallCommand() {
<div className="install-command" role="tabpanel" id={`${id}-command`} aria-labelledby={`${id}-tab-${selected}`} tabIndex={0}>
<span className="install-prompt" aria-hidden="true">$</span>
<code>{command}</code>
<button type="button" className="btn btn--ghost btn--icon" aria-label="Copy install command" title={result?.success ? 'Copied!' : 'Copy command'} onClick={copy}>
{result?.success ? <Check aria-hidden="true" /> : <Copy aria-hidden="true" />}
</button>
<CopyButton key={command} text={command} label="Copy install command" />
</div>
<span role="status" className={result && !result.success ? 'install-feedback' : 'sr-only'}>
{result ? result.success ? 'Command copied to clipboard.' : 'Could not copy. Select the command to copy it manually.' : ''}
</span>
<p className="install-platforms">Bun · Node.js · Browsers · Workers</p>
</div>;
}
Loading
Loading