@gradeui/ui 0.9.0 → 1.0.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/components/ui/accordion.md +30 -0
- package/components/ui/ai-chat-composer.md +37 -0
- package/components/ui/ai-chat.md +81 -0
- package/components/ui/alert.md +0 -0
- package/components/ui/app-shell.md +178 -0
- package/components/ui/avatar.md +29 -0
- package/components/ui/badge.md +18 -0
- package/components/ui/breadcrumb.md +101 -0
- package/components/ui/button.md +63 -0
- package/components/ui/calendar.md +39 -0
- package/components/ui/callout.md +45 -0
- package/components/ui/card.md +40 -0
- package/components/ui/carousel.md +56 -0
- package/components/ui/chart.md +48 -0
- package/components/ui/checkbox.md +20 -0
- package/components/ui/collapsible.md +28 -0
- package/components/ui/command.md +38 -0
- package/components/ui/date-picker.md +52 -0
- package/components/ui/dialog.md +40 -0
- package/components/ui/dropdown-menu.md +45 -0
- package/components/ui/flex.md +41 -0
- package/components/ui/grid.md +44 -0
- package/components/ui/hover-card.md +35 -0
- package/components/ui/input.md +17 -0
- package/components/ui/label.md +15 -0
- package/components/ui/map.md +80 -0
- package/components/ui/media-surface.md +61 -0
- package/components/ui/multi-select.md +114 -0
- package/components/ui/popover.md +43 -0
- package/components/ui/progress.md +15 -0
- package/components/ui/radio-group.md +37 -0
- package/components/ui/resizable.md +30 -0
- package/components/ui/rive-player.md +38 -0
- package/components/ui/row.md +32 -0
- package/components/ui/scroll-area.md +27 -0
- package/components/ui/select.md +24 -0
- package/components/ui/separator.md +16 -0
- package/components/ui/shader-preset-picker.md +26 -0
- package/components/ui/shader-preset-preview.md +24 -0
- package/components/ui/sheet.md +52 -0
- package/components/ui/side-menu.md +0 -0
- package/components/ui/sidebar.md +121 -0
- package/components/ui/simple-tabs.md +0 -0
- package/components/ui/skeleton.md +17 -0
- package/components/ui/slider.md +48 -0
- package/components/ui/sortable.md +101 -0
- package/components/ui/stack.md +50 -0
- package/components/ui/switch.md +20 -0
- package/components/ui/table.md +28 -0
- package/components/ui/tabs.md +56 -0
- package/components/ui/textarea.md +14 -0
- package/components/ui/three-scene.md +226 -0
- package/components/ui/toast.md +38 -0
- package/components/ui/toggle-group.md +43 -0
- package/components/ui/toggle.md +36 -0
- package/components/ui/toolbar.md +167 -0
- package/components/ui/tooltip.md +28 -0
- package/components/ui/video-player.md +27 -0
- package/dist/contracts.d.mts +14 -0
- package/dist/contracts.d.ts +14 -0
- package/dist/contracts.js +63 -0
- package/dist/contracts.js.map +1 -0
- package/dist/contracts.mjs +63 -0
- package/dist/contracts.mjs.map +1 -0
- package/dist/index.d.mts +1339 -191
- package/dist/index.d.ts +1339 -191
- package/dist/index.js +111 -49
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +111 -49
- package/dist/index.mjs.map +1 -1
- package/dist/map/google.js +1 -0
- package/dist/map/google.js.map +1 -1
- package/dist/map/google.mjs +1 -0
- package/dist/map/google.mjs.map +1 -1
- package/dist/map/mapbox.js +1 -0
- package/dist/map/mapbox.js.map +1 -1
- package/dist/map/mapbox.mjs +1 -0
- package/dist/map/mapbox.mjs.map +1 -1
- package/dist/map/maplibre.js +1 -0
- package/dist/map/maplibre.js.map +1 -1
- package/dist/map/maplibre.mjs +1 -0
- package/dist/map/maplibre.mjs.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/tailwind-preset.js +1 -1
- package/dist/tailwind-preset.js.map +1 -1
- package/dist/tailwind-preset.mjs +1 -1
- package/dist/tailwind-preset.mjs.map +1 -1
- package/package.json +26 -10
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ThreeScene
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- preset?: "space" | "plasma" | "voronoi" | "synthwave" — shader preset id from the registry
|
|
6
|
+
- fragmentShader?: string — user-authored GLSL body; takes precedence over preset
|
|
7
|
+
- onShaderError?: (error: ShaderCompileError) => void — fires on compile failure; scene falls back to `preset="space"`
|
|
8
|
+
- postPreset?: "none" | "vhs" | "cinematic" | "synthwave" | "crt" (default "vhs") — post-processing pass
|
|
9
|
+
- palette?: Partial<{ primary; secondary; accent; background }> — any CSS-legal colour string per slot. Re-tints automatically when the theme changes. Unset slots fall back to defaults.
|
|
10
|
+
- createScene?: (ctx) => SceneHandle — custom full scene factory; takes precedence over preset AND fragmentShader
|
|
11
|
+
- controls?: boolean (default false) — play/pause overlay
|
|
12
|
+
- autoPlay?: boolean (default true) — respects reduced-motion
|
|
13
|
+
- pauseOffscreen?: boolean (default true) — big win for WebGL battery life
|
|
14
|
+
- aspect?: "video" | "square" | "portrait" | "wide" | "auto" (default "video")
|
|
15
|
+
- maxDpr?: number (default min(devicePixelRatio, 2)) — lower for thumbnails / low-end devices
|
|
16
|
+
- radius?: "none" | "sm" | "md" | "lg" | "xl" (default "lg")
|
|
17
|
+
when_to_use: WebGL primitive for shader backgrounds, generative visuals, and bespoke three.js scenes. Three authoring paths, in order of preference — (1) pick a `preset` id; (2) if nothing in the registry fits, write a `fragmentShader` against the fixed uniform contract; (3) only as a last resort, pass a full `createScene` factory. For looping video, use VideoPlayer; for interactive animations, use RivePlayer.
|
|
18
|
+
composes_with: [MediaSurface (internal), foreground content stacked above with `position: absolute/relative z-10`]
|
|
19
|
+
aliases: [three, threejs, webgl, shader, scene, 3d, generative, hero background, fragment shader, glsl]
|
|
20
|
+
notes: |
|
|
21
|
+
Depends on `three` and `postprocessing` (bundled into @gradeui/ui). Safari caps concurrent WebGL contexts at ~8 — for preset galleries, prefer ShaderPresetPreview with `live="hover"`.
|
|
22
|
+
|
|
23
|
+
## Path 1 — `preset` (pick one, fastest, highest quality)
|
|
24
|
+
|
|
25
|
+
Valid `preset` ids (complete list — do NOT invent any others):
|
|
26
|
+
- "space" — Hyperspace starfield, streaking stars. Default post: "vhs".
|
|
27
|
+
- "plasma" — soft rolling colour clouds, ambient/abstract. Default post: "synthwave".
|
|
28
|
+
- "voronoi" — jittered cellular grid with glowing edges. Default post: "crt".
|
|
29
|
+
- "synthwave" — retro perspective grid + banded sun. Default post: "synthwave".
|
|
30
|
+
|
|
31
|
+
Any other preset id renders an empty surface. If these don't cover the ask, DO NOT invent a name — jump to Path 2 (`fragmentShader`) and write the shader directly.
|
|
32
|
+
|
|
33
|
+
Valid `postPreset` ids (complete list): "none" | "vhs" | "cinematic" | "synthwave" | "crt".
|
|
34
|
+
|
|
35
|
+
Re-skin any preset with `palette={{ primary, secondary, accent, background }}` to shift its mood. Preset + palette + postPreset is usually enough to hit ocean / lava / neon / forest vibes.
|
|
36
|
+
|
|
37
|
+
### Palette values — what counts as a valid colour
|
|
38
|
+
|
|
39
|
+
Each slot accepts ANY CSS-legal colour expression. Values are normalised via a browser probe before being handed to three.js, so all of these work:
|
|
40
|
+
|
|
41
|
+
- CSS custom properties wrapped in a colour function — `"oklch(var(--primary))"`, `"oklch(var(--foreground))"`. **This is the recommended pattern for gradeui consumers.** gradeui tokens (like shadcn) are bare channel triplets (`--primary: 0.610 0.128 20`), so `var(--primary)` alone is NOT a valid CSS colour and will render black. ALWAYS wrap as `oklch(var(--token))`. The shader re-tints automatically on theme change.
|
|
42
|
+
- Hex — `"#ff5fb9"`, `"#f5b"`.
|
|
43
|
+
- `rgb()` / `rgba()` — `"rgb(255 95 185)"`, `"rgb(255, 95, 185)"`.
|
|
44
|
+
- `hsl()` / `hsla()` — `"hsl(330 100% 69%)"`.
|
|
45
|
+
- `oklch()` / `lab()` / `lch()` / `oklab()` — `"oklch(0.74 0.18 350)"`. Full CSS Color 4.
|
|
46
|
+
- Named colours — `"tomato"`, `"dodgerblue"`, `"black"`.
|
|
47
|
+
|
|
48
|
+
INVALID — these DO NOT work and will silently fall back to the default palette slot:
|
|
49
|
+
|
|
50
|
+
- Literal bare triplets passed as a palette string — `"0.4 0.1 0.9"` is NOT a colour; wrap it as `"oklch(0.4 0.1 0.9)"`. (The var()-based auto-wrap above only kicks in when the palette value is `var(--token)` and the token itself is a triplet — it can't rescue a raw triplet passed directly.)
|
|
51
|
+
- three.js hex numbers — `0xff5fb9` (number). Use the string `"#ff5fb9"`.
|
|
52
|
+
- Colour arrays — `[0.4, 0.1, 0.9]`. Not accepted.
|
|
53
|
+
|
|
54
|
+
Theme reactivity: when the host document's root class or `data-theme` attribute changes, the scene re-reads the palette and pushes new uniforms into the running shader WITHOUT tearing down the WebGL context. Dark/light swaps are essentially free.
|
|
55
|
+
|
|
56
|
+
### gradeui token semantics — pick the RIGHT tokens, and ALWAYS wrap in `oklch()`
|
|
57
|
+
|
|
58
|
+
gradeui tokens are bare OKLCH channel triplets (`--primary: 0.610 0.128 20`, no `oklch()` wrapper) — same convention as shadcn. That means **every `var(--token)` passed to the palette MUST be wrapped in `oklch(...)` at the call site**: `"oklch(var(--primary))"`, not `"var(--primary)"`. Unwrapped values resolve to invalid CSS and render black.
|
|
59
|
+
|
|
60
|
+
Token role cheat-sheet when picking which slot maps to what:
|
|
61
|
+
|
|
62
|
+
- `--primary` — brand hue 1. USE for `palette.primary` (and often `palette.accent` too).
|
|
63
|
+
- `--accent` — brand hue 2. USE for `palette.secondary` — gradeui's `--secondary` is a NEUTRAL surface (identical to `--muted`) and will render as a flat near-white wash in the shader.
|
|
64
|
+
- `--foreground` — inverted neutral (dark in light mode, light in dark mode). USE for `palette.background` — the raw `--background` token is the page background (near-white in light mode) and will wash the shader out.
|
|
65
|
+
|
|
66
|
+
Idiomatic theme-reactive palette for gradeui consumers (copy verbatim):
|
|
67
|
+
|
|
68
|
+
```jsx
|
|
69
|
+
palette={{
|
|
70
|
+
primary: "oklch(var(--primary))",
|
|
71
|
+
secondary: "oklch(var(--accent))", // NOT var(--secondary) — that's a neutral
|
|
72
|
+
accent: "oklch(var(--primary))",
|
|
73
|
+
background: "oklch(var(--foreground))", // NOT var(--background) — that's the page bg
|
|
74
|
+
}}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Path 2 — `fragmentShader` (custom GLSL)
|
|
78
|
+
|
|
79
|
+
Pass a GLSL fragment shader body as a string. Runs on a fullscreen quad. Header is AUTO-INJECTED — write `void main()` only and use the uniforms below as given. Do NOT redeclare them, do NOT add `#version` directives, do NOT `import * as THREE` — you are writing shader text, not JavaScript.
|
|
80
|
+
|
|
81
|
+
Auto-injected header (available to every fragmentShader):
|
|
82
|
+
|
|
83
|
+
```glsl
|
|
84
|
+
precision highp float;
|
|
85
|
+
varying vec2 vUv; // [0,1] across the quad
|
|
86
|
+
uniform float uTime; // elapsed seconds
|
|
87
|
+
uniform vec2 uResolution; // pixel size of the canvas
|
|
88
|
+
uniform vec2 uMouse; // [0,1], y-up (GLSL convention); defaults to (0.5, 0.5)
|
|
89
|
+
uniform vec3 uPrimary; // palette.primary (theme-driven)
|
|
90
|
+
uniform vec3 uSecondary; // palette.secondary
|
|
91
|
+
uniform vec3 uAccent; // palette.accent
|
|
92
|
+
uniform vec3 uBackground; // palette.background
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Minimal working skeleton:
|
|
96
|
+
|
|
97
|
+
```glsl
|
|
98
|
+
void main() {
|
|
99
|
+
vec2 uv = vUv - 0.5;
|
|
100
|
+
float t = uTime;
|
|
101
|
+
vec3 col = mix(uBackground, uPrimary, 0.5 + 0.5 * sin(length(uv) * 10.0 - t));
|
|
102
|
+
gl_FragColor = vec4(col, 1.0);
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
GLSL syntax rules:
|
|
107
|
+
- Use `gl_FragColor` for output (NOT `out vec4`).
|
|
108
|
+
- Use `varying` for inputs (NOT `in`).
|
|
109
|
+
- Use `texture2D` if sampling textures (not `texture`). In practice you won't need textures — stick to procedural colour.
|
|
110
|
+
- Hard cap: keep shaders under ~200 lines. Long raymarchers are usually both slow and wrong.
|
|
111
|
+
|
|
112
|
+
Error handling: if the GLSL fails to compile, the component fires `onShaderError` with the GL info log and renders `preset="space"` as a fallback. Never returns a blank surface.
|
|
113
|
+
|
|
114
|
+
## Path 3 — `createScene` (escape hatch)
|
|
115
|
+
|
|
116
|
+
A full `SceneFactory` that returns `{ scene, camera, update, resize, setPalette, dispose }`. Only reach for this if you need real geometry, multiple passes, or a custom camera. 95% of "make me a shader" asks are better served by Path 2.
|
|
117
|
+
|
|
118
|
+
## Fullscreen backgrounds
|
|
119
|
+
|
|
120
|
+
Surface defaults to `aspect="video"` (16:9). For a full-bleed hero background using `className="absolute inset-0"`, ALWAYS also pass `aspect="auto"` — otherwise the aspect-ratio constraint fights the absolute positioning and you get letterboxing.
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
```jsx
|
|
124
|
+
// Path 1 — named preset (fastest path)
|
|
125
|
+
<ThreeScene preset="plasma" postPreset="synthwave" aspect="wide" />
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```jsx
|
|
129
|
+
// Path 1 — preset + palette re-skin to hit a custom mood
|
|
130
|
+
<ThreeScene
|
|
131
|
+
preset="space"
|
|
132
|
+
postPreset="cinematic"
|
|
133
|
+
palette={{
|
|
134
|
+
primary: "#00e0ff",
|
|
135
|
+
secondary: "#1a7eff",
|
|
136
|
+
accent: "#ffffff",
|
|
137
|
+
background: "#000512",
|
|
138
|
+
}}
|
|
139
|
+
/>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```jsx
|
|
143
|
+
// Path 1 — palette from the active theme via CSS variables.
|
|
144
|
+
// Recolors automatically when the theme switches.
|
|
145
|
+
//
|
|
146
|
+
// gradeui tokens are bare OKLCH triplets (shadcn-style), so EVERY var() MUST
|
|
147
|
+
// be wrapped in oklch(...) at the call site — unwrapped `var(--primary)` is
|
|
148
|
+
// invalid CSS and will render black.
|
|
149
|
+
//
|
|
150
|
+
// Slot mapping: `--secondary` is a neutral surface in gradeui (not a brand hue)
|
|
151
|
+
// and `--background` is the page bg (near-white in light mode). Map secondary
|
|
152
|
+
// to `--accent` and background to `--foreground` for a punchy, theme-reactive
|
|
153
|
+
// palette that inverts cleanly on dark-mode toggle.
|
|
154
|
+
<ThreeScene
|
|
155
|
+
preset="plasma"
|
|
156
|
+
palette={{
|
|
157
|
+
primary: "oklch(var(--primary))",
|
|
158
|
+
secondary: "oklch(var(--accent))",
|
|
159
|
+
accent: "oklch(var(--primary))",
|
|
160
|
+
background: "oklch(var(--foreground))",
|
|
161
|
+
}}
|
|
162
|
+
/>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```jsx
|
|
166
|
+
// Path 1 — CSS Color 4 (oklch) works too.
|
|
167
|
+
<ThreeScene
|
|
168
|
+
preset="voronoi"
|
|
169
|
+
palette={{
|
|
170
|
+
primary: "oklch(0.74 0.18 350)",
|
|
171
|
+
secondary: "oklch(0.62 0.22 260)",
|
|
172
|
+
accent: "oklch(0.92 0.11 95)",
|
|
173
|
+
background: "oklch(0.1 0.04 280)",
|
|
174
|
+
}}
|
|
175
|
+
/>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```jsx
|
|
179
|
+
// Path 2 — custom fragment shader. Header is auto-injected; just write main().
|
|
180
|
+
// This one: concentric rings in the theme's primary colour, breathing on uTime.
|
|
181
|
+
<ThreeScene
|
|
182
|
+
fragmentShader={`
|
|
183
|
+
void main() {
|
|
184
|
+
vec2 uv = vUv - 0.5;
|
|
185
|
+
uv.x *= uResolution.x / uResolution.y;
|
|
186
|
+
float d = length(uv);
|
|
187
|
+
float rings = 0.5 + 0.5 * sin(d * 30.0 - uTime * 2.0);
|
|
188
|
+
vec3 col = mix(uBackground, uPrimary, rings);
|
|
189
|
+
col = mix(col, uAccent, smoothstep(0.45, 0.5, d) * 0.4);
|
|
190
|
+
gl_FragColor = vec4(col, 1.0);
|
|
191
|
+
}
|
|
192
|
+
`}
|
|
193
|
+
postPreset="vhs"
|
|
194
|
+
aspect="square"
|
|
195
|
+
/>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
```jsx
|
|
199
|
+
// Path 2 — interactive: follow the pointer with uMouse.
|
|
200
|
+
<ThreeScene
|
|
201
|
+
fragmentShader={`
|
|
202
|
+
void main() {
|
|
203
|
+
vec2 uv = vUv;
|
|
204
|
+
float d = distance(uv, uMouse);
|
|
205
|
+
float glow = smoothstep(0.3, 0.0, d);
|
|
206
|
+
vec3 col = mix(uBackground, uPrimary, glow);
|
|
207
|
+
gl_FragColor = vec4(col, 1.0);
|
|
208
|
+
}
|
|
209
|
+
`}
|
|
210
|
+
/>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
```jsx
|
|
214
|
+
// Fullscreen hero — shader behind, content on top.
|
|
215
|
+
// `aspect="auto"` is required for inset-0 to fill the parent.
|
|
216
|
+
<div className="relative h-screen w-full overflow-hidden">
|
|
217
|
+
<ThreeScene
|
|
218
|
+
preset="synthwave"
|
|
219
|
+
aspect="auto"
|
|
220
|
+
className="absolute inset-0"
|
|
221
|
+
/>
|
|
222
|
+
<div className="relative z-10 py-16 px-6 text-center text-white">
|
|
223
|
+
<h1 className="text-5xl font-bold">Build at the speed of thought</h1>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Toaster
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
aliases: [toast, toaster, sonner, notification, snackbar, alert toast, transient alert, transient banner, banner notification, toastandroid]
|
|
5
|
+
props:
|
|
6
|
+
- Toaster: position? "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right" (default "bottom-right")
|
|
7
|
+
- Toaster: theme? "light" | "dark" | "system"
|
|
8
|
+
- Toaster: richColors?: boolean — colored variants for success/error/warning/info
|
|
9
|
+
- Toaster: expand?: boolean — keep multiple toasts visually separated rather than stacked
|
|
10
|
+
- Toaster: visibleToasts?: number — max concurrent toasts on screen (default 3)
|
|
11
|
+
- Toaster: duration?: number — default ms before auto-dismiss
|
|
12
|
+
when_to_use: Transient, non-blocking feedback that confirms or warns about an action — "Saved", "Failed to upload", "Copied to clipboard", "Invitation sent". For permanent inline messages reach for Callout. For confirmations that block until acknowledged use Dialog. Mount <Toaster /> ONCE at the root of the app; everywhere else, call the `toast` helper.
|
|
13
|
+
composes_with: [App root layout (single <Toaster /> mount), Form submit handlers (success/error toasts), Async actions]
|
|
14
|
+
notes: Backed by Sonner under the hood — `import { toast } from "sonner"` to fire toasts from anywhere.
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
// At the app root, mount once.
|
|
19
|
+
<Toaster richColors position="bottom-right" />
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```jsx
|
|
23
|
+
// Anywhere else, fire via the helper.
|
|
24
|
+
import { toast } from "sonner";
|
|
25
|
+
|
|
26
|
+
<Button
|
|
27
|
+
onClick={async () => {
|
|
28
|
+
try {
|
|
29
|
+
await saveProfile();
|
|
30
|
+
toast.success("Saved");
|
|
31
|
+
} catch (err) {
|
|
32
|
+
toast.error("Couldn't save", { description: err.message });
|
|
33
|
+
}
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
Save changes
|
|
37
|
+
</Button>
|
|
38
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ToggleGroup
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [ToggleGroupItem]
|
|
5
|
+
variants: [default, outline]
|
|
6
|
+
sizes: [sm, md, lg]
|
|
7
|
+
props:
|
|
8
|
+
- ToggleGroup: type: "single" | "multiple" — single picks one, multiple picks any number
|
|
9
|
+
- ToggleGroup: value?: string | string[] — controlled; matches `type` (string for single, string[] for multiple)
|
|
10
|
+
- ToggleGroup: defaultValue?: string | string[] — uncontrolled initial
|
|
11
|
+
- ToggleGroup: onValueChange?: (value: string | string[]) => void
|
|
12
|
+
- ToggleGroup: size? (sm | md | lg, default md) — cascades to every ToggleGroupItem via context, matches Tabs/Button heights
|
|
13
|
+
- ToggleGroup: variant? (default | outline)
|
|
14
|
+
- ToggleGroupItem: value: string — what the group reports when this item is pressed
|
|
15
|
+
- ToggleGroupItem: tooltip?: ReactNode — when set, wraps the item in a Tooltip; required for icon-only items where the visible chrome doesn't carry a label
|
|
16
|
+
- ToggleGroupItem: tooltipSide? ("top" | "right" | "bottom" | "left", default "top") — side the tooltip renders on
|
|
17
|
+
- ToggleGroupItem: tooltipDelay?: number — per-item delay override; falls back to the upstream TooltipProvider's delayDuration
|
|
18
|
+
when_to_use: A small set of mutually-exclusive (`type="single"`) or independent (`type="multiple"`) binary options that live side-by-side as a segmented control — viewport size picker (Mobile/Tablet/Desktop), text alignment, view density. Reads identically to a TabsList of the same size; reach for ToggleGroup when each option emits a value (like a form input) rather than swapping panels. Use Tabs for panel switching, Toggle for a single on/off.
|
|
19
|
+
composes_with: [Card (header controls), Row, AppShellHeader chrome, settings panels]
|
|
20
|
+
aliases: [toggle group, segmented control, segmented buttons, button group, pill group, view selector, segmented picker, segmentedcontrolios, segmented buttons group, rn segmented control]
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
```jsx
|
|
24
|
+
// Single-select segmented control — viewport size picker with
|
|
25
|
+
// icon-only items + tooltips. The `tooltip` prop also fills in
|
|
26
|
+
// `aria-label` for screen readers, so consumers don't have to
|
|
27
|
+
// duplicate the label.
|
|
28
|
+
<ToggleGroup type="single" defaultValue="desktop" size="sm">
|
|
29
|
+
<ToggleGroupItem value="mobile" tooltip="Mobile — 390px"><Smartphone /></ToggleGroupItem>
|
|
30
|
+
<ToggleGroupItem value="tablet" tooltip="Tablet — 768px"><Tablet /></ToggleGroupItem>
|
|
31
|
+
<ToggleGroupItem value="desktop" tooltip="Desktop — 1024px"><Monitor /></ToggleGroupItem>
|
|
32
|
+
<ToggleGroupItem value="responsive" tooltip="Responsive — fills the column"><MoveHorizontal /></ToggleGroupItem>
|
|
33
|
+
</ToggleGroup>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```jsx
|
|
37
|
+
// Multi-select — text formatting toolbar.
|
|
38
|
+
<ToggleGroup type="multiple">
|
|
39
|
+
<ToggleGroupItem value="bold" aria-label="Bold"><Bold /></ToggleGroupItem>
|
|
40
|
+
<ToggleGroupItem value="italic" aria-label="Italic"><Italic /></ToggleGroupItem>
|
|
41
|
+
<ToggleGroupItem value="underline" aria-label="Underline"><Underline /></ToggleGroupItem>
|
|
42
|
+
</ToggleGroup>
|
|
43
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Toggle
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
variants: [default, outline]
|
|
5
|
+
sizes: [default, sm, lg]
|
|
6
|
+
props:
|
|
7
|
+
- variant? (default | outline) — outline adds a border, default is borderless and ghost-like
|
|
8
|
+
- size? (default | sm | lg)
|
|
9
|
+
- pressed?: boolean — controlled pressed state
|
|
10
|
+
- defaultPressed?: boolean — uncontrolled initial state
|
|
11
|
+
- onPressedChange?: (pressed: boolean) => void
|
|
12
|
+
- disabled?: boolean
|
|
13
|
+
- children: React.ReactNode — usually an icon or short label
|
|
14
|
+
when_to_use: A standalone on/off button — Bold/Italic in a toolbar, "Show grid" in a header, single binary toggle that doesn't belong inside a Switch row. For two-or-more mutually-exclusive options use ToggleGroup. For a labeled settings switch ("Active: on/off") use Switch.
|
|
15
|
+
composes_with: [Tooltip (wrap an icon-only Toggle), Row, TabsList (sibling)]
|
|
16
|
+
aliases: [toggle, toggle button, press button, bold button, italic button]
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
```jsx
|
|
20
|
+
// Single toolbar toggle — icon + tooltip for screen readers.
|
|
21
|
+
<TooltipProvider>
|
|
22
|
+
<Tooltip>
|
|
23
|
+
<TooltipTrigger asChild>
|
|
24
|
+
<Toggle aria-label="Toggle bold">
|
|
25
|
+
<Bold />
|
|
26
|
+
</Toggle>
|
|
27
|
+
</TooltipTrigger>
|
|
28
|
+
<TooltipContent>Bold</TooltipContent>
|
|
29
|
+
</Tooltip>
|
|
30
|
+
</TooltipProvider>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```jsx
|
|
34
|
+
// Borderless variant — fits inline among other controls.
|
|
35
|
+
<Toggle defaultPressed>Show grid</Toggle>
|
|
36
|
+
```
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Toolbar
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
role: layout
|
|
5
|
+
subcomponents: [ToolbarSlot]
|
|
6
|
+
props:
|
|
7
|
+
- leading?: React.ReactNode — left-aligned region (logo + primary nav)
|
|
8
|
+
- center?: React.ReactNode — center region (search, page title, segmented control)
|
|
9
|
+
- trailing?: React.ReactNode — right-aligned region (action icons, avatar, primary CTA)
|
|
10
|
+
- children?: React.ReactNode — escape hatch; bypasses slot layout
|
|
11
|
+
- position?: "top" | "bottom" | "inline" (default "top") — border placement
|
|
12
|
+
- variant?: "default" | "subtle" | "transparent" (default "default")
|
|
13
|
+
- size?: "sm" | "md" | "lg" (default "md") — height + padding
|
|
14
|
+
- sticky?: boolean (default false) — pin to top/bottom of scroll container
|
|
15
|
+
- aria-label?: string (default "Toolbar") — required by WAI-ARIA toolbar pattern
|
|
16
|
+
- className?: string
|
|
17
|
+
when_to_use: |
|
|
18
|
+
ANY three-region chrome bar — the leading/center/trailing pattern Apple HIG
|
|
19
|
+
describes as a "Toolbar." App window chrome (Reddit, Twitter, GitHub, Linear,
|
|
20
|
+
most desktop apps), section toolbars inside Cards or panels, bottom action
|
|
21
|
+
bars on mobile layouts, persistent footer toolbars.
|
|
22
|
+
|
|
23
|
+
Don't hand-roll `<Row justify="between">` with a flex-1 on a middle child and
|
|
24
|
+
manual min-width juggling — Toolbar gives you the canonical `auto 1fr auto`
|
|
25
|
+
grid for free, with `role="toolbar"`, `data-gds-part` markers, position
|
|
26
|
+
variants for top/bottom borders, and sticky sizing.
|
|
27
|
+
|
|
28
|
+
Slot semantics:
|
|
29
|
+
leading — Logo + nav rail (e.g. a `<Row>` of Buttons or Link components)
|
|
30
|
+
center — Search input, page title chip, segmented Tab strip
|
|
31
|
+
trailing — Icon buttons, notification bell, avatar, primary CTA
|
|
32
|
+
|
|
33
|
+
When a slot is omitted, its column collapses cleanly. Center stays visually
|
|
34
|
+
centered in the bar regardless of leading/trailing widths because the grid
|
|
35
|
+
template is `auto 1fr auto` (the center column absorbs available width).
|
|
36
|
+
|
|
37
|
+
Use as the top child of `<AppShellHeader>` for window-level chrome:
|
|
38
|
+
<AppShellHeader>
|
|
39
|
+
<Toolbar leading={<Logo/>} center={<Search/>} trailing={<Avatar/>} />
|
|
40
|
+
</AppShellHeader>
|
|
41
|
+
|
|
42
|
+
Use directly inside a Card or page section for section-scoped toolbars:
|
|
43
|
+
<Card>
|
|
44
|
+
<Toolbar size="sm" variant="subtle" leading={...} trailing={...} />
|
|
45
|
+
{content}
|
|
46
|
+
</Card>
|
|
47
|
+
composes_with: [Button, Avatar, Input, Logo, Badge, AppShellHeader, Card, Row, Stack]
|
|
48
|
+
aliases: [
|
|
49
|
+
toolbar, tool bar, top bar, topbar, app bar, appbar, header bar, header,
|
|
50
|
+
navigation bar, nav bar, navbar, window chrome, window toolbar, title bar,
|
|
51
|
+
titlebar, action bar, actionbar, command bar, ribbon,
|
|
52
|
+
three-region nav, leading center trailing, leading-center-trailing,
|
|
53
|
+
apple hig toolbar, hig toolbar, native toolbar, segmented toolbar,
|
|
54
|
+
bottom toolbar, footer toolbar, fixed toolbar, sticky header
|
|
55
|
+
]
|
|
56
|
+
notes: |
|
|
57
|
+
Apple HIG reference: https://developer.apple.com/design/human-interface-guidelines/toolbars
|
|
58
|
+
WAI-ARIA toolbar pattern: https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/
|
|
59
|
+
|
|
60
|
+
Roving tabindex for arrow-key navigation between toolbar items is NOT
|
|
61
|
+
implemented in v1. For a tight cluster of related controls (an editor
|
|
62
|
+
toolbar — B / I / S / link), compose with @radix-ui/react-toolbar inside
|
|
63
|
+
the slots if you need arrow-key navigation. For an app chrome bar (logo
|
|
64
|
+
+ nav + actions), standard tab order is the expected pattern and a
|
|
65
|
+
single aria-label is sufficient.
|
|
66
|
+
|
|
67
|
+
Center vs. leading for the page title:
|
|
68
|
+
- Use `center` for a CENTERED page title (Apple-style window chrome).
|
|
69
|
+
- Use `leading` after the logo for a LEFT-ALIGNED page title (web-app
|
|
70
|
+
style — GitHub, Linear). Mixing is fine.
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
```jsx
|
|
74
|
+
// App window chrome — Reddit / Twitter / GitHub shape.
|
|
75
|
+
<Toolbar
|
|
76
|
+
leading={
|
|
77
|
+
<Row gap="sm" align="center">
|
|
78
|
+
<Logo />
|
|
79
|
+
<Button variant="ghost" size="sm">Home</Button>
|
|
80
|
+
<Button variant="ghost" size="sm">Explore</Button>
|
|
81
|
+
</Row>
|
|
82
|
+
}
|
|
83
|
+
center={
|
|
84
|
+
<Input placeholder="Search" className="max-w-md" />
|
|
85
|
+
}
|
|
86
|
+
trailing={
|
|
87
|
+
<Row gap="xs" align="center">
|
|
88
|
+
<Button variant="ghost" size="icon"><Bell /></Button>
|
|
89
|
+
<Avatar><AvatarFallback>AL</AvatarFallback></Avatar>
|
|
90
|
+
</Row>
|
|
91
|
+
}
|
|
92
|
+
/>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```jsx
|
|
96
|
+
// Section toolbar inside a Card — small, subtle, no border.
|
|
97
|
+
<Card>
|
|
98
|
+
<Toolbar
|
|
99
|
+
size="sm"
|
|
100
|
+
variant="subtle"
|
|
101
|
+
position="inline"
|
|
102
|
+
leading={<span className="text-sm font-medium">Recent activity</span>}
|
|
103
|
+
trailing={
|
|
104
|
+
<Button variant="ghost" size="sm">View all</Button>
|
|
105
|
+
}
|
|
106
|
+
/>
|
|
107
|
+
<CardContent>…</CardContent>
|
|
108
|
+
</Card>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```jsx
|
|
112
|
+
// Bottom action toolbar — common on mobile-style detail pages.
|
|
113
|
+
<Toolbar
|
|
114
|
+
position="bottom"
|
|
115
|
+
sticky
|
|
116
|
+
leading={<Button variant="outline" size="sm">Cancel</Button>}
|
|
117
|
+
trailing={<Button size="sm">Save changes</Button>}
|
|
118
|
+
/>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```jsx
|
|
122
|
+
// Inside AppShellHeader — the canonical "app chrome" composition.
|
|
123
|
+
<AppShell nav="side">
|
|
124
|
+
<AppShellHeader>
|
|
125
|
+
<Toolbar
|
|
126
|
+
leading={<Logo />}
|
|
127
|
+
trailing={
|
|
128
|
+
<Row gap="xs">
|
|
129
|
+
<Button variant="ghost" size="icon"><Bell /></Button>
|
|
130
|
+
<Avatar><AvatarFallback>AL</AvatarFallback></Avatar>
|
|
131
|
+
</Row>
|
|
132
|
+
}
|
|
133
|
+
/>
|
|
134
|
+
</AppShellHeader>
|
|
135
|
+
<AppShellNav placement="side">{/* sidebar */}</AppShellNav>
|
|
136
|
+
<AppShellMain>{/* content */}</AppShellMain>
|
|
137
|
+
</AppShell>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Anti-patterns
|
|
141
|
+
|
|
142
|
+
```jsx
|
|
143
|
+
// ❌ Hand-rolling the three-region grid every time.
|
|
144
|
+
<Row justify="between" align="center" className="px-4 py-3 border-b border-border">
|
|
145
|
+
<Row gap="sm" align="center"><Logo /></Row>
|
|
146
|
+
<div className="flex-1 flex justify-center"><Input className="max-w-md" /></div>
|
|
147
|
+
<Row gap="xs" align="center"><Bell /><Avatar /></Row>
|
|
148
|
+
</Row>
|
|
149
|
+
|
|
150
|
+
// ✅ Toolbar collapses this to slot props + the right ARIA role.
|
|
151
|
+
<Toolbar
|
|
152
|
+
leading={<Logo />}
|
|
153
|
+
center={<Input className="max-w-md" />}
|
|
154
|
+
trailing={<Row gap="xs"><Bell /><Avatar /></Row>}
|
|
155
|
+
/>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```jsx
|
|
159
|
+
// ❌ Cramming an editor-style toolbar (B / I / S / link) into the leading
|
|
160
|
+
// slot. Toolbar's slot layout is for chrome bars; for a tight cluster
|
|
161
|
+
// of related controls with arrow-key navigation, compose with Radix
|
|
162
|
+
// Toolbar primitives inside the leading slot OR use a plain <Row>.
|
|
163
|
+
|
|
164
|
+
// ✅ Editor toolbar lives inside the section it's editing, not in the
|
|
165
|
+
// window chrome. Use a Row of Buttons or @radix-ui/react-toolbar
|
|
166
|
+
// inside the section.
|
|
167
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Tooltip
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: [TooltipTrigger, TooltipContent, TooltipProvider]
|
|
5
|
+
props:
|
|
6
|
+
- TooltipProvider: delayDuration? number (default 700) — ms hover before show; mount ONCE near the app root
|
|
7
|
+
- TooltipProvider: skipDelayDuration? number (default 300) — ms gap that still feels like "same hover"
|
|
8
|
+
- Tooltip: open?, defaultOpen?, onOpenChange?
|
|
9
|
+
- TooltipTrigger: asChild?: boolean — usually wraps a Button or icon
|
|
10
|
+
- TooltipContent: side? "top" | "right" | "bottom" | "left" (default "top"); align? "start" | "center" | "end"; sideOffset?: number
|
|
11
|
+
when_to_use: A short, non-essential label that explains a control on hover/focus — icon-only buttons in toolbars, abbreviated column headers, status dots. NEVER hide critical info inside a tooltip — they're invisible on touch and can be skipped by screen readers if implemented carelessly. For richer hover content use HoverCard. For inline help text that's always visible, use a description paragraph.
|
|
12
|
+
composes_with: [Button (icon-only), Toggle, TabsTrigger (the canonical tabs already have a `tooltip` prop that wraps this), Avatar (status badge meaning)]
|
|
13
|
+
aliases: [tooltip, tip, hover tip, hint, label on hover, help tag, hint, helper text bubble, info tip]
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
```jsx
|
|
17
|
+
// Icon-only Button with a tooltip — accessible name still set via aria-label.
|
|
18
|
+
<TooltipProvider>
|
|
19
|
+
<Tooltip>
|
|
20
|
+
<TooltipTrigger asChild>
|
|
21
|
+
<Button variant="ghost" size="icon" aria-label="Open settings">
|
|
22
|
+
<Settings />
|
|
23
|
+
</Button>
|
|
24
|
+
</TooltipTrigger>
|
|
25
|
+
<TooltipContent>Settings</TooltipContent>
|
|
26
|
+
</Tooltip>
|
|
27
|
+
</TooltipProvider>
|
|
28
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: VideoPlayer
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- src: string — video URL
|
|
6
|
+
- controls?: boolean (default true) — show native controls; false for chromeless hero/background video
|
|
7
|
+
- autoPlay?: boolean (default false) — forces muted=true (browser restriction)
|
|
8
|
+
- loop?: boolean (default false)
|
|
9
|
+
- muted?: boolean (default = autoPlay)
|
|
10
|
+
- pauseOffscreen?: boolean (default true) — pause when scrolled out of viewport
|
|
11
|
+
- aspect?: "video" | "square" | "portrait" | "wide" | "auto" (default "video")
|
|
12
|
+
- radius?: "none" | "sm" | "md" | "lg" | "xl" (default "lg") — driven by `--gds-media-radius`
|
|
13
|
+
- objectFit?: "cover" | "contain" | "fill" (default "cover")
|
|
14
|
+
- poster?: string — image shown before playback. Always rendered as a `loading="lazy"` `<img>` overlay (not the native `poster` attribute, which fetches eagerly).
|
|
15
|
+
- playbackRate?: number (default 1)
|
|
16
|
+
when_to_use: HTML5 video wrapped in the shared media surface. Controls-on for a standard player, controls-off (+ autoplay/muted/loop) for hero / background video. Prefer Rive for anything interactive, Three Scene for shader backgrounds.
|
|
17
|
+
composes_with: [MediaSurface (internal), Card (wrap for thumbnail grids)]
|
|
18
|
+
aliases: [video, mp4, movie, webm, clip, video view, av player, react native video, video element]
|
|
19
|
+
notes: Poster images are always lazy-loaded. We don't use the native `<video poster>` attribute because browsers fetch it eagerly even when the surface is off-screen, which wastes the offscreen-pause savings. Instead we render `<img loading="lazy" decoding="async">` layered over the video, then fade it out on `onPlaying`. When no `src` is given nothing renders — always pass a URL.
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
```jsx
|
|
23
|
+
<VideoPlayer src="/sample.mp4" poster="/movie-poster.jpg" controls />
|
|
24
|
+
|
|
25
|
+
// Chromeless hero video
|
|
26
|
+
<VideoPlayer src="/sample.mp4" controls={false} autoPlay loop muted aspect="wide" />
|
|
27
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ComponentContract } from '@gradeui/contracts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Component contract registry — auto-managed by
|
|
5
|
+
* scripts/generate-contracts.mjs. Hand-authored contracts (MediaSurface,
|
|
6
|
+
* etc.) are also wired in here; the generator preserves them on each
|
|
7
|
+
* run.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
declare const COMPONENT_CONTRACTS: Readonly<Record<string, ComponentContract>>;
|
|
11
|
+
declare function getComponentContract(componentName: string | null | undefined): ComponentContract | null;
|
|
12
|
+
declare function listContractedComponents(): string[];
|
|
13
|
+
|
|
14
|
+
export { COMPONENT_CONTRACTS, getComponentContract, listContractedComponents };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ComponentContract } from '@gradeui/contracts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Component contract registry — auto-managed by
|
|
5
|
+
* scripts/generate-contracts.mjs. Hand-authored contracts (MediaSurface,
|
|
6
|
+
* etc.) are also wired in here; the generator preserves them on each
|
|
7
|
+
* run.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
declare const COMPONENT_CONTRACTS: Readonly<Record<string, ComponentContract>>;
|
|
11
|
+
declare function getComponentContract(componentName: string | null | undefined): ComponentContract | null;
|
|
12
|
+
declare function listContractedComponents(): string[];
|
|
13
|
+
|
|
14
|
+
export { COMPONENT_CONTRACTS, getComponentContract, listContractedComponents };
|