@designtools/docs 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +155 -2
- package/dist/a11y-panel.d.ts +58 -0
- package/dist/a11y-panel.js +75 -0
- package/dist/adherence-summary.d.ts +9 -0
- package/dist/adherence-summary.js +27 -0
- package/dist/agent-view.d.ts +19 -0
- package/dist/agent-view.js +23 -0
- package/dist/anatomy.d.ts +12 -0
- package/dist/anatomy.js +39 -0
- package/dist/ask-claude.d.ts +14 -0
- package/dist/ask-claude.js +40 -0
- package/dist/cli.js +322 -0
- package/dist/code-view.d.ts +17 -0
- package/dist/code-view.js +42 -0
- package/dist/examples.d.ts +25 -0
- package/dist/examples.js +48 -0
- package/dist/glossary.d.ts +5 -0
- package/dist/glossary.js +21 -0
- package/dist/imagery.d.ts +6 -0
- package/dist/imagery.js +17 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +34 -0
- package/dist/lib/adherence-types.d.ts +168 -0
- package/dist/lib/adherence-types.js +13 -0
- package/dist/lib/boundary.d.ts +16 -0
- package/dist/lib/boundary.js +15 -0
- package/dist/lib/contrast.d.ts +8 -0
- package/dist/lib/contrast.js +42 -0
- package/dist/lib/cx.d.ts +24 -0
- package/dist/lib/cx.js +26 -0
- package/dist/lib/jsx.d.ts +6 -0
- package/dist/lib/jsx.js +29 -0
- package/dist/lib/live.d.ts +10 -0
- package/dist/lib/live.js +10 -0
- package/dist/lib/lookup.d.ts +35 -0
- package/dist/lib/lookup.js +42 -0
- package/dist/lib/manifest-types.d.ts +253 -0
- package/dist/lib/manifest-types.js +14 -0
- package/dist/lib/manifest.d.ts +98 -0
- package/dist/lib/manifest.js +173 -0
- package/dist/lib/markdown.d.ts +50 -0
- package/dist/lib/markdown.js +286 -0
- package/dist/lib/measure.d.ts +12 -0
- package/dist/lib/measure.js +62 -0
- package/dist/lib/pseudo.d.ts +11 -0
- package/dist/lib/pseudo.js +71 -0
- package/dist/lib/registry.d.ts +21 -0
- package/dist/lib/registry.js +11 -0
- package/dist/lib/search.d.ts +44 -0
- package/dist/lib/search.js +62 -0
- package/dist/lib/standards.d.ts +17 -0
- package/dist/lib/standards.js +81 -0
- package/dist/lib/status.d.ts +6 -0
- package/dist/lib/status.js +17 -0
- package/dist/lib/terms.d.ts +15 -0
- package/dist/lib/terms.js +20 -0
- package/dist/lib/text.d.ts +8 -0
- package/dist/lib/text.js +9 -0
- package/dist/lib/ui.d.ts +100 -0
- package/dist/lib/ui.js +89 -0
- package/dist/lib/vision.d.ts +18 -0
- package/dist/lib/vision.js +27 -0
- package/dist/logo-usage.d.ts +14 -0
- package/dist/logo-usage.js +45 -0
- package/dist/next/config.d.ts +105 -0
- package/dist/next/config.js +1 -0
- package/dist/next/index.d.ts +65 -0
- package/dist/next/index.js +66 -0
- package/dist/next/markdown.d.ts +20 -0
- package/dist/next/markdown.js +95 -0
- package/dist/next/pages.d.ts +6 -0
- package/dist/next/pages.js +188 -0
- package/dist/next/prose.d.ts +9 -0
- package/dist/next/prose.js +39 -0
- package/dist/next/proxy.d.ts +19 -0
- package/dist/next/proxy.js +56 -0
- package/dist/next/site.d.ts +53 -0
- package/dist/next/site.js +136 -0
- package/dist/page-actions.d.ts +9 -0
- package/dist/page-actions.js +32 -0
- package/dist/pattern.d.ts +11 -0
- package/dist/pattern.js +22 -0
- package/dist/playground.d.ts +62 -0
- package/dist/playground.js +111 -0
- package/dist/preview-frame.d.ts +31 -0
- package/dist/preview-frame.js +63 -0
- package/dist/props-table.d.ts +10 -0
- package/dist/props-table.js +45 -0
- package/dist/rule.d.ts +16 -0
- package/dist/rule.js +37 -0
- package/dist/scale.d.ts +9 -0
- package/dist/scale.js +58 -0
- package/dist/search.d.ts +16 -0
- package/dist/search.js +46 -0
- package/dist/shell.d.ts +92 -0
- package/dist/shell.js +201 -0
- package/dist/standards.d.ts +13 -0
- package/dist/standards.js +19 -0
- package/dist/styles.css +3708 -0
- package/dist/swatches.d.ts +28 -0
- package/dist/swatches.js +100 -0
- package/dist/theme-switch.d.ts +20 -0
- package/dist/theme-switch.js +68 -0
- package/dist/type-ramp.d.ts +10 -0
- package/dist/type-ramp.js +31 -0
- package/dist/usage.d.ts +13 -0
- package/dist/usage.js +55 -0
- package/dist/variant-matrix.d.ts +15 -0
- package/dist/variant-matrix.js +58 -0
- package/dist/voice-terms.d.ts +5 -0
- package/dist/voice-terms.js +14 -0
- package/dist/workbench.d.ts +33 -0
- package/dist/workbench.js +274 -0
- package/package.json +107 -3
- package/src/a11y-panel.tsx +297 -0
- package/src/adherence-summary.tsx +134 -0
- package/src/agent-view.tsx +54 -0
- package/src/anatomy.tsx +119 -0
- package/src/ask-claude.tsx +90 -0
- package/src/code-view.tsx +112 -0
- package/src/css.d.ts +1 -0
- package/src/examples.tsx +141 -0
- package/src/glossary.tsx +70 -0
- package/src/imagery.tsx +64 -0
- package/src/index.ts +35 -0
- package/src/lib/adherence-types.ts +181 -0
- package/src/lib/boundary.tsx +21 -0
- package/src/lib/contrast.ts +44 -0
- package/src/lib/cx.ts +30 -0
- package/src/lib/jsx.ts +29 -0
- package/src/lib/live.tsx +15 -0
- package/src/lib/lookup.tsx +50 -0
- package/src/lib/manifest-types.ts +284 -0
- package/src/lib/manifest.ts +228 -0
- package/src/lib/markdown.ts +288 -0
- package/src/lib/measure.tsx +101 -0
- package/src/lib/pseudo.ts +74 -0
- package/src/lib/registry.tsx +29 -0
- package/src/lib/search.ts +99 -0
- package/src/lib/standards.ts +82 -0
- package/src/lib/status.tsx +32 -0
- package/src/lib/terms.tsx +53 -0
- package/src/lib/text.tsx +27 -0
- package/src/lib/ui.tsx +291 -0
- package/src/lib/vision.tsx +44 -0
- package/src/logo-usage.tsx +140 -0
- package/src/next/config.ts +101 -0
- package/src/next/index.tsx +103 -0
- package/src/next/markdown.ts +119 -0
- package/src/next/pages.tsx +483 -0
- package/src/next/prose.tsx +97 -0
- package/src/next/proxy.ts +55 -0
- package/src/next/site.ts +173 -0
- package/src/page-actions.tsx +92 -0
- package/src/pattern.tsx +72 -0
- package/src/playground.tsx +357 -0
- package/src/preview-frame.tsx +131 -0
- package/src/props-table.tsx +154 -0
- package/src/rule.tsx +165 -0
- package/src/scale.tsx +175 -0
- package/src/search.tsx +155 -0
- package/src/shell.tsx +569 -0
- package/src/standards.tsx +58 -0
- package/src/styles.css +157 -0
- package/src/swatches.tsx +298 -0
- package/src/theme-switch.tsx +90 -0
- package/src/type-ramp.tsx +150 -0
- package/src/usage.tsx +148 -0
- package/src/variant-matrix.tsx +178 -0
- package/src/voice-terms.tsx +37 -0
- package/src/workbench.tsx +587 -0
package/src/styles.css
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The docs' styles, compiled at build and scoped to `[data-designtools]`, so they
|
|
3
|
+
* style the docs and nothing else on the page. They never match inside the
|
|
4
|
+
* project's own components shown in the docs (`Live`, `[data-docs-content]`), and
|
|
5
|
+
* on the docs' own elements they carry one attribute more than a project utility,
|
|
6
|
+
* so neither side's utilities leak into the other, whichever loads last.
|
|
7
|
+
*
|
|
8
|
+
* Themed by the project's own variables. Colours are @designtools/tokens'
|
|
9
|
+
* semantic names (--canvas, --surface, --primary, …), so a project on our tokens
|
|
10
|
+
* needs nothing; any other project sets those names on the docs. Fonts take
|
|
11
|
+
* --docs-font-sans / --docs-font-mono, else the project's --font-sans and
|
|
12
|
+
* --font-mono; headings --docs-display, else --font-display. Sizes, radii,
|
|
13
|
+
* shadows, leading and tracking follow the project's scale where it has one.
|
|
14
|
+
* Every value has a fallback, so the docs render on a project with none of it.
|
|
15
|
+
*/
|
|
16
|
+
@layer theme, base, components, utilities;
|
|
17
|
+
@import "tailwindcss/theme.css" layer(theme) theme(inline reference);
|
|
18
|
+
@import "tailwindcss/preflight.css" layer(base);
|
|
19
|
+
@import "tailwindcss/utilities.css" layer(utilities);
|
|
20
|
+
|
|
21
|
+
@source "./";
|
|
22
|
+
@source not "./lib/adherence-types.ts";
|
|
23
|
+
|
|
24
|
+
@theme inline reference {
|
|
25
|
+
--color-*: initial;
|
|
26
|
+
--color-white: #fff;
|
|
27
|
+
--color-black: #000;
|
|
28
|
+
--color-current: currentColor;
|
|
29
|
+
--color-transparent: transparent;
|
|
30
|
+
|
|
31
|
+
--color-canvas: var(--canvas, #ffffff);
|
|
32
|
+
--color-canvas-foreground: var(--canvas-foreground, #1c1f26);
|
|
33
|
+
--color-surface: var(--surface, #ffffff);
|
|
34
|
+
--color-surface-foreground: var(--surface-foreground, #1c1f26);
|
|
35
|
+
--color-overlay: var(--overlay, #ffffff);
|
|
36
|
+
--color-overlay-foreground: var(--overlay-foreground, #1c1f26);
|
|
37
|
+
--color-muted: var(--muted, #f4f5f7);
|
|
38
|
+
--color-muted-foreground: var(--muted-foreground, #5a606b);
|
|
39
|
+
--color-border: var(--border, #e2e4e8);
|
|
40
|
+
--color-input: var(--input, #8a909b);
|
|
41
|
+
--color-ring: var(--ring, #2b5ce6);
|
|
42
|
+
--color-shadow: var(--shadow, #0b0d12);
|
|
43
|
+
|
|
44
|
+
--color-primary: var(--primary, #2b5ce6);
|
|
45
|
+
--color-primary-foreground: var(--primary-foreground, #ffffff);
|
|
46
|
+
--color-primary-subdued: var(--primary-subdued, #eef2fd);
|
|
47
|
+
--color-primary-subdued-foreground: var(--primary-subdued-foreground, #1d40a8);
|
|
48
|
+
--color-primary-hover: var(--primary-hover, #2149c2);
|
|
49
|
+
--color-neutral: var(--neutral, #5a606b);
|
|
50
|
+
--color-neutral-foreground: var(--neutral-foreground, #ffffff);
|
|
51
|
+
--color-success: var(--success, #23823f);
|
|
52
|
+
--color-success-foreground: var(--success-foreground, #ffffff);
|
|
53
|
+
--color-success-subdued: var(--success-subdued, #e9f6ed);
|
|
54
|
+
--color-success-subdued-foreground: var(--success-subdued-foreground, #1b6631);
|
|
55
|
+
--color-destructive: var(--destructive, #c7302f);
|
|
56
|
+
--color-destructive-foreground: var(--destructive-foreground, #ffffff);
|
|
57
|
+
--color-destructive-subdued: var(--destructive-subdued, #fcecec);
|
|
58
|
+
--color-destructive-subdued-foreground: var(--destructive-subdued-foreground, #9b2221);
|
|
59
|
+
--color-warning: var(--warning, #ed8a3c);
|
|
60
|
+
--color-warning-foreground: var(--warning-foreground, #1c1f26);
|
|
61
|
+
--color-warning-subdued: var(--warning-subdued, #fdf0e3);
|
|
62
|
+
--color-warning-subdued-foreground: var(--warning-subdued-foreground, #6b3a10);
|
|
63
|
+
|
|
64
|
+
--default-font-family: var(--docs-font-sans, var(--font-sans, ui-sans-serif, system-ui, sans-serif));
|
|
65
|
+
--default-mono-font-family: var(--docs-font-mono, var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace));
|
|
66
|
+
--font-sans: var(--docs-font-sans, var(--font-sans, ui-sans-serif, system-ui, sans-serif));
|
|
67
|
+
--font-mono: var(--docs-font-mono, var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace));
|
|
68
|
+
|
|
69
|
+
--text-xs: var(--text-xs, 0.75rem);
|
|
70
|
+
--text-xs--line-height: var(--text-xs--line-height, 1rem);
|
|
71
|
+
--text-sm: var(--text-sm, 0.875rem);
|
|
72
|
+
--text-sm--line-height: var(--text-sm--line-height, 1.25rem);
|
|
73
|
+
--text-base: var(--text-base, 1rem);
|
|
74
|
+
--text-base--line-height: var(--text-base--line-height, 1.5rem);
|
|
75
|
+
--text-lg: var(--text-lg, 1.125rem);
|
|
76
|
+
--text-lg--line-height: var(--text-lg--line-height, 1.75rem);
|
|
77
|
+
--text-xl: var(--text-xl, 1.25rem);
|
|
78
|
+
--text-xl--line-height: var(--text-xl--line-height, 1.75rem);
|
|
79
|
+
--text-2xl: var(--text-2xl, 1.5rem);
|
|
80
|
+
--text-2xl--line-height: var(--text-2xl--line-height, 2rem);
|
|
81
|
+
--text-3xl: var(--text-3xl, 1.875rem);
|
|
82
|
+
--text-3xl--line-height: var(--text-3xl--line-height, 2.25rem);
|
|
83
|
+
--text-4xl: var(--text-4xl, 2.25rem);
|
|
84
|
+
--text-4xl--line-height: var(--text-4xl--line-height, 2.5rem);
|
|
85
|
+
--text-5xl: var(--text-5xl, 3rem);
|
|
86
|
+
--text-5xl--line-height: var(--text-5xl--line-height, 1);
|
|
87
|
+
--text-6xl: var(--text-6xl, 3.75rem);
|
|
88
|
+
--text-6xl--line-height: var(--text-6xl--line-height, 1);
|
|
89
|
+
--text-7xl: var(--text-7xl, 4.5rem);
|
|
90
|
+
--text-7xl--line-height: var(--text-7xl--line-height, 1);
|
|
91
|
+
--text-8xl: var(--text-8xl, 6rem);
|
|
92
|
+
--text-8xl--line-height: var(--text-8xl--line-height, 1);
|
|
93
|
+
|
|
94
|
+
--font-weight-light: var(--font-weight-light, 300);
|
|
95
|
+
--font-weight-normal: var(--font-weight-normal, 400);
|
|
96
|
+
--font-weight-medium: var(--font-weight-medium, 500);
|
|
97
|
+
--font-weight-semibold: var(--font-weight-semibold, 600);
|
|
98
|
+
--font-weight-bold: var(--font-weight-bold, 700);
|
|
99
|
+
|
|
100
|
+
--leading-none: var(--leading-none, 1);
|
|
101
|
+
--leading-tight: var(--leading-tight, 1.25);
|
|
102
|
+
--leading-snug: var(--leading-snug, 1.375);
|
|
103
|
+
--leading-normal: var(--leading-normal, 1.5);
|
|
104
|
+
--leading-relaxed: var(--leading-relaxed, 1.625);
|
|
105
|
+
|
|
106
|
+
--tracking-tighter: var(--tracking-tighter, -0.04em);
|
|
107
|
+
--tracking-tight: var(--tracking-tight, -0.02em);
|
|
108
|
+
--tracking-normal: var(--tracking-normal, 0em);
|
|
109
|
+
--tracking-wide: var(--tracking-wide, 0.04em);
|
|
110
|
+
--tracking-widest: var(--tracking-widest, 0.16em);
|
|
111
|
+
|
|
112
|
+
--radius-xs: var(--docs-radius-xs, var(--radius-xs, 0.125rem));
|
|
113
|
+
--radius-sm: var(--docs-radius-sm, var(--radius-sm, 0.25rem));
|
|
114
|
+
--radius-md: var(--docs-radius-md, var(--radius-md, 0.375rem));
|
|
115
|
+
--radius-lg: var(--docs-radius-lg, var(--radius-lg, 0.5rem));
|
|
116
|
+
--radius-xl: var(--docs-radius-xl, var(--radius-xl, 0.75rem));
|
|
117
|
+
--radius-2xl: var(--docs-radius-2xl, var(--radius-2xl, 1rem));
|
|
118
|
+
|
|
119
|
+
--shadow-xs: var(--shadow-xs, 0 1px 2px 0 color-mix(in oklch, var(--shadow, #0b0d12) 6%, transparent));
|
|
120
|
+
--shadow-sm: var(--shadow-sm, 0 1px 3px 0 color-mix(in oklch, var(--shadow, #0b0d12) 10%, transparent));
|
|
121
|
+
--shadow-md: var(--shadow-md, 0 4px 12px -2px color-mix(in oklch, var(--shadow, #0b0d12) 12%, transparent));
|
|
122
|
+
--shadow-lg: var(--shadow-lg, 0 12px 28px -6px color-mix(in oklch, var(--shadow, #0b0d12) 16%, transparent));
|
|
123
|
+
--shadow-xl: var(--shadow-xl, 0 24px 48px -12px color-mix(in oklch, var(--shadow, #0b0d12) 20%, transparent));
|
|
124
|
+
|
|
125
|
+
/* the docs' own geometry: a 4px step, whatever the project's */
|
|
126
|
+
--spacing: 0.25rem;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* The scale the blocks size and time themselves with: the project's, where it has one */
|
|
130
|
+
@layer base {
|
|
131
|
+
[data-designtools] {
|
|
132
|
+
--dt-size-xs: var(--size-xs, 1rem);
|
|
133
|
+
--dt-size-sm: var(--size-sm, 1.25rem);
|
|
134
|
+
--dt-size-md: var(--size-md, 1.5rem);
|
|
135
|
+
--dt-size-lg: var(--size-lg, 2rem);
|
|
136
|
+
--dt-size-xl: var(--size-xl, 2.5rem);
|
|
137
|
+
--dt-size-2xl: var(--size-2xl, 3rem);
|
|
138
|
+
--dt-size-3xl: var(--size-3xl, 4rem);
|
|
139
|
+
--dt-duration-fast: var(--duration-fast, 120ms);
|
|
140
|
+
--dt-duration-base: var(--duration-base, 200ms);
|
|
141
|
+
--dt-duration-slow: var(--duration-slow, 320ms);
|
|
142
|
+
--dt-ease-out: var(--ease-out, cubic-bezier(0.2, 0, 0, 1));
|
|
143
|
+
--dt-ease-in: var(--ease-in, cubic-bezier(0.4, 0, 1, 1));
|
|
144
|
+
--dt-ease-in-out: var(--ease-in-out, cubic-bezier(0.4, 0, 0.2, 1));
|
|
145
|
+
background: var(--canvas, #fff);
|
|
146
|
+
color: var(--canvas-foreground, #1c1f26);
|
|
147
|
+
}
|
|
148
|
+
/* Where the project's components render, they inherit the project's type, not the docs' */
|
|
149
|
+
[data-designtools] [data-docs-stage] {
|
|
150
|
+
font-family: var(--font-sans, inherit);
|
|
151
|
+
line-height: var(--leading-normal, 1.5);
|
|
152
|
+
}
|
|
153
|
+
/* Tailwind v4 puts buttons back to the arrow; anything pressable in the docs gets the pointer */
|
|
154
|
+
[data-designtools] :is(button:not(:disabled), [role="button"]:not([aria-disabled="true"]), summary) {
|
|
155
|
+
cursor: pointer;
|
|
156
|
+
}
|
|
157
|
+
}
|
package/src/swatches.tsx
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type ReactNode, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { contrastRatio, wcagGrade } from "./lib/contrast";
|
|
5
|
+
import { cx } from "./lib/cx";
|
|
6
|
+
import { ramps, semanticColours, valueIn, type Mode } from "./lib/manifest";
|
|
7
|
+
import type { TokensManifest } from "./lib/manifest-types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Colour, painted from the tokens rather than restated. Each colour with a
|
|
11
|
+
* subdued pair is a card: its fill, subdued, hover and press, in light and dark
|
|
12
|
+
* side by side. Then the surfaces, then the utility colours. Every sample sits
|
|
13
|
+
* inside its own `data-theme`, so dark is shown as the dark tokens resolve, and
|
|
14
|
+
* every pair's contrast is measured on the colours the browser actually paints.
|
|
15
|
+
*/
|
|
16
|
+
export function SemanticSwatches({
|
|
17
|
+
tokens,
|
|
18
|
+
modes = ["light", "dark"],
|
|
19
|
+
}: {
|
|
20
|
+
tokens: TokensManifest;
|
|
21
|
+
modes?: Mode[];
|
|
22
|
+
}) {
|
|
23
|
+
const colours = semanticColours(tokens);
|
|
24
|
+
const byName = new Map(colours.map((c) => [c.name, c]));
|
|
25
|
+
const families = colours.filter((c) => byName.has(`${c.name}-subdued`)).map((c) => c.name);
|
|
26
|
+
const inFamily = new Set(families.flatMap((f) => [f, `${f}-subdued`, `${f}-hover`, `${f}-active`]));
|
|
27
|
+
const surfaces = colours.filter((c) => !inFamily.has(c.name) && c.foreground).map((c) => c.name);
|
|
28
|
+
const utility = colours.filter((c) => !inFamily.has(c.name) && !c.foreground).map((c) => c.name);
|
|
29
|
+
const step = (name: string, mode: Mode) => {
|
|
30
|
+
const token = byName.get(name)?.background;
|
|
31
|
+
return token && pointsAt(valueIn(token, mode));
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// one level of framing: a panel per mode (the dark one has to be dark), chips inside it, and nothing around the family
|
|
35
|
+
const panel = (mode: Mode) =>
|
|
36
|
+
cx("flex flex-col gap-3 rounded-2xl bg-canvas p-4 text-canvas-foreground", mode === "light" && "ring-1 ring-border ring-inset");
|
|
37
|
+
const modeLabel = (mode: Mode) => <span className="text-xs font-medium text-muted-foreground capitalize">{mode}</span>;
|
|
38
|
+
const row = (title: string, meta: ReactNode, body: ReactNode, key?: string) => (
|
|
39
|
+
<div key={key ?? title} data-slot="swatch-row" className="grid gap-x-10 gap-y-5 border-t border-border pt-8 first:border-t-0 first:pt-0 lg:grid-cols-[11rem_minmax(0,1fr)]">
|
|
40
|
+
<div className="flex flex-col gap-1.5">
|
|
41
|
+
<h3 className="text-2xl leading-tight tracking-tight">{title}</h3>
|
|
42
|
+
{meta}
|
|
43
|
+
</div>
|
|
44
|
+
{body}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div data-slot="swatches" className="flex flex-col gap-10">
|
|
50
|
+
{families.map((f) =>
|
|
51
|
+
row(
|
|
52
|
+
(([first, ...rest]) => first.toUpperCase() + rest.join(""))(f.slice(2).replace(/-/g, " ")),
|
|
53
|
+
<code className="font-mono text-xs text-muted-foreground">{f}</code>,
|
|
54
|
+
<div className={cx("grid gap-4", modes.length > 1 && "md:grid-cols-2")}>
|
|
55
|
+
{modes.map((mode) => (
|
|
56
|
+
<div key={mode} data-theme={mode} data-slot="swatch-family" className={panel(mode)}>
|
|
57
|
+
{modeLabel(mode)}
|
|
58
|
+
<Chip name={f} label="fill" step={step(f, mode)} size="lg" />
|
|
59
|
+
<div className="grid grid-cols-3 gap-2">
|
|
60
|
+
<Chip name={`${f}-subdued`} label="subdued" step={step(`${f}-subdued`, mode)} />
|
|
61
|
+
{byName.has(`${f}-hover`) && <Chip name={`${f}-hover`} foreground={`${f}-foreground`} label="hover" />}
|
|
62
|
+
{byName.has(`${f}-active`) && <Chip name={`${f}-active`} foreground={`${f}-foreground`} label="press" />}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
))}
|
|
66
|
+
</div>,
|
|
67
|
+
f,
|
|
68
|
+
),
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
{surfaces.length > 0 &&
|
|
72
|
+
row(
|
|
73
|
+
"Surfaces",
|
|
74
|
+
<p className="text-sm text-muted-foreground">The page, panels and popups, each with its text.</p>,
|
|
75
|
+
<div className={cx("grid gap-4", modes.length > 1 && "md:grid-cols-2")}>
|
|
76
|
+
{modes.map((mode) => (
|
|
77
|
+
<div key={mode} data-theme={mode} className={panel(mode)}>
|
|
78
|
+
{modeLabel(mode)}
|
|
79
|
+
<div className="grid grid-cols-2 gap-2 xl:grid-cols-3">
|
|
80
|
+
{surfaces.map((name) => (
|
|
81
|
+
<Chip key={name} name={name} label={name.slice(2)} step={step(name, mode)} outlined />
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
))}
|
|
86
|
+
</div>,
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{utility.length > 0 &&
|
|
90
|
+
row(
|
|
91
|
+
"Lines, focus and shadow",
|
|
92
|
+
<p className="text-sm text-muted-foreground">Borders, input edges, the focus ring and the shadow colour.</p>,
|
|
93
|
+
<div className={cx("grid gap-4", modes.length > 1 && "md:grid-cols-2")}>
|
|
94
|
+
{modes.map((mode) => (
|
|
95
|
+
<div key={mode} data-theme={mode} className={panel(mode)}>
|
|
96
|
+
{modeLabel(mode)}
|
|
97
|
+
<ul className="grid grid-cols-2 gap-x-4 gap-y-3">
|
|
98
|
+
{utility.map((name) => (
|
|
99
|
+
<li key={name} data-slot="swatch" className="flex items-center gap-3">
|
|
100
|
+
<span aria-hidden className="size-9 shrink-0 rounded-lg border border-border" style={{ background: `var(${name})` }} />
|
|
101
|
+
<span className="flex min-w-0 flex-col font-mono text-xs">
|
|
102
|
+
<span className="truncate font-medium">{name.slice(2)}</span>
|
|
103
|
+
<span className="truncate text-muted-foreground">{step(name, mode)}</span>
|
|
104
|
+
</span>
|
|
105
|
+
</li>
|
|
106
|
+
))}
|
|
107
|
+
</ul>
|
|
108
|
+
</div>
|
|
109
|
+
))}
|
|
110
|
+
</div>,
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** `var(--color-primary-500)` → `primary-500`: the step it points at, which is what a person compares. */
|
|
117
|
+
function pointsAt(value: string | undefined): string | undefined {
|
|
118
|
+
const m = value && /^var\(--color-([\w-]+)\)$/.exec(value);
|
|
119
|
+
if (m) return m[1];
|
|
120
|
+
return value?.startsWith("oklch(") ? "generated" : value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* One background with its foreground on it, labelled in that foreground, with the
|
|
125
|
+
* contrast measured on what the browser paints. It reads its mode from the nearest
|
|
126
|
+
* `data-theme` around it.
|
|
127
|
+
*/
|
|
128
|
+
function Chip({
|
|
129
|
+
name,
|
|
130
|
+
foreground = `${name}-foreground`,
|
|
131
|
+
label,
|
|
132
|
+
step,
|
|
133
|
+
size = "md",
|
|
134
|
+
outlined,
|
|
135
|
+
}: {
|
|
136
|
+
name: string;
|
|
137
|
+
foreground?: string;
|
|
138
|
+
label: string;
|
|
139
|
+
step?: string;
|
|
140
|
+
size?: "md" | "lg";
|
|
141
|
+
outlined?: boolean;
|
|
142
|
+
}) {
|
|
143
|
+
const chip = useRef<HTMLDivElement>(null);
|
|
144
|
+
const [ratio, setRatio] = useState<number | null>(null);
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const node = chip.current;
|
|
147
|
+
if (!node) return;
|
|
148
|
+
const style = getComputedStyle(node);
|
|
149
|
+
setRatio(contrastRatio(style.color, style.backgroundColor));
|
|
150
|
+
}, [name, foreground]);
|
|
151
|
+
const grade = ratio === null ? null : wcagGrade(ratio);
|
|
152
|
+
const fails = grade === "Fail";
|
|
153
|
+
const measured = grade && ratio !== null ? `${ratio.toFixed(1)}:1 ${grade}` : undefined;
|
|
154
|
+
return (
|
|
155
|
+
<div
|
|
156
|
+
ref={chip}
|
|
157
|
+
data-slot="swatch"
|
|
158
|
+
title={`${name} with ${foreground}${step ? ` · ${step}` : ""}${measured ? ` · ${measured}` : ""}`}
|
|
159
|
+
className={cx("flex min-w-0 flex-col justify-between gap-3 rounded-lg p-3", size === "lg" ? "min-h-28" : "min-h-20", outlined && "border border-border")}
|
|
160
|
+
style={{ background: `var(${name})`, color: `var(${foreground})` }}
|
|
161
|
+
>
|
|
162
|
+
{size === "lg" ? (
|
|
163
|
+
<span className="flex items-start justify-between gap-2">
|
|
164
|
+
<span className="text-3xl leading-none tracking-tight">Aa</span>
|
|
165
|
+
{measured && (
|
|
166
|
+
<span
|
|
167
|
+
data-slot="swatch-contrast"
|
|
168
|
+
className={cx("rounded-full px-2 py-0.5 font-mono text-xs whitespace-nowrap", fails ? "bg-destructive font-medium text-destructive-foreground" : "ring-1 ring-current/30")}
|
|
169
|
+
>
|
|
170
|
+
{measured}
|
|
171
|
+
</span>
|
|
172
|
+
)}
|
|
173
|
+
</span>
|
|
174
|
+
) : (
|
|
175
|
+
<span className="text-base leading-none font-medium">Aa</span>
|
|
176
|
+
)}
|
|
177
|
+
<span className="flex min-w-0 flex-col font-mono text-xs leading-snug">
|
|
178
|
+
<span className="truncate font-medium">{label}</span>
|
|
179
|
+
{size === "lg" ? (
|
|
180
|
+
step && <span className="truncate opacity-75">{step}</span>
|
|
181
|
+
) : (
|
|
182
|
+
measured && (
|
|
183
|
+
<span data-slot="swatch-contrast" className={cx("truncate", fails ? "w-fit rounded-sm bg-destructive px-1 text-destructive-foreground" : "opacity-75")}>
|
|
184
|
+
{measured}
|
|
185
|
+
</span>
|
|
186
|
+
)
|
|
187
|
+
)}
|
|
188
|
+
</span>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Each colour on each background, per mode: a label is measured on its own fill, and this is
|
|
195
|
+
* everything else, red text on the page, a brand fill on a dark section. Backgrounds are the
|
|
196
|
+
* canvas, surface and overlay and every named `--surface-<name>`; colours are each semantic fill
|
|
197
|
+
* and muted text. T: fine as text (4.5:1); S: fine as a shape only (3:1); none: neither.
|
|
198
|
+
* Measured on what the browser paints, inside each mode's `data-theme`.
|
|
199
|
+
*/
|
|
200
|
+
export function PageContrast({ tokens, modes = ["light", "dark"] }: { tokens: TokensManifest; modes?: Mode[] }) {
|
|
201
|
+
const names = new Set(semanticColours(tokens).map((c) => c.name));
|
|
202
|
+
const colours = [...names].filter((n) => names.has(`${n}-subdued`)).concat(names.has("--muted") ? ["--muted-foreground"] : []);
|
|
203
|
+
const grounds = ["--canvas", "--surface", "--overlay", ...[...names].filter((n) => n.startsWith("--surface-") && !n.endsWith("-foreground") && n !== "--surface-foreground")];
|
|
204
|
+
return (
|
|
205
|
+
<div data-slot="page-contrast" className="flex flex-col gap-12">
|
|
206
|
+
{modes.map((mode) => (
|
|
207
|
+
<PageGrid key={mode} mode={mode} colours={colours} grounds={grounds} />
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function PageGrid({ mode, colours, grounds }: { mode: Mode; colours: string[]; grounds: string[] }) {
|
|
214
|
+
const probe = useRef<HTMLDivElement>(null);
|
|
215
|
+
const [ratios, setRatios] = useState<Record<string, number | null>>({});
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
const node = probe.current;
|
|
218
|
+
if (!node) return;
|
|
219
|
+
const paint = (v: string) => {
|
|
220
|
+
node.style.color = `var(${v})`;
|
|
221
|
+
return getComputedStyle(node).color;
|
|
222
|
+
};
|
|
223
|
+
const painted = Object.fromEntries([...colours, ...grounds].map((v) => [v, paint(v)]));
|
|
224
|
+
const out: Record<string, number | null> = {};
|
|
225
|
+
for (const c of colours) for (const g of grounds) out[`${c}|${g}`] = contrastRatio(painted[c], painted[g]);
|
|
226
|
+
setRatios(out);
|
|
227
|
+
}, [colours, grounds]);
|
|
228
|
+
return (
|
|
229
|
+
<div className="-mx-1 overflow-x-auto px-1" tabIndex={0} role="region" aria-label={`Colours on each background, ${mode} mode`}>
|
|
230
|
+
<div ref={probe} data-theme={mode} hidden />
|
|
231
|
+
<table className="w-full min-w-[40rem] border-collapse text-left text-sm">
|
|
232
|
+
<caption className="pb-4 text-left text-lg font-medium capitalize">{mode}</caption>
|
|
233
|
+
<thead>
|
|
234
|
+
<tr className="text-xs text-muted-foreground">
|
|
235
|
+
<th scope="col" className="pr-4 pb-3 font-medium">Colour</th>
|
|
236
|
+
{grounds.map((g) => (
|
|
237
|
+
<th key={g} scope="col" className="px-3 pb-3 font-mono font-normal">{g.slice(2)}</th>
|
|
238
|
+
))}
|
|
239
|
+
</tr>
|
|
240
|
+
</thead>
|
|
241
|
+
<tbody>
|
|
242
|
+
{colours.map((c) => (
|
|
243
|
+
<tr key={c} className="border-t border-border">
|
|
244
|
+
<th scope="row" className="py-3.5 pr-4 font-mono text-xs font-medium">{c.slice(2)}</th>
|
|
245
|
+
{grounds.map((g) => {
|
|
246
|
+
const r = ratios[`${c}|${g}`];
|
|
247
|
+
const mark = r == null ? "" : r >= 4.5 ? "text" : r >= 3 ? "shape" : "none";
|
|
248
|
+
return (
|
|
249
|
+
<td key={g} className="px-3 py-3.5">
|
|
250
|
+
<div className="flex items-center gap-2.5">
|
|
251
|
+
{/* only the sample takes the mode; the number is read on the page */}
|
|
252
|
+
<span data-theme={mode} className="flex h-10 w-14 items-center justify-center rounded-lg border border-(--border) text-base font-bold" style={{ background: `var(${g})`, color: `var(${c})` }} aria-hidden>
|
|
253
|
+
Aa
|
|
254
|
+
</span>
|
|
255
|
+
{r != null && (
|
|
256
|
+
<span
|
|
257
|
+
className={cx(
|
|
258
|
+
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-mono text-xs",
|
|
259
|
+
mark === "text" && "bg-success-subdued text-success-subdued-foreground",
|
|
260
|
+
mark === "shape" && "bg-muted text-muted-foreground",
|
|
261
|
+
mark === "none" && "bg-destructive-subdued text-destructive-subdued-foreground",
|
|
262
|
+
)}
|
|
263
|
+
>
|
|
264
|
+
{r.toFixed(1)} {mark}
|
|
265
|
+
</span>
|
|
266
|
+
)}
|
|
267
|
+
</div>
|
|
268
|
+
</td>
|
|
269
|
+
);
|
|
270
|
+
})}
|
|
271
|
+
</tr>
|
|
272
|
+
))}
|
|
273
|
+
</tbody>
|
|
274
|
+
</table>
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** The primitive ramps, one row per colour family, each step painted from its own token. */
|
|
280
|
+
export function RampSwatches({ tokens }: { tokens: TokensManifest }) {
|
|
281
|
+
return (
|
|
282
|
+
<div data-slot="ramps" className="flex flex-col gap-5 overflow-x-auto" tabIndex={0} role="region" aria-label="Ramps">
|
|
283
|
+
{ramps(tokens).map((ramp) => (
|
|
284
|
+
<div key={ramp.family} data-slot="ramp" className="grid min-w-[36rem] grid-cols-[6rem_1fr] items-center gap-4">
|
|
285
|
+
<p className="font-mono text-xs font-medium capitalize">{ramp.family}</p>
|
|
286
|
+
<ol className="grid grid-flow-col auto-cols-fr gap-1">
|
|
287
|
+
{ramp.steps.map(({ step, token }) => (
|
|
288
|
+
<li key={step} data-slot="ramp-step" title={`${token.name}: ${token.values.default ?? ""}`} className="flex flex-col gap-1.5">
|
|
289
|
+
<span aria-hidden className="h-12 rounded-md ring-1 ring-border ring-inset" style={{ background: `var(${token.name})` }} />
|
|
290
|
+
<span className="text-center font-mono text-xs text-muted-foreground">{step}</span>
|
|
291
|
+
</li>
|
|
292
|
+
))}
|
|
293
|
+
</ol>
|
|
294
|
+
</div>
|
|
295
|
+
))}
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Monitor, Moon, Sun } from "lucide-react";
|
|
3
|
+
import { useServerInsertedHTML } from "next/navigation";
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import { cx } from "./lib/cx";
|
|
6
|
+
|
|
7
|
+
type Mode = "system" | "light" | "dark";
|
|
8
|
+
const MODES = [
|
|
9
|
+
{ mode: "system", label: "System", Icon: Monitor },
|
|
10
|
+
{ mode: "light", label: "Light", Icon: Sun },
|
|
11
|
+
{ mode: "dark", label: "Dark", Icon: Moon },
|
|
12
|
+
] as const;
|
|
13
|
+
const KEY = "docs-theme";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Runs before the first paint, so a saved choice never flashes the other mode. It
|
|
17
|
+
* sets `data-theme` on <html>, which the generated tokens read, so give <html>
|
|
18
|
+
* `suppressHydrationWarning`. System sets nothing and leaves it to the media query.
|
|
19
|
+
*/
|
|
20
|
+
export const themeScript = `try{var t=localStorage.getItem("${KEY}");if(t==="light"||t==="dark")document.documentElement.dataset.theme=t}catch(e){}`;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Writes `themeScript` into the server's HTML, once, ahead of the page. Through
|
|
24
|
+
* Next's `useServerInsertedHTML`, because React will not run a <script> a client
|
|
25
|
+
* component renders, and says so. The shell calls it; call it once yourself if the
|
|
26
|
+
* switch lives somewhere else.
|
|
27
|
+
*/
|
|
28
|
+
export function useThemeScript() {
|
|
29
|
+
const done = useRef(false);
|
|
30
|
+
useServerInsertedHTML(() => {
|
|
31
|
+
if (done.current) return null;
|
|
32
|
+
done.current = true;
|
|
33
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: a fixed string, written here, with nothing from the request in it
|
|
34
|
+
return <script dangerouslySetInnerHTML={{ __html: themeScript }} />;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function apply(mode: Mode) {
|
|
39
|
+
const root = document.documentElement;
|
|
40
|
+
if (mode === "system") delete root.dataset.theme;
|
|
41
|
+
else root.dataset.theme = mode;
|
|
42
|
+
try {
|
|
43
|
+
if (mode === "system") localStorage.removeItem(KEY);
|
|
44
|
+
else localStorage.setItem(KEY, mode);
|
|
45
|
+
} catch {
|
|
46
|
+
// private windows and blocked storage: the choice lasts for this page only
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* System, light or dark for the whole site, as three icons in a track. Kept per
|
|
52
|
+
* viewer. Each is a 44px target however small it is drawn.
|
|
53
|
+
*/
|
|
54
|
+
export function ThemeSwitch({ className }: { className?: string }) {
|
|
55
|
+
const [mode, setMode] = useState<Mode>("system");
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const t = document.documentElement.dataset.theme;
|
|
58
|
+
if (t === "light" || t === "dark") setMode(t);
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div role="group" aria-label="Colour mode" data-slot="docs-theme-switch" className={cx("flex items-center rounded-full bg-muted p-0.5", className)}>
|
|
63
|
+
{MODES.map(({ mode: m, label, Icon }) => (
|
|
64
|
+
<button
|
|
65
|
+
key={m}
|
|
66
|
+
type="button"
|
|
67
|
+
aria-pressed={mode === m}
|
|
68
|
+
aria-label={label}
|
|
69
|
+
title={label}
|
|
70
|
+
onClick={() => {
|
|
71
|
+
setMode(m);
|
|
72
|
+
apply(m);
|
|
73
|
+
}}
|
|
74
|
+
// drawn at 32px; the transparent ring of padding makes each a 44px target
|
|
75
|
+
className="relative -my-1.5 inline-flex size-(--docs-target,2.75rem) cursor-pointer items-center justify-center rounded-full p-1.5 text-muted-foreground transition-colors duration-(--dt-duration-fast) first:-ml-1.5 last:-mr-1.5 hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring aria-pressed:text-canvas-foreground"
|
|
76
|
+
>
|
|
77
|
+
<span
|
|
78
|
+
aria-hidden
|
|
79
|
+
className={cx(
|
|
80
|
+
"flex size-full items-center justify-center rounded-full transition-colors duration-(--dt-duration-fast)",
|
|
81
|
+
mode === m && "bg-surface shadow-xs ring-1 ring-border",
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
<Icon className="size-(--dt-size-xs)" />
|
|
85
|
+
</span>
|
|
86
|
+
</button>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|