@stasho/ds 0.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/package.json +88 -0
- package/src/components/alert/alert.tsx +204 -0
- package/src/components/badge/badge.tsx +139 -0
- package/src/components/breadcrumb/breadcrumb.tsx +130 -0
- package/src/components/button/button.tsx +153 -0
- package/src/components/card/card.tsx +48 -0
- package/src/components/checkbox/checkbox.tsx +82 -0
- package/src/components/combobox/combobox.tsx +166 -0
- package/src/components/copyable-text/copyable-text.tsx +194 -0
- package/src/components/dialog/dialog.tsx +145 -0
- package/src/components/form-field/form-field.tsx +71 -0
- package/src/components/input/input.tsx +52 -0
- package/src/components/logo/logo.tsx +68 -0
- package/src/components/multi-select/multi-select.tsx +312 -0
- package/src/components/pagination/pagination.tsx +218 -0
- package/src/components/progress-bar/progress-bar.tsx +134 -0
- package/src/components/radio-group/radio-group.tsx +87 -0
- package/src/components/select/select.tsx +124 -0
- package/src/components/slider/slider.tsx +127 -0
- package/src/components/status-dot/status-dot.tsx +53 -0
- package/src/components/stepper/stepper.tsx +229 -0
- package/src/components/switch/switch.tsx +74 -0
- package/src/components/table/table.tsx +260 -0
- package/src/components/tabs/tabs.tsx +493 -0
- package/src/components/textarea/textarea.tsx +56 -0
- package/src/components/tooltip/tooltip.tsx +35 -0
- package/src/components/ui/skeleton.tsx +21 -0
- package/src/components/ui/spinner.tsx +27 -0
- package/src/lib/cn.ts +6 -0
- package/src/styles/tokens.css +459 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
forwardRef,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
type ComponentPropsWithoutRef,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { Tabs as TabsPrimitive, DropdownMenu } from "radix-ui";
|
|
12
|
+
import { DotsThree } from "@phosphor-icons/react";
|
|
13
|
+
import { cn } from "@ac/lib/cn";
|
|
14
|
+
|
|
15
|
+
/* ── Root (direct re-export) ─────────────────── */
|
|
16
|
+
|
|
17
|
+
const Tabs = TabsPrimitive.Root;
|
|
18
|
+
|
|
19
|
+
/* ── List (with sliding indicator) ───────────── */
|
|
20
|
+
|
|
21
|
+
type TabsVariant = "underline" | "pill";
|
|
22
|
+
type TabsSize = "sm" | "md";
|
|
23
|
+
|
|
24
|
+
type TabsListProps = ComponentPropsWithoutRef<typeof TabsPrimitive.List> & {
|
|
25
|
+
variant?: TabsVariant;
|
|
26
|
+
size?: TabsSize;
|
|
27
|
+
overflow?: "collapse";
|
|
28
|
+
maxVisible?: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/* ── Overflow hook ───────────────────────────── */
|
|
32
|
+
|
|
33
|
+
function useOverflow(
|
|
34
|
+
listRef: React.RefObject<HTMLElement | null>,
|
|
35
|
+
overflowTriggerRef: React.RefObject<HTMLElement | null>,
|
|
36
|
+
enabled: boolean,
|
|
37
|
+
maxVisible: number | undefined,
|
|
38
|
+
) {
|
|
39
|
+
const [hiddenTabs, setHiddenTabs] = useState<HiddenTab[]>([]);
|
|
40
|
+
const [hasActiveHidden, setHasActiveHidden] = useState(false);
|
|
41
|
+
|
|
42
|
+
const measure = useCallback(() => {
|
|
43
|
+
const list = listRef.current;
|
|
44
|
+
const trigger = overflowTriggerRef.current;
|
|
45
|
+
if (!list || !trigger || !enabled) {
|
|
46
|
+
setHiddenTabs([]);
|
|
47
|
+
setHasActiveHidden(false);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const tabs = Array.from(
|
|
52
|
+
list.querySelectorAll<HTMLElement>('[role="tab"]'),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Reset all tabs to normal flow for measurement
|
|
56
|
+
for (const tab of tabs) {
|
|
57
|
+
tab.style.visibility = "";
|
|
58
|
+
tab.style.position = "";
|
|
59
|
+
tab.style.pointerEvents = "";
|
|
60
|
+
}
|
|
61
|
+
list.style.minHeight = "";
|
|
62
|
+
|
|
63
|
+
// Snapshot container height before hiding tabs so the tallest
|
|
64
|
+
// tab's height is captured. Applied as min-height after hiding
|
|
65
|
+
// to prevent layout collapse when the tallest tab overflows.
|
|
66
|
+
const containerHeight = list.offsetHeight;
|
|
67
|
+
const containerLeft = list.getBoundingClientRect().left;
|
|
68
|
+
const containerWidth = list.clientWidth;
|
|
69
|
+
const triggerWidth = trigger.offsetWidth;
|
|
70
|
+
|
|
71
|
+
let widthBreakIndex: number | null = null;
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < tabs.length; i++) {
|
|
74
|
+
const tab = tabs[i] as HTMLElement;
|
|
75
|
+
const tabRight = tab.getBoundingClientRect().right - containerLeft;
|
|
76
|
+
if (tabRight + triggerWidth > containerWidth) {
|
|
77
|
+
widthBreakIndex = i;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Final break index — stricter of width-based and count-based limits.
|
|
83
|
+
// null means no overflow.
|
|
84
|
+
let newBreakIndex: number | null = null;
|
|
85
|
+
if (widthBreakIndex !== null && maxVisible !== undefined) {
|
|
86
|
+
newBreakIndex = Math.min(widthBreakIndex, maxVisible);
|
|
87
|
+
} else if (widthBreakIndex !== null) {
|
|
88
|
+
newBreakIndex = widthBreakIndex;
|
|
89
|
+
} else if (maxVisible !== undefined && maxVisible < tabs.length) {
|
|
90
|
+
newBreakIndex = maxVisible;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// All tabs fit by width — verify last tab actually fits before bailing
|
|
94
|
+
if (newBreakIndex === null) {
|
|
95
|
+
const lastTab = tabs[tabs.length - 1];
|
|
96
|
+
if (lastTab) {
|
|
97
|
+
const lastRight =
|
|
98
|
+
lastTab.getBoundingClientRect().right - containerLeft;
|
|
99
|
+
if (lastRight <= containerWidth) {
|
|
100
|
+
setHiddenTabs([]);
|
|
101
|
+
setHasActiveHidden(false);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
setHiddenTabs([]);
|
|
106
|
+
setHasActiveHidden(false);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Hide overflowed tabs
|
|
111
|
+
for (let i = newBreakIndex; i < tabs.length; i++) {
|
|
112
|
+
const tab = tabs[i] as HTMLElement;
|
|
113
|
+
tab.style.visibility = "hidden";
|
|
114
|
+
tab.style.position = "absolute";
|
|
115
|
+
tab.style.pointerEvents = "none";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Lock container height so it doesn't collapse when the
|
|
119
|
+
// tallest tab is removed from flow by position:absolute
|
|
120
|
+
list.style.minHeight = `${String(containerHeight)}px`;
|
|
121
|
+
|
|
122
|
+
// Focus management: move focus to trigger if focused tab overflowed
|
|
123
|
+
const focused = document.activeElement;
|
|
124
|
+
if (focused instanceof HTMLElement) {
|
|
125
|
+
for (let i = newBreakIndex; i < tabs.length; i++) {
|
|
126
|
+
const tab = tabs[i] as HTMLElement;
|
|
127
|
+
if (tab === focused || tab.contains(focused)) {
|
|
128
|
+
trigger.focus();
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Build hiddenTabs array
|
|
135
|
+
const newHidden: HiddenTab[] = [];
|
|
136
|
+
let activeHidden = false;
|
|
137
|
+
for (let i = newBreakIndex; i < tabs.length; i++) {
|
|
138
|
+
const tab = tabs[i] as HTMLElement;
|
|
139
|
+
newHidden.push({
|
|
140
|
+
value: tab.getAttribute("data-value") ?? tab.id ?? "",
|
|
141
|
+
label: tab.textContent ?? "",
|
|
142
|
+
disabled: tab.hasAttribute("disabled"),
|
|
143
|
+
triggerEl: tab,
|
|
144
|
+
});
|
|
145
|
+
if (tab.dataset["state"] === "active") activeHidden = true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
setHiddenTabs(newHidden);
|
|
149
|
+
setHasActiveHidden(activeHidden);
|
|
150
|
+
}, [listRef, overflowTriggerRef, enabled, maxVisible]);
|
|
151
|
+
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
const list = listRef.current;
|
|
154
|
+
if (!list || !enabled) return;
|
|
155
|
+
|
|
156
|
+
measure();
|
|
157
|
+
|
|
158
|
+
const resizeObserver = new ResizeObserver(measure);
|
|
159
|
+
resizeObserver.observe(list);
|
|
160
|
+
|
|
161
|
+
const mutationObserver = new MutationObserver(measure);
|
|
162
|
+
mutationObserver.observe(list, {
|
|
163
|
+
attributes: true,
|
|
164
|
+
subtree: true,
|
|
165
|
+
attributeFilter: ["data-state"],
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return () => {
|
|
169
|
+
resizeObserver.disconnect();
|
|
170
|
+
mutationObserver.disconnect();
|
|
171
|
+
};
|
|
172
|
+
}, [enabled, measure]);
|
|
173
|
+
|
|
174
|
+
return { hiddenTabs, hasActiveHidden };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* ── Overflow types & trigger ────────────────── */
|
|
178
|
+
|
|
179
|
+
type HiddenTab = {
|
|
180
|
+
value: string;
|
|
181
|
+
label: string;
|
|
182
|
+
disabled: boolean;
|
|
183
|
+
triggerEl: HTMLElement;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
type OverflowTriggerProps = {
|
|
187
|
+
isPill: boolean;
|
|
188
|
+
isSmall: boolean;
|
|
189
|
+
hiddenTabs: HiddenTab[];
|
|
190
|
+
hasActiveHidden: boolean;
|
|
191
|
+
visible: boolean;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const OverflowTrigger = forwardRef<HTMLButtonElement, OverflowTriggerProps>(
|
|
195
|
+
({ isPill, isSmall, hiddenTabs, hasActiveHidden, visible }, ref) => (
|
|
196
|
+
<DropdownMenu.Root>
|
|
197
|
+
<DropdownMenu.Trigger asChild>
|
|
198
|
+
<button
|
|
199
|
+
ref={ref}
|
|
200
|
+
type="button"
|
|
201
|
+
aria-label="More tabs"
|
|
202
|
+
className={cn(
|
|
203
|
+
"inline-flex items-center justify-center shrink-0",
|
|
204
|
+
"font-heading font-bold",
|
|
205
|
+
hasActiveHidden && isPill
|
|
206
|
+
? "text-white"
|
|
207
|
+
: hasActiveHidden
|
|
208
|
+
? "text-primary-600 dark:text-primary-400"
|
|
209
|
+
: "text-muted-foreground",
|
|
210
|
+
"transition-colors duration-200",
|
|
211
|
+
"hover:text-primary-600 dark:hover:text-primary-400",
|
|
212
|
+
"focus-visible:outline-none focus-visible:ring-2",
|
|
213
|
+
"focus-visible:ring-primary-400 focus-visible:ring-offset-2",
|
|
214
|
+
"motion-reduce:transition-none",
|
|
215
|
+
isPill
|
|
216
|
+
? cn(
|
|
217
|
+
"relative z-10 rounded-full",
|
|
218
|
+
isSmall ? "px-2 py-0.5 text-xs" : "px-3 py-1.5 text-sm",
|
|
219
|
+
)
|
|
220
|
+
: isSmall
|
|
221
|
+
? "px-3 py-1.5 text-sm"
|
|
222
|
+
: "px-4 py-3 text-lg",
|
|
223
|
+
!visible && "invisible",
|
|
224
|
+
)}
|
|
225
|
+
>
|
|
226
|
+
<DotsThree weight="bold" className="size-5" aria-hidden="true" />
|
|
227
|
+
</button>
|
|
228
|
+
</DropdownMenu.Trigger>
|
|
229
|
+
<DropdownMenu.Portal>
|
|
230
|
+
<DropdownMenu.Content
|
|
231
|
+
className={cn(
|
|
232
|
+
"z-50 min-w-[8rem]",
|
|
233
|
+
"rounded-md bg-surface border border-edge shadow-brand",
|
|
234
|
+
"p-1",
|
|
235
|
+
"motion-reduce:transition-none",
|
|
236
|
+
)}
|
|
237
|
+
sideOffset={4}
|
|
238
|
+
align="end"
|
|
239
|
+
>
|
|
240
|
+
{hiddenTabs.map((tab) => (
|
|
241
|
+
<DropdownMenu.Item
|
|
242
|
+
key={tab.value}
|
|
243
|
+
disabled={tab.disabled}
|
|
244
|
+
onSelect={() => {
|
|
245
|
+
const el = tab.triggerEl;
|
|
246
|
+
// Defer focus until after DropdownMenu closes and
|
|
247
|
+
// releases its focus trap. Restoring visibility lets
|
|
248
|
+
// the trigger receive focus, which activates the tab
|
|
249
|
+
// via Radix. measure() re-hides via MutationObserver
|
|
250
|
+
// after data-state updates.
|
|
251
|
+
requestAnimationFrame(() => {
|
|
252
|
+
el.style.visibility = "";
|
|
253
|
+
el.style.pointerEvents = "";
|
|
254
|
+
el.focus();
|
|
255
|
+
});
|
|
256
|
+
}}
|
|
257
|
+
className={cn(
|
|
258
|
+
"flex w-full items-center rounded-sm px-3 py-2",
|
|
259
|
+
"text-sm text-foreground cursor-pointer select-none",
|
|
260
|
+
"outline-none",
|
|
261
|
+
"hover:bg-muted focus-visible:bg-muted",
|
|
262
|
+
"data-[disabled]:opacity-50 data-[disabled]:pointer-events-none",
|
|
263
|
+
tab.triggerEl.dataset["state"] === "active" &&
|
|
264
|
+
"text-primary-600 dark:text-primary-400 font-bold",
|
|
265
|
+
)}
|
|
266
|
+
>
|
|
267
|
+
{tab.label}
|
|
268
|
+
</DropdownMenu.Item>
|
|
269
|
+
))}
|
|
270
|
+
</DropdownMenu.Content>
|
|
271
|
+
</DropdownMenu.Portal>
|
|
272
|
+
</DropdownMenu.Root>
|
|
273
|
+
),
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
OverflowTrigger.displayName = "OverflowTrigger";
|
|
277
|
+
|
|
278
|
+
const TabsList = forwardRef<HTMLDivElement, TabsListProps>(
|
|
279
|
+
({ className, children, variant = "underline", size = "md", overflow, maxVisible, ...rest }, ref) => {
|
|
280
|
+
const innerRef = useRef<HTMLDivElement>(null);
|
|
281
|
+
const indicatorRef = useRef<HTMLDivElement>(null);
|
|
282
|
+
const overflowTriggerRef = useRef<HTMLButtonElement>(null);
|
|
283
|
+
const [ready, setReady] = useState(false);
|
|
284
|
+
const isPill = variant === "pill";
|
|
285
|
+
const isSmall = size === "sm";
|
|
286
|
+
const isCollapse = overflow === "collapse";
|
|
287
|
+
// maxVisible activates the same overflow code path; isCollapse stays
|
|
288
|
+
// bound to overflow="collapse" so layout (full-width pill) only changes
|
|
289
|
+
// when the consumer opts into width-based collapse.
|
|
290
|
+
const showOverflow = isCollapse || maxVisible !== undefined;
|
|
291
|
+
|
|
292
|
+
const setRefs = (node: HTMLDivElement | null) => {
|
|
293
|
+
innerRef.current = node;
|
|
294
|
+
if (typeof ref === "function") ref(node);
|
|
295
|
+
else if (ref) ref.current = node;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const { hiddenTabs, hasActiveHidden } = useOverflow(
|
|
299
|
+
innerRef,
|
|
300
|
+
overflowTriggerRef,
|
|
301
|
+
showOverflow,
|
|
302
|
+
maxVisible,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
const list = innerRef.current;
|
|
307
|
+
const indicator = indicatorRef.current;
|
|
308
|
+
if (!list || !indicator) return;
|
|
309
|
+
|
|
310
|
+
function updateIndicator() {
|
|
311
|
+
const activeTab = list!.querySelector<HTMLElement>(
|
|
312
|
+
'[data-state="active"]',
|
|
313
|
+
);
|
|
314
|
+
if (!activeTab || !indicator) return;
|
|
315
|
+
|
|
316
|
+
// When the active tab is overflowed, slide the indicator
|
|
317
|
+
// behind the "..." trigger instead of hiding it
|
|
318
|
+
if (activeTab.style.visibility === "hidden") {
|
|
319
|
+
const trigger = overflowTriggerRef.current;
|
|
320
|
+
if (trigger) {
|
|
321
|
+
indicator.style.opacity = "";
|
|
322
|
+
indicator.style.transform = `translateX(${String(trigger.offsetLeft)}px)`;
|
|
323
|
+
indicator.style.width = `${String(trigger.offsetWidth)}px`;
|
|
324
|
+
if (!ready) setReady(true);
|
|
325
|
+
} else {
|
|
326
|
+
indicator.style.opacity = "0";
|
|
327
|
+
}
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
indicator.style.opacity = "";
|
|
331
|
+
|
|
332
|
+
const left = activeTab.offsetLeft;
|
|
333
|
+
const width = activeTab.offsetWidth;
|
|
334
|
+
indicator.style.transform = `translateX(${String(left)}px)`;
|
|
335
|
+
indicator.style.width = `${String(width)}px`;
|
|
336
|
+
if (!ready) setReady(true);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
updateIndicator();
|
|
340
|
+
|
|
341
|
+
const observer = new MutationObserver(updateIndicator);
|
|
342
|
+
observer.observe(list, {
|
|
343
|
+
attributes: true,
|
|
344
|
+
subtree: true,
|
|
345
|
+
attributeFilter: ["data-state"],
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const resizeObserver = new ResizeObserver(updateIndicator);
|
|
349
|
+
resizeObserver.observe(list);
|
|
350
|
+
|
|
351
|
+
return () => {
|
|
352
|
+
observer.disconnect();
|
|
353
|
+
resizeObserver.disconnect();
|
|
354
|
+
};
|
|
355
|
+
}, [ready]);
|
|
356
|
+
|
|
357
|
+
return (
|
|
358
|
+
<TabsPrimitive.List
|
|
359
|
+
ref={setRefs}
|
|
360
|
+
data-variant={variant}
|
|
361
|
+
data-size={size}
|
|
362
|
+
className={cn(
|
|
363
|
+
"group relative flex",
|
|
364
|
+
isPill
|
|
365
|
+
? [
|
|
366
|
+
"rounded-full bg-muted",
|
|
367
|
+
isSmall ? "p-0.5" : "p-1",
|
|
368
|
+
!isCollapse && "inline-flex",
|
|
369
|
+
]
|
|
370
|
+
: isSmall
|
|
371
|
+
? "border-b-2 border-edge/40"
|
|
372
|
+
: "border-b-4 border-edge/40",
|
|
373
|
+
className,
|
|
374
|
+
)}
|
|
375
|
+
{...rest}
|
|
376
|
+
>
|
|
377
|
+
{children}
|
|
378
|
+
{showOverflow && (
|
|
379
|
+
<OverflowTrigger
|
|
380
|
+
ref={overflowTriggerRef}
|
|
381
|
+
isPill={isPill}
|
|
382
|
+
isSmall={isSmall}
|
|
383
|
+
hiddenTabs={hiddenTabs}
|
|
384
|
+
hasActiveHidden={hasActiveHidden}
|
|
385
|
+
visible={hiddenTabs.length > 0}
|
|
386
|
+
/>
|
|
387
|
+
)}
|
|
388
|
+
<div
|
|
389
|
+
ref={indicatorRef}
|
|
390
|
+
className={cn(
|
|
391
|
+
"absolute left-0",
|
|
392
|
+
isPill
|
|
393
|
+
? [
|
|
394
|
+
isSmall ? "inset-y-0.5" : "inset-y-1",
|
|
395
|
+
"rounded-full bg-primary-600 dark:bg-primary-500",
|
|
396
|
+
ready ? "opacity-100" : "opacity-0",
|
|
397
|
+
ready
|
|
398
|
+
? "transition-[transform,width,opacity] duration-200 ease-out"
|
|
399
|
+
: "",
|
|
400
|
+
]
|
|
401
|
+
: [
|
|
402
|
+
isSmall ? "-bottom-0.5 h-0.5" : "-bottom-1 h-1",
|
|
403
|
+
"bg-primary-600 dark:bg-primary-400",
|
|
404
|
+
ready
|
|
405
|
+
? "transition-[transform,width] duration-200 ease-out"
|
|
406
|
+
: "",
|
|
407
|
+
],
|
|
408
|
+
"motion-reduce:transition-none",
|
|
409
|
+
)}
|
|
410
|
+
aria-hidden
|
|
411
|
+
/>
|
|
412
|
+
</TabsPrimitive.List>
|
|
413
|
+
);
|
|
414
|
+
},
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
TabsList.displayName = "TabsList";
|
|
418
|
+
|
|
419
|
+
/* ── Trigger ─────────────────────────────────── */
|
|
420
|
+
|
|
421
|
+
const TabsTrigger = forwardRef<
|
|
422
|
+
HTMLButtonElement,
|
|
423
|
+
ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
|
424
|
+
>(({ className, ...rest }, ref) => (
|
|
425
|
+
<TabsPrimitive.Trigger
|
|
426
|
+
ref={ref}
|
|
427
|
+
className={cn(
|
|
428
|
+
[
|
|
429
|
+
"inline-flex items-center gap-2 px-4 py-3",
|
|
430
|
+
"font-heading font-bold text-lg",
|
|
431
|
+
"text-foreground",
|
|
432
|
+
"transition-[color,transform] duration-200 ease-out",
|
|
433
|
+
"hover:text-primary-600 dark:hover:text-primary-400",
|
|
434
|
+
"data-[state=active]:text-primary-600",
|
|
435
|
+
"dark:data-[state=active]:text-primary-400",
|
|
436
|
+
"data-[state=active]:-translate-y-0.5",
|
|
437
|
+
"disabled:opacity-20 disabled:pointer-events-none",
|
|
438
|
+
"focus-visible:outline-none focus-visible:ring-2",
|
|
439
|
+
"focus-visible:ring-primary-400 focus-visible:ring-offset-2",
|
|
440
|
+
"motion-reduce:transition-none",
|
|
441
|
+
// Small size overrides (underline)
|
|
442
|
+
"group-data-[size=sm]:px-3 group-data-[size=sm]:py-1.5",
|
|
443
|
+
"group-data-[size=sm]:text-sm group-data-[size=sm]:gap-1.5",
|
|
444
|
+
// Pill variant overrides (via group data attribute on TabsList)
|
|
445
|
+
"group-data-[variant=pill]:relative group-data-[variant=pill]:z-10",
|
|
446
|
+
"group-data-[variant=pill]:rounded-full",
|
|
447
|
+
"group-data-[variant=pill]:px-5 group-data-[variant=pill]:py-1.5",
|
|
448
|
+
"group-data-[variant=pill]:text-sm",
|
|
449
|
+
"group-data-[variant=pill]:text-muted-foreground",
|
|
450
|
+
"group-data-[variant=pill]:translate-y-0",
|
|
451
|
+
"group-data-[variant=pill]:hover:text-foreground",
|
|
452
|
+
"group-data-[variant=pill]:data-[state=active]:text-white",
|
|
453
|
+
"group-data-[variant=pill]:data-[state=active]:translate-y-0",
|
|
454
|
+
"group-data-[variant=pill]:focus-visible:ring-offset-0",
|
|
455
|
+
// Small pill overrides (compound group selector)
|
|
456
|
+
"group-[[data-variant=pill][data-size=sm]]:px-3",
|
|
457
|
+
"group-[[data-variant=pill][data-size=sm]]:py-1",
|
|
458
|
+
"group-[[data-variant=pill][data-size=sm]]:text-xs",
|
|
459
|
+
].join(" "),
|
|
460
|
+
className,
|
|
461
|
+
)}
|
|
462
|
+
{...rest}
|
|
463
|
+
/>
|
|
464
|
+
));
|
|
465
|
+
|
|
466
|
+
TabsTrigger.displayName = "TabsTrigger";
|
|
467
|
+
|
|
468
|
+
/* ── Content ─────────────────────────────────── */
|
|
469
|
+
|
|
470
|
+
const TabsContent = forwardRef<
|
|
471
|
+
HTMLDivElement,
|
|
472
|
+
ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
|
473
|
+
>(({ className, ...rest }, ref) => (
|
|
474
|
+
<TabsPrimitive.Content
|
|
475
|
+
ref={ref}
|
|
476
|
+
className={cn("mt-4", className)}
|
|
477
|
+
{...rest}
|
|
478
|
+
/>
|
|
479
|
+
));
|
|
480
|
+
|
|
481
|
+
TabsContent.displayName = "TabsContent";
|
|
482
|
+
|
|
483
|
+
/* ── Exports ─────────────────────────────────── */
|
|
484
|
+
|
|
485
|
+
export {
|
|
486
|
+
Tabs,
|
|
487
|
+
TabsContent,
|
|
488
|
+
TabsList,
|
|
489
|
+
TabsTrigger,
|
|
490
|
+
type TabsListProps,
|
|
491
|
+
type TabsSize,
|
|
492
|
+
type TabsVariant,
|
|
493
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { forwardRef, type TextareaHTMLAttributes } from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "@ac/lib/cn";
|
|
4
|
+
|
|
5
|
+
const textareaVariants = cva(
|
|
6
|
+
[
|
|
7
|
+
"w-full font-sans text-foreground bg-primary-100 dark:bg-base-700",
|
|
8
|
+
"border-0 rounded-2xl",
|
|
9
|
+
"placeholder:text-muted-foreground",
|
|
10
|
+
"focus-visible:outline-none focus-visible:ring-3",
|
|
11
|
+
"focus-visible:ring-primary-500",
|
|
12
|
+
"disabled:opacity-50 disabled:pointer-events-none",
|
|
13
|
+
"ring-0 resize-y transition-colors",
|
|
14
|
+
].join(" "),
|
|
15
|
+
{
|
|
16
|
+
variants: {
|
|
17
|
+
size: {
|
|
18
|
+
sm: "py-1.5 px-4 text-sm",
|
|
19
|
+
md: "py-2 px-5 text-base",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
size: "md",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
type TextareaProps = Omit<
|
|
29
|
+
TextareaHTMLAttributes<HTMLTextAreaElement>,
|
|
30
|
+
"size"
|
|
31
|
+
> &
|
|
32
|
+
VariantProps<typeof textareaVariants> & {
|
|
33
|
+
error?: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
37
|
+
({ size, error = false, rows = 4, className, ...rest }, ref) => {
|
|
38
|
+
return (
|
|
39
|
+
<textarea
|
|
40
|
+
ref={ref}
|
|
41
|
+
rows={rows}
|
|
42
|
+
className={cn(
|
|
43
|
+
textareaVariants({ size }),
|
|
44
|
+
error && "border-3 border-error-400 hover:border-error-500",
|
|
45
|
+
className,
|
|
46
|
+
)}
|
|
47
|
+
aria-invalid={error || undefined}
|
|
48
|
+
{...rest}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
Textarea.displayName = "Textarea";
|
|
55
|
+
|
|
56
|
+
export { Textarea, textareaVariants, type TextareaProps };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { forwardRef, type ComponentPropsWithoutRef } from "react";
|
|
2
|
+
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
|
3
|
+
import { cn } from "@ac/lib/cn";
|
|
4
|
+
|
|
5
|
+
const TooltipProvider = TooltipPrimitive.Provider;
|
|
6
|
+
const Tooltip = TooltipPrimitive.Root;
|
|
7
|
+
const TooltipTrigger = TooltipPrimitive.Trigger;
|
|
8
|
+
|
|
9
|
+
const TooltipContent = forwardRef<
|
|
10
|
+
HTMLDivElement,
|
|
11
|
+
ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
|
12
|
+
>(({ className, sideOffset = 6, ...rest }, ref) => (
|
|
13
|
+
<TooltipPrimitive.Portal>
|
|
14
|
+
<TooltipPrimitive.Content
|
|
15
|
+
ref={ref}
|
|
16
|
+
sideOffset={sideOffset}
|
|
17
|
+
className={cn(
|
|
18
|
+
[
|
|
19
|
+
"z-50 rounded-lg bg-neutral-900 dark:bg-base-800 px-3 py-1.5",
|
|
20
|
+
"text-sm text-white shadow-brand-sm",
|
|
21
|
+
"animate-in fade-in-0 zoom-in-95",
|
|
22
|
+
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
|
|
23
|
+
"data-[state=closed]:zoom-out-95",
|
|
24
|
+
"motion-reduce:animate-none",
|
|
25
|
+
].join(" "),
|
|
26
|
+
className,
|
|
27
|
+
)}
|
|
28
|
+
{...rest}
|
|
29
|
+
/>
|
|
30
|
+
</TooltipPrimitive.Portal>
|
|
31
|
+
));
|
|
32
|
+
|
|
33
|
+
TooltipContent.displayName = "TooltipContent";
|
|
34
|
+
|
|
35
|
+
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "@ac/lib/cn";
|
|
3
|
+
|
|
4
|
+
type SkeletonProps = HTMLAttributes<HTMLDivElement>;
|
|
5
|
+
|
|
6
|
+
const Skeleton = forwardRef<HTMLDivElement, SkeletonProps>(
|
|
7
|
+
({ className, ...rest }, ref) => {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
ref={ref}
|
|
11
|
+
aria-hidden="true"
|
|
12
|
+
className={cn("animate-pulse motion-reduce:animate-none rounded-md bg-muted", className)}
|
|
13
|
+
{...rest}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
},
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
Skeleton.displayName = "Skeleton";
|
|
20
|
+
|
|
21
|
+
export { Skeleton, type SkeletonProps };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { cn } from "@ac/lib/cn";
|
|
2
|
+
|
|
3
|
+
export function Spinner({ className }: { className?: string }) {
|
|
4
|
+
return (
|
|
5
|
+
<svg
|
|
6
|
+
className={cn("animate-spin motion-reduce:animate-none", className)}
|
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
+
fill="none"
|
|
9
|
+
viewBox="0 0 24 24"
|
|
10
|
+
aria-hidden="true"
|
|
11
|
+
>
|
|
12
|
+
<circle
|
|
13
|
+
className="opacity-25"
|
|
14
|
+
cx="12"
|
|
15
|
+
cy="12"
|
|
16
|
+
r="10"
|
|
17
|
+
stroke="currentColor"
|
|
18
|
+
strokeWidth="4"
|
|
19
|
+
/>
|
|
20
|
+
<path
|
|
21
|
+
className="opacity-75"
|
|
22
|
+
fill="currentColor"
|
|
23
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
24
|
+
/>
|
|
25
|
+
</svg>
|
|
26
|
+
);
|
|
27
|
+
}
|