@reactberry/system 2.0.0-beta
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 +48 -0
- package/package.json +74 -0
- package/src/blocks/Accordion/index.tsx +158 -0
- package/src/blocks/AnimatedCarousel/index.tsx +188 -0
- package/src/blocks/AppleGlow/index.tsx +144 -0
- package/src/blocks/Avatar/index.tsx +167 -0
- package/src/blocks/Await/index.tsx +45 -0
- package/src/blocks/Cards/AnimatedCard/index.tsx +175 -0
- package/src/blocks/Cards/FluorescentCard/index.tsx +180 -0
- package/src/blocks/Cards/InfoCard/index.tsx +206 -0
- package/src/blocks/Cards/TickerCard/index.tsx +125 -0
- package/src/blocks/Carousel/index.tsx +216 -0
- package/src/blocks/Checkbox/index.tsx +101 -0
- package/src/blocks/Collection/index.tsx +59 -0
- package/src/blocks/Container/index.tsx +55 -0
- package/src/blocks/Controls/Control.tsx +67 -0
- package/src/blocks/Controls/index.tsx +11 -0
- package/src/blocks/CyclingNumber/index.tsx +78 -0
- package/src/blocks/DisplaySet/index.tsx +42 -0
- package/src/blocks/Divider/index.tsx +14 -0
- package/src/blocks/Draggable/index.tsx +266 -0
- package/src/blocks/Drawer/index.tsx +136 -0
- package/src/blocks/DynamicIsland/DynamicIsland.tsx +89 -0
- package/src/blocks/DynamicIsland/index.tsx +2 -0
- package/src/blocks/Fader/index.tsx +145 -0
- package/src/blocks/FamilyDrawer/README.md +116 -0
- package/src/blocks/FamilyDrawer/example.tsx +108 -0
- package/src/blocks/FamilyDrawer/index.tsx +119 -0
- package/src/blocks/FamilyDrawer/views/DefaultView.tsx +93 -0
- package/src/blocks/FamilyDrawer/views/KeyView.tsx +129 -0
- package/src/blocks/FamilyDrawer/views/PhraseView.tsx +129 -0
- package/src/blocks/FamilyDrawer/views/RemoveView.tsx +81 -0
- package/src/blocks/FieldSet/index.tsx +173 -0
- package/src/blocks/Filesystem/index.tsx +198 -0
- package/src/blocks/Gallery/Carousel/index.tsx +257 -0
- package/src/blocks/Gallery/Modal/index.tsx +83 -0
- package/src/blocks/Gallery/index.tsx +57 -0
- package/src/blocks/Gallery/utils/animationVariants.ts +18 -0
- package/src/blocks/Gallery/utils/aspectRatio.ts +14 -0
- package/src/blocks/Gallery/utils/downloadPhoto.ts +24 -0
- package/src/blocks/Gallery/utils/range.ts +11 -0
- package/src/blocks/GradientMesh/index.tsx +106 -0
- package/src/blocks/Group/index.tsx +152 -0
- package/src/blocks/Heading/index.tsx +111 -0
- package/src/blocks/HorizontalScroller/index.tsx +135 -0
- package/src/blocks/Icon/index.tsx +45 -0
- package/src/blocks/Indicator/index.tsx +27 -0
- package/src/blocks/InlineEditor/index.tsx +216 -0
- package/src/blocks/List/index.tsx +657 -0
- package/src/blocks/Main/index.tsx +17 -0
- package/src/blocks/Marquee/index.tsx +116 -0
- package/src/blocks/MaskedField/index.tsx +199 -0
- package/src/blocks/Menu/MenuContent.tsx +246 -0
- package/src/blocks/Menu/MenuContext.tsx +34 -0
- package/src/blocks/Menu/MenuItem.tsx +104 -0
- package/src/blocks/Menu/index.tsx +60 -0
- package/src/blocks/Modal/index.tsx +268 -0
- package/src/blocks/MorphingPopover/index.tsx +294 -0
- package/src/blocks/Overlay/Backdrop.tsx +48 -0
- package/src/blocks/Overlay/OverscrollGuard.tsx +36 -0
- package/src/blocks/Overlay/index.ts +2 -0
- package/src/blocks/Parallax/index.tsx +117 -0
- package/src/blocks/ParallaxSection/index.tsx +61 -0
- package/src/blocks/Placeholder/index.tsx +48 -0
- package/src/blocks/Popover/index.tsx +402 -0
- package/src/blocks/Progress/getProgressColor.ts +61 -0
- package/src/blocks/Progress/index.tsx +179 -0
- package/src/blocks/ProgressiveBlur/index.tsx +75 -0
- package/src/blocks/README.md +15 -0
- package/src/blocks/RenderAsset/index.tsx +18 -0
- package/src/blocks/ScrollContainer/index.tsx +93 -0
- package/src/blocks/ShinyText/index.tsx +72 -0
- package/src/blocks/Skeleton/index.tsx +71 -0
- package/src/blocks/Slider/SliderControls.tsx +119 -0
- package/src/blocks/Slider/index.tsx +140 -0
- package/src/blocks/Slider/useSlider.ts +126 -0
- package/src/blocks/Slideshow/index.tsx +177 -0
- package/src/blocks/Spotlight/index.tsx +144 -0
- package/src/blocks/Steps/StepIndicator.tsx +149 -0
- package/src/blocks/Steps/StepProgress.tsx +164 -0
- package/src/blocks/Steps/Steps.tsx +197 -0
- package/src/blocks/Steps/StepsNav.tsx +30 -0
- package/src/blocks/Steps/StepsTracker.tsx +80 -0
- package/src/blocks/Steps/hooks.ts +71 -0
- package/src/blocks/Steps/index.tsx +16 -0
- package/src/blocks/Steps/types.ts +71 -0
- package/src/blocks/StickySectionStack/index.tsx +136 -0
- package/src/blocks/Switch/index.tsx +85 -0
- package/src/blocks/SystemNotice/index.tsx +81 -0
- package/src/blocks/Table/README.md +251 -0
- package/src/blocks/Table/Table.tsx +207 -0
- package/src/blocks/Table/TablePagination.tsx +189 -0
- package/src/blocks/Table/index.ts +33 -0
- package/src/blocks/Table/useTableControls.ts +331 -0
- package/src/blocks/Tag/index.tsx +27 -0
- package/src/blocks/TextBreak/index.tsx +96 -0
- package/src/blocks/TextReveal/index.tsx +104 -0
- package/src/blocks/Thumbnail/index.tsx +26 -0
- package/src/blocks/Ticker/index.tsx +112 -0
- package/src/blocks/Toast/index.tsx +77 -0
- package/src/blocks/Tooltip/index.tsx +174 -0
- package/src/blocks/Underlay/index.tsx +104 -0
- package/src/blocks/Upload/Dropzone.tsx +92 -0
- package/src/blocks/Upload/UploadBtn.tsx +38 -0
- package/src/blocks/Upload/index.tsx +61 -0
- package/src/blocks/Upload/types.ts +37 -0
- package/src/blocks/VideoMarquee/index.tsx +511 -0
- package/src/blocks/index.ts +119 -0
- package/src/blocks/pagination/Pagination.tsx +148 -0
- package/src/blocks/pagination/PaginationList.tsx +41 -0
- package/src/blocks/pagination/index.ts +2 -0
- package/src/charts/BarChart.tsx +63 -0
- package/src/charts/PieChart.tsx +39 -0
- package/src/charts/index.ts +3 -0
- package/src/charts/utils.ts +103 -0
- package/src/docs/README.md +373 -0
- package/src/docs/reference/README.md +299 -0
- package/src/elements/box.ts +163 -0
- package/src/elements/button.ts +49 -0
- package/src/elements/field.ts +129 -0
- package/src/elements/index.ts +8 -0
- package/src/elements/text.ts +47 -0
- package/src/elements/utils.js +97 -0
- package/src/hooks/use-copy-to-clipboard.tsx +33 -0
- package/src/hooks/use-enter-submit.tsx +23 -0
- package/src/hooks/use-local-storage.ts +42 -0
- package/src/hooks/use-sidebar.tsx +109 -0
- package/src/hooks/useAnimatedText.ts +32 -0
- package/src/hooks/useAutosizeTextArea.ts +45 -0
- package/src/hooks/useBreakpoint.tsx +123 -0
- package/src/hooks/useClickOutside.tsx +38 -0
- package/src/hooks/useHover.tsx +33 -0
- package/src/hooks/useHoverList.tsx +17 -0
- package/src/hooks/useKeyboardShortcuts.ts +91 -0
- package/src/hooks/useKeypress.ts +27 -0
- package/src/hooks/useOverlay.ts +32 -0
- package/src/hooks/useReducedMotion.ts +25 -0
- package/src/hooks/useStandaloneMode.ts +35 -0
- package/src/hooks/useTouchDevice.ts +34 -0
- package/src/icons/index.tsx +129 -0
- package/src/index.ts +12 -0
- package/src/providers/DesignSystemProvider.tsx +35 -0
- package/src/providers/StyledComponentsRegistry.tsx +30 -0
- package/src/providers/index.ts +2 -0
- package/src/themes/README.md +30 -0
- package/src/themes/default/assets/badge-avatar.tsx +45 -0
- package/src/themes/default/assets/logo.tsx +42 -0
- package/src/themes/default/global.ts +138 -0
- package/src/themes/default/modes/dark/config.js +49 -0
- package/src/themes/default/modes/dark/skins.js +631 -0
- package/src/themes/default/modes/dark/theme.js +87 -0
- package/src/themes/default/modes/light/config.js +48 -0
- package/src/themes/default/modes/light/skins.js +1026 -0
- package/src/themes/default/modes/light/theme.js +74 -0
- package/src/themes/default/tokens/controls.js +53 -0
- package/src/themes/default/tokens/shadows.js +63 -0
- package/src/themes/default/tokens/shapes.js +37 -0
- package/src/themes/default/tokens/space.js +143 -0
- package/src/themes/default/tokens/spectre.js +16 -0
- package/src/themes/default/utils.js +523 -0
- package/src/themes/index.ts +11 -0
- package/src/types.ts +394 -0
- package/src/utils/overlayTheme.ts +61 -0
- package/src/utils/pickColor.ts +15 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Box, Text } from "@/design-system/elements";
|
|
3
|
+
import React, { useRef, useEffect, useState, useCallback } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* List Component with Column Grouping
|
|
7
|
+
*
|
|
8
|
+
* A flexible data grid component that supports column grouping functionality.
|
|
9
|
+
* Columns can be grouped under common headers for better organization and visual hierarchy.
|
|
10
|
+
*
|
|
11
|
+
* ## Basic Usage
|
|
12
|
+
* ```tsx
|
|
13
|
+
* <List data={myData} grid={myColumns} />
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* ## Column Grouping Methods
|
|
17
|
+
*
|
|
18
|
+
* ### Method 1: Direct Group Property (Simple)
|
|
19
|
+
* Add a `group` property to any GridColumn to group it under a header:
|
|
20
|
+
*
|
|
21
|
+
* ```tsx
|
|
22
|
+
* const groupedColumns = [
|
|
23
|
+
* {
|
|
24
|
+
* label: "Name",
|
|
25
|
+
* width: "200px",
|
|
26
|
+
* key: "name",
|
|
27
|
+
* component: (props) => <Text>{props.name}</Text>
|
|
28
|
+
* // No group - this will be standalone
|
|
29
|
+
* },
|
|
30
|
+
* {
|
|
31
|
+
* label: "Street",
|
|
32
|
+
* width: "150px",
|
|
33
|
+
* key: "street",
|
|
34
|
+
* group: "Address", // Grouped under "Address"
|
|
35
|
+
* component: (props) => <Text>{props.street}</Text>
|
|
36
|
+
* },
|
|
37
|
+
* {
|
|
38
|
+
* label: "City",
|
|
39
|
+
* width: "100px",
|
|
40
|
+
* key: "city",
|
|
41
|
+
* group: "Address", // Also grouped under "Address"
|
|
42
|
+
* component: (props) => <Text>{props.city}</Text>
|
|
43
|
+
* }
|
|
44
|
+
* ];
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* ### Method 2: createColumnGroup Helper (Recommended)
|
|
48
|
+
* Use `createColumnGroup()` to create grouped columns directly in your grid array:
|
|
49
|
+
*
|
|
50
|
+
* ```tsx
|
|
51
|
+
* const addressGroup = createColumnGroup("Address Info", [
|
|
52
|
+
* { label: "Street", width: "150px", key: "street", component: StreetComponent },
|
|
53
|
+
* { label: "City", width: "100px", key: "city", component: CityComponent }
|
|
54
|
+
* ]);
|
|
55
|
+
*
|
|
56
|
+
* const contactGroup = createColumnGroup("Contact Info", [
|
|
57
|
+
* { label: "Phone", width: "120px", key: "phone", component: PhoneComponent },
|
|
58
|
+
* { label: "Email", width: "180px", key: "email", component: EmailComponent }
|
|
59
|
+
* ]);
|
|
60
|
+
*
|
|
61
|
+
* // Use directly in grid array
|
|
62
|
+
* const grid = [
|
|
63
|
+
* { label: "Name", width: "200px", key: "name", component: NameComponent },
|
|
64
|
+
* addressGroup, // Direct usage!
|
|
65
|
+
* contactGroup // Direct usage!
|
|
66
|
+
* ];
|
|
67
|
+
*
|
|
68
|
+
* <List data={myData} grid={grid} />
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* ### Rendering Result:
|
|
72
|
+
* ```
|
|
73
|
+
* Name | Address Info | Contact Info
|
|
74
|
+
* | Street | City | Phone | Email
|
|
75
|
+
* --------|--------|------|-------|-------
|
|
76
|
+
* John | 123 | NY | 555-1 | j@...
|
|
77
|
+
* Jane | 456 | LA | 555-2 | jane@...
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* ## Advanced Usage
|
|
81
|
+
*
|
|
82
|
+
* ### Custom Group Styling
|
|
83
|
+
* ```tsx
|
|
84
|
+
* const styledGroup = createColumnGroup(
|
|
85
|
+
* "Financial Data",
|
|
86
|
+
* [...columns],
|
|
87
|
+
* { bg: "palette.blues.1", color: "palette.blues.8" } // Custom group header props
|
|
88
|
+
* );
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* ### Mixed Arrays
|
|
92
|
+
* You can mix standalone columns with grouped columns:
|
|
93
|
+
* ```tsx
|
|
94
|
+
* const grid = [
|
|
95
|
+
* standaloneColumn,
|
|
96
|
+
* groupedColumns1,
|
|
97
|
+
* anotherStandaloneColumn,
|
|
98
|
+
* groupedColumns2
|
|
99
|
+
* ];
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
|
|
103
|
+
function debounce<T extends (...args: any[]) => any>(
|
|
104
|
+
fn: T,
|
|
105
|
+
ms: number
|
|
106
|
+
): (...args: Parameters<T>) => void {
|
|
107
|
+
let timer: NodeJS.Timeout;
|
|
108
|
+
return (...args: Parameters<T>) => {
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
timer = setTimeout(() => {
|
|
111
|
+
fn(...args);
|
|
112
|
+
}, ms);
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface DataItem {
|
|
117
|
+
[key: string]: any;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface ListProps {
|
|
121
|
+
data: DataItem[];
|
|
122
|
+
grid?: (GridColumn | GroupedColumn)[];
|
|
123
|
+
gap?: string;
|
|
124
|
+
selected?: string[];
|
|
125
|
+
onSelect?: (id: string) => void;
|
|
126
|
+
/**
|
|
127
|
+
* Called when a row is clicked. Receives the full item.
|
|
128
|
+
*/
|
|
129
|
+
onRowClick?: (item: DataItem) => void;
|
|
130
|
+
/**
|
|
131
|
+
* Fixed height for each row. Default is "2rem".
|
|
132
|
+
*/
|
|
133
|
+
rowHeight?: string;
|
|
134
|
+
[key: string]: any;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface GridColumn {
|
|
138
|
+
/** Column header label - can be text or React component */
|
|
139
|
+
label: string | React.ReactNode;
|
|
140
|
+
/** Column width (CSS width value) */
|
|
141
|
+
width: string;
|
|
142
|
+
/** Data key(s) to extract from row data */
|
|
143
|
+
key: string | string[];
|
|
144
|
+
/** Column alignment */
|
|
145
|
+
align?: any;
|
|
146
|
+
/** Minimum column width */
|
|
147
|
+
minWidth?: string;
|
|
148
|
+
/** Maximum column width */
|
|
149
|
+
maxWidth?: string;
|
|
150
|
+
/** Enable auto-width calculation based on content */
|
|
151
|
+
autoWidth?: boolean;
|
|
152
|
+
/** Component to render cell content */
|
|
153
|
+
component: React.ComponentType<any>;
|
|
154
|
+
/** Props passed to column header */
|
|
155
|
+
columnProps?: any;
|
|
156
|
+
/** Props passed to individual cells */
|
|
157
|
+
cellProps?: any;
|
|
158
|
+
/**
|
|
159
|
+
* Group identifier - columns with same group value will be grouped together.
|
|
160
|
+
* If undefined, column will not be grouped.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```tsx
|
|
164
|
+
* { group: "Personal Info" } // Groups under "Personal Info" header
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
group?: string;
|
|
168
|
+
[key: string]: any;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Represents a group of columns under a common header
|
|
173
|
+
*/
|
|
174
|
+
export interface GroupedColumn {
|
|
175
|
+
/** Header text/component displayed above the grouped columns */
|
|
176
|
+
groupHeader: string | React.ReactNode;
|
|
177
|
+
/** Array of columns belonging to this group */
|
|
178
|
+
columns: GridColumn[];
|
|
179
|
+
/** Props passed to the group header element */
|
|
180
|
+
groupProps?: any;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const DEFAULT_GRID_SETUP: GridColumn[] = [
|
|
184
|
+
{
|
|
185
|
+
label: "id",
|
|
186
|
+
width: "4rem",
|
|
187
|
+
key: "id",
|
|
188
|
+
align: "center",
|
|
189
|
+
component: (props: DataItem) => <Text fontWeight={600}>{props.id}</Text>,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
label: "name",
|
|
193
|
+
width: "1fr",
|
|
194
|
+
key: "name",
|
|
195
|
+
align: "start",
|
|
196
|
+
component: (props: DataItem) => <Text fontWeight={600}>{props.name}</Text>,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
label: "",
|
|
200
|
+
width: "1fr",
|
|
201
|
+
key: "empty",
|
|
202
|
+
align: "start",
|
|
203
|
+
component: () => <></>,
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Utility function to create a grouped column structure
|
|
209
|
+
*
|
|
210
|
+
* @param groupHeader - The header text/component for the group
|
|
211
|
+
* @param columns - Array of columns to include in this group
|
|
212
|
+
* @param groupProps - Optional props for the group header styling
|
|
213
|
+
* @returns GroupedColumn object
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```tsx
|
|
217
|
+
* const contactGroup = createColumnGroup(
|
|
218
|
+
* "Contact Information",
|
|
219
|
+
* [
|
|
220
|
+
* { label: "Email", width: "200px", key: "email", component: EmailCell },
|
|
221
|
+
* { label: "Phone", width: "150px", key: "phone", component: PhoneCell }
|
|
222
|
+
* ],
|
|
223
|
+
* { bg: "palette.blues.1" } // Custom group header styling
|
|
224
|
+
* );
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
export const createColumnGroup = (
|
|
228
|
+
groupHeader: string | React.ReactNode,
|
|
229
|
+
columns: GridColumn[],
|
|
230
|
+
groupProps?: any
|
|
231
|
+
): GroupedColumn => ({
|
|
232
|
+
groupHeader,
|
|
233
|
+
columns,
|
|
234
|
+
groupProps,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Internal utility function to process mixed grid input.
|
|
239
|
+
* Handles both flat arrays of columns with group properties and
|
|
240
|
+
* arrays containing GroupedColumn objects.
|
|
241
|
+
*
|
|
242
|
+
* @param grid - Array of GridColumn and/or GroupedColumn objects
|
|
243
|
+
* @returns Array containing both individual columns and GroupedColumn objects
|
|
244
|
+
*/
|
|
245
|
+
const processGridInput = (
|
|
246
|
+
grid: (GridColumn | GroupedColumn)[]
|
|
247
|
+
): (GridColumn | GroupedColumn)[] => {
|
|
248
|
+
const result: (GridColumn | GroupedColumn)[] = [];
|
|
249
|
+
|
|
250
|
+
grid.forEach((item) => {
|
|
251
|
+
if ("columns" in item) {
|
|
252
|
+
// It's already a GroupedColumn, add it directly
|
|
253
|
+
result.push(item);
|
|
254
|
+
} else {
|
|
255
|
+
// It's a GridColumn, check if it has a group property
|
|
256
|
+
if (item.group) {
|
|
257
|
+
// Find if we already have a group with this name
|
|
258
|
+
const existingGroupIndex = result.findIndex(
|
|
259
|
+
(r) => "columns" in r && r.groupHeader === item.group
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
if (existingGroupIndex >= 0) {
|
|
263
|
+
// Add to existing group
|
|
264
|
+
(result[existingGroupIndex] as GroupedColumn).columns.push(item);
|
|
265
|
+
} else {
|
|
266
|
+
// Create new group
|
|
267
|
+
result.push({
|
|
268
|
+
groupHeader: item.group,
|
|
269
|
+
columns: [item],
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
// No group, add as standalone column
|
|
274
|
+
result.push(item);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
return result;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Internal utility function to flatten grouped columns back to regular columns.
|
|
284
|
+
* Used for data processing where the flat structure is needed.
|
|
285
|
+
*
|
|
286
|
+
* @param groupedColumns - Mixed array of columns and groups
|
|
287
|
+
* @returns Flat array of GridColumn objects
|
|
288
|
+
*/
|
|
289
|
+
const flattenColumns = (
|
|
290
|
+
groupedColumns: (GridColumn | GroupedColumn)[]
|
|
291
|
+
): GridColumn[] => {
|
|
292
|
+
const flattened: GridColumn[] = [];
|
|
293
|
+
|
|
294
|
+
groupedColumns.forEach((item) => {
|
|
295
|
+
if ("columns" in item) {
|
|
296
|
+
flattened.push(...item.columns);
|
|
297
|
+
} else {
|
|
298
|
+
flattened.push(item);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
return flattened;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// Utility function to filter data items based on grid keys
|
|
306
|
+
const filterDataByGridKeys = (data: DataItem[], grid: GridColumn[]) => {
|
|
307
|
+
const gridKeys = grid.map((col) => col.key).flat();
|
|
308
|
+
return data.map((item) => {
|
|
309
|
+
const filteredItem: Partial<DataItem> = {};
|
|
310
|
+
gridKeys.forEach((key) => {
|
|
311
|
+
if (typeof key === "string" && key in item) {
|
|
312
|
+
filteredItem[key] = item[key as keyof DataItem];
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
return filteredItem as DataItem;
|
|
316
|
+
});
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// Helper function to get value from item using key or array of keys
|
|
320
|
+
const getItemValue = (item: DataItem, key: string | string[]) => {
|
|
321
|
+
if (typeof key === "string") {
|
|
322
|
+
return item[key];
|
|
323
|
+
}
|
|
324
|
+
// If key is an array, return an object with all specified keys
|
|
325
|
+
return key.reduce(
|
|
326
|
+
(acc, k) => ({
|
|
327
|
+
...acc,
|
|
328
|
+
[k]: item[k],
|
|
329
|
+
}),
|
|
330
|
+
{}
|
|
331
|
+
);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const List: React.FC<ListProps> = ({
|
|
335
|
+
data = [],
|
|
336
|
+
grid = DEFAULT_GRID_SETUP,
|
|
337
|
+
selected = [],
|
|
338
|
+
gap = "xxxsmall",
|
|
339
|
+
rowHeight = "2rem",
|
|
340
|
+
onSelect,
|
|
341
|
+
onRowClick,
|
|
342
|
+
...rest
|
|
343
|
+
}) => {
|
|
344
|
+
// Process grid input and flatten for processing
|
|
345
|
+
const groupedColumns = processGridInput(grid);
|
|
346
|
+
const flattenedGrid = flattenColumns(groupedColumns);
|
|
347
|
+
|
|
348
|
+
// Create a ref for each column that will hold calculated widths
|
|
349
|
+
const [columnWidths, setColumnWidths] = useState<string[]>([]);
|
|
350
|
+
const columnRefs = useRef<(HTMLDivElement | null)[]>([]);
|
|
351
|
+
const headerRefs = useRef<(HTMLDivElement | null)[]>([]);
|
|
352
|
+
const hasAutoWidthColumns = flattenedGrid.some((col) => col.autoWidth);
|
|
353
|
+
const [measuredOnce, setMeasuredOnce] = useState(false);
|
|
354
|
+
|
|
355
|
+
// Create initial grid template based on fixed widths
|
|
356
|
+
const initialGridTemplate = flattenedGrid
|
|
357
|
+
.map((col: GridColumn) => (col.autoWidth ? "auto" : col.width))
|
|
358
|
+
.join(" ");
|
|
359
|
+
|
|
360
|
+
// Filter the data based on the grid keys
|
|
361
|
+
const filteredData = filterDataByGridKeys(data, flattenedGrid);
|
|
362
|
+
|
|
363
|
+
// Memoize grid structure to prevent unnecessary recalculations
|
|
364
|
+
const gridStructureKey = JSON.stringify(
|
|
365
|
+
flattenedGrid.map((col) => ({
|
|
366
|
+
width: col.width,
|
|
367
|
+
autoWidth: col.autoWidth,
|
|
368
|
+
}))
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
const measureColumnWidths = useCallback(() => {
|
|
372
|
+
if (filteredData.length === 0 || !hasAutoWidthColumns) return;
|
|
373
|
+
|
|
374
|
+
// Initialize or resize the refs array to match grid length
|
|
375
|
+
columnRefs.current = columnRefs.current.slice(0, flattenedGrid.length);
|
|
376
|
+
headerRefs.current = headerRefs.current.slice(0, flattenedGrid.length);
|
|
377
|
+
|
|
378
|
+
const widths = flattenedGrid.map((col, index) => {
|
|
379
|
+
if (!col.autoWidth) return col.width;
|
|
380
|
+
|
|
381
|
+
// Get width of the column content and header
|
|
382
|
+
const columnElements = document.querySelectorAll(`.column-${index}`);
|
|
383
|
+
let maxWidth = 0;
|
|
384
|
+
|
|
385
|
+
// Include header width in calculation
|
|
386
|
+
const headerEl = headerRefs.current[index];
|
|
387
|
+
if (headerEl) {
|
|
388
|
+
const headerWidth = headerEl.getBoundingClientRect().width;
|
|
389
|
+
maxWidth = Math.max(maxWidth, headerWidth);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Find the max width among all cells in this column
|
|
393
|
+
// Only measure a reasonable number of rows for performance
|
|
394
|
+
const maxRowsToMeasure = Math.min(columnElements.length, 20);
|
|
395
|
+
for (let i = 0; i < maxRowsToMeasure; i++) {
|
|
396
|
+
const el = columnElements[i];
|
|
397
|
+
const width = el.getBoundingClientRect().width;
|
|
398
|
+
maxWidth = Math.max(maxWidth, width);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Add padding + buffer to prevent layout shifts
|
|
402
|
+
return `${maxWidth + 24}px`;
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// Only update if there's a significant change
|
|
406
|
+
const hasSignificantChange = widths.some((width, i) => {
|
|
407
|
+
const current = columnWidths[i];
|
|
408
|
+
if (!current || current === "auto") return true;
|
|
409
|
+
|
|
410
|
+
// Extract numeric values for comparison
|
|
411
|
+
const currentVal = parseFloat(current);
|
|
412
|
+
const newVal = parseFloat(width);
|
|
413
|
+
return Math.abs(currentVal - newVal) > 5; // 5px threshold
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
if (hasSignificantChange || !measuredOnce) {
|
|
417
|
+
setColumnWidths(widths);
|
|
418
|
+
setMeasuredOnce(true);
|
|
419
|
+
}
|
|
420
|
+
}, [
|
|
421
|
+
filteredData.length,
|
|
422
|
+
flattenedGrid,
|
|
423
|
+
hasAutoWidthColumns,
|
|
424
|
+
columnWidths,
|
|
425
|
+
measuredOnce,
|
|
426
|
+
]);
|
|
427
|
+
// Use a layout effect to measure sizes after DOM update but before paint
|
|
428
|
+
useEffect(() => {
|
|
429
|
+
if (!hasAutoWidthColumns) return;
|
|
430
|
+
|
|
431
|
+
// Only measure if we haven't measured yet or if grid structure changed
|
|
432
|
+
if (!measuredOnce || columnWidths.length !== flattenedGrid.length) {
|
|
433
|
+
// Use requestAnimationFrame to ensure DOM is ready
|
|
434
|
+
const timeoutId = setTimeout(() => {
|
|
435
|
+
measureColumnWidths();
|
|
436
|
+
}, 0);
|
|
437
|
+
|
|
438
|
+
return () => clearTimeout(timeoutId);
|
|
439
|
+
}
|
|
440
|
+
}, [
|
|
441
|
+
gridStructureKey,
|
|
442
|
+
measureColumnWidths,
|
|
443
|
+
measuredOnce,
|
|
444
|
+
hasAutoWidthColumns,
|
|
445
|
+
flattenedGrid.length,
|
|
446
|
+
columnWidths.length,
|
|
447
|
+
]);
|
|
448
|
+
|
|
449
|
+
// Remeasure when window resizes
|
|
450
|
+
useEffect(() => {
|
|
451
|
+
if (!hasAutoWidthColumns) return;
|
|
452
|
+
|
|
453
|
+
// Use a debounced resize handler that only triggers on width changes
|
|
454
|
+
let lastWidth = window.innerWidth;
|
|
455
|
+
const handleResize = () => {
|
|
456
|
+
// Only remeasure on horizontal resizes
|
|
457
|
+
if (window.innerWidth !== lastWidth) {
|
|
458
|
+
lastWidth = window.innerWidth;
|
|
459
|
+
measureColumnWidths();
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const debouncedResize = debounce(handleResize, 250);
|
|
464
|
+
window.addEventListener("resize", debouncedResize);
|
|
465
|
+
|
|
466
|
+
return () => {
|
|
467
|
+
window.removeEventListener("resize", debouncedResize);
|
|
468
|
+
};
|
|
469
|
+
}, [hasAutoWidthColumns, measureColumnWidths]);
|
|
470
|
+
|
|
471
|
+
// Determine which grid template to use (calculated or initial)
|
|
472
|
+
const gridTemplateColumns =
|
|
473
|
+
columnWidths.length === flattenedGrid.length
|
|
474
|
+
? columnWidths.join(" ")
|
|
475
|
+
: initialGridTemplate;
|
|
476
|
+
|
|
477
|
+
return (
|
|
478
|
+
<Box
|
|
479
|
+
width="100%"
|
|
480
|
+
overflow="hidden"
|
|
481
|
+
shape="rounded"
|
|
482
|
+
border="1px solid"
|
|
483
|
+
skin="surface"
|
|
484
|
+
>
|
|
485
|
+
{/* Group Headers */}
|
|
486
|
+
{groupedColumns.some((item) => "columns" in item) && (
|
|
487
|
+
<Box
|
|
488
|
+
display={"grid"}
|
|
489
|
+
gridTemplateColumns={gridTemplateColumns}
|
|
490
|
+
pb={gap}
|
|
491
|
+
border="none"
|
|
492
|
+
gap={gap}
|
|
493
|
+
backgroundColor="palette.neutrals.0"
|
|
494
|
+
borderTopLeftRadius="rounded"
|
|
495
|
+
borderTopRightRadius="rounded"
|
|
496
|
+
width="100%"
|
|
497
|
+
{...rest}
|
|
498
|
+
>
|
|
499
|
+
{(() => {
|
|
500
|
+
let colIndex = 0;
|
|
501
|
+
return groupedColumns.map((item, groupIndex) => {
|
|
502
|
+
if ("columns" in item) {
|
|
503
|
+
const groupSpan = item.columns.length;
|
|
504
|
+
const groupHeader = (
|
|
505
|
+
<Text
|
|
506
|
+
key={`group-${groupIndex}`}
|
|
507
|
+
as="div"
|
|
508
|
+
skin="row.group"
|
|
509
|
+
shape="roundedSmall"
|
|
510
|
+
fontSize="small"
|
|
511
|
+
fontWeight={600}
|
|
512
|
+
textAlign="center"
|
|
513
|
+
display={"flex"}
|
|
514
|
+
alignItems="center"
|
|
515
|
+
justifyContent="center"
|
|
516
|
+
width="100%"
|
|
517
|
+
height="2.5rem"
|
|
518
|
+
backgroundColor="palette.neutrals.1"
|
|
519
|
+
borderBottom="1px solid"
|
|
520
|
+
borderColor="border"
|
|
521
|
+
padding="xxsmall"
|
|
522
|
+
gridColumn={`${colIndex + 1} / span ${groupSpan}`}
|
|
523
|
+
{...item.columns[0].groupProps}
|
|
524
|
+
>
|
|
525
|
+
{item.groupHeader}
|
|
526
|
+
</Text>
|
|
527
|
+
);
|
|
528
|
+
colIndex += groupSpan;
|
|
529
|
+
return groupHeader;
|
|
530
|
+
} else {
|
|
531
|
+
colIndex += 1;
|
|
532
|
+
return <Box key={`empty-${groupIndex}`} />;
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
})()}
|
|
536
|
+
</Box>
|
|
537
|
+
)}
|
|
538
|
+
|
|
539
|
+
{/* Column Headers */}
|
|
540
|
+
<Box
|
|
541
|
+
display={"grid"}
|
|
542
|
+
gridTemplateColumns={gridTemplateColumns}
|
|
543
|
+
border="none"
|
|
544
|
+
//gap={gap}
|
|
545
|
+
borderBottom="1px solid"
|
|
546
|
+
borderColor="neutral"
|
|
547
|
+
skin="base"
|
|
548
|
+
p="xxxs"
|
|
549
|
+
>
|
|
550
|
+
{flattenedGrid.map((column: GridColumn, i: number) => {
|
|
551
|
+
const align = column?.align || "start";
|
|
552
|
+
return (
|
|
553
|
+
<Text
|
|
554
|
+
as={React.isValidElement(column.label) ? "div" : "span"}
|
|
555
|
+
key={i}
|
|
556
|
+
ref={(el: any) => (headerRefs.current[i] = el)}
|
|
557
|
+
// shape="roundedSmall"
|
|
558
|
+
fontSize="xs"
|
|
559
|
+
fontWeight={600}
|
|
560
|
+
textAlign={align}
|
|
561
|
+
width="100%"
|
|
562
|
+
justifyContent={align}
|
|
563
|
+
px="xxxs"
|
|
564
|
+
color="primary"
|
|
565
|
+
{...column.columnProps}
|
|
566
|
+
lineClamp={1}
|
|
567
|
+
>
|
|
568
|
+
{React.isValidElement(column.label) ? column.label : column.label}
|
|
569
|
+
</Text>
|
|
570
|
+
);
|
|
571
|
+
})}
|
|
572
|
+
</Box>
|
|
573
|
+
<Box display="grid" overflow="hidden" width="100%">
|
|
574
|
+
{filteredData?.map((item, rowIndex) => {
|
|
575
|
+
const isSelected = selected.includes(item.id);
|
|
576
|
+
return (
|
|
577
|
+
<Text
|
|
578
|
+
key={rowIndex}
|
|
579
|
+
as="div"
|
|
580
|
+
display="grid"
|
|
581
|
+
gridTemplateColumns={gridTemplateColumns}
|
|
582
|
+
alignItems="center"
|
|
583
|
+
// py="xxxsmall"
|
|
584
|
+
fontSize={"small"}
|
|
585
|
+
borderBottom={
|
|
586
|
+
rowIndex !== filteredData.length - 1 ? "1px solid" : "0"
|
|
587
|
+
}
|
|
588
|
+
height={rowHeight}
|
|
589
|
+
maxHeight={rowHeight}
|
|
590
|
+
// skin={rowIndex % 2 === 1 ? "row.alt" : "row"}
|
|
591
|
+
borderColor="neutral"
|
|
592
|
+
onClick={() => {
|
|
593
|
+
if (onSelect) onSelect(item.id);
|
|
594
|
+
if (onRowClick) onRowClick(item);
|
|
595
|
+
}}
|
|
596
|
+
cursor={onSelect || onRowClick ? "pointer" : "default"}
|
|
597
|
+
// gap={gap}
|
|
598
|
+
>
|
|
599
|
+
{flattenedGrid.map((column, colIndex) => {
|
|
600
|
+
const Component = column.component;
|
|
601
|
+
const align = column?.align || "start";
|
|
602
|
+
const itemValue = getItemValue(item, column.key);
|
|
603
|
+
return (
|
|
604
|
+
<Text
|
|
605
|
+
as="div"
|
|
606
|
+
key={colIndex}
|
|
607
|
+
className={`column-${colIndex}`}
|
|
608
|
+
ref={
|
|
609
|
+
rowIndex === 0
|
|
610
|
+
? (el: any) => (columnRefs.current[colIndex] = el)
|
|
611
|
+
: null
|
|
612
|
+
}
|
|
613
|
+
display="flex"
|
|
614
|
+
alignItems="center"
|
|
615
|
+
justifyContent={align}
|
|
616
|
+
width="100%"
|
|
617
|
+
height={"100%"}
|
|
618
|
+
// maxHeight={rowHeight}
|
|
619
|
+
// whiteSpace="nowrap"
|
|
620
|
+
overflow="hidden"
|
|
621
|
+
textOverflow="ellipsis"
|
|
622
|
+
borderRight={
|
|
623
|
+
colIndex !== flattenedGrid.length - 1 ? "1px solid" : "0"
|
|
624
|
+
}
|
|
625
|
+
//skin={rowIndex % 2 === 1 ? "row.alt" : "row"}
|
|
626
|
+
borderColor="neutral"
|
|
627
|
+
// px={colIndex === 0 ? "small" : "xxsmall"}
|
|
628
|
+
minWidth={column.minWidth || "initial"}
|
|
629
|
+
maxWidth={column.maxWidth || "initial"}
|
|
630
|
+
{...column.cellProps}
|
|
631
|
+
>
|
|
632
|
+
{isSelected && <>✅</>}
|
|
633
|
+
<Box
|
|
634
|
+
width="100%"
|
|
635
|
+
overflow="hidden"
|
|
636
|
+
display="flex"
|
|
637
|
+
alignItems="center"
|
|
638
|
+
justifyContent={align}
|
|
639
|
+
maxHeight="100%"
|
|
640
|
+
style={{
|
|
641
|
+
textOverflow: "ellipsis",
|
|
642
|
+
}}
|
|
643
|
+
>
|
|
644
|
+
<Component {...item} value={itemValue} />
|
|
645
|
+
</Box>
|
|
646
|
+
</Text>
|
|
647
|
+
);
|
|
648
|
+
})}
|
|
649
|
+
</Text>
|
|
650
|
+
);
|
|
651
|
+
})}
|
|
652
|
+
</Box>
|
|
653
|
+
</Box>
|
|
654
|
+
);
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
export default List;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import Box, { BoxProps } from "@/design-system/elements/box";
|
|
3
|
+
import { ThemeContext } from "styled-components";
|
|
4
|
+
import React, { useContext } from "react";
|
|
5
|
+
|
|
6
|
+
export default function Main({
|
|
7
|
+
children,
|
|
8
|
+
...props
|
|
9
|
+
}: { children: React.ReactNode; props?: any } & BoxProps) {
|
|
10
|
+
const theme: any = useContext(ThemeContext);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<Box {...theme?.main} {...props}>
|
|
14
|
+
{children}
|
|
15
|
+
</Box>
|
|
16
|
+
);
|
|
17
|
+
}
|