@syscore/ui-library 1.8.0 → 1.9.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/client/components/ui/accordion.tsx +501 -45
- package/client/components/ui/label.tsx +2 -2
- package/client/components/ui/tag.tsx +34 -7
- package/client/global.css +28 -0
- package/client/ui/Accordion.stories.tsx +430 -0
- package/client/ui/PageHeader.stories.tsx +6 -4
- package/client/ui/Panel.stories.tsx +12 -9
- package/client/ui/Tag.stories.tsx +153 -46
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +27 -30
- package/dist/index.es.js +401 -453
- package/package.json +1 -1
- package/client/components/ui/code-badge.tsx +0 -22
- package/client/components/ui/standard-table.tsx +0 -554
- package/client/ui/Accordion/Accordion.stories.tsx +0 -74
- package/client/ui/CodeBadge.stories.tsx +0 -76
- package/client/ui/StandardTable.stories.tsx +0 -311
|
@@ -1,53 +1,509 @@
|
|
|
1
|
-
|
|
2
|
-
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
|
3
|
-
import { ChevronDown } from "lucide-react";
|
|
1
|
+
"use client";
|
|
4
2
|
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { motion, AnimatePresence } from "motion/react";
|
|
5
5
|
import { cn } from "@/lib/utils";
|
|
6
|
+
import { Button } from "./button";
|
|
7
|
+
import { UtilityChevronDown } from "../icons/UtilityChevronDown";
|
|
8
|
+
|
|
9
|
+
// Context Definitions
|
|
10
|
+
interface AccordionContextValue {
|
|
11
|
+
/** Set of currently expanded item values */
|
|
12
|
+
expandedValues: Set<string>;
|
|
13
|
+
/** Check if an item is expanded */
|
|
14
|
+
isExpanded: (value: string) => boolean;
|
|
15
|
+
/** Toggle a specific item's expansion */
|
|
16
|
+
toggle: (value: string) => void;
|
|
17
|
+
/** Expand all items */
|
|
18
|
+
expandAll: () => void;
|
|
19
|
+
/** Collapse all items */
|
|
20
|
+
collapseAll: () => void;
|
|
21
|
+
/** Check if any item is expanded */
|
|
22
|
+
hasAnyExpanded: boolean;
|
|
23
|
+
/** Allow multiple items to be expanded at once */
|
|
24
|
+
allowMultiple: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const AccordionContext = React.createContext<AccordionContextValue | null>(
|
|
28
|
+
null,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
interface AccordionItemContextValue {
|
|
32
|
+
/** Current item's value */
|
|
33
|
+
value: string;
|
|
34
|
+
/** Whether this item is expanded */
|
|
35
|
+
isExpanded: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const AccordionItemContext =
|
|
39
|
+
React.createContext<AccordionItemContextValue | null>(null);
|
|
40
|
+
|
|
41
|
+
// Hooks
|
|
42
|
+
function useAccordion() {
|
|
43
|
+
const context = React.useContext(AccordionContext);
|
|
44
|
+
if (!context) {
|
|
45
|
+
throw new Error("Accordion components must be used within <Accordion>");
|
|
46
|
+
}
|
|
47
|
+
return context;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function useAccordionItem() {
|
|
51
|
+
const context = React.useContext(AccordionItemContext);
|
|
52
|
+
if (!context) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
"AccordionTrigger and AccordionContent must be used within <AccordionItem>",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return context;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// useAccordionState Hook (for external state management)
|
|
61
|
+
interface UseAccordionStateOptions {
|
|
62
|
+
/** Allow multiple items to be expanded at once */
|
|
63
|
+
allowMultiple?: boolean;
|
|
64
|
+
/** Initially expanded item value(s) */
|
|
65
|
+
defaultExpanded?: string | string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface UseAccordionStateReturn {
|
|
69
|
+
/** Current expanded values */
|
|
70
|
+
expandedValues: Set<string>;
|
|
71
|
+
/** Set expanded values */
|
|
72
|
+
setExpandedValues: (values: Set<string>) => void;
|
|
73
|
+
/** Check if any item is expanded */
|
|
74
|
+
hasAnyExpanded: boolean;
|
|
75
|
+
/** Toggle all items */
|
|
76
|
+
toggleAll: () => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function useAccordionState(
|
|
80
|
+
options: UseAccordionStateOptions = {},
|
|
81
|
+
): UseAccordionStateReturn {
|
|
82
|
+
const { allowMultiple = false, defaultExpanded } = options;
|
|
83
|
+
|
|
84
|
+
// Initialize expanded values from default
|
|
85
|
+
const getInitialExpanded = (): Set<string> => {
|
|
86
|
+
if (!defaultExpanded) return new Set();
|
|
87
|
+
if (Array.isArray(defaultExpanded)) return new Set(defaultExpanded);
|
|
88
|
+
return new Set([defaultExpanded]);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const [expandedValues, setExpandedValues] =
|
|
92
|
+
React.useState<Set<string>>(getInitialExpanded);
|
|
93
|
+
const [isAllExpanded, setIsAllExpanded] = React.useState(false);
|
|
94
|
+
|
|
95
|
+
const hasAnyExpanded = React.useMemo(
|
|
96
|
+
() => isAllExpanded || expandedValues.size > 0,
|
|
97
|
+
[isAllExpanded, expandedValues],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const toggleAll = React.useCallback(() => {
|
|
101
|
+
if (hasAnyExpanded) {
|
|
102
|
+
setIsAllExpanded(false);
|
|
103
|
+
setExpandedValues(new Set());
|
|
104
|
+
} else {
|
|
105
|
+
setIsAllExpanded(true);
|
|
106
|
+
setExpandedValues(new Set());
|
|
107
|
+
}
|
|
108
|
+
}, [hasAnyExpanded]);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
expandedValues,
|
|
112
|
+
setExpandedValues,
|
|
113
|
+
hasAnyExpanded,
|
|
114
|
+
toggleAll,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Accordion (Root Component)
|
|
119
|
+
interface AccordionProps extends React.ComponentPropsWithoutRef<"div"> {
|
|
120
|
+
/** Allow multiple items to be expanded at once (default: false for accordion behavior) */
|
|
121
|
+
allowMultiple?: boolean;
|
|
122
|
+
/** Initially expanded item value(s) (uncontrolled) */
|
|
123
|
+
defaultExpanded?: string | string[];
|
|
124
|
+
/** Controlled expanded values */
|
|
125
|
+
expandedValues?: Set<string>;
|
|
126
|
+
/** Controlled callback when expanded values change */
|
|
127
|
+
onExpandedChange?: (values: Set<string>) => void;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
|
|
131
|
+
(
|
|
132
|
+
{
|
|
133
|
+
allowMultiple = false,
|
|
134
|
+
defaultExpanded,
|
|
135
|
+
expandedValues: controlledExpandedValues,
|
|
136
|
+
onExpandedChange,
|
|
137
|
+
children,
|
|
138
|
+
className,
|
|
139
|
+
...props
|
|
140
|
+
},
|
|
141
|
+
ref,
|
|
142
|
+
) => {
|
|
143
|
+
// Initialize expanded values from default
|
|
144
|
+
const getInitialExpanded = (): Set<string> => {
|
|
145
|
+
if (!defaultExpanded) return new Set();
|
|
146
|
+
if (Array.isArray(defaultExpanded)) return new Set(defaultExpanded);
|
|
147
|
+
return new Set([defaultExpanded]);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const [uncontrolledExpandedValues, setUncontrolledExpandedValues] =
|
|
151
|
+
React.useState<Set<string>>(getInitialExpanded);
|
|
152
|
+
const [isAllExpanded, setIsAllExpanded] = React.useState(false);
|
|
153
|
+
|
|
154
|
+
// Determine if controlled
|
|
155
|
+
const isControlled = controlledExpandedValues !== undefined;
|
|
156
|
+
const expandedValues = isControlled
|
|
157
|
+
? controlledExpandedValues
|
|
158
|
+
: uncontrolledExpandedValues;
|
|
159
|
+
|
|
160
|
+
// Check if any item is expanded
|
|
161
|
+
const hasAnyExpanded = React.useMemo(
|
|
162
|
+
() => isAllExpanded || expandedValues.size > 0,
|
|
163
|
+
[isAllExpanded, expandedValues],
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Check if a specific item is expanded
|
|
167
|
+
const isExpanded = React.useCallback(
|
|
168
|
+
(value: string): boolean => isAllExpanded || expandedValues.has(value),
|
|
169
|
+
[isAllExpanded, expandedValues],
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Toggle a specific item
|
|
173
|
+
const toggle = React.useCallback(
|
|
174
|
+
(value: string) => {
|
|
175
|
+
const newValues = new Set(expandedValues);
|
|
176
|
+
if (newValues.has(value)) {
|
|
177
|
+
newValues.delete(value);
|
|
178
|
+
} else {
|
|
179
|
+
if (!allowMultiple) {
|
|
180
|
+
newValues.clear(); // Accordion mode: close all others
|
|
181
|
+
}
|
|
182
|
+
newValues.add(value);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!isControlled) {
|
|
186
|
+
setUncontrolledExpandedValues(newValues);
|
|
187
|
+
}
|
|
188
|
+
onExpandedChange?.(newValues);
|
|
189
|
+
setIsAllExpanded(false);
|
|
190
|
+
},
|
|
191
|
+
[allowMultiple, expandedValues, isControlled, onExpandedChange],
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// Expand all items
|
|
195
|
+
const expandAll = React.useCallback(() => {
|
|
196
|
+
setIsAllExpanded(true);
|
|
197
|
+
const emptySet = new Set<string>();
|
|
198
|
+
if (!isControlled) {
|
|
199
|
+
setUncontrolledExpandedValues(emptySet);
|
|
200
|
+
}
|
|
201
|
+
onExpandedChange?.(emptySet);
|
|
202
|
+
}, [isControlled, onExpandedChange]);
|
|
203
|
+
|
|
204
|
+
// Collapse all items
|
|
205
|
+
const collapseAll = React.useCallback(() => {
|
|
206
|
+
setIsAllExpanded(false);
|
|
207
|
+
const emptySet = new Set<string>();
|
|
208
|
+
if (!isControlled) {
|
|
209
|
+
setUncontrolledExpandedValues(emptySet);
|
|
210
|
+
}
|
|
211
|
+
onExpandedChange?.(emptySet);
|
|
212
|
+
}, [isControlled, onExpandedChange]);
|
|
213
|
+
|
|
214
|
+
const contextValue = React.useMemo(
|
|
215
|
+
() => ({
|
|
216
|
+
expandedValues,
|
|
217
|
+
isExpanded,
|
|
218
|
+
toggle,
|
|
219
|
+
expandAll,
|
|
220
|
+
collapseAll,
|
|
221
|
+
hasAnyExpanded,
|
|
222
|
+
allowMultiple,
|
|
223
|
+
}),
|
|
224
|
+
[
|
|
225
|
+
expandedValues,
|
|
226
|
+
isExpanded,
|
|
227
|
+
toggle,
|
|
228
|
+
expandAll,
|
|
229
|
+
collapseAll,
|
|
230
|
+
hasAnyExpanded,
|
|
231
|
+
allowMultiple,
|
|
232
|
+
],
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<AccordionContext.Provider value={contextValue}>
|
|
237
|
+
<div ref={ref} className={cn(className)} {...props}>
|
|
238
|
+
{children}
|
|
239
|
+
</div>
|
|
240
|
+
</AccordionContext.Provider>
|
|
241
|
+
);
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
Accordion.displayName = "Accordion";
|
|
246
|
+
|
|
247
|
+
// AccordionSectionHeader
|
|
248
|
+
interface AccordionSectionHeaderProps {
|
|
249
|
+
/** Header title text */
|
|
250
|
+
title: string;
|
|
251
|
+
/** Whether any items are expanded */
|
|
252
|
+
hasExpanded?: boolean;
|
|
253
|
+
/** Toggle all callback */
|
|
254
|
+
onToggleAll?: () => void;
|
|
255
|
+
/** Optional className for the container */
|
|
256
|
+
className?: string;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const AccordionSectionHeader = React.forwardRef<
|
|
260
|
+
HTMLDivElement,
|
|
261
|
+
AccordionSectionHeaderProps
|
|
262
|
+
>(({ title, hasExpanded, onToggleAll, className }, ref) => {
|
|
263
|
+
return (
|
|
264
|
+
<div ref={ref} className={cn(className)}>
|
|
265
|
+
<h2>{title}</h2>
|
|
266
|
+
|
|
267
|
+
{onToggleAll && (
|
|
268
|
+
<motion.div
|
|
269
|
+
animate={{ rotate: hasExpanded ? 180 : 0 }}
|
|
270
|
+
transition={{ duration: 0.2 }}
|
|
271
|
+
>
|
|
272
|
+
<Button size="icon" onClick={onToggleAll}>
|
|
273
|
+
<UtilityChevronDown />
|
|
274
|
+
</Button>
|
|
275
|
+
</motion.div>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
AccordionSectionHeader.displayName = "AccordionSectionHeader";
|
|
282
|
+
|
|
283
|
+
// AccordionHeaderRow (inside Accordion with context access)
|
|
284
|
+
interface AccordionHeaderRowProps {
|
|
285
|
+
/** Header title text */
|
|
286
|
+
title: string;
|
|
287
|
+
/** Optional className for the container */
|
|
288
|
+
className?: string;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const AccordionHeaderRow = React.forwardRef<
|
|
292
|
+
HTMLDivElement,
|
|
293
|
+
AccordionHeaderRowProps
|
|
294
|
+
>(({ title, className }, ref) => {
|
|
295
|
+
const { hasAnyExpanded, expandAll, collapseAll } = useAccordion();
|
|
296
|
+
|
|
297
|
+
const handleToggleAll = React.useCallback(() => {
|
|
298
|
+
if (hasAnyExpanded) {
|
|
299
|
+
collapseAll();
|
|
300
|
+
} else {
|
|
301
|
+
expandAll();
|
|
302
|
+
}
|
|
303
|
+
}, [hasAnyExpanded, collapseAll, expandAll]);
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<div ref={ref} className={cn(className)}>
|
|
307
|
+
<h2>{title}</h2>
|
|
308
|
+
<motion.div
|
|
309
|
+
animate={{ rotate: hasAnyExpanded ? 180 : 0 }}
|
|
310
|
+
transition={{ duration: 0.2 }}
|
|
311
|
+
style={{ willChange: "transform" }}
|
|
312
|
+
>
|
|
313
|
+
<Button size="icon" onClick={handleToggleAll}>
|
|
314
|
+
<UtilityChevronDown />
|
|
315
|
+
</Button>
|
|
316
|
+
</motion.div>
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
AccordionHeaderRow.displayName = "AccordionHeaderRow";
|
|
322
|
+
|
|
323
|
+
// AccordionItem
|
|
324
|
+
interface AccordionItemProps extends React.ComponentPropsWithoutRef<"div"> {
|
|
325
|
+
/** Unique value for this item */
|
|
326
|
+
value: string;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const AccordionItem = React.forwardRef<HTMLDivElement, AccordionItemProps>(
|
|
330
|
+
({ value, children, className, ...props }, ref) => {
|
|
331
|
+
const { isExpanded: isExpandedFn } = useAccordion();
|
|
332
|
+
const isExpanded = isExpandedFn(value);
|
|
333
|
+
|
|
334
|
+
const itemContextValue = React.useMemo(
|
|
335
|
+
() => ({ value, isExpanded }),
|
|
336
|
+
[value, isExpanded],
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<AccordionItemContext.Provider value={itemContextValue}>
|
|
341
|
+
<div ref={ref} className={cn(className)} {...props}>
|
|
342
|
+
{children}
|
|
343
|
+
</div>
|
|
344
|
+
</AccordionItemContext.Provider>
|
|
345
|
+
);
|
|
346
|
+
},
|
|
347
|
+
);
|
|
6
348
|
|
|
7
|
-
const Accordion = AccordionPrimitive.Root;
|
|
8
|
-
|
|
9
|
-
const AccordionItem = React.forwardRef<
|
|
10
|
-
React.ElementRef<typeof AccordionPrimitive.Item>,
|
|
11
|
-
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
|
12
|
-
>(({ className, ...props }, ref) => (
|
|
13
|
-
<AccordionPrimitive.Item
|
|
14
|
-
ref={ref}
|
|
15
|
-
className={cn("accordion-item", className)}
|
|
16
|
-
{...props}
|
|
17
|
-
/>
|
|
18
|
-
));
|
|
19
349
|
AccordionItem.displayName = "AccordionItem";
|
|
20
350
|
|
|
351
|
+
// AccordionHeader
|
|
352
|
+
type AccordionHeaderProps = React.ComponentPropsWithoutRef<"div">;
|
|
353
|
+
|
|
354
|
+
const AccordionHeader = React.forwardRef<HTMLDivElement, AccordionHeaderProps>(
|
|
355
|
+
({ children, className, onClick, ...props }, ref) => {
|
|
356
|
+
return (
|
|
357
|
+
<div ref={ref} onClick={onClick} className={cn(className)} {...props}>
|
|
358
|
+
{children}
|
|
359
|
+
</div>
|
|
360
|
+
);
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
AccordionHeader.displayName = "AccordionHeader";
|
|
365
|
+
|
|
366
|
+
// AccordionTrigger (Chevron Button)
|
|
367
|
+
type AccordionTriggerProps = React.ComponentPropsWithoutRef<"button">;
|
|
368
|
+
|
|
21
369
|
const AccordionTrigger = React.forwardRef<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
>(({ className,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
370
|
+
HTMLButtonElement,
|
|
371
|
+
AccordionTriggerProps
|
|
372
|
+
>(({ className, onClick, ...props }, ref) => {
|
|
373
|
+
const { toggle } = useAccordion();
|
|
374
|
+
const { value, isExpanded } = useAccordionItem();
|
|
375
|
+
|
|
376
|
+
const handleClick = React.useCallback(
|
|
377
|
+
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
378
|
+
e.stopPropagation(); // Prevent header click
|
|
379
|
+
toggle(value);
|
|
380
|
+
onClick?.(e);
|
|
381
|
+
},
|
|
382
|
+
[toggle, value, onClick],
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
return (
|
|
386
|
+
<motion.div
|
|
387
|
+
animate={{ rotate: isExpanded ? 180 : 0 }}
|
|
388
|
+
transition={{ duration: 0.2 }}
|
|
389
|
+
style={{ willChange: "transform" }}
|
|
30
390
|
>
|
|
391
|
+
<Button
|
|
392
|
+
ref={ref}
|
|
393
|
+
size="icon"
|
|
394
|
+
variant="clear"
|
|
395
|
+
onClick={handleClick}
|
|
396
|
+
className={cn(className)}
|
|
397
|
+
{...props}
|
|
398
|
+
>
|
|
399
|
+
<UtilityChevronDown />
|
|
400
|
+
</Button>
|
|
401
|
+
</motion.div>
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
AccordionTrigger.displayName = "AccordionTrigger";
|
|
406
|
+
|
|
407
|
+
// AccordionContent (Expandable Content Area)
|
|
408
|
+
type AccordionContentProps = React.ComponentPropsWithoutRef<"div">;
|
|
409
|
+
|
|
410
|
+
const AccordionContent = React.forwardRef<HTMLDivElement, AccordionContentProps>(
|
|
411
|
+
({ children, className, ...props }, ref) => {
|
|
412
|
+
const { isExpanded } = useAccordionItem();
|
|
413
|
+
|
|
414
|
+
return (
|
|
415
|
+
<AnimatePresence initial={false}>
|
|
416
|
+
{isExpanded && (
|
|
417
|
+
<motion.div
|
|
418
|
+
initial={{ height: 0, opacity: 0 }}
|
|
419
|
+
animate={{ height: "auto", opacity: 1 }}
|
|
420
|
+
exit={{ height: 0, opacity: 0 }}
|
|
421
|
+
transition={{ duration: 0.3, ease: [0.25, 0.46, 0.45, 0.94] }}
|
|
422
|
+
className={cn(className)}
|
|
423
|
+
style={{ willChange: "opacity" }}
|
|
424
|
+
>
|
|
425
|
+
<div ref={ref} {...props}>
|
|
426
|
+
{children}
|
|
427
|
+
</div>
|
|
428
|
+
</motion.div>
|
|
429
|
+
)}
|
|
430
|
+
</AnimatePresence>
|
|
431
|
+
);
|
|
432
|
+
},
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
AccordionContent.displayName = "AccordionContent";
|
|
436
|
+
|
|
437
|
+
// AccordionListRow (for nested items)
|
|
438
|
+
interface AccordionListRowProps extends React.ComponentPropsWithoutRef<"div"> {
|
|
439
|
+
/** Optional icon element */
|
|
440
|
+
icon?: React.ReactNode;
|
|
441
|
+
/** Optional badge element */
|
|
442
|
+
badge?: React.ReactNode;
|
|
443
|
+
/** Row title */
|
|
444
|
+
title: string;
|
|
445
|
+
/** Optional title className */
|
|
446
|
+
titleClassName?: string;
|
|
447
|
+
/** Optional right-side content */
|
|
448
|
+
rightContent?: React.ReactNode;
|
|
449
|
+
/** Visual variant */
|
|
450
|
+
variant?: "default" | "nested";
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const AccordionListRow = React.forwardRef<HTMLDivElement, AccordionListRowProps>(
|
|
454
|
+
(
|
|
455
|
+
{
|
|
456
|
+
icon,
|
|
457
|
+
badge,
|
|
458
|
+
title,
|
|
459
|
+
titleClassName,
|
|
460
|
+
rightContent,
|
|
461
|
+
onClick,
|
|
462
|
+
className,
|
|
463
|
+
variant = "default",
|
|
464
|
+
...props
|
|
465
|
+
},
|
|
466
|
+
ref,
|
|
467
|
+
) => {
|
|
468
|
+
return (
|
|
469
|
+
<div ref={ref} onClick={onClick} className={cn(className)} {...props}>
|
|
470
|
+
{icon}
|
|
471
|
+
{badge}
|
|
472
|
+
<span className={cn(titleClassName)}>{title}</span>
|
|
473
|
+
{rightContent}
|
|
474
|
+
</div>
|
|
475
|
+
);
|
|
476
|
+
},
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
AccordionListRow.displayName = "AccordionListRow";
|
|
480
|
+
|
|
481
|
+
type AccordionContainerProps = React.ComponentPropsWithoutRef<"section">;
|
|
482
|
+
|
|
483
|
+
const AccordionContainer = React.forwardRef<
|
|
484
|
+
HTMLElement,
|
|
485
|
+
AccordionContainerProps
|
|
486
|
+
>(({ children, className, ...props }, ref) => {
|
|
487
|
+
return (
|
|
488
|
+
<section ref={ref} className={cn(className)} {...props}>
|
|
31
489
|
{children}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
|
490
|
+
</section>
|
|
491
|
+
);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
AccordionContainer.displayName = "AccordionContainer";
|
|
495
|
+
|
|
496
|
+
export {
|
|
497
|
+
useAccordionState,
|
|
498
|
+
useAccordion,
|
|
499
|
+
useAccordionItem,
|
|
500
|
+
Accordion,
|
|
501
|
+
AccordionSectionHeader,
|
|
502
|
+
AccordionHeaderRow,
|
|
503
|
+
AccordionItem,
|
|
504
|
+
AccordionHeader,
|
|
505
|
+
AccordionTrigger,
|
|
506
|
+
AccordionContent,
|
|
507
|
+
AccordionListRow,
|
|
508
|
+
AccordionContainer,
|
|
509
|
+
};
|
|
@@ -13,10 +13,10 @@ const Label = React.forwardRef<
|
|
|
13
13
|
>(({ className, children, ...props }, ref) => (
|
|
14
14
|
<LabelPrimitive.Root
|
|
15
15
|
ref={ref}
|
|
16
|
-
className={cn(labelVariants(),
|
|
16
|
+
className={cn(labelVariants(), className)}
|
|
17
17
|
{...props}
|
|
18
18
|
>
|
|
19
|
-
{children}
|
|
19
|
+
<div className="overline-medium"> {children}</div>
|
|
20
20
|
</LabelPrimitive.Root>
|
|
21
21
|
));
|
|
22
22
|
Label.displayName = LabelPrimitive.Root.displayName;
|
|
@@ -2,44 +2,53 @@ import * as React from "react";
|
|
|
2
2
|
import { cn } from "@/lib/utils";
|
|
3
3
|
|
|
4
4
|
export type TagStatus = "todo" | "low" | "medium" | "high" | "done";
|
|
5
|
+
export type TagVariant = "text" | "code";
|
|
5
6
|
|
|
6
7
|
export interface TagProps
|
|
7
8
|
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
8
9
|
children: React.ReactNode;
|
|
10
|
+
/** Visual variant - "text" for normal tags, "code" for code badges */
|
|
11
|
+
variant?: TagVariant;
|
|
12
|
+
/** Active state for toggle behavior in forms/filters */
|
|
9
13
|
active?: boolean;
|
|
14
|
+
/** Status for status-specific styling (overrides variant) */
|
|
10
15
|
status?: TagStatus;
|
|
11
|
-
|
|
16
|
+
/** Color scheme for status tags */
|
|
17
|
+
colorScheme?: "light" | "dark";
|
|
12
18
|
onClick?: () => void;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
const getStatusClass = (
|
|
16
22
|
status: TagStatus,
|
|
17
|
-
|
|
23
|
+
colorScheme: "light" | "dark" = "light",
|
|
18
24
|
) => {
|
|
19
|
-
return `status-tag tag--${
|
|
25
|
+
return `status-tag tag--${colorScheme}-${status}`;
|
|
20
26
|
};
|
|
21
27
|
|
|
22
28
|
export const Tag = React.forwardRef<HTMLButtonElement, TagProps>(
|
|
23
29
|
(
|
|
24
30
|
{
|
|
25
31
|
children,
|
|
32
|
+
variant = "text",
|
|
26
33
|
active = false,
|
|
27
34
|
status,
|
|
28
|
-
|
|
35
|
+
colorScheme = "light",
|
|
29
36
|
className,
|
|
37
|
+
style,
|
|
30
38
|
onClick,
|
|
31
39
|
...props
|
|
32
40
|
},
|
|
33
41
|
ref,
|
|
34
42
|
) => {
|
|
35
|
-
// Status tag styling
|
|
43
|
+
// Status tag styling (highest priority)
|
|
36
44
|
if (status) {
|
|
37
|
-
const statusClass = getStatusClass(status,
|
|
45
|
+
const statusClass = getStatusClass(status, colorScheme);
|
|
38
46
|
return (
|
|
39
47
|
<button
|
|
40
48
|
ref={ref}
|
|
41
49
|
onClick={onClick}
|
|
42
50
|
className={cn("overline-medium", statusClass, className)}
|
|
51
|
+
style={style}
|
|
43
52
|
{...props}
|
|
44
53
|
>
|
|
45
54
|
{children}
|
|
@@ -47,7 +56,24 @@ export const Tag = React.forwardRef<HTMLButtonElement, TagProps>(
|
|
|
47
56
|
);
|
|
48
57
|
}
|
|
49
58
|
|
|
50
|
-
//
|
|
59
|
+
// Code variant - for code badges like "C1", "A3"
|
|
60
|
+
if (variant === "code") {
|
|
61
|
+
return (
|
|
62
|
+
<button
|
|
63
|
+
ref={ref}
|
|
64
|
+
onClick={onClick}
|
|
65
|
+
className={cn("tag-code", className)}
|
|
66
|
+
style={style}
|
|
67
|
+
{...props}
|
|
68
|
+
>
|
|
69
|
+
<span className="number-small font-semibold" style={{ color: "inherit" }}>
|
|
70
|
+
{children}
|
|
71
|
+
</span>
|
|
72
|
+
</button>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Text variant (default) - general purpose tag for forms/filters
|
|
51
77
|
return (
|
|
52
78
|
<button
|
|
53
79
|
ref={ref}
|
|
@@ -57,6 +83,7 @@ export const Tag = React.forwardRef<HTMLButtonElement, TagProps>(
|
|
|
57
83
|
active ? "tag-general--active" : "tag-general--inactive",
|
|
58
84
|
className,
|
|
59
85
|
)}
|
|
86
|
+
style={style}
|
|
60
87
|
{...props}
|
|
61
88
|
>
|
|
62
89
|
{children}
|
package/client/global.css
CHANGED
|
@@ -1780,6 +1780,34 @@ body {
|
|
|
1780
1780
|
background-color: var(--color-blue-200, #cbe0f1);
|
|
1781
1781
|
}
|
|
1782
1782
|
|
|
1783
|
+
/* Tag Code Variant - for code badges like "C1", "A3" */
|
|
1784
|
+
.tag-code {
|
|
1785
|
+
display: flex;
|
|
1786
|
+
align-items: center;
|
|
1787
|
+
justify-content: center;
|
|
1788
|
+
height: 2rem;
|
|
1789
|
+
width: 3rem;
|
|
1790
|
+
border-radius: calc(var(--radius-sm, 6px));
|
|
1791
|
+
flex-shrink: 0;
|
|
1792
|
+
border: 1.5px solid currentColor;
|
|
1793
|
+
padding-left: 1px;
|
|
1794
|
+
padding-right: 1px;
|
|
1795
|
+
background: transparent;
|
|
1796
|
+
cursor: pointer;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
.tag-code:focus-visible {
|
|
1800
|
+
outline: none;
|
|
1801
|
+
box-shadow:
|
|
1802
|
+
0 0 0 2px hsl(var(--ring)),
|
|
1803
|
+
0 0 0 4px var(--color-white, #fff);
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
.tag-code:disabled {
|
|
1807
|
+
opacity: 0.5;
|
|
1808
|
+
cursor: not-allowed;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1783
1811
|
/* Toggle/Segmented Control Styles */
|
|
1784
1812
|
.toggle {
|
|
1785
1813
|
display: inline-flex;
|