@gradeui/ui 1.1.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
 
@@ -18,8 +18,9 @@ props:
18
18
  - filename?: string — optional label rendered in the header chrome
19
19
  - wrap?: boolean — wrap long lines instead of horizontal scroll
20
20
  - bare?: boolean — drop chrome (border, header, padding) — for inline use
21
+ - size? (xs | sm | md) — type-scale preset. `xs` (12px) for dense changelog cards / inline blocks; `sm` (14px, default) for marketing heroes and docs; `md` (16px) for focal-point displays.
21
22
  - height? (auto | number | string) — container sizing. `auto` (default) grows with content. Number = pixels (`300` → `300px`). String passes through as CSS (`"20rem"`, `"50vh"`).
22
- - maxLines?: number — cap the visible line count at exactly N line-heights. Wins over `height`. Use for terminal windows, code-tour cards, and surfaces that need a stable vertical rhythm regardless of snippet length.
23
+ - maxLines?: number — cap the visible line count at exactly N line-heights. Wins over `height`. Inherits the current size's line-height automatically.
23
24
  when_to_use: Read-only code surface for marketing heroes, docs, changelog entries, AI-output displays. Use `diff` for the "diff hero" pattern (before/after side-by-side or stacked). Use `reveal="lines"` with `trigger="inView"` for scroll-driven marketing pages. Use `reveal="typewriter"` for AI-output / chat-style displays. Use `bare` for inline code inside prose. NOT a code editor — for editable code, reach for an external editor primitive (CodeMirror / Monaco).
24
25
  composes_with: [SectionBlock, Card, Tabs (for multi-file examples), Carousel (slide-to-slide code progression)]
25
26
  aliases: [code block, code, code snippet, code surface, syntax highlighted code, diff hero, diff view, diff block, changelog code, before after code, scroll-triggered code, typewriter code]
