@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,263 @@
1
+ ---
2
+ name: Message
3
+ import: "@gradeui/ui"
4
+ props:
5
+ - author: string — display name of the message author
6
+ - timestamp?: ReactNode — string ("11:24", "2 hours ago") or any node for custom formatting
7
+ - avatar?: ReactNode — slot for any `<Avatar>` composition; omit for grouped messages from the same author
8
+ - badge?: ReactNode — small chip(s) next to the author name (OP, Bot, Admin, role tag)
9
+ - edited?: boolean | string — renders "(edited)" hint next to timestamp; pass a string to customise ("(edited 2 minutes ago)")
10
+ - pinned?: boolean — renders a pin glyph + "Pinned" label above the header row for sticky / pinned messages
11
+ - actions?: ReactNode — end-of-header slot, typically hover-revealed icon buttons (reply / react / more)
12
+ - reactions?: ReactNode — slot below the body, typically a Row of reaction chips (emoji + count)
13
+ - threadCount?: number — renders a "N replies" link affordance below the body
14
+ - onThreadClick?: () => void — handler for the threadCount affordance
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.
17
+ - children: ReactNode — body content (plain text or rich nodes)
18
+ - className?: string
19
+ when_to_use: |
20
+ The canonical "avatar + author + timestamp + body" row. THE PRIMITIVE
21
+ for any chat surface, comment thread, post-reply, activity log, or
22
+ notification feed that follows the people-and-text shape.
23
+
24
+ CONCRETE TEST — if you find yourself composing an `<Avatar>` followed
25
+ by a `<Row>` of author name + timestamp, with a `<p>` or `<span>`
26
+ body below, STOP. That is `<Message>`. Reach for it directly.
27
+
28
+ Slack-style channel feed, Discord messages, Teams chat, Linear /
29
+ GitHub / Jira comments, Reddit replies, Twitter/X posts in a thread,
30
+ Notion comment sidebars, in-app activity logs, notification rows —
31
+ every one of these IS `<Message>`. Do not roll the layout inline.
32
+
33
+ For non-people activity (system events, log lines, status pings) use
34
+ Callout or a plain Row instead — Message implies a human author.
35
+ composes_with: [Avatar (in the avatar slot — pair with AvatarFallback tone="..." for stable per-author colour), Badge (in the badge slot for role / OP / bot tags), Button (in actions, typically size="icon" + variant="ghost"), Stack (host multiple Messages in a thread), Card (wrap a Stack of Messages for a comment-thread block)]
36
+ aliases: [
37
+ message, chat message, comment, post, reply, activity row, notification row,
38
+ thread row, channel message, dm message, slack message, discord message,
39
+ teams message, channel feed message, feed item, feed row, message row,
40
+ user message, user post, conversation message, conversation row,
41
+ inline comment, threaded reply, message bubble, chat bubble, talk bubble
42
+ ]
43
+ ---
44
+
45
+ ```jsx
46
+ // Comment thread shape — avatar left, body below the author row.
47
+ <Stack gap="md">
48
+ <Message
49
+ author="alice"
50
+ timestamp="2 hours ago"
51
+ avatar={
52
+ <Avatar size="sm">
53
+ <AvatarFallback tone="violet">A</AvatarFallback>
54
+ </Avatar>
55
+ }
56
+ >
57
+ Splitting this into two PRs makes the review tractable.
58
+ </Message>
59
+ <Message
60
+ author="ben"
61
+ timestamp="1 hour ago"
62
+ badge={<Badge variant="outline" className="text-[10px]">OP</Badge>}
63
+ avatar={
64
+ <Avatar size="sm">
65
+ <AvatarFallback tone="amber">B</AvatarFallback>
66
+ </Avatar>
67
+ }
68
+ >
69
+ Agreed. I'll take the schema PR.
70
+ </Message>
71
+ </Stack>
72
+ ```
73
+
74
+ ```jsx
75
+ // Chat shape — your messages right-aligned via align="end".
76
+ <Stack gap="md">
77
+ <Message
78
+ author="alice"
79
+ timestamp="11:24"
80
+ avatar={
81
+ <Avatar size="xs">
82
+ <AvatarFallback tone="violet">A</AvatarFallback>
83
+ </Avatar>
84
+ }
85
+ >
86
+ Hey, how's the launch going?
87
+ </Message>
88
+ <Message
89
+ author="you"
90
+ timestamp="11:26"
91
+ align="end"
92
+ avatar={
93
+ <Avatar size="xs">
94
+ <AvatarFallback tone="emerald">Y</AvatarFallback>
95
+ </Avatar>
96
+ }
97
+ >
98
+ Launch image is in, scheduling now.
99
+ </Message>
100
+ </Stack>
101
+ ```
102
+
103
+ ```jsx
104
+ // Full Slack-style message — edited indicator, pinned flag, reactions
105
+ // row, threaded reply count, role badge, hover actions.
106
+ <Message
107
+ author="alice"
108
+ timestamp="11:24"
109
+ edited
110
+ pinned
111
+ badge={<Badge variant="secondary" className="text-[10px]">Designer</Badge>}
112
+ avatar={
113
+ <Avatar size="md">
114
+ <AvatarFallback tone="violet">A</AvatarFallback>
115
+ </Avatar>
116
+ }
117
+ reactions={
118
+ <>
119
+ <Badge variant="outline" className="gap-1 cursor-pointer">👍 4</Badge>
120
+ <Badge variant="outline" className="gap-1 cursor-pointer">🎉 2</Badge>
121
+ </>
122
+ }
123
+ threadCount={3}
124
+ onThreadClick={() => openThread(messageId)}
125
+ >
126
+ Updated the token spec — review when you have a chance.
127
+ </Message>
128
+ ```
129
+
130
+ ```jsx
131
+ // Slack / Discord channel feed — with role badge + hover-revealed actions.
132
+ <Stack gap="lg">
133
+ {messages.map((m) => (
134
+ <Message
135
+ key={m.id}
136
+ author={m.user}
137
+ timestamp={m.time}
138
+ badge={<Badge variant="secondary" className="text-[10px]">{m.role}</Badge>}
139
+ avatar={
140
+ <Avatar size="md">
141
+ <AvatarImage src={m.avatar} />
142
+ <AvatarFallback tone="sky">{m.user.charAt(0)}</AvatarFallback>
143
+ </Avatar>
144
+ }
145
+ actions={
146
+ <Row gap="xs" className="opacity-0 group-hover:opacity-100 transition-opacity">
147
+ <Button size="icon" variant="ghost" className="h-6 w-6"><Smile className="h-3 w-3" /></Button>
148
+ <Button size="icon" variant="ghost" className="h-6 w-6"><Reply className="h-3 w-3" /></Button>
149
+ <Button size="icon" variant="ghost" className="h-6 w-6"><MoreHorizontal className="h-3 w-3" /></Button>
150
+ </Row>
151
+ }
152
+ className="group"
153
+ >
154
+ {m.text}
155
+ </Message>
156
+ ))}
157
+ </Stack>
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
+
193
+ ## Anti-patterns
194
+
195
+ ```jsx
196
+ // ❌ Rolling the message layout by hand from Avatar + Row + Badge + spans.
197
+ // This is the EXACT shape Message exists to consolidate — caught in
198
+ // the wild on a "Slack clone" prompt where the model assembled this
199
+ // inline instead of reaching for Message. The result loses the
200
+ // align="end" knob, the actions slot, the data-gds-part hooks, and
201
+ // duplicates the same flex template across every consumer.
202
+ {messages.map((msg) => (
203
+ <div className="group flex gap-4">
204
+ <Avatar className="w-9 h-9 shrink-0">
205
+ <AvatarImage src={msg.avatar} />
206
+ <AvatarFallback>{msg.user.charAt(0)}</AvatarFallback>
207
+ </Avatar>
208
+ <div className="flex-1 min-w-0">
209
+ <Row gap="sm" align="baseline">
210
+ <span className="font-semibold text-sm">{msg.user}</span>
211
+ <Badge variant="secondary" className="text-[10px]">{msg.role}</Badge>
212
+ <span className="text-[10px] text-muted-foreground">{msg.time}</span>
213
+ </Row>
214
+ <p className="text-sm mt-1">{msg.text}</p>
215
+ </div>
216
+ </div>
217
+ ))}
218
+
219
+ // ✅ The Grade way.
220
+ {messages.map((msg) => (
221
+ <Message
222
+ key={msg.id}
223
+ author={msg.user}
224
+ timestamp={msg.time}
225
+ badge={<Badge variant="secondary" className="text-[10px]">{msg.role}</Badge>}
226
+ avatar={
227
+ <Avatar size="md">
228
+ <AvatarImage src={msg.avatar} />
229
+ <AvatarFallback>{msg.user.charAt(0)}</AvatarFallback>
230
+ </Avatar>
231
+ }
232
+ >
233
+ {msg.text}
234
+ </Message>
235
+ ))}
236
+ ```
237
+
238
+ ```jsx
239
+ // ❌ Building a custom "AuthorDot" or "MessageRow" component inline as
240
+ // a one-off helper inside a scaffold. Three scaffolds did this before
241
+ // Message landed; the pattern is always identical.
242
+ function MessageRow({ user, body, time }) {
243
+ return (
244
+ <div className="flex gap-3 items-start">
245
+ <div className="h-7 w-7 rounded-full bg-violet-500/20 ...">{user[0]}</div>
246
+ <div>
247
+ <Row><strong>{user}</strong> <small>{time}</small></Row>
248
+ <p>{body}</p>
249
+ </div>
250
+ </div>
251
+ );
252
+ }
253
+
254
+ // ✅ Use Message. The colored-initials avatar pattern is covered by
255
+ // Avatar + AvatarFallback tone="...".
256
+ <Message
257
+ author={user}
258
+ timestamp={time}
259
+ avatar={<Avatar size="sm"><AvatarFallback tone="violet">{user[0]}</AvatarFallback></Avatar>}
260
+ >
261
+ {body}
262
+ </Message>
263
+ ```
@@ -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