@fragments-sdk/ui 0.8.3 → 0.8.5
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 +1 -1
- package/fragments.json +1 -1
- package/package.json +29 -7
- package/src/blocks/NavigationHeader.block.ts +89 -0
- package/src/components/AppShell/AppShell.fragment.tsx +26 -25
- package/src/components/AppShell/AppShell.module.scss +18 -19
- package/src/components/AppShell/index.tsx +37 -12
- package/src/components/Chart/Chart.fragment.tsx +3 -0
- package/src/components/Chart/index.tsx +62 -14
- package/src/components/CodeBlock/CodeBlock.fragment.tsx +3 -0
- package/src/components/CodeBlock/CodeBlock.test.tsx +6 -6
- package/src/components/CodeBlock/index.tsx +38 -3
- package/src/components/ColorPicker/ColorPicker.fragment.tsx +3 -0
- package/src/components/ColorPicker/index.tsx +24 -2
- package/src/components/DatePicker/DatePicker.fragment.tsx +4 -0
- package/src/components/DatePicker/index.tsx +101 -43
- package/src/components/Header/Header.fragment.tsx +45 -8
- package/src/components/Link/Link.fragment.tsx +17 -0
- package/src/components/Link/Link.test.tsx +23 -0
- package/src/components/Link/index.tsx +20 -0
- package/src/components/Markdown/Markdown.fragment.tsx +4 -0
- package/src/components/NavigationMenu/NavigationMenu.fragment.tsx +270 -0
- package/src/components/NavigationMenu/NavigationMenu.module.scss +516 -0
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +457 -0
- package/src/components/NavigationMenu/NavigationMenuContext.ts +89 -0
- package/src/components/NavigationMenu/index.tsx +854 -0
- package/src/components/NavigationMenu/useNavigationMenu.ts +91 -0
- package/src/components/Sidebar/Sidebar.fragment.tsx +1 -1
- package/src/components/Sidebar/Sidebar.module.scss +2 -1
- package/src/components/Table/Table.fragment.tsx +3 -0
- package/src/components/Table/index.tsx +70 -24
- package/src/index.ts +25 -0
- package/src/tokens/_palettes.scss +8 -7
- package/src/tokens/_seeds.scss +2 -2
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import type { NavigationMenuItemInfo } from './NavigationMenuContext';
|
|
5
|
+
|
|
6
|
+
export interface UseNavigationMenuOptions {
|
|
7
|
+
value?: string;
|
|
8
|
+
defaultValue?: string;
|
|
9
|
+
onValueChange?: (value: string) => void;
|
|
10
|
+
delayDuration?: number;
|
|
11
|
+
skipDelayDuration?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useNavigationMenu({
|
|
15
|
+
value: controlledValue,
|
|
16
|
+
defaultValue = '',
|
|
17
|
+
onValueChange,
|
|
18
|
+
delayDuration = 200,
|
|
19
|
+
skipDelayDuration = 300,
|
|
20
|
+
}: UseNavigationMenuOptions) {
|
|
21
|
+
const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);
|
|
22
|
+
const isControlled = controlledValue !== undefined;
|
|
23
|
+
const value = isControlled ? controlledValue : uncontrolledValue;
|
|
24
|
+
|
|
25
|
+
const setValue = React.useCallback(
|
|
26
|
+
(newValue: string) => {
|
|
27
|
+
if (!isControlled) {
|
|
28
|
+
setUncontrolledValue(newValue);
|
|
29
|
+
}
|
|
30
|
+
onValueChange?.(newValue);
|
|
31
|
+
},
|
|
32
|
+
[isControlled, onValueChange]
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// Hover delay timers
|
|
36
|
+
const openTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
37
|
+
const closeTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
38
|
+
|
|
39
|
+
// Skip-delay: when moving between triggers, open instantly
|
|
40
|
+
const isRecentlyOpenRef = React.useRef(false);
|
|
41
|
+
const skipDelayTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
42
|
+
|
|
43
|
+
// Trigger registry for keyboard nav
|
|
44
|
+
const triggerRefs = React.useRef<Map<string, HTMLButtonElement>>(new Map());
|
|
45
|
+
const triggerOrder = React.useRef<string[]>([]);
|
|
46
|
+
|
|
47
|
+
// Item info registry for mobile drawer
|
|
48
|
+
const itemInfoMap = React.useRef<Map<string, NavigationMenuItemInfo>>(new Map());
|
|
49
|
+
|
|
50
|
+
// Viewport sizing
|
|
51
|
+
const [viewportSize, setViewportSize] = React.useState({ width: 0, height: 0 });
|
|
52
|
+
const viewportRef = React.useRef<HTMLDivElement>(null);
|
|
53
|
+
|
|
54
|
+
// Track previous value for animation direction
|
|
55
|
+
const previousValue = React.useRef(value);
|
|
56
|
+
|
|
57
|
+
// Mobile state
|
|
58
|
+
const [mobileOpen, setMobileOpen] = React.useState(false);
|
|
59
|
+
const [mobileContentChildren, setMobileContentChildren] = React.useState<React.ReactNode>(null);
|
|
60
|
+
|
|
61
|
+
// Clean up timers on unmount
|
|
62
|
+
React.useEffect(() => {
|
|
63
|
+
return () => {
|
|
64
|
+
if (openTimerRef.current) clearTimeout(openTimerRef.current);
|
|
65
|
+
if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
|
|
66
|
+
if (skipDelayTimerRef.current) clearTimeout(skipDelayTimerRef.current);
|
|
67
|
+
};
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
value,
|
|
72
|
+
setValue,
|
|
73
|
+
delayDuration,
|
|
74
|
+
skipDelayDuration,
|
|
75
|
+
openTimerRef,
|
|
76
|
+
closeTimerRef,
|
|
77
|
+
isRecentlyOpenRef,
|
|
78
|
+
skipDelayTimerRef,
|
|
79
|
+
triggerRefs,
|
|
80
|
+
triggerOrder,
|
|
81
|
+
itemInfoMap,
|
|
82
|
+
viewportSize,
|
|
83
|
+
setViewportSize,
|
|
84
|
+
viewportRef,
|
|
85
|
+
previousValue,
|
|
86
|
+
mobileOpen,
|
|
87
|
+
setMobileOpen,
|
|
88
|
+
mobileContentChildren,
|
|
89
|
+
setMobileContentChildren,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -511,7 +511,7 @@ export default defineFragment({
|
|
|
511
511
|
|
|
512
512
|
ai: {
|
|
513
513
|
compositionPattern: 'compound',
|
|
514
|
-
subComponents: ['Header', 'Nav', 'Section', 'Item', 'SubItem', 'Submenu', 'Footer', '
|
|
514
|
+
subComponents: ['Header', 'Nav', 'Section', 'SectionAction', 'Item', 'SubItem', 'Submenu', 'Footer', 'Trigger', 'Overlay', 'CollapseToggle', 'Rail', 'MenuSkeleton'],
|
|
515
515
|
requiredChildren: ['Nav'],
|
|
516
516
|
commonPatterns: [
|
|
517
517
|
'<Sidebar><Sidebar.Header>{logo}</Sidebar.Header><Sidebar.Nav><Sidebar.Section><Sidebar.Item icon={icon} active>{label}</Sidebar.Item></Sidebar.Section></Sidebar.Nav><Sidebar.Footer><Sidebar.CollapseToggle /></Sidebar.Footer></Sidebar>',
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
display: flex;
|
|
11
11
|
flex-direction: column;
|
|
12
12
|
width: var(--sidebar-width);
|
|
13
|
-
height:
|
|
13
|
+
height: 100%;
|
|
14
|
+
max-height: 100vh;
|
|
14
15
|
background-color: var(--fui-bg-primary, $fui-bg-primary);
|
|
15
16
|
transition: width var(--fui-transition-normal, $fui-transition-normal);
|
|
16
17
|
overflow: hidden;
|
|
@@ -47,6 +47,9 @@ export default defineFragment({
|
|
|
47
47
|
status: 'stable',
|
|
48
48
|
tags: ['table', 'data', 'grid', 'list', 'sorting'],
|
|
49
49
|
since: '0.1.0',
|
|
50
|
+
dependencies: [
|
|
51
|
+
{ name: '@tanstack/react-table', version: '>=8.0.0', reason: 'Table state management and rendering' },
|
|
52
|
+
],
|
|
50
53
|
},
|
|
51
54
|
|
|
52
55
|
usage: {
|
|
@@ -1,21 +1,58 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
useReactTable,
|
|
4
|
-
getCoreRowModel,
|
|
5
|
-
getSortedRowModel,
|
|
6
|
-
flexRender,
|
|
7
|
-
type ColumnDef,
|
|
8
|
-
type SortingState,
|
|
9
|
-
type RowSelectionState,
|
|
10
|
-
type OnChangeFn,
|
|
11
|
-
} from '@tanstack/react-table';
|
|
12
2
|
// Import globals to ensure CSS variables are defined
|
|
13
3
|
import '../../styles/globals.scss';
|
|
14
4
|
import styles from './Table.module.scss';
|
|
15
5
|
|
|
16
|
-
//
|
|
6
|
+
// ============================================
|
|
7
|
+
// Types (self-owned — no external dependency for types)
|
|
8
|
+
// ============================================
|
|
9
|
+
|
|
10
|
+
/** Column definition compatible with @tanstack/react-table */
|
|
11
|
+
export type ColumnDef<TData = unknown, TValue = unknown> = {
|
|
12
|
+
id?: string;
|
|
13
|
+
accessorKey?: string;
|
|
14
|
+
accessorFn?: (row: TData) => TValue;
|
|
15
|
+
header?: string | ((context: any) => React.ReactNode);
|
|
16
|
+
cell?: string | ((context: any) => React.ReactNode);
|
|
17
|
+
size?: number;
|
|
18
|
+
minSize?: number;
|
|
19
|
+
maxSize?: number;
|
|
20
|
+
enableSorting?: boolean;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type SortingState = Array<{ id: string; desc: boolean }>;
|
|
25
|
+
export type RowSelectionState = Record<string, boolean>;
|
|
26
|
+
type OnChangeFn<T> = ((updaterOrValue: T | ((prev: T) => T)) => void);
|
|
27
|
+
|
|
17
28
|
export type TableColumn<T> = ColumnDef<T, unknown>;
|
|
18
29
|
|
|
30
|
+
// ============================================
|
|
31
|
+
// Lazy-loaded dependency (@tanstack/react-table)
|
|
32
|
+
// ============================================
|
|
33
|
+
|
|
34
|
+
let _useReactTable: any = null;
|
|
35
|
+
let _getCoreRowModel: any = null;
|
|
36
|
+
let _getSortedRowModel: any = null;
|
|
37
|
+
let _flexRender: any = null;
|
|
38
|
+
let _tableLoaded = false;
|
|
39
|
+
let _tableFailed = false;
|
|
40
|
+
|
|
41
|
+
function loadTableDeps() {
|
|
42
|
+
if (_tableLoaded) return;
|
|
43
|
+
_tableLoaded = true;
|
|
44
|
+
try {
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
46
|
+
const rt = require('@tanstack/react-table');
|
|
47
|
+
_useReactTable = rt.useReactTable;
|
|
48
|
+
_getCoreRowModel = rt.getCoreRowModel;
|
|
49
|
+
_getSortedRowModel = rt.getSortedRowModel;
|
|
50
|
+
_flexRender = rt.flexRender;
|
|
51
|
+
} catch {
|
|
52
|
+
_tableFailed = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
19
56
|
export interface TableProps<T> extends Omit<React.HTMLAttributes<HTMLTableElement>, 'onClick'> {
|
|
20
57
|
/** Column definitions */
|
|
21
58
|
columns: TableColumn<T>[];
|
|
@@ -73,6 +110,8 @@ function TableRoot<T>({
|
|
|
73
110
|
'aria-describedby': ariaDescribedBy,
|
|
74
111
|
...htmlProps
|
|
75
112
|
}: TableProps<T>) {
|
|
113
|
+
loadTableDeps();
|
|
114
|
+
|
|
76
115
|
// Internal sorting state when uncontrolled
|
|
77
116
|
const [internalSorting, setInternalSorting] = React.useState<SortingState>([]);
|
|
78
117
|
const sorting = controlledSorting ?? internalSorting;
|
|
@@ -83,12 +122,22 @@ function TableRoot<T>({
|
|
|
83
122
|
const rowSelection = controlledRowSelection ?? internalRowSelection;
|
|
84
123
|
const handleRowSelectionChange = onRowSelectionChange ?? setInternalRowSelection;
|
|
85
124
|
|
|
86
|
-
|
|
125
|
+
if (_tableFailed || !_useReactTable) {
|
|
126
|
+
if (_tableFailed && process.env.NODE_ENV === 'development') {
|
|
127
|
+
console.warn(
|
|
128
|
+
'[@fragments-sdk/ui] Table: @tanstack/react-table is not installed. ' +
|
|
129
|
+
'Install it with: npm install @tanstack/react-table'
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const table = _useReactTable({
|
|
87
136
|
data,
|
|
88
137
|
columns,
|
|
89
138
|
getRowId,
|
|
90
|
-
getCoreRowModel:
|
|
91
|
-
getSortedRowModel: sortable ?
|
|
139
|
+
getCoreRowModel: _getCoreRowModel(),
|
|
140
|
+
getSortedRowModel: sortable ? _getSortedRowModel() : undefined,
|
|
92
141
|
state: {
|
|
93
142
|
sorting: sortable ? sorting : undefined,
|
|
94
143
|
rowSelection: selectable ? rowSelection : undefined,
|
|
@@ -178,9 +227,9 @@ function TableRoot<T>({
|
|
|
178
227
|
</caption>
|
|
179
228
|
)}
|
|
180
229
|
<thead className={styles.thead}>
|
|
181
|
-
{table.getHeaderGroups().map((headerGroup) => (
|
|
230
|
+
{table.getHeaderGroups().map((headerGroup: any) => (
|
|
182
231
|
<tr key={headerGroup.id} className={styles.headerRow}>
|
|
183
|
-
{headerGroup.headers.map((header) => {
|
|
232
|
+
{headerGroup.headers.map((header: any) => {
|
|
184
233
|
const canSort = sortable && header.column.getCanSort();
|
|
185
234
|
const sortDirection = header.column.getIsSorted();
|
|
186
235
|
const toggleSorting = canSort ? header.column.getToggleSortingHandler() : undefined;
|
|
@@ -210,7 +259,7 @@ function TableRoot<T>({
|
|
|
210
259
|
<span className={styles.headerContent}>
|
|
211
260
|
{header.isPlaceholder
|
|
212
261
|
? null
|
|
213
|
-
:
|
|
262
|
+
: _flexRender(
|
|
214
263
|
header.column.columnDef.header,
|
|
215
264
|
header.getContext()
|
|
216
265
|
)}
|
|
@@ -229,7 +278,7 @@ function TableRoot<T>({
|
|
|
229
278
|
<div className={styles.headerContent}>
|
|
230
279
|
{header.isPlaceholder
|
|
231
280
|
? null
|
|
232
|
-
:
|
|
281
|
+
: _flexRender(
|
|
233
282
|
header.column.columnDef.header,
|
|
234
283
|
header.getContext()
|
|
235
284
|
)}
|
|
@@ -242,7 +291,7 @@ function TableRoot<T>({
|
|
|
242
291
|
))}
|
|
243
292
|
</thead>
|
|
244
293
|
<tbody className={styles.tbody}>
|
|
245
|
-
{table.getRowModel().rows.map((row) => {
|
|
294
|
+
{table.getRowModel().rows.map((row: any) => {
|
|
246
295
|
const isClickable = !!onRowClick;
|
|
247
296
|
const isSelected = selectable ? row.getIsSelected() : false;
|
|
248
297
|
const handleRowClick = (event: React.MouseEvent<HTMLTableRowElement>) => {
|
|
@@ -275,9 +324,9 @@ function TableRoot<T>({
|
|
|
275
324
|
tabIndex={isClickable ? 0 : undefined}
|
|
276
325
|
data-selected={isSelected || undefined}
|
|
277
326
|
>
|
|
278
|
-
{row.getVisibleCells().map((cell) => (
|
|
327
|
+
{row.getVisibleCells().map((cell: any) => (
|
|
279
328
|
<td key={cell.id} className={styles.td} style={getColumnSizeStyle(cell.column)}>
|
|
280
|
-
{
|
|
329
|
+
{_flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
281
330
|
</td>
|
|
282
331
|
))}
|
|
283
332
|
</tr>
|
|
@@ -366,9 +415,6 @@ export function createColumns<T>(
|
|
|
366
415
|
}));
|
|
367
416
|
}
|
|
368
417
|
|
|
369
|
-
// Re-export useful types
|
|
370
|
-
export type { ColumnDef, SortingState, RowSelectionState };
|
|
371
|
-
|
|
372
418
|
export const Table = Object.assign(TableRoot, {
|
|
373
419
|
Root: TableRoot,
|
|
374
420
|
Columns: createColumns,
|
package/src/index.ts
CHANGED
|
@@ -529,6 +529,31 @@ export {
|
|
|
529
529
|
// Assets
|
|
530
530
|
export { FragmentsLogo, type FragmentsLogoProps } from './assets/fragments-logo';
|
|
531
531
|
|
|
532
|
+
// NavigationMenu
|
|
533
|
+
export {
|
|
534
|
+
NavigationMenu,
|
|
535
|
+
NavigationMenuRoot,
|
|
536
|
+
NavigationMenuList,
|
|
537
|
+
NavigationMenuItem,
|
|
538
|
+
NavigationMenuTrigger,
|
|
539
|
+
NavigationMenuContent,
|
|
540
|
+
NavigationMenuLink,
|
|
541
|
+
NavigationMenuIndicator,
|
|
542
|
+
NavigationMenuViewport,
|
|
543
|
+
NavigationMenuMobileContent,
|
|
544
|
+
NavigationMenuMobileSection,
|
|
545
|
+
type NavigationMenuProps,
|
|
546
|
+
type NavigationMenuListProps,
|
|
547
|
+
type NavigationMenuItemProps,
|
|
548
|
+
type NavigationMenuTriggerProps,
|
|
549
|
+
type NavigationMenuContentProps,
|
|
550
|
+
type NavigationMenuLinkProps,
|
|
551
|
+
type NavigationMenuIndicatorProps,
|
|
552
|
+
type NavigationMenuViewportProps,
|
|
553
|
+
type NavigationMenuMobileContentProps,
|
|
554
|
+
type NavigationMenuMobileSectionProps,
|
|
555
|
+
} from './components/NavigationMenu';
|
|
556
|
+
|
|
532
557
|
// Accessibility Utilities
|
|
533
558
|
export {
|
|
534
559
|
useId,
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
@use 'sass:math';
|
|
13
13
|
|
|
14
14
|
// --------------------------------------------
|
|
15
|
-
//
|
|
15
|
+
// Stone - Muted cool neutrals (Default)
|
|
16
16
|
// Subtle, balanced feel - matches current zinc-like palette
|
|
17
17
|
// --------------------------------------------
|
|
18
|
-
$palette-
|
|
18
|
+
$palette-stone: (
|
|
19
19
|
50: #fafafa,
|
|
20
20
|
100: #f4f4f5,
|
|
21
21
|
200: #e4e4e7,
|
|
@@ -105,11 +105,12 @@ $palette-fire: (
|
|
|
105
105
|
// Palette Map for Dynamic Access
|
|
106
106
|
// --------------------------------------------
|
|
107
107
|
$palettes: (
|
|
108
|
-
"
|
|
108
|
+
"stone": $palette-stone,
|
|
109
109
|
"ice": $palette-ice,
|
|
110
110
|
"sand": $palette-sand,
|
|
111
111
|
"earth": $palette-earth,
|
|
112
|
-
"fire": $palette-fire
|
|
112
|
+
"fire": $palette-fire,
|
|
113
|
+
"wind": $palette-stone // Legacy alias — "wind" was renamed to "stone"
|
|
113
114
|
);
|
|
114
115
|
|
|
115
116
|
// --------------------------------------------
|
|
@@ -117,12 +118,12 @@ $palettes: (
|
|
|
117
118
|
// --------------------------------------------
|
|
118
119
|
|
|
119
120
|
/// Get a palette map by name
|
|
120
|
-
/// @param {String} $name - Palette name (
|
|
121
|
+
/// @param {String} $name - Palette name (stone, ice, sand, earth, fire)
|
|
121
122
|
/// @return {Map} The palette map with shades 50-950
|
|
122
123
|
@function get-palette($name) {
|
|
123
124
|
@if not map.has-key($palettes, $name) {
|
|
124
|
-
@warn "Unknown palette '#{$name}'. Using '
|
|
125
|
-
@return $palette-
|
|
125
|
+
@warn "Unknown palette '#{$name}'. Using 'stone' as fallback.";
|
|
126
|
+
@return $palette-stone;
|
|
126
127
|
}
|
|
127
128
|
@return map.get($palettes, $name);
|
|
128
129
|
}
|
package/src/tokens/_seeds.scss
CHANGED
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
$fui-brand: #18181b !default;
|
|
23
23
|
|
|
24
24
|
/// Neutral palette name for surfaces, text, and borders
|
|
25
|
-
/// Options: "
|
|
25
|
+
/// Options: "stone" | "ice" | "earth" | "sand" | "fire"
|
|
26
26
|
/// @type String
|
|
27
|
-
$fui-neutral: "
|
|
27
|
+
$fui-neutral: "stone" !default;
|
|
28
28
|
|
|
29
29
|
/// Density preset controlling spacing scale
|
|
30
30
|
/// Options: "compact" | "default" | "relaxed"
|