@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.
- package/fragments.json +1 -1
- package/package.json +2 -2
- package/src/components/Accordion/Accordion.module.scss +1 -1
- package/src/components/Accordion/Accordion.test.tsx +1 -2
- package/src/components/Avatar/Avatar.fragment.tsx +18 -0
- package/src/components/Avatar/Avatar.test.tsx +18 -0
- package/src/components/Avatar/index.tsx +16 -0
- package/src/components/Badge/index.tsx +2 -0
- package/src/components/BentoGrid/BentoGrid.fragment.tsx +147 -0
- package/src/components/BentoGrid/BentoGrid.module.scss +123 -0
- package/src/components/BentoGrid/BentoGrid.test.tsx +140 -0
- package/src/components/BentoGrid/index.tsx +150 -0
- package/src/components/Button/index.tsx +2 -0
- package/src/components/Card/index.tsx +2 -0
- package/src/components/Chart/Chart.test.tsx +2 -2
- package/src/components/Checkbox/index.tsx +2 -0
- package/src/components/CodeBlock/index.tsx +1 -1
- package/src/components/Command/Command.test.tsx +1 -1
- package/src/components/Command/index.tsx +1 -1
- package/src/components/DatePicker/index.tsx +1 -1
- package/src/components/Dialog/index.tsx +2 -0
- package/src/components/Drawer/index.tsx +2 -0
- package/src/components/EmptyState/index.tsx +2 -0
- package/src/components/Field/index.tsx +2 -0
- package/src/components/Fieldset/index.tsx +2 -0
- package/src/components/Form/index.tsx +2 -0
- package/src/components/Header/Header.module.scss +4 -0
- package/src/components/Icon/index.tsx +2 -0
- package/src/components/List/index.tsx +2 -0
- package/src/components/Menu/index.tsx +2 -0
- package/src/components/NavigationMenu/NavigationMenu.module.scss +1 -2
- package/src/components/NavigationMenu/NavigationMenuContext.ts +2 -0
- package/src/components/NavigationMenu/index.tsx +51 -24
- package/src/components/NavigationMenu/useNavigationMenu.ts +3 -0
- package/src/components/Pagination/index.tsx +2 -0
- package/src/components/Popover/index.tsx +2 -0
- package/src/components/Progress/index.tsx +2 -0
- package/src/components/RadioGroup/index.tsx +2 -0
- package/src/components/Separator/index.tsx +2 -0
- package/src/components/Switch/index.ts +2 -0
- package/src/components/Theme/index.tsx +4 -3
- package/src/components/Toggle/index.tsx +2 -0
- package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +96 -79
- package/src/components/ToggleGroup/index.tsx +2 -0
- package/src/components/Tooltip/index.tsx +2 -0
- package/src/index.ts +3 -2
- package/src/styles/globals.scss +5 -0
- 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;
|
|
@@ -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 = {
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
353
|
+
[selectedRangeProp, onRangeSelect]
|
|
354
354
|
);
|
|
355
355
|
|
|
356
356
|
const defaultPlaceholder = mode === 'range' ? 'Select date range' : 'Pick a date';
|
|
@@ -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:
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
}
|
|
@@ -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(() => {
|