@syscore/ui-library 1.4.0 → 1.5.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.
@@ -0,0 +1,145 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { motion, AnimatePresence } from "motion/react";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ // Root Component
8
+ interface PageHeaderProps extends React.ComponentPropsWithoutRef<"header"> {}
9
+
10
+ const PageHeader = React.forwardRef<HTMLElement, PageHeaderProps>(
11
+ ({ children, className, ...props }, ref) => {
12
+ return (
13
+ <header ref={ref} className={cn("page-header", className)} {...props}>
14
+ {children}
15
+ </header>
16
+ );
17
+ }
18
+ );
19
+ PageHeader.displayName = "PageHeader";
20
+
21
+ // TopSection - flex container for badges/actions
22
+ interface PageHeaderTopSectionProps
23
+ extends React.ComponentPropsWithoutRef<"div"> {}
24
+
25
+ const PageHeaderTopSection = React.forwardRef<
26
+ HTMLDivElement,
27
+ PageHeaderTopSectionProps
28
+ >(({ children, className, ...props }, ref) => {
29
+ return (
30
+ <div
31
+ ref={ref}
32
+ className={cn("page-header-top-section", className)}
33
+ {...props}
34
+ >
35
+ {children}
36
+ </div>
37
+ );
38
+ });
39
+ PageHeaderTopSection.displayName = "PageHeaderTopSection";
40
+
41
+ // LeftContent - groups left-side elements
42
+ interface PageHeaderLeftContentProps
43
+ extends React.ComponentPropsWithoutRef<"div"> {}
44
+
45
+ const PageHeaderLeftContent = React.forwardRef<
46
+ HTMLDivElement,
47
+ PageHeaderLeftContentProps
48
+ >(({ children, className, ...props }, ref) => {
49
+ return (
50
+ <div
51
+ ref={ref}
52
+ className={cn("page-header-left-content", className)}
53
+ {...props}
54
+ >
55
+ {children}
56
+ </div>
57
+ );
58
+ });
59
+ PageHeaderLeftContent.displayName = "PageHeaderLeftContent";
60
+
61
+ // Badge - with optional animation
62
+ interface PageHeaderBadgeProps extends React.ComponentPropsWithoutRef<"div"> {
63
+ /** Whether to animate the badge appearance */
64
+ animated?: boolean;
65
+ /** Show badge conditionally */
66
+ show?: boolean;
67
+ }
68
+
69
+ const PageHeaderBadge = React.forwardRef<HTMLDivElement, PageHeaderBadgeProps>(
70
+ ({ children, animated = false, show = true, className, style, ...props }, ref) => {
71
+ if (animated) {
72
+ return (
73
+ <AnimatePresence>
74
+ {show && (
75
+ <motion.div
76
+ initial={{ opacity: 0, scale: 0.8 }}
77
+ animate={{ opacity: 1, scale: 1 }}
78
+ exit={{ opacity: 0, scale: 0.8 }}
79
+ transition={{ duration: 0.2 }}
80
+ className={className}
81
+ style={style}
82
+ >
83
+ {children}
84
+ </motion.div>
85
+ )}
86
+ </AnimatePresence>
87
+ );
88
+ }
89
+
90
+ if (!show) return null;
91
+
92
+ return (
93
+ <div ref={ref} className={className} style={style} {...props}>
94
+ {children}
95
+ </div>
96
+ );
97
+ }
98
+ );
99
+ PageHeaderBadge.displayName = "PageHeaderBadge";
100
+
101
+ // Title
102
+ interface PageHeaderTitleProps
103
+ extends React.ComponentPropsWithoutRef<"h1"> {}
104
+
105
+ const PageHeaderTitle = React.forwardRef<
106
+ HTMLHeadingElement,
107
+ PageHeaderTitleProps
108
+ >(({ children, className, ...props }, ref) => {
109
+ return (
110
+ <h1 ref={ref} className={cn("page-header-title", className)} {...props}>
111
+ {children}
112
+ </h1>
113
+ );
114
+ });
115
+ PageHeaderTitle.displayName = "PageHeaderTitle";
116
+
117
+ // Description
118
+ interface PageHeaderDescriptionProps
119
+ extends React.ComponentPropsWithoutRef<"p"> {}
120
+
121
+ const PageHeaderDescription = React.forwardRef<
122
+ HTMLParagraphElement,
123
+ PageHeaderDescriptionProps
124
+ >(({ children, className, ...props }, ref) => {
125
+ return (
126
+ <p
127
+ ref={ref}
128
+ className={cn("page-header-description", className)}
129
+ {...props}
130
+ >
131
+ {children}
132
+ </p>
133
+ );
134
+ });
135
+ PageHeaderDescription.displayName = "PageHeaderDescription";
136
+
137
+ // Compound exports
138
+ export {
139
+ PageHeader,
140
+ PageHeaderTopSection,
141
+ PageHeaderLeftContent,
142
+ PageHeaderBadge,
143
+ PageHeaderTitle,
144
+ PageHeaderDescription,
145
+ };
@@ -0,0 +1,554 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { motion, AnimatePresence } from "motion/react";
5
+ import { cn } from "@/lib/utils";
6
+ import { Button } from "./button";
7
+ import { UtilityChevronDown } from "../icons/UtilityChevronDown";
8
+
9
+ // Context Definitions
10
+ interface StandardTableContextValue {
11
+ /** Set of currently expanded row values */
12
+ expandedValues: Set<string>;
13
+ /** Check if a row is expanded */
14
+ isExpanded: (value: string) => boolean;
15
+ /** Toggle a specific row's expansion */
16
+ toggle: (value: string) => void;
17
+ /** Expand all rows */
18
+ expandAll: () => void;
19
+ /** Collapse all rows */
20
+ collapseAll: () => void;
21
+ /** Check if any row is expanded */
22
+ hasAnyExpanded: boolean;
23
+ /** Allow multiple rows to be expanded at once */
24
+ allowMultiple: boolean;
25
+ }
26
+
27
+ const StandardTableContext =
28
+ React.createContext<StandardTableContextValue | null>(null);
29
+
30
+ interface StandardTableRowContextValue {
31
+ /** Current row's value */
32
+ value: string;
33
+ /** Whether this row is expanded */
34
+ isExpanded: boolean;
35
+ }
36
+
37
+ const StandardTableRowContext =
38
+ React.createContext<StandardTableRowContextValue | null>(null);
39
+
40
+ // Hooks
41
+ function useStandardTable() {
42
+ const context = React.useContext(StandardTableContext);
43
+ if (!context) {
44
+ throw new Error(
45
+ "StandardTable components must be used within <StandardTable>",
46
+ );
47
+ }
48
+ return context;
49
+ }
50
+
51
+ function useStandardTableRow() {
52
+ const context = React.useContext(StandardTableRowContext);
53
+ if (!context) {
54
+ throw new Error(
55
+ "StandardTableTrigger and StandardTableContent must be used within <StandardTableRow>",
56
+ );
57
+ }
58
+ return context;
59
+ }
60
+
61
+ // useStandardTableState Hook (for external state management)
62
+ interface UseStandardTableStateOptions {
63
+ /** Allow multiple rows to be expanded at once */
64
+ allowMultiple?: boolean;
65
+ /** Initially expanded row value(s) */
66
+ defaultExpanded?: string | string[];
67
+ }
68
+
69
+ interface UseStandardTableStateReturn {
70
+ /** Current expanded values */
71
+ expandedValues: Set<string>;
72
+ /** Set expanded values */
73
+ setExpandedValues: (values: Set<string>) => void;
74
+ /** Check if any row is expanded */
75
+ hasAnyExpanded: boolean;
76
+ /** Toggle all rows */
77
+ toggleAll: () => void;
78
+ }
79
+
80
+ function useStandardTableState(
81
+ options: UseStandardTableStateOptions = {},
82
+ ): UseStandardTableStateReturn {
83
+ const { allowMultiple = false, defaultExpanded } = options;
84
+
85
+ // Initialize expanded values from default
86
+ const getInitialExpanded = (): Set<string> => {
87
+ if (!defaultExpanded) return new Set();
88
+ if (Array.isArray(defaultExpanded)) return new Set(defaultExpanded);
89
+ return new Set([defaultExpanded]);
90
+ };
91
+
92
+ const [expandedValues, setExpandedValues] =
93
+ React.useState<Set<string>>(getInitialExpanded);
94
+ const [isAllExpanded, setIsAllExpanded] = React.useState(false);
95
+
96
+ const hasAnyExpanded = React.useMemo(
97
+ () => isAllExpanded || expandedValues.size > 0,
98
+ [isAllExpanded, expandedValues],
99
+ );
100
+
101
+ const toggleAll = React.useCallback(() => {
102
+ if (hasAnyExpanded) {
103
+ setIsAllExpanded(false);
104
+ setExpandedValues(new Set());
105
+ } else {
106
+ setIsAllExpanded(true);
107
+ setExpandedValues(new Set());
108
+ }
109
+ }, [hasAnyExpanded]);
110
+
111
+ return {
112
+ expandedValues,
113
+ setExpandedValues,
114
+ hasAnyExpanded,
115
+ toggleAll,
116
+ };
117
+ }
118
+
119
+ // StandardTable (Root Component)
120
+ interface StandardTableProps extends React.ComponentPropsWithoutRef<"div"> {
121
+ /** Allow multiple rows to be expanded at once (default: false for accordion behavior) */
122
+ allowMultiple?: boolean;
123
+ /** Initially expanded row value(s) (uncontrolled) */
124
+ defaultExpanded?: string | string[];
125
+ /** Controlled expanded values */
126
+ expandedValues?: Set<string>;
127
+ /** Controlled callback when expanded values change */
128
+ onExpandedChange?: (values: Set<string>) => void;
129
+ }
130
+
131
+ const StandardTable = React.forwardRef<HTMLDivElement, StandardTableProps>(
132
+ (
133
+ {
134
+ allowMultiple = false,
135
+ defaultExpanded,
136
+ expandedValues: controlledExpandedValues,
137
+ onExpandedChange,
138
+ children,
139
+ className,
140
+ ...props
141
+ },
142
+ ref,
143
+ ) => {
144
+ // Initialize expanded values from default
145
+ const getInitialExpanded = (): Set<string> => {
146
+ if (!defaultExpanded) return new Set();
147
+ if (Array.isArray(defaultExpanded)) return new Set(defaultExpanded);
148
+ return new Set([defaultExpanded]);
149
+ };
150
+
151
+ const [uncontrolledExpandedValues, setUncontrolledExpandedValues] =
152
+ React.useState<Set<string>>(getInitialExpanded);
153
+ const [isAllExpanded, setIsAllExpanded] = React.useState(false);
154
+
155
+ // Determine if controlled
156
+ const isControlled = controlledExpandedValues !== undefined;
157
+ const expandedValues = isControlled
158
+ ? controlledExpandedValues
159
+ : uncontrolledExpandedValues;
160
+
161
+ // Check if any row is expanded
162
+ const hasAnyExpanded = React.useMemo(
163
+ () => isAllExpanded || expandedValues.size > 0,
164
+ [isAllExpanded, expandedValues],
165
+ );
166
+
167
+ // Check if a specific row is expanded
168
+ const isExpanded = React.useCallback(
169
+ (value: string): boolean => isAllExpanded || expandedValues.has(value),
170
+ [isAllExpanded, expandedValues],
171
+ );
172
+
173
+ // Toggle a specific row
174
+ const toggle = React.useCallback(
175
+ (value: string) => {
176
+ const newValues = new Set(expandedValues);
177
+ if (newValues.has(value)) {
178
+ newValues.delete(value);
179
+ } else {
180
+ if (!allowMultiple) {
181
+ newValues.clear(); // Accordion mode: close all others
182
+ }
183
+ newValues.add(value);
184
+ }
185
+
186
+ if (!isControlled) {
187
+ setUncontrolledExpandedValues(newValues);
188
+ }
189
+ onExpandedChange?.(newValues);
190
+ setIsAllExpanded(false);
191
+ },
192
+ [allowMultiple, expandedValues, isControlled, onExpandedChange],
193
+ );
194
+
195
+ // Expand all rows
196
+ const expandAll = React.useCallback(() => {
197
+ setIsAllExpanded(true);
198
+ const emptySet = new Set<string>();
199
+ if (!isControlled) {
200
+ setUncontrolledExpandedValues(emptySet);
201
+ }
202
+ onExpandedChange?.(emptySet);
203
+ }, [isControlled, onExpandedChange]);
204
+
205
+ // Collapse all rows
206
+ const collapseAll = React.useCallback(() => {
207
+ setIsAllExpanded(false);
208
+ const emptySet = new Set<string>();
209
+ if (!isControlled) {
210
+ setUncontrolledExpandedValues(emptySet);
211
+ }
212
+ onExpandedChange?.(emptySet);
213
+ }, [isControlled, onExpandedChange]);
214
+
215
+ const contextValue = React.useMemo(
216
+ () => ({
217
+ expandedValues,
218
+ isExpanded,
219
+ toggle,
220
+ expandAll,
221
+ collapseAll,
222
+ hasAnyExpanded,
223
+ allowMultiple,
224
+ }),
225
+ [
226
+ expandedValues,
227
+ isExpanded,
228
+ toggle,
229
+ expandAll,
230
+ collapseAll,
231
+ hasAnyExpanded,
232
+ allowMultiple,
233
+ ],
234
+ );
235
+
236
+ return (
237
+ <StandardTableContext.Provider value={contextValue}>
238
+ <div
239
+ ref={ref}
240
+ className={cn(
241
+ "border border-blue-200 rounded-xl overflow-hidden",
242
+ className,
243
+ )}
244
+ {...props}
245
+ >
246
+ {children}
247
+ </div>
248
+ </StandardTableContext.Provider>
249
+ );
250
+ },
251
+ );
252
+
253
+ StandardTable.displayName = "StandardTable";
254
+
255
+ // StandardTableHeader
256
+ interface StandardTableHeaderProps {
257
+ /** Header title text */
258
+ title: string;
259
+ /** Whether any items are expanded */
260
+ hasExpanded?: boolean;
261
+ /** Toggle all callback */
262
+ onToggleAll?: () => void;
263
+ /** Optional className for the container */
264
+ className?: string;
265
+ }
266
+
267
+ const StandardTableHeader = React.forwardRef<
268
+ HTMLDivElement,
269
+ StandardTableHeaderProps
270
+ >(({ title, hasExpanded, onToggleAll, className }, ref) => {
271
+ return (
272
+ <div ref={ref} className={cn("standard-table-header", className)}>
273
+ <h2 className="standard-table-header__title body-large">{title}</h2>
274
+
275
+ {onToggleAll && (
276
+ <motion.div
277
+ animate={{ rotate: hasExpanded ? 180 : 0 }}
278
+ transition={{ duration: 0.2 }}
279
+ className="mx-6"
280
+ >
281
+ <Button size="icon" onClick={onToggleAll}>
282
+ <UtilityChevronDown />
283
+ </Button>
284
+ </motion.div>
285
+ )}
286
+ </div>
287
+ );
288
+ });
289
+
290
+ StandardTableHeader.displayName = "StandardTableHeader";
291
+
292
+ // StandardTableHeaderRow (inside StandardTable with context access)
293
+ interface StandardTableHeaderRowProps {
294
+ /** Header title text */
295
+ title: string;
296
+ /** Optional className for the container */
297
+ className?: string;
298
+ }
299
+
300
+ const StandardTableHeaderRow = React.forwardRef<
301
+ HTMLDivElement,
302
+ StandardTableHeaderRowProps
303
+ >(({ title, className }, ref) => {
304
+ const { hasAnyExpanded, expandAll, collapseAll } = useStandardTable();
305
+
306
+ const handleToggleAll = React.useCallback(() => {
307
+ if (hasAnyExpanded) {
308
+ collapseAll();
309
+ } else {
310
+ expandAll();
311
+ }
312
+ }, [hasAnyExpanded, collapseAll, expandAll]);
313
+
314
+ return (
315
+ <div ref={ref} className={cn("standard-table-header", className)}>
316
+ <h2 className="standard-table-header__title body-large">{title}</h2>
317
+ <motion.div
318
+ animate={{ rotate: hasAnyExpanded ? 180 : 0 }}
319
+ transition={{ duration: 0.2 }}
320
+ style={{ willChange: "transform" }}
321
+ >
322
+ <Button size="icon" onClick={handleToggleAll}>
323
+ <UtilityChevronDown />
324
+ </Button>
325
+ </motion.div>
326
+ </div>
327
+ );
328
+ });
329
+
330
+ StandardTableHeaderRow.displayName = "StandardTableHeaderRow";
331
+
332
+ // StandardTableRow
333
+ interface StandardTableRowProps extends React.ComponentPropsWithoutRef<"div"> {
334
+ /** Unique value for this row */
335
+ value: string;
336
+ }
337
+
338
+ const StandardTableRow = React.forwardRef<
339
+ HTMLDivElement,
340
+ StandardTableRowProps
341
+ >(({ value, children, className, ...props }, ref) => {
342
+ const { isExpanded: isExpandedFn } = useStandardTable();
343
+ const isExpanded = isExpandedFn(value);
344
+
345
+ const rowContextValue = React.useMemo(
346
+ () => ({ value, isExpanded }),
347
+ [value, isExpanded],
348
+ );
349
+
350
+ return (
351
+ <StandardTableRowContext.Provider value={rowContextValue}>
352
+ <div ref={ref} className={cn("standard-table-row", className)} {...props}>
353
+ {children}
354
+ </div>
355
+ </StandardTableRowContext.Provider>
356
+ );
357
+ });
358
+
359
+ StandardTableRow.displayName = "StandardTableRow";
360
+
361
+ // StandardTableRowHeader
362
+ type StandardTableRowHeaderProps = React.ComponentPropsWithoutRef<"div">;
363
+
364
+ const StandardTableRowHeader = React.forwardRef<
365
+ HTMLDivElement,
366
+ StandardTableRowHeaderProps
367
+ >(({ children, className, onClick, ...props }, ref) => {
368
+ return (
369
+ <div
370
+ ref={ref}
371
+ onClick={onClick}
372
+ className={cn("standard-table-row-header", className)}
373
+ {...props}
374
+ >
375
+ {children}
376
+ </div>
377
+ );
378
+ });
379
+
380
+ StandardTableRowHeader.displayName = "StandardTableRowHeader";
381
+
382
+ // StandardTableTrigger (Chevron Button)
383
+ type StandardTableTriggerProps = React.ComponentPropsWithoutRef<"button">;
384
+
385
+ const StandardTableTrigger = React.forwardRef<
386
+ HTMLButtonElement,
387
+ StandardTableTriggerProps
388
+ >(({ className, onClick, ...props }, ref) => {
389
+ const { toggle } = useStandardTable();
390
+ const { value, isExpanded } = useStandardTableRow();
391
+
392
+ const handleClick = React.useCallback(
393
+ (e: React.MouseEvent<HTMLButtonElement>) => {
394
+ e.stopPropagation(); // Prevent row header click
395
+ toggle(value);
396
+ onClick?.(e);
397
+ },
398
+ [toggle, value, onClick],
399
+ );
400
+
401
+ return (
402
+ <motion.div
403
+ animate={{ rotate: isExpanded ? 180 : 0 }}
404
+ transition={{ duration: 0.2 }}
405
+ style={{ willChange: "transform" }}
406
+ >
407
+ <Button
408
+ ref={ref}
409
+ size="icon"
410
+ variant="clear"
411
+ onClick={handleClick}
412
+ className={cn("standard-table-trigger", className)}
413
+ {...props}
414
+ >
415
+ <UtilityChevronDown />
416
+ </Button>
417
+ </motion.div>
418
+ );
419
+ });
420
+
421
+ StandardTableTrigger.displayName = "StandardTableTrigger";
422
+
423
+ // StandardTableContent (Expandable Content Area)
424
+ type StandardTableContentProps = React.ComponentPropsWithoutRef<"div">;
425
+
426
+ const StandardTableContent = React.forwardRef<
427
+ HTMLDivElement,
428
+ StandardTableContentProps
429
+ >(({ children, className, ...props }, ref) => {
430
+ const { isExpanded } = useStandardTableRow();
431
+
432
+ return (
433
+ <AnimatePresence initial={false}>
434
+ {isExpanded && (
435
+ <motion.div
436
+ initial={{ height: 0, opacity: 0 }}
437
+ animate={{ height: "auto", opacity: 1 }}
438
+ exit={{ height: 0, opacity: 0 }}
439
+ transition={{ duration: 0.3, ease: [0.25, 0.46, 0.45, 0.94] }}
440
+ className={cn("standard-table-content", className)}
441
+ style={{ willChange: "opacity" }}
442
+ >
443
+ <div
444
+ ref={ref}
445
+ className="standard-table-content__inner"
446
+ {...props}
447
+ >
448
+ {children}
449
+ </div>
450
+ </motion.div>
451
+ )}
452
+ </AnimatePresence>
453
+ );
454
+ });
455
+
456
+ StandardTableContent.displayName = "StandardTableContent";
457
+
458
+ // StandardTableListRow (for nested items like ListRow)
459
+ interface StandardTableListRowProps
460
+ extends React.ComponentPropsWithoutRef<"div"> {
461
+ /** Optional icon element */
462
+ icon?: React.ReactNode;
463
+ /** Optional badge element */
464
+ badge?: React.ReactNode;
465
+ /** Row title */
466
+ title: string;
467
+ /** Optional title className */
468
+ titleClassName?: string;
469
+ /** Optional right-side content */
470
+ rightContent?: React.ReactNode;
471
+ /** Visual variant */
472
+ variant?: "default" | "nested";
473
+ }
474
+
475
+ const StandardTableListRow = React.forwardRef<
476
+ HTMLDivElement,
477
+ StandardTableListRowProps
478
+ >(
479
+ (
480
+ {
481
+ icon,
482
+ badge,
483
+ title,
484
+ titleClassName,
485
+ rightContent,
486
+ onClick,
487
+ className,
488
+ variant = "default",
489
+ ...props
490
+ },
491
+ ref,
492
+ ) => {
493
+ return (
494
+ <div
495
+ ref={ref}
496
+ onClick={onClick}
497
+ className={cn(
498
+ "standard-table-list-row",
499
+ variant === "default"
500
+ ? "standard-table-list-row--default"
501
+ : "standard-table-list-row--nested",
502
+ className,
503
+ )}
504
+ {...props}
505
+ >
506
+ {icon}
507
+ {badge}
508
+ <span
509
+ className={cn(
510
+ "standard-table-list-row__title",
511
+ titleClassName || "body-large",
512
+ )}
513
+ >
514
+ {title}
515
+ </span>
516
+ {rightContent}
517
+ </div>
518
+ );
519
+ },
520
+ );
521
+
522
+ StandardTableListRow.displayName = "StandardTableListRow";
523
+
524
+ type StandardTableContainerProps = React.ComponentPropsWithoutRef<"section">;
525
+
526
+ const StandardTableContainer = React.forwardRef<
527
+ HTMLElement,
528
+ StandardTableContainerProps
529
+ >(({ children, className, ...props }, ref) => {
530
+ return (
531
+ <section
532
+ ref={ref}
533
+ className={cn("standard-table-container", className)}
534
+ {...props}
535
+ >
536
+ {children}
537
+ </section>
538
+ );
539
+ });
540
+
541
+ StandardTableContainer.displayName = "StandardTableContainer";
542
+
543
+ export {
544
+ useStandardTableState,
545
+ StandardTable,
546
+ StandardTableHeader,
547
+ StandardTableHeaderRow,
548
+ StandardTableRow,
549
+ StandardTableRowHeader,
550
+ StandardTableTrigger,
551
+ StandardTableContent,
552
+ StandardTableListRow,
553
+ StandardTableContainer,
554
+ };