@shadow-js/ui 0.0.0-stage → 1.0.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.
Files changed (190) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +598 -2
  4. package/dist/cli.mjs +2293 -0
  5. package/dist/dev/cli/schema.d.ts +73 -0
  6. package/dist/dev/index.d.ts +6 -0
  7. package/dist/dev/index.js +5 -0
  8. package/dist/dev/locales/ar.d.ts +141 -0
  9. package/dist/dev/locales/ar.js +128 -0
  10. package/dist/dev/locales/ar.js.map +1 -0
  11. package/dist/dev/locales/en.d.ts +141 -0
  12. package/dist/dev/locales/en.js +128 -0
  13. package/dist/dev/locales/en.js.map +1 -0
  14. package/dist/dev/registry/utils/utils.d.ts +29 -0
  15. package/dist/dev/registry/utils/utils.js +27 -0
  16. package/dist/dev/registry/utils/utils.js.map +1 -0
  17. package/dist/dev/theme/theme-logic.d.ts +187 -0
  18. package/dist/dev/theme/theme-logic.js +184 -0
  19. package/dist/dev/theme/theme-logic.js.map +1 -0
  20. package/dist/dev/theme/theme-provider.d.ts +90 -0
  21. package/dist/dev/theme/theme-provider.js +150 -0
  22. package/dist/dev/theme/theme-provider.js.map +1 -0
  23. package/dist/prod/index.js +1 -0
  24. package/dist/prod/locales/ar.js +2 -0
  25. package/dist/prod/locales/ar.js.map +1 -0
  26. package/dist/prod/locales/en.js +2 -0
  27. package/dist/prod/locales/en.js.map +1 -0
  28. package/dist/prod/registry/utils/utils.js +2 -0
  29. package/dist/prod/registry/utils/utils.js.map +1 -0
  30. package/dist/prod/theme/theme-logic.js +2 -0
  31. package/dist/prod/theme/theme-logic.js.map +1 -0
  32. package/dist/prod/theme/theme-provider.js +2 -0
  33. package/dist/prod/theme/theme-provider.js.map +1 -0
  34. package/package.json +102 -4
  35. package/registry/accordion/accordion.tsx +210 -0
  36. package/registry/accordion/item.json +4 -0
  37. package/registry/alert/alert.tsx +200 -0
  38. package/registry/alert/item.json +4 -0
  39. package/registry/alert-dialog/alert-dialog.tsx +530 -0
  40. package/registry/alert-dialog/item.json +4 -0
  41. package/registry/aspect-ratio/aspect-ratio.tsx +71 -0
  42. package/registry/aspect-ratio/item.json +4 -0
  43. package/registry/attachment/attachment.tsx +459 -0
  44. package/registry/attachment/item.json +4 -0
  45. package/registry/avatar/avatar.tsx +385 -0
  46. package/registry/avatar/item.json +4 -0
  47. package/registry/badge/badge.tsx +108 -0
  48. package/registry/badge/item.json +4 -0
  49. package/registry/breadcrumb/breadcrumb.tsx +354 -0
  50. package/registry/breadcrumb/item.json +4 -0
  51. package/registry/bubble/bubble.tsx +260 -0
  52. package/registry/bubble/item.json +4 -0
  53. package/registry/button/button.tsx +125 -0
  54. package/registry/button/item.json +4 -0
  55. package/registry/button-group/button-group.tsx +189 -0
  56. package/registry/button-group/item.json +4 -0
  57. package/registry/calendar/calendar.tsx +348 -0
  58. package/registry/calendar/item.json +4 -0
  59. package/registry/card/card.tsx +311 -0
  60. package/registry/card/item.json +4 -0
  61. package/registry/carousel/carousel.tsx +851 -0
  62. package/registry/carousel/item.json +4 -0
  63. package/registry/chart/chart.tsx +1956 -0
  64. package/registry/chart/item.json +4 -0
  65. package/registry/chart-scale/chart-scale.ts +670 -0
  66. package/registry/chart-scale/item.json +5 -0
  67. package/registry/checkbox/checkbox.tsx +79 -0
  68. package/registry/checkbox/item.json +4 -0
  69. package/registry/code-block/code-block.tsx +470 -0
  70. package/registry/code-block/item.json +4 -0
  71. package/registry/collapsible/collapsible.tsx +117 -0
  72. package/registry/collapsible/item.json +4 -0
  73. package/registry/combobox/combobox.tsx +977 -0
  74. package/registry/combobox/item.json +4 -0
  75. package/registry/command/command.tsx +757 -0
  76. package/registry/command/item.json +4 -0
  77. package/registry/context-menu/context-menu.tsx +697 -0
  78. package/registry/context-menu/item.json +4 -0
  79. package/registry/data-table/data-table.tsx +437 -0
  80. package/registry/data-table/item.json +4 -0
  81. package/registry/date-picker/date-picker.tsx +349 -0
  82. package/registry/date-picker/item.json +4 -0
  83. package/registry/dialog/dialog.tsx +412 -0
  84. package/registry/dialog/item.json +4 -0
  85. package/registry/direction/direction.tsx +88 -0
  86. package/registry/direction/item.json +4 -0
  87. package/registry/drawer/drawer.tsx +787 -0
  88. package/registry/drawer/item.json +4 -0
  89. package/registry/dropdown-menu/dropdown-menu.tsx +692 -0
  90. package/registry/dropdown-menu/item.json +4 -0
  91. package/registry/empty/empty.tsx +240 -0
  92. package/registry/empty/item.json +4 -0
  93. package/registry/field/field.tsx +624 -0
  94. package/registry/field/item.json +4 -0
  95. package/registry/form/form.tsx +1112 -0
  96. package/registry/form/item.json +4 -0
  97. package/registry/hover-card/hover-card.tsx +144 -0
  98. package/registry/hover-card/item.json +4 -0
  99. package/registry/icons/icons.tsx +1691 -0
  100. package/registry/icons/item.json +4 -0
  101. package/registry/input/input.tsx +43 -0
  102. package/registry/input/item.json +4 -0
  103. package/registry/input-group/input-group.tsx +315 -0
  104. package/registry/input-group/item.json +4 -0
  105. package/registry/input-otp/input-otp.tsx +329 -0
  106. package/registry/input-otp/item.json +4 -0
  107. package/registry/item/item.json +4 -0
  108. package/registry/item/item.tsx +439 -0
  109. package/registry/kbd/item.json +4 -0
  110. package/registry/kbd/kbd.tsx +65 -0
  111. package/registry/label/item.json +4 -0
  112. package/registry/label/label.tsx +60 -0
  113. package/registry/marker/item.json +4 -0
  114. package/registry/marker/marker.tsx +175 -0
  115. package/registry/menubar/item.json +4 -0
  116. package/registry/menubar/menubar.tsx +694 -0
  117. package/registry/message/item.json +4 -0
  118. package/registry/message/message.tsx +280 -0
  119. package/registry/message-scroller/item.json +4 -0
  120. package/registry/message-scroller/message-scroller.tsx +262 -0
  121. package/registry/native-select/item.json +4 -0
  122. package/registry/native-select/native-select.tsx +113 -0
  123. package/registry/navigation-menu/item.json +4 -0
  124. package/registry/navigation-menu/navigation-menu.tsx +362 -0
  125. package/registry/pagination/item.json +4 -0
  126. package/registry/pagination/pagination.tsx +460 -0
  127. package/registry/password-input/item.json +5 -0
  128. package/registry/password-input/password-input.tsx +494 -0
  129. package/registry/popover/item.json +4 -0
  130. package/registry/popover/popover.tsx +284 -0
  131. package/registry/progress/item.json +4 -0
  132. package/registry/progress/progress.tsx +190 -0
  133. package/registry/questionnaire/item.json +4 -0
  134. package/registry/questionnaire/questionnaire.tsx +569 -0
  135. package/registry/radio-group/item.json +4 -0
  136. package/registry/radio-group/radio-group.tsx +101 -0
  137. package/registry/resizable/item.json +4 -0
  138. package/registry/resizable/resizable.tsx +455 -0
  139. package/registry/scroll-area/item.json +4 -0
  140. package/registry/scroll-area/scroll-area.tsx +149 -0
  141. package/registry/select/item.json +4 -0
  142. package/registry/select/select.tsx +571 -0
  143. package/registry/separator/item.json +4 -0
  144. package/registry/separator/separator.tsx +52 -0
  145. package/registry/sheet/item.json +4 -0
  146. package/registry/sheet/sheet.tsx +476 -0
  147. package/registry/sidebar/item.json +4 -0
  148. package/registry/sidebar/sidebar.tsx +1870 -0
  149. package/registry/skeleton/item.json +4 -0
  150. package/registry/skeleton/skeleton.tsx +47 -0
  151. package/registry/slider/item.json +4 -0
  152. package/registry/slider/slider.tsx +149 -0
  153. package/registry/spinner/item.json +4 -0
  154. package/registry/spinner/spinner.tsx +51 -0
  155. package/registry/switch/item.json +4 -0
  156. package/registry/switch/switch.tsx +93 -0
  157. package/registry/table/item.json +4 -0
  158. package/registry/table/table.tsx +373 -0
  159. package/registry/tabs/item.json +4 -0
  160. package/registry/tabs/tabs.tsx +209 -0
  161. package/registry/textarea/item.json +4 -0
  162. package/registry/textarea/textarea.tsx +51 -0
  163. package/registry/toast/item.json +4 -0
  164. package/registry/toast/toast.tsx +949 -0
  165. package/registry/toggle/item.json +4 -0
  166. package/registry/toggle/toggle.tsx +110 -0
  167. package/registry/toggle-group/item.json +4 -0
  168. package/registry/toggle-group/toggle-group.tsx +189 -0
  169. package/registry/tooltip/item.json +4 -0
  170. package/registry/tooltip/tooltip.tsx +169 -0
  171. package/registry/trace/item.json +4 -0
  172. package/registry/trace/trace.tsx +356 -0
  173. package/registry/ui-strings/item.json +5 -0
  174. package/registry/ui-strings/ui-strings.tsx +454 -0
  175. package/registry/use-highlighted-code/item.json +5 -0
  176. package/registry/use-highlighted-code/use-highlighted-code.tsx +145 -0
  177. package/registry/use-mobile/item.json +5 -0
  178. package/registry/use-mobile/use-mobile.ts +46 -0
  179. package/registry/utils/item.json +5 -0
  180. package/registry/utils/utils.ts +38 -0
  181. package/registry.json +3274 -0
  182. package/styles/animations.css +321 -0
  183. package/styles/theme-core.css +55 -0
  184. package/styles/theme.css +12 -0
  185. package/styles/themes/gray.css +69 -0
  186. package/styles/themes/neutral.css +69 -0
  187. package/styles/themes/slate.css +69 -0
  188. package/styles/themes/stone.css +69 -0
  189. package/styles/themes/zinc.css +69 -0
  190. 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,4 @@
1
+ {
2
+ "title": "Message Scroller",
3
+ "description": "A chat transcript scroller that keeps the reader's place while a reply streams: anchored turns, following the newest text, history loading and a jump button."
4
+ }
@@ -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,4 @@
1
+ {
2
+ "title": "Native Select",
3
+ "description": "The browser's own select, styled like an input, with its picker and keyboard support."
4
+ }
@@ -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 };
@@ -0,0 +1,4 @@
1
+ {
2
+ "title": "Navigation Menu",
3
+ "description": "Site navigation with flyout content, keyboard arrows between triggers, Escape to close and focus return."
4
+ }