@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,385 @@
1
+ import {
2
+ type Accessor,
3
+ createContext,
4
+ onCleanup,
5
+ splitProps,
6
+ untrack,
7
+ useContext,
8
+ useEffect,
9
+ useSignal,
10
+ } from "@shadow-js/core";
11
+ import { mergeElementProps } from "@shadow-js/primitives";
12
+ import { type ComponentProps, type JSX, Show } from "@shadow-js/web";
13
+ import { cn } from "@/lib/utils";
14
+
15
+ /**
16
+ * Where the image of an avatar is: `idle` (no image rendered yet), `loading`, `loaded` or `error`
17
+ * (the image failed or has no `src`).
18
+ * @since 1.0.0
19
+ * @see {@link AvatarImage}
20
+ */
21
+ export type AvatarLoadingStatus = "idle" | "loading" | "loaded" | "error";
22
+
23
+ /** What the image tells the root so the fallback knows when to show. */
24
+ interface AvatarContextValue {
25
+ readonly status: Accessor<AvatarLoadingStatus>;
26
+ readonly setStatus: (status: AvatarLoadingStatus) => void;
27
+ }
28
+
29
+ const avatarContext = createContext<AvatarContextValue>(undefined, { name: "Avatar" });
30
+
31
+ /**
32
+ * Props of {@link Avatar}, including the shared size of its image, fallback and badge.
33
+ * @example
34
+ * import type { AvatarProps } from "@/components/ui/avatar";
35
+ * const props: AvatarProps = { size: "sm" };
36
+ * @since 1.0.0
37
+ */
38
+ export type AvatarProps = ComponentProps<"span"> & {
39
+ /** The avatar diameter: default 2rem, sm 1.5rem, lg 2.5rem. @default "default" */
40
+ readonly size?: "default" | "sm" | "lg" | undefined;
41
+ };
42
+
43
+ /**
44
+ * A user's picture with a fallback. Put an {@link AvatarImage} and an {@link AvatarFallback}
45
+ * inside; the fallback shows until the image has loaded, and for good when the image fails or has
46
+ * no `src`.
47
+ *
48
+ * Keyboard and ARIA: the avatar is not interactive, so it has no keys and no role. The picture's
49
+ * accessible name is the `alt` of the {@link AvatarImage}. Give it the person's name. When the name
50
+ * is already written next to the avatar, pass `alt=""` to hide the picture from assistive
51
+ * technology.
52
+ *
53
+ * Compared with shadcn/ui: `class` replaces `className`, and `delayMs` and `onLoadingStatusChange`
54
+ * are ShadowJS props.
55
+ *
56
+ * @param props - The root props: `children` and the `span` attributes.
57
+ * @returns The avatar root element.
58
+ * @example
59
+ * ```tsx
60
+ * import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
61
+ *
62
+ * export function UserAvatar() {
63
+ * return (
64
+ * <Avatar>
65
+ * <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
66
+ * <AvatarFallback>CN</AvatarFallback>
67
+ * </Avatar>
68
+ * );
69
+ * }
70
+ * ```
71
+ * @since 1.0.0
72
+ * @see {@link AvatarImage}
73
+ * @see {@link AvatarFallback}
74
+ */
75
+ function Avatar(props: AvatarProps): JSX.Element {
76
+ const [local, rest] = splitProps(props, ["class", "children", "size"]);
77
+ const [status, setStatus] = useSignal<AvatarLoadingStatus>("idle");
78
+ return (
79
+ <avatarContext.Provider value={{ status, setStatus: (next) => setStatus(next) }}>
80
+ <span
81
+ data-slot="avatar"
82
+ data-size={local.size ?? "default"}
83
+ class={cn(
84
+ "group/avatar relative flex shrink-0 rounded-full",
85
+ local.size === "sm" ? "size-6" : local.size === "lg" ? "size-10" : "size-8",
86
+ local.class,
87
+ )}
88
+ {...rest}
89
+ >
90
+ {local.children}
91
+ </span>
92
+ </avatarContext.Provider>
93
+ );
94
+ }
95
+
96
+ /**
97
+ * Own props of {@link AvatarImage}.
98
+ * @since 1.0.0
99
+ * @see {@link AvatarImage}
100
+ */
101
+ export interface AvatarImageExtraProps {
102
+ /**
103
+ * Called whenever the loading status changes (client only).
104
+ * @default none
105
+ */
106
+ readonly onLoadingStatusChange?: ((status: AvatarLoadingStatus) => void) | undefined;
107
+ }
108
+
109
+ /**
110
+ * Props of {@link AvatarImage}: the attributes of an `img` plus `onLoadingStatusChange`.
111
+ * @since 1.0.0
112
+ * @see {@link AvatarImage}
113
+ */
114
+ export type AvatarImageProps = ComponentProps<"img"> & AvatarImageExtraProps;
115
+
116
+ /** The outcome of loading one `src`. */
117
+ interface Outcome {
118
+ readonly src: string;
119
+ readonly loaded: boolean;
120
+ }
121
+
122
+ /**
123
+ * The image of an avatar. It is in the markup from the start, so the browser loads it and the
124
+ * server renders it, but it stays hidden until it has loaded. An image that finished loading before
125
+ * the page hydrated counts as loaded. Give it an `alt` with the person's name.
126
+ *
127
+ * Keyboard and ARIA: the image has the `img` role. Its accessible name is the `alt` text, and it
128
+ * has no keys of its own. The `onLoad`, `onError` and `ref` you pass still run.
129
+ *
130
+ * @param props - The image props: `src`, `alt`, `onLoadingStatusChange`, and the `img` attributes.
131
+ * @returns The image element.
132
+ * @example
133
+ * ```tsx
134
+ * import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
135
+ *
136
+ * export function Me() {
137
+ * return (
138
+ * <Avatar>
139
+ * <AvatarImage src="/me.png" alt="Ada Lovelace" />
140
+ * <AvatarFallback>AL</AvatarFallback>
141
+ * </Avatar>
142
+ * );
143
+ * }
144
+ * ```
145
+ * @since 1.0.0
146
+ * @see {@link AvatarFallback}
147
+ */
148
+ function AvatarImage(props: AvatarImageProps): JSX.Element {
149
+ const root = useContext(avatarContext);
150
+ const [local, rest] = splitProps(props, ["class", "src", "onLoadingStatusChange"]);
151
+ const [outcome, setOutcome] = useSignal<Outcome | undefined>(undefined);
152
+ const status = (): AvatarLoadingStatus => {
153
+ const src = local.src;
154
+ if (typeof src !== "string" || src === "") return "error";
155
+ const result = outcome();
156
+ if (result === undefined || result.src !== src) return "loading";
157
+ return result.loaded ? "loaded" : "error";
158
+ };
159
+ useEffect(() => {
160
+ const next = status();
161
+ root.setStatus(next);
162
+ untrack(() => local.onLoadingStatusChange?.(next));
163
+ });
164
+ onCleanup(() => root.setStatus("idle"));
165
+ const settle = (loaded: boolean): void => {
166
+ const src = untrack(() => local.src);
167
+ if (typeof src === "string" && src !== "") setOutcome(() => ({ src, loaded }));
168
+ };
169
+ // The caller's `onLoad`, `onError` and `ref` run too: the handlers are composed, not replaced.
170
+ const merged = mergeElementProps(
171
+ {
172
+ onLoad: () => settle(true),
173
+ onError: () => settle(false),
174
+ ref: (node: HTMLImageElement) => {
175
+ // Cached images, and images that loaded before hydration, fire their `load` event too
176
+ // early to be heard. A broken one keeps the fallback anyway: it stays hidden.
177
+ if (node.complete && node.naturalWidth > 0) settle(true);
178
+ },
179
+ },
180
+ rest,
181
+ );
182
+ return (
183
+ <img
184
+ data-slot="avatar-image"
185
+ src={local.src}
186
+ hidden={status() !== "loaded"}
187
+ class={cn("aspect-square size-full rounded-full", local.class)}
188
+ {...merged}
189
+ />
190
+ );
191
+ }
192
+
193
+ /**
194
+ * Own props of {@link AvatarFallback}.
195
+ * @since 1.0.0
196
+ * @see {@link AvatarFallback}
197
+ */
198
+ export interface AvatarFallbackExtraProps {
199
+ /**
200
+ * Wait this many milliseconds before showing the fallback, so a fast image does not flash it.
201
+ * With a delay the fallback is not part of the server markup.
202
+ * @default none (the fallback shows at once)
203
+ */
204
+ readonly delayMs?: number | undefined;
205
+ }
206
+
207
+ /**
208
+ * Props of {@link AvatarFallback}: the attributes of a `span` plus `delayMs`.
209
+ * @since 1.0.0
210
+ * @see {@link AvatarFallback}
211
+ */
212
+ export type AvatarFallbackProps = ComponentProps<"span"> & AvatarFallbackExtraProps;
213
+
214
+ /**
215
+ * What shows while the image loads or when it fails: initials or an icon. It is hidden once the
216
+ * image has loaded. With `delayMs` it shows only after that many milliseconds, so a fast image never
217
+ * flashes it.
218
+ *
219
+ * Keyboard and ARIA: the fallback is a plain `span` with no role and no keys. It adds no ARIA
220
+ * attributes, so put text in it (initials) that reads well on its own.
221
+ *
222
+ * @param props - The fallback props: `delayMs`, `children`, and the `span` attributes.
223
+ * @returns The fallback element, or nothing while the image is showing.
224
+ * @example
225
+ * ```tsx
226
+ * import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
227
+ *
228
+ * export function Initials() {
229
+ * return (
230
+ * <Avatar>
231
+ * <AvatarImage src="/me.png" alt="Ada Lovelace" />
232
+ * <AvatarFallback delayMs={600}>AL</AvatarFallback>
233
+ * </Avatar>
234
+ * );
235
+ * }
236
+ * ```
237
+ * @since 1.0.0
238
+ * @see {@link AvatarImage}
239
+ */
240
+ function AvatarFallback(props: AvatarFallbackProps): JSX.Element {
241
+ const root = useContext(avatarContext);
242
+ const [local, rest] = splitProps(props, ["class", "delayMs", "children"]);
243
+ const [elapsed, setElapsed] = useSignal(false);
244
+ useEffect(() => {
245
+ const delay = local.delayMs;
246
+ if (delay === undefined) return;
247
+ const timer = setTimeout(() => setElapsed(true), delay);
248
+ onCleanup(() => clearTimeout(timer));
249
+ });
250
+ const visible = (): boolean =>
251
+ root.status() !== "loaded" && (local.delayMs === undefined || elapsed());
252
+ return (
253
+ <Show when={visible()}>
254
+ <span
255
+ data-slot="avatar-fallback"
256
+ class={cn(
257
+ "flex size-full items-center justify-center rounded-full bg-muted forced-colors:border",
258
+ local.class,
259
+ )}
260
+ {...rest}
261
+ >
262
+ {local.children}
263
+ </span>
264
+ </Show>
265
+ );
266
+ }
267
+
268
+ /**
269
+ * Props of {@link AvatarBadge}.
270
+ * @example
271
+ * import type { AvatarBadgeProps } from "@/components/ui/avatar";
272
+ * const props: AvatarBadgeProps = { "aria-label": "Available" };
273
+ * @since 1.0.0
274
+ */
275
+ export type AvatarBadgeProps = ComponentProps<"span">;
276
+
277
+ /**
278
+ * A status badge on the avatar's logical bottom end, sized with its parent avatar.
279
+ * Give meaningful status an accessible label; decorative content may use `aria-hidden`.
280
+ * @param props Span attributes and badge content.
281
+ * @returns The badge span. It has no keyboard interaction.
282
+ * @example
283
+ * import { Avatar, AvatarBadge, AvatarFallback } from "@/components/ui/avatar";
284
+ * <Avatar><AvatarFallback>AL</AvatarFallback><AvatarBadge aria-label="Available" /></Avatar>
285
+ * @see Avatar
286
+ * @since 1.0.0
287
+ */
288
+ function AvatarBadge(props: AvatarBadgeProps): JSX.Element {
289
+ const [local, rest] = splitProps(props, ["class", "children"]);
290
+ return (
291
+ <span
292
+ data-slot="avatar-badge"
293
+ role={
294
+ props["aria-label"] !== undefined || props["aria-labelledby"] !== undefined
295
+ ? "img"
296
+ : undefined
297
+ }
298
+ class={cn(
299
+ "absolute end-0 bottom-0 flex size-2.5 items-center justify-center rounded-full bg-primary text-primary-foreground ring-2 ring-background group-data-[size=sm]/avatar:size-2 group-data-[size=lg]/avatar:size-3 forced-colors:border",
300
+ local.class,
301
+ )}
302
+ {...rest}
303
+ >
304
+ {local.children}
305
+ </span>
306
+ );
307
+ }
308
+
309
+ /**
310
+ * Props of {@link AvatarGroup}.
311
+ * @example
312
+ * import type { AvatarGroupProps } from "@/components/ui/avatar";
313
+ * const props: AvatarGroupProps = { "aria-label": "Reviewers", dir: "rtl" };
314
+ * @since 1.0.0
315
+ */
316
+ export type AvatarGroupProps = ComponentProps<"div">;
317
+
318
+ /**
319
+ * Groups overlapping avatars in reading order using the logical start edge in LTR and RTL.
320
+ * @param props Div attributes, optional accessible name and avatar children.
321
+ * @returns The div with the group role. It has no keyboard interaction.
322
+ * @example
323
+ * import { Avatar, AvatarFallback, AvatarGroup, AvatarGroupCount } from "@/components/ui/avatar";
324
+ * <AvatarGroup><Avatar><AvatarFallback>AL</AvatarFallback></Avatar><AvatarGroupCount>+3</AvatarGroupCount></AvatarGroup>
325
+ * @see AvatarGroupCount
326
+ * @since 1.0.0
327
+ */
328
+ function AvatarGroup(props: AvatarGroupProps): JSX.Element {
329
+ const [local, rest] = splitProps(props, ["class", "children"]);
330
+ return (
331
+ <div
332
+ data-slot="avatar-group"
333
+ role="group"
334
+ class={cn(
335
+ "group/avatar-group flex items-center [&>*+*]:-ms-2 [&>[data-slot=avatar]]:ring-2 [&>[data-slot=avatar]]:ring-background",
336
+ local.class,
337
+ )}
338
+ {...rest}
339
+ >
340
+ {local.children}
341
+ </div>
342
+ );
343
+ }
344
+
345
+ /**
346
+ * Props of {@link AvatarGroupCount}.
347
+ * @example
348
+ * import type { AvatarGroupCountProps } from "@/components/ui/avatar";
349
+ * const props: AvatarGroupCountProps = { "aria-label": "Three more reviewers" };
350
+ * @since 1.0.0
351
+ */
352
+ export type AvatarGroupCountProps = ComponentProps<"span">;
353
+
354
+ /**
355
+ * Displays the number of additional people in an avatar group; content can use localized digits.
356
+ * @param props Span attributes and count content. Use `aria-label` to explain the count.
357
+ * @returns The count span, sized with the group's avatars. It has no keyboard interaction.
358
+ * @example
359
+ * import { AvatarGroupCount } from "@/components/ui/avatar";
360
+ * <AvatarGroupCount aria-label="Three more reviewers">+3</AvatarGroupCount>
361
+ * @see AvatarGroup
362
+ * @since 1.0.0
363
+ */
364
+ function AvatarGroupCount(props: AvatarGroupCountProps): JSX.Element {
365
+ const [local, rest] = splitProps(props, ["class", "children"]);
366
+ return (
367
+ <span
368
+ data-slot="avatar-group-count"
369
+ role={
370
+ props["aria-label"] !== undefined || props["aria-labelledby"] !== undefined
371
+ ? "img"
372
+ : undefined
373
+ }
374
+ class={cn(
375
+ "flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-[[data-size=sm]]/avatar-group:size-6 group-has-[[data-size=sm]]/avatar-group:text-xs group-has-[[data-size=lg]]/avatar-group:size-10 forced-colors:border",
376
+ local.class,
377
+ )}
378
+ {...rest}
379
+ >
380
+ {local.children}
381
+ </span>
382
+ );
383
+ }
384
+
385
+ export { Avatar, AvatarBadge, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage };
@@ -0,0 +1,4 @@
1
+ {
2
+ "title": "Avatar",
3
+ "description": "User images with fallbacks, size variants, status badges and overlapping groups."
4
+ }
@@ -0,0 +1,108 @@
1
+ import { splitProps } from "@shadow-js/core";
2
+ import { Polymorphic, type PolymorphicProps, type PolymorphicTarget } from "@shadow-js/primitives";
3
+ import type { JSX } from "@shadow-js/web";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import { type ClassValue, cn } from "@/lib/utils";
6
+
7
+ /**
8
+ * The class list of a badge for each `variant`. Use it to style any element like a badge.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * import { badgeVariants } from "@/components/ui/badge";
13
+ *
14
+ * export function NewLink() {
15
+ * return (
16
+ * <a class={badgeVariants({ variant: "outline" })} href="/new">
17
+ * New
18
+ * </a>
19
+ * );
20
+ * }
21
+ * ```
22
+ * @since 1.0.0
23
+ * @see {@link Badge}
24
+ */
25
+ const badgeVariants = cva(
26
+ "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-md border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-reduce:transition-none aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3 outline-none focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-transparent",
27
+ {
28
+ variants: {
29
+ variant: {
30
+ default: "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
31
+ secondary:
32
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
33
+ destructive:
34
+ "border-transparent bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
35
+ outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
36
+ ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
37
+ link: "text-primary underline-offset-4 [a&]:hover:underline",
38
+ },
39
+ },
40
+ defaultVariants: {
41
+ variant: "default",
42
+ },
43
+ },
44
+ );
45
+
46
+ /** The props as the implementation sees them: the typed own props plus whatever the target takes. */
47
+ type Loose = VariantProps<typeof badgeVariants> & {
48
+ readonly as?: PolymorphicTarget;
49
+ readonly class?: ClassValue;
50
+ readonly [prop: string]: unknown;
51
+ };
52
+
53
+ /**
54
+ * Props of {@link Badge}: every attribute of the chosen element plus `variant`. The `variant` is
55
+ * one of `"default"` (the default), `"secondary"`, `"destructive"` or `"outline"`. The `as` prop
56
+ * picks the element (a `span` by default).
57
+ * @since 1.0.0
58
+ * @see {@link Badge}
59
+ */
60
+ export type BadgeProps<T extends PolymorphicTarget = "span"> = PolymorphicProps<
61
+ "span",
62
+ VariantProps<typeof badgeVariants>,
63
+ T
64
+ >;
65
+
66
+ /**
67
+ * A small label for a status, count or category. It renders a `span` by default. Render a link
68
+ * with `as`, for example `<Badge as="a" href="/new">New</Badge>`; links get a hover style.
69
+ *
70
+ * Keyboard and ARIA: a badge has no role and no keys. As a link it is focusable and followed with
71
+ * the native link keys, and it shows a focus ring. Its text is the only label, so keep the text
72
+ * meaningful on its own.
73
+ *
74
+ * Compared with shadcn/ui: `as` replaces `asChild`, `class` replaces `className`, and the element
75
+ * carries `data-slot="badge"`.
76
+ *
77
+ * @param props - The badge props: `variant`, `as`, `children`, and the attributes of the element.
78
+ * @returns The badge element.
79
+ * @example
80
+ * ```tsx
81
+ * import { Badge } from "@/components/ui/badge";
82
+ *
83
+ * export function BetaLabel() {
84
+ * return (
85
+ * <div>
86
+ * <Badge variant="secondary">Beta</Badge>
87
+ * <Badge as="a" href="/new" variant="outline">New</Badge>
88
+ * </div>
89
+ * );
90
+ * }
91
+ * ```
92
+ * @since 1.0.0
93
+ * @see {@link BadgeProps}
94
+ * @see {@link badgeVariants}
95
+ */
96
+ function Badge<T extends PolymorphicTarget = "span">(props: BadgeProps<T>): JSX.Element {
97
+ const [local, rest] = splitProps(props as unknown as Loose, ["as", "class", "variant"]);
98
+ return (
99
+ <Polymorphic
100
+ as={local.as ?? "span"}
101
+ data-slot="badge"
102
+ class={cn(badgeVariants({ variant: local.variant }), local.class)}
103
+ {...rest}
104
+ />
105
+ );
106
+ }
107
+
108
+ export { Badge, badgeVariants };
@@ -0,0 +1,4 @@
1
+ {
2
+ "title": "Badge",
3
+ "description": "Displays a badge or a component that looks like a badge."
4
+ }