@optilogic/core 1.0.0-beta.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/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/index.cjs +6003 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2310 -0
- package/dist/index.d.ts +2310 -0
- package/dist/index.js +5828 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +96 -0
- package/dist/tailwind-preset.cjs +106 -0
- package/dist/tailwind-preset.cjs.map +1 -0
- package/dist/tailwind-preset.d.cts +23 -0
- package/dist/tailwind-preset.d.ts +23 -0
- package/dist/tailwind-preset.js +101 -0
- package/dist/tailwind-preset.js.map +1 -0
- package/package.json +154 -0
- package/src/components/accordion.tsx +187 -0
- package/src/components/alert-dialog.tsx +143 -0
- package/src/components/autocomplete.tsx +271 -0
- package/src/components/badge.tsx +62 -0
- package/src/components/button.tsx +85 -0
- package/src/components/calendar.tsx +235 -0
- package/src/components/card.tsx +94 -0
- package/src/components/checkbox.tsx +77 -0
- package/src/components/chip.tsx +77 -0
- package/src/components/confirmation-modal.tsx +195 -0
- package/src/components/context-menu.tsx +406 -0
- package/src/components/copy-button.tsx +84 -0
- package/src/components/data-grid/DataGrid.tsx +1027 -0
- package/src/components/data-grid/components/CellEditor.tsx +346 -0
- package/src/components/data-grid/components/FilterPopover.tsx +459 -0
- package/src/components/data-grid/components/HeaderCell.tsx +207 -0
- package/src/components/data-grid/components/index.ts +14 -0
- package/src/components/data-grid/hooks/index.ts +28 -0
- package/src/components/data-grid/hooks/useColumnResize.ts +378 -0
- package/src/components/data-grid/hooks/useDataGridState.ts +346 -0
- package/src/components/data-grid/hooks/useKeyboardNavigation.ts +361 -0
- package/src/components/data-grid/index.ts +71 -0
- package/src/components/data-grid/types.ts +478 -0
- package/src/components/data-grid/utils/dataProcessing.ts +277 -0
- package/src/components/data-grid/utils/index.ts +12 -0
- package/src/components/date-picker.tsx +366 -0
- package/src/components/dropdown-menu.tsx +230 -0
- package/src/components/icon-button.tsx +157 -0
- package/src/components/input.tsx +40 -0
- package/src/components/label.tsx +37 -0
- package/src/components/loading-spinner.tsx +113 -0
- package/src/components/modal.tsx +207 -0
- package/src/components/popover.tsx +62 -0
- package/src/components/progress.tsx +41 -0
- package/src/components/resizable-panel.tsx +434 -0
- package/src/components/resize-handle.tsx +187 -0
- package/src/components/select.tsx +160 -0
- package/src/components/separator.tsx +50 -0
- package/src/components/skeleton.tsx +37 -0
- package/src/components/switch.tsx +59 -0
- package/src/components/table.tsx +136 -0
- package/src/components/tabs.tsx +102 -0
- package/src/components/textarea.tsx +36 -0
- package/src/components/theme-picker.tsx +245 -0
- package/src/components/toaster.tsx +84 -0
- package/src/components/tooltip.tsx +199 -0
- package/src/index.ts +318 -0
- package/src/styles.css +96 -0
- package/src/tailwind-preset.ts +129 -0
- package/src/theme/index.ts +41 -0
- package/src/theme/presets.ts +502 -0
- package/src/theme/types.ts +164 -0
- package/src/theme/utils.ts +309 -0
- package/src/utils/cn.ts +14 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CellEditor Component
|
|
3
|
+
*
|
|
4
|
+
* Renders an inline cell editor based on column editor type:
|
|
5
|
+
* - Text: Input field
|
|
6
|
+
* - Number: Number input
|
|
7
|
+
* - Date: DatePicker with calendar
|
|
8
|
+
* - Select: Dropdown select
|
|
9
|
+
* - Boolean: Checkbox
|
|
10
|
+
*
|
|
11
|
+
* Supports validation with visual feedback.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as React from "react";
|
|
15
|
+
import { format, parseISO, isValid } from "date-fns";
|
|
16
|
+
import { cn } from "../../../utils/cn";
|
|
17
|
+
import { Input } from "../../input";
|
|
18
|
+
import { Checkbox } from "../../checkbox";
|
|
19
|
+
import { DatePicker } from "../../date-picker";
|
|
20
|
+
import {
|
|
21
|
+
Select,
|
|
22
|
+
SelectContent,
|
|
23
|
+
SelectItem,
|
|
24
|
+
SelectTrigger,
|
|
25
|
+
SelectValue,
|
|
26
|
+
} from "../../select";
|
|
27
|
+
import type { ColumnDef, EditorType, CellValue } from "../types";
|
|
28
|
+
|
|
29
|
+
export interface CellEditorProps<T = Record<string, CellValue>> {
|
|
30
|
+
/** Column definition */
|
|
31
|
+
column: ColumnDef<T>;
|
|
32
|
+
/** Current value being edited */
|
|
33
|
+
value: CellValue;
|
|
34
|
+
/** The row data */
|
|
35
|
+
row: T;
|
|
36
|
+
/** Row index */
|
|
37
|
+
rowIndex: number;
|
|
38
|
+
/** Callback to commit the edit */
|
|
39
|
+
onCommit: (value: CellValue) => void;
|
|
40
|
+
/** Callback to cancel the edit */
|
|
41
|
+
onCancel: () => void;
|
|
42
|
+
/** Callback when value changes (for controlled updates) */
|
|
43
|
+
onChange?: (value: CellValue) => void;
|
|
44
|
+
/** Additional class name */
|
|
45
|
+
className?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function CellEditor<T = Record<string, CellValue>>({
|
|
49
|
+
column,
|
|
50
|
+
value,
|
|
51
|
+
row,
|
|
52
|
+
rowIndex,
|
|
53
|
+
onCommit,
|
|
54
|
+
onCancel,
|
|
55
|
+
onChange,
|
|
56
|
+
className,
|
|
57
|
+
}: CellEditorProps<T>) {
|
|
58
|
+
const editorType = column.editorType || "text";
|
|
59
|
+
const [internalValue, setInternalValue] = React.useState(value);
|
|
60
|
+
const [validationError, setValidationError] = React.useState<string | null>(
|
|
61
|
+
null
|
|
62
|
+
);
|
|
63
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
64
|
+
const selectRef = React.useRef<HTMLButtonElement>(null);
|
|
65
|
+
|
|
66
|
+
const currentValue = onChange ? value : internalValue;
|
|
67
|
+
const setValue = onChange || setInternalValue;
|
|
68
|
+
|
|
69
|
+
const validate = React.useCallback(
|
|
70
|
+
(val: CellValue): boolean => {
|
|
71
|
+
if (!column.validator) return true;
|
|
72
|
+
|
|
73
|
+
const result = column.validator(val, row);
|
|
74
|
+
if (result === true) {
|
|
75
|
+
setValidationError(null);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
setValidationError(typeof result === "string" ? result : "Invalid value");
|
|
80
|
+
return false;
|
|
81
|
+
},
|
|
82
|
+
[column, row]
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const handleChange = (newValue: CellValue) => {
|
|
86
|
+
setValue(newValue);
|
|
87
|
+
setValidationError(null);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleCommit = () => {
|
|
91
|
+
if (validate(currentValue)) {
|
|
92
|
+
onCommit(currentValue);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
97
|
+
if (e.key === "Enter") {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
e.stopPropagation();
|
|
100
|
+
handleCommit();
|
|
101
|
+
} else if (e.key === "Escape") {
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
e.stopPropagation();
|
|
104
|
+
onCancel();
|
|
105
|
+
} else if (e.key === "Tab") {
|
|
106
|
+
handleCommit();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const handleBlur = (e: React.FocusEvent) => {
|
|
111
|
+
const relatedTarget = e.relatedTarget as HTMLElement;
|
|
112
|
+
|
|
113
|
+
if (relatedTarget?.closest("[data-radix-select-viewport]")) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (relatedTarget?.closest("[data-cell-editor]")) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (relatedTarget?.closest("[data-radix-popper-content-wrapper]")) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
handleCommit();
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
React.useEffect(() => {
|
|
129
|
+
const timer = setTimeout(() => {
|
|
130
|
+
if (editorType === "select") {
|
|
131
|
+
selectRef.current?.focus();
|
|
132
|
+
} else {
|
|
133
|
+
inputRef.current?.focus();
|
|
134
|
+
inputRef.current?.select();
|
|
135
|
+
}
|
|
136
|
+
}, 0);
|
|
137
|
+
|
|
138
|
+
return () => clearTimeout(timer);
|
|
139
|
+
}, [editorType]);
|
|
140
|
+
|
|
141
|
+
const renderTextEditor = () => (
|
|
142
|
+
<Input
|
|
143
|
+
ref={inputRef}
|
|
144
|
+
type="text"
|
|
145
|
+
value={currentValue != null ? String(currentValue) : ""}
|
|
146
|
+
onChange={(e) => handleChange(e.target.value)}
|
|
147
|
+
onKeyDown={handleKeyDown}
|
|
148
|
+
onBlur={handleBlur}
|
|
149
|
+
className={cn(
|
|
150
|
+
"h-full w-full border-0 rounded-none focus:ring-2 focus:ring-primary text-sm px-2",
|
|
151
|
+
validationError && "ring-2 ring-destructive focus:ring-destructive",
|
|
152
|
+
className
|
|
153
|
+
)}
|
|
154
|
+
aria-invalid={!!validationError}
|
|
155
|
+
aria-describedby={validationError ? "cell-editor-error" : undefined}
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const renderNumberEditor = () => (
|
|
160
|
+
<Input
|
|
161
|
+
ref={inputRef}
|
|
162
|
+
type="number"
|
|
163
|
+
value={typeof currentValue === "number" ? currentValue : (currentValue != null ? String(currentValue) : "")}
|
|
164
|
+
onChange={(e) => handleChange(e.target.valueAsNumber || e.target.value)}
|
|
165
|
+
onKeyDown={handleKeyDown}
|
|
166
|
+
onBlur={handleBlur}
|
|
167
|
+
className={cn(
|
|
168
|
+
"h-full w-full border-0 rounded-none focus:ring-2 focus:ring-primary text-sm px-2",
|
|
169
|
+
validationError && "ring-2 ring-destructive focus:ring-destructive",
|
|
170
|
+
className
|
|
171
|
+
)}
|
|
172
|
+
aria-invalid={!!validationError}
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const renderDateEditor = () => {
|
|
177
|
+
const dateValue = React.useMemo(() => {
|
|
178
|
+
if (!currentValue) return undefined;
|
|
179
|
+
if (currentValue instanceof Date) return currentValue;
|
|
180
|
+
if (typeof currentValue === "string") {
|
|
181
|
+
const parsed = parseISO(currentValue);
|
|
182
|
+
return isValid(parsed) ? parsed : undefined;
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}, [currentValue]);
|
|
186
|
+
|
|
187
|
+
const handleDateChange = (date: Date | undefined) => {
|
|
188
|
+
const stringValue = date ? format(date, "yyyy-MM-dd") : "";
|
|
189
|
+
if (validate(stringValue)) {
|
|
190
|
+
onCommit(stringValue);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<div
|
|
196
|
+
className="h-full w-full"
|
|
197
|
+
onKeyDown={(e) => {
|
|
198
|
+
if (e.key === "Escape") {
|
|
199
|
+
e.preventDefault();
|
|
200
|
+
e.stopPropagation();
|
|
201
|
+
onCancel();
|
|
202
|
+
}
|
|
203
|
+
}}
|
|
204
|
+
>
|
|
205
|
+
<DatePicker
|
|
206
|
+
value={dateValue}
|
|
207
|
+
onChange={handleDateChange}
|
|
208
|
+
placeholder="Select date..."
|
|
209
|
+
className={cn(
|
|
210
|
+
"h-full w-full border-0 rounded-none [&>button]:h-full [&>button]:rounded-none [&>button]:border-0",
|
|
211
|
+
validationError && "ring-2 ring-destructive",
|
|
212
|
+
className
|
|
213
|
+
)}
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const renderSelectEditor = () => {
|
|
220
|
+
const options = column.editorOptions || [];
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<Select
|
|
224
|
+
value={String(currentValue ?? "")}
|
|
225
|
+
onValueChange={(val) => {
|
|
226
|
+
handleChange(val);
|
|
227
|
+
setTimeout(() => {
|
|
228
|
+
if (validate(val)) {
|
|
229
|
+
onCommit(val);
|
|
230
|
+
}
|
|
231
|
+
}, 0);
|
|
232
|
+
}}
|
|
233
|
+
>
|
|
234
|
+
<SelectTrigger
|
|
235
|
+
ref={selectRef}
|
|
236
|
+
className={cn(
|
|
237
|
+
"h-full w-full border-0 rounded-none focus:ring-2 focus:ring-primary text-sm",
|
|
238
|
+
validationError && "ring-2 ring-destructive focus:ring-destructive",
|
|
239
|
+
className
|
|
240
|
+
)}
|
|
241
|
+
onKeyDown={(e) => {
|
|
242
|
+
if (e.key === "Escape") {
|
|
243
|
+
e.preventDefault();
|
|
244
|
+
e.stopPropagation();
|
|
245
|
+
onCancel();
|
|
246
|
+
}
|
|
247
|
+
}}
|
|
248
|
+
>
|
|
249
|
+
<SelectValue placeholder="Select..." />
|
|
250
|
+
</SelectTrigger>
|
|
251
|
+
<SelectContent>
|
|
252
|
+
{options.map((option) => (
|
|
253
|
+
<SelectItem key={option.value} value={option.value}>
|
|
254
|
+
{option.label}
|
|
255
|
+
</SelectItem>
|
|
256
|
+
))}
|
|
257
|
+
</SelectContent>
|
|
258
|
+
</Select>
|
|
259
|
+
);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const renderBooleanEditor = () => {
|
|
263
|
+
const handleToggle = (checked: boolean | "indeterminate") => {
|
|
264
|
+
const boolValue = checked === true;
|
|
265
|
+
if (validate(boolValue)) {
|
|
266
|
+
onCommit(boolValue);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<div
|
|
272
|
+
className={cn(
|
|
273
|
+
"flex items-center justify-center h-full w-full cursor-pointer",
|
|
274
|
+
className
|
|
275
|
+
)}
|
|
276
|
+
onKeyDown={(e) => {
|
|
277
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
278
|
+
e.preventDefault();
|
|
279
|
+
e.stopPropagation();
|
|
280
|
+
handleToggle(!currentValue);
|
|
281
|
+
} else if (e.key === "Escape") {
|
|
282
|
+
e.preventDefault();
|
|
283
|
+
e.stopPropagation();
|
|
284
|
+
onCancel();
|
|
285
|
+
}
|
|
286
|
+
}}
|
|
287
|
+
onClick={(e) => {
|
|
288
|
+
e.stopPropagation();
|
|
289
|
+
handleToggle(!currentValue);
|
|
290
|
+
}}
|
|
291
|
+
tabIndex={0}
|
|
292
|
+
ref={(el) => el?.focus()}
|
|
293
|
+
role="checkbox"
|
|
294
|
+
aria-checked={!!currentValue}
|
|
295
|
+
>
|
|
296
|
+
<Checkbox
|
|
297
|
+
checked={!!currentValue}
|
|
298
|
+
onCheckedChange={handleToggle}
|
|
299
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
300
|
+
onClick={(e) => e.stopPropagation()}
|
|
301
|
+
/>
|
|
302
|
+
</div>
|
|
303
|
+
);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const renderEditor = () => {
|
|
307
|
+
switch (editorType) {
|
|
308
|
+
case "text":
|
|
309
|
+
return renderTextEditor();
|
|
310
|
+
case "number":
|
|
311
|
+
return renderNumberEditor();
|
|
312
|
+
case "date":
|
|
313
|
+
return renderDateEditor();
|
|
314
|
+
case "select":
|
|
315
|
+
return renderSelectEditor();
|
|
316
|
+
case "boolean":
|
|
317
|
+
return renderBooleanEditor();
|
|
318
|
+
default:
|
|
319
|
+
return renderTextEditor();
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<div
|
|
325
|
+
className="relative w-full h-full"
|
|
326
|
+
data-cell-editor
|
|
327
|
+
onMouseDown={(e) => {
|
|
328
|
+
if (e.target !== e.currentTarget) {
|
|
329
|
+
e.stopPropagation();
|
|
330
|
+
}
|
|
331
|
+
}}
|
|
332
|
+
>
|
|
333
|
+
{renderEditor()}
|
|
334
|
+
|
|
335
|
+
{validationError && (
|
|
336
|
+
<div
|
|
337
|
+
id="cell-editor-error"
|
|
338
|
+
className="absolute top-full left-0 mt-1 z-50 px-2 py-1 text-xs text-destructive-foreground bg-destructive rounded shadow-lg whitespace-nowrap"
|
|
339
|
+
role="alert"
|
|
340
|
+
>
|
|
341
|
+
{validationError}
|
|
342
|
+
</div>
|
|
343
|
+
)}
|
|
344
|
+
</div>
|
|
345
|
+
);
|
|
346
|
+
}
|