@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.
@@ -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
+ ```
@@ -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
+ ```
@@ -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).
@@ -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), Stack (vertical list), Card (radio card pattern)]
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`.
@@ -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; nest SelectValue inside
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: accepts items via children
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
+ ```
@@ -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
  ---