@fragments-sdk/ui 0.8.8 → 0.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.
Files changed (48) hide show
  1. package/fragments.json +1 -1
  2. package/package.json +2 -2
  3. package/src/components/Accordion/Accordion.module.scss +1 -1
  4. package/src/components/Accordion/Accordion.test.tsx +1 -2
  5. package/src/components/Avatar/Avatar.fragment.tsx +18 -0
  6. package/src/components/Avatar/Avatar.test.tsx +18 -0
  7. package/src/components/Avatar/index.tsx +16 -0
  8. package/src/components/Badge/index.tsx +2 -0
  9. package/src/components/BentoGrid/BentoGrid.fragment.tsx +147 -0
  10. package/src/components/BentoGrid/BentoGrid.module.scss +123 -0
  11. package/src/components/BentoGrid/BentoGrid.test.tsx +140 -0
  12. package/src/components/BentoGrid/index.tsx +150 -0
  13. package/src/components/Button/index.tsx +2 -0
  14. package/src/components/Card/index.tsx +2 -0
  15. package/src/components/Chart/Chart.test.tsx +2 -2
  16. package/src/components/Checkbox/index.tsx +2 -0
  17. package/src/components/CodeBlock/index.tsx +1 -1
  18. package/src/components/Command/Command.test.tsx +1 -1
  19. package/src/components/Command/index.tsx +1 -1
  20. package/src/components/DatePicker/index.tsx +1 -1
  21. package/src/components/Dialog/index.tsx +2 -0
  22. package/src/components/Drawer/index.tsx +2 -0
  23. package/src/components/EmptyState/index.tsx +2 -0
  24. package/src/components/Field/index.tsx +2 -0
  25. package/src/components/Fieldset/index.tsx +2 -0
  26. package/src/components/Form/index.tsx +2 -0
  27. package/src/components/Header/Header.module.scss +4 -0
  28. package/src/components/Icon/index.tsx +2 -0
  29. package/src/components/List/index.tsx +2 -0
  30. package/src/components/Menu/index.tsx +2 -0
  31. package/src/components/NavigationMenu/NavigationMenu.module.scss +1 -2
  32. package/src/components/NavigationMenu/NavigationMenuContext.ts +2 -0
  33. package/src/components/NavigationMenu/index.tsx +51 -24
  34. package/src/components/NavigationMenu/useNavigationMenu.ts +3 -0
  35. package/src/components/Pagination/index.tsx +2 -0
  36. package/src/components/Popover/index.tsx +2 -0
  37. package/src/components/Progress/index.tsx +2 -0
  38. package/src/components/RadioGroup/index.tsx +2 -0
  39. package/src/components/Separator/index.tsx +2 -0
  40. package/src/components/Switch/index.ts +2 -0
  41. package/src/components/Theme/index.tsx +4 -3
  42. package/src/components/Toggle/index.tsx +2 -0
  43. package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +96 -79
  44. package/src/components/ToggleGroup/index.tsx +2 -0
  45. package/src/components/Tooltip/index.tsx +2 -0
  46. package/src/index.ts +3 -2
  47. package/src/styles/globals.scss +5 -0
  48. package/src/tokens/_variables.scss +1 -1
