@classytic/fluid 0.4.0 → 0.4.1
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/dist/client/spreadsheet.d.mts +207 -0
- package/dist/client/spreadsheet.mjs +611 -0
- package/dist/forms.mjs +3 -3
- package/package.json +32 -15
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
import * as react from "react";
|
|
3
|
+
import { ComponentType, ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
//#region src/components/spreadsheet/types.d.ts
|
|
6
|
+
interface SpreadsheetColumn<TData = unknown> {
|
|
7
|
+
/** Unique column identifier — also used as the field key unless `field` is set */
|
|
8
|
+
id: string;
|
|
9
|
+
/** Optional field key on the data object (defaults to `id`) */
|
|
10
|
+
field?: string;
|
|
11
|
+
/** Column header label */
|
|
12
|
+
header: string | ReactNode;
|
|
13
|
+
/** CSS width: '20%', '120px', 'minmax(100px, 1fr)', etc. */
|
|
14
|
+
width: string;
|
|
15
|
+
/** Text alignment */
|
|
16
|
+
align?: "left" | "right" | "center";
|
|
17
|
+
/** Component that renders the cell content */
|
|
18
|
+
cell: ComponentType<CellRenderProps<TData>>;
|
|
19
|
+
/** Label shown above the field in mobile card layout */
|
|
20
|
+
mobileLabel?: string;
|
|
21
|
+
/** grid-cols-12 span for mobile card layout (default 12 = full width) */
|
|
22
|
+
mobileSpan?: number;
|
|
23
|
+
/** Hide column conditionally (e.g., hide actions when read-only) */
|
|
24
|
+
hiddenWhen?: (ctx: {
|
|
25
|
+
isReadOnly: boolean;
|
|
26
|
+
}) => boolean;
|
|
27
|
+
/** Data type hint for clipboard paste coercion. "number" coerces pasted strings to numbers. */
|
|
28
|
+
dataType?: "string" | "number";
|
|
29
|
+
}
|
|
30
|
+
interface CellRenderProps<TData = unknown> {
|
|
31
|
+
/** Current field value */
|
|
32
|
+
value: unknown;
|
|
33
|
+
/** Full row data object */
|
|
34
|
+
row: TData;
|
|
35
|
+
/** Stable row ID (UUID) */
|
|
36
|
+
rowId: string;
|
|
37
|
+
/** Visual row index (0-based, accounts for filtering) */
|
|
38
|
+
rowIndex: number;
|
|
39
|
+
/** Column definition */
|
|
40
|
+
column: SpreadsheetColumn<TData>;
|
|
41
|
+
/**
|
|
42
|
+
* Commit a value change.
|
|
43
|
+
* - Single field: `onChange("debit", 100)`
|
|
44
|
+
* - Multi-field batch: `onChange({ debit: 100, credit: 0 })`
|
|
45
|
+
*/
|
|
46
|
+
onChange: (fieldOrPatch: string | Record<string, unknown>, value?: unknown) => void;
|
|
47
|
+
/** Whether the spreadsheet is in read-only mode */
|
|
48
|
+
isReadOnly: boolean;
|
|
49
|
+
/** Data attributes for keyboard navigation */
|
|
50
|
+
cellRef?: string;
|
|
51
|
+
}
|
|
52
|
+
interface SpreadsheetTableProps<TData = unknown> {
|
|
53
|
+
/** Column definitions */
|
|
54
|
+
columns: SpreadsheetColumn<TData>[];
|
|
55
|
+
/** Items state from useSpreadsheetStore */
|
|
56
|
+
items: Map<string, TData>;
|
|
57
|
+
/** Ordered row IDs (can be pre-filtered for search) */
|
|
58
|
+
orderedIds: string[];
|
|
59
|
+
/** Dispatch function from useSpreadsheetStore */
|
|
60
|
+
dispatch: (action: SpreadsheetAction<TData>) => void;
|
|
61
|
+
/** Read-only mode (e.g., posted journal entries) */
|
|
62
|
+
isReadOnly?: boolean;
|
|
63
|
+
/** Enable row virtualization (recommended when > 30 rows) */
|
|
64
|
+
virtualize?: boolean;
|
|
65
|
+
/** Estimated row height for virtualization (default: 48) */
|
|
66
|
+
estimateRowHeight?: number;
|
|
67
|
+
/** Render a footer row (e.g., totals) — used inside <tfoot> on desktop */
|
|
68
|
+
footer?: ReactNode;
|
|
69
|
+
/** Separate footer for mobile card layout (avoids <tr> inside <div>) */
|
|
70
|
+
mobileFooter?: ReactNode;
|
|
71
|
+
/** Render row action buttons (add/remove) */
|
|
72
|
+
rowActions?: (props: {
|
|
73
|
+
rowId: string;
|
|
74
|
+
rowIndex: number;
|
|
75
|
+
isReadOnly: boolean;
|
|
76
|
+
}) => ReactNode;
|
|
77
|
+
/** CSS class for the table container */
|
|
78
|
+
className?: string;
|
|
79
|
+
/** Accessible label for the table */
|
|
80
|
+
ariaLabel?: string;
|
|
81
|
+
/** Called when a row is added (receives the ID of the row to insert after) */
|
|
82
|
+
onAddRow?: (afterRowId: string) => void;
|
|
83
|
+
/** Called when a row is removed */
|
|
84
|
+
onRemoveRow?: (rowId: string) => void;
|
|
85
|
+
}
|
|
86
|
+
interface SpreadsheetState<TData> {
|
|
87
|
+
items: Map<string, TData>;
|
|
88
|
+
orderedIds: string[];
|
|
89
|
+
}
|
|
90
|
+
type SpreadsheetAction<TData = unknown> = {
|
|
91
|
+
type: "SET_ALL";
|
|
92
|
+
items: TData[];
|
|
93
|
+
getId?: (item: TData) => string;
|
|
94
|
+
} | {
|
|
95
|
+
type: "UPDATE_CELL";
|
|
96
|
+
rowId: string;
|
|
97
|
+
field: string;
|
|
98
|
+
value: unknown;
|
|
99
|
+
} | {
|
|
100
|
+
type: "UPDATE_ROW";
|
|
101
|
+
rowId: string;
|
|
102
|
+
patch: Partial<TData>;
|
|
103
|
+
} | {
|
|
104
|
+
type: "ADD_ROW";
|
|
105
|
+
afterRowId: string;
|
|
106
|
+
item: TData;
|
|
107
|
+
newId?: string;
|
|
108
|
+
} | {
|
|
109
|
+
type: "REMOVE_ROW";
|
|
110
|
+
rowId: string;
|
|
111
|
+
} | {
|
|
112
|
+
type: "BATCH";
|
|
113
|
+
actions: SpreadsheetAction<TData>[];
|
|
114
|
+
};
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/components/spreadsheet/spreadsheet-table.d.ts
|
|
117
|
+
declare function SpreadsheetTable<TData = unknown>({
|
|
118
|
+
columns,
|
|
119
|
+
items,
|
|
120
|
+
orderedIds,
|
|
121
|
+
dispatch,
|
|
122
|
+
isReadOnly,
|
|
123
|
+
footer,
|
|
124
|
+
mobileFooter,
|
|
125
|
+
rowActions,
|
|
126
|
+
className,
|
|
127
|
+
ariaLabel,
|
|
128
|
+
onAddRow,
|
|
129
|
+
virtualize,
|
|
130
|
+
estimateRowHeight
|
|
131
|
+
}: SpreadsheetTableProps<TData>): react_jsx_runtime0.JSX.Element;
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/components/spreadsheet/spreadsheet-row.d.ts
|
|
134
|
+
interface SpreadsheetRowProps<TData = unknown> {
|
|
135
|
+
rowId: string;
|
|
136
|
+
rowIndex: number;
|
|
137
|
+
item: TData;
|
|
138
|
+
visibleColumns: SpreadsheetColumn<TData>[];
|
|
139
|
+
dispatch: (action: SpreadsheetAction<TData>) => void;
|
|
140
|
+
isReadOnly: boolean;
|
|
141
|
+
rowActions?: (props: {
|
|
142
|
+
rowId: string;
|
|
143
|
+
rowIndex: number;
|
|
144
|
+
isReadOnly: boolean;
|
|
145
|
+
}) => ReactNode;
|
|
146
|
+
onCellKeyDown?: (e: React.KeyboardEvent, rowId: string, colId: string) => void;
|
|
147
|
+
cellAttr?: string;
|
|
148
|
+
isMobile?: boolean;
|
|
149
|
+
style?: React.CSSProperties;
|
|
150
|
+
/** Ref callback from virtualizer for dynamic row measurement */
|
|
151
|
+
measureRef?: (node: HTMLElement | null) => void;
|
|
152
|
+
}
|
|
153
|
+
declare function SpreadsheetRowInner<TData>({
|
|
154
|
+
rowId,
|
|
155
|
+
rowIndex,
|
|
156
|
+
item,
|
|
157
|
+
visibleColumns,
|
|
158
|
+
dispatch,
|
|
159
|
+
isReadOnly,
|
|
160
|
+
rowActions,
|
|
161
|
+
onCellKeyDown,
|
|
162
|
+
cellAttr,
|
|
163
|
+
isMobile,
|
|
164
|
+
style,
|
|
165
|
+
measureRef
|
|
166
|
+
}: SpreadsheetRowProps<TData>): react_jsx_runtime0.JSX.Element;
|
|
167
|
+
declare const SpreadsheetRow: typeof SpreadsheetRowInner;
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/components/spreadsheet/use-spreadsheet-store.d.ts
|
|
170
|
+
declare function useSpreadsheetStore<TData>(initialItems?: TData[], getId?: (item: TData) => string): {
|
|
171
|
+
items: Map<string, TData>;
|
|
172
|
+
orderedIds: string[];
|
|
173
|
+
dispatch: react.ActionDispatch<[action: SpreadsheetAction<TData>]>;
|
|
174
|
+
getOrderedItems: () => TData[];
|
|
175
|
+
rowCount: number;
|
|
176
|
+
};
|
|
177
|
+
type SpreadsheetStore<TData> = ReturnType<typeof useSpreadsheetStore<TData>>;
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/components/spreadsheet/use-spreadsheet-keyboard.d.ts
|
|
180
|
+
interface UseSpreadsheetKeyboardOptions {
|
|
181
|
+
/** Ordered row IDs (visible/filtered) */
|
|
182
|
+
orderedIds: string[];
|
|
183
|
+
/** Column IDs in display order */
|
|
184
|
+
columnIds: string[];
|
|
185
|
+
/** Ref to the table container element */
|
|
186
|
+
containerRef: React.RefObject<HTMLElement | null>;
|
|
187
|
+
/** Add a row when navigating past the end */
|
|
188
|
+
onAddRow?: (afterRowId: string) => void;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Keyboard navigation for spreadsheet cells.
|
|
192
|
+
*
|
|
193
|
+
* Tab → next cell → wrap to next row
|
|
194
|
+
* Shift+Tab → previous cell → wrap to previous row
|
|
195
|
+
* Enter → commit + move down (same column, next row)
|
|
196
|
+
*/
|
|
197
|
+
declare function useSpreadsheetKeyboard({
|
|
198
|
+
orderedIds,
|
|
199
|
+
columnIds,
|
|
200
|
+
containerRef,
|
|
201
|
+
onAddRow
|
|
202
|
+
}: UseSpreadsheetKeyboardOptions): {
|
|
203
|
+
handleCellKeyDown: (e: React.KeyboardEvent, rowId: string, colId: string) => void;
|
|
204
|
+
CELL_ATTR: string;
|
|
205
|
+
};
|
|
206
|
+
//#endregion
|
|
207
|
+
export { type CellRenderProps, type SpreadsheetAction, type SpreadsheetColumn, SpreadsheetRow, type SpreadsheetState, type SpreadsheetStore, SpreadsheetTable, type SpreadsheetTableProps, useSpreadsheetKeyboard, useSpreadsheetStore };
|
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { memo, useCallback, useEffect, useMemo, useReducer, useRef } from "react";
|
|
5
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
6
|
+
|
|
7
|
+
//#region src/components/spreadsheet/spreadsheet-row.tsx
|
|
8
|
+
function SpreadsheetRowInner({ rowId, rowIndex, item, visibleColumns, dispatch, isReadOnly, rowActions, onCellKeyDown, cellAttr = "data-cell", isMobile = false, style, measureRef }) {
|
|
9
|
+
const handleChange = useCallback((fieldOrPatch, value) => {
|
|
10
|
+
if (typeof fieldOrPatch === "string") dispatch({
|
|
11
|
+
type: "UPDATE_CELL",
|
|
12
|
+
rowId,
|
|
13
|
+
field: fieldOrPatch,
|
|
14
|
+
value
|
|
15
|
+
});
|
|
16
|
+
else dispatch({
|
|
17
|
+
type: "UPDATE_ROW",
|
|
18
|
+
rowId,
|
|
19
|
+
patch: fieldOrPatch
|
|
20
|
+
});
|
|
21
|
+
}, [dispatch, rowId]);
|
|
22
|
+
if (isMobile) return /* @__PURE__ */ jsxs("div", {
|
|
23
|
+
className: "rounded-lg border bg-card p-3 space-y-2",
|
|
24
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
25
|
+
className: "grid grid-cols-12 gap-2",
|
|
26
|
+
children: visibleColumns.map((col) => {
|
|
27
|
+
const CellComponent = col.cell;
|
|
28
|
+
const field = col.field ?? col.id;
|
|
29
|
+
const value = item?.[field];
|
|
30
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
31
|
+
style: { gridColumn: `span ${col.mobileSpan ?? 12}` },
|
|
32
|
+
[cellAttr]: `${rowId}:${col.id}`,
|
|
33
|
+
children: [col.mobileLabel && /* @__PURE__ */ jsx("label", {
|
|
34
|
+
className: "text-xs text-muted-foreground mb-1 block",
|
|
35
|
+
children: col.mobileLabel
|
|
36
|
+
}), /* @__PURE__ */ jsx(CellComponent, {
|
|
37
|
+
value,
|
|
38
|
+
row: item,
|
|
39
|
+
rowId,
|
|
40
|
+
rowIndex,
|
|
41
|
+
column: col,
|
|
42
|
+
onChange: handleChange,
|
|
43
|
+
isReadOnly
|
|
44
|
+
})]
|
|
45
|
+
}, col.id);
|
|
46
|
+
})
|
|
47
|
+
}), rowActions && /* @__PURE__ */ jsx("div", {
|
|
48
|
+
className: "flex justify-end pt-1",
|
|
49
|
+
children: rowActions({
|
|
50
|
+
rowId,
|
|
51
|
+
rowIndex,
|
|
52
|
+
isReadOnly
|
|
53
|
+
})
|
|
54
|
+
})]
|
|
55
|
+
});
|
|
56
|
+
const isVirtual = !!style;
|
|
57
|
+
return /* @__PURE__ */ jsxs("tr", {
|
|
58
|
+
ref: measureRef,
|
|
59
|
+
"data-index": measureRef ? rowIndex : void 0,
|
|
60
|
+
className: "border-b transition-colors hover:bg-muted/30",
|
|
61
|
+
style,
|
|
62
|
+
children: [visibleColumns.map((col) => {
|
|
63
|
+
const CellComponent = col.cell;
|
|
64
|
+
const field = col.field ?? col.id;
|
|
65
|
+
const value = item?.[field];
|
|
66
|
+
return /* @__PURE__ */ jsx("td", {
|
|
67
|
+
className: `px-2 py-1 overflow-hidden ${col.align === "right" ? "text-right" : col.align === "center" ? "text-center" : "text-left"}`,
|
|
68
|
+
style: isVirtual ? {
|
|
69
|
+
flex: `0 0 ${col.width}`,
|
|
70
|
+
minWidth: 0
|
|
71
|
+
} : void 0,
|
|
72
|
+
[cellAttr]: `${rowId}:${col.id}`,
|
|
73
|
+
onKeyDown: onCellKeyDown ? (e) => onCellKeyDown(e, rowId, col.id) : void 0,
|
|
74
|
+
children: /* @__PURE__ */ jsx(CellComponent, {
|
|
75
|
+
value,
|
|
76
|
+
row: item,
|
|
77
|
+
rowId,
|
|
78
|
+
rowIndex,
|
|
79
|
+
column: col,
|
|
80
|
+
onChange: handleChange,
|
|
81
|
+
isReadOnly
|
|
82
|
+
})
|
|
83
|
+
}, col.id);
|
|
84
|
+
}), rowActions && /* @__PURE__ */ jsx("td", {
|
|
85
|
+
className: "px-2 py-1",
|
|
86
|
+
style: isVirtual ? {
|
|
87
|
+
flex: "0 0 8%",
|
|
88
|
+
minWidth: 0
|
|
89
|
+
} : { width: "8%" },
|
|
90
|
+
children: rowActions({
|
|
91
|
+
rowId,
|
|
92
|
+
rowIndex,
|
|
93
|
+
isReadOnly
|
|
94
|
+
})
|
|
95
|
+
})]
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function rowPropsAreEqual(prev, next) {
|
|
99
|
+
return prev.rowId === next.rowId && prev.rowIndex === next.rowIndex && prev.item === next.item && prev.isReadOnly === next.isReadOnly && prev.isMobile === next.isMobile && prev.visibleColumns === next.visibleColumns && prev.measureRef === next.measureRef && prev.style?.transform === next.style?.transform;
|
|
100
|
+
}
|
|
101
|
+
const SpreadsheetRow = memo(SpreadsheetRowInner, rowPropsAreEqual);
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/components/spreadsheet/use-spreadsheet-keyboard.ts
|
|
105
|
+
/**
|
|
106
|
+
* Data attribute used to mark focusable cells for keyboard navigation.
|
|
107
|
+
* Each cell should have: data-cell="rowId:colId"
|
|
108
|
+
*/
|
|
109
|
+
const CELL_ATTR = "data-cell";
|
|
110
|
+
function findCell(container, rowId, colId) {
|
|
111
|
+
return container.querySelector(`[${CELL_ATTR}="${rowId}:${colId}"]`);
|
|
112
|
+
}
|
|
113
|
+
function focusCell(cell) {
|
|
114
|
+
const focusable = cell.querySelector("input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])");
|
|
115
|
+
if (focusable) focusable.focus();
|
|
116
|
+
else cell.focus();
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Keyboard navigation for spreadsheet cells.
|
|
120
|
+
*
|
|
121
|
+
* Tab → next cell → wrap to next row
|
|
122
|
+
* Shift+Tab → previous cell → wrap to previous row
|
|
123
|
+
* Enter → commit + move down (same column, next row)
|
|
124
|
+
*/
|
|
125
|
+
function useSpreadsheetKeyboard({ orderedIds, columnIds, containerRef, onAddRow }) {
|
|
126
|
+
const orderedIdsRef = useRef(orderedIds);
|
|
127
|
+
const columnIdsRef = useRef(columnIds);
|
|
128
|
+
const onAddRowRef = useRef(onAddRow);
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
orderedIdsRef.current = orderedIds;
|
|
131
|
+
columnIdsRef.current = columnIds;
|
|
132
|
+
onAddRowRef.current = onAddRow;
|
|
133
|
+
}, [
|
|
134
|
+
orderedIds,
|
|
135
|
+
columnIds,
|
|
136
|
+
onAddRow
|
|
137
|
+
]);
|
|
138
|
+
return {
|
|
139
|
+
handleCellKeyDown: useCallback((e, rowId, colId) => {
|
|
140
|
+
const container = containerRef.current;
|
|
141
|
+
if (!container) return;
|
|
142
|
+
const currentOrderedIds = orderedIdsRef.current;
|
|
143
|
+
const currentColumnIds = columnIdsRef.current;
|
|
144
|
+
const rowIdx = currentOrderedIds.indexOf(rowId);
|
|
145
|
+
const colIdx = currentColumnIds.indexOf(colId);
|
|
146
|
+
if (rowIdx === -1 || colIdx === -1) return;
|
|
147
|
+
let targetRowIdx = rowIdx;
|
|
148
|
+
let targetColIdx = colIdx;
|
|
149
|
+
if (e.key === "Tab") {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
if (e.shiftKey) {
|
|
152
|
+
targetColIdx--;
|
|
153
|
+
if (targetColIdx < 0) {
|
|
154
|
+
targetRowIdx--;
|
|
155
|
+
targetColIdx = currentColumnIds.length - 1;
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
targetColIdx++;
|
|
159
|
+
if (targetColIdx >= currentColumnIds.length) {
|
|
160
|
+
targetRowIdx++;
|
|
161
|
+
targetColIdx = 0;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} else if (e.key === "Enter" && !e.shiftKey) {
|
|
165
|
+
if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
targetRowIdx++;
|
|
168
|
+
} else if (e.key === "ArrowDown") {
|
|
169
|
+
e.preventDefault();
|
|
170
|
+
targetRowIdx++;
|
|
171
|
+
} else if (e.key === "ArrowUp") {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
targetRowIdx--;
|
|
174
|
+
} else if (e.key === "ArrowRight") {
|
|
175
|
+
const active = document.activeElement;
|
|
176
|
+
if (!active || active.tagName !== "INPUT" || active.selectionStart === active.value?.length) {
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
targetColIdx++;
|
|
179
|
+
} else return;
|
|
180
|
+
} else if (e.key === "ArrowLeft") {
|
|
181
|
+
const active = document.activeElement;
|
|
182
|
+
if (!active || active.tagName !== "INPUT" || active.selectionEnd === 0) {
|
|
183
|
+
e.preventDefault();
|
|
184
|
+
targetColIdx--;
|
|
185
|
+
} else return;
|
|
186
|
+
} else return;
|
|
187
|
+
if (targetRowIdx >= currentOrderedIds.length) {
|
|
188
|
+
if (onAddRowRef.current) {
|
|
189
|
+
const lastRowId = currentOrderedIds[currentOrderedIds.length - 1];
|
|
190
|
+
onAddRowRef.current(lastRowId);
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (targetRowIdx < 0) return;
|
|
195
|
+
if (targetColIdx < 0 || targetColIdx >= currentColumnIds.length) return;
|
|
196
|
+
const targetRowId = currentOrderedIds[targetRowIdx];
|
|
197
|
+
const targetColId = currentColumnIds[targetColIdx];
|
|
198
|
+
const targetCell = findCell(container, targetRowId, targetColId);
|
|
199
|
+
if (targetCell) requestAnimationFrame(() => focusCell(targetCell));
|
|
200
|
+
}, [containerRef]),
|
|
201
|
+
CELL_ATTR
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/components/spreadsheet/use-spreadsheet-clipboard.ts
|
|
207
|
+
/**
|
|
208
|
+
* Handles copy/pasting from Excel or other spreadsheets natively.
|
|
209
|
+
* Supports matrix pasting (multiple rows and columns at once) via BATCH updates.
|
|
210
|
+
*/
|
|
211
|
+
function useSpreadsheetClipboard({ containerRef, orderedIds, columns, dispatch, isReadOnly = false }) {
|
|
212
|
+
const orderedIdsRef = useRef(orderedIds);
|
|
213
|
+
const columnsRef = useRef(columns);
|
|
214
|
+
const isReadOnlyRef = useRef(isReadOnly);
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
orderedIdsRef.current = orderedIds;
|
|
217
|
+
columnsRef.current = columns;
|
|
218
|
+
isReadOnlyRef.current = isReadOnly;
|
|
219
|
+
}, [
|
|
220
|
+
orderedIds,
|
|
221
|
+
columns,
|
|
222
|
+
isReadOnly
|
|
223
|
+
]);
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
const container = containerRef.current;
|
|
226
|
+
if (!container) return;
|
|
227
|
+
function handlePaste(e) {
|
|
228
|
+
if (isReadOnlyRef.current) return;
|
|
229
|
+
const activeElement = document.activeElement;
|
|
230
|
+
if (!activeElement || !container?.contains(activeElement)) return;
|
|
231
|
+
const cellElement = activeElement.closest("[data-cell]");
|
|
232
|
+
if (!cellElement) return;
|
|
233
|
+
const cellCoord = cellElement.getAttribute("data-cell");
|
|
234
|
+
if (!cellCoord) return;
|
|
235
|
+
const [startRowId, startColId] = cellCoord.split(":");
|
|
236
|
+
const clipboardData = e.clipboardData;
|
|
237
|
+
if (!clipboardData) return;
|
|
238
|
+
const pastedText = clipboardData.getData("text/plain");
|
|
239
|
+
if (!pastedText) return;
|
|
240
|
+
if (!(pastedText.includes(" ") || pastedText.includes("\n"))) return;
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
const currentOrderedIds = orderedIdsRef.current;
|
|
243
|
+
const visibleCols = columnsRef.current.filter((c) => !c.hiddenWhen?.({ isReadOnly: isReadOnlyRef.current }));
|
|
244
|
+
const startRowIdx = currentOrderedIds.indexOf(startRowId);
|
|
245
|
+
const startColIdx = visibleCols.findIndex((c) => c.id === startColId);
|
|
246
|
+
if (startRowIdx === -1 || startColIdx === -1) return;
|
|
247
|
+
const rows = pastedText.replace(/\r?\n$/, "").split(/\r?\n/);
|
|
248
|
+
const batchActions = [];
|
|
249
|
+
rows.forEach((row, rIdx) => {
|
|
250
|
+
const targetRowIdx = startRowIdx + rIdx;
|
|
251
|
+
if (targetRowIdx >= currentOrderedIds.length) return;
|
|
252
|
+
const targetRowId = currentOrderedIds[targetRowIdx];
|
|
253
|
+
row.split(" ").forEach((cellValue, cIdx) => {
|
|
254
|
+
const targetColIdx = startColIdx + cIdx;
|
|
255
|
+
if (targetColIdx >= visibleCols.length) return;
|
|
256
|
+
const col = visibleCols[targetColIdx];
|
|
257
|
+
const field = col.field ?? col.id;
|
|
258
|
+
let value = cellValue;
|
|
259
|
+
if (col.dataType === "number") {
|
|
260
|
+
const num = Number(cellValue);
|
|
261
|
+
value = isNaN(num) ? 0 : num;
|
|
262
|
+
}
|
|
263
|
+
batchActions.push({
|
|
264
|
+
type: "UPDATE_CELL",
|
|
265
|
+
rowId: targetRowId,
|
|
266
|
+
field,
|
|
267
|
+
value
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
if (batchActions.length > 0) dispatch({
|
|
272
|
+
type: "BATCH",
|
|
273
|
+
actions: batchActions
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
container.addEventListener("paste", handlePaste);
|
|
277
|
+
return () => container.removeEventListener("paste", handlePaste);
|
|
278
|
+
}, [containerRef, dispatch]);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/components/spreadsheet/spreadsheet-table.tsx
|
|
283
|
+
const VIRTUAL_ROW_BASE = {
|
|
284
|
+
display: "flex",
|
|
285
|
+
alignItems: "stretch",
|
|
286
|
+
position: "absolute",
|
|
287
|
+
top: 0,
|
|
288
|
+
left: 0,
|
|
289
|
+
width: "100%",
|
|
290
|
+
willChange: "transform"
|
|
291
|
+
};
|
|
292
|
+
function SpreadsheetTable({ columns, items, orderedIds, dispatch, isReadOnly = false, footer, mobileFooter, rowActions, className = "", ariaLabel = "Spreadsheet", onAddRow, virtualize = false, estimateRowHeight = 48 }) {
|
|
293
|
+
const containerRef = useRef(null);
|
|
294
|
+
const tableContainerRef = useRef(null);
|
|
295
|
+
const mobileContainerRef = useRef(null);
|
|
296
|
+
const visibleColumns = useMemo(() => columns.filter((col) => !col.hiddenWhen?.({ isReadOnly })), [columns, isReadOnly]);
|
|
297
|
+
const { handleCellKeyDown, CELL_ATTR } = useSpreadsheetKeyboard({
|
|
298
|
+
orderedIds,
|
|
299
|
+
columnIds: useMemo(() => visibleColumns.map((col) => col.id), [visibleColumns]),
|
|
300
|
+
containerRef,
|
|
301
|
+
onAddRow
|
|
302
|
+
});
|
|
303
|
+
useSpreadsheetClipboard({
|
|
304
|
+
containerRef,
|
|
305
|
+
orderedIds,
|
|
306
|
+
columns,
|
|
307
|
+
dispatch,
|
|
308
|
+
isReadOnly
|
|
309
|
+
});
|
|
310
|
+
const getItemKey = useCallback((index) => orderedIds[index] ?? index, [orderedIds]);
|
|
311
|
+
const estimateSize = useCallback(() => estimateRowHeight, [estimateRowHeight]);
|
|
312
|
+
const desktopVirtualizer = useVirtualizer({
|
|
313
|
+
count: orderedIds.length,
|
|
314
|
+
getScrollElement: () => tableContainerRef.current,
|
|
315
|
+
estimateSize,
|
|
316
|
+
measureElement: virtualize ? (el) => el.getBoundingClientRect().height : void 0,
|
|
317
|
+
overscan: 5,
|
|
318
|
+
getItemKey,
|
|
319
|
+
enabled: virtualize
|
|
320
|
+
});
|
|
321
|
+
const mobileVirtualizer = useVirtualizer({
|
|
322
|
+
count: orderedIds.length,
|
|
323
|
+
getScrollElement: () => mobileContainerRef.current,
|
|
324
|
+
estimateSize,
|
|
325
|
+
measureElement: virtualize ? (el) => el.getBoundingClientRect().height : void 0,
|
|
326
|
+
overscan: 3,
|
|
327
|
+
getItemKey,
|
|
328
|
+
enabled: virtualize
|
|
329
|
+
});
|
|
330
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
331
|
+
ref: containerRef,
|
|
332
|
+
className,
|
|
333
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
334
|
+
ref: tableContainerRef,
|
|
335
|
+
className: "hidden md:block overflow-x-auto",
|
|
336
|
+
style: virtualize ? {
|
|
337
|
+
maxHeight: "600px",
|
|
338
|
+
overflowY: "auto"
|
|
339
|
+
} : void 0,
|
|
340
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
341
|
+
className: "w-full border-collapse table-fixed",
|
|
342
|
+
style: { minWidth: "800px" },
|
|
343
|
+
"aria-label": ariaLabel,
|
|
344
|
+
children: [
|
|
345
|
+
/* @__PURE__ */ jsx("thead", {
|
|
346
|
+
className: "sticky top-0 z-10 bg-background",
|
|
347
|
+
children: /* @__PURE__ */ jsxs("tr", {
|
|
348
|
+
className: "border-b bg-muted/50",
|
|
349
|
+
style: virtualize ? {
|
|
350
|
+
display: "flex",
|
|
351
|
+
width: "100%"
|
|
352
|
+
} : void 0,
|
|
353
|
+
children: [visibleColumns.map((col) => /* @__PURE__ */ jsx("th", {
|
|
354
|
+
className: `px-2 py-2 text-xs font-medium text-muted-foreground ${col.align === "right" ? "text-right" : col.align === "center" ? "text-center" : "text-left"}`,
|
|
355
|
+
style: virtualize ? {
|
|
356
|
+
width: col.width,
|
|
357
|
+
flex: `0 0 ${col.width}`,
|
|
358
|
+
minWidth: 0
|
|
359
|
+
} : { width: col.width },
|
|
360
|
+
children: col.header
|
|
361
|
+
}, col.id)), rowActions && /* @__PURE__ */ jsx("th", {
|
|
362
|
+
className: "px-2 py-2",
|
|
363
|
+
style: virtualize ? {
|
|
364
|
+
width: "8%",
|
|
365
|
+
flex: "0 0 8%",
|
|
366
|
+
minWidth: 0
|
|
367
|
+
} : { width: "8%" }
|
|
368
|
+
})]
|
|
369
|
+
})
|
|
370
|
+
}),
|
|
371
|
+
/* @__PURE__ */ jsx("tbody", {
|
|
372
|
+
style: virtualize ? {
|
|
373
|
+
height: `${desktopVirtualizer.getTotalSize()}px`,
|
|
374
|
+
position: "relative"
|
|
375
|
+
} : void 0,
|
|
376
|
+
children: virtualize ? desktopVirtualizer.getVirtualItems().map((virtualRow) => {
|
|
377
|
+
const rowId = orderedIds[virtualRow.index];
|
|
378
|
+
const item = items.get(rowId);
|
|
379
|
+
if (!item) return null;
|
|
380
|
+
return /* @__PURE__ */ jsx(SpreadsheetRow, {
|
|
381
|
+
rowId,
|
|
382
|
+
rowIndex: virtualRow.index,
|
|
383
|
+
item,
|
|
384
|
+
visibleColumns,
|
|
385
|
+
dispatch,
|
|
386
|
+
isReadOnly,
|
|
387
|
+
rowActions,
|
|
388
|
+
onCellKeyDown: handleCellKeyDown,
|
|
389
|
+
cellAttr: CELL_ATTR,
|
|
390
|
+
isMobile: false,
|
|
391
|
+
measureRef: desktopVirtualizer.measureElement,
|
|
392
|
+
style: {
|
|
393
|
+
...VIRTUAL_ROW_BASE,
|
|
394
|
+
transform: `translateY(${virtualRow.start}px)`
|
|
395
|
+
}
|
|
396
|
+
}, virtualRow.key);
|
|
397
|
+
}) : orderedIds.map((rowId, index) => {
|
|
398
|
+
const item = items.get(rowId);
|
|
399
|
+
if (!item) return null;
|
|
400
|
+
return /* @__PURE__ */ jsx(SpreadsheetRow, {
|
|
401
|
+
rowId,
|
|
402
|
+
rowIndex: index,
|
|
403
|
+
item,
|
|
404
|
+
visibleColumns,
|
|
405
|
+
dispatch,
|
|
406
|
+
isReadOnly,
|
|
407
|
+
rowActions,
|
|
408
|
+
onCellKeyDown: handleCellKeyDown,
|
|
409
|
+
cellAttr: CELL_ATTR,
|
|
410
|
+
isMobile: false
|
|
411
|
+
}, rowId);
|
|
412
|
+
})
|
|
413
|
+
}),
|
|
414
|
+
footer && /* @__PURE__ */ jsx("tfoot", { children: footer })
|
|
415
|
+
]
|
|
416
|
+
})
|
|
417
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
418
|
+
ref: mobileContainerRef,
|
|
419
|
+
className: "md:hidden space-y-3",
|
|
420
|
+
style: virtualize ? {
|
|
421
|
+
maxHeight: "600px",
|
|
422
|
+
overflowY: "auto"
|
|
423
|
+
} : void 0,
|
|
424
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
425
|
+
style: virtualize ? {
|
|
426
|
+
height: `${mobileVirtualizer.getTotalSize()}px`,
|
|
427
|
+
position: "relative"
|
|
428
|
+
} : void 0,
|
|
429
|
+
children: virtualize ? mobileVirtualizer.getVirtualItems().map((virtualRow) => {
|
|
430
|
+
const rowId = orderedIds[virtualRow.index];
|
|
431
|
+
const item = items.get(rowId);
|
|
432
|
+
if (!item) return null;
|
|
433
|
+
return /* @__PURE__ */ jsx(SpreadsheetRow, {
|
|
434
|
+
rowId,
|
|
435
|
+
rowIndex: virtualRow.index,
|
|
436
|
+
item,
|
|
437
|
+
visibleColumns,
|
|
438
|
+
dispatch,
|
|
439
|
+
isReadOnly,
|
|
440
|
+
rowActions,
|
|
441
|
+
cellAttr: CELL_ATTR,
|
|
442
|
+
isMobile: true,
|
|
443
|
+
measureRef: mobileVirtualizer.measureElement,
|
|
444
|
+
style: {
|
|
445
|
+
...VIRTUAL_ROW_BASE,
|
|
446
|
+
transform: `translateY(${virtualRow.start}px)`
|
|
447
|
+
}
|
|
448
|
+
}, virtualRow.key);
|
|
449
|
+
}) : orderedIds.map((rowId, index) => {
|
|
450
|
+
const item = items.get(rowId);
|
|
451
|
+
if (!item) return null;
|
|
452
|
+
return /* @__PURE__ */ jsx(SpreadsheetRow, {
|
|
453
|
+
rowId,
|
|
454
|
+
rowIndex: index,
|
|
455
|
+
item,
|
|
456
|
+
visibleColumns,
|
|
457
|
+
dispatch,
|
|
458
|
+
isReadOnly,
|
|
459
|
+
rowActions,
|
|
460
|
+
cellAttr: CELL_ATTR,
|
|
461
|
+
isMobile: true
|
|
462
|
+
}, rowId);
|
|
463
|
+
})
|
|
464
|
+
}), mobileFooter && /* @__PURE__ */ jsx("div", {
|
|
465
|
+
className: "rounded-lg border bg-card p-3",
|
|
466
|
+
children: mobileFooter
|
|
467
|
+
})]
|
|
468
|
+
})]
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
//#endregion
|
|
473
|
+
//#region src/components/spreadsheet/use-spreadsheet-store.ts
|
|
474
|
+
function defaultGetId(item) {
|
|
475
|
+
if (item && typeof item === "object") {
|
|
476
|
+
const obj = item;
|
|
477
|
+
const id = obj._id ?? obj.id;
|
|
478
|
+
if (typeof id === "string" && id) return id;
|
|
479
|
+
}
|
|
480
|
+
return crypto.randomUUID();
|
|
481
|
+
}
|
|
482
|
+
function spreadsheetReducer(state, action) {
|
|
483
|
+
switch (action.type) {
|
|
484
|
+
case "SET_ALL": {
|
|
485
|
+
const getId = action.getId ?? defaultGetId;
|
|
486
|
+
const items = /* @__PURE__ */ new Map();
|
|
487
|
+
const orderedIds = [];
|
|
488
|
+
for (const item of action.items) {
|
|
489
|
+
let id = getId(item);
|
|
490
|
+
while (items.has(id)) id = `${id}_${crypto.randomUUID().slice(0, 8)}`;
|
|
491
|
+
items.set(id, item);
|
|
492
|
+
orderedIds.push(id);
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
items,
|
|
496
|
+
orderedIds
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
case "UPDATE_CELL": {
|
|
500
|
+
const existing = state.items.get(action.rowId);
|
|
501
|
+
if (!existing) return state;
|
|
502
|
+
const updated = {
|
|
503
|
+
...existing,
|
|
504
|
+
[action.field]: action.value
|
|
505
|
+
};
|
|
506
|
+
const newItems = new Map(state.items);
|
|
507
|
+
newItems.set(action.rowId, updated);
|
|
508
|
+
return {
|
|
509
|
+
...state,
|
|
510
|
+
items: newItems
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
case "UPDATE_ROW": {
|
|
514
|
+
const existing = state.items.get(action.rowId);
|
|
515
|
+
if (!existing) return state;
|
|
516
|
+
const updated = {
|
|
517
|
+
...existing,
|
|
518
|
+
...action.patch
|
|
519
|
+
};
|
|
520
|
+
const newItems = new Map(state.items);
|
|
521
|
+
newItems.set(action.rowId, updated);
|
|
522
|
+
return {
|
|
523
|
+
...state,
|
|
524
|
+
items: newItems
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
case "ADD_ROW": {
|
|
528
|
+
const newId = action.newId ?? crypto.randomUUID();
|
|
529
|
+
const newItems = new Map(state.items);
|
|
530
|
+
newItems.set(newId, action.item);
|
|
531
|
+
const idx = state.orderedIds.indexOf(action.afterRowId);
|
|
532
|
+
const insertAt = idx === -1 ? state.orderedIds.length : idx + 1;
|
|
533
|
+
const newIds = [...state.orderedIds];
|
|
534
|
+
newIds.splice(insertAt, 0, newId);
|
|
535
|
+
return {
|
|
536
|
+
items: newItems,
|
|
537
|
+
orderedIds: newIds
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
case "REMOVE_ROW": {
|
|
541
|
+
if (state.orderedIds.length <= 1) return state;
|
|
542
|
+
const newItems = new Map(state.items);
|
|
543
|
+
newItems.delete(action.rowId);
|
|
544
|
+
return {
|
|
545
|
+
items: newItems,
|
|
546
|
+
orderedIds: state.orderedIds.filter((id) => id !== action.rowId)
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
case "BATCH": {
|
|
550
|
+
if (action.actions.every((a) => a.type === "UPDATE_CELL")) {
|
|
551
|
+
const newItems = new Map(state.items);
|
|
552
|
+
for (const sub of action.actions) {
|
|
553
|
+
if (sub.type !== "UPDATE_CELL") continue;
|
|
554
|
+
const existing = newItems.get(sub.rowId);
|
|
555
|
+
if (!existing) continue;
|
|
556
|
+
newItems.set(sub.rowId, {
|
|
557
|
+
...existing,
|
|
558
|
+
[sub.field]: sub.value
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
...state,
|
|
563
|
+
items: newItems
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
let current = state;
|
|
567
|
+
for (const sub of action.actions) current = spreadsheetReducer(current, sub);
|
|
568
|
+
return current;
|
|
569
|
+
}
|
|
570
|
+
default: return state;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const EMPTY_STATE = {
|
|
574
|
+
items: /* @__PURE__ */ new Map(),
|
|
575
|
+
orderedIds: []
|
|
576
|
+
};
|
|
577
|
+
function useSpreadsheetStore(initialItems, getId) {
|
|
578
|
+
const [state, dispatch] = useReducer(spreadsheetReducer, initialItems, (items) => {
|
|
579
|
+
if (!items || items.length === 0) return EMPTY_STATE;
|
|
580
|
+
return spreadsheetReducer(EMPTY_STATE, {
|
|
581
|
+
type: "SET_ALL",
|
|
582
|
+
items,
|
|
583
|
+
getId
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
const stateRef = useRef(state);
|
|
587
|
+
stateRef.current = state;
|
|
588
|
+
/** Get items as an ordered array (for API submission) — stable reference */
|
|
589
|
+
const getOrderedItems = useCallback(() => {
|
|
590
|
+
const { orderedIds, items } = stateRef.current;
|
|
591
|
+
return orderedIds.map((id) => items.get(id)).filter(Boolean);
|
|
592
|
+
}, []);
|
|
593
|
+
/** Convenience: total row count */
|
|
594
|
+
const rowCount = state.orderedIds.length;
|
|
595
|
+
return useMemo(() => ({
|
|
596
|
+
items: state.items,
|
|
597
|
+
orderedIds: state.orderedIds,
|
|
598
|
+
dispatch,
|
|
599
|
+
getOrderedItems,
|
|
600
|
+
rowCount
|
|
601
|
+
}), [
|
|
602
|
+
state.items,
|
|
603
|
+
state.orderedIds,
|
|
604
|
+
dispatch,
|
|
605
|
+
getOrderedItems,
|
|
606
|
+
rowCount
|
|
607
|
+
]);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
//#endregion
|
|
611
|
+
export { SpreadsheetRow, SpreadsheetTable, useSpreadsheetKeyboard, useSpreadsheetStore };
|
package/dist/forms.mjs
CHANGED
|
@@ -1325,7 +1325,7 @@ function TagChoiceInputInternal({ name, label, description, placeholder = "Selec
|
|
|
1325
1325
|
}),
|
|
1326
1326
|
children: [/* @__PURE__ */ jsx("div", {
|
|
1327
1327
|
className: "flex flex-1 flex-wrap items-center gap-1 px-3 py-1.5 min-h-[2.25rem]",
|
|
1328
|
-
children: selectedValues.length > 0 ? selectedValues.map((val) => {
|
|
1328
|
+
children: selectedValues.length > 0 ? selectedValues.map((val, idx) => {
|
|
1329
1329
|
const item = items.find((i) => i.value === val);
|
|
1330
1330
|
return /* @__PURE__ */ jsxs(Badge, {
|
|
1331
1331
|
variant: "secondary",
|
|
@@ -1338,7 +1338,7 @@ function TagChoiceInputInternal({ name, label, description, placeholder = "Selec
|
|
|
1338
1338
|
"aria-label": `Remove ${item?.label || val}`,
|
|
1339
1339
|
children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
|
|
1340
1340
|
})]
|
|
1341
|
-
}, val);
|
|
1341
|
+
}, val || `tag-${idx}`);
|
|
1342
1342
|
}) : /* @__PURE__ */ jsx("span", {
|
|
1343
1343
|
className: "text-sm text-muted-foreground",
|
|
1344
1344
|
children: placeholder
|
|
@@ -1485,7 +1485,7 @@ function ComboboxInput({ control, name, label, placeholder = "Select...", emptyT
|
|
|
1485
1485
|
onValueChangeRef.current = onValueChange;
|
|
1486
1486
|
const propOnChangeRef = useRef(propOnChange);
|
|
1487
1487
|
propOnChangeRef.current = propOnChange;
|
|
1488
|
-
const fieldRef = useRef();
|
|
1488
|
+
const fieldRef = useRef(void 0);
|
|
1489
1489
|
const handleValueChange = useCallback((newItem) => {
|
|
1490
1490
|
const safeValue = newItem?.value || "";
|
|
1491
1491
|
if (safeValue === currentValueRef.current) return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/fluid",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Fluid UI - Custom components built on shadcn/ui and base ui by Classytic",
|
|
6
6
|
"main": "./dist/index.mjs",
|
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
"types": "./dist/client/calendar.d.mts",
|
|
36
36
|
"default": "./dist/client/calendar.mjs"
|
|
37
37
|
},
|
|
38
|
+
"./client/spreadsheet": {
|
|
39
|
+
"types": "./dist/client/spreadsheet.d.mts",
|
|
40
|
+
"default": "./dist/client/spreadsheet.mjs"
|
|
41
|
+
},
|
|
38
42
|
"./forms": {
|
|
39
43
|
"types": "./dist/forms.d.mts",
|
|
40
44
|
"default": "./dist/forms.mjs"
|
|
@@ -71,30 +75,36 @@
|
|
|
71
75
|
"scripts": {
|
|
72
76
|
"build": "tsdown",
|
|
73
77
|
"dev": "tsdown --watch",
|
|
78
|
+
"test": "vitest run",
|
|
79
|
+
"test:watch": "vitest",
|
|
74
80
|
"typecheck": "tsc --noEmit",
|
|
75
81
|
"lint": "tsc --noEmit",
|
|
76
82
|
"clean": "rimraf dist",
|
|
77
|
-
"prepublishOnly": "npm run clean && npm run build"
|
|
83
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run clean && npm run build"
|
|
78
84
|
},
|
|
79
85
|
"peerDependencies": {
|
|
80
|
-
"@tanstack/react-table": "
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
86
|
+
"@tanstack/react-table": ">=8.21.0",
|
|
87
|
+
"@tanstack/react-virtual": ">=3.11.2",
|
|
88
|
+
"class-variance-authority": ">=0.7.0",
|
|
89
|
+
"clsx": ">=2.1.0",
|
|
90
|
+
"date-fns": ">=4.1.0",
|
|
84
91
|
"lucide-react": ">=0.460.0",
|
|
85
|
-
"next": "
|
|
86
|
-
"next-themes": "
|
|
87
|
-
"react": "
|
|
88
|
-
"react-dom": "
|
|
89
|
-
"react-error-boundary": "
|
|
90
|
-
"react-hook-form": "
|
|
91
|
-
"tailwind-merge": "
|
|
92
|
-
"vaul": "
|
|
92
|
+
"next": ">=16.0.0",
|
|
93
|
+
"next-themes": ">=0.4.0",
|
|
94
|
+
"react": ">=19.0.0",
|
|
95
|
+
"react-dom": ">=19.0.0",
|
|
96
|
+
"react-error-boundary": ">=6.0.0",
|
|
97
|
+
"react-hook-form": ">=7.54.0",
|
|
98
|
+
"tailwind-merge": ">=2.2.0",
|
|
99
|
+
"vaul": ">=1.0.0"
|
|
93
100
|
},
|
|
94
101
|
"peerDependenciesMeta": {
|
|
95
102
|
"@tanstack/react-table": {
|
|
96
103
|
"optional": true
|
|
97
104
|
},
|
|
105
|
+
"@tanstack/react-virtual": {
|
|
106
|
+
"optional": true
|
|
107
|
+
},
|
|
98
108
|
"next-themes": {
|
|
99
109
|
"optional": true
|
|
100
110
|
},
|
|
@@ -113,12 +123,18 @@
|
|
|
113
123
|
},
|
|
114
124
|
"devDependencies": {
|
|
115
125
|
"@tanstack/react-table": "^8.21.0",
|
|
126
|
+
"@tanstack/react-virtual": "^3.13.23",
|
|
127
|
+
"@testing-library/dom": "^10.4.1",
|
|
128
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
129
|
+
"@testing-library/react": "^16.3.2",
|
|
130
|
+
"@testing-library/user-event": "^14.6.1",
|
|
116
131
|
"@types/node": "^22.0.0",
|
|
117
132
|
"@types/react": "^19.0.0",
|
|
118
133
|
"@types/react-dom": "^19.0.0",
|
|
119
134
|
"class-variance-authority": "^0.7.0",
|
|
120
135
|
"clsx": "^2.1.0",
|
|
121
136
|
"date-fns": "^4.1.0",
|
|
137
|
+
"jsdom": "^29.0.0",
|
|
122
138
|
"lucide-react": "^0.460.0",
|
|
123
139
|
"next-themes": "^0.4.0",
|
|
124
140
|
"react": "^19.0.0",
|
|
@@ -128,7 +144,8 @@
|
|
|
128
144
|
"rimraf": "^5.0.0",
|
|
129
145
|
"tailwind-merge": "^3.5.0",
|
|
130
146
|
"tsdown": "^0.21.0-beta.2",
|
|
131
|
-
"typescript": "^5.7.0"
|
|
147
|
+
"typescript": "^5.7.0",
|
|
148
|
+
"vitest": "^4.1.0"
|
|
132
149
|
},
|
|
133
150
|
"publishConfig": {
|
|
134
151
|
"access": "public"
|