@shadow-js/ui 0.0.0-stage → 1.0.1
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/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +598 -2
- package/dist/cli.mjs +2281 -0
- package/dist/dev/cli/schema.d.ts +71 -0
- package/dist/dev/index.d.ts +6 -0
- package/dist/dev/index.js +5 -0
- package/dist/dev/locales/ar.d.ts +139 -0
- package/dist/dev/locales/ar.js +127 -0
- package/dist/dev/locales/ar.js.map +1 -0
- package/dist/dev/locales/en.d.ts +139 -0
- package/dist/dev/locales/en.js +127 -0
- package/dist/dev/locales/en.js.map +1 -0
- package/dist/dev/registry/utils/utils.d.ts +27 -0
- package/dist/dev/registry/utils/utils.js +26 -0
- package/dist/dev/registry/utils/utils.js.map +1 -0
- package/dist/dev/theme/theme-logic.d.ts +185 -0
- package/dist/dev/theme/theme-logic.js +183 -0
- package/dist/dev/theme/theme-logic.js.map +1 -0
- package/dist/dev/theme/theme-provider.d.ts +88 -0
- package/dist/dev/theme/theme-provider.js +149 -0
- package/dist/dev/theme/theme-provider.js.map +1 -0
- package/dist/prod/index.js +1 -0
- package/dist/prod/locales/ar.js +2 -0
- package/dist/prod/locales/ar.js.map +1 -0
- package/dist/prod/locales/en.js +2 -0
- package/dist/prod/locales/en.js.map +1 -0
- package/dist/prod/registry/utils/utils.js +2 -0
- package/dist/prod/registry/utils/utils.js.map +1 -0
- package/dist/prod/theme/theme-logic.js +2 -0
- package/dist/prod/theme/theme-logic.js.map +1 -0
- package/dist/prod/theme/theme-provider.js +2 -0
- package/dist/prod/theme/theme-provider.js.map +1 -0
- package/package.json +102 -4
- package/registry/accordion/accordion.tsx +210 -0
- package/registry/accordion/item.json +4 -0
- package/registry/alert/alert.tsx +200 -0
- package/registry/alert/item.json +4 -0
- package/registry/alert-dialog/alert-dialog.tsx +530 -0
- package/registry/alert-dialog/item.json +4 -0
- package/registry/aspect-ratio/aspect-ratio.tsx +71 -0
- package/registry/aspect-ratio/item.json +4 -0
- package/registry/attachment/attachment.tsx +459 -0
- package/registry/attachment/item.json +4 -0
- package/registry/avatar/avatar.tsx +385 -0
- package/registry/avatar/item.json +4 -0
- package/registry/badge/badge.tsx +108 -0
- package/registry/badge/item.json +4 -0
- package/registry/breadcrumb/breadcrumb.tsx +354 -0
- package/registry/breadcrumb/item.json +4 -0
- package/registry/bubble/bubble.tsx +260 -0
- package/registry/bubble/item.json +4 -0
- package/registry/button/button.tsx +125 -0
- package/registry/button/item.json +4 -0
- package/registry/button-group/button-group.tsx +189 -0
- package/registry/button-group/item.json +4 -0
- package/registry/calendar/calendar.tsx +348 -0
- package/registry/calendar/item.json +4 -0
- package/registry/card/card.tsx +311 -0
- package/registry/card/item.json +4 -0
- package/registry/carousel/carousel.tsx +851 -0
- package/registry/carousel/item.json +4 -0
- package/registry/chart/chart.tsx +1956 -0
- package/registry/chart/item.json +4 -0
- package/registry/chart-scale/chart-scale.ts +670 -0
- package/registry/chart-scale/item.json +5 -0
- package/registry/checkbox/checkbox.tsx +79 -0
- package/registry/checkbox/item.json +4 -0
- package/registry/code-block/code-block.tsx +470 -0
- package/registry/code-block/item.json +4 -0
- package/registry/collapsible/collapsible.tsx +117 -0
- package/registry/collapsible/item.json +4 -0
- package/registry/combobox/combobox.tsx +977 -0
- package/registry/combobox/item.json +4 -0
- package/registry/command/command.tsx +757 -0
- package/registry/command/item.json +4 -0
- package/registry/context-menu/context-menu.tsx +697 -0
- package/registry/context-menu/item.json +4 -0
- package/registry/data-table/data-table.tsx +437 -0
- package/registry/data-table/item.json +4 -0
- package/registry/date-picker/date-picker.tsx +349 -0
- package/registry/date-picker/item.json +4 -0
- package/registry/dialog/dialog.tsx +412 -0
- package/registry/dialog/item.json +4 -0
- package/registry/direction/direction.tsx +88 -0
- package/registry/direction/item.json +4 -0
- package/registry/drawer/drawer.tsx +787 -0
- package/registry/drawer/item.json +4 -0
- package/registry/dropdown-menu/dropdown-menu.tsx +692 -0
- package/registry/dropdown-menu/item.json +4 -0
- package/registry/empty/empty.tsx +240 -0
- package/registry/empty/item.json +4 -0
- package/registry/field/field.tsx +624 -0
- package/registry/field/item.json +4 -0
- package/registry/form/form.tsx +1112 -0
- package/registry/form/item.json +4 -0
- package/registry/hover-card/hover-card.tsx +144 -0
- package/registry/hover-card/item.json +4 -0
- package/registry/icons/icons.tsx +1691 -0
- package/registry/icons/item.json +4 -0
- package/registry/input/input.tsx +43 -0
- package/registry/input/item.json +4 -0
- package/registry/input-group/input-group.tsx +315 -0
- package/registry/input-group/item.json +4 -0
- package/registry/input-otp/input-otp.tsx +329 -0
- package/registry/input-otp/item.json +4 -0
- package/registry/item/item.json +4 -0
- package/registry/item/item.tsx +439 -0
- package/registry/kbd/item.json +4 -0
- package/registry/kbd/kbd.tsx +65 -0
- package/registry/label/item.json +4 -0
- package/registry/label/label.tsx +60 -0
- package/registry/marker/item.json +4 -0
- package/registry/marker/marker.tsx +175 -0
- package/registry/menubar/item.json +4 -0
- package/registry/menubar/menubar.tsx +694 -0
- package/registry/message/item.json +4 -0
- package/registry/message/message.tsx +280 -0
- package/registry/message-scroller/item.json +4 -0
- package/registry/message-scroller/message-scroller.tsx +262 -0
- package/registry/native-select/item.json +4 -0
- package/registry/native-select/native-select.tsx +113 -0
- package/registry/navigation-menu/item.json +4 -0
- package/registry/navigation-menu/navigation-menu.tsx +362 -0
- package/registry/pagination/item.json +4 -0
- package/registry/pagination/pagination.tsx +460 -0
- package/registry/password-input/item.json +5 -0
- package/registry/password-input/password-input.tsx +494 -0
- package/registry/popover/item.json +4 -0
- package/registry/popover/popover.tsx +284 -0
- package/registry/progress/item.json +4 -0
- package/registry/progress/progress.tsx +190 -0
- package/registry/questionnaire/item.json +4 -0
- package/registry/questionnaire/questionnaire.tsx +569 -0
- package/registry/radio-group/item.json +4 -0
- package/registry/radio-group/radio-group.tsx +101 -0
- package/registry/resizable/item.json +4 -0
- package/registry/resizable/resizable.tsx +455 -0
- package/registry/scroll-area/item.json +4 -0
- package/registry/scroll-area/scroll-area.tsx +149 -0
- package/registry/select/item.json +4 -0
- package/registry/select/select.tsx +571 -0
- package/registry/separator/item.json +4 -0
- package/registry/separator/separator.tsx +52 -0
- package/registry/sheet/item.json +4 -0
- package/registry/sheet/sheet.tsx +476 -0
- package/registry/sidebar/item.json +4 -0
- package/registry/sidebar/sidebar.tsx +1870 -0
- package/registry/skeleton/item.json +4 -0
- package/registry/skeleton/skeleton.tsx +47 -0
- package/registry/slider/item.json +4 -0
- package/registry/slider/slider.tsx +149 -0
- package/registry/spinner/item.json +4 -0
- package/registry/spinner/spinner.tsx +51 -0
- package/registry/switch/item.json +4 -0
- package/registry/switch/switch.tsx +93 -0
- package/registry/table/item.json +4 -0
- package/registry/table/table.tsx +373 -0
- package/registry/tabs/item.json +4 -0
- package/registry/tabs/tabs.tsx +209 -0
- package/registry/textarea/item.json +4 -0
- package/registry/textarea/textarea.tsx +51 -0
- package/registry/toast/item.json +4 -0
- package/registry/toast/toast.tsx +949 -0
- package/registry/toggle/item.json +4 -0
- package/registry/toggle/toggle.tsx +110 -0
- package/registry/toggle-group/item.json +4 -0
- package/registry/toggle-group/toggle-group.tsx +189 -0
- package/registry/tooltip/item.json +4 -0
- package/registry/tooltip/tooltip.tsx +169 -0
- package/registry/trace/item.json +4 -0
- package/registry/trace/trace.tsx +356 -0
- package/registry/ui-strings/item.json +5 -0
- package/registry/ui-strings/ui-strings.tsx +454 -0
- package/registry/use-highlighted-code/item.json +5 -0
- package/registry/use-highlighted-code/use-highlighted-code.tsx +145 -0
- package/registry/use-mobile/item.json +5 -0
- package/registry/use-mobile/use-mobile.ts +46 -0
- package/registry/utils/item.json +5 -0
- package/registry/utils/utils.ts +38 -0
- package/registry.json +3274 -0
- package/styles/animations.css +321 -0
- package/styles/theme-core.css +55 -0
- package/styles/theme.css +12 -0
- package/styles/themes/gray.css +69 -0
- package/styles/themes/neutral.css +69 -0
- package/styles/themes/slate.css +69 -0
- package/styles/themes/stone.css +69 -0
- package/styles/themes/zinc.css +69 -0
- package/styles/typeset.css +251 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { splitProps } from "@shadow-js/core";
|
|
2
|
+
import type { ComponentProps, JSX } from "@shadow-js/web";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The class list of a message row for each `align`. Use it to lay out any element like a message.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* import { messageVariants } from "@/components/ui/message";
|
|
12
|
+
*
|
|
13
|
+
* export const Row = <div class={messageVariants({ align: "end" })}>Hello</div>;
|
|
14
|
+
* ```
|
|
15
|
+
* @since 1.0.0
|
|
16
|
+
* @see {@link Message}
|
|
17
|
+
*/
|
|
18
|
+
const messageVariants = cva("group/message flex w-full min-w-0 items-end gap-2", {
|
|
19
|
+
variants: {
|
|
20
|
+
align: {
|
|
21
|
+
start: "flex-row justify-start",
|
|
22
|
+
end: "flex-row-reverse justify-start",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: { align: "start" },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Props of {@link Message}: the attributes of a `div` plus `align`, which is `"start"` (the default, for
|
|
30
|
+
* the other side of the conversation) or `"end"` (for the reader's own messages).
|
|
31
|
+
* @since 1.0.0
|
|
32
|
+
* @see {@link Message}
|
|
33
|
+
*/
|
|
34
|
+
export type MessageProps = ComponentProps<"div"> & VariantProps<typeof messageVariants>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* One row of a conversation. It lays out the avatar, the header, the message surface (a `Bubble`, a
|
|
38
|
+
* paragraph, a card) and the footer, and aligns them to the sender's side. The surface itself is a
|
|
39
|
+
* separate component, so a message can hold a bubble, rendered markdown or a tool call. Use
|
|
40
|
+
* {@link MessageGroup} to stack consecutive messages from one sender. For the scrolling container
|
|
41
|
+
* around the conversation, use `MessageScroller`.
|
|
42
|
+
*
|
|
43
|
+
* Keyboard and ARIA: a message is presentational and adds no role or keys; what it shows depends on
|
|
44
|
+
* the content inside. For an in-progress message, use a `Marker` with `role="status"` so assistive technology announces the update.
|
|
45
|
+
*
|
|
46
|
+
* Compared with shadcn/ui: `class` replaces `className`, and every part has a `data-slot`.
|
|
47
|
+
*
|
|
48
|
+
* @param props - The message props: `align`, `children` and the `div` attributes.
|
|
49
|
+
* @returns The message row.
|
|
50
|
+
* @example
|
|
51
|
+
* ```tsx
|
|
52
|
+
* import { Bubble, BubbleContent } from "@/components/ui/bubble";
|
|
53
|
+
* import { Message, MessageContent } from "@/components/ui/message";
|
|
54
|
+
*
|
|
55
|
+
* export function Greeting() {
|
|
56
|
+
* return (
|
|
57
|
+
* <Message>
|
|
58
|
+
* <MessageContent>
|
|
59
|
+
* <Bubble variant="muted">
|
|
60
|
+
* <BubbleContent>How can I help you today?</BubbleContent>
|
|
61
|
+
* </Bubble>
|
|
62
|
+
* </MessageContent>
|
|
63
|
+
* </Message>
|
|
64
|
+
* );
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
* @since 1.0.0
|
|
68
|
+
* @see {@link MessageContent}
|
|
69
|
+
* @see {@link MessageAvatar}
|
|
70
|
+
* @see {@link MessageGroup}
|
|
71
|
+
*/
|
|
72
|
+
function Message(props: MessageProps): JSX.Element {
|
|
73
|
+
const [local, rest] = splitProps(props, ["class", "align"]);
|
|
74
|
+
return (
|
|
75
|
+
<div
|
|
76
|
+
data-slot="message"
|
|
77
|
+
data-align={local.align ?? "start"}
|
|
78
|
+
class={cn(messageVariants({ align: local.align }), local.class)}
|
|
79
|
+
{...rest}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Stacks consecutive messages from one sender. Give the earlier messages an empty {@link MessageAvatar} so
|
|
86
|
+
* they stay aligned with the avatar on the last one.
|
|
87
|
+
*
|
|
88
|
+
* Keyboard and ARIA: a plain `div` with no role and no keys.
|
|
89
|
+
*
|
|
90
|
+
* @param props - The `div` attributes and `class`.
|
|
91
|
+
* @returns The group.
|
|
92
|
+
* @example
|
|
93
|
+
* ```tsx
|
|
94
|
+
* import { Message, MessageContent, MessageGroup } from "@/components/ui/message";
|
|
95
|
+
*
|
|
96
|
+
* export function Burst() {
|
|
97
|
+
* return (
|
|
98
|
+
* <MessageGroup>
|
|
99
|
+
* <Message>
|
|
100
|
+
* <MessageContent>One</MessageContent>
|
|
101
|
+
* </Message>
|
|
102
|
+
* <Message>
|
|
103
|
+
* <MessageContent>Two</MessageContent>
|
|
104
|
+
* </Message>
|
|
105
|
+
* </MessageGroup>
|
|
106
|
+
* );
|
|
107
|
+
* }
|
|
108
|
+
* ```
|
|
109
|
+
* @since 1.0.0
|
|
110
|
+
* @see {@link Message}
|
|
111
|
+
*/
|
|
112
|
+
function MessageGroup(props: ComponentProps<"div">): JSX.Element {
|
|
113
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
114
|
+
return (
|
|
115
|
+
<div
|
|
116
|
+
data-slot="message-group"
|
|
117
|
+
class={cn("flex w-full flex-col gap-1", local.class)}
|
|
118
|
+
{...rest}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The avatar slot beside the message, anchored to the bottom of the row. When the message has a
|
|
125
|
+
* {@link MessageFooter} the avatar sits level with the message surface, not with the footer. Render it empty
|
|
126
|
+
* (no children) to keep the space on earlier messages of a group.
|
|
127
|
+
*
|
|
128
|
+
* Keyboard and ARIA: a plain `div`; the avatar inside carries its own name.
|
|
129
|
+
*
|
|
130
|
+
* @param props - The `div` attributes and `class`.
|
|
131
|
+
* @returns The avatar slot.
|
|
132
|
+
* @example
|
|
133
|
+
* ```tsx
|
|
134
|
+
* import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
135
|
+
* import { Message, MessageAvatar, MessageContent } from "@/components/ui/message";
|
|
136
|
+
*
|
|
137
|
+
* export function WithAvatar() {
|
|
138
|
+
* return (
|
|
139
|
+
* <Message>
|
|
140
|
+
* <MessageAvatar>
|
|
141
|
+
* <Avatar>
|
|
142
|
+
* <AvatarFallback>AL</AvatarFallback>
|
|
143
|
+
* </Avatar>
|
|
144
|
+
* </MessageAvatar>
|
|
145
|
+
* <MessageContent>Hello</MessageContent>
|
|
146
|
+
* </Message>
|
|
147
|
+
* );
|
|
148
|
+
* }
|
|
149
|
+
* ```
|
|
150
|
+
* @since 1.0.0
|
|
151
|
+
* @see {@link Message}
|
|
152
|
+
*/
|
|
153
|
+
function MessageAvatar(props: ComponentProps<"div">): JSX.Element {
|
|
154
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
155
|
+
return (
|
|
156
|
+
<div
|
|
157
|
+
data-slot="message-avatar"
|
|
158
|
+
class={cn(
|
|
159
|
+
"flex size-8 shrink-0 items-end justify-center group-has-data-[slot=message-footer]/message:mb-7",
|
|
160
|
+
local.class,
|
|
161
|
+
)}
|
|
162
|
+
{...rest}
|
|
163
|
+
/>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Holds the header, the message surface and the footer in a column. It is as wide as its content, up to the
|
|
169
|
+
* width of the row, and follows the message's side.
|
|
170
|
+
*
|
|
171
|
+
* Keyboard and ARIA: a plain `div` with no role and no keys.
|
|
172
|
+
*
|
|
173
|
+
* @param props - The `div` attributes and `class`.
|
|
174
|
+
* @returns The content column.
|
|
175
|
+
* @example
|
|
176
|
+
* ```tsx
|
|
177
|
+
* import { MessageContent, MessageHeader } from "@/components/ui/message";
|
|
178
|
+
*
|
|
179
|
+
* export function Named() {
|
|
180
|
+
* return (
|
|
181
|
+
* <MessageContent>
|
|
182
|
+
* <MessageHeader>Ada</MessageHeader>
|
|
183
|
+
* <p>Hello</p>
|
|
184
|
+
* </MessageContent>
|
|
185
|
+
* );
|
|
186
|
+
* }
|
|
187
|
+
* ```
|
|
188
|
+
* @since 1.0.0
|
|
189
|
+
* @see {@link Message}
|
|
190
|
+
*/
|
|
191
|
+
function MessageContent(props: ComponentProps<"div">): JSX.Element {
|
|
192
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
193
|
+
return (
|
|
194
|
+
<div
|
|
195
|
+
data-slot="message-content"
|
|
196
|
+
class={cn(
|
|
197
|
+
"flex min-w-0 max-w-full flex-1 flex-col gap-1 group-data-[align=end]/message:items-end",
|
|
198
|
+
local.class,
|
|
199
|
+
)}
|
|
200
|
+
{...rest}
|
|
201
|
+
/>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Content above the message surface, such as the sender's name. It is always aligned to the start,
|
|
207
|
+
* whatever the message's `align`.
|
|
208
|
+
*
|
|
209
|
+
* Keyboard and ARIA: a plain `div` with no role and no keys.
|
|
210
|
+
*
|
|
211
|
+
* @param props - The `div` attributes and `class`.
|
|
212
|
+
* @returns The header.
|
|
213
|
+
* @example
|
|
214
|
+
* ```tsx
|
|
215
|
+
* import { MessageHeader } from "@/components/ui/message";
|
|
216
|
+
*
|
|
217
|
+
* export const Sender = <MessageHeader>Ada Lovelace</MessageHeader>;
|
|
218
|
+
* ```
|
|
219
|
+
* @since 1.0.0
|
|
220
|
+
* @see {@link MessageContent}
|
|
221
|
+
*/
|
|
222
|
+
function MessageHeader(props: ComponentProps<"div">): JSX.Element {
|
|
223
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
224
|
+
return (
|
|
225
|
+
<div
|
|
226
|
+
data-slot="message-header"
|
|
227
|
+
class={cn("self-start px-1 text-xs text-muted-foreground", local.class)}
|
|
228
|
+
{...rest}
|
|
229
|
+
/>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Content below the message surface: delivery status, a time, or actions such as copy and retry. It follows the
|
|
235
|
+
* message's side, so the actions of a message with `align="end"` sit at the end. Give each icon-only button an `aria-label`.
|
|
236
|
+
*
|
|
237
|
+
* Keyboard and ARIA: a plain `div` with no role and no keys; the buttons inside keep theirs.
|
|
238
|
+
*
|
|
239
|
+
* @param props - The `div` attributes and `class`.
|
|
240
|
+
* @returns The footer.
|
|
241
|
+
* @example
|
|
242
|
+
* ```tsx
|
|
243
|
+
* import { Button } from "@/components/ui/button";
|
|
244
|
+
* import { CopyIcon } from "@/components/ui/icons";
|
|
245
|
+
* import { MessageFooter } from "@/components/ui/message";
|
|
246
|
+
*
|
|
247
|
+
* export const Actions = (
|
|
248
|
+
* <MessageFooter>
|
|
249
|
+
* <Button variant="ghost" size="icon-sm" aria-label="Copy">
|
|
250
|
+
* <CopyIcon />
|
|
251
|
+
* </Button>
|
|
252
|
+
* </MessageFooter>
|
|
253
|
+
* );
|
|
254
|
+
* ```
|
|
255
|
+
* @since 1.0.0
|
|
256
|
+
* @see {@link MessageContent}
|
|
257
|
+
*/
|
|
258
|
+
function MessageFooter(props: ComponentProps<"div">): JSX.Element {
|
|
259
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
260
|
+
return (
|
|
261
|
+
<div
|
|
262
|
+
data-slot="message-footer"
|
|
263
|
+
class={cn(
|
|
264
|
+
"flex items-center gap-1 text-xs text-muted-foreground group-data-[align=end]/message:justify-end",
|
|
265
|
+
local.class,
|
|
266
|
+
)}
|
|
267
|
+
{...rest}
|
|
268
|
+
/>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export {
|
|
273
|
+
Message,
|
|
274
|
+
MessageAvatar,
|
|
275
|
+
MessageContent,
|
|
276
|
+
MessageFooter,
|
|
277
|
+
MessageGroup,
|
|
278
|
+
MessageHeader,
|
|
279
|
+
messageVariants,
|
|
280
|
+
};
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { splitProps } from "@shadow-js/core";
|
|
2
|
+
import {
|
|
3
|
+
type MessageScrollerButtonProps,
|
|
4
|
+
type MessageScrollerContentProps,
|
|
5
|
+
type MessageScrollerItemProps,
|
|
6
|
+
MessageScroller as MessageScrollerPrimitive,
|
|
7
|
+
type MessageScrollerProviderProps,
|
|
8
|
+
type MessageScrollerRootProps,
|
|
9
|
+
type MessageScrollerViewportProps,
|
|
10
|
+
type PolymorphicTarget,
|
|
11
|
+
useMessageScroller,
|
|
12
|
+
useMessageScrollerScrollable,
|
|
13
|
+
useMessageScrollerVisibility,
|
|
14
|
+
} from "@shadow-js/primitives";
|
|
15
|
+
import type { JSX } from "@shadow-js/web";
|
|
16
|
+
import { ArrowRightIcon } from "@/components/ui/icons";
|
|
17
|
+
import { useUiStrings } from "@/lib/ui-strings";
|
|
18
|
+
import { cn } from "@/lib/utils";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The state of a conversation's scroller. It renders no element: put the {@link MessageScroller} frame, the viewport, the content and the rows inside it.
|
|
22
|
+
* Its props are the behavior: `autoScroll` follows the newest text while the reader stays at the bottom, `defaultScrollPosition` says where a saved
|
|
23
|
+
* thread opens (`"start"`, `"end"` or `"last-anchor"`), `scrollPreviousItemPeek` is how much of the row before a new turn stays visible, and
|
|
24
|
+
* `scrollMargin` and `scrollEdgeThreshold` tune the placement and the edges.
|
|
25
|
+
*
|
|
26
|
+
* Keyboard and ARIA: none of its own. See {@link MessageScrollerViewport}.
|
|
27
|
+
*
|
|
28
|
+
* @param props - The behavior props and the subtree.
|
|
29
|
+
* @returns The subtree.
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* import {
|
|
33
|
+
* MessageScroller,
|
|
34
|
+
* MessageScrollerButton,
|
|
35
|
+
* MessageScrollerContent,
|
|
36
|
+
* MessageScrollerItem,
|
|
37
|
+
* MessageScrollerProvider,
|
|
38
|
+
* MessageScrollerViewport,
|
|
39
|
+
* } from "@/components/ui/message-scroller";
|
|
40
|
+
*
|
|
41
|
+
* export function Thread() {
|
|
42
|
+
* return (
|
|
43
|
+
* <MessageScrollerProvider autoScroll>
|
|
44
|
+
* <MessageScroller class="h-96">
|
|
45
|
+
* <MessageScrollerViewport>
|
|
46
|
+
* <MessageScrollerContent>
|
|
47
|
+
* <MessageScrollerItem messageId="1" scrollAnchor>
|
|
48
|
+
* Hello
|
|
49
|
+
* </MessageScrollerItem>
|
|
50
|
+
* </MessageScrollerContent>
|
|
51
|
+
* </MessageScrollerViewport>
|
|
52
|
+
* <MessageScrollerButton />
|
|
53
|
+
* </MessageScroller>
|
|
54
|
+
* </MessageScrollerProvider>
|
|
55
|
+
* );
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
* @since 1.0.0
|
|
59
|
+
* @see {@link MessageScroller}
|
|
60
|
+
*/
|
|
61
|
+
function MessageScrollerProvider(props: MessageScrollerProviderProps): JSX.Element {
|
|
62
|
+
return <MessageScrollerPrimitive.Provider {...props} />;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The frame around the viewport and the button. It fills its parent, so put it in something with a height. The button is placed against it.
|
|
67
|
+
*
|
|
68
|
+
* Keyboard and ARIA: a plain `div`; none of its own. It carries `data-scrollable`, `data-autoscrolling` and `data-pending-scroll`.
|
|
69
|
+
*
|
|
70
|
+
* @param props - The frame props.
|
|
71
|
+
* @returns The frame.
|
|
72
|
+
* @example
|
|
73
|
+
* ```tsx
|
|
74
|
+
* import { MessageScroller, MessageScrollerProvider } from "@/components/ui/message-scroller";
|
|
75
|
+
*
|
|
76
|
+
* export const Frame = (
|
|
77
|
+
* <MessageScrollerProvider>
|
|
78
|
+
* <MessageScroller class="h-96" />
|
|
79
|
+
* </MessageScrollerProvider>
|
|
80
|
+
* );
|
|
81
|
+
* ```
|
|
82
|
+
* @since 1.0.0
|
|
83
|
+
* @see {@link MessageScrollerProvider}
|
|
84
|
+
*/
|
|
85
|
+
function MessageScroller<T extends PolymorphicTarget = "div">(
|
|
86
|
+
props: MessageScrollerRootProps<T>,
|
|
87
|
+
): JSX.Element {
|
|
88
|
+
const [local, rest] = splitProps(props as unknown as { readonly class?: string }, ["class"]);
|
|
89
|
+
return (
|
|
90
|
+
<MessageScrollerPrimitive.Root
|
|
91
|
+
data-slot="message-scroller"
|
|
92
|
+
class={cn("relative flex h-full min-h-0 w-full flex-col overflow-hidden", local.class)}
|
|
93
|
+
{...(rest as object)}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The scrollable region. It is a labeled landmark ("Messages") and a tab stop, so a keyboard user can scroll the thread with the arrow and page keys. It is hidden
|
|
100
|
+
* with `visibility` while the opening position is being applied, so a saved thread never visibly jumps. `preserveScrollOnPrepend` (on by default) keeps the row the
|
|
101
|
+
* reader is looking at still when older rows load above it.
|
|
102
|
+
*
|
|
103
|
+
* Keyboard and ARIA: `role="region"` named by the kit's string `messageScroller.label` (override with `aria-label`), `tabindex="0"`; the browser's own scroll keys.
|
|
104
|
+
*
|
|
105
|
+
* @param props - The viewport props.
|
|
106
|
+
* @returns The viewport.
|
|
107
|
+
* @example
|
|
108
|
+
* ```tsx
|
|
109
|
+
* import { MessageScrollerViewport } from "@/components/ui/message-scroller";
|
|
110
|
+
*
|
|
111
|
+
* export const Viewport = <MessageScrollerViewport aria-label="Conversation with Ada" />;
|
|
112
|
+
* ```
|
|
113
|
+
* @since 1.0.0
|
|
114
|
+
* @see {@link MessageScrollerContent}
|
|
115
|
+
*/
|
|
116
|
+
function MessageScrollerViewport<T extends PolymorphicTarget = "div">(
|
|
117
|
+
props: MessageScrollerViewportProps<T>,
|
|
118
|
+
): JSX.Element {
|
|
119
|
+
const [local, rest] = splitProps(props as unknown as { readonly class?: string }, ["class"]);
|
|
120
|
+
const t = useUiStrings();
|
|
121
|
+
return (
|
|
122
|
+
<MessageScrollerPrimitive.Viewport
|
|
123
|
+
data-slot="message-scroller-viewport"
|
|
124
|
+
aria-label={t("messageScroller.label", "Messages")}
|
|
125
|
+
class={cn(
|
|
126
|
+
"size-full min-h-0 overflow-y-auto overscroll-contain outline-none focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50 data-[pending-scroll]:invisible focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-transparent",
|
|
127
|
+
local.class,
|
|
128
|
+
)}
|
|
129
|
+
{...(rest as object)}
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The list of rows, a polite `log`: each row that is added is announced once. Pass `aria-busy` while a reply streams so the announcement waits until the message is complete. A
|
|
136
|
+
* column with a gap by default.
|
|
137
|
+
*
|
|
138
|
+
* Keyboard and ARIA: `role="log"` and `aria-relevant="additions"`; no keys. The spacer at its end is `aria-hidden`.
|
|
139
|
+
*
|
|
140
|
+
* @param props - The content props.
|
|
141
|
+
* @returns The content.
|
|
142
|
+
* @example
|
|
143
|
+
* ```tsx
|
|
144
|
+
* import { MessageScrollerContent } from "@/components/ui/message-scroller";
|
|
145
|
+
*
|
|
146
|
+
* export const Content = <MessageScrollerContent aria-busy={true} />;
|
|
147
|
+
* ```
|
|
148
|
+
* @since 1.0.0
|
|
149
|
+
* @see {@link MessageScrollerItem}
|
|
150
|
+
*/
|
|
151
|
+
function MessageScrollerContent<T extends PolymorphicTarget = "div">(
|
|
152
|
+
props: MessageScrollerContentProps<T>,
|
|
153
|
+
): JSX.Element {
|
|
154
|
+
const [local, rest] = splitProps(props as unknown as { readonly class?: string }, ["class"]);
|
|
155
|
+
return (
|
|
156
|
+
<MessageScrollerPrimitive.Content
|
|
157
|
+
data-slot="message-scroller-content"
|
|
158
|
+
class={cn("flex min-h-full flex-col gap-4 p-4", local.class)}
|
|
159
|
+
{...(rest as object)}
|
|
160
|
+
/>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* One row of the thread. Give it a stable `messageId`, and `scrollAnchor` on the reader's own turn so a new question settles near the top. A row
|
|
166
|
+
* far from the window is not painted (`content-visibility: auto`) and keeps the size it last had, so a long thread stays cheap.
|
|
167
|
+
*
|
|
168
|
+
* Keyboard and ARIA: a plain `div`; none of its own.
|
|
169
|
+
*
|
|
170
|
+
* @param props - The row props: `messageId`, `scrollAnchor` and the `div` attributes.
|
|
171
|
+
* @returns The row.
|
|
172
|
+
* @example
|
|
173
|
+
* ```tsx
|
|
174
|
+
* import { MessageScrollerItem } from "@/components/ui/message-scroller";
|
|
175
|
+
*
|
|
176
|
+
* export const Row = (
|
|
177
|
+
* <MessageScrollerItem messageId="42" scrollAnchor>
|
|
178
|
+
* How do I add a dialog?
|
|
179
|
+
* </MessageScrollerItem>
|
|
180
|
+
* );
|
|
181
|
+
* ```
|
|
182
|
+
* @since 1.0.0
|
|
183
|
+
* @see {@link MessageScrollerContent}
|
|
184
|
+
*/
|
|
185
|
+
function MessageScrollerItem<T extends PolymorphicTarget = "div">(
|
|
186
|
+
props: MessageScrollerItemProps<T>,
|
|
187
|
+
): JSX.Element {
|
|
188
|
+
const [local, rest] = splitProps(props as unknown as { readonly class?: string }, ["class"]);
|
|
189
|
+
return (
|
|
190
|
+
<MessageScrollerPrimitive.Item
|
|
191
|
+
data-slot="message-scroller-item"
|
|
192
|
+
class={cn("[contain-intrinsic-size:auto_5rem] [content-visibility:auto]", local.class)}
|
|
193
|
+
{...(rest as object)}
|
|
194
|
+
/>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* A round button that scrolls to the end of the thread (or to the start with `direction="start"`). It fades out and leaves the tab order while there is nothing to
|
|
200
|
+
* scroll toward, and it sits at the bottom center of the frame. With no children it shows an arrow and no text; its name is the kit's string
|
|
201
|
+
* `messageScroller.toEnd` ("Scroll to latest") or `messageScroller.toStart`.
|
|
202
|
+
*
|
|
203
|
+
* Keyboard and ARIA: a native button (Enter and Space). Inactive, it is `inert` and `tabindex="-1"`.
|
|
204
|
+
*
|
|
205
|
+
* @param props - The button props: `direction`, `behavior` and the button attributes.
|
|
206
|
+
* @returns The button.
|
|
207
|
+
* @example
|
|
208
|
+
* ```tsx
|
|
209
|
+
* import { MessageScrollerButton } from "@/components/ui/message-scroller";
|
|
210
|
+
*
|
|
211
|
+
* export const Jump = <MessageScrollerButton />;
|
|
212
|
+
* ```
|
|
213
|
+
* @since 1.0.0
|
|
214
|
+
* @see {@link MessageScrollerProvider}
|
|
215
|
+
*/
|
|
216
|
+
function MessageScrollerButton<T extends PolymorphicTarget = "button">(
|
|
217
|
+
props: MessageScrollerButtonProps<T>,
|
|
218
|
+
): JSX.Element {
|
|
219
|
+
const [local, rest] = splitProps(
|
|
220
|
+
props as unknown as {
|
|
221
|
+
readonly class?: string;
|
|
222
|
+
readonly children?: JSX.Element;
|
|
223
|
+
readonly direction?: "start" | "end";
|
|
224
|
+
},
|
|
225
|
+
["class", "children", "direction"],
|
|
226
|
+
);
|
|
227
|
+
const t = useUiStrings();
|
|
228
|
+
const start = (): boolean => local.direction === "start";
|
|
229
|
+
return (
|
|
230
|
+
<MessageScrollerPrimitive.Button
|
|
231
|
+
data-slot="message-scroller-button"
|
|
232
|
+
direction={local.direction}
|
|
233
|
+
aria-label={
|
|
234
|
+
start()
|
|
235
|
+
? t("messageScroller.toStart", "Scroll to start")
|
|
236
|
+
: t("messageScroller.toEnd", "Scroll to latest")
|
|
237
|
+
}
|
|
238
|
+
class={cn(
|
|
239
|
+
"absolute start-1/2 z-10 inline-flex size-8 -translate-x-1/2 items-center justify-center rounded-full border bg-background text-foreground shadow-md outline-none transition-opacity hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50 rtl:translate-x-1/2 motion-reduce:transition-none data-[active=false]:pointer-events-none data-[active=false]:opacity-0 [&_svg]:size-4 focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-transparent",
|
|
240
|
+
start() ? "top-3" : "bottom-3",
|
|
241
|
+
local.class,
|
|
242
|
+
)}
|
|
243
|
+
{...(rest as object)}
|
|
244
|
+
>
|
|
245
|
+
{local.children ?? (
|
|
246
|
+
<ArrowRightIcon aria-hidden="true" class={start() ? "-rotate-90" : "rotate-90"} />
|
|
247
|
+
)}
|
|
248
|
+
</MessageScrollerPrimitive.Button>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export {
|
|
253
|
+
MessageScroller,
|
|
254
|
+
MessageScrollerButton,
|
|
255
|
+
MessageScrollerContent,
|
|
256
|
+
MessageScrollerItem,
|
|
257
|
+
MessageScrollerProvider,
|
|
258
|
+
MessageScrollerViewport,
|
|
259
|
+
useMessageScroller,
|
|
260
|
+
useMessageScrollerScrollable,
|
|
261
|
+
useMessageScrollerVisibility,
|
|
262
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { splitProps } from "@shadow-js/core";
|
|
2
|
+
import type { ComponentProps, JSX } from "@shadow-js/web";
|
|
3
|
+
import { ChevronDownIcon } from "@/components/ui/icons";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Props of {@link NativeSelect}: the attributes of the `select`, and its size.
|
|
8
|
+
* @since 1.0.0
|
|
9
|
+
* @see {@link NativeSelect}
|
|
10
|
+
*/
|
|
11
|
+
export type NativeSelectProps = ComponentProps<"select"> & {
|
|
12
|
+
/**
|
|
13
|
+
* The height of the control: `default` is 36px and `sm` is 32px.
|
|
14
|
+
* @default "default"
|
|
15
|
+
*/
|
|
16
|
+
readonly size?: "sm" | "default" | undefined;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The browser's own select, in the kit's look: the operating system's picker and keyboard, styled
|
|
21
|
+
* like an input, with a chevron at the end (on the right in left-to-right text). Use
|
|
22
|
+
* {@link NativeSelectOption} and {@link NativeSelectOptGroup} for its options.
|
|
23
|
+
*
|
|
24
|
+
* Keyboard and ARIA: the keys are the native select's. The select has the `combobox` role and
|
|
25
|
+
* adds no ARIA attribute of its own. It has no visible name, so name it with `aria-label`, or with
|
|
26
|
+
* a {@link Label} whose `htmlFor` points at its `id`. `aria-invalid="true"`, which you pass, turns
|
|
27
|
+
* the border destructive.
|
|
28
|
+
*
|
|
29
|
+
* @param props The `select` attributes plus `size`. `class` styles the select itself; the chevron
|
|
30
|
+
* sits in the wrapper.
|
|
31
|
+
* @returns The wrapper `div` with the select and its chevron.
|
|
32
|
+
* @example
|
|
33
|
+
* import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select";
|
|
34
|
+
*
|
|
35
|
+
* <NativeSelect aria-label="Country">
|
|
36
|
+
* <NativeSelectOption value="se">Sweden</NativeSelectOption>
|
|
37
|
+
* </NativeSelect>
|
|
38
|
+
* @since 1.0.0
|
|
39
|
+
* @see {@link NativeSelectOption}
|
|
40
|
+
*/
|
|
41
|
+
function NativeSelect(props: NativeSelectProps): JSX.Element {
|
|
42
|
+
const [local, rest] = splitProps(props as NativeSelectProps & { readonly class?: unknown }, [
|
|
43
|
+
"class",
|
|
44
|
+
"size",
|
|
45
|
+
]);
|
|
46
|
+
return (
|
|
47
|
+
<div data-slot="native-select-wrapper" class="group/native-select relative w-fit">
|
|
48
|
+
<select
|
|
49
|
+
data-slot="native-select"
|
|
50
|
+
data-size={(local.size as string | undefined) ?? "default"}
|
|
51
|
+
class={cn(
|
|
52
|
+
"h-9 w-full min-w-0 appearance-none rounded-md border border-input bg-transparent py-2 ps-3 pe-9 text-sm shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50",
|
|
53
|
+
// The small box is 32px: its padding shrinks with it, or the 20px text line is clipped.
|
|
54
|
+
"data-[size=sm]:h-8 data-[size=sm]:py-1",
|
|
55
|
+
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 outline-none focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-transparent",
|
|
56
|
+
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
|
57
|
+
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
58
|
+
local.class as string | undefined,
|
|
59
|
+
)}
|
|
60
|
+
{...rest}
|
|
61
|
+
/>
|
|
62
|
+
<ChevronDownIcon
|
|
63
|
+
aria-hidden="true"
|
|
64
|
+
data-slot="native-select-icon"
|
|
65
|
+
class="pointer-events-none absolute top-1/2 end-3.5 size-4 -translate-y-1/2 opacity-50 select-none group-has-[:disabled]/native-select:opacity-30 rtl:-scale-x-100"
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* An option of a {@link NativeSelect}: a native `option` with its `value` and its label as text.
|
|
73
|
+
*
|
|
74
|
+
* Keyboard and ARIA: the select's keys choose it; the option has the `option` role of the browser.
|
|
75
|
+
*
|
|
76
|
+
* @param props The `option` attributes (`value`, `disabled`, `selected`); the children are its label.
|
|
77
|
+
* @returns The `option` element.
|
|
78
|
+
* @example
|
|
79
|
+
* import { NativeSelectOption } from "@/components/ui/native-select";
|
|
80
|
+
*
|
|
81
|
+
* <NativeSelectOption value="se">Sweden</NativeSelectOption>
|
|
82
|
+
* @since 1.0.0
|
|
83
|
+
* @see {@link NativeSelect}
|
|
84
|
+
*/
|
|
85
|
+
function NativeSelectOption(props: ComponentProps<"option">): JSX.Element {
|
|
86
|
+
return <option data-slot="native-select-option" {...props} />;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A labeled group of options of a {@link NativeSelect}: a native `optgroup`. Its `label` is shown
|
|
91
|
+
* as the group's heading in the picker.
|
|
92
|
+
*
|
|
93
|
+
* Keyboard and ARIA: no keys and no role of its own; the browser announces the `label` as the
|
|
94
|
+
* group's name.
|
|
95
|
+
*
|
|
96
|
+
* @param props The `optgroup` attributes, with `label` as the group's name. The children are the options.
|
|
97
|
+
* @returns The `optgroup` element.
|
|
98
|
+
* @example
|
|
99
|
+
* import { NativeSelect, NativeSelectOptGroup, NativeSelectOption } from "@/components/ui/native-select";
|
|
100
|
+
*
|
|
101
|
+
* <NativeSelect aria-label="Country">
|
|
102
|
+
* <NativeSelectOptGroup label="Nordic">
|
|
103
|
+
* <NativeSelectOption value="se">Sweden</NativeSelectOption>
|
|
104
|
+
* </NativeSelectOptGroup>
|
|
105
|
+
* </NativeSelect>
|
|
106
|
+
* @since 1.0.0
|
|
107
|
+
* @see {@link NativeSelectOption}
|
|
108
|
+
*/
|
|
109
|
+
function NativeSelectOptGroup(props: ComponentProps<"optgroup">): JSX.Element {
|
|
110
|
+
return <optgroup data-slot="native-select-optgroup" {...props} />;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { NativeSelect, NativeSelectOptGroup, NativeSelectOption };
|