@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.
- package/components/ui/background-fill.md +109 -0
- package/components/ui/checkbox-card.md +43 -0
- package/components/ui/checkbox.md +2 -2
- package/components/ui/code.md +2 -1
- package/components/ui/composer.md +226 -0
- package/components/ui/dropdown-menu.md +2 -0
- package/components/ui/field.md +26 -0
- package/components/ui/fill-picker.md +36 -0
- package/components/ui/input.md +27 -2
- package/components/ui/label.md +2 -1
- package/components/ui/logo.md +57 -0
- package/components/ui/message.md +263 -0
- package/components/ui/radio-card.md +41 -0
- package/components/ui/radio-group.md +2 -2
- package/components/ui/select.md +16 -4
- package/components/ui/switch-card.md +30 -0
- package/components/ui/switch.md +2 -2
- package/components/ui/textarea.md +2 -1
- package/components/ui/three-scene.md +22 -1
- package/dist/contracts.js +48 -4
- package/dist/contracts.js.map +1 -1
- package/dist/contracts.mjs +48 -4
- package/dist/contracts.mjs.map +1 -1
- package/dist/index.d.mts +1255 -39
- package/dist/index.d.ts +1255 -39
- package/dist/index.js +122 -47
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +122 -47
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +3 -1
- package/dist/tailwind-preset.js +1 -1
- package/dist/tailwind-preset.js.map +1 -1
- package/dist/tailwind-preset.mjs +1 -1
- package/dist/tailwind-preset.mjs.map +1 -1
- package/package.json +11 -1
|
@@ -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
|
|
package/components/ui/code.md
CHANGED
|
@@ -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`.
|
|
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
|
+
```
|
package/components/ui/input.md
CHANGED
|
@@ -3,10 +3,13 @@ name: Input
|
|
|
3
3
|
import: "@gradeui/ui"
|
|
4
4
|
props:
|
|
5
5
|
- type?: string (text | email | password | number | search | url | tel | date)
|
|
6
|
+
- size?: "default" | "sm" | "xs" — control density. `default` (h-9) for forms; `sm` (h-8) and `xs` (h-7) for dense tool panels like the inspector.
|
|
7
|
+
- startSlot?: ReactNode — adornment rendered inside the leading edge (icon, prefix, currency symbol). Non-interactive by default so clicks focus the input.
|
|
8
|
+
- endSlot?: ReactNode — adornment rendered inside the trailing edge (unit like "px", a clear button, a stepper). Same pointer rules as startSlot.
|
|
6
9
|
- All native input HTML attrs (value, onChange, placeholder, disabled, required)
|
|
7
|
-
when_to_use: Any single-line text entry. Always pair with a Label for accessibility.
|
|
10
|
+
when_to_use: Any single-line text entry. Always pair with a Label for accessibility. Use startSlot/endSlot for icons, prefixes and units instead of hand-positioning absolute children; use size="sm"/"xs" in dense tool panels.
|
|
8
11
|
composes_with: [Label, Form, Card (in CardContent), Button (form submit)]
|
|
9
|
-
aliases: [text field, textbox, textfield, form field, text input, secure field, search field, url field, number field, textinput, text input field, react native textinput]
|
|
12
|
+
aliases: [text field, textbox, textfield, form field, text input, secure field, search field, url field, number field, textinput, text input field, react native textinput, unit input, input with icon]
|
|
10
13
|
---
|
|
11
14
|
|
|
12
15
|
```jsx
|
|
@@ -15,3 +18,25 @@ aliases: [text field, textbox, textfield, form field, text input, secure field,
|
|
|
15
18
|
<Input id="email" type="email" placeholder="you@example.com" />
|
|
16
19
|
</div>
|
|
17
20
|
```
|
|
21
|
+
|
|
22
|
+
Slots — a leading icon and a trailing unit, no manual positioning:
|
|
23
|
+
|
|
24
|
+
```jsx
|
|
25
|
+
<Input
|
|
26
|
+
size="sm"
|
|
27
|
+
type="number"
|
|
28
|
+
placeholder="0"
|
|
29
|
+
startSlot={<Ruler className="size-4" />}
|
|
30
|
+
endSlot={<span className="text-xs text-muted-foreground">px</span>}
|
|
31
|
+
/>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Sizes — `default` for forms, `sm` / `xs` for dense panels:
|
|
35
|
+
|
|
36
|
+
```jsx
|
|
37
|
+
<div className="grid gap-2">
|
|
38
|
+
<Input size="default" placeholder="Default (h-9)" />
|
|
39
|
+
<Input size="sm" placeholder="Small (h-8)" />
|
|
40
|
+
<Input size="xs" placeholder="Extra small (h-7)" />
|
|
41
|
+
</div>
|
|
42
|
+
```
|
package/components/ui/label.md
CHANGED
|
@@ -3,8 +3,9 @@ name: Label
|
|
|
3
3
|
import: "@gradeui/ui"
|
|
4
4
|
props:
|
|
5
5
|
- htmlFor?: string — binds to the input's id
|
|
6
|
+
- size?: "default" | "sm" | "xs" — text size, mirrors Input/Select/Textarea so a field and its label scale together. default = text-sm; xs = 11px for dense tool panels.
|
|
6
7
|
- All native label HTML attrs
|
|
7
|
-
when_to_use: Every Input / Textarea / Checkbox / Switch / RadioGroup. Always use htmlFor so clicking the label focuses the control.
|
|
8
|
+
when_to_use: Every Input / Textarea / Checkbox / Switch / RadioGroup. Always use htmlFor so clicking the label focuses the control. Match `size` to the field it labels (size="xs" label over a size="xs" input).
|
|
8
9
|
composes_with: [Input, Textarea, Checkbox, Switch, RadioGroup, Select]
|
|
9
10
|
aliases: [label, form label, field label, caption]
|
|
10
11
|
---
|
|
@@ -0,0 +1,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).
|