@gradeui/ui 1.2.0 → 2.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/background-fill.md +135 -0
- package/components/ui/checkbox-card.md +43 -0
- package/components/ui/checkbox.md +2 -2
- package/components/ui/dropdown-menu.md +2 -0
- package/components/ui/field.md +26 -0
- package/components/ui/fill-picker.md +36 -0
- package/components/ui/input.md +27 -2
- package/components/ui/label.md +2 -1
- package/components/ui/logo.md +59 -0
- package/components/ui/message.md +34 -0
- package/components/ui/radio-card.md +41 -0
- package/components/ui/radio-group.md +2 -2
- package/components/ui/screen-animator.md +54 -0
- package/components/ui/select.md +16 -4
- package/components/ui/switch-card.md +30 -0
- package/components/ui/switch.md +2 -2
- package/components/ui/textarea.md +2 -1
- package/components/ui/three-scene.md +22 -1
- package/dist/contracts.js +6 -6
- package/dist/contracts.js.map +1 -1
- package/dist/contracts.mjs +6 -6
- package/dist/contracts.mjs.map +1 -1
- package/dist/index.d.mts +703 -42
- package/dist/index.d.ts +703 -42
- package/dist/index.js +90 -56
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +90 -56
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +2 -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 +4 -4
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: BackgroundFill
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- type: "none" | "solid" | "gradient" | "image" | "video" | "shader" — which paint to render (required)
|
|
6
|
+
- color?: string — solid fill; a token name (`primary`, `card`, `muted`, `accent`, `secondary`, `destructive`, `background`, `transparent`) or any CSS colour
|
|
7
|
+
- gradient?: { from?; via?; to?; angle?; shape?; at?; size? } — stops are token names or CSS colours. shape: "linear" (default, uses `angle`, default 135°) | "radial" (uses `at` — CSS position like "top" / "30% 20%", default "center" — and optional `size` like "45rem 50rem", default farthest-corner)
|
|
8
|
+
- src?: string — image or video URL
|
|
9
|
+
- fit?: "cover" | "contain" | "fill" | "none" — object-fit for image/video (default "cover")
|
|
10
|
+
- position?: string — CSS object/background position (default "center")
|
|
11
|
+
- repeat?: boolean — tile the image (background-repeat) instead of a single <img>
|
|
12
|
+
- tileSize?: string — CSS background-size when repeating (e.g. "120px")
|
|
13
|
+
- preset?: string — shader preset id (see ThreeScene)
|
|
14
|
+
- fragmentShader?: string — custom GLSL (takes precedence over preset)
|
|
15
|
+
- palette?: Partial<{ primary; secondary; accent; background }> — shader palette overrides; wrap tokens as `oklch(var(--token))`
|
|
16
|
+
- postPreset?: string | PostPreset — shader post-FX
|
|
17
|
+
- opacity?: number — layer opacity 0–1
|
|
18
|
+
- blendMode?: CSS mix-blend-mode — blend against the frame behind it
|
|
19
|
+
- radius?: "none" | "sm" | "md" | "lg" | "xl" — match the frame's radius so the paint clips cleanly
|
|
20
|
+
when_to_use: The background *paint* of a frame — a generative shader, image, video, gradient, repeating texture, or solid token rendered as a layer BEHIND the frame's content. Use it as the first child of a `relative` frame; it paints an `absolute inset-0`, `z-0`, `pointer-events-none` layer, so content carrying `relative z-10` sits on top. This is the canonical way to give any container a rich background — never drop a full-bleed `<ThreeScene>` or `<img>` as a free-standing sibling. For a sized, in-flow media element (a hero card, a thumbnail), use ThreeScene / MediaSurface / VideoPlayer directly instead.
|
|
21
|
+
composes_with: [AppShell, Card, Stack, Row, Grid (any relative container), ThreeScene (shader fill), MediaSurface]
|
|
22
|
+
aliases: [background, fill, frame fill, backdrop, surface fill, background image, background video, background gradient, background shader, texture, paint]
|
|
23
|
+
notes: |
|
|
24
|
+
## The fill model
|
|
25
|
+
|
|
26
|
+
A background is a PROPERTY of a frame, not a node you select — exactly
|
|
27
|
+
like a fill in Figma / Paper. Select the frame; its Fill controls drive
|
|
28
|
+
this layer. BackgroundFill is the render boundary that makes that true.
|
|
29
|
+
|
|
30
|
+
### Required frame setup
|
|
31
|
+
|
|
32
|
+
The parent frame must be `relative` (so the `absolute inset-0` layer
|
|
33
|
+
anchors to it) and ideally `overflow-hidden` (so the paint clips to the
|
|
34
|
+
frame's corners). Content that should sit ABOVE the fill needs its own
|
|
35
|
+
stacking context — wrap it `relative z-10`:
|
|
36
|
+
|
|
37
|
+
```jsx
|
|
38
|
+
<Card className="relative overflow-hidden">
|
|
39
|
+
<BackgroundFill type="shader" preset="mesh" opacity={0.3} />
|
|
40
|
+
<div className="relative z-10">…content…</div>
|
|
41
|
+
</Card>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Why a layer (and why pointer-events-none)
|
|
45
|
+
|
|
46
|
+
A solid colour does not strictly need a layer — it could be the frame's
|
|
47
|
+
own `background`. Every other paint (image, video, gradient, shader,
|
|
48
|
+
tiled texture) needs real pixels, so it renders as an absolutely-
|
|
49
|
+
positioned layer. The layer is `z-0` + `pointer-events-none` so it sits
|
|
50
|
+
behind content and never intercepts clicks. It carries
|
|
51
|
+
`data-gds-part="frame-fill"` + `aria-hidden` so Studio treats it as
|
|
52
|
+
chrome (the frame is the selectable unit) and assistive tech skips it.
|
|
53
|
+
|
|
54
|
+
### Type cheat-sheet
|
|
55
|
+
|
|
56
|
+
- solid — `color` (token or CSS colour). Cheapest.
|
|
57
|
+
- gradient — `gradient={{ from, via?, to, angle }}` for linear;
|
|
58
|
+
`gradient={{ shape: "radial", at: "top", from, to }}` for a radial
|
|
59
|
+
glow/wash. Tokens get wrapped in oklch() automatically.
|
|
60
|
+
- image — `src` + `fit` / `position`; set `repeat` (+ `tileSize`) for a tiled texture.
|
|
61
|
+
- video — `src` (autoplays muted + looped + inline).
|
|
62
|
+
- shader — `preset` OR `fragmentShader`, + `palette` / `postPreset`. Delegates to ThreeScene.
|
|
63
|
+
|
|
64
|
+
Anti-patterns to avoid:
|
|
65
|
+
|
|
66
|
+
- DO NOT build gradients with arbitrary-value Tailwind classes —
|
|
67
|
+
`bg-[radial-gradient(45rem_50rem_at_top,theme(colors.indigo.50),white)]`
|
|
68
|
+
renders NOTHING in the Studio preview (no runtime Tailwind compiler) and
|
|
69
|
+
`theme(colors.*)` colours ignore the active Grade theme. Use
|
|
70
|
+
`type="gradient"` with token stops instead — themeable, and it always renders.
|
|
71
|
+
- DO NOT hand-roll `style={{ backgroundImage: "linear-gradient(…)" }}` on the
|
|
72
|
+
frame itself when a BackgroundFill child does the same job — the fill layer
|
|
73
|
+
keeps the paint selectable/editable as a Fill in Studio's inspector.
|
|
74
|
+
|
|
75
|
+
`opacity` + `blendMode` apply to every type — the same two controls as
|
|
76
|
+
the inspector's Blending section, so a loud shader/image can be dialled
|
|
77
|
+
back to a subtle wash behind text.
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
```jsx
|
|
81
|
+
// Shader background behind a hero, dialled back so text stays readable.
|
|
82
|
+
<section className="relative overflow-hidden rounded-xl">
|
|
83
|
+
<BackgroundFill
|
|
84
|
+
type="shader"
|
|
85
|
+
preset="mesh"
|
|
86
|
+
palette={{
|
|
87
|
+
primary: "oklch(var(--primary))",
|
|
88
|
+
secondary: "oklch(var(--accent))",
|
|
89
|
+
accent: "oklch(var(--primary))",
|
|
90
|
+
background: "oklch(var(--foreground))",
|
|
91
|
+
}}
|
|
92
|
+
opacity={0.35}
|
|
93
|
+
/>
|
|
94
|
+
<div className="relative z-10 p-12">
|
|
95
|
+
<h1 className="text-4xl font-bold">Build at the speed of thought</h1>
|
|
96
|
+
</div>
|
|
97
|
+
</section>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```jsx
|
|
101
|
+
// Gradient wash on a card.
|
|
102
|
+
<Card className="relative overflow-hidden">
|
|
103
|
+
<BackgroundFill type="gradient" gradient={{ from: "primary", to: "accent", angle: 120 }} opacity={0.18} />
|
|
104
|
+
<CardContent className="relative z-10">…</CardContent>
|
|
105
|
+
</Card>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```jsx
|
|
109
|
+
// Radial glow from the top of a hero — the token-true version of the
|
|
110
|
+
// classic `radial-gradient(45rem 50rem at top, indigo-50, white)` wash.
|
|
111
|
+
<section className="relative overflow-hidden">
|
|
112
|
+
<BackgroundFill
|
|
113
|
+
type="gradient"
|
|
114
|
+
gradient={{ shape: "radial", at: "top", size: "45rem 50rem", from: "primary", to: "background" }}
|
|
115
|
+
opacity={0.2}
|
|
116
|
+
/>
|
|
117
|
+
<div className="relative z-10 py-24 text-center">…hero content…</div>
|
|
118
|
+
</section>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```jsx
|
|
122
|
+
// Image background, cover-fit, with a blend mode.
|
|
123
|
+
<div className="relative h-64 overflow-hidden rounded-lg">
|
|
124
|
+
<BackgroundFill type="image" src="/hero.jpg" fit="cover" blendMode="multiply" />
|
|
125
|
+
<div className="relative z-10 p-6 text-white">Featured</div>
|
|
126
|
+
</div>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```jsx
|
|
130
|
+
// Tiled texture.
|
|
131
|
+
<div className="relative overflow-hidden">
|
|
132
|
+
<BackgroundFill type="image" src="/noise.png" repeat tileSize="160px" opacity={0.08} />
|
|
133
|
+
<div className="relative z-10">…</div>
|
|
134
|
+
</div>
|
|
135
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: CheckboxCard
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- checked? / defaultChecked? / onCheckedChange? — standard checkbox state
|
|
6
|
+
- label?: ReactNode — title line
|
|
7
|
+
- description?: ReactNode — secondary line
|
|
8
|
+
- aside?: ReactNode — slot before the indicator (a Badge, price, hint)
|
|
9
|
+
- hideIndicator?: boolean — hide the check; selection shown by the card border + background
|
|
10
|
+
- indicatorPosition?: "leading" | "trailing" — default trailing
|
|
11
|
+
- children?: ReactNode — arbitrary static content instead of label/description
|
|
12
|
+
when_to_use: Multi-select where each option is a whole selectable card (add-ons, feature toggles, opt-ins). The whole card is the control, so focus and the checked state live on the card surface. Standalone (not in a group). Static content only — never nest an interactive control inside. For a plain checkbox + label row use Field instead.
|
|
13
|
+
composes_with: [Badge (in aside), MediaSurface (custom children), Stack / Grid (laying out several)]
|
|
14
|
+
aliases: [checkbox card, selectable card, multi-select card, add-on card, feature card, opt-in card]
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
<div className="grid gap-3">
|
|
19
|
+
<CheckboxCard label="Priority support" description="24/7 response within an hour" defaultChecked />
|
|
20
|
+
<CheckboxCard label="Extended warranty" description="3 years parts and labour" />
|
|
21
|
+
</div>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Indicator on the leading edge, with a Badge in the `aside` slot:
|
|
25
|
+
|
|
26
|
+
```jsx
|
|
27
|
+
<CheckboxCard
|
|
28
|
+
indicatorPosition="leading"
|
|
29
|
+
label="Priority support"
|
|
30
|
+
description="24/7 response within an hour"
|
|
31
|
+
aside={<Badge variant="info-soft">Popular</Badge>}
|
|
32
|
+
defaultChecked
|
|
33
|
+
/>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
No visible tick (selection reads from the card border + background), in a two-up grid:
|
|
37
|
+
|
|
38
|
+
```jsx
|
|
39
|
+
<div className="grid grid-cols-2 gap-3">
|
|
40
|
+
<CheckboxCard hideIndicator label="Email" description="Weekly digest" defaultChecked />
|
|
41
|
+
<CheckboxCard hideIndicator label="SMS" description="Critical alerts only" />
|
|
42
|
+
</div>
|
|
43
|
+
```
|
|
@@ -7,8 +7,8 @@ props:
|
|
|
7
7
|
- defaultChecked?: boolean
|
|
8
8
|
- disabled?: boolean
|
|
9
9
|
- id?: string — bind a Label's htmlFor to this
|
|
10
|
-
when_to_use: Binary on/off tied to a list (select multiple, agree to terms). Single on/off that controls a setting is better with Switch.
|
|
11
|
-
composes_with: [Label (via htmlFor), Card, Form rows, Table (for row selection)]
|
|
10
|
+
when_to_use: Binary on/off tied to a list (select multiple, agree to terms). Single on/off that controls a setting is better with Switch. For a label + description row, wrap in Field. When each option should be a whole selectable card (label + description, selected state on the card surface), use CheckboxCard.
|
|
11
|
+
composes_with: [Label (via htmlFor), Field (label + description row), CheckboxCard (whole-card selectable option), Card, Form rows, Table (for row selection)]
|
|
12
12
|
aliases: [checkbox, tickbox, tick box, check, multi-select item]
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -7,7 +7,9 @@ props:
|
|
|
7
7
|
- DropdownMenuTrigger: asChild?: boolean — usually wraps a Button
|
|
8
8
|
- DropdownMenuContent: align? "start" | "center" | "end"; side? "top" | "right" | "bottom" | "left"; sideOffset? number
|
|
9
9
|
- DropdownMenuContent: surface? (solid | translucent | glass | glass-strong) — what the menu surface is *made of*. `solid` (default) is `bg-popover`. `translucent` matches Apple HIG / iOS menu sheets. `glass` for menus floating over rich canvases.
|
|
10
|
+
- DropdownMenuContent: size? "default" | "sm" | "xs" — menu density; cascades to every item (Item, Checkbox, Radio, SubTrigger, Label) via context so a compact trigger gets a compact menu. Use "xs" in dense tool panels.
|
|
10
11
|
- DropdownMenuSubContent: surface? (solid | translucent | glass | glass-strong) — same axis applied to nested submenu surfaces
|
|
12
|
+
- DropdownMenuSubContent: size? "default" | "sm" | "xs" — match the parent content's size down the tree
|
|
11
13
|
- DropdownMenuItem: onSelect?, disabled?, asChild?, inset?
|
|
12
14
|
- DropdownMenuCheckboxItem / DropdownMenuRadioItem: checked? / value, onCheckedChange? / onValueChange? (radio is on the group)
|
|
13
15
|
- DropdownMenuSub / DropdownMenuSubTrigger / DropdownMenuSubContent: nested menu — sub-trigger shows children, sub-content holds the deeper items
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Field
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- layout?: "option" | "setting" — option (default): control leads, text beside it; setting: text leads, control pinned trailing
|
|
6
|
+
- children: one bare control (Checkbox / RadioGroupItem / Switch) + Field.Label + Field.Description? + Field.Trailing? — order does not matter
|
|
7
|
+
when_to_use: Pair a bare control with a label and optional description in a row, with id + aria-describedby wired automatically. Use layout="setting" for the classic settings row (label on the left, Switch on the right). For a selectable CARD where the whole surface is the control, use RadioCard / CheckboxCard / SwitchCard instead.
|
|
8
|
+
composes_with: [Checkbox, RadioGroup, RadioGroupItem, Switch, Badge (inside Field.Trailing)]
|
|
9
|
+
aliases: [field, form field, control row, label and description, two line checkbox, option row, setting row, toggle row]
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
```jsx
|
|
13
|
+
<Field>
|
|
14
|
+
<Checkbox value="terms" />
|
|
15
|
+
<Field.Label>Accept terms</Field.Label>
|
|
16
|
+
<Field.Description>You agree to the privacy policy.</Field.Description>
|
|
17
|
+
</Field>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```jsx
|
|
21
|
+
<Field layout="setting">
|
|
22
|
+
<Field.Label>Email notifications</Field.Label>
|
|
23
|
+
<Field.Description>Weekly digest of activity.</Field.Description>
|
|
24
|
+
<Switch defaultChecked />
|
|
25
|
+
</Field>
|
|
26
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: FillPicker
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- value: FillValue — current paint ({ type, color?, gradient?, src?, fit?, repeat?, tileSize?, preset?, palette?, postPreset?, opacity? }) (required)
|
|
6
|
+
- onChange: (value: FillValue) => void — called on any change (required)
|
|
7
|
+
when_to_use: Grade's paint picker — the control for choosing a frame's background fill, modelled on Figma's fill popover. A fill-type icon row (solid · gradient · image · pattern · video · shader) switches the panel below; a global opacity sits at the foot. Emits a FillValue that maps 1:1 onto BackgroundFill props. This is a Studio/inspector chrome control — pair it with BackgroundFill, which renders the chosen paint. Not for app content.
|
|
8
|
+
composes_with: [BackgroundFill (renders the FillValue), Popover (host it in a popover), ShaderPresetPicker (the shader tab), the inspector Fill section]
|
|
9
|
+
aliases: [fill picker, paint picker, background picker, fill chooser, fill popover]
|
|
10
|
+
notes: |
|
|
11
|
+
Grade is token-led, so the solid + gradient tabs lead with theme-token
|
|
12
|
+
swatches (`primary`, `accent`, `secondary`, `muted`, `card`,
|
|
13
|
+
`background`, `destructive`, `transparent`) rather than a freeform HSV
|
|
14
|
+
square. The "pattern" tab is sugar for an image fill with `repeat` on.
|
|
15
|
+
|
|
16
|
+
The `FillValue` is the shared data shape: store it on a frame and feed
|
|
17
|
+
it straight to `<BackgroundFill {...value} />`. Solid colour can be a
|
|
18
|
+
className (`bg-<token>`) instead of a layer; every other type renders
|
|
19
|
+
as a `<BackgroundFill>` child of the frame.
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
```jsx
|
|
23
|
+
const [fill, setFill] = useState({ type: "shader", preset: "mesh", opacity: 0.35 });
|
|
24
|
+
|
|
25
|
+
<Popover>
|
|
26
|
+
<PopoverTrigger asChild><button>Fill</button></PopoverTrigger>
|
|
27
|
+
<PopoverContent className="w-[320px] p-3">
|
|
28
|
+
<FillPicker value={fill} onChange={setFill} />
|
|
29
|
+
</PopoverContent>
|
|
30
|
+
</Popover>
|
|
31
|
+
|
|
32
|
+
<div className="relative overflow-hidden">
|
|
33
|
+
<BackgroundFill {...fill} />
|
|
34
|
+
<div className="relative z-10">…content…</div>
|
|
35
|
+
</div>
|
|
36
|
+
```
|
package/components/ui/input.md
CHANGED
|
@@ -3,10 +3,13 @@ name: Input
|
|
|
3
3
|
import: "@gradeui/ui"
|
|
4
4
|
props:
|
|
5
5
|
- type?: string (text | email | password | number | search | url | tel | date)
|
|
6
|
+
- size?: "default" | "sm" | "xs" — control density. `default` (h-9) for forms; `sm` (h-8) and `xs` (h-7) for dense tool panels like the inspector.
|
|
7
|
+
- startSlot?: ReactNode — adornment rendered inside the leading edge (icon, prefix, currency symbol). Non-interactive by default so clicks focus the input.
|
|
8
|
+
- endSlot?: ReactNode — adornment rendered inside the trailing edge (unit like "px", a clear button, a stepper). Same pointer rules as startSlot.
|
|
6
9
|
- All native input HTML attrs (value, onChange, placeholder, disabled, required)
|
|
7
|
-
when_to_use: Any single-line text entry. Always pair with a Label for accessibility.
|
|
10
|
+
when_to_use: Any single-line text entry. Always pair with a Label for accessibility. Use startSlot/endSlot for icons, prefixes and units instead of hand-positioning absolute children; use size="sm"/"xs" in dense tool panels.
|
|
8
11
|
composes_with: [Label, Form, Card (in CardContent), Button (form submit)]
|
|
9
|
-
aliases: [text field, textbox, textfield, form field, text input, secure field, search field, url field, number field, textinput, text input field, react native textinput]
|
|
12
|
+
aliases: [text field, textbox, textfield, form field, text input, secure field, search field, url field, number field, textinput, text input field, react native textinput, unit input, input with icon]
|
|
10
13
|
---
|
|
11
14
|
|
|
12
15
|
```jsx
|
|
@@ -15,3 +18,25 @@ aliases: [text field, textbox, textfield, form field, text input, secure field,
|
|
|
15
18
|
<Input id="email" type="email" placeholder="you@example.com" />
|
|
16
19
|
</div>
|
|
17
20
|
```
|
|
21
|
+
|
|
22
|
+
Slots — a leading icon and a trailing unit, no manual positioning:
|
|
23
|
+
|
|
24
|
+
```jsx
|
|
25
|
+
<Input
|
|
26
|
+
size="sm"
|
|
27
|
+
type="number"
|
|
28
|
+
placeholder="0"
|
|
29
|
+
startSlot={<Ruler className="size-4" />}
|
|
30
|
+
endSlot={<span className="text-xs text-muted-foreground">px</span>}
|
|
31
|
+
/>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Sizes — `default` for forms, `sm` / `xs` for dense panels:
|
|
35
|
+
|
|
36
|
+
```jsx
|
|
37
|
+
<div className="grid gap-2">
|
|
38
|
+
<Input size="default" placeholder="Default (h-9)" />
|
|
39
|
+
<Input size="sm" placeholder="Small (h-8)" />
|
|
40
|
+
<Input size="xs" placeholder="Extra small (h-7)" />
|
|
41
|
+
</div>
|
|
42
|
+
```
|
package/components/ui/label.md
CHANGED
|
@@ -3,8 +3,9 @@ name: Label
|
|
|
3
3
|
import: "@gradeui/ui"
|
|
4
4
|
props:
|
|
5
5
|
- htmlFor?: string — binds to the input's id
|
|
6
|
+
- size?: "default" | "sm" | "xs" — text size, mirrors Input/Select/Textarea so a field and its label scale together. default = text-sm; xs = 11px for dense tool panels.
|
|
6
7
|
- All native label HTML attrs
|
|
7
|
-
when_to_use: Every Input / Textarea / Checkbox / Switch / RadioGroup. Always use htmlFor so clicking the label focuses the control.
|
|
8
|
+
when_to_use: Every Input / Textarea / Checkbox / Switch / RadioGroup. Always use htmlFor so clicking the label focuses the control. Match `size` to the field it labels (size="xs" label over a size="xs" input).
|
|
8
9
|
composes_with: [Input, Textarea, Checkbox, Switch, RadioGroup, Select]
|
|
9
10
|
aliases: [label, form label, field label, caption]
|
|
10
11
|
---
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Logo
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: []
|
|
5
|
+
props:
|
|
6
|
+
- sources?: LogoSources — artwork keyed by lockup then appearance:
|
|
7
|
+
{ square?: { light?, dark?, mono? }, horizontal?: {...}, icon?: {...} }.
|
|
8
|
+
Each slot is any node (inline <svg>, <img>, component). Omit entirely
|
|
9
|
+
and a neutral "Logo" placeholder renders (use this in prototypes
|
|
10
|
+
before real artwork exists).
|
|
11
|
+
- lockup?: "square" | "horizontal" | "icon" (default "horizontal")
|
|
12
|
+
- mode?: "light" | "dark" (default "light") — the background the logo sits on
|
|
13
|
+
- mono?: boolean (default false) — use the single-colour artwork (inherits currentColor)
|
|
14
|
+
- size?: "sm" | "md" | "lg" | "xl" | number (default "md") — height; width is intrinsic
|
|
15
|
+
- label?: string — accessible name (brand name); becomes aria-label + role="img"
|
|
16
|
+
- decorative?: boolean — aria-hidden when the name is already nearby
|
|
17
|
+
- href?: string — renders the logo as a link (logo-links-home)
|
|
18
|
+
- className?: string
|
|
19
|
+
when_to_use: A brand mark with built-in variations — a square mark for tight
|
|
20
|
+
spaces, a horizontal lockup for headers, monochrome for busy/inverted
|
|
21
|
+
surfaces. Reach for Logo in toolbars, sidenav headers, and footers instead
|
|
22
|
+
of dropping a bare <img>, so the lockup and on-dark/on-light treatment are
|
|
23
|
+
switchable by prop. The artwork is supplied by the consumer; Logo just picks
|
|
24
|
+
the right slot for the context.
|
|
25
|
+
composes_with: [AppShell, AppShellHeader, Sidebar, SidebarHeader, Row, Stack]
|
|
26
|
+
aliases: [logo, brand, brandmark, wordmark, lockup, brand logo, app logo, logotype]
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
```jsx
|
|
30
|
+
// Sidenav header: square mark when collapsed, horizontal when expanded.
|
|
31
|
+
// Supply your own artwork per slot; here inline SVGs stand in.
|
|
32
|
+
<Logo
|
|
33
|
+
lockup="horizontal"
|
|
34
|
+
mode="dark"
|
|
35
|
+
size="md"
|
|
36
|
+
label="Acme"
|
|
37
|
+
sources={{
|
|
38
|
+
square: { light: <AcmeSquare />, dark: <AcmeSquareWhite /> },
|
|
39
|
+
horizontal: { light: <AcmeWide />, dark: <AcmeWideWhite /> },
|
|
40
|
+
icon: { mono: <AcmeGlyph /> },
|
|
41
|
+
}}
|
|
42
|
+
/>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Anti-patterns
|
|
46
|
+
|
|
47
|
+
DO NOT drop a bare `<img src="logo.png">` in a toolbar/sidenav/footer when you
|
|
48
|
+
want light/dark or square/horizontal switching — use `<Logo>` so the variant
|
|
49
|
+
is a prop.
|
|
50
|
+
|
|
51
|
+
DO NOT invert a colour logo with a CSS filter to fake a dark version — supply
|
|
52
|
+
the brand's real `dark` artwork in the `sources` slot.
|
|
53
|
+
|
|
54
|
+
DO NOT set both `label` and `decorative` — `decorative` hides the logo from
|
|
55
|
+
assistive tech; `label` names it. Pick one (name it unless the brand name is
|
|
56
|
+
already in the DOM right beside it).
|
|
57
|
+
|
|
58
|
+
DO NOT hardcode a width — `size` sets the height and the artwork keeps its own
|
|
59
|
+
aspect ratio (square/icon are 1:1, horizontal stays wide).
|
package/components/ui/message.md
CHANGED
|
@@ -13,6 +13,7 @@ props:
|
|
|
13
13
|
- threadCount?: number — renders a "N replies" link affordance below the body
|
|
14
14
|
- onThreadClick?: () => void — handler for the threadCount affordance
|
|
15
15
|
- align?: "start" | "end" — `start` (default) puts the avatar on the left; `end` mirrors for "your messages" in DM threads
|
|
16
|
+
- density?: "default" | "compact" — `default` is the canonical chat / channel-feed rhythm; `compact` tightens text sizes + gaps for dense side panels (Studio comments, activity feeds). Pair with `Avatar size="xs"` for the tightest stack.
|
|
16
17
|
- children: ReactNode — body content (plain text or rich nodes)
|
|
17
18
|
- className?: string
|
|
18
19
|
when_to_use: |
|
|
@@ -156,6 +157,39 @@ aliases: [
|
|
|
156
157
|
</Stack>
|
|
157
158
|
```
|
|
158
159
|
|
|
160
|
+
```jsx
|
|
161
|
+
// Compact density — for narrow side panels (Studio Comments tab,
|
|
162
|
+
// activity feeds, notification rows). Notice the smaller Avatar size
|
|
163
|
+
// pairs naturally with density="compact".
|
|
164
|
+
<Stack gap="sm">
|
|
165
|
+
<Message
|
|
166
|
+
density="compact"
|
|
167
|
+
author="alice"
|
|
168
|
+
timestamp="2m ago"
|
|
169
|
+
edited="· edited 1m ago"
|
|
170
|
+
avatar={
|
|
171
|
+
<Avatar size="xs">
|
|
172
|
+
<AvatarFallback tone="violet">A</AvatarFallback>
|
|
173
|
+
</Avatar>
|
|
174
|
+
}
|
|
175
|
+
>
|
|
176
|
+
Splitting this into two PRs makes the review tractable.
|
|
177
|
+
</Message>
|
|
178
|
+
<Message
|
|
179
|
+
density="compact"
|
|
180
|
+
author="ben"
|
|
181
|
+
timestamp="1m ago"
|
|
182
|
+
avatar={
|
|
183
|
+
<Avatar size="xs">
|
|
184
|
+
<AvatarFallback tone="amber">B</AvatarFallback>
|
|
185
|
+
</Avatar>
|
|
186
|
+
}
|
|
187
|
+
>
|
|
188
|
+
Agreed. I'll take the schema PR.
|
|
189
|
+
</Message>
|
|
190
|
+
</Stack>
|
|
191
|
+
```
|
|
192
|
+
|
|
159
193
|
## Anti-patterns
|
|
160
194
|
|
|
161
195
|
```jsx
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: RadioCard
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- value: string (required) — the radio value
|
|
6
|
+
- label?: ReactNode — title line
|
|
7
|
+
- description?: ReactNode — secondary line
|
|
8
|
+
- aside?: ReactNode — slot before the indicator (a Badge, price, hint)
|
|
9
|
+
- hideIndicator?: boolean — hide the dot; selection shown by the card border + background
|
|
10
|
+
- indicatorPosition?: "leading" | "trailing" — default trailing
|
|
11
|
+
- children?: ReactNode — arbitrary static content (image, custom layout) instead of label/description
|
|
12
|
+
when_to_use: Single-select where each option is a whole selectable card (shipping options, plan picker, onboarding choices). The whole card is the control, so focus and the checked state live on the card surface and the entire card is clickable. MUST sit inside a RadioGroup (keeps roving focus + single-select). Static content only — never nest an interactive control (Slider/Input/Button/link) inside. For a plain radio + label row use Field instead.
|
|
13
|
+
composes_with: [RadioGroup (required parent), Badge (in aside), MediaSurface (custom children)]
|
|
14
|
+
aliases: [radio card, selectable card, option card, plan picker, choice card, pricing tier, segmented choice card]
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
<RadioGroup defaultValue="standard" className="grid gap-3">
|
|
19
|
+
<RadioCard value="standard" label="Standard" description="4–10 business days" />
|
|
20
|
+
<RadioCard value="fast" label="Fast" description="2–5 business days" />
|
|
21
|
+
<RadioCard value="next-day" label="Next day" description="1 business day" />
|
|
22
|
+
</RadioGroup>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Indicator on the leading edge instead of trailing:
|
|
26
|
+
|
|
27
|
+
```jsx
|
|
28
|
+
<RadioGroup defaultValue="standard" className="grid gap-3">
|
|
29
|
+
<RadioCard value="standard" indicatorPosition="leading" label="Standard" description="4–10 business days" />
|
|
30
|
+
<RadioCard value="fast" indicatorPosition="leading" label="Fast" description="2–5 business days" />
|
|
31
|
+
</RadioGroup>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
No visible dot (selection reads from the card border + background), laid out in a grid via className on the group:
|
|
35
|
+
|
|
36
|
+
```jsx
|
|
37
|
+
<RadioGroup defaultValue="m" className="grid grid-cols-2 gap-3">
|
|
38
|
+
<RadioCard value="s" hideIndicator label="Small" description="Up to 10 seats" />
|
|
39
|
+
<RadioCard value="m" hideIndicator label="Medium" description="Up to 50 seats" />
|
|
40
|
+
</RadioGroup>
|
|
41
|
+
```
|
|
@@ -12,8 +12,8 @@ props:
|
|
|
12
12
|
- RadioGroupItem: value: string — what the group emits when this item is picked
|
|
13
13
|
- RadioGroupItem: id?: string — pair with a <Label htmlFor> for click-on-label
|
|
14
14
|
- RadioGroupItem: disabled?: boolean
|
|
15
|
-
when_to_use: A small set of mutually-exclusive options where the user needs to SEE all of them at once — pricing tiers (3-4 options), shipping speed, payment method radio cards. For 5+ options use Select. For a segmented control as part of a toolbar use ToggleGroup. For yes/no use Switch.
|
|
16
|
-
composes_with: [Label (paired with each item via htmlFor),
|
|
15
|
+
when_to_use: A small set of mutually-exclusive options where the user needs to SEE all of them at once — pricing tiers (3-4 options), shipping speed, payment method radio cards. When each option should be a whole clickable card (label + description, selected state on the card), use RadioCard inside the RadioGroup instead of a Card with a radio in the corner. For a plain label + description row, wrap RadioGroupItem in Field. For 5+ options use Select. For a segmented control as part of a toolbar use ToggleGroup. For yes/no use Switch.
|
|
16
|
+
composes_with: [Label (paired with each item via htmlFor), Field (label + description row), RadioCard (whole-card selectable option), Stack (vertical list)]
|
|
17
17
|
aliases: [radio group, radio buttons, single-choice, pricing options, payment method, radio buttons, radio control, single-select]
|
|
18
18
|
---
|
|
19
19
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ScreenAnimator
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
subcomponents: []
|
|
5
|
+
props:
|
|
6
|
+
- shots?: Array<{ zoom?, cx?, cy?, hold?, trans?, label? }> — the tour. Each
|
|
7
|
+
shot is a zoom (1 = fit, >1 push in), focal point cx/cy (0..1 fractions of
|
|
8
|
+
the content), hold (ms dwell), trans (ms glide-in), and a caption label.
|
|
9
|
+
Omit for a static framed view.
|
|
10
|
+
- autoplay?: boolean (default true)
|
|
11
|
+
- loop?: boolean (default true) — fly in → shots → back to start → exit → repeat
|
|
12
|
+
- controls?: boolean (default true) — play / pause / restart transport
|
|
13
|
+
- spotlight?: boolean (default false) — opt in to dim the edges (vignette) when pushed in
|
|
14
|
+
- cursor?: boolean (default true) — synthetic cursor pulse on detail shots
|
|
15
|
+
- enter?: boolean (default true) — fly in from offscreen on start
|
|
16
|
+
- stage?: string — CSS background of the stage behind the screen (default dark)
|
|
17
|
+
- backdrop?: React.ReactNode — a live layer behind the content (image, gradient, or a <ThreeScene> shader)
|
|
18
|
+
- className?: string
|
|
19
|
+
- children: React.ReactNode (the screen to animate)
|
|
20
|
+
when_to_use: Wrap ANY screen or section in a directed camera — a "live demo
|
|
21
|
+
director". Give it a list of shots and it tours them (zoom + pan) over the
|
|
22
|
+
live, still-interactive content, with a focus spotlight, captions, a synthetic
|
|
23
|
+
cursor, and play/pause. Use it to turn a built screen into an auto-playing
|
|
24
|
+
product demo (embed it, or drop it on a marketing page). It's the live,
|
|
25
|
+
editable, re-renderable answer to a screen-recording video.
|
|
26
|
+
composes_with: [AppShell, ThreeScene, Card, Grid, the whole component set (it wraps a screen)]
|
|
27
|
+
aliases: [screen animator, camera, camera tour, director, demo, product demo, zoom pan, spotlight, ken burns, presenter]
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
```jsx
|
|
31
|
+
// Wrap a live screen; the camera tours the shots and loops.
|
|
32
|
+
<ScreenAnimator
|
|
33
|
+
shots={[
|
|
34
|
+
{ zoom: 1, cx: 0.5, cy: 0.5, hold: 2400, label: "Overview" },
|
|
35
|
+
{ zoom: 2.4, cx: 0.2, cy: 0.34, hold: 2600, label: "Revenue up 24%" },
|
|
36
|
+
{ zoom: 1.8, cx: 0.5, cy: 0.6, hold: 2800, label: "Pipeline" },
|
|
37
|
+
]}
|
|
38
|
+
backdrop={<ThreeScene preset="aurora" />}
|
|
39
|
+
>
|
|
40
|
+
<Dashboard />
|
|
41
|
+
</ScreenAnimator>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Anti-patterns
|
|
45
|
+
|
|
46
|
+
DO NOT use it as a layout wrapper — it positions `absolute inset-0` and takes
|
|
47
|
+
over the frame. It's for a whole screen/section you want to direct, not a div.
|
|
48
|
+
|
|
49
|
+
DO NOT hand-tune `trans`/`hold` per shot unless you need to — the defaults
|
|
50
|
+
(soft settle on overview, snappier push on detail) read well. `cx`/`cy` are the
|
|
51
|
+
knobs that matter; they're fractions of the screen (0 = left/top, 0.5 = centre).
|
|
52
|
+
|
|
53
|
+
DO NOT worry about reduced motion — it settles on the starter frame and stops
|
|
54
|
+
moving automatically under `prefers-reduced-motion`.
|
package/components/ui/select.md
CHANGED
|
@@ -4,11 +4,11 @@ import: "@gradeui/ui"
|
|
|
4
4
|
subcomponents: [SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectLabel, SelectSeparator]
|
|
5
5
|
props:
|
|
6
6
|
- Select: value?, onValueChange?, defaultValue?, disabled? — Radix root
|
|
7
|
-
- SelectTrigger: wraps the clickable control
|
|
7
|
+
- SelectTrigger: size?: "default" | "sm" | "xs" — control density; wraps the clickable control, nest SelectValue inside
|
|
8
8
|
- SelectValue: placeholder?: string — text when nothing is selected
|
|
9
|
-
- SelectContent:
|
|
10
|
-
- SelectItem: value: string — required; content is the label
|
|
11
|
-
when_to_use: Single-choice from 3+ known options. Fewer than 3 → RadioGroup. Huge list with search → use a Combobox (not in DS yet). Multi-select → not supported by this primitive.
|
|
9
|
+
- SelectContent: size?: "default" | "sm" | "xs" — menu density; cascades to every SelectItem inside via context so a compact trigger gets a compact menu. Accepts items via children.
|
|
10
|
+
- SelectItem: value: string — required; content is the label. Inherits density from SelectContent.
|
|
11
|
+
when_to_use: Single-choice from 3+ known options. Fewer than 3 → RadioGroup. Huge list with search → use a Combobox (not in DS yet). Multi-select → not supported by this primitive. In dense tool panels, set size="xs" on BOTH the trigger and the content so the closed control and open menu match.
|
|
12
12
|
composes_with: [Label (above SelectTrigger), Form, Card]
|
|
13
13
|
aliases: [dropdown, combobox, picker, select, pop-up button, popup button, popup picker, picker view, rnpickerselect, react native picker, native picker]
|
|
14
14
|
---
|
|
@@ -22,3 +22,15 @@ aliases: [dropdown, combobox, picker, select, pop-up button, popup button, popup
|
|
|
22
22
|
</SelectContent>
|
|
23
23
|
</Select>
|
|
24
24
|
```
|
|
25
|
+
|
|
26
|
+
Compact, for dense panels — match the trigger and menu density:
|
|
27
|
+
|
|
28
|
+
```jsx
|
|
29
|
+
<Select defaultValue="md">
|
|
30
|
+
<SelectTrigger size="xs"><SelectValue /></SelectTrigger>
|
|
31
|
+
<SelectContent size="xs">
|
|
32
|
+
<SelectItem value="sm">Small</SelectItem>
|
|
33
|
+
<SelectItem value="md">Medium</SelectItem>
|
|
34
|
+
</SelectContent>
|
|
35
|
+
</Select>
|
|
36
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: SwitchCard
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- checked? / defaultChecked? / onCheckedChange? — standard switch state
|
|
6
|
+
- label?: ReactNode — title line
|
|
7
|
+
- description?: ReactNode — secondary line
|
|
8
|
+
- aside?: ReactNode — slot before the indicator (a Badge, price, hint)
|
|
9
|
+
- hideIndicator?: boolean — hide the switch glyph; state shown by the card border + background
|
|
10
|
+
- indicatorPosition?: "leading" | "trailing" — default trailing
|
|
11
|
+
- children?: ReactNode — arbitrary static content instead of label/description
|
|
12
|
+
when_to_use: A prominent on/off setting presented as a whole selectable card. The whole card is the switch, so the toggled state lives on the card surface. Standalone. For a row of compact settings (label left, small Switch right) use Field layout="setting" instead — SwitchCard is for the heavier, card-sized toggle.
|
|
13
|
+
composes_with: [Badge (in aside), Stack (stacking several)]
|
|
14
|
+
aliases: [switch card, toggle card, setting card, feature toggle card]
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
<SwitchCard label="Auto-renew" description="Renew this plan automatically each month" defaultChecked />
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Indicator on the leading edge:
|
|
22
|
+
|
|
23
|
+
```jsx
|
|
24
|
+
<SwitchCard
|
|
25
|
+
indicatorPosition="leading"
|
|
26
|
+
label="Auto-renew"
|
|
27
|
+
description="Renew this plan automatically each month"
|
|
28
|
+
defaultChecked
|
|
29
|
+
/>
|
|
30
|
+
```
|
package/components/ui/switch.md
CHANGED
|
@@ -7,8 +7,8 @@ props:
|
|
|
7
7
|
- defaultChecked?: boolean
|
|
8
8
|
- disabled?: boolean
|
|
9
9
|
- id?: string
|
|
10
|
-
when_to_use: Instant on/off setting ("Enable notifications", "Dark mode"). Commits on toggle — no submit button needed. For selecting-from-a-list use Checkbox.
|
|
11
|
-
composes_with: [Label (via htmlFor), Card (settings rows)]
|
|
10
|
+
when_to_use: Instant on/off setting ("Enable notifications", "Dark mode"). Commits on toggle — no submit button needed. For selecting-from-a-list use Checkbox. For a settings row (label + description on the left, Switch on the right) use Field layout="setting". For a prominent on/off presented as a whole selectable card, use SwitchCard.
|
|
11
|
+
composes_with: [Label (via htmlFor), Field (layout="setting" settings row), SwitchCard (whole-card toggle), Card (settings rows)]
|
|
12
12
|
aliases: [toggle, switch, on/off switch, ios toggle, toggle switch, switch control, react native switch]
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
name: Textarea
|
|
3
3
|
import: "@gradeui/ui"
|
|
4
4
|
props:
|
|
5
|
+
- size?: "default" | "sm" | "xs" — control density, mirrors Input. default = min-h-80 / text-sm; sm and xs shrink the min-height + padding for dense panels.
|
|
5
6
|
- All native textarea HTML attrs (rows, value, onChange, placeholder, disabled)
|
|
6
|
-
when_to_use: Multi-line text entry (descriptions, messages, comments). Pair with a Label. Single-line input → use Input instead.
|
|
7
|
+
when_to_use: Multi-line text entry (descriptions, messages, comments). Pair with a Label. Single-line input → use Input instead. Use size="sm"/"xs" in dense tool panels.
|
|
7
8
|
composes_with: [Label, Form, Card (in CardContent)]
|
|
8
9
|
aliases: [text area, multiline, comment box, message field, text editor, multi-line text, multiline input, multiline text field, comments box, multiline textinput]
|
|
9
10
|
---
|