@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,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,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 };
|