@@ -0,0 +1,150 @@
1
+ import * as React from 'react';
2
+ import styles from './BentoGrid.module.scss';
3
+ // Import globals to ensure CSS variables are defined
4
+ import '../../styles/globals.scss';
5
+
6
+ // ============================================
7
+ // Types
8
+ // ============================================
9
+
10
+ type SpanValue = 1 | 2 | 3;
11
+
12
+ export interface ResponsiveSpan {
13
+ /** Default (mobile-first) */
14
+ base?: SpanValue;
15
+ /** ≥640px */
16
+ sm?: SpanValue;
17
+ /** ≥768px */
18
+ md?: SpanValue;
19
+ /** ≥1024px */
20
+ lg?: SpanValue;
21
+ /** ≥1280px */
22
+ xl?: SpanValue;
23
+ }
24
+
25
+ export interface BentoGridProps {
26
+ children?: React.ReactNode;
27
+ /** Number of columns (default: 3) — auto-collapses responsively */
28
+ columns?: 2 | 3 | 4;
29
+ /** Gap between grid items */
30
+ gap?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
31
+ /** Additional class name */
32
+ className?: string;
33
+ /** Inline styles */
34
+ style?: React.CSSProperties;
35
+ }
36
+
37
+ export interface BentoGridItemProps {
38
+ children?: React.ReactNode;
39
+ /** Columns to span — number for all breakpoints, object for per-breakpoint */
40
+ colSpan?: SpanValue | ResponsiveSpan;
41
+ /** Rows to span — number for all breakpoints, object for per-breakpoint */
42
+ rowSpan?: SpanValue | ResponsiveSpan;
43
+ /** Additional class name */
44
+ className?: string;
45
+ }
46
+
47
+ // ============================================
48
+ // Helpers
49
+ // ============================================
50
+
51
+ const gapClasses: Record<NonNullable<BentoGridProps['gap']>, string> = {
52
+ none: styles.gapNone,
53
+ xs: styles.gapXs,
54
+ sm: styles.gapSm,
55
+ md: styles.gapMd,
56
+ lg: styles.gapLg,
57
+ xl: styles.gapXl,
58
+ };
59
+
60
+ function getSpanVars(
61
+ span: SpanValue | ResponsiveSpan | undefined,
62
+ prefix: string
63
+ ): Record<string, number> {
64
+ if (!span || span === 1) return {};
65
+ if (typeof span === 'number') return { [`--${prefix}`]: span };
66
+ const vars: Record<string, number> = {};
67
+ if (span.base && span.base > 1) vars[`--${prefix}`] = span.base;
68
+ if (span.sm) vars[`--${prefix}-sm`] = span.sm;
69
+ if (span.md) vars[`--${prefix}-md`] = span.md;
70
+ if (span.lg) vars[`--${prefix}-lg`] = span.lg;
71
+ if (span.xl) vars[`--${prefix}-xl`] = span.xl;
72
+ return vars;
73
+ }
74
+
75
+ // ============================================
76
+ // BentoGrid Component
77
+ // ============================================
78
+
79
+ export const BentoGrid = React.forwardRef<HTMLDivElement, BentoGridProps>(
80
+ function BentoGrid(
81
+ {
82
+ children,
83
+ columns = 3,
84
+ gap = 'md',
85
+ className,
86
+ style,
87
+ },
88
+ ref
89
+ ) {
90
+ const classes = [
91
+ styles.grid,
92
+ styles[`columns${columns}`],
93
+ gapClasses[gap],
94
+ className,
95
+ ]
96
+ .filter(Boolean)
97
+ .join(' ');
98
+
99
+ return (
100
+ <div ref={ref} className={classes} style={style}>
101
+ {children}
102
+ </div>
103
+ );
104
+ }
105
+ ) as BentoGridComponent;
106
+
107
+ // ============================================
108
+ // BentoGrid.Item Sub-component
109
+ // ============================================
110
+
111
+ const BentoGridItem = React.forwardRef<HTMLDivElement, BentoGridItemProps>(
112
+ function BentoGridItem(
113
+ {
114
+ children,
115
+ colSpan,
116
+ rowSpan,
117
+ className,
118
+ },
119
+ ref
120
+ ) {
121
+ const spanVars = {
122
+ ...getSpanVars(colSpan, 'bento-col-span'),
123
+ ...getSpanVars(rowSpan, 'bento-row-span'),
124
+ };
125
+
126
+ const hasVars = Object.keys(spanVars).length > 0;
127
+ const inlineStyle = hasVars
128
+ ? (spanVars as unknown as React.CSSProperties)
129
+ : undefined;
130
+
131
+ const classes = [styles.item, className].filter(Boolean).join(' ');
132
+
133
+ return (
134
+ <div ref={ref} className={classes} style={inlineStyle}>
135
+ {children}
136
+ </div>
137
+ );
138
+ }
139
+ );
140
+
141
+ // ============================================
142
+ // Compound component type
143
+ // ============================================
144
+
145
+ interface BentoGridComponent
146
+ extends React.ForwardRefExoticComponent<BentoGridProps & React.RefAttributes<HTMLDivElement>> {
147
+ Item: typeof BentoGridItem;
148
+ }
149
+
150
+ (BentoGrid as BentoGridComponent).Item = BentoGridItem;
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import * as React from 'react';
2
4
  import { Button as BaseButton } from '@base-ui/react/button';
3
5
  import styles from './Button.module.scss';
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import * as React from 'react';
2
4
  import styles from './Card.module.scss';
3
5
  // Import globals to ensure CSS variables are defined