@@ -0,0 +1,226 @@
1
+ ---
2
+ name: Composer
3
+ import: "@gradeui/ui"
4
+ props:
5
+ - placeholder?: string
6
+ - initialText?: string — plain text content to seed on mount
7
+ - initialJson?: string — Lexical state JSON (from a previous onSubmit round-trip)
8
+ - formats?: ComposerFormat[] | false — available formats (defaults to bold/italic/underline/strikethrough/code/h1/h2/blockquote/ul/ol); pass false for plain text only
9
+ - toolbar?: boolean | "top" — show the formatting toolbar above the editor; default false
10
+ - triggers?: ComposerTriggerConfig[] — mention/slash configs, eg. `[{ char: "@", items: people }, { char: "/", items: commands }]`
11
+ - attachments?: boolean | ComposerAttachmentConfig — enable image paste + paperclip when true/object; default off
12
+ - onSubmit?: (content: ComposerContent, attachments?: ComposerAttachment[]) => void
13
+ - isLoading?: boolean — disables editor, swaps default Send for Stop
14
+ - onStop?: () => void
15
+ - maxLength?: number
16
+ - autoFocus?: boolean
17
+ - submitOnEnter?: boolean — default true (Shift-Enter still inserts newline)
18
+ - leftActions?: ReactNode — override the default paperclip
19
+ - rightActions?: ReactNode — override the default Send/Stop
20
+ - hideSend?: boolean — hide the default Send without replacing it
21
+ - steps?: ComposerStep[] — scripted demo sequence
22
+ - trigger?: DemoTrigger — "mount" | "inView" | "manual"; default "mount"
23
+ - play?: boolean — for trigger="manual"
24
+ - speed?: DemoSpeed — "slow" | "normal" | "fast"; default "normal"
25
+ - loop?: boolean
26
+ - loopDelay?: number — ms between loop iterations, default 2000
27
+ - readOnly?: boolean — disables editing AND focusability; programmatic playback still works; use for marketing demos so the script doesn't steal focus
28
+ - bare?: boolean — strip the card chrome
29
+ - className?: string
30
+ when_to_use: |
31
+ THE PRIMITIVE for any text composition surface — Slack / Discord /
32
+ Teams chat input, AI chat / copilot prompt box, comment thread input,
33
+ GitHub / Linear / Jira comment box, Reddit / Twitter reply box,
34
+ Notion / Linear document body, email composer, post body, anywhere
35
+ a user types text and submits.
36
+
37
+ CONCRETE TEST — if you find yourself writing a `<textarea>` (or
38
+ `<Input>` styled tall) with a row of `<Bold>` / `<Italic>` /
39
+ `<Paperclip>` / `<Send>` buttons below or beside it, STOP. That is
40
+ `<Composer>`. Use it.
41
+
42
+ Common shapes:
43
+ Chat input with formatting + attachments + send
44
+ → <Composer formats={["bold","italic","code"]} toolbar attachments />
45
+ AI prompt box with paperclip + send
46
+ → <AIChatComposer /> (preset wrapping Composer)
47
+ Comment / reply input
48
+ → <ComposerReply triggers={[{char:"@", items: people}]} />
49
+ Document body editor
50
+ → <Composer toolbar formats={[...]} bare />
51
+
52
+ Built on Lexical for rich text, mentions, slash commands. The
53
+ `attachments` prop wires image paste + paperclip + chip preview row
54
+ with object URL lifecycle handled internally — don't roll that
55
+ plumbing yourself. The `triggers` prop wires @mentions and /slash
56
+ commands with a typeahead popover. The `formats` array picks which
57
+ toolbar buttons render when `toolbar` is on.
58
+
59
+ Shares the lib/demo step vocabulary with <Code> so scripted
60
+ typing/format/mention demos animate in the same rhythm as your
61
+ terminal demos.
62
+ composes_with: [AIChatComposer (preset wrapping this with paperclip + send + attachments), ComposerReply (preset for comment threads), AIChat (uses AIChatComposer internally), Card (host above for reply boxes), Avatar (in leftActions slot for "your" avatar next to the input)]
63
+ aliases: [
64
+ composer, message input, message bar, rich text editor, rich text input,
65
+ mention input, slash input, text editor, prompt input, comment composer,
66
+ comment input, reply input, reply box,
67
+ chat input, chat box, chat input bar, chat composer, chat field,
68
+ slack input, slack composer, slack message box, discord input,
69
+ discord composer, teams chat input, message composer, post composer,
70
+ textarea with toolbar, formatting input, formatted text input,
71
+ message field, send message input, write a message, compose message
72
+ ]
73
+ ---
74
+
75
+ ```jsx
76
+ // Plain text chat-style composer
77
+ <Composer
78
+ placeholder="Ask anything…"
79
+ onSubmit={(content) => send(content.text)}
80
+ formats={false}
81
+ />
82
+
83
+ // Comment composer with mentions
84
+ <Composer
85
+ placeholder="Add a comment…"
86
+ triggers={[{ char: "@", items: teamMembers }]}
87
+ onSubmit={(content) => postComment(content.text, content.mentions)}
88
+ submitOnEnter={false}
89
+ formats={["bold", "italic", "code"]}
90
+ toolbar
91
+ />
92
+
93
+ // AI chat composer with attachments, mentions AND slash commands
94
+ <Composer
95
+ placeholder="Describe a UI, or paste a screenshot…"
96
+ triggers={[
97
+ { char: "@", items: docs },
98
+ { char: "/", items: commands, stripTrigger: true },
99
+ ]}
100
+ attachments
101
+ onSubmit={(content, atts) => {
102
+ sendToAssistant(content.text, content.mentions, atts?.map(a => a.file));
103
+ }}
104
+ isLoading={isStreaming}
105
+ onStop={stop}
106
+ />
107
+
108
+ // Marketing demo — scripted playback
109
+ <Composer
110
+ placeholder="Type a message…"
111
+ triggers={[{ char: "@", items: [{ id: "1", value: "alice" }] }]}
112
+ steps={[
113
+ { type: "type", text: "Hey " },
114
+ { type: "mention", trigger: "@", value: "alice", query: "ali" },
115
+ { type: "type", text: ", check out " },
116
+ { type: "select", text: "check out" },
117
+ { type: "format", format: "italic" },
118
+ { type: "wait", ms: 800 },
119
+ { type: "submit" },
120
+ ]}
121
+ trigger="inView"
122
+ speed="normal"
123
+ loop
124
+ />
125
+ ```
126
+
127
+ ## Demo step vocabulary
128
+
129
+ Shares `type` / `wait` / `clear` with `<Code>` (driven by the same `useScriptedDemo` hook). Adds Composer-specific verbs:
130
+
131
+ - `{ type: "mention", trigger, value, query? }` — insert a mention/slash token. Pass `query` to show the typeahead in flight, then resolve to `value`.
132
+ - `{ type: "format", format }` — apply a format to the current selection.
133
+ - `{ type: "select", text }` — select a substring (precondition for `format`).
134
+ - `{ type: "newline" }` — insert a paragraph break.
135
+ - `{ type: "submit" }` — fire `onSubmit`.
136
+
137
+ ## Imperative handle
138
+
139
+ ```tsx
140
+ const ref = useRef<ComposerHandle>(null);
141
+ ref.current?.focus();
142
+ ref.current?.clear();
143
+ ref.current?.insert("…");
144
+ ref.current?.restart(); // replay scripted steps from the start
145
+ ref.current?.restart(3000); // replay after a 3s delay
146
+ ref.current?.getContent(); // { text, json, mentions }
147
+ ref.current?.getEditor(); // underlying Lexical editor (escape hatch)
148
+ ```
149
+
150
+ ## Themes
151
+
152
+ All colours read from CSS variables (`--gds-composer-*` palette in `globals.css`). The mention pills, toolbar buttons, attachment chips, and editor surface all rebrand with the active gradeui theme without component changes.
153
+
154
+ ## Anti-patterns
155
+
156
+ ```jsx
157
+ // ❌ Rolling a chat / Slack / Discord input as <textarea> + manual
158
+ // toolbar buttons + Send button. This is the EXACT shape Composer
159
+ // exists to consolidate — caught in the wild on a "Slack clone"
160
+ // generation where the model assembled this inline.
161
+ // Loses: attachment intake + object URL lifecycle, mention popover,
162
+ // slash commands, action-row slots, the Lexical state graph for
163
+ // rich content round-trip, the scripted-demo step machine.
164
+ <div className="border rounded-xl bg-card">
165
+ <textarea
166
+ placeholder="Message #general"
167
+ value={inputText}
168
+ onChange={(e) => setInputText(e.target.value)}
169
+ onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) handleSend(); }}
170
+ rows={3}
171
+ className="w-full bg-transparent p-3 resize-none focus:outline-none"
172
+ />
173
+ <Row justify="between" align="center" className="px-3 py-2 border-t">
174
+ <Row gap="xs">
175
+ <Button size="icon" variant="ghost"><Bold /></Button>
176
+ <Button size="icon" variant="ghost"><Italic /></Button>
177
+ <Button size="icon" variant="ghost"><List /></Button>
178
+ <Button size="icon" variant="ghost"><Smile /></Button>
179
+ <Button size="icon" variant="ghost"><Paperclip /></Button>
180
+ </Row>
181
+ <Button onClick={handleSend}>Send</Button>
182
+ </Row>
183
+ </div>
184
+
185
+ // ✅ The Grade way. Same shape, every affordance free.
186
+ <Composer
187
+ placeholder="Message #general"
188
+ formats={["bold", "italic", "code", "ul"]}
189
+ toolbar
190
+ attachments
191
+ triggers={[{ char: "@", items: teamMembers }]}
192
+ onSubmit={(content, atts) => handleSend(content.text, atts)}
193
+ />
194
+ ```
195
+
196
+ ```jsx
197
+ // ❌ Reaching for <Input> (single-line) for a multi-line chat / reply
198
+ // surface. Input is for one-line text fields. Use Composer for any
199
+ // surface where the user might type more than one line — chat,
200
+ // comments, post bodies.
201
+ <Input
202
+ placeholder="Reply to thread…"
203
+ value={reply}
204
+ onChange={(e) => setReply(e.target.value)}
205
+ />
206
+ <Button onClick={postReply}>Reply</Button>
207
+
208
+ // ✅ ComposerReply preset has the right defaults for a reply box.
209
+ <ComposerReply
210
+ placeholder="Reply to thread…"
211
+ triggers={[{ char: "@", items: people }]}
212
+ onSubmit={(content) => postReply(content.text)}
213
+ />
214
+ ```
215
+
216
+ ```jsx
217
+ // ❌ Importing TipTap, Lexical, Slate, or any other editor framework
218
+ // directly into a scaffold. Composer already wraps Lexical and
219
+ // handles all the plumbing.
220
+ import { useEditor, EditorContent } from "@tiptap/react";
221
+ const editor = useEditor({ extensions: [StarterKit, ...] });
222
+ <EditorContent editor={editor} />
223
+
224
+ // ✅ Use Composer. Same capability, integrated with the design system.
225
+ <Composer toolbar formats={["bold", "italic", "h1", "h2", "blockquote", "ul", "ol"]} />
226
+ ```
@@ -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).