@fragments-sdk/ui 0.7.5 → 0.8.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/README.md +58 -25
- package/fragments.json +1 -1
- package/package.json +15 -5
- package/src/blocks/AppShell.block.ts +2 -2
- package/src/blocks/InsetDashboardLayout.block.ts +1 -1
- package/src/blocks/LoginForm.block.ts +14 -7
- package/src/components/Accordion/Accordion.fragment.tsx +8 -2
- package/src/components/Alert/Alert.module.scss +4 -4
- package/src/components/AppShell/AppShell.fragment.tsx +1 -1
- package/src/components/AppShell/index.tsx +2 -0
- package/src/components/Avatar/Avatar.fragment.tsx +5 -1
- package/src/components/Avatar/Avatar.module.scss +1 -1
- package/src/components/Avatar/index.tsx +37 -1
- package/src/components/Badge/Badge.fragment.tsx +3 -3
- package/src/components/Badge/Badge.module.scss +4 -4
- package/src/components/Badge/index.tsx +5 -1
- package/src/components/Box/index.tsx +5 -1
- package/src/components/Button/Button.fragment.tsx +17 -16
- package/src/components/Button/index.tsx +5 -1
- package/src/components/ButtonGroup/index.tsx +5 -1
- package/src/components/Card/Card.fragment.tsx +5 -5
- package/src/components/Chart/Chart.fragment.tsx +9 -1
- package/src/components/Chart/index.tsx +22 -4
- package/src/components/Checkbox/index.tsx +5 -1
- package/src/components/Chip/Chip.fragment.tsx +0 -5
- package/src/components/Chip/Chip.module.scss +2 -2
- package/src/components/CodeBlock/CodeBlock.fragment.tsx +9 -3
- package/src/components/CodeBlock/CodeBlock.module.scss +1 -1
- package/src/components/ColorPicker/index.tsx +5 -1
- package/src/components/Combobox/Combobox.fragment.tsx +15 -7
- package/src/components/ConversationList/ConversationList.fragment.tsx +3 -3
- package/src/components/ConversationList/ConversationList.module.scss +1 -1
- package/src/components/DatePicker/DatePicker.fragment.tsx +245 -0
- package/src/components/DatePicker/DatePicker.module.scss +394 -0
- package/src/components/DatePicker/DatePicker.test.tsx +264 -0
- package/src/components/DatePicker/index.tsx +535 -0
- package/src/components/Field/Field.fragment.tsx +5 -4
- package/src/components/Fieldset/Fieldset.fragment.tsx +5 -4
- package/src/components/Form/Form.fragment.tsx +9 -3
- package/src/components/Form/index.tsx +5 -1
- package/src/components/Grid/Grid.fragment.tsx +4 -0
- package/src/components/Header/Header.fragment.tsx +36 -13
- package/src/components/Header/Header.module.scss +114 -1
- package/src/components/Header/Header.test.tsx +106 -1
- package/src/components/Header/index.tsx +100 -31
- package/src/components/Icon/Icon.fragment.tsx +6 -1
- package/src/components/Icon/index.tsx +5 -1
- package/src/components/Image/Image.fragment.tsx +2 -2
- package/src/components/Image/index.tsx +5 -1
- package/src/components/Input/Input.fragment.tsx +21 -3
- package/src/components/Input/Input.module.scss +1 -1
- package/src/components/Input/index.tsx +5 -1
- package/src/components/Link/Link.fragment.tsx +0 -4
- package/src/components/Link/index.tsx +5 -1
- package/src/components/Listbox/Listbox.fragment.tsx +0 -12
- package/src/components/Markdown/Markdown.module.scss +6 -3
- package/src/components/Markdown/index.tsx +5 -1
- package/src/components/Message/Message.fragment.tsx +8 -6
- package/src/components/Message/Message.module.scss +1 -1
- package/src/components/Progress/Progress.fragment.tsx +14 -0
- package/src/components/Progress/index.tsx +9 -2
- package/src/components/Prompt/Prompt.fragment.tsx +11 -0
- package/src/components/RadioGroup/RadioGroup.fragment.tsx +5 -0
- package/src/components/ScrollArea/ScrollArea.fragment.tsx +185 -0
- package/src/components/ScrollArea/ScrollArea.module.scss +136 -0
- package/src/components/ScrollArea/ScrollArea.test.tsx +38 -0
- package/src/components/ScrollArea/index.tsx +121 -0
- package/src/components/Select/Select.fragment.tsx +13 -5
- package/src/components/Separator/index.tsx +5 -1
- package/src/components/Sidebar/Sidebar.fragment.tsx +64 -11
- package/src/components/Sidebar/Sidebar.module.scss +68 -16
- package/src/components/Sidebar/Sidebar.test.tsx +31 -2
- package/src/components/Sidebar/index.tsx +69 -45
- package/src/components/Skeleton/Skeleton.fragment.tsx +5 -0
- package/src/components/Slider/index.tsx +5 -1
- package/src/components/Stack/Stack.fragment.tsx +2 -2
- package/src/components/Stack/index.tsx +5 -1
- package/src/components/Table/Table.fragment.tsx +29 -0
- package/src/components/Table/index.tsx +6 -1
- package/src/components/TableOfContents/TableOfContents.fragment.tsx +149 -0
- package/src/components/TableOfContents/TableOfContents.module.scss +71 -0
- package/src/components/TableOfContents/TableOfContents.test.tsx +126 -0
- package/src/components/TableOfContents/index.tsx +105 -0
- package/src/components/Text/index.tsx +5 -1
- package/src/components/Textarea/Textarea.fragment.tsx +8 -0
- package/src/components/Textarea/index.tsx +5 -1
- package/src/components/Theme/index.tsx +7 -0
- package/src/components/ThinkingIndicator/ThinkingIndicator.fragment.tsx +3 -2
- package/src/components/Toast/Toast.fragment.tsx +12 -0
- package/src/components/Toggle/index.tsx +5 -1
- package/src/components/Tooltip/Tooltip.fragment.tsx +18 -0
- package/src/components/Tooltip/index.tsx +6 -1
- package/src/components/VisuallyHidden/index.tsx +5 -1
- package/src/components/compound-pattern.test.ts +40 -0
- package/src/index.ts +29 -0
- package/src/recipes/AppShell.recipe.ts +2 -2
- package/src/recipes/LoginForm.recipe.ts +14 -7
- package/src/tokens/_computed.scss +12 -0
- package/src/tokens/_derive.scss +71 -0
- package/src/tokens/_variables.scss +22 -0
|
@@ -212,7 +212,7 @@ function CloseIcon() {
|
|
|
212
212
|
);
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
function
|
|
215
|
+
function CollapsePanelIcon() {
|
|
216
216
|
return (
|
|
217
217
|
<svg
|
|
218
218
|
xmlns="http://www.w3.org/2000/svg"
|
|
@@ -222,22 +222,7 @@ function CollapseLeftIcon() {
|
|
|
222
222
|
fill="currentColor"
|
|
223
223
|
aria-hidden="true"
|
|
224
224
|
>
|
|
225
|
-
<path d="
|
|
226
|
-
</svg>
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function CollapseRightIcon() {
|
|
231
|
-
return (
|
|
232
|
-
<svg
|
|
233
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
234
|
-
width="20"
|
|
235
|
-
height="20"
|
|
236
|
-
viewBox="0 0 256 256"
|
|
237
|
-
fill="currentColor"
|
|
238
|
-
aria-hidden="true"
|
|
239
|
-
>
|
|
240
|
-
<path d="M141.66,133.66l-48,48a8,8,0,0,1-11.32-11.32L124.69,128,82.34,85.66a8,8,0,0,1,11.32-11.32l48,48A8,8,0,0,1,141.66,133.66Zm40-11.32-48-48a8,8,0,0,0-11.32,11.32L164.69,128l-42.35,42.34a8,8,0,0,0,11.32,11.32l48-48A8,8,0,0,0,181.66,122.34Z" />
|
|
225
|
+
<path d="M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40ZM40,56H80V200H40ZM216,200H96V56H216V200Z" />
|
|
241
226
|
</svg>
|
|
242
227
|
);
|
|
243
228
|
}
|
|
@@ -271,6 +256,7 @@ interface SidebarContextValue {
|
|
|
271
256
|
width: string;
|
|
272
257
|
collapsedWidth: string;
|
|
273
258
|
collapsible: SidebarCollapsible;
|
|
259
|
+
hasIcons: boolean;
|
|
274
260
|
toggleSidebar: () => void;
|
|
275
261
|
sidebarId: string;
|
|
276
262
|
}
|
|
@@ -293,8 +279,9 @@ function useSidebar() {
|
|
|
293
279
|
isMobile: false,
|
|
294
280
|
position: 'left' as const,
|
|
295
281
|
width: '240px',
|
|
296
|
-
collapsedWidth: '
|
|
282
|
+
collapsedWidth: '56px',
|
|
297
283
|
collapsible: 'icon' as SidebarCollapsible,
|
|
284
|
+
hasIcons: true,
|
|
298
285
|
toggleSidebar: () => {},
|
|
299
286
|
sidebarId: 'sidebar',
|
|
300
287
|
state: 'expanded' as 'expanded' | 'collapsed' | 'open' | 'closed',
|
|
@@ -359,6 +346,32 @@ function useControllableState<T>(
|
|
|
359
346
|
return [value, setValue];
|
|
360
347
|
}
|
|
361
348
|
|
|
349
|
+
function hasSidebarItemIcons(children: React.ReactNode): boolean {
|
|
350
|
+
let found = false;
|
|
351
|
+
|
|
352
|
+
const visit = (nodes: React.ReactNode) => {
|
|
353
|
+
React.Children.forEach(nodes, child => {
|
|
354
|
+
if (found || !React.isValidElement(child)) return;
|
|
355
|
+
|
|
356
|
+
if (child.type === SidebarItem) {
|
|
357
|
+
const props = child.props as SidebarItemProps;
|
|
358
|
+
if (props.icon) {
|
|
359
|
+
found = true;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const childProps = child.props as { children?: React.ReactNode };
|
|
365
|
+
if (childProps?.children) {
|
|
366
|
+
visit(childProps.children);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
visit(children);
|
|
372
|
+
return found;
|
|
373
|
+
}
|
|
374
|
+
|
|
362
375
|
// ============================================
|
|
363
376
|
// Components
|
|
364
377
|
// ============================================
|
|
@@ -376,7 +389,7 @@ function SidebarProvider({
|
|
|
376
389
|
defaultOpen = false,
|
|
377
390
|
onOpenChange,
|
|
378
391
|
width = '240px',
|
|
379
|
-
collapsedWidth = '
|
|
392
|
+
collapsedWidth = '56px',
|
|
380
393
|
position = 'left',
|
|
381
394
|
collapsible = 'icon',
|
|
382
395
|
enableKeyboardShortcut = true,
|
|
@@ -459,6 +472,7 @@ function SidebarProvider({
|
|
|
459
472
|
width,
|
|
460
473
|
collapsedWidth,
|
|
461
474
|
collapsible,
|
|
475
|
+
hasIcons: true,
|
|
462
476
|
toggleSidebar,
|
|
463
477
|
sidebarId,
|
|
464
478
|
};
|
|
@@ -479,7 +493,7 @@ function SidebarRoot({
|
|
|
479
493
|
defaultOpen = false,
|
|
480
494
|
onOpenChange,
|
|
481
495
|
width = '240px',
|
|
482
|
-
collapsedWidth = '
|
|
496
|
+
collapsedWidth = '56px',
|
|
483
497
|
position = 'left',
|
|
484
498
|
collapsible = 'icon',
|
|
485
499
|
className,
|
|
@@ -512,6 +526,10 @@ function SidebarRoot({
|
|
|
512
526
|
const resolvedWidth = existingContext ? existingContext.width : width;
|
|
513
527
|
const resolvedCollapsedWidth = existingContext ? existingContext.collapsedWidth : collapsedWidth;
|
|
514
528
|
const resolvedCollapsible = existingContext ? existingContext.collapsible : collapsible;
|
|
529
|
+
const hasIcons = React.useMemo(() => hasSidebarItemIcons(children), [children]);
|
|
530
|
+
const shouldCollapseToZero = !isMobile && resolvedCollapsible === 'icon' && collapsed && !hasIcons;
|
|
531
|
+
const isOffcanvasCollapsed = !isMobile && resolvedCollapsible === 'offcanvas' && collapsed;
|
|
532
|
+
const effectiveCollapsedWidth = (shouldCollapseToZero || isOffcanvasCollapsed) ? '0px' : resolvedCollapsedWidth;
|
|
515
533
|
const sidebarId = React.useId();
|
|
516
534
|
const resolvedSidebarId = existingContext ? existingContext.sidebarId : sidebarId;
|
|
517
535
|
const sidebarRef = React.useRef<HTMLElement>(null);
|
|
@@ -558,28 +576,32 @@ function SidebarRoot({
|
|
|
558
576
|
};
|
|
559
577
|
}, [existingContext, isMobile, open]);
|
|
560
578
|
|
|
561
|
-
const contextValue: SidebarContextValue =
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
579
|
+
const contextValue: SidebarContextValue = {
|
|
580
|
+
...(existingContext || {
|
|
581
|
+
collapsed,
|
|
582
|
+
setCollapsed,
|
|
583
|
+
open,
|
|
584
|
+
setOpen,
|
|
585
|
+
isMobile,
|
|
586
|
+
position: resolvedPosition,
|
|
587
|
+
width: resolvedWidth,
|
|
588
|
+
collapsedWidth: resolvedCollapsedWidth,
|
|
589
|
+
collapsible: resolvedCollapsible,
|
|
590
|
+
hasIcons,
|
|
591
|
+
toggleSidebar,
|
|
592
|
+
sidebarId: resolvedSidebarId,
|
|
593
|
+
}),
|
|
594
|
+
hasIcons,
|
|
573
595
|
};
|
|
574
596
|
|
|
575
597
|
const isCollapsedForStyle = resolvedCollapsible === 'icon' && collapsed;
|
|
576
|
-
const isOffcanvas = resolvedCollapsible === 'offcanvas' && collapsed;
|
|
577
598
|
|
|
578
599
|
const classes = [
|
|
579
600
|
styles.root,
|
|
580
601
|
isMobile && styles.mobile,
|
|
581
602
|
!isMobile && isCollapsedForStyle && styles.collapsed,
|
|
582
|
-
!isMobile &&
|
|
603
|
+
!isMobile && isCollapsedForStyle && shouldCollapseToZero && styles.collapsedNoIcons,
|
|
604
|
+
isOffcanvasCollapsed && styles.offcanvasCollapsed,
|
|
583
605
|
resolvedPosition === 'right' && styles.positionRight,
|
|
584
606
|
className,
|
|
585
607
|
].filter(Boolean).join(' ');
|
|
@@ -587,6 +609,7 @@ function SidebarRoot({
|
|
|
587
609
|
const style: React.CSSProperties = {
|
|
588
610
|
'--sidebar-width': resolvedWidth,
|
|
589
611
|
'--sidebar-collapsed-width': resolvedCollapsedWidth,
|
|
612
|
+
'--sidebar-effective-collapsed-width': effectiveCollapsedWidth,
|
|
590
613
|
...styleProp,
|
|
591
614
|
} as React.CSSProperties;
|
|
592
615
|
|
|
@@ -604,16 +627,12 @@ function SidebarRoot({
|
|
|
604
627
|
data-state={isMobile ? (open ? 'open' : 'closed') : (collapsed ? 'collapsed' : 'expanded')}
|
|
605
628
|
data-position={resolvedPosition}
|
|
606
629
|
data-collapsible={resolvedCollapsible}
|
|
630
|
+
data-icon-collapse={resolvedCollapsible === 'icon' ? (hasIcons ? 'icons' : 'none') : undefined}
|
|
607
631
|
>
|
|
608
632
|
{children}
|
|
609
633
|
</aside>
|
|
610
634
|
);
|
|
611
635
|
|
|
612
|
-
// If already inside a provider, don't wrap with another provider
|
|
613
|
-
if (existingContext) {
|
|
614
|
-
return content;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
636
|
return (
|
|
618
637
|
<SidebarContext.Provider value={contextValue}>
|
|
619
638
|
{content}
|
|
@@ -951,19 +970,24 @@ function SidebarOverlay({ className }: SidebarOverlayProps) {
|
|
|
951
970
|
}
|
|
952
971
|
|
|
953
972
|
function SidebarCollapseToggle({ 'aria-label': ariaLabel, className }: SidebarCollapseToggleProps) {
|
|
954
|
-
const { collapsed, setCollapsed, isMobile,
|
|
973
|
+
const { collapsed, setCollapsed, isMobile, collapsible, hasIcons } = useSidebarContext();
|
|
955
974
|
|
|
956
975
|
// Don't show on mobile or when collapsing is disabled
|
|
957
976
|
if (isMobile || collapsible === 'none') {
|
|
958
977
|
return null;
|
|
959
978
|
}
|
|
960
979
|
|
|
961
|
-
const
|
|
980
|
+
const shouldFloat = collapsed && (
|
|
981
|
+
(collapsible === 'icon' && !hasIcons) ||
|
|
982
|
+
collapsible === 'offcanvas'
|
|
983
|
+
);
|
|
984
|
+
const classes = [
|
|
985
|
+
styles.collapseToggle,
|
|
986
|
+
shouldFloat && styles.collapseToggleFloating,
|
|
987
|
+
className,
|
|
988
|
+
].filter(Boolean).join(' ');
|
|
962
989
|
const label = ariaLabel || (collapsed ? 'Expand sidebar' : 'Collapse sidebar');
|
|
963
990
|
|
|
964
|
-
// Determine which icon to show based on position and state
|
|
965
|
-
const showExpandIcon = position === 'left' ? collapsed : !collapsed;
|
|
966
|
-
|
|
967
991
|
return (
|
|
968
992
|
<button
|
|
969
993
|
type="button"
|
|
@@ -971,7 +995,7 @@ function SidebarCollapseToggle({ 'aria-label': ariaLabel, className }: SidebarCo
|
|
|
971
995
|
onClick={() => setCollapsed(!collapsed)}
|
|
972
996
|
aria-label={label}
|
|
973
997
|
>
|
|
974
|
-
|
|
998
|
+
<CollapsePanelIcon />
|
|
975
999
|
</button>
|
|
976
1000
|
);
|
|
977
1001
|
}
|
|
@@ -69,6 +69,11 @@ export default defineSegment({
|
|
|
69
69
|
values: ['none', 'sm', 'md', 'lg', 'full'],
|
|
70
70
|
description: 'Border radius override',
|
|
71
71
|
},
|
|
72
|
+
static: {
|
|
73
|
+
type: 'boolean',
|
|
74
|
+
default: false,
|
|
75
|
+
description: 'Disable skeleton animation',
|
|
76
|
+
},
|
|
72
77
|
},
|
|
73
78
|
|
|
74
79
|
relations: [
|
|
@@ -24,7 +24,7 @@ export interface SliderProps extends Omit<React.HTMLAttributes<HTMLDivElement>,
|
|
|
24
24
|
'aria-describedby'?: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
const SliderRoot = React.forwardRef<HTMLDivElement, SliderProps>(
|
|
28
28
|
function Slider(
|
|
29
29
|
{
|
|
30
30
|
label,
|
|
@@ -96,3 +96,7 @@ export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
|
|
|
96
96
|
);
|
|
97
97
|
}
|
|
98
98
|
);
|
|
99
|
+
|
|
100
|
+
export const Slider = Object.assign(SliderRoot, {
|
|
101
|
+
Root: SliderRoot,
|
|
102
|
+
});
|
|
@@ -48,12 +48,12 @@ export default defineSegment({
|
|
|
48
48
|
required: true,
|
|
49
49
|
},
|
|
50
50
|
direction: {
|
|
51
|
-
type: '
|
|
51
|
+
type: 'union',
|
|
52
52
|
description: 'Stack direction: "row", "column", or responsive object',
|
|
53
53
|
default: 'column',
|
|
54
54
|
},
|
|
55
55
|
gap: {
|
|
56
|
-
type: '
|
|
56
|
+
type: 'union',
|
|
57
57
|
description: 'Spacing between items: "none", "xs", "sm", "md", "lg", "xl", or responsive object',
|
|
58
58
|
default: 'md',
|
|
59
59
|
},
|
|
@@ -71,7 +71,7 @@ function isResponsiveGap(gap: StackProps['gap']): gap is ResponsiveGap {
|
|
|
71
71
|
return typeof gap === 'object' && gap !== null;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
const StackRoot = React.forwardRef<HTMLElement, StackProps>(
|
|
75
75
|
function Stack(
|
|
76
76
|
{
|
|
77
77
|
children,
|
|
@@ -180,3 +180,7 @@ function gapToSpace(gap: Gap): string {
|
|
|
180
180
|
};
|
|
181
181
|
return map[gap];
|
|
182
182
|
}
|
|
183
|
+
|
|
184
|
+
export const Stack = Object.assign(StackRoot, {
|
|
185
|
+
Root: StackRoot,
|
|
186
|
+
});
|
|
@@ -86,16 +86,36 @@ export default defineSegment({
|
|
|
86
86
|
description: 'Data rows to display',
|
|
87
87
|
required: true,
|
|
88
88
|
},
|
|
89
|
+
getRowId: {
|
|
90
|
+
type: 'function',
|
|
91
|
+
description: 'Unique key extractor for each row',
|
|
92
|
+
},
|
|
89
93
|
sortable: {
|
|
90
94
|
type: 'boolean',
|
|
91
95
|
description: 'Enable column sorting',
|
|
92
96
|
default: 'false',
|
|
93
97
|
},
|
|
98
|
+
sorting: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
description: 'Controlled sorting state',
|
|
101
|
+
},
|
|
102
|
+
onSortingChange: {
|
|
103
|
+
type: 'function',
|
|
104
|
+
description: 'Sorting change handler',
|
|
105
|
+
},
|
|
94
106
|
selectable: {
|
|
95
107
|
type: 'boolean',
|
|
96
108
|
description: 'Enable row selection',
|
|
97
109
|
default: 'false',
|
|
98
110
|
},
|
|
111
|
+
rowSelection: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
description: 'Controlled row selection state',
|
|
114
|
+
},
|
|
115
|
+
onRowSelectionChange: {
|
|
116
|
+
type: 'function',
|
|
117
|
+
description: 'Row selection change handler',
|
|
118
|
+
},
|
|
99
119
|
onRowClick: {
|
|
100
120
|
type: 'function',
|
|
101
121
|
description: 'Handler for row clicks',
|
|
@@ -111,6 +131,15 @@ export default defineSegment({
|
|
|
111
131
|
values: ['sm', 'md'],
|
|
112
132
|
default: 'md',
|
|
113
133
|
},
|
|
134
|
+
caption: {
|
|
135
|
+
type: 'string',
|
|
136
|
+
description: 'Visible caption for the table',
|
|
137
|
+
},
|
|
138
|
+
captionHidden: {
|
|
139
|
+
type: 'boolean',
|
|
140
|
+
default: 'false',
|
|
141
|
+
description: 'Hide caption visually but keep it for screen readers',
|
|
142
|
+
},
|
|
114
143
|
striped: {
|
|
115
144
|
type: 'boolean',
|
|
116
145
|
description: 'Show alternating row backgrounds',
|
|
@@ -51,7 +51,7 @@ export interface TableProps<T> extends Omit<React.HTMLAttributes<HTMLTableElemen
|
|
|
51
51
|
bordered?: boolean;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
function TableRoot<T>({
|
|
55
55
|
columns,
|
|
56
56
|
data,
|
|
57
57
|
getRowId,
|
|
@@ -330,3 +330,8 @@ export function createColumns<T>(
|
|
|
330
330
|
|
|
331
331
|
// Re-export useful types
|
|
332
332
|
export type { ColumnDef, SortingState, RowSelectionState };
|
|
333
|
+
|
|
334
|
+
export const Table = Object.assign(TableRoot, {
|
|
335
|
+
Root: TableRoot,
|
|
336
|
+
Columns: createColumns,
|
|
337
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineSegment } from '@fragments/core';
|
|
3
|
+
import { TableOfContents } from '.';
|
|
4
|
+
|
|
5
|
+
export default defineSegment({
|
|
6
|
+
component: TableOfContents,
|
|
7
|
+
|
|
8
|
+
meta: {
|
|
9
|
+
name: 'TableOfContents',
|
|
10
|
+
description: 'Sticky sidebar navigation for long-form content. Renders heading links with active state highlighting for scroll spy integration.',
|
|
11
|
+
category: 'navigation',
|
|
12
|
+
status: 'stable',
|
|
13
|
+
tags: ['toc', 'table-of-contents', 'navigation', 'sidebar', 'scroll-spy', 'headings'],
|
|
14
|
+
since: '0.9.0',
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
usage: {
|
|
18
|
+
when: [
|
|
19
|
+
'Long-form content pages (docs, blog posts, articles)',
|
|
20
|
+
'Component documentation with multiple sections',
|
|
21
|
+
'Any page with 3+ headings that benefits from quick navigation',
|
|
22
|
+
],
|
|
23
|
+
whenNot: [
|
|
24
|
+
'Short pages with only 1-2 sections',
|
|
25
|
+
'Primary site navigation (use Sidebar or Header)',
|
|
26
|
+
'Step-by-step flows (use Stepper)',
|
|
27
|
+
],
|
|
28
|
+
guidelines: [
|
|
29
|
+
'Pair with a scroll spy hook (e.g., IntersectionObserver) to track active heading',
|
|
30
|
+
'Use indent on sub-headings (h3) to show hierarchy',
|
|
31
|
+
'Place in a sticky aside for best UX',
|
|
32
|
+
'Heading IDs must match between TOC items and the actual DOM headings',
|
|
33
|
+
],
|
|
34
|
+
accessibility: [
|
|
35
|
+
'Uses <nav aria-label="Table of contents"> for landmark navigation',
|
|
36
|
+
'Active item is marked with aria-current="true"',
|
|
37
|
+
'All items are links with smooth scroll behavior',
|
|
38
|
+
'Focus-visible ring on keyboard navigation',
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
props: {
|
|
43
|
+
children: {
|
|
44
|
+
type: 'node',
|
|
45
|
+
description: 'TableOfContents.Item elements',
|
|
46
|
+
required: true,
|
|
47
|
+
},
|
|
48
|
+
label: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
description: 'Accessible label for the nav landmark',
|
|
51
|
+
default: '"Table of contents"',
|
|
52
|
+
},
|
|
53
|
+
title: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
description: 'Visible title above the list',
|
|
56
|
+
default: '"On This Page"',
|
|
57
|
+
},
|
|
58
|
+
hideTitle: {
|
|
59
|
+
type: 'boolean',
|
|
60
|
+
description: 'Hide the visible title',
|
|
61
|
+
default: 'false',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
relations: [
|
|
66
|
+
{ component: 'Breadcrumbs', relationship: 'complementary', note: 'Breadcrumbs show hierarchy, TOC shows page sections' },
|
|
67
|
+
{ component: 'Sidebar', relationship: 'complementary', note: 'Sidebar for site nav, TOC for in-page nav' },
|
|
68
|
+
{ component: 'Tabs', relationship: 'alternative', note: 'Tabs for switching views, TOC for scrolling to sections' },
|
|
69
|
+
],
|
|
70
|
+
|
|
71
|
+
contract: {
|
|
72
|
+
propsSummary: [
|
|
73
|
+
'title: string - visible heading (default "On This Page")',
|
|
74
|
+
'hideTitle: boolean - hide the title',
|
|
75
|
+
'label: string - aria-label for nav landmark',
|
|
76
|
+
'TableOfContents.Item id: string - heading ID to link to',
|
|
77
|
+
'TableOfContents.Item active: boolean - highlight as current',
|
|
78
|
+
'TableOfContents.Item indent: boolean - indent for sub-headings',
|
|
79
|
+
],
|
|
80
|
+
scenarioTags: [
|
|
81
|
+
'navigation.toc',
|
|
82
|
+
'navigation.scroll-spy',
|
|
83
|
+
'layout.sidebar',
|
|
84
|
+
],
|
|
85
|
+
a11yRules: ['A11Y_NAV_LANDMARK', 'A11Y_ARIA_CURRENT'],
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
ai: {
|
|
89
|
+
compositionPattern: 'compound',
|
|
90
|
+
subComponents: ['Item'],
|
|
91
|
+
requiredChildren: ['Item'],
|
|
92
|
+
commonPatterns: [
|
|
93
|
+
'<TableOfContents>{headings.map(h => <TableOfContents.Item key={h.id} id={h.id} active={activeId === h.id} indent={h.level === 3}>{h.text}</TableOfContents.Item>)}</TableOfContents>',
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
variants: [
|
|
98
|
+
{
|
|
99
|
+
name: 'Default',
|
|
100
|
+
description: 'Basic table of contents with section links',
|
|
101
|
+
render: () => (
|
|
102
|
+
<TableOfContents>
|
|
103
|
+
<TableOfContents.Item id="introduction">Introduction</TableOfContents.Item>
|
|
104
|
+
<TableOfContents.Item id="getting-started">Getting Started</TableOfContents.Item>
|
|
105
|
+
<TableOfContents.Item id="installation" indent>Installation</TableOfContents.Item>
|
|
106
|
+
<TableOfContents.Item id="configuration" indent>Configuration</TableOfContents.Item>
|
|
107
|
+
<TableOfContents.Item id="api-reference">API Reference</TableOfContents.Item>
|
|
108
|
+
<TableOfContents.Item id="examples">Examples</TableOfContents.Item>
|
|
109
|
+
</TableOfContents>
|
|
110
|
+
),
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'With Active Item',
|
|
114
|
+
description: 'Active state highlighting the current section',
|
|
115
|
+
render: () => (
|
|
116
|
+
<TableOfContents>
|
|
117
|
+
<TableOfContents.Item id="overview">Overview</TableOfContents.Item>
|
|
118
|
+
<TableOfContents.Item id="setup" active>Setup</TableOfContents.Item>
|
|
119
|
+
<TableOfContents.Item id="usage" indent>Basic Usage</TableOfContents.Item>
|
|
120
|
+
<TableOfContents.Item id="advanced" indent>Advanced</TableOfContents.Item>
|
|
121
|
+
<TableOfContents.Item id="props">Props</TableOfContents.Item>
|
|
122
|
+
<TableOfContents.Item id="accessibility">Accessibility</TableOfContents.Item>
|
|
123
|
+
</TableOfContents>
|
|
124
|
+
),
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'Custom Title',
|
|
128
|
+
description: 'Table of contents with a custom title',
|
|
129
|
+
render: () => (
|
|
130
|
+
<TableOfContents title="Contents">
|
|
131
|
+
<TableOfContents.Item id="chapter-1">Chapter 1: The Beginning</TableOfContents.Item>
|
|
132
|
+
<TableOfContents.Item id="chapter-2">Chapter 2: The Middle</TableOfContents.Item>
|
|
133
|
+
<TableOfContents.Item id="chapter-3">Chapter 3: The End</TableOfContents.Item>
|
|
134
|
+
</TableOfContents>
|
|
135
|
+
),
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'No Title',
|
|
139
|
+
description: 'Table of contents without a visible title',
|
|
140
|
+
render: () => (
|
|
141
|
+
<TableOfContents hideTitle>
|
|
142
|
+
<TableOfContents.Item id="section-a">Section A</TableOfContents.Item>
|
|
143
|
+
<TableOfContents.Item id="section-b" active>Section B</TableOfContents.Item>
|
|
144
|
+
<TableOfContents.Item id="section-c">Section C</TableOfContents.Item>
|
|
145
|
+
</TableOfContents>
|
|
146
|
+
),
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
|
|
3
|
+
// Root nav wrapper
|
|
4
|
+
.root {
|
|
5
|
+
font-family: var(--fui-font-sans, $fui-font-sans);
|
|
6
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
7
|
+
line-height: var(--fui-line-height-normal, $fui-line-height-normal);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Title text ("On This Page")
|
|
11
|
+
.title {
|
|
12
|
+
font-size: var(--fui-font-size-xs, $fui-font-size-xs);
|
|
13
|
+
font-weight: var(--fui-font-weight-semibold, $fui-font-weight-semibold);
|
|
14
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
15
|
+
text-transform: uppercase;
|
|
16
|
+
letter-spacing: 0.05em;
|
|
17
|
+
margin: 0;
|
|
18
|
+
padding: 0 0 var(--fui-space-2, $fui-space-2) 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// List container
|
|
22
|
+
.list {
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
list-style: none;
|
|
26
|
+
margin: 0;
|
|
27
|
+
padding: 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Individual item
|
|
31
|
+
.item {
|
|
32
|
+
display: block;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Link styling shared by all items
|
|
36
|
+
.link {
|
|
37
|
+
display: block;
|
|
38
|
+
padding: var(--fui-space-1, $fui-space-1) 0 var(--fui-space-1, $fui-space-1) var(--fui-space-3, $fui-space-3);
|
|
39
|
+
border-left: 2px solid transparent;
|
|
40
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
41
|
+
font-size: var(--fui-font-size-xs, $fui-font-size-xs);
|
|
42
|
+
line-height: var(--fui-line-height-normal, $fui-line-height-normal);
|
|
43
|
+
text-decoration: none;
|
|
44
|
+
transition: color var(--fui-transition-fast, $fui-transition-fast),
|
|
45
|
+
border-color var(--fui-transition-fast, $fui-transition-fast);
|
|
46
|
+
|
|
47
|
+
&:hover {
|
|
48
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
&:focus-visible {
|
|
52
|
+
outline: var(--fui-focus-ring-width, $fui-focus-ring-width) solid var(--fui-focus-ring-color, $fui-focus-ring-color);
|
|
53
|
+
outline-offset: var(--fui-focus-ring-offset, $fui-focus-ring-offset);
|
|
54
|
+
border-radius: var(--fui-radius-sm, $fui-radius-sm);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Indent for depth > 2 (h3s)
|
|
59
|
+
.indent {
|
|
60
|
+
padding-left: var(--fui-space-6, $fui-space-6);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Active state
|
|
64
|
+
.active {
|
|
65
|
+
border-left-color: var(--fui-color-accent, $fui-color-accent);
|
|
66
|
+
color: var(--fui-color-accent, $fui-color-accent);
|
|
67
|
+
|
|
68
|
+
&:hover {
|
|
69
|
+
color: var(--fui-color-accent, $fui-color-accent);
|
|
70
|
+
}
|
|
71
|
+
}
|