@@ -11,8 +11,8 @@ import {
11
11
 
12
12
  // Mock recharts to avoid SVG rendering issues in jsdom
13
13
  vi.mock('recharts', () => ({
14
- Tooltip: ({ content, ...props }: any) => <div data-testid="recharts-tooltip" {...props} />,
15
- Legend: ({ content, ...props }: any) => <div data-testid="recharts-legend" {...props} />,
14
+ Tooltip: ({ content: _content, ...props }: any) => <div data-testid="recharts-tooltip" {...props} />,
15
+ Legend: ({ content: _content, ...props }: any) => <div data-testid="recharts-legend" {...props} />,
16
16
  }));
17
17
 
18
18
  const config: ChartConfig = {
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import * as React from 'react';
2
4
  import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox';
3
5
  import styles from './Checkbox.module.scss';
@@ -668,7 +668,7 @@ const CodeBlockBase = React.forwardRef<HTMLDivElement, CodeBlockProps>(
668
668
  : {};
669
669
 
670
670
  return (
671
- <div ref={ref} {...htmlProps} className={classNames}>
671
+ <div ref={ref} {...htmlProps} className={classNames} data-theme="dark">
672
672
  {title && <div className={styles.title}>{title}</div>}
673
673
  <div className={wrapperClasses}>
674
674
  {shouldRenderHeader && (
@@ -283,7 +283,7 @@ describe('Command', () => {
283
283
  const user = userEvent.setup();
284
284
  const onSearchChange = vi.fn();
285
285
 
286
- const { rerender } = render(
286
+ render(
287
287
  <Command search="open" onSearchChange={onSearchChange}>
288
288
  <Command.Input placeholder="Search..." />
289
289
  <Command.List>
@@ -53,7 +53,7 @@ export interface CommandEmptyProps extends React.HTMLAttributes<HTMLDivElement>
53
53
  children: React.ReactNode;
54
54
  }
55
55
 
56
- export interface CommandSeparatorProps extends React.HTMLAttributes<HTMLDivElement> {}
56
+ export type CommandSeparatorProps = React.HTMLAttributes<HTMLDivElement>;
57
57
 
58
58
  // ============================================
59
59
  // Default filter
@@ -350,7 +350,7 @@ function DatePickerRoot({
350
350
  // matches shadcn behavior and avoids premature close on first
351
351
  // click or preset selection.
352
352
  },
353
- [selectedRangeProp, onRangeSelect, isControlledOpen, onOpenChange]
353
+ [selectedRangeProp, onRangeSelect]
354
354
  );
355
355
 
356
356
  const defaultPlaceholder = mode === 'range' ? 'Select date range' : 'Pick a date';
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import * as React from 'react';
2
4
  import { Dialog as BaseDialog } from '@base-ui/react/dialog';
3
5
  import styles from './Dialog.module.scss';
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import * as React from 'react';
2
4
  import { Dialog as BaseDialog } from '@base-ui/react/dialog';
3
5
  import styles from './Drawer.module.scss';
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import * as React from 'react';
2
4
  import styles from './EmptyState.module.scss';
3
5
  // Import globals to ensure CSS variables are defined
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import * as React from 'react';
2
4
  import { Field as BaseField, type FieldValidityState } from '@base-ui/react/field';
3
5
  import styles from './Field.module.scss';
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import * as React from 'react';
2
4
  import { Fieldset as BaseFieldset } from '@base-ui/react/fieldset';
3
5
  import styles from './Fieldset.module.scss';
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import * as React from 'react';
2
4
  import { Form as BaseForm } from '@base-ui/react/form';
3
5
  import styles from './Form.module.scss';
@@ -33,6 +33,10 @@
33
33
  gap: var(--fui-space-4, $fui-space-4);
34
34
  width: 100%;
35
35
  max-width: 100%;
36
+
37
+ @include below-md {
38
+ gap: var(--fui-space-1, $fui-space-1);
39
+ }
36
40
  }
37
41
 
38
42
  // ============================================
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import * as React from 'react';
2
4
  import type { IconProps as PhosphorIconProps } from '@phosphor-icons/react';
3
5
  import styles from './Icon.module.scss';
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import * as React from 'react';
2
4
  import styles from './List.module.scss';
3
5
  import '../../styles/globals.scss';
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import * as React from 'react';
2
4
  import { Menu as BaseMenu } from '@base-ui/react/menu';
3
5
  import styles from './Menu.module.scss';
@@ -277,7 +277,6 @@
277
277
  width: var(--fui-navmenu-viewport-width);
278
278
  height: var(--fui-navmenu-viewport-height);
279
279
  transition:
280
- left var(--fui-transition-fast, $fui-transition-fast),
281
280
  opacity var(--fui-transition-fast, $fui-transition-fast);
282
281
 
283
282
  // Hidden when no item is open
@@ -372,7 +371,7 @@
372
371
  display: flex;
373
372
  align-items: center;
374
373
  justify-content: space-between;
375
- padding: var(--fui-space-3, $fui-space-3) var(--fui-padding-container-md, $fui-padding-container-md);
374
+ padding: 0 var(--fui-padding-container-md, $fui-padding-container-md);
376
375
  border-bottom: 1px solid var(--fui-border, $fui-border);
377
376
  min-height: var(--fui-appshell-header-height, $fui-appshell-header-height);
378
377
  }
@@ -53,6 +53,8 @@ export interface NavigationMenuContextValue {
53
53
  setMobileOpen: (open: boolean) => void;
54
54
  mobileContentChildren: React.ReactNode;
55
55
  setMobileContentChildren: (children: React.ReactNode) => void;
56
+ mobileBrandChildren: React.ReactNode;
57
+ setMobileBrandChildren: (children: React.ReactNode) => void;
56
58
 
57
59
  // Root nav id
58
60
  rootId: string;
@@ -89,6 +89,10 @@ export interface NavigationMenuMobileContentProps {
89
89
  children: React.ReactNode;
90
90
  }
91
91
 
92
+ export interface NavigationMenuMobileBrandProps {
93
+ children: React.ReactNode;
94
+ }
95
+
92
96
  export interface NavigationMenuMobileSectionProps {
93
97
  children: React.ReactNode;
94
98
  /** Section heading label */
@@ -664,6 +668,22 @@ function NavigationMenuMobileContent({ children }: NavigationMenuMobileContentPr
664
668
  return null;
665
669
  }
666
670
 
671
+ // ============================================
672
+ // MobileBrand (slot for brand in mobile drawer header)
673
+ // ============================================
674
+
675
+ function NavigationMenuMobileBrand({ children }: NavigationMenuMobileBrandProps) {
676
+ const ctx = useNavigationMenuContext();
677
+
678
+ React.useEffect(() => {
679
+ ctx.setMobileBrandChildren(children);
680
+ return () => ctx.setMobileBrandChildren(null);
681
+ // eslint-disable-next-line react-hooks/exhaustive-deps
682
+ }, [children]);
683
+
684
+ return null;
685
+ }
686
+
667
687
  // ============================================
668
688
  // MobileSection
669
689
  // ============================================
@@ -755,7 +775,7 @@ function MobileDrawer() {
755
775
  aria-label="Navigation"
756
776
  >
757
777
  <div className={styles.drawerHeader}>
758
- <span />
778
+ {ctx.mobileBrandChildren ?? <span />}
759
779
  <button
760
780
  type="button"
761
781
  className={styles.drawerClose}
@@ -766,29 +786,34 @@ function MobileDrawer() {
766
786
  </button>
767
787
  </div>
768
788
  <ScrollArea orientation="vertical" className={styles.drawerBody}>
769
- <div className={styles.drawerNav}>
770
- {autoItems.map((item) =>
771
- item.contentChildren ? (
772
- <MobileCollapsibleSection
773
- key={item.value}
774
- label={item.triggerLabel}
775
- onLinkClick={handleLinkClick}
776
- >
777
- {item.contentChildren}
778
- </MobileCollapsibleSection>
779
- ) : item.linkHref ? (
780
- <a
781
- key={item.value}
782
- className={styles.drawerLink}
783
- href={item.linkHref}
784
- onClick={handleLinkClick}
785
- >
786
- {item.triggerLabel}
787
- </a>
788
- ) : null
789
- )}
790
- </div>
791
- {ctx.mobileContentChildren}
789
+ {/* When MobileContent is provided, it takes full control of the drawer nav.
790
+ Otherwise, auto-convert registered Trigger+Content items. */}
791
+ {ctx.mobileContentChildren ? (
792
+ ctx.mobileContentChildren
793
+ ) : (
794
+ <div className={styles.drawerNav}>
795
+ {autoItems.map((item) =>
796
+ item.contentChildren ? (
797
+ <MobileCollapsibleSection
798
+ key={item.value}
799
+ label={item.triggerLabel}
800
+ onLinkClick={handleLinkClick}
801
+ >
802
+ {item.contentChildren}
803
+ </MobileCollapsibleSection>
804
+ ) : item.linkHref ? (
805
+ <a
806
+ key={item.value}
807
+ className={styles.drawerLink}
808
+ href={item.linkHref}
809
+ onClick={handleLinkClick}
810
+ >
811
+ {item.triggerLabel}
812
+ </a>
813
+ ) : null
814
+ )}
815
+ </div>
816
+ )}
792
817
  </ScrollArea>
793
818
  </div>
794
819
  </>
@@ -837,6 +862,7 @@ export const NavigationMenu = Object.assign(NavigationMenuRoot, {
837
862
  Link: NavigationMenuLink,
838
863
  Indicator: NavigationMenuIndicator,
839
864
  Viewport: NavigationMenuViewport,
865
+ MobileBrand: NavigationMenuMobileBrand,
840
866
  MobileContent: NavigationMenuMobileContent,
841
867
  MobileSection: NavigationMenuMobileSection,
842
868
  });
@@ -850,6 +876,7 @@ export {
850
876
  NavigationMenuLink,
851
877
  NavigationMenuIndicator,
852
878
  NavigationMenuViewport,
879
+ NavigationMenuMobileBrand,
853
880
  NavigationMenuMobileContent,
854
881
  NavigationMenuMobileSection,
855
882
  };
@@ -57,6 +57,7 @@ export function useNavigationMenu({
57
57
  // Mobile state
58
58
  const [mobileOpen, setMobileOpen] = React.useState(false);
59
59
  const [mobileContentChildren, setMobileContentChildren] = React.useState<React.ReactNode>(null);
60
+ const [mobileBrandChildren, setMobileBrandChildren] = React.useState<React.ReactNode>(null);
60
61
 
61
62
  // Clean up timers on unmount
62
63
  React.useEffect(() => {
@@ -87,5 +88,7 @@ export function useNavigationMenu({
87
88
  setMobileOpen,
88
89
  mobileContentChildren,
89
90
  setMobileContentChildren,
91
+ mobileBrandChildren,
92
+ setMobileBrandChildren,
90
93
  };
91
94
  }
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import * as React from 'react';
2
4
  import styles from './Pagination.module.scss';
3
5
  import '../../styles/globals.scss';
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import * as React from 'react';
2
4
  import { Popover as BasePopover } from '@base-ui/react/popover';
3
5
  import styles from './Popover.module.scss';
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import * as React from 'react';
2
4
  import { Progress as BaseProgress } from '@base-ui/react/progress';
3
5
  import styles from './Progress.module.scss';
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import * as React from 'react';
2
4
  import { RadioGroup as BaseRadioGroup } from '@base-ui/react/radio-group';
3
5
  import { Radio as BaseRadio } from '@base-ui/react/radio';
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import * as React from 'react';
2
4
  import { Separator as BaseSeparator } from '@base-ui/react/separator';
3
5
  import styles from './Separator.module.scss';
@@ -1 +1,3 @@
1
+ 'use client';
2
+
1
3
  export { Switch, type SwitchProps, Toggle, type ToggleProps } from '../Toggle';
@@ -206,9 +206,10 @@ function ThemeProvider({
206
206
  setMounted(true);
207
207
  }, [isControlled, storageKey]);
208
208
 
209
- // Apply theme to DOM
209
+ // Apply theme to DOM — skip until mounted so we don't overwrite
210
+ // the inline script that prevents flash on initial page load
210
211
  React.useEffect(() => {
211
- if (typeof document === 'undefined') return;
212
+ if (typeof document === 'undefined' || !mounted) return;
212
213
 
213
214
  const root = document.documentElement;
214
215
 
@@ -218,7 +219,7 @@ function ThemeProvider({
218
219
  root.classList.remove('light', 'dark');
219
220
  root.classList.add(resolvedMode);
220
221
  }
221
- }, [resolvedMode, attribute]);
222
+ }, [resolvedMode, attribute, mounted]);
222
223
 
223
224
  // Persist to localStorage when mode changes
224
225
  React.useEffect(() => {
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import * as React from 'react';
2
4
  import { Switch as BaseSwitch } from '@base-ui/react/switch';
3
5
  import styles from './Toggle.module.scss';