@gradeui/ui 1.0.0 → 1.2.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/banner.md +146 -0
- package/components/ui/card.md +170 -15
- package/components/ui/code.md +133 -0
- package/components/ui/composer.md +226 -0
- package/components/ui/dialog.md +106 -13
- package/components/ui/dropdown-menu.md +93 -4
- package/components/ui/hover-card.md +98 -4
- package/components/ui/message.md +229 -0
- package/components/ui/popover.md +119 -7
- package/components/ui/section-block.md +153 -0
- package/components/ui/sheet.md +98 -6
- 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 +1027 -29
- package/dist/index.d.ts +1027 -29
- package/dist/index.js +102 -49
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +102 -49
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +15 -1
|
@@ -0,0 +1,229 @@
|
|
|
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
|
+
- children: ReactNode — body content (plain text or rich nodes)
|
|
17
|
+
- className?: string
|
|
18
|
+
when_to_use: |
|
|
19
|
+
The canonical "avatar + author + timestamp + body" row. THE PRIMITIVE
|
|
20
|
+
for any chat surface, comment thread, post-reply, activity log, or
|
|
21
|
+
notification feed that follows the people-and-text shape.
|
|
22
|
+
|
|
23
|
+
CONCRETE TEST — if you find yourself composing an `<Avatar>` followed
|
|
24
|
+
by a `<Row>` of author name + timestamp, with a `<p>` or `<span>`
|
|
25
|
+
body below, STOP. That is `<Message>`. Reach for it directly.
|
|
26
|
+
|
|
27
|
+
Slack-style channel feed, Discord messages, Teams chat, Linear /
|
|
28
|
+
GitHub / Jira comments, Reddit replies, Twitter/X posts in a thread,
|
|
29
|
+
Notion comment sidebars, in-app activity logs, notification rows —
|
|
30
|
+
every one of these IS `<Message>`. Do not roll the layout inline.
|
|
31
|
+
|
|
32
|
+
For non-people activity (system events, log lines, status pings) use
|
|
33
|
+
Callout or a plain Row instead — Message implies a human author.
|
|
34
|
+
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)]
|
|
35
|
+
aliases: [
|
|
36
|
+
message, chat message, comment, post, reply, activity row, notification row,
|
|
37
|
+
thread row, channel message, dm message, slack message, discord message,
|
|
38
|
+
teams message, channel feed message, feed item, feed row, message row,
|
|
39
|
+
user message, user post, conversation message, conversation row,
|
|
40
|
+
inline comment, threaded reply, message bubble, chat bubble, talk bubble
|
|
41
|
+
]
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
```jsx
|
|
45
|
+
// Comment thread shape — avatar left, body below the author row.
|
|
46
|
+
<Stack gap="md">
|
|
47
|
+
<Message
|
|
48
|
+
author="alice"
|
|
49
|
+
timestamp="2 hours ago"
|
|
50
|
+
avatar={
|
|
51
|
+
<Avatar size="sm">
|
|
52
|
+
<AvatarFallback tone="violet">A</AvatarFallback>
|
|
53
|
+
</Avatar>
|
|
54
|
+
}
|
|
55
|
+
>
|
|
56
|
+
Splitting this into two PRs makes the review tractable.
|
|
57
|
+
</Message>
|
|
58
|
+
<Message
|
|
59
|
+
author="ben"
|
|
60
|
+
timestamp="1 hour ago"
|
|
61
|
+
badge={<Badge variant="outline" className="text-[10px]">OP</Badge>}
|
|
62
|
+
avatar={
|
|
63
|
+
<Avatar size="sm">
|
|
64
|
+
<AvatarFallback tone="amber">B</AvatarFallback>
|
|
65
|
+
</Avatar>
|
|
66
|
+
}
|
|
67
|
+
>
|
|
68
|
+
Agreed. I'll take the schema PR.
|
|
69
|
+
</Message>
|
|
70
|
+
</Stack>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```jsx
|
|
74
|
+
// Chat shape — your messages right-aligned via align="end".
|
|
75
|
+
<Stack gap="md">
|
|
76
|
+
<Message
|
|
77
|
+
author="alice"
|
|
78
|
+
timestamp="11:24"
|
|
79
|
+
avatar={
|
|
80
|
+
<Avatar size="xs">
|
|
81
|
+
<AvatarFallback tone="violet">A</AvatarFallback>
|
|
82
|
+
</Avatar>
|
|
83
|
+
}
|
|
84
|
+
>
|
|
85
|
+
Hey, how's the launch going?
|
|
86
|
+
</Message>
|
|
87
|
+
<Message
|
|
88
|
+
author="you"
|
|
89
|
+
timestamp="11:26"
|
|
90
|
+
align="end"
|
|
91
|
+
avatar={
|
|
92
|
+
<Avatar size="xs">
|
|
93
|
+
<AvatarFallback tone="emerald">Y</AvatarFallback>
|
|
94
|
+
</Avatar>
|
|
95
|
+
}
|
|
96
|
+
>
|
|
97
|
+
Launch image is in, scheduling now.
|
|
98
|
+
</Message>
|
|
99
|
+
</Stack>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```jsx
|
|
103
|
+
// Full Slack-style message — edited indicator, pinned flag, reactions
|
|
104
|
+
// row, threaded reply count, role badge, hover actions.
|
|
105
|
+
<Message
|
|
106
|
+
author="alice"
|
|
107
|
+
timestamp="11:24"
|
|
108
|
+
edited
|
|
109
|
+
pinned
|
|
110
|
+
badge={<Badge variant="secondary" className="text-[10px]">Designer</Badge>}
|
|
111
|
+
avatar={
|
|
112
|
+
<Avatar size="md">
|
|
113
|
+
<AvatarFallback tone="violet">A</AvatarFallback>
|
|
114
|
+
</Avatar>
|
|
115
|
+
}
|
|
116
|
+
reactions={
|
|
117
|
+
<>
|
|
118
|
+
<Badge variant="outline" className="gap-1 cursor-pointer">👍 4</Badge>
|
|
119
|
+
<Badge variant="outline" className="gap-1 cursor-pointer">🎉 2</Badge>
|
|
120
|
+
</>
|
|
121
|
+
}
|
|
122
|
+
threadCount={3}
|
|
123
|
+
onThreadClick={() => openThread(messageId)}
|
|
124
|
+
>
|
|
125
|
+
Updated the token spec — review when you have a chance.
|
|
126
|
+
</Message>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```jsx
|
|
130
|
+
// Slack / Discord channel feed — with role badge + hover-revealed actions.
|
|
131
|
+
<Stack gap="lg">
|
|
132
|
+
{messages.map((m) => (
|
|
133
|
+
<Message
|
|
134
|
+
key={m.id}
|
|
135
|
+
author={m.user}
|
|
136
|
+
timestamp={m.time}
|
|
137
|
+
badge={<Badge variant="secondary" className="text-[10px]">{m.role}</Badge>}
|
|
138
|
+
avatar={
|
|
139
|
+
<Avatar size="md">
|
|
140
|
+
<AvatarImage src={m.avatar} />
|
|
141
|
+
<AvatarFallback tone="sky">{m.user.charAt(0)}</AvatarFallback>
|
|
142
|
+
</Avatar>
|
|
143
|
+
}
|
|
144
|
+
actions={
|
|
145
|
+
<Row gap="xs" className="opacity-0 group-hover:opacity-100 transition-opacity">
|
|
146
|
+
<Button size="icon" variant="ghost" className="h-6 w-6"><Smile className="h-3 w-3" /></Button>
|
|
147
|
+
<Button size="icon" variant="ghost" className="h-6 w-6"><Reply className="h-3 w-3" /></Button>
|
|
148
|
+
<Button size="icon" variant="ghost" className="h-6 w-6"><MoreHorizontal className="h-3 w-3" /></Button>
|
|
149
|
+
</Row>
|
|
150
|
+
}
|
|
151
|
+
className="group"
|
|
152
|
+
>
|
|
153
|
+
{m.text}
|
|
154
|
+
</Message>
|
|
155
|
+
))}
|
|
156
|
+
</Stack>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Anti-patterns
|
|
160
|
+
|
|
161
|
+
```jsx
|
|
162
|
+
// ❌ Rolling the message layout by hand from Avatar + Row + Badge + spans.
|
|
163
|
+
// This is the EXACT shape Message exists to consolidate — caught in
|
|
164
|
+
// the wild on a "Slack clone" prompt where the model assembled this
|
|
165
|
+
// inline instead of reaching for Message. The result loses the
|
|
166
|
+
// align="end" knob, the actions slot, the data-gds-part hooks, and
|
|
167
|
+
// duplicates the same flex template across every consumer.
|
|
168
|
+
{messages.map((msg) => (
|
|
169
|
+
<div className="group flex gap-4">
|
|
170
|
+
<Avatar className="w-9 h-9 shrink-0">
|
|
171
|
+
<AvatarImage src={msg.avatar} />
|
|
172
|
+
<AvatarFallback>{msg.user.charAt(0)}</AvatarFallback>
|
|
173
|
+
</Avatar>
|
|
174
|
+
<div className="flex-1 min-w-0">
|
|
175
|
+
<Row gap="sm" align="baseline">
|
|
176
|
+
<span className="font-semibold text-sm">{msg.user}</span>
|
|
177
|
+
<Badge variant="secondary" className="text-[10px]">{msg.role}</Badge>
|
|
178
|
+
<span className="text-[10px] text-muted-foreground">{msg.time}</span>
|
|
179
|
+
</Row>
|
|
180
|
+
<p className="text-sm mt-1">{msg.text}</p>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
))}
|
|
184
|
+
|
|
185
|
+
// ✅ The Grade way.
|
|
186
|
+
{messages.map((msg) => (
|
|
187
|
+
<Message
|
|
188
|
+
key={msg.id}
|
|
189
|
+
author={msg.user}
|
|
190
|
+
timestamp={msg.time}
|
|
191
|
+
badge={<Badge variant="secondary" className="text-[10px]">{msg.role}</Badge>}
|
|
192
|
+
avatar={
|
|
193
|
+
<Avatar size="md">
|
|
194
|
+
<AvatarImage src={msg.avatar} />
|
|
195
|
+
<AvatarFallback>{msg.user.charAt(0)}</AvatarFallback>
|
|
196
|
+
</Avatar>
|
|
197
|
+
}
|
|
198
|
+
>
|
|
199
|
+
{msg.text}
|
|
200
|
+
</Message>
|
|
201
|
+
))}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
```jsx
|
|
205
|
+
// ❌ Building a custom "AuthorDot" or "MessageRow" component inline as
|
|
206
|
+
// a one-off helper inside a scaffold. Three scaffolds did this before
|
|
207
|
+
// Message landed; the pattern is always identical.
|
|
208
|
+
function MessageRow({ user, body, time }) {
|
|
209
|
+
return (
|
|
210
|
+
<div className="flex gap-3 items-start">
|
|
211
|
+
<div className="h-7 w-7 rounded-full bg-violet-500/20 ...">{user[0]}</div>
|
|
212
|
+
<div>
|
|
213
|
+
<Row><strong>{user}</strong> <small>{time}</small></Row>
|
|
214
|
+
<p>{body}</p>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ✅ Use Message. The colored-initials avatar pattern is covered by
|
|
221
|
+
// Avatar + AvatarFallback tone="...".
|
|
222
|
+
<Message
|
|
223
|
+
author={user}
|
|
224
|
+
timestamp={time}
|
|
225
|
+
avatar={<Avatar size="sm"><AvatarFallback tone="violet">{user[0]}</AvatarFallback></Avatar>}
|
|
226
|
+
>
|
|
227
|
+
{body}
|
|
228
|
+
</Message>
|
|
229
|
+
```
|
package/components/ui/popover.md
CHANGED
|
@@ -6,18 +6,26 @@ props:
|
|
|
6
6
|
- Popover: open?, defaultOpen?, onOpenChange?, modal? (default false)
|
|
7
7
|
- PopoverTrigger: asChild?: boolean — usually a Button
|
|
8
8
|
- PopoverContent: side? "top" | "right" | "bottom" | "left"; align? "start" | "center" | "end"; sideOffset?, alignOffset?, collisionPadding?, className?
|
|
9
|
+
- PopoverContent: surface? (solid | translucent | glass | glass-strong) — what the popover surface is *made of*. `solid` is the default opaque `bg-popover`. `translucent` is the Apple HIG menu-sheet feel. `glass` for floating panels over rich canvases (Studio inspector, image-tool palette).
|
|
9
10
|
- PopoverAnchor: asChild?: boolean — pin the popover to a different element than the trigger
|
|
10
11
|
when_to_use: A floating panel anchored to a trigger that contains interactive content — date pickers, color pickers, filter pickers, "more info" panels, inline forms. Differs from Tooltip (hover-only, no focusable content) and Dialog (modal, blocks the page). DatePicker, DateRangePicker, and the Combobox pattern all compose Popover internally.
|
|
11
|
-
composes_with: [Button (as trigger), Calendar (date picker), Command (combobox), Form controls (inline edit popover)]
|
|
12
|
-
aliases: [popover, dropdown panel, floating panel, inline editor, attached panel, filter pop, popover view, popoverpresentation, attached popover]
|
|
12
|
+
composes_with: [Button (as trigger), Calendar (date picker), Command (combobox), Form controls (inline edit popover), Code (code-detail popovers)]
|
|
13
|
+
aliases: [popover, dropdown panel, floating panel, inline editor, attached panel, filter pop, popover view, popoverpresentation, attached popover, glass popover, frosted popover, inspector popover]
|
|
13
14
|
---
|
|
14
15
|
|
|
16
|
+
PopoverContent sits at elevation-4. Three scenario recipes — match the material to the canvas the popover floats over.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
### Scenario 1 — Filter popover (default opaque)
|
|
21
|
+
|
|
22
|
+
You're attaching a filter picker to a button in a list/table header. The page behind is mostly white space and a table — there's nothing visually important to preserve through the popover. Opaque is the right default.
|
|
23
|
+
|
|
15
24
|
```jsx
|
|
16
|
-
// Filter popover anchored to a Button trigger.
|
|
17
25
|
<Popover>
|
|
18
26
|
<PopoverTrigger asChild>
|
|
19
27
|
<Button variant="outline" size="sm">
|
|
20
|
-
<Filter /> Filters
|
|
28
|
+
<Filter className="h-4 w-4" /> Filters
|
|
21
29
|
</Button>
|
|
22
30
|
</PopoverTrigger>
|
|
23
31
|
<PopoverContent className="w-72" align="end">
|
|
@@ -30,14 +38,118 @@ aliases: [popover, dropdown panel, floating panel, inline editor, attached panel
|
|
|
30
38
|
<Label>Status</Label>
|
|
31
39
|
<Select>{/* … */}</Select>
|
|
32
40
|
</Stack>
|
|
41
|
+
<Row justify="end" gap="xs">
|
|
42
|
+
<Button variant="ghost" size="sm">Clear</Button>
|
|
43
|
+
<Button size="sm">Apply</Button>
|
|
44
|
+
</Row>
|
|
33
45
|
</Stack>
|
|
34
46
|
</PopoverContent>
|
|
35
47
|
</Popover>
|
|
36
48
|
```
|
|
37
49
|
|
|
38
|
-
|
|
50
|
+
`solid` keeps the form fields maximally legible. Filter popovers are read-heavy; legibility wins over aesthetic.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### Scenario 2 — Glass inspector popover (creative tool aesthetic)
|
|
55
|
+
|
|
56
|
+
You're building Studio, a presentation editor, or a vector tool. The user clicked a selected layer and a popover offers per-element knobs. The canvas behind is the work — they need to keep spatial awareness of what they just clicked. Glass is the canonical signal.
|
|
39
57
|
|
|
40
58
|
```jsx
|
|
41
|
-
<
|
|
42
|
-
<
|
|
59
|
+
<Popover>
|
|
60
|
+
<PopoverTrigger asChild>
|
|
61
|
+
<Button variant="ghost" size="icon"><Palette className="h-4 w-4" /></Button>
|
|
62
|
+
</PopoverTrigger>
|
|
63
|
+
<PopoverContent
|
|
64
|
+
surface="glass"
|
|
65
|
+
className="w-80 shadow-elevation-4"
|
|
66
|
+
align="end"
|
|
67
|
+
sideOffset={8}
|
|
68
|
+
>
|
|
69
|
+
<Stack gap="md">
|
|
70
|
+
<Row justify="between" align="center">
|
|
71
|
+
<span className="text-sm font-medium">Button — selected</span>
|
|
72
|
+
<Badge variant="outline">raised</Badge>
|
|
73
|
+
</Row>
|
|
74
|
+
|
|
75
|
+
<Stack gap="xs">
|
|
76
|
+
<Label>Tone</Label>
|
|
77
|
+
<Row gap="xs">
|
|
78
|
+
<Button size="sm" variant="raised" style={{ "--btn-glow": "var(--selected-glow)" }} />
|
|
79
|
+
<Button size="sm" variant="raised" style={{ "--btn-glow": "var(--success)" }} />
|
|
80
|
+
<Button size="sm" variant="raised" style={{ "--btn-glow": "var(--warning)" }} />
|
|
81
|
+
<Button size="sm" variant="raised" style={{ "--btn-glow": "var(--destructive)" }} />
|
|
82
|
+
</Row>
|
|
83
|
+
</Stack>
|
|
84
|
+
|
|
85
|
+
<Stack gap="xs">
|
|
86
|
+
<Label>Size</Label>
|
|
87
|
+
<ToggleGroup type="single" defaultValue="md">
|
|
88
|
+
<ToggleGroupItem value="sm">sm</ToggleGroupItem>
|
|
89
|
+
<ToggleGroupItem value="md">md</ToggleGroupItem>
|
|
90
|
+
<ToggleGroupItem value="lg">lg</ToggleGroupItem>
|
|
91
|
+
</ToggleGroup>
|
|
92
|
+
</Stack>
|
|
93
|
+
</Stack>
|
|
94
|
+
</PopoverContent>
|
|
95
|
+
</Popover>
|
|
43
96
|
```
|
|
97
|
+
|
|
98
|
+
`surface="glass"` + `shadow-elevation-4` is the Studio-inspector signature. The user's eye stays on the canvas; the popover reads as chrome layered above it.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
### Scenario 3 — AI suggestion popover (translucent + aura)
|
|
103
|
+
|
|
104
|
+
A different shape from the destructive Dialog confirmation: an inline AI suggestion that surfaces while the user keeps working. Translucent stays light; aura announces the AI origin.
|
|
105
|
+
|
|
106
|
+
```jsx
|
|
107
|
+
<Popover open={hasSuggestion}>
|
|
108
|
+
<PopoverAnchor>
|
|
109
|
+
<Code source={selectedSnippet} language="tsx" highlight={[3]} bare />
|
|
110
|
+
</PopoverAnchor>
|
|
111
|
+
<PopoverContent
|
|
112
|
+
surface="translucent"
|
|
113
|
+
className="w-96 shadow-elevation-4 gds-aura-ring"
|
|
114
|
+
style={{ "--aura-color": "var(--selected-glow)" }}
|
|
115
|
+
side="bottom"
|
|
116
|
+
align="start"
|
|
117
|
+
>
|
|
118
|
+
<Stack gap="sm">
|
|
119
|
+
<Row gap="xs" align="center">
|
|
120
|
+
<Sparkles className="h-4 w-4" />
|
|
121
|
+
<span className="text-sm font-medium">Studio suggestion</span>
|
|
122
|
+
</Row>
|
|
123
|
+
<p className="text-sm">
|
|
124
|
+
This Toolbar would line up edge-to-edge with the TabsList below if it used <code>size="sm"</code>. Apply?
|
|
125
|
+
</p>
|
|
126
|
+
<Row justify="end" gap="xs">
|
|
127
|
+
<Button variant="ghost" size="sm">Dismiss</Button>
|
|
128
|
+
<Button size="sm">Apply</Button>
|
|
129
|
+
</Row>
|
|
130
|
+
</Stack>
|
|
131
|
+
</PopoverContent>
|
|
132
|
+
</Popover>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Note `PopoverAnchor` — the popover is pinned to the selected snippet, not to a trigger button. This is the "annotation surfaces next to the thing it annotates" pattern.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
### Anti-patterns
|
|
140
|
+
|
|
141
|
+
**DO NOT roll glass by hand on PopoverContent.**
|
|
142
|
+
|
|
143
|
+
```jsx
|
|
144
|
+
{/* ❌ Misses edge highlight, fixed-step blur. */}
|
|
145
|
+
<PopoverContent className="bg-popover/50 backdrop-blur-md">
|
|
146
|
+
|
|
147
|
+
{/* ✅ */}
|
|
148
|
+
<PopoverContent surface="glass">
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**DO NOT use Popover for content that needs a modal interaction.** Popover is non-modal — pointer-down outside dismisses it. If the user must decide before continuing, reach for Dialog.
|
|
152
|
+
|
|
153
|
+
**DO NOT use `surface="glass-strong"` on PopoverContent.** It's tuned for full-page overlays; on a 288px popover it just reads as washed out.
|
|
154
|
+
|
|
155
|
+
**DO NOT use Popover when the trigger is a hover target with no focusable content.** That's Tooltip's job — Popover requires focus, Tooltip dismisses on hover-out.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: SectionBlock
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- padding? (none | sm | md | lg | xl) — vertical rhythm. Defaults to `lg`.
|
|
6
|
+
- background? (transparent | muted | card | primary | gradient) — tonal direction of the section bg.
|
|
7
|
+
- surface? (solid | translucent | glass | glass-strong) — what the section is *made of*. Orthogonal to `background`. Use `glass` for hero sections that float over a generative backdrop / image / dot grid.
|
|
8
|
+
- container? (default | wide | narrow | full) — max-width of the inner content.
|
|
9
|
+
- alignment? (left | center | right) — header / CTA alignment.
|
|
10
|
+
- titleSize? (sm | md | lg | xl)
|
|
11
|
+
- title?: string
|
|
12
|
+
- subtitle?: string
|
|
13
|
+
- cta1? / cta2? — string or `{ text, variant, href, onClick }` config
|
|
14
|
+
- backgroundImage?: string — direct CSS background image url
|
|
15
|
+
- as? "section" | "div" | "article" — semantic root
|
|
16
|
+
- fullBleed?: boolean
|
|
17
|
+
when_to_use: The top-level container for a marketing page section — hero, feature row, pricing table, testimonial strip, FAQ section. Always reach for SectionBlock over a hand-rolled `<section>` so vertical rhythm, container width, and tonal background stay consistent across the page. Pair `background="gradient"` + `surface="glass"` inner Cards for the "modern marketing hero" pattern.
|
|
18
|
+
composes_with: [Card (the most common child — especially with surface="glass"), Grid (feature rows), Stack (hero column), MediaSurface (hero imagery), Code (developer hero), Carousel (logo strips)]
|
|
19
|
+
aliases: [section, section block, hero section, marketing section, page section, content section, container section, feature section, hero, page hero, marketing hero, glass section, gradient section, mesh hero]
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
SectionBlock is the **container axis** of a marketing page; Card is the **content axis** inside it. Three Presence axes still apply to SectionBlock: `background` (tonal direction), `surface` (material), `padding` (depth of vertical rhythm).
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
### Scenario 1 — Standard feature row (default)
|
|
27
|
+
|
|
28
|
+
You're laying out a feature section on a marketing page — a row of cards explaining capabilities. Calm tonal background, generous padding, default container width.
|
|
29
|
+
|
|
30
|
+
```jsx
|
|
31
|
+
<SectionBlock
|
|
32
|
+
padding="lg"
|
|
33
|
+
background="muted"
|
|
34
|
+
title="Built for production"
|
|
35
|
+
subtitle="The hard primitives every team eventually needs."
|
|
36
|
+
alignment="center"
|
|
37
|
+
>
|
|
38
|
+
<Grid cols="3" gap="md">
|
|
39
|
+
<Card>
|
|
40
|
+
<CardHeader>
|
|
41
|
+
<Database className="h-5 w-5" />
|
|
42
|
+
<CardTitle>Data tables</CardTitle>
|
|
43
|
+
<CardDescription>Sorting, filtering, virtualisation.</CardDescription>
|
|
44
|
+
</CardHeader>
|
|
45
|
+
</Card>
|
|
46
|
+
<Card>
|
|
47
|
+
<CardHeader>
|
|
48
|
+
<Map className="h-5 w-5" />
|
|
49
|
+
<CardTitle>Maps</CardTitle>
|
|
50
|
+
<CardDescription>MapLibre default. Mapbox + Google adapters.</CardDescription>
|
|
51
|
+
</CardHeader>
|
|
52
|
+
</Card>
|
|
53
|
+
<Card>
|
|
54
|
+
<CardHeader>
|
|
55
|
+
<MoveVertical className="h-5 w-5" />
|
|
56
|
+
<CardTitle>Drag and drop</CardTitle>
|
|
57
|
+
<CardDescription>dnd-kit underneath, themed against tokens.</CardDescription>
|
|
58
|
+
</CardHeader>
|
|
59
|
+
</Card>
|
|
60
|
+
</Grid>
|
|
61
|
+
</SectionBlock>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
No `surface` prop. The default `solid` is the right answer for in-flow feature rows — the muted background sets the section apart from neighbouring sections cleanly.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### Scenario 2 — Gradient hero with glass cards (modern marketing pattern)
|
|
69
|
+
|
|
70
|
+
The canonical "shadcn-killer marketing hero" pattern. SectionBlock supplies the gradient mesh; Card children opt into glass; the two compose without either having to know about the other.
|
|
71
|
+
|
|
72
|
+
```jsx
|
|
73
|
+
<SectionBlock
|
|
74
|
+
padding="xl"
|
|
75
|
+
background="gradient"
|
|
76
|
+
alignment="center"
|
|
77
|
+
title="Open the markup. Tell me which one you would merge."
|
|
78
|
+
subtitle="GradeUI produces code you would actually integrate."
|
|
79
|
+
cta1={{ text: "Open Studio", href: "/studio" }}
|
|
80
|
+
cta2={{ text: "Install the library", variant: "outline" }}
|
|
81
|
+
>
|
|
82
|
+
<Grid cols="2" gap="md" className="mt-8">
|
|
83
|
+
<Card surface="glass" className="shadow-elevation-4">
|
|
84
|
+
<CardHeader>
|
|
85
|
+
<CardTitle>v0 — sidebar component</CardTitle>
|
|
86
|
+
<CardDescription>~300 lines</CardDescription>
|
|
87
|
+
</CardHeader>
|
|
88
|
+
<CardContent className="p-0">
|
|
89
|
+
<Code source={v0Code} language="tsx" bare className="p-4 text-xs max-h-72" />
|
|
90
|
+
</CardContent>
|
|
91
|
+
</Card>
|
|
92
|
+
|
|
93
|
+
<Card surface="glass" className="shadow-elevation-4 gds-aura-ring">
|
|
94
|
+
<CardHeader>
|
|
95
|
+
<CardTitle>GradeUI — sidebar component</CardTitle>
|
|
96
|
+
<CardDescription>6 lines</CardDescription>
|
|
97
|
+
</CardHeader>
|
|
98
|
+
<CardContent className="p-0">
|
|
99
|
+
<Code source={gradeCode} language="tsx" bare className="p-4 text-xs max-h-72" />
|
|
100
|
+
</CardContent>
|
|
101
|
+
</Card>
|
|
102
|
+
</Grid>
|
|
103
|
+
</SectionBlock>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
This is the pattern the home-diff-hero scaffold uses. `background="gradient"` paints the mesh; the Cards float through it via `surface="glass"`; `gds-aura-ring` on the second card draws the eye to the recommended path. No Tailwind soup anywhere.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### Scenario 3 — Glass section over a backgroundImage (image hero)
|
|
111
|
+
|
|
112
|
+
You're using a hero image as the section background. A solid section panel over it would defeat the image. A glass section keeps the image visible while focusing the eye on the content overlay.
|
|
113
|
+
|
|
114
|
+
```jsx
|
|
115
|
+
<SectionBlock
|
|
116
|
+
padding="xl"
|
|
117
|
+
surface="glass"
|
|
118
|
+
backgroundImage="/hero/teams-shipping.jpg"
|
|
119
|
+
alignment="center"
|
|
120
|
+
title="For teams shipping software"
|
|
121
|
+
subtitle="The primitive layer modern product teams actually use."
|
|
122
|
+
cta1={{ text: "Open Studio" }}
|
|
123
|
+
container="narrow"
|
|
124
|
+
>
|
|
125
|
+
<Row justify="center" gap="lg" className="text-sm text-muted-foreground">
|
|
126
|
+
<span>Linear</span><span>Vercel</span><span>Stripe</span><span>Anthropic</span>
|
|
127
|
+
</Row>
|
|
128
|
+
</SectionBlock>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`background` stays at the default `transparent` so the image shows through; `surface="glass"` paints the frosted overlay on top with edge highlight + theme-tuned blur. The narrow container caps content width so the hero stays readable over the image.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
### Anti-patterns
|
|
136
|
+
|
|
137
|
+
**DO NOT roll glass by hand at the section level.**
|
|
138
|
+
|
|
139
|
+
```jsx
|
|
140
|
+
{/* ❌ */}
|
|
141
|
+
<section className="py-20 bg-card/40 backdrop-blur-md">
|
|
142
|
+
|
|
143
|
+
{/* ✅ */}
|
|
144
|
+
<SectionBlock surface="glass" padding="xl">
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**DO NOT use `background="primary"` + `surface="glass"`.** The primary fill is intentionally opaque (it's a brand statement). Layering glass on top makes the brand colour read as washed-out. Pick one signal.
|
|
148
|
+
|
|
149
|
+
**DO NOT skip SectionBlock for marketing rows.** Hand-rolling `<section className="py-20">` means every section gets a slightly different vertical rhythm and container width — the page reads as drift. SectionBlock is the rhythm primitive.
|
|
150
|
+
|
|
151
|
+
**DO NOT use `padding="xl"` for in-app sections.** xl padding is marketing-page territory. In-app section breaks should use `sm` or `md` — anything more and your dashboard reads as a marketing page.
|
|
152
|
+
|
|
153
|
+
**DO NOT use `surface="glass-strong"` on SectionBlock unless the section is acting as a full-page overlay.** It's tuned for very heavy de-emphasis of what's underneath; on a regular section it just looks washed-out.
|
package/components/ui/sheet.md
CHANGED
|
@@ -6,16 +6,24 @@ props:
|
|
|
6
6
|
- Sheet: open?, defaultOpen?, onOpenChange?, modal? (default true)
|
|
7
7
|
- SheetTrigger: asChild?: boolean
|
|
8
8
|
- SheetContent: side? "top" | "right" | "bottom" | "left" (default "right")
|
|
9
|
+
- SheetContent: surface? (solid | translucent | glass | glass-strong) — what the sheet panel is *made of*. `solid` is the default opaque `bg-background`. Reach for `glass` whenever the canvas behind the sheet (a layout in progress, a media gallery, a dashboard) should remain visible.
|
|
9
10
|
- SheetContent: className?: string — usually set a width (right/left) or height (top/bottom)
|
|
10
11
|
- SheetTitle / SheetDescription: identify the sheet to screen readers; required for accessibility even if visually styled differently
|
|
11
12
|
- SheetClose: asChild? — usually wraps a Button labelled Cancel or Done
|
|
12
|
-
when_to_use: A panel that slides in from a screen edge — mobile nav drawers, side panels for editing a single record without leaving the list, filter trays on small viewports. For a centered focus modal use Dialog. For a transient announcement use Toast (Sonner). For inline reveals use Collapsible.
|
|
13
|
-
composes_with: [Form controls (an inline edit sheet), Button (trigger + close), AppShellNav (mobile-only swap)]
|
|
14
|
-
aliases: [sheet, drawer, side panel, slide-in, nav drawer, mobile drawer, slide-over, action sheet, modal sheet, bottom sheet, side sheet, react native modal sheet, bottom-sheet, ios action sheet]
|
|
13
|
+
when_to_use: A panel that slides in from a screen edge — mobile nav drawers, side panels for editing a single record without leaving the list, filter trays on small viewports, Studio-style inspector panels. For a centered focus modal use Dialog. For a transient announcement use Toast (Sonner). For inline reveals use Collapsible.
|
|
14
|
+
composes_with: [Form controls (an inline edit sheet), Button (trigger + close), AppShellNav (mobile-only swap), Code (changelog drawers), MediaSurface (image-detail sheets)]
|
|
15
|
+
aliases: [sheet, drawer, side panel, slide-in, nav drawer, mobile drawer, slide-over, action sheet, modal sheet, bottom sheet, side sheet, react native modal sheet, bottom-sheet, ios action sheet, inspector panel, glass sheet, frosted drawer]
|
|
15
16
|
---
|
|
16
17
|
|
|
18
|
+
SheetContent sits at elevation-5. The `surface` axis controls material independently of `side` (which controls layout direction) — every combination is valid.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
### Scenario 1 — Edit-record drawer (default opaque)
|
|
23
|
+
|
|
24
|
+
A right-edge drawer that lets a user edit one record without losing their place in a list. The list is the user's context — the drawer doesn't need to blur it; it just needs to be visibly distinct.
|
|
25
|
+
|
|
17
26
|
```jsx
|
|
18
|
-
// Edit-record drawer from the right edge.
|
|
19
27
|
<Sheet>
|
|
20
28
|
<SheetTrigger asChild>
|
|
21
29
|
<Button variant="outline">Edit user</Button>
|
|
@@ -45,8 +53,92 @@ aliases: [sheet, drawer, side panel, slide-in, nav drawer, mobile drawer, slide-
|
|
|
45
53
|
</Sheet>
|
|
46
54
|
```
|
|
47
55
|
|
|
48
|
-
|
|
56
|
+
`solid` is the right default for editing workflows. Form fields need maximum legibility; blur behind them works against that.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
### Scenario 2 — Glass inspector panel (creative tool aesthetic)
|
|
61
|
+
|
|
62
|
+
You're building a creative tool. The canvas is the work — a Studio layout, an image being annotated, a presentation slide. The inspector panel needs to live alongside the work without obscuring it. Glass is the canonical "I am chrome, not content" signal.
|
|
63
|
+
|
|
64
|
+
```jsx
|
|
65
|
+
<Sheet open={hasSelection} modal={false}>
|
|
66
|
+
<SheetContent
|
|
67
|
+
side="right"
|
|
68
|
+
surface="glass"
|
|
69
|
+
className="w-96 shadow-elevation-5"
|
|
70
|
+
>
|
|
71
|
+
<SheetHeader>
|
|
72
|
+
<SheetTitle>Selection</SheetTitle>
|
|
73
|
+
<SheetDescription>Button — Toolbar > trailing</SheetDescription>
|
|
74
|
+
</SheetHeader>
|
|
75
|
+
|
|
76
|
+
<Stack gap="md" className="py-4">
|
|
77
|
+
<Stack gap="xs">
|
|
78
|
+
<Label>Variant</Label>
|
|
79
|
+
<Select defaultValue="raised">{/* … */}</Select>
|
|
80
|
+
</Stack>
|
|
81
|
+
<Stack gap="xs">
|
|
82
|
+
<Label>Size</Label>
|
|
83
|
+
<ToggleGroup type="single" defaultValue="md">
|
|
84
|
+
<ToggleGroupItem value="sm">sm</ToggleGroupItem>
|
|
85
|
+
<ToggleGroupItem value="md">md</ToggleGroupItem>
|
|
86
|
+
<ToggleGroupItem value="lg">lg</ToggleGroupItem>
|
|
87
|
+
</ToggleGroup>
|
|
88
|
+
</Stack>
|
|
89
|
+
</Stack>
|
|
90
|
+
</SheetContent>
|
|
91
|
+
</Sheet>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Three things to notice: `modal={false}` so the user keeps interacting with the canvas while the inspector is open; `surface="glass"` so the canvas reads through; `shadow-elevation-5` to lift the panel cleanly off the canvas. This is the Studio inspector pattern.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
### Scenario 3 — Bottom action sheet (mobile, glass for iOS feel)
|
|
99
|
+
|
|
100
|
+
The iOS-native action sheet has glass behind it. Matching that material on mobile flows is "feels like a native app" by default.
|
|
101
|
+
|
|
102
|
+
```jsx
|
|
103
|
+
<Sheet open={pickerOpen} onOpenChange={setPickerOpen}>
|
|
104
|
+
<SheetContent
|
|
105
|
+
side="bottom"
|
|
106
|
+
surface="glass"
|
|
107
|
+
className="rounded-t-2xl"
|
|
108
|
+
>
|
|
109
|
+
<SheetHeader className="text-center">
|
|
110
|
+
<SheetTitle>Share screen</SheetTitle>
|
|
111
|
+
</SheetHeader>
|
|
112
|
+
<Stack gap="xs" className="py-4">
|
|
113
|
+
<Button variant="ghost" className="justify-start"><Mail /> Email</Button>
|
|
114
|
+
<Button variant="ghost" className="justify-start"><MessageCircle /> Message</Button>
|
|
115
|
+
<Button variant="ghost" className="justify-start"><Copy /> Copy link</Button>
|
|
116
|
+
</Stack>
|
|
117
|
+
<SheetClose asChild>
|
|
118
|
+
<Button variant="outline" className="w-full">Cancel</Button>
|
|
119
|
+
</SheetClose>
|
|
120
|
+
</SheetContent>
|
|
121
|
+
</Sheet>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`side="bottom"` + `surface="glass"` + `rounded-t-2xl` is the iOS action-sheet recipe. The rounded top corners signal "this can be dismissed by dragging down" even before any gesture handler is wired up.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
### Anti-patterns
|
|
129
|
+
|
|
130
|
+
**DO NOT roll glass by hand on SheetContent.**
|
|
49
131
|
|
|
50
132
|
```jsx
|
|
51
|
-
|
|
133
|
+
{/* ❌ Tailwind soup — no edge highlight, blur isn't theme-tuned. */}
|
|
134
|
+
<SheetContent className="bg-background/60 backdrop-blur-md">
|
|
135
|
+
|
|
136
|
+
{/* ✅ */}
|
|
137
|
+
<SheetContent surface="glass">
|
|
52
138
|
```
|
|
139
|
+
|
|
140
|
+
**DO NOT use `surface="glass"` for a modal sheet that contains a long form.** Form legibility wins over aesthetic. If the user is going to spend 30 seconds in this sheet, give them an opaque background.
|
|
141
|
+
|
|
142
|
+
**DO NOT pair `surface="glass"` with `modal={true}` and the default scrim.** The scrim already dims the canvas — adding glass on top of a dimmed canvas reads as "two competing layers of de-emphasis". Either turn off the scrim (`modal={false}`), or use `surface="solid"`.
|
|
143
|
+
|
|
144
|
+
**DO NOT skip SheetTitle.** Screen readers announce it on open. If the design has no visible title, wrap a `sr-only` one.
|