@emara/ui 1.1.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.
- package/components/ui/.gitkeep +0 -0
- package/components/ui/accordion.stories.tsx +231 -0
- package/components/ui/accordion.tsx +250 -0
- package/components/ui/app-shell.stories.tsx +270 -0
- package/components/ui/app-shell.tsx +491 -0
- package/components/ui/avatar.stories.tsx +174 -0
- package/components/ui/avatar.tsx +257 -0
- package/components/ui/badge.stories.tsx +127 -0
- package/components/ui/badge.tsx +146 -0
- package/components/ui/breadcrumb.stories.tsx +92 -0
- package/components/ui/breadcrumb.tsx +302 -0
- package/components/ui/button.stories.tsx +186 -0
- package/components/ui/button.tsx +128 -0
- package/components/ui/card.stories.tsx +279 -0
- package/components/ui/card.tsx +250 -0
- package/components/ui/checkbox.stories.tsx +93 -0
- package/components/ui/checkbox.tsx +131 -0
- package/components/ui/combobox.stories.tsx +489 -0
- package/components/ui/combobox.tsx +874 -0
- package/components/ui/context-menu.stories.tsx +202 -0
- package/components/ui/context-menu.tsx +309 -0
- package/components/ui/data-table.stories.tsx +227 -0
- package/components/ui/data-table.tsx +539 -0
- package/components/ui/date-picker.stories.tsx +225 -0
- package/components/ui/date-picker.tsx +597 -0
- package/components/ui/dialog.stories.tsx +193 -0
- package/components/ui/dialog.tsx +262 -0
- package/components/ui/divider.stories.tsx +84 -0
- package/components/ui/divider.tsx +135 -0
- package/components/ui/drawer.stories.tsx +218 -0
- package/components/ui/drawer.tsx +329 -0
- package/components/ui/dropdown-menu.stories.tsx +270 -0
- package/components/ui/dropdown-menu.tsx +353 -0
- package/components/ui/empty-state.stories.tsx +121 -0
- package/components/ui/empty-state.tsx +289 -0
- package/components/ui/field-group.stories.tsx +201 -0
- package/components/ui/field-group.tsx +276 -0
- package/components/ui/form.stories.tsx +219 -0
- package/components/ui/form.tsx +542 -0
- package/components/ui/input.stories.tsx +154 -0
- package/components/ui/input.tsx +208 -0
- package/components/ui/label.stories.tsx +84 -0
- package/components/ui/label.tsx +98 -0
- package/components/ui/page-header.stories.tsx +136 -0
- package/components/ui/page-header.tsx +315 -0
- package/components/ui/pagination.stories.tsx +136 -0
- package/components/ui/pagination.tsx +427 -0
- package/components/ui/popover.stories.tsx +212 -0
- package/components/ui/popover.tsx +167 -0
- package/components/ui/radio-group.stories.tsx +96 -0
- package/components/ui/radio-group.tsx +250 -0
- package/components/ui/select.stories.tsx +203 -0
- package/components/ui/select.tsx +318 -0
- package/components/ui/sidebar.stories.tsx +186 -0
- package/components/ui/sidebar.tsx +623 -0
- package/components/ui/skeleton.stories.tsx +131 -0
- package/components/ui/skeleton.tsx +311 -0
- package/components/ui/switch.stories.tsx +74 -0
- package/components/ui/switch.tsx +186 -0
- package/components/ui/table.stories.tsx +107 -0
- package/components/ui/table.tsx +285 -0
- package/components/ui/tabs.stories.tsx +222 -0
- package/components/ui/tabs.tsx +287 -0
- package/components/ui/textarea.stories.tsx +96 -0
- package/components/ui/textarea.tsx +182 -0
- package/components/ui/toast.stories.tsx +169 -0
- package/components/ui/toast.tsx +250 -0
- package/components/ui/tooltip.stories.tsx +146 -0
- package/components/ui/tooltip.tsx +156 -0
- package/components/ui/top-bar.stories.tsx +182 -0
- package/components/ui/top-bar.tsx +155 -0
- package/dist/components/ui/accordion.d.ts +45 -0
- package/dist/components/ui/accordion.d.ts.map +1 -0
- package/dist/components/ui/accordion.js +99 -0
- package/dist/components/ui/accordion.js.map +1 -0
- package/dist/components/ui/app-shell.d.ts +70 -0
- package/dist/components/ui/app-shell.d.ts.map +1 -0
- package/dist/components/ui/app-shell.js +199 -0
- package/dist/components/ui/app-shell.js.map +1 -0
- package/dist/components/ui/avatar.d.ts +41 -0
- package/dist/components/ui/avatar.d.ts.map +1 -0
- package/dist/components/ui/avatar.js +104 -0
- package/dist/components/ui/avatar.js.map +1 -0
- package/dist/components/ui/badge.d.ts +27 -0
- package/dist/components/ui/badge.d.ts.map +1 -0
- package/dist/components/ui/badge.js +65 -0
- package/dist/components/ui/badge.js.map +1 -0
- package/dist/components/ui/breadcrumb.d.ts +35 -0
- package/dist/components/ui/breadcrumb.d.ts.map +1 -0
- package/dist/components/ui/breadcrumb.js +88 -0
- package/dist/components/ui/breadcrumb.js.map +1 -0
- package/dist/components/ui/button.d.ts +26 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/button.js +73 -0
- package/dist/components/ui/button.js.map +1 -0
- package/dist/components/ui/card.d.ts +52 -0
- package/dist/components/ui/card.d.ts.map +1 -0
- package/dist/components/ui/card.js +96 -0
- package/dist/components/ui/card.js.map +1 -0
- package/dist/components/ui/checkbox.d.ts +18 -0
- package/dist/components/ui/checkbox.d.ts.map +1 -0
- package/dist/components/ui/checkbox.js +59 -0
- package/dist/components/ui/checkbox.js.map +1 -0
- package/dist/components/ui/combobox.d.ts +194 -0
- package/dist/components/ui/combobox.d.ts.map +1 -0
- package/dist/components/ui/combobox.js +361 -0
- package/dist/components/ui/combobox.js.map +1 -0
- package/dist/components/ui/context-menu.d.ts +46 -0
- package/dist/components/ui/context-menu.d.ts.map +1 -0
- package/dist/components/ui/context-menu.js +95 -0
- package/dist/components/ui/context-menu.js.map +1 -0
- package/dist/components/ui/data-table.d.ts +53 -0
- package/dist/components/ui/data-table.d.ts.map +1 -0
- package/dist/components/ui/data-table.js +163 -0
- package/dist/components/ui/data-table.js.map +1 -0
- package/dist/components/ui/date-picker.d.ts +103 -0
- package/dist/components/ui/date-picker.d.ts.map +1 -0
- package/dist/components/ui/date-picker.js +306 -0
- package/dist/components/ui/date-picker.js.map +1 -0
- package/dist/components/ui/dialog.d.ts +40 -0
- package/dist/components/ui/dialog.d.ts.map +1 -0
- package/dist/components/ui/dialog.js +110 -0
- package/dist/components/ui/dialog.js.map +1 -0
- package/dist/components/ui/divider.d.ts +30 -0
- package/dist/components/ui/divider.d.ts.map +1 -0
- package/dist/components/ui/divider.js +62 -0
- package/dist/components/ui/divider.js.map +1 -0
- package/dist/components/ui/drawer.d.ts +56 -0
- package/dist/components/ui/drawer.d.ts.map +1 -0
- package/dist/components/ui/drawer.js +147 -0
- package/dist/components/ui/drawer.js.map +1 -0
- package/dist/components/ui/dropdown-menu.d.ts +63 -0
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/components/ui/dropdown-menu.js +116 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -0
- package/dist/components/ui/empty-state.d.ts +43 -0
- package/dist/components/ui/empty-state.d.ts.map +1 -0
- package/dist/components/ui/empty-state.js +128 -0
- package/dist/components/ui/empty-state.js.map +1 -0
- package/dist/components/ui/field-group.d.ts +38 -0
- package/dist/components/ui/field-group.d.ts.map +1 -0
- package/dist/components/ui/field-group.js +107 -0
- package/dist/components/ui/field-group.js.map +1 -0
- package/dist/components/ui/form.d.ts +67 -0
- package/dist/components/ui/form.d.ts.map +1 -0
- package/dist/components/ui/form.js +286 -0
- package/dist/components/ui/form.js.map +1 -0
- package/dist/components/ui/input.d.ts +36 -0
- package/dist/components/ui/input.d.ts.map +1 -0
- package/dist/components/ui/input.js +99 -0
- package/dist/components/ui/input.js.map +1 -0
- package/dist/components/ui/label.d.ts +37 -0
- package/dist/components/ui/label.d.ts.map +1 -0
- package/dist/components/ui/label.js +34 -0
- package/dist/components/ui/label.js.map +1 -0
- package/dist/components/ui/page-header.d.ts +65 -0
- package/dist/components/ui/page-header.d.ts.map +1 -0
- package/dist/components/ui/page-header.js +140 -0
- package/dist/components/ui/page-header.js.map +1 -0
- package/dist/components/ui/pagination.d.ts +67 -0
- package/dist/components/ui/pagination.d.ts.map +1 -0
- package/dist/components/ui/pagination.js +109 -0
- package/dist/components/ui/pagination.js.map +1 -0
- package/dist/components/ui/popover.d.ts +28 -0
- package/dist/components/ui/popover.d.ts.map +1 -0
- package/dist/components/ui/popover.js +85 -0
- package/dist/components/ui/popover.js.map +1 -0
- package/dist/components/ui/radio-group.d.ts +35 -0
- package/dist/components/ui/radio-group.d.ts.map +1 -0
- package/dist/components/ui/radio-group.js +103 -0
- package/dist/components/ui/radio-group.js.map +1 -0
- package/dist/components/ui/select.d.ts +42 -0
- package/dist/components/ui/select.d.ts.map +1 -0
- package/dist/components/ui/select.js +86 -0
- package/dist/components/ui/select.js.map +1 -0
- package/dist/components/ui/sidebar.d.ts +59 -0
- package/dist/components/ui/sidebar.d.ts.map +1 -0
- package/dist/components/ui/sidebar.js +189 -0
- package/dist/components/ui/sidebar.js.map +1 -0
- package/dist/components/ui/skeleton.d.ts +77 -0
- package/dist/components/ui/skeleton.d.ts.map +1 -0
- package/dist/components/ui/skeleton.js +115 -0
- package/dist/components/ui/skeleton.js.map +1 -0
- package/dist/components/ui/switch.d.ts +26 -0
- package/dist/components/ui/switch.d.ts.map +1 -0
- package/dist/components/ui/switch.js +84 -0
- package/dist/components/ui/switch.js.map +1 -0
- package/dist/components/ui/table.d.ts +52 -0
- package/dist/components/ui/table.d.ts.map +1 -0
- package/dist/components/ui/table.js +109 -0
- package/dist/components/ui/table.js.map +1 -0
- package/dist/components/ui/tabs.d.ts +42 -0
- package/dist/components/ui/tabs.d.ts.map +1 -0
- package/dist/components/ui/tabs.js +163 -0
- package/dist/components/ui/tabs.js.map +1 -0
- package/dist/components/ui/textarea.d.ts +26 -0
- package/dist/components/ui/textarea.d.ts.map +1 -0
- package/dist/components/ui/textarea.js +96 -0
- package/dist/components/ui/textarea.js.map +1 -0
- package/dist/components/ui/toast.d.ts +77 -0
- package/dist/components/ui/toast.d.ts.map +1 -0
- package/dist/components/ui/toast.js +141 -0
- package/dist/components/ui/toast.js.map +1 -0
- package/dist/components/ui/tooltip.d.ts +31 -0
- package/dist/components/ui/tooltip.d.ts.map +1 -0
- package/dist/components/ui/tooltip.js +71 -0
- package/dist/components/ui/tooltip.js.map +1 -0
- package/dist/components/ui/top-bar.d.ts +30 -0
- package/dist/components/ui/top-bar.d.ts.map +1 -0
- package/dist/components/ui/top-bar.js +64 -0
- package/dist/components/ui/top-bar.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/lib/utils.ts +6 -0
- package/package.json +112 -0
- package/styles/globals.css +685 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, forwardRef, useContext, useMemo } from "react";
|
|
4
|
+
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
|
|
9
|
+
// Per docs/emara-ui-phase-1-components.md §7.
|
|
10
|
+
|
|
11
|
+
// ----------------------------------------------------------------------------
|
|
12
|
+
// Sizing — single source of truth shared by Avatar root and SkeletonCircle-
|
|
13
|
+
// style children.
|
|
14
|
+
// ----------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
type Size = "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
|
|
17
|
+
|
|
18
|
+
const SIZE_TO_DIM: Record<Size, number> = {
|
|
19
|
+
xs: 24,
|
|
20
|
+
sm: 32,
|
|
21
|
+
md: 40,
|
|
22
|
+
lg: 48,
|
|
23
|
+
xl: 64,
|
|
24
|
+
"2xl": 96,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const avatarSizeClass: Record<Size, string> = {
|
|
28
|
+
xs: "size-6",
|
|
29
|
+
sm: "size-8",
|
|
30
|
+
md: "size-10",
|
|
31
|
+
lg: "size-12",
|
|
32
|
+
xl: "size-16",
|
|
33
|
+
"2xl": "size-24",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const statusDotSize: Record<Size, string> = {
|
|
37
|
+
xs: "size-1.5",
|
|
38
|
+
sm: "size-2",
|
|
39
|
+
md: "size-2.5",
|
|
40
|
+
lg: "size-3",
|
|
41
|
+
xl: "size-3.5",
|
|
42
|
+
"2xl": "size-4",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const fallbackTextSize: Record<Size, string> = {
|
|
46
|
+
xs: "text-[10px]",
|
|
47
|
+
sm: "text-xs",
|
|
48
|
+
md: "text-sm",
|
|
49
|
+
lg: "text-base",
|
|
50
|
+
xl: "text-lg",
|
|
51
|
+
"2xl": "text-2xl",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ----------------------------------------------------------------------------
|
|
55
|
+
// AvatarGroup context — children inherit a default `size` from their parent
|
|
56
|
+
// group when no per-Avatar size is set.
|
|
57
|
+
// ----------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
interface AvatarGroupContextValue {
|
|
60
|
+
size: Size | undefined;
|
|
61
|
+
}
|
|
62
|
+
const AvatarGroupContext = createContext<AvatarGroupContextValue | null>(null);
|
|
63
|
+
|
|
64
|
+
function useAvatarGroupSize(): Size | undefined {
|
|
65
|
+
return useContext(AvatarGroupContext)?.size;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ----------------------------------------------------------------------------
|
|
69
|
+
// Avatar (root)
|
|
70
|
+
// ----------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
const avatarVariants = cva(
|
|
73
|
+
"relative inline-flex shrink-0 overflow-hidden border-2 border-background bg-muted text-muted-foreground",
|
|
74
|
+
{
|
|
75
|
+
variants: {
|
|
76
|
+
shape: {
|
|
77
|
+
circle: "rounded-full",
|
|
78
|
+
rounded: "rounded-md",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
defaultVariants: {
|
|
82
|
+
shape: "circle",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
type Status = "online" | "offline" | "busy" | "away" | "none";
|
|
88
|
+
|
|
89
|
+
const statusLabel: Record<Exclude<Status, "none">, string> = {
|
|
90
|
+
online: "Online",
|
|
91
|
+
offline: "Offline",
|
|
92
|
+
busy: "Busy",
|
|
93
|
+
away: "Away",
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const statusBgClass: Record<Exclude<Status, "none">, string> = {
|
|
97
|
+
online: "bg-success",
|
|
98
|
+
offline: "bg-muted-foreground",
|
|
99
|
+
busy: "bg-destructive",
|
|
100
|
+
away: "bg-warning",
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
type AvatarProps = Omit<
|
|
104
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>,
|
|
105
|
+
"asChild"
|
|
106
|
+
> &
|
|
107
|
+
VariantProps<typeof avatarVariants> & {
|
|
108
|
+
size?: Size;
|
|
109
|
+
status?: Status;
|
|
110
|
+
statusPosition?: "top-end" | "bottom-end";
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const Avatar = forwardRef<
|
|
114
|
+
React.ElementRef<typeof AvatarPrimitive.Root>,
|
|
115
|
+
AvatarProps
|
|
116
|
+
>(function Avatar(
|
|
117
|
+
{
|
|
118
|
+
className,
|
|
119
|
+
size,
|
|
120
|
+
shape,
|
|
121
|
+
status = "none",
|
|
122
|
+
statusPosition = "bottom-end",
|
|
123
|
+
children,
|
|
124
|
+
...props
|
|
125
|
+
},
|
|
126
|
+
ref,
|
|
127
|
+
) {
|
|
128
|
+
const groupSize = useAvatarGroupSize();
|
|
129
|
+
const resolvedSize: Size = size ?? groupSize ?? "md";
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<AvatarPrimitive.Root
|
|
133
|
+
ref={ref}
|
|
134
|
+
className={cn(avatarVariants({ shape }), avatarSizeClass[resolvedSize], className)}
|
|
135
|
+
{...props}
|
|
136
|
+
>
|
|
137
|
+
{children}
|
|
138
|
+
{status !== "none" ? (
|
|
139
|
+
<span
|
|
140
|
+
aria-hidden="true"
|
|
141
|
+
className={cn(
|
|
142
|
+
"absolute block rounded-full ring-2 ring-background",
|
|
143
|
+
statusBgClass[status],
|
|
144
|
+
statusDotSize[resolvedSize],
|
|
145
|
+
statusPosition === "top-end" ? "top-0 end-0" : "bottom-0 end-0",
|
|
146
|
+
)}
|
|
147
|
+
/>
|
|
148
|
+
) : null}
|
|
149
|
+
{status !== "none" ? <span className="sr-only">{statusLabel[status]}</span> : null}
|
|
150
|
+
</AvatarPrimitive.Root>
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
Avatar.displayName = "Avatar";
|
|
154
|
+
|
|
155
|
+
// ----------------------------------------------------------------------------
|
|
156
|
+
// AvatarImage / AvatarFallback — thin Radix wrappers with default classes.
|
|
157
|
+
// ----------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
const AvatarImage = forwardRef<
|
|
160
|
+
React.ElementRef<typeof AvatarPrimitive.Image>,
|
|
161
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
|
162
|
+
>(function AvatarImage({ className, alt, ...props }, ref) {
|
|
163
|
+
return (
|
|
164
|
+
<AvatarPrimitive.Image
|
|
165
|
+
ref={ref}
|
|
166
|
+
alt={alt}
|
|
167
|
+
className={cn("aspect-square h-full w-full object-cover", className)}
|
|
168
|
+
{...props}
|
|
169
|
+
/>
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
AvatarImage.displayName = "AvatarImage";
|
|
173
|
+
|
|
174
|
+
type AvatarFallbackProps = React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> & {
|
|
175
|
+
size?: Size;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const AvatarFallback = forwardRef<
|
|
179
|
+
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
|
180
|
+
AvatarFallbackProps
|
|
181
|
+
>(function AvatarFallback({ className, size, ...props }, ref) {
|
|
182
|
+
const groupSize = useAvatarGroupSize();
|
|
183
|
+
const resolved: Size = size ?? groupSize ?? "md";
|
|
184
|
+
return (
|
|
185
|
+
<AvatarPrimitive.Fallback
|
|
186
|
+
ref={ref}
|
|
187
|
+
className={cn(
|
|
188
|
+
"flex h-full w-full items-center justify-center bg-muted font-medium text-muted-foreground select-none",
|
|
189
|
+
fallbackTextSize[resolved],
|
|
190
|
+
className,
|
|
191
|
+
)}
|
|
192
|
+
{...props}
|
|
193
|
+
/>
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
AvatarFallback.displayName = "AvatarFallback";
|
|
197
|
+
|
|
198
|
+
// ----------------------------------------------------------------------------
|
|
199
|
+
// AvatarGroup — overlaps children, optionally truncates with a "+N" pill.
|
|
200
|
+
// ----------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
type AvatarGroupProps = Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & {
|
|
203
|
+
size?: Size;
|
|
204
|
+
spacing?: "tight" | "normal" | "loose";
|
|
205
|
+
max?: number;
|
|
206
|
+
children: React.ReactNode;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const spacingMargin: Record<"tight" | "normal" | "loose", string> = {
|
|
210
|
+
// Negative margin-inline-start — RTL flips automatically.
|
|
211
|
+
tight: "[&>*+*]:-ms-3",
|
|
212
|
+
normal: "[&>*+*]:-ms-2",
|
|
213
|
+
loose: "[&>*+*]:-ms-1",
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const AvatarGroup = forwardRef<HTMLDivElement, AvatarGroupProps>(function AvatarGroup(
|
|
217
|
+
{ className, size, spacing = "normal", max, children, ...props },
|
|
218
|
+
ref,
|
|
219
|
+
) {
|
|
220
|
+
const ctx = useMemo<AvatarGroupContextValue>(() => ({ size }), [size]);
|
|
221
|
+
|
|
222
|
+
const items = (Array.isArray(children) ? children : [children]).filter(Boolean);
|
|
223
|
+
const visible = max !== undefined ? items.slice(0, max) : items;
|
|
224
|
+
const hidden = max !== undefined ? Math.max(0, items.length - max) : 0;
|
|
225
|
+
const dim = SIZE_TO_DIM[size ?? "md"];
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<AvatarGroupContext.Provider value={ctx}>
|
|
229
|
+
<div
|
|
230
|
+
ref={ref}
|
|
231
|
+
className={cn("inline-flex isolate", spacingMargin[spacing], className)}
|
|
232
|
+
{...props}
|
|
233
|
+
>
|
|
234
|
+
{visible}
|
|
235
|
+
{hidden > 0 ? (
|
|
236
|
+
<span
|
|
237
|
+
role="status"
|
|
238
|
+
aria-label={`${hidden} more`}
|
|
239
|
+
className={cn(
|
|
240
|
+
"relative inline-flex shrink-0 items-center justify-center rounded-full",
|
|
241
|
+
"border-2 border-background bg-muted text-muted-foreground font-medium",
|
|
242
|
+
avatarSizeClass[size ?? "md"],
|
|
243
|
+
fallbackTextSize[size ?? "md"],
|
|
244
|
+
)}
|
|
245
|
+
style={{ width: dim, height: dim }}
|
|
246
|
+
>
|
|
247
|
+
+{hidden}
|
|
248
|
+
</span>
|
|
249
|
+
) : null}
|
|
250
|
+
</div>
|
|
251
|
+
</AvatarGroupContext.Provider>
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
AvatarGroup.displayName = "AvatarGroup";
|
|
255
|
+
|
|
256
|
+
export { Avatar, AvatarImage, AvatarFallback, AvatarGroup };
|
|
257
|
+
export type { AvatarProps, AvatarFallbackProps, AvatarGroupProps };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { RiCheckLine, RiStarLine } from "@remixicon/react";
|
|
3
|
+
|
|
4
|
+
import { Badge } from "./badge";
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Badge> = {
|
|
7
|
+
title: "Foundations/Badge",
|
|
8
|
+
component: Badge,
|
|
9
|
+
parameters: { layout: "centered" },
|
|
10
|
+
argTypes: {
|
|
11
|
+
variant: {
|
|
12
|
+
control: "select",
|
|
13
|
+
options: ["default", "secondary", "destructive", "outline", "success", "warning", "info"],
|
|
14
|
+
},
|
|
15
|
+
size: { control: "select", options: ["xs", "sm", "md", "lg"] },
|
|
16
|
+
dot: { control: "boolean" },
|
|
17
|
+
pulse: { control: "boolean" },
|
|
18
|
+
removable: { control: "boolean" },
|
|
19
|
+
},
|
|
20
|
+
args: { children: "Badge" },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
type Story = StoryObj<typeof Badge>;
|
|
25
|
+
|
|
26
|
+
export const Default: Story = {};
|
|
27
|
+
|
|
28
|
+
export const Variants: Story = {
|
|
29
|
+
render: () => (
|
|
30
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
31
|
+
<Badge variant="default">Default</Badge>
|
|
32
|
+
<Badge variant="secondary">Secondary</Badge>
|
|
33
|
+
<Badge variant="destructive">Destructive</Badge>
|
|
34
|
+
<Badge variant="outline">Outline</Badge>
|
|
35
|
+
<Badge variant="success">Success</Badge>
|
|
36
|
+
<Badge variant="warning">Warning</Badge>
|
|
37
|
+
<Badge variant="info">Info</Badge>
|
|
38
|
+
</div>
|
|
39
|
+
),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const Sizes: Story = {
|
|
43
|
+
render: () => (
|
|
44
|
+
<div className="flex flex-wrap items-end gap-2">
|
|
45
|
+
<Badge size="xs">xs</Badge>
|
|
46
|
+
<Badge size="sm">sm</Badge>
|
|
47
|
+
<Badge size="md">md</Badge>
|
|
48
|
+
<Badge size="lg">lg</Badge>
|
|
49
|
+
</div>
|
|
50
|
+
),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const WithDot: Story = {
|
|
54
|
+
render: () => (
|
|
55
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
56
|
+
<Badge dot variant="default">
|
|
57
|
+
Default
|
|
58
|
+
</Badge>
|
|
59
|
+
<Badge dot variant="secondary">
|
|
60
|
+
Secondary
|
|
61
|
+
</Badge>
|
|
62
|
+
<Badge dot variant="destructive">
|
|
63
|
+
Destructive
|
|
64
|
+
</Badge>
|
|
65
|
+
<Badge dot variant="outline">
|
|
66
|
+
Outline
|
|
67
|
+
</Badge>
|
|
68
|
+
<Badge dot variant="success">
|
|
69
|
+
Success
|
|
70
|
+
</Badge>
|
|
71
|
+
<Badge dot variant="warning">
|
|
72
|
+
Warning
|
|
73
|
+
</Badge>
|
|
74
|
+
<Badge dot variant="info">
|
|
75
|
+
Info
|
|
76
|
+
</Badge>
|
|
77
|
+
</div>
|
|
78
|
+
),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const PulsingDot: Story = {
|
|
82
|
+
render: () => (
|
|
83
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
84
|
+
<Badge dot pulse variant="success">
|
|
85
|
+
Live
|
|
86
|
+
</Badge>
|
|
87
|
+
<Badge dot pulse variant="destructive">
|
|
88
|
+
Recording
|
|
89
|
+
</Badge>
|
|
90
|
+
<Badge dot pulse variant="info">
|
|
91
|
+
Streaming
|
|
92
|
+
</Badge>
|
|
93
|
+
</div>
|
|
94
|
+
),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const WithIcon: Story = {
|
|
98
|
+
render: () => (
|
|
99
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
100
|
+
<Badge icon={<RiCheckLine />} variant="success">
|
|
101
|
+
Verified
|
|
102
|
+
</Badge>
|
|
103
|
+
<Badge icon={<RiStarLine />} variant="warning">
|
|
104
|
+
Featured
|
|
105
|
+
</Badge>
|
|
106
|
+
<Badge icon={<RiCheckLine />} variant="outline">
|
|
107
|
+
Approved
|
|
108
|
+
</Badge>
|
|
109
|
+
</div>
|
|
110
|
+
),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const Removable: Story = {
|
|
114
|
+
render: () => (
|
|
115
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
116
|
+
<Badge removable onRemove={() => console.log("remove default")}>
|
|
117
|
+
Default
|
|
118
|
+
</Badge>
|
|
119
|
+
<Badge removable onRemove={() => console.log("remove tag")} variant="secondary">
|
|
120
|
+
TypeScript
|
|
121
|
+
</Badge>
|
|
122
|
+
<Badge removable onRemove={() => console.log("remove tag")} variant="outline">
|
|
123
|
+
Filter
|
|
124
|
+
</Badge>
|
|
125
|
+
</div>
|
|
126
|
+
),
|
|
127
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
5
|
+
import { RiCloseLine } from "@remixicon/react";
|
|
6
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
7
|
+
|
|
8
|
+
import { cn } from "@/lib/utils";
|
|
9
|
+
|
|
10
|
+
// Per docs/emara-ui-phase-1-components.md §6.
|
|
11
|
+
|
|
12
|
+
const badgeVariants = cva(
|
|
13
|
+
[
|
|
14
|
+
"inline-flex items-center gap-1 font-medium select-none",
|
|
15
|
+
"rounded-full border whitespace-nowrap",
|
|
16
|
+
"transition-colors",
|
|
17
|
+
].join(" "),
|
|
18
|
+
{
|
|
19
|
+
variants: {
|
|
20
|
+
variant: {
|
|
21
|
+
default: "border-transparent bg-primary text-primary-foreground",
|
|
22
|
+
secondary: "border-transparent bg-secondary text-secondary-foreground",
|
|
23
|
+
destructive: "border-transparent bg-destructive text-destructive-foreground",
|
|
24
|
+
outline: "border-border text-foreground",
|
|
25
|
+
success: "border-transparent bg-success text-primary-foreground",
|
|
26
|
+
warning: "border-transparent bg-warning text-primary-foreground",
|
|
27
|
+
info: "border-transparent bg-info text-primary-foreground",
|
|
28
|
+
},
|
|
29
|
+
size: {
|
|
30
|
+
xs: "h-4 px-1.5 text-[10px] leading-none",
|
|
31
|
+
sm: "h-5 px-2 text-xs leading-none",
|
|
32
|
+
md: "h-6 px-2.5 text-xs leading-none",
|
|
33
|
+
lg: "h-7 px-3 text-sm leading-none",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
defaultVariants: { variant: "default", size: "md" },
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Per-variant dot color (the dot inherits the variant's foreground for
|
|
41
|
+
// outline; otherwise uses currentColor at full opacity).
|
|
42
|
+
const dotVariants = cva("inline-block size-1.5 rounded-full me-1", {
|
|
43
|
+
variants: {
|
|
44
|
+
variant: {
|
|
45
|
+
default: "bg-primary-foreground/90",
|
|
46
|
+
secondary: "bg-secondary-foreground/80",
|
|
47
|
+
destructive: "bg-destructive-foreground/90",
|
|
48
|
+
outline: "bg-foreground/80",
|
|
49
|
+
success: "bg-primary-foreground/90",
|
|
50
|
+
warning: "bg-primary-foreground/90",
|
|
51
|
+
info: "bg-primary-foreground/90",
|
|
52
|
+
},
|
|
53
|
+
size: {
|
|
54
|
+
xs: "size-1",
|
|
55
|
+
sm: "size-1.5",
|
|
56
|
+
md: "size-1.5",
|
|
57
|
+
lg: "size-2",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
defaultVariants: { variant: "default", size: "md" },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
type BadgeVariants = VariantProps<typeof badgeVariants>;
|
|
64
|
+
|
|
65
|
+
type BadgeProps = Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> &
|
|
66
|
+
BadgeVariants & {
|
|
67
|
+
asChild?: boolean;
|
|
68
|
+
dot?: boolean;
|
|
69
|
+
icon?: React.ReactNode;
|
|
70
|
+
removable?: boolean;
|
|
71
|
+
onRemove?: () => void;
|
|
72
|
+
pulse?: boolean;
|
|
73
|
+
children?: React.ReactNode;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const Badge = forwardRef<HTMLSpanElement, BadgeProps>(function Badge(
|
|
77
|
+
{
|
|
78
|
+
className,
|
|
79
|
+
variant,
|
|
80
|
+
size,
|
|
81
|
+
asChild = false,
|
|
82
|
+
dot = false,
|
|
83
|
+
icon,
|
|
84
|
+
removable = false,
|
|
85
|
+
onRemove,
|
|
86
|
+
pulse = false,
|
|
87
|
+
children,
|
|
88
|
+
...props
|
|
89
|
+
},
|
|
90
|
+
ref,
|
|
91
|
+
) {
|
|
92
|
+
const Comp = asChild ? Slot : "span";
|
|
93
|
+
|
|
94
|
+
const innerContent = (
|
|
95
|
+
<>
|
|
96
|
+
{dot ? (
|
|
97
|
+
<span aria-hidden="true" className="relative inline-flex">
|
|
98
|
+
<span className={cn(dotVariants({ variant, size }))} />
|
|
99
|
+
{pulse ? (
|
|
100
|
+
<span
|
|
101
|
+
aria-hidden="true"
|
|
102
|
+
className={cn(
|
|
103
|
+
dotVariants({ variant, size }),
|
|
104
|
+
"absolute inset-0 me-0 animate-ping opacity-75",
|
|
105
|
+
)}
|
|
106
|
+
/>
|
|
107
|
+
) : null}
|
|
108
|
+
</span>
|
|
109
|
+
) : null}
|
|
110
|
+
{icon ? (
|
|
111
|
+
<span aria-hidden="true" className="me-1 inline-flex [&_svg]:size-3.5 [&_svg]:shrink-0">
|
|
112
|
+
{icon}
|
|
113
|
+
</span>
|
|
114
|
+
) : null}
|
|
115
|
+
<span>{children}</span>
|
|
116
|
+
{removable ? (
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
aria-label="Remove"
|
|
120
|
+
onClick={(e) => {
|
|
121
|
+
e.stopPropagation();
|
|
122
|
+
onRemove?.();
|
|
123
|
+
}}
|
|
124
|
+
className={cn(
|
|
125
|
+
"ms-1 inline-flex items-center justify-center rounded-full p-0.5",
|
|
126
|
+
"hover:bg-foreground/10",
|
|
127
|
+
"focus-visible:ring-ring focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
|
|
128
|
+
"[&_svg]:size-3 [&_svg]:shrink-0",
|
|
129
|
+
)}
|
|
130
|
+
>
|
|
131
|
+
<RiCloseLine />
|
|
132
|
+
</button>
|
|
133
|
+
) : null}
|
|
134
|
+
</>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<Comp ref={ref} className={cn(badgeVariants({ variant, size }), className)} {...props}>
|
|
139
|
+
{asChild ? children : innerContent}
|
|
140
|
+
</Comp>
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
Badge.displayName = "Badge";
|
|
144
|
+
|
|
145
|
+
export { Badge, badgeVariants };
|
|
146
|
+
export type { BadgeProps };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { RiFolderLine, RiHome4Line, RiSettings4Line } from "@remixicon/react";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
Breadcrumb,
|
|
6
|
+
BreadcrumbEllipsis,
|
|
7
|
+
BreadcrumbItem,
|
|
8
|
+
BreadcrumbLink,
|
|
9
|
+
BreadcrumbList,
|
|
10
|
+
BreadcrumbPage,
|
|
11
|
+
BreadcrumbSeparator,
|
|
12
|
+
Breadcrumbs,
|
|
13
|
+
} from "./breadcrumb";
|
|
14
|
+
|
|
15
|
+
const meta: Meta<typeof Breadcrumbs> = {
|
|
16
|
+
title: "Layout/Breadcrumb",
|
|
17
|
+
component: Breadcrumbs,
|
|
18
|
+
parameters: { layout: "padded" },
|
|
19
|
+
argTypes: {
|
|
20
|
+
size: { control: "select", options: ["sm", "md", "lg"] },
|
|
21
|
+
maxItems: { control: "number" },
|
|
22
|
+
itemsBeforeCollapse: { control: "number" },
|
|
23
|
+
itemsAfterCollapse: { control: "number" },
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default meta;
|
|
28
|
+
type Story = StoryObj<typeof Breadcrumbs>;
|
|
29
|
+
|
|
30
|
+
const ITEMS = [
|
|
31
|
+
{ label: "Home", href: "/", icon: <RiHome4Line /> },
|
|
32
|
+
{ label: "Settings", href: "/settings", icon: <RiSettings4Line /> },
|
|
33
|
+
{ label: "Account" },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export const Default: Story = {
|
|
37
|
+
args: { items: ITEMS },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const Sizes: Story = {
|
|
41
|
+
render: () => (
|
|
42
|
+
<div className="space-y-3">
|
|
43
|
+
<Breadcrumbs items={ITEMS} size="sm" />
|
|
44
|
+
<Breadcrumbs items={ITEMS} size="md" />
|
|
45
|
+
<Breadcrumbs items={ITEMS} size="lg" />
|
|
46
|
+
</div>
|
|
47
|
+
),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const MANY = [
|
|
51
|
+
{ label: "Home", href: "/", icon: <RiHome4Line /> },
|
|
52
|
+
{ label: "Workspaces", href: "/workspaces" },
|
|
53
|
+
{ label: "Acme", href: "/workspaces/acme" },
|
|
54
|
+
{ label: "Projects", href: "/workspaces/acme/projects" },
|
|
55
|
+
{ label: "Q3 launch", href: "/workspaces/acme/projects/q3", icon: <RiFolderLine /> },
|
|
56
|
+
{ label: "Tasks" },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
export const Collapsing: Story = {
|
|
60
|
+
args: { items: MANY, maxItems: 4, itemsBeforeCollapse: 1, itemsAfterCollapse: 2 },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const CustomSeparator: Story = {
|
|
64
|
+
args: { items: ITEMS, separator: <span className="text-muted-foreground/60">/</span> },
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Manual primitives — when you need full layout control. */
|
|
68
|
+
export const Primitives: Story = {
|
|
69
|
+
render: () => (
|
|
70
|
+
<Breadcrumb>
|
|
71
|
+
<BreadcrumbList>
|
|
72
|
+
<BreadcrumbItem>
|
|
73
|
+
<BreadcrumbLink href="/">
|
|
74
|
+
<RiHome4Line /> Home
|
|
75
|
+
</BreadcrumbLink>
|
|
76
|
+
</BreadcrumbItem>
|
|
77
|
+
<BreadcrumbSeparator />
|
|
78
|
+
<BreadcrumbItem>
|
|
79
|
+
<BreadcrumbLink href="/library">Library</BreadcrumbLink>
|
|
80
|
+
</BreadcrumbItem>
|
|
81
|
+
<BreadcrumbSeparator />
|
|
82
|
+
<BreadcrumbItem>
|
|
83
|
+
<BreadcrumbEllipsis />
|
|
84
|
+
</BreadcrumbItem>
|
|
85
|
+
<BreadcrumbSeparator />
|
|
86
|
+
<BreadcrumbItem>
|
|
87
|
+
<BreadcrumbPage>Components</BreadcrumbPage>
|
|
88
|
+
</BreadcrumbItem>
|
|
89
|
+
</BreadcrumbList>
|
|
90
|
+
</Breadcrumb>
|
|
91
|
+
),
|
|
92
|
+
};
|