@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.
Files changed (34) hide show
  1. package/README.md +1 -1
  2. package/fragments.json +1 -1
  3. package/package.json +29 -7
  4. package/src/blocks/NavigationHeader.block.ts +89 -0
  5. package/src/components/AppShell/AppShell.fragment.tsx +26 -25
  6. package/src/components/AppShell/AppShell.module.scss +18 -19
  7. package/src/components/AppShell/index.tsx +37 -12
  8. package/src/components/Chart/Chart.fragment.tsx +3 -0
  9. package/src/components/Chart/index.tsx +62 -14
  10. package/src/components/CodeBlock/CodeBlock.fragment.tsx +3 -0
  11. package/src/components/CodeBlock/CodeBlock.test.tsx +6 -6
  12. package/src/components/CodeBlock/index.tsx +38 -3
  13. package/src/components/ColorPicker/ColorPicker.fragment.tsx +3 -0
  14. package/src/components/ColorPicker/index.tsx +24 -2
  15. package/src/components/DatePicker/DatePicker.fragment.tsx +4 -0
  16. package/src/components/DatePicker/index.tsx +101 -43
  17. package/src/components/Header/Header.fragment.tsx +45 -8
  18. package/src/components/Link/Link.fragment.tsx +17 -0
  19. package/src/components/Link/Link.test.tsx +23 -0
  20. package/src/components/Link/index.tsx +20 -0
  21. package/src/components/Markdown/Markdown.fragment.tsx +4 -0
  22. package/src/components/NavigationMenu/NavigationMenu.fragment.tsx +270 -0
  23. package/src/components/NavigationMenu/NavigationMenu.module.scss +516 -0
  24. package/src/components/NavigationMenu/NavigationMenu.test.tsx +457 -0
  25. package/src/components/NavigationMenu/NavigationMenuContext.ts +89 -0
  26. package/src/components/NavigationMenu/index.tsx +854 -0
  27. package/src/components/NavigationMenu/useNavigationMenu.ts +91 -0
  28. package/src/components/Sidebar/Sidebar.fragment.tsx +1 -1
  29. package/src/components/Sidebar/Sidebar.module.scss +2 -1
  30. package/src/components/Table/Table.fragment.tsx +3 -0
  31. package/src/components/Table/index.tsx +70 -24
  32. package/src/index.ts +25 -0
  33. package/src/tokens/_palettes.scss +8 -7
  34. 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', 'CollapseToggle', 'Rail', 'MenuSkeleton', 'SectionAction'],
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: 100vh;
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
- // Column definition helper type
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
- const table = useReactTable({
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: getCoreRowModel(),
91
- getSortedRowModel: sortable ? getSortedRowModel() : undefined,
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
- : flexRender(
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
- : flexRender(
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
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
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
- // Wind - Muted cool neutrals (Default)
15
+ // Stone - Muted cool neutrals (Default)
16
16
  // Subtle, balanced feel - matches current zinc-like palette
17
17
  // --------------------------------------------
18
- $palette-wind: (
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
- "wind": $palette-wind,
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 (wind, ice, sand, earth, fire)
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 'wind' as fallback.";
125
- @return $palette-wind;
125
+ @warn "Unknown palette '#{$name}'. Using 'stone' as fallback.";
126
+ @return $palette-stone;
126
127
  }
127
128
  @return map.get($palettes, $name);
128
129
  }
@@ -22,9 +22,9 @@
22
22
  $fui-brand: #18181b !default;
23
23
 
24
24
  /// Neutral palette name for surfaces, text, and borders
25
- /// Options: "wind" | "ice" | "earth" | "sand" | "fire"
25
+ /// Options: "stone" | "ice" | "earth" | "sand" | "fire"
26
26
  /// @type String
27
- $fui-neutral: "wind" !default;
27
+ $fui-neutral: "stone" !default;
28
28
 
29
29
  /// Density preset controlling spacing scale
30
30
  /// Options: "compact" | "default" | "relaxed"