@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.
Files changed (190) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +598 -2
  4. package/dist/cli.mjs +2281 -0
  5. package/dist/dev/cli/schema.d.ts +71 -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 +139 -0
  9. package/dist/dev/locales/ar.js +127 -0
  10. package/dist/dev/locales/ar.js.map +1 -0
  11. package/dist/dev/locales/en.d.ts +139 -0
  12. package/dist/dev/locales/en.js +127 -0
  13. package/dist/dev/locales/en.js.map +1 -0
  14. package/dist/dev/registry/utils/utils.d.ts +27 -0
  15. package/dist/dev/registry/utils/utils.js +26 -0
  16. package/dist/dev/registry/utils/utils.js.map +1 -0
  17. package/dist/dev/theme/theme-logic.d.ts +185 -0
  18. package/dist/dev/theme/theme-logic.js +183 -0
  19. package/dist/dev/theme/theme-logic.js.map +1 -0
  20. package/dist/dev/theme/theme-provider.d.ts +88 -0
  21. package/dist/dev/theme/theme-provider.js +149 -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,459 @@
1
+ import { splitProps } from "@shadow-js/core";
2
+ import { Polymorphic, type PolymorphicProps, type PolymorphicTarget } from "@shadow-js/primitives";
3
+ import type { ComponentProps, JSX } from "@shadow-js/web";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import { Button, type ButtonProps } from "@/components/ui/button";
6
+ import { type ClassValue, cn } from "@/lib/utils";
7
+
8
+ /**
9
+ * The class list of an attachment card for each `size` and `orientation`. Use it to style any element like an attachment.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * import { attachmentVariants } from "@/components/ui/attachment";
14
+ *
15
+ * export const Chip = <a class={attachmentVariants({ size: "sm" })} href="/files/report.pdf">report.pdf</a>;
16
+ * ```
17
+ * @since 1.0.0
18
+ * @see {@link Attachment}
19
+ */
20
+ const attachmentVariants = cva(
21
+ "group/attachment relative flex min-w-0 items-center rounded-lg border bg-card text-card-foreground transition-colors has-[[data-slot=attachment-trigger]:hover]:bg-accent/50 motion-reduce:transition-none data-[state=error]:border-destructive/50 data-[state=error]:bg-destructive/5",
22
+ {
23
+ variants: {
24
+ size: {
25
+ default: "gap-3 p-2.5 text-sm",
26
+ sm: "gap-2.5 p-2 text-sm",
27
+ xs: "gap-2 p-1.5 text-xs",
28
+ },
29
+ orientation: {
30
+ horizontal: "flex-row",
31
+ vertical: "flex-col items-stretch",
32
+ },
33
+ },
34
+ defaultVariants: { size: "default", orientation: "horizontal" },
35
+ },
36
+ );
37
+
38
+ /**
39
+ * The states an attachment can be in: `idle`, `uploading`, `processing`, `error` or `done`.
40
+ * @since 1.0.0
41
+ * @see {@link Attachment}
42
+ */
43
+ export type AttachmentState = "idle" | "uploading" | "processing" | "error" | "done";
44
+
45
+ /**
46
+ * Props of {@link Attachment}: the attributes of a `div` plus `state` (`"idle"`, `"uploading"`,
47
+ * `"processing"`, `"error"` or `"done"`, the default), `size` (`"default"`, `"sm"` or `"xs"`) and
48
+ * `orientation` (`"horizontal"`, the default, or `"vertical"`).
49
+ * @since 1.0.0
50
+ * @see {@link Attachment}
51
+ */
52
+ export type AttachmentProps = ComponentProps<"div"> &
53
+ VariantProps<typeof attachmentVariants> & { readonly state?: AttachmentState | undefined };
54
+
55
+ /**
56
+ * A file or an image in a message, a composer or an upload list: its media, its name and details, its
57
+ * upload state and its actions. `state` drives the look: `uploading` and `processing` shimmer the title, and
58
+ * `error` colors the border. Put the media, the {@link AttachmentContent} and the {@link AttachmentActions} inside; add an
59
+ * {@link AttachmentTrigger} to make the whole card open something while the actions stay pressable.
60
+ *
61
+ * Keyboard and ARIA: the card is a plain `div` with no role. Its trigger and actions are the focusable
62
+ * controls. Give an icon-only action an `aria-label`, and the trigger one too ("Preview report.pdf"). Say why a
63
+ * file failed in the description: do not rely on color alone for `error`.
64
+ *
65
+ * Compared with shadcn/ui: `as` replaces `render` on the trigger, `class` replaces `className`, and every part has a `data-slot`.
66
+ *
67
+ * @param props - The attachment props: `state`, `size`, `orientation` and the `div` attributes.
68
+ * @returns The attachment.
69
+ * @example
70
+ * ```tsx
71
+ * import {
72
+ * Attachment,
73
+ * AttachmentContent,
74
+ * AttachmentDescription,
75
+ * AttachmentMedia,
76
+ * AttachmentTitle,
77
+ * } from "@/components/ui/attachment";
78
+ * import { FileTextIcon } from "@/components/ui/icons";
79
+ *
80
+ * export function Report() {
81
+ * return (
82
+ * <Attachment>
83
+ * <AttachmentMedia>
84
+ * <FileTextIcon />
85
+ * </AttachmentMedia>
86
+ * <AttachmentContent>
87
+ * <AttachmentTitle>report.pdf</AttachmentTitle>
88
+ * <AttachmentDescription>PDF, 2.4 MB</AttachmentDescription>
89
+ * </AttachmentContent>
90
+ * </Attachment>
91
+ * );
92
+ * }
93
+ * ```
94
+ * @since 1.0.0
95
+ * @see {@link AttachmentMedia}
96
+ * @see {@link AttachmentContent}
97
+ * @see {@link AttachmentGroup}
98
+ */
99
+ function Attachment(props: AttachmentProps): JSX.Element {
100
+ const [local, rest] = splitProps(props, ["class", "state", "size", "orientation"]);
101
+ return (
102
+ <div
103
+ data-slot="attachment"
104
+ data-state={local.state ?? "done"}
105
+ data-size={local.size ?? "default"}
106
+ data-orientation={local.orientation ?? "horizontal"}
107
+ class={cn(
108
+ attachmentVariants({ size: local.size, orientation: local.orientation }),
109
+ local.class,
110
+ )}
111
+ {...rest}
112
+ />
113
+ );
114
+ }
115
+
116
+ const mediaVariants = cva(
117
+ "relative flex shrink-0 items-center justify-center overflow-hidden rounded-md bg-muted text-muted-foreground group-data-[state=error]/attachment:text-destructive [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-5",
118
+ {
119
+ variants: {
120
+ variant: {
121
+ icon: "size-10 group-data-[size=sm]/attachment:size-9 group-data-[size=xs]/attachment:size-7 group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-4",
122
+ image:
123
+ "size-10 group-data-[size=sm]/attachment:size-9 group-data-[size=xs]/attachment:size-7 group-data-[orientation=vertical]/attachment:aspect-video group-data-[orientation=vertical]/attachment:h-auto group-data-[orientation=vertical]/attachment:w-full [&_img]:size-full [&_img]:object-cover",
124
+ },
125
+ },
126
+ defaultVariants: { variant: "icon" },
127
+ },
128
+ );
129
+
130
+ /**
131
+ * Props of {@link AttachmentMedia}: the attributes of a `div` plus `variant`, which is `"icon"` (the
132
+ * default, an icon on a tile) or `"image"` (a preview picture that fills it).
133
+ * @since 1.0.0
134
+ * @see {@link AttachmentMedia}
135
+ */
136
+ export type AttachmentMediaProps = ComponentProps<"div"> & VariantProps<typeof mediaVariants>;
137
+
138
+ /**
139
+ * The media slot: an icon on a tile, or an `img` preview. In a vertical attachment an image fills the top
140
+ * of the card. An image needs its own text alternative (`alt=""` when the title already says it).
141
+ *
142
+ * Keyboard and ARIA: a plain `div`, no role. An icon in it is decorative.
143
+ *
144
+ * @param props - The media props: `variant` and the `div` attributes.
145
+ * @returns The media slot.
146
+ * @example
147
+ * ```tsx
148
+ * import { Attachment, AttachmentMedia } from "@/components/ui/attachment";
149
+ *
150
+ * export function Photo() {
151
+ * return (
152
+ * <Attachment orientation="vertical">
153
+ * <AttachmentMedia variant="image">
154
+ * <img src="/photo.jpg" alt="" />
155
+ * </AttachmentMedia>
156
+ * </Attachment>
157
+ * );
158
+ * }
159
+ * ```
160
+ * @since 1.0.0
161
+ * @see {@link Attachment}
162
+ */
163
+ function AttachmentMedia(props: AttachmentMediaProps): JSX.Element {
164
+ const [local, rest] = splitProps(props, ["class", "variant"]);
165
+ return (
166
+ <div
167
+ data-slot="attachment-media"
168
+ data-variant={local.variant ?? "icon"}
169
+ class={cn(mediaVariants({ variant: local.variant }), local.class)}
170
+ {...rest}
171
+ />
172
+ );
173
+ }
174
+
175
+ /**
176
+ * Wraps the title and the description. It takes the space between the media and the actions and truncates its text.
177
+ *
178
+ * Keyboard and ARIA: a plain `div`; no role and no keys.
179
+ *
180
+ * @param props - The `div` attributes and `class`.
181
+ * @returns The content slot.
182
+ * @example
183
+ * ```tsx
184
+ * import { AttachmentContent, AttachmentTitle } from "@/components/ui/attachment";
185
+ *
186
+ * export const Name = (
187
+ * <AttachmentContent>
188
+ * <AttachmentTitle>report.pdf</AttachmentTitle>
189
+ * </AttachmentContent>
190
+ * );
191
+ * ```
192
+ * @since 1.0.0
193
+ * @see {@link Attachment}
194
+ */
195
+ function AttachmentContent(props: ComponentProps<"div">): JSX.Element {
196
+ const [local, rest] = splitProps(props, ["class"]);
197
+ return (
198
+ <div
199
+ data-slot="attachment-content"
200
+ class={cn("flex min-w-0 flex-1 flex-col gap-0.5", local.class)}
201
+ {...rest}
202
+ />
203
+ );
204
+ }
205
+
206
+ /**
207
+ * The name of the attachment. It truncates, and it shimmers while the attachment is `uploading` or `processing`.
208
+ *
209
+ * Keyboard and ARIA: a plain `div`; no role and no keys. While it shimmers its text stays real text, so a screen reader reads it.
210
+ *
211
+ * @param props - The `div` attributes and `class`.
212
+ * @returns The title.
213
+ * @example
214
+ * ```tsx
215
+ * import { AttachmentTitle } from "@/components/ui/attachment";
216
+ *
217
+ * export const Title = <AttachmentTitle>report.pdf</AttachmentTitle>;
218
+ * ```
219
+ * @since 1.0.0
220
+ * @see {@link Attachment}
221
+ */
222
+ function AttachmentTitle(props: ComponentProps<"div">): JSX.Element {
223
+ const [local, rest] = splitProps(props, ["class"]);
224
+ return (
225
+ <div
226
+ data-slot="attachment-title"
227
+ class={cn(
228
+ "truncate font-medium group-data-[state=error]/attachment:text-destructive group-data-[state=processing]/attachment:shimmer group-data-[state=uploading]/attachment:shimmer",
229
+ local.class,
230
+ )}
231
+ {...rest}
232
+ />
233
+ );
234
+ }
235
+
236
+ /**
237
+ * Secondary details: the type and size of a file, or the upload status ("Uploading, 40%"). For an
238
+ * `error` state, say what went wrong here.
239
+ *
240
+ * Keyboard and ARIA: a plain `div`; no role and no keys. For an upload that a screen reader user should follow,
241
+ * put the status in a `role="status"` element.
242
+ *
243
+ * @param props - The `div` attributes and `class`.
244
+ * @returns The description.
245
+ * @example
246
+ * ```tsx
247
+ * import { AttachmentDescription } from "@/components/ui/attachment";
248
+ *
249
+ * export const Details = <AttachmentDescription>PDF, 2.4 MB</AttachmentDescription>;
250
+ * ```
251
+ * @since 1.0.0
252
+ * @see {@link Attachment}
253
+ */
254
+ function AttachmentDescription(props: ComponentProps<"div">): JSX.Element {
255
+ const [local, rest] = splitProps(props, ["class"]);
256
+ return (
257
+ <div
258
+ data-slot="attachment-description"
259
+ class={cn(
260
+ "truncate text-xs text-muted-foreground group-data-[state=error]/attachment:text-destructive",
261
+ local.class,
262
+ )}
263
+ {...rest}
264
+ />
265
+ );
266
+ }
267
+
268
+ /**
269
+ * Holds the actions, at the end of the attachment. It sits above the {@link AttachmentTrigger}, so the actions stay pressable.
270
+ *
271
+ * Keyboard and ARIA: a plain `div`; the actions inside are the focus stops.
272
+ *
273
+ * @param props - The `div` attributes and `class`.
274
+ * @returns The actions container.
275
+ * @example
276
+ * ```tsx
277
+ * import { AttachmentAction, AttachmentActions } from "@/components/ui/attachment";
278
+ * import { XIcon } from "@/components/ui/icons";
279
+ *
280
+ * export const Remove = (
281
+ * <AttachmentActions>
282
+ * <AttachmentAction aria-label="Remove report.pdf">
283
+ * <XIcon />
284
+ * </AttachmentAction>
285
+ * </AttachmentActions>
286
+ * );
287
+ * ```
288
+ * @since 1.0.0
289
+ * @see {@link AttachmentAction}
290
+ */
291
+ function AttachmentActions(props: ComponentProps<"div">): JSX.Element {
292
+ const [local, rest] = splitProps(props, ["class"]);
293
+ return (
294
+ <div
295
+ data-slot="attachment-actions"
296
+ class={cn("relative z-10 ms-auto flex shrink-0 items-center gap-1", local.class)}
297
+ {...rest}
298
+ />
299
+ );
300
+ }
301
+
302
+ /**
303
+ * Props of {@link AttachmentAction}: every prop of {@link Button}. The `size` is `"icon-xs"` unless given
304
+ * and the `variant` is `"ghost"`.
305
+ * @since 1.0.0
306
+ * @see {@link AttachmentAction}
307
+ */
308
+ export type AttachmentActionProps<T extends PolymorphicTarget = "button"> = ButtonProps<T>;
309
+
310
+ /**
311
+ * An action on an attachment: a {@link Button}, small and quiet by default. An icon-only action needs an `aria-label` that names the file ("Remove report.pdf").
312
+ *
313
+ * Keyboard and ARIA: a native button; Enter and Space press it.
314
+ *
315
+ * @param props - The button props.
316
+ * @returns The action button.
317
+ * @example
318
+ * ```tsx
319
+ * import { AttachmentAction } from "@/components/ui/attachment";
320
+ * import { XIcon } from "@/components/ui/icons";
321
+ *
322
+ * export const Remove = (
323
+ * <AttachmentAction aria-label="Remove report.pdf">
324
+ * <XIcon />
325
+ * </AttachmentAction>
326
+ * );
327
+ * ```
328
+ * @since 1.0.0
329
+ * @see {@link Button}
330
+ */
331
+ function AttachmentAction<T extends PolymorphicTarget = "button">(
332
+ props: AttachmentActionProps<T>,
333
+ ): JSX.Element {
334
+ const [local, rest] = splitProps(
335
+ props as unknown as {
336
+ readonly class?: ClassValue;
337
+ readonly size?: string;
338
+ readonly variant?: string;
339
+ readonly [prop: string]: unknown;
340
+ },
341
+ ["class", "size", "variant"],
342
+ );
343
+ return (
344
+ <Button
345
+ data-slot="attachment-action"
346
+ variant={(local.variant as ButtonProps["variant"]) ?? "ghost"}
347
+ size={(local.size as ButtonProps["size"]) ?? "icon-xs"}
348
+ class={cn(local.class)}
349
+ {...(rest as object)}
350
+ />
351
+ );
352
+ }
353
+
354
+ /** The props as the implementation sees them. */
355
+ type Loose = {
356
+ readonly as?: PolymorphicTarget;
357
+ readonly class?: ClassValue;
358
+ readonly [prop: string]: unknown;
359
+ };
360
+
361
+ /**
362
+ * Props of {@link AttachmentTrigger}: every attribute of the chosen element. The `as` prop picks the element (a `button` by default; use `"a"` for a link).
363
+ * @since 1.0.0
364
+ * @see {@link AttachmentTrigger}
365
+ */
366
+ export type AttachmentTriggerProps<T extends PolymorphicTarget = "button"> = PolymorphicProps<
367
+ "button",
368
+ object,
369
+ T
370
+ >;
371
+
372
+ /**
373
+ * A full-card overlay that activates the attachment: a link to the file, or the trigger of a dialog that previews it. It sits behind the
374
+ * {@link AttachmentActions}, so both the card and the actions can be focused and pressed. It needs an `aria-label` that names the file.
375
+ *
376
+ * Keyboard and ARIA: a native `button` by default (Enter and Space), or a link with `as="a"` (Enter). It is a tab stop before the actions.
377
+ *
378
+ * @param props - The trigger props: `as` and the attributes of the element.
379
+ * @returns The trigger.
380
+ * @example
381
+ * ```tsx
382
+ * import { Attachment, AttachmentTrigger } from "@/components/ui/attachment";
383
+ *
384
+ * export const Open = (
385
+ * <Attachment>
386
+ * <AttachmentTrigger as="a" href="/report.pdf" aria-label="Open report.pdf" />
387
+ * </Attachment>
388
+ * );
389
+ * ```
390
+ * @since 1.0.0
391
+ * @see {@link Attachment}
392
+ */
393
+ function AttachmentTrigger<T extends PolymorphicTarget = "button">(
394
+ props: AttachmentTriggerProps<T>,
395
+ ): JSX.Element {
396
+ const [local, rest] = splitProps(props as unknown as Loose, ["as", "class"]);
397
+ return (
398
+ <Polymorphic
399
+ as={local.as ?? "button"}
400
+ data-slot="attachment-trigger"
401
+ {...(local.as === undefined || local.as === "button" ? { type: "button" } : {})}
402
+ class={cn(
403
+ "absolute inset-0 z-0 rounded-[inherit] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-transparent",
404
+ local.class,
405
+ )}
406
+ {...rest}
407
+ />
408
+ );
409
+ }
410
+
411
+ /**
412
+ * Lays out attachments in a horizontal row that scrolls and snaps, with a fade at the edges that have more.
413
+ * A group of attachments that are not interactive can still be scrolled by keyboard: give it
414
+ * `tabindex="0"`, `role="group"` and an `aria-label`.
415
+ *
416
+ * Keyboard and ARIA: a plain `div`; the scrolling is native.
417
+ *
418
+ * @param props - The `div` attributes and `class`.
419
+ * @returns The group.
420
+ * @example
421
+ * ```tsx
422
+ * import { Attachment, AttachmentGroup } from "@/components/ui/attachment";
423
+ *
424
+ * export const Files = (
425
+ * <AttachmentGroup role="group" aria-label="Attachments" tabindex="0">
426
+ * <Attachment class="w-56 shrink-0 snap-start" />
427
+ * <Attachment class="w-56 shrink-0 snap-start" />
428
+ * </AttachmentGroup>
429
+ * );
430
+ * ```
431
+ * @since 1.0.0
432
+ * @see {@link Attachment}
433
+ */
434
+ function AttachmentGroup(props: ComponentProps<"div">): JSX.Element {
435
+ const [local, rest] = splitProps(props, ["class"]);
436
+ return (
437
+ <div
438
+ data-slot="attachment-group"
439
+ class={cn(
440
+ "scroll-fade-x flex w-full snap-x snap-mandatory gap-2 overflow-x-auto pb-1 outline-none [scrollbar-width:thin] focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-transparent",
441
+ local.class,
442
+ )}
443
+ {...rest}
444
+ />
445
+ );
446
+ }
447
+
448
+ export {
449
+ Attachment,
450
+ AttachmentAction,
451
+ AttachmentActions,
452
+ AttachmentContent,
453
+ AttachmentDescription,
454
+ AttachmentGroup,
455
+ AttachmentMedia,
456
+ AttachmentTitle,
457
+ AttachmentTrigger,
458
+ attachmentVariants,
459
+ };
@@ -0,0 +1,4 @@
1
+ {
2
+ "title": "Attachment",
3
+ "description": "A file or an image with its media, name, details, upload state and actions, for composers, threads and upload lists."
4
+ }