@gradeui/ui 1.2.0 → 1.3.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,109 @@
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? } — gradient stops (token names or CSS colours) + angle in degrees (default 135)
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 }}`. Tokens get wrapped in oklch() automatically.
58
+ - image — `src` + `fit` / `position`; set `repeat` (+ `tileSize`) for a tiled texture.
59
+ - video — `src` (autoplays muted + looped + inline).
60
+ - shader — `preset` OR `fragmentShader`, + `palette` / `postPreset`. Delegates to ThreeScene.
61
+
62
+ `opacity` + `blendMode` apply to every type — the same two controls as
63
+ the inspector's Blending section, so a loud shader/image can be dialled
64
+ back to a subtle wash behind text.
65
+ ---
66
+
67
+ ```jsx
68
+ // Shader background behind a hero, dialled back so text stays readable.
69
+ <section className="relative overflow-hidden rounded-xl">
70
+ <BackgroundFill
71
+ type="shader"
72
+ preset="mesh"
73
+ palette={{
74
+ primary: "oklch(var(--primary))",
75
+ secondary: "oklch(var(--accent))",
76
+ accent: "oklch(var(--primary))",
77
+ background: "oklch(var(--foreground))",
78
+ }}
79
+ opacity={0.35}
80
+ />
81
+ <div className="relative z-10 p-12">
82
+ <h1 className="text-4xl font-bold">Build at the speed of thought</h1>
83
+ </div>
84
+ </section>
85
+ ```
86
+
87
+ ```jsx
88
+ // Gradient wash on a card.
89
+ <Card className="relative overflow-hidden">
90
+ <BackgroundFill type="gradient" gradient={{ from: "primary", to: "accent", angle: 120 }} opacity={0.18} />
91
+ <CardContent className="relative z-10">…</CardContent>
92
+ </Card>
93
+ ```
94
+
95
+ ```jsx
96
+ // Image background, cover-fit, with a blend mode.
97
+ <div className="relative h-64 overflow-hidden rounded-lg">
98
+ <BackgroundFill type="image" src="/hero.jpg" fit="cover" blendMode="multiply" />
99
+ <div className="relative z-10 p-6 text-white">Featured</div>
100
+ </div>
101
+ ```
102
+
103
+ ```jsx
104
+ // Tiled texture.
105
+ <div className="relative overflow-hidden">
106
+ <BackgroundFill type="image" src="/noise.png" repeat tileSize="160px" opacity={0.08} />
107
+ <div className="relative z-10">…</div>
108
+ </div>
109
+ ```
@@ -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,57 @@
1
+ ---
2
+ name: Logo
3
+ import: "@gradeui/ui"
4
+ subcomponents: []
5
+ props:
6
+ - sources: LogoSources (required) — artwork keyed by lockup then appearance:
7
+ { square?: { light?, dark?, mono? }, horizontal?: {...}, icon?: {...} }.
8
+ Each slot is any node (inline <svg>, <img>, component).
9
+ - lockup?: "square" | "horizontal" | "icon" (default "horizontal")
10
+ - mode?: "light" | "dark" (default "light") — the background the logo sits on
11
+ - mono?: boolean (default false) — use the single-colour artwork (inherits currentColor)
12
+ - size?: "sm" | "md" | "lg" | "xl" | number (default "md") — height; width is intrinsic
13
+ - label?: string — accessible name (brand name); becomes aria-label + role="img"
14
+ - decorative?: boolean — aria-hidden when the name is already nearby
15
+ - href?: string — renders the logo as a link (logo-links-home)
16
+ - className?: string
17
+ when_to_use: A brand mark with built-in variations — a square mark for tight
18
+ spaces, a horizontal lockup for headers, monochrome for busy/inverted
19
+ surfaces. Reach for Logo in toolbars, sidenav headers, and footers instead
20
+ of dropping a bare <img>, so the lockup and on-dark/on-light treatment are
21
+ switchable by prop. The artwork is supplied by the consumer; Logo just picks
22
+ the right slot for the context.
23
+ composes_with: [AppShell, AppShellHeader, Sidebar, SidebarHeader, Row, Stack]
24
+ aliases: [logo, brand, brandmark, wordmark, lockup, brand logo, app logo, logotype]
25
+ ---
26
+
27
+ ```jsx
28
+ // Sidenav header: square mark when collapsed, horizontal when expanded.
29
+ // Supply your own artwork per slot; here inline SVGs stand in.
30
+ <Logo
31
+ lockup="horizontal"
32
+ mode="dark"
33
+ size="md"
34
+ label="Acme"
35
+ sources={{
36
+ square: { light: <AcmeSquare />, dark: <AcmeSquareWhite /> },
37
+ horizontal: { light: <AcmeWide />, dark: <AcmeWideWhite /> },
38
+ icon: { mono: <AcmeGlyph /> },
39
+ }}
40
+ />
41
+ ```
42
+
43
+ ### Anti-patterns
44
+
45
+ DO NOT drop a bare `<img src="logo.png">` in a toolbar/sidenav/footer when you
46
+ want light/dark or square/horizontal switching — use `<Logo>` so the variant
47
+ is a prop.
48
+
49
+ DO NOT invert a colour logo with a CSS filter to fake a dark version — supply
50
+ the brand's real `dark` artwork in the `sources` slot.
51
+
52
+ DO NOT set both `label` and `decorative` — `decorative` hides the logo from
53
+ assistive tech; `label` names it. Pick one (name it unless the brand name is
54
+ already in the DOM right beside it).
55
+
56
+ DO NOT hardcode a width — `size` sets the height and the artwork keeps its own
57
+ 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
 
@@ -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
  ---
@@ -2,7 +2,7 @@
2
2
  name: ThreeScene
3
3
  import: "@gradeui/ui"
4
4
  props:
5
- - preset?: "space" | "plasma" | "voronoi" | "synthwave" — shader preset id from the registry
5
+ - preset?: "mesh" | "waves" | "space" | "plasma" | "voronoi" | "synthwave" — shader preset id from the registry
6
6
  - fragmentShader?: string — user-authored GLSL body; takes precedence over preset
7
7
  - onShaderError?: (error: ShaderCompileError) => void — fires on compile failure; scene falls back to `preset="space"`
8
8
  - postPreset?: "none" | "vhs" | "cinematic" | "synthwave" | "crt" (default "vhs") — post-processing pass
@@ -23,6 +23,8 @@ notes: |
23
23
  ## Path 1 — `preset` (pick one, fastest, highest quality)
24
24
 
25
25
  Valid `preset` ids (complete list — do NOT invent any others):
26
+ - "mesh" — smooth moving blobs of primary/secondary/accent over the background; soft, theme-reactive. THE default soft background. Default post: "none".
27
+ - "waves" — flowing banded ribbons rippling across the surface; clean motion for headers/heroes. Default post: "none".
26
28
  - "space" — Hyperspace starfield, streaking stars. Default post: "vhs".
27
29
  - "plasma" — soft rolling colour clouds, ambient/abstract. Default post: "synthwave".
28
30
  - "voronoi" — jittered cellular grid with glowing edges. Default post: "crt".
@@ -118,6 +120,25 @@ notes: |
118
120
  ## Fullscreen backgrounds
119
121
 
120
122
  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.
123
+
124
+ ## Layering & tweakable params (direction)
125
+
126
+ Shaders are composable, not monolithic. A rendered visual is a BASE
127
+ layer (the generative scene — gradient, dots, waves, space…) plus a
128
+ stack of EFFECT layers applied on top (grain, dither, vignette,
129
+ chromatic…). This is the same model as the post-FX composer — an
130
+ effect is independent of the base it sits over, so e.g. `grain`
131
+ applies to ALL bases (mix-and-match).
132
+
133
+ Every layer — base and effect — declares a `params: ParamSpec[]`
134
+ schema (see lib/three/types.ts): `range` (slider + number),
135
+ `segmented`, `select`, `toggle`, `color`, `colorList`. A param's
136
+ `key` doubles as the GLSL uniform name, so values map to uniforms
137
+ generically. The same `ParamSpec` shape is what a controls panel
138
+ renders from — the Paper-style "Presets + sliders + swatches" panel —
139
+ and is the canonical "this section is a form" descriptor shared with
140
+ the inspector controls kit (Input slots, sized Select, segmented
141
+ control, slider+number).
121
142
  ---
122
143
 
123
144
  ```jsx