@object-ui/components 0.5.0 → 3.0.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/.turbo/turbo-build.log +12 -25
- package/CHANGELOG.md +32 -0
- package/dist/index.css +1 -1
- package/dist/index.js +23987 -22576
- package/dist/index.umd.cjs +30 -30
- package/dist/src/custom/action-param-dialog.d.ts +21 -0
- package/dist/src/custom/index.d.ts +4 -0
- package/dist/src/custom/navigation-overlay.d.ts +50 -0
- package/dist/src/custom/view-skeleton.d.ts +37 -0
- package/dist/src/custom/view-states.d.ts +33 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/renderers/action/action-button.d.ts +11 -0
- package/dist/src/renderers/action/action-group.d.ts +25 -0
- package/dist/src/renderers/action/action-icon.d.ts +10 -0
- package/dist/src/renderers/action/action-menu.d.ts +19 -0
- package/dist/src/renderers/action/index.d.ts +0 -0
- package/dist/src/renderers/action/resolve-icon.d.ts +6 -0
- package/package.json +20 -19
- package/src/__tests__/PageRendererRegions.test.tsx +664 -55
- package/src/__tests__/__snapshots__/snapshot-critical.test.tsx.snap +811 -0
- package/src/__tests__/__snapshots__/snapshot.test.tsx.snap +327 -0
- package/src/__tests__/accessibility.test.tsx +137 -0
- package/src/__tests__/api-consistency.test.tsx +596 -0
- package/src/__tests__/color-contrast.test.tsx +212 -0
- package/src/__tests__/compliance.test.tsx +72 -0
- package/src/__tests__/edge-cases.test.tsx +285 -0
- package/src/__tests__/navigation-overlay.test.tsx +273 -0
- package/src/__tests__/snapshot-critical.test.tsx +317 -0
- package/src/__tests__/snapshot.test.tsx +205 -0
- package/src/__tests__/view-compliance.test.tsx +153 -0
- package/src/__tests__/wcag-audit.test.tsx +493 -0
- package/src/custom/action-param-dialog.tsx +264 -0
- package/src/custom/index.ts +4 -0
- package/src/custom/navigation-overlay.tsx +296 -0
- package/src/custom/view-skeleton.tsx +243 -0
- package/src/custom/view-states.tsx +153 -0
- package/src/index.ts +1 -0
- package/src/renderers/action/action-button.tsx +147 -0
- package/src/renderers/action/action-group.tsx +270 -0
- package/src/renderers/action/action-icon.tsx +150 -0
- package/src/renderers/action/action-menu.tsx +203 -0
- package/src/renderers/action/index.ts +18 -0
- package/src/renderers/action/resolve-icon.ts +35 -0
- package/src/renderers/complex/__tests__/data-table-batch-editing.test.tsx +275 -0
- package/src/renderers/complex/__tests__/data-table-cell-renderer.test.tsx +120 -0
- package/src/renderers/complex/__tests__/data-table-editing.test.tsx +221 -0
- package/src/renderers/complex/data-table.tsx +269 -33
- package/src/renderers/complex/resizable.tsx +20 -17
- package/src/renderers/data-display/list.tsx +1 -1
- package/src/renderers/data-display/table.tsx +1 -1
- package/src/renderers/data-display/tree-view.tsx +2 -1
- package/src/renderers/form/form.tsx +33 -10
- package/src/renderers/index.ts +1 -0
- package/src/renderers/layout/aspect-ratio.tsx +1 -1
- package/src/renderers/layout/page.tsx +416 -52
- package/src/renderers/navigation/sidebar.tsx +6 -0
- package/src/renderers/placeholders.tsx +2 -2
- package/src/stories/MockedData.stories.tsx +87 -37
- package/src/stories-json/Accessibility.mdx +297 -0
- package/src/stories-json/EdgeCases.stories.tsx +160 -0
- package/src/stories-json/GettingStarted.mdx +89 -0
- package/src/stories-json/Introduction.mdx +127 -0
- package/src/stories-json/accordion.stories.tsx +1 -1
- package/src/stories-json/aggrid.stories.tsx +1 -1
- package/src/stories-json/alert.stories.tsx +1 -1
- package/src/stories-json/aspect-ratio.stories.tsx +1 -1
- package/src/stories-json/avatar.stories.tsx +1 -1
- package/src/stories-json/badge.stories.tsx +1 -1
- package/src/stories-json/breadcrumb.stories.tsx +1 -1
- package/src/stories-json/button-group.stories.tsx +1 -1
- package/src/stories-json/button.stories.tsx +1 -1
- package/src/stories-json/calendar.stories.tsx +1 -1
- package/src/stories-json/card.stories.tsx +1 -1
- package/src/stories-json/carousel.stories.tsx +1 -1
- package/src/stories-json/charts.stories.tsx +1 -1
- package/src/stories-json/chatbot.stories.tsx +1 -1
- package/src/stories-json/code-editor.stories.tsx +1 -1
- package/src/stories-json/collapsible.stories.tsx +1 -1
- package/src/stories-json/controls.stories.tsx +1 -1
- package/src/stories-json/crm-live-data.stories.tsx +154 -0
- package/src/stories-json/data-table.stories.tsx +80 -4
- package/src/stories-json/data_display_extras.stories.tsx +1 -1
- package/src/stories-json/date-picker.stories.tsx +1 -1
- package/src/stories-json/detail-view.stories.tsx +1 -1
- package/src/stories-json/dialog.stories.tsx +1 -1
- package/src/stories-json/feedback_extras.stories.tsx +1 -1
- package/src/stories-json/feedback_others.stories.tsx +1 -1
- package/src/stories-json/form-variants.stories.tsx +210 -0
- package/src/stories-json/form_advanced.stories.tsx +1 -1
- package/src/stories-json/form_extras.stories.tsx +1 -1
- package/src/stories-json/grid.stories.tsx +1 -1
- package/src/stories-json/icon.stories.tsx +1 -1
- package/src/stories-json/input.stories.tsx +1 -1
- package/src/stories-json/kanban.stories.tsx +1 -1
- package/src/stories-json/layout_extended.stories.tsx +1 -1
- package/src/stories-json/layout_flex.stories.tsx +1 -1
- package/src/stories-json/list-view.stories.tsx +1 -1
- package/src/stories-json/markdown.stories.tsx +1 -1
- package/src/stories-json/menus.stories.tsx +1 -1
- package/src/stories-json/metric-card.stories.tsx +1 -1
- package/src/stories-json/navigation-menu.stories.tsx +1 -1
- package/src/stories-json/object-aggrid-advanced.stories.tsx +389 -0
- package/src/stories-json/object-aggrid.stories.tsx +1 -1
- package/src/stories-json/object-form.stories.tsx +1 -1
- package/src/stories-json/object-gantt.stories.tsx +1 -1
- package/src/stories-json/object-grid.stories.tsx +159 -1
- package/src/stories-json/object-map.stories.tsx +1 -1
- package/src/stories-json/object-view.stories.tsx +1 -1
- package/src/stories-json/overlay_extras.stories.tsx +1 -1
- package/src/stories-json/overlay_others.stories.tsx +1 -1
- package/src/stories-json/resizable.stories.tsx +1 -1
- package/src/stories-json/select.stories.tsx +1 -1
- package/src/stories-json/separator.stories.tsx +1 -1
- package/src/stories-json/statistic.stories.tsx +1 -1
- package/src/stories-json/tabs.stories.tsx +1 -1
- package/src/stories-json/timeline.stories.tsx +1 -1
- package/src/stories-json/typography.stories.tsx +1 -1
- package/src/ui/slider.tsx +6 -2
- package/src/stories/Introduction.mdx +0 -34
|
@@ -44,6 +44,8 @@ import {
|
|
|
44
44
|
ChevronsLeft,
|
|
45
45
|
ChevronsRight,
|
|
46
46
|
GripVertical,
|
|
47
|
+
Save,
|
|
48
|
+
X,
|
|
47
49
|
} from 'lucide-react';
|
|
48
50
|
|
|
49
51
|
type SortDirection = 'asc' | 'desc' | null;
|
|
@@ -97,6 +99,8 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
|
|
|
97
99
|
rowActions = false,
|
|
98
100
|
resizableColumns = true,
|
|
99
101
|
reorderableColumns = true,
|
|
102
|
+
editable = false,
|
|
103
|
+
rowClassName,
|
|
100
104
|
className,
|
|
101
105
|
} = schema;
|
|
102
106
|
|
|
@@ -120,11 +124,17 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
|
|
|
120
124
|
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
|
|
121
125
|
const [draggedColumn, setDraggedColumn] = useState<number | null>(null);
|
|
122
126
|
const [dragOverColumn, setDragOverColumn] = useState<number | null>(null);
|
|
127
|
+
const [editingCell, setEditingCell] = useState<{ rowIndex: number; columnKey: string } | null>(null);
|
|
128
|
+
const [editValue, setEditValue] = useState<any>('');
|
|
129
|
+
// Track pending changes for multi-cell editing: rowIndex -> { columnKey -> newValue }
|
|
130
|
+
const [pendingChanges, setPendingChanges] = useState<Map<number, Record<string, any>>>(new Map());
|
|
131
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
123
132
|
|
|
124
133
|
// Refs for column resizing
|
|
125
134
|
const resizingColumn = useRef<string | null>(null);
|
|
126
135
|
const startX = useRef<number>(0);
|
|
127
136
|
const startWidth = useRef<number>(0);
|
|
137
|
+
const editInputRef = useRef<HTMLInputElement>(null);
|
|
128
138
|
|
|
129
139
|
// Update columns when schema changes
|
|
130
140
|
useEffect(() => {
|
|
@@ -340,6 +350,141 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
|
|
|
340
350
|
setDragOverColumn(null);
|
|
341
351
|
};
|
|
342
352
|
|
|
353
|
+
// Cell editing handlers
|
|
354
|
+
const startEdit = (rowIndex: number, columnKey: string) => {
|
|
355
|
+
if (!editable) return;
|
|
356
|
+
|
|
357
|
+
const column = columns.find(col => col.accessorKey === columnKey);
|
|
358
|
+
if (column?.editable === false) return;
|
|
359
|
+
|
|
360
|
+
setEditingCell({ rowIndex, columnKey });
|
|
361
|
+
|
|
362
|
+
// Check if there's a pending change for this cell, otherwise use current data value
|
|
363
|
+
const rowChanges = pendingChanges.get(rowIndex);
|
|
364
|
+
const currentValue = paginatedData[rowIndex][columnKey];
|
|
365
|
+
const valueToEdit = rowChanges?.[columnKey] ?? currentValue ?? '';
|
|
366
|
+
setEditValue(valueToEdit);
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const saveEdit = (force: boolean = false) => {
|
|
370
|
+
if (!editingCell) return;
|
|
371
|
+
|
|
372
|
+
// Don't save if we're in cancelled state (unless forced)
|
|
373
|
+
if (!force && editingCell === null) return;
|
|
374
|
+
|
|
375
|
+
const { rowIndex, columnKey } = editingCell;
|
|
376
|
+
const globalIndex = (currentPage - 1) * pageSize + rowIndex;
|
|
377
|
+
const row = sortedData[globalIndex];
|
|
378
|
+
|
|
379
|
+
// Update pending changes
|
|
380
|
+
const newPendingChanges = new Map(pendingChanges);
|
|
381
|
+
const rowChanges = newPendingChanges.get(rowIndex) || {};
|
|
382
|
+
rowChanges[columnKey] = editValue;
|
|
383
|
+
newPendingChanges.set(rowIndex, rowChanges);
|
|
384
|
+
setPendingChanges(newPendingChanges);
|
|
385
|
+
|
|
386
|
+
// Call the legacy onCellChange callback if provided
|
|
387
|
+
if (schema.onCellChange) {
|
|
388
|
+
schema.onCellChange(globalIndex, columnKey, editValue, row);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
setEditingCell(null);
|
|
392
|
+
setEditValue('');
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const cancelEdit = () => {
|
|
396
|
+
setEditingCell(null);
|
|
397
|
+
setEditValue('');
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const saveRow = async (rowIndex: number) => {
|
|
401
|
+
const globalIndex = (currentPage - 1) * pageSize + rowIndex;
|
|
402
|
+
const row = sortedData[globalIndex];
|
|
403
|
+
const rowChanges = pendingChanges.get(rowIndex);
|
|
404
|
+
|
|
405
|
+
if (!rowChanges || Object.keys(rowChanges).length === 0) return;
|
|
406
|
+
|
|
407
|
+
setIsSaving(true);
|
|
408
|
+
try {
|
|
409
|
+
if (schema.onRowSave) {
|
|
410
|
+
await schema.onRowSave(globalIndex, rowChanges, row);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Clear pending changes for this row
|
|
414
|
+
const newPendingChanges = new Map(pendingChanges);
|
|
415
|
+
newPendingChanges.delete(rowIndex);
|
|
416
|
+
setPendingChanges(newPendingChanges);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
console.error('Failed to save row:', error);
|
|
419
|
+
} finally {
|
|
420
|
+
setIsSaving(false);
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const cancelRowChanges = (rowIndex: number) => {
|
|
425
|
+
const newPendingChanges = new Map(pendingChanges);
|
|
426
|
+
newPendingChanges.delete(rowIndex);
|
|
427
|
+
setPendingChanges(newPendingChanges);
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const saveBatch = async () => {
|
|
431
|
+
if (pendingChanges.size === 0) return;
|
|
432
|
+
|
|
433
|
+
setIsSaving(true);
|
|
434
|
+
try {
|
|
435
|
+
const changesToSave = Array.from(pendingChanges.entries()).map(([rowIndex, changes]) => {
|
|
436
|
+
const globalIndex = (currentPage - 1) * pageSize + rowIndex;
|
|
437
|
+
const row = sortedData[globalIndex];
|
|
438
|
+
return { rowIndex: globalIndex, changes, row };
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
if (schema.onBatchSave) {
|
|
442
|
+
await schema.onBatchSave(changesToSave);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Clear all pending changes
|
|
446
|
+
setPendingChanges(new Map());
|
|
447
|
+
} catch (error) {
|
|
448
|
+
console.error('Failed to save batch:', error);
|
|
449
|
+
} finally {
|
|
450
|
+
setIsSaving(false);
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const cancelAllChanges = () => {
|
|
455
|
+
setPendingChanges(new Map());
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const handleCellKeyDown = (e: React.KeyboardEvent, rowIndex: number, columnKey: string) => {
|
|
459
|
+
if (!editable) return;
|
|
460
|
+
|
|
461
|
+
const column = columns.find(col => col.accessorKey === columnKey);
|
|
462
|
+
if (column?.editable === false) return;
|
|
463
|
+
|
|
464
|
+
if (e.key === 'Enter' && !editingCell) {
|
|
465
|
+
e.preventDefault();
|
|
466
|
+
startEdit(rowIndex, columnKey);
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const handleEditKeyDown = (e: React.KeyboardEvent) => {
|
|
471
|
+
if (e.key === 'Enter') {
|
|
472
|
+
e.preventDefault();
|
|
473
|
+
saveEdit(true);
|
|
474
|
+
} else if (e.key === 'Escape') {
|
|
475
|
+
e.preventDefault();
|
|
476
|
+
cancelEdit();
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
// Auto-focus on edit input when entering edit mode
|
|
481
|
+
useEffect(() => {
|
|
482
|
+
if (editingCell && editInputRef.current) {
|
|
483
|
+
editInputRef.current.focus();
|
|
484
|
+
editInputRef.current.select();
|
|
485
|
+
}
|
|
486
|
+
}, [editingCell]);
|
|
487
|
+
|
|
343
488
|
// Cleanup on unmount
|
|
344
489
|
useEffect(() => {
|
|
345
490
|
return () => {
|
|
@@ -361,16 +506,17 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
|
|
|
361
506
|
return selectedRowIds.has(rowId);
|
|
362
507
|
}) && !allPageRowsSelected;
|
|
363
508
|
|
|
364
|
-
const
|
|
509
|
+
const hasPendingChanges = pendingChanges.size > 0;
|
|
510
|
+
const showToolbar = searchable || exportable || (selectable && selectedRowIds.size > 0) || hasPendingChanges;
|
|
365
511
|
|
|
366
512
|
return (
|
|
367
|
-
<div className={`flex flex-col h-full gap-4 ${className || ''}`}>
|
|
513
|
+
<div className={`flex flex-col h-full gap-2 sm:gap-4 ${className || ''}`}>
|
|
368
514
|
{/* Toolbar */}
|
|
369
515
|
{showToolbar && (
|
|
370
|
-
<div className="flex items-center justify-between gap-4 flex-none">
|
|
516
|
+
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-2 sm:gap-4 flex-none">
|
|
371
517
|
<div className="flex items-center gap-2 flex-1">
|
|
372
518
|
{searchable && (
|
|
373
|
-
<div className="relative max-w-sm flex-1">
|
|
519
|
+
<div className="relative w-full sm:max-w-sm flex-1">
|
|
374
520
|
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
375
521
|
<Input
|
|
376
522
|
placeholder="Search..."
|
|
@@ -385,7 +531,33 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
|
|
|
385
531
|
)}
|
|
386
532
|
</div>
|
|
387
533
|
|
|
388
|
-
<div className="flex items-center gap-2">
|
|
534
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
535
|
+
{hasPendingChanges && (
|
|
536
|
+
<>
|
|
537
|
+
<div className="text-sm text-muted-foreground">
|
|
538
|
+
{pendingChanges.size} row{pendingChanges.size > 1 ? 's' : ''} modified
|
|
539
|
+
</div>
|
|
540
|
+
<Button
|
|
541
|
+
variant="outline"
|
|
542
|
+
size="sm"
|
|
543
|
+
onClick={cancelAllChanges}
|
|
544
|
+
disabled={isSaving}
|
|
545
|
+
>
|
|
546
|
+
<X className="h-4 w-4 mr-2" />
|
|
547
|
+
Cancel All
|
|
548
|
+
</Button>
|
|
549
|
+
<Button
|
|
550
|
+
variant="default"
|
|
551
|
+
size="sm"
|
|
552
|
+
onClick={saveBatch}
|
|
553
|
+
disabled={isSaving}
|
|
554
|
+
>
|
|
555
|
+
<Save className="h-4 w-4 mr-2" />
|
|
556
|
+
Save All ({pendingChanges.size})
|
|
557
|
+
</Button>
|
|
558
|
+
</>
|
|
559
|
+
)}
|
|
560
|
+
|
|
389
561
|
{exportable && (
|
|
390
562
|
<Button
|
|
391
563
|
variant="outline"
|
|
@@ -407,8 +579,8 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
|
|
|
407
579
|
</div>
|
|
408
580
|
)}
|
|
409
581
|
|
|
410
|
-
{/* Table */}
|
|
411
|
-
<div className="rounded-md border flex-1 min-h-0 overflow-auto relative bg-background">
|
|
582
|
+
{/* Table - horizontal scroll indicator via inset shadow on mobile */}
|
|
583
|
+
<div className="rounded-md border flex-1 min-h-0 overflow-auto relative bg-background [-webkit-overflow-scrolling:touch] shadow-[inset_-8px_0_8px_-8px_rgba(0,0,0,0.08)]">
|
|
412
584
|
<Table>
|
|
413
585
|
{caption && <TableCaption>{caption}</TableCaption>}
|
|
414
586
|
<TableHeader className="sticky top-0 bg-background z-10 shadow-sm">
|
|
@@ -429,7 +601,15 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
|
|
|
429
601
|
return (
|
|
430
602
|
<TableHead
|
|
431
603
|
key={col.accessorKey}
|
|
432
|
-
className={
|
|
604
|
+
className={cn(
|
|
605
|
+
col.className,
|
|
606
|
+
sortable && col.sortable !== false && 'cursor-pointer select-none',
|
|
607
|
+
isDragging && 'opacity-50',
|
|
608
|
+
isDragOver && 'border-l-2 border-primary',
|
|
609
|
+
col.align === 'right' && 'text-right',
|
|
610
|
+
col.align === 'center' && 'text-center',
|
|
611
|
+
'relative group bg-background'
|
|
612
|
+
)}
|
|
433
613
|
style={{
|
|
434
614
|
width: columnWidth,
|
|
435
615
|
minWidth: columnWidth
|
|
@@ -441,7 +621,10 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
|
|
|
441
621
|
onDragEnd={handleColumnDragEnd}
|
|
442
622
|
onClick={() => sortable && col.sortable !== false && handleSort(col.accessorKey)}
|
|
443
623
|
>
|
|
444
|
-
<div className=
|
|
624
|
+
<div className={cn(
|
|
625
|
+
"flex items-center",
|
|
626
|
+
col.align === 'right' ? 'justify-end' : 'justify-between'
|
|
627
|
+
)}>
|
|
445
628
|
<div className="flex items-center gap-1">
|
|
446
629
|
{reorderableColumns && (
|
|
447
630
|
<GripVertical className="h-4 w-4 opacity-0 group-hover:opacity-50 cursor-grab active:cursor-grabbing flex-shrink-0" />
|
|
@@ -485,24 +668,25 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
|
|
|
485
668
|
const globalIndex = (currentPage - 1) * pageSize + rowIndex;
|
|
486
669
|
const rowId = getRowId(row, globalIndex);
|
|
487
670
|
const isSelected = selectedRowIds.has(rowId);
|
|
671
|
+
const rowHasChanges = pendingChanges.has(rowIndex);
|
|
672
|
+
const rowChanges = pendingChanges.get(rowIndex) || {};
|
|
488
673
|
|
|
489
674
|
return (
|
|
490
675
|
<TableRow
|
|
491
676
|
key={rowId}
|
|
492
677
|
data-state={isSelected ? 'selected' : undefined}
|
|
493
678
|
className={cn(
|
|
494
|
-
|
|
495
|
-
|
|
679
|
+
schema.onRowClick && "cursor-pointer",
|
|
680
|
+
rowHasChanges && "bg-amber-50 dark:bg-amber-950/20",
|
|
681
|
+
rowClassName && rowClassName(row, rowIndex)
|
|
496
682
|
)}
|
|
497
683
|
onClick={(e) => {
|
|
498
|
-
// @ts-expect-error - onRowClick might not be in schema type definition
|
|
499
684
|
if (schema.onRowClick && !e.defaultPrevented) {
|
|
500
685
|
// Simple heuristic to avoid triggering on interactive elements if they didn't stop propagation
|
|
501
686
|
const target = e.target as HTMLElement;
|
|
502
687
|
if (target.closest('button') || target.closest('[role="checkbox"]') || target.closest('a')) {
|
|
503
688
|
return;
|
|
504
689
|
}
|
|
505
|
-
// @ts-expect-error - onRowClick might not be in schema type definition
|
|
506
690
|
schema.onRowClick(row);
|
|
507
691
|
}
|
|
508
692
|
}}
|
|
@@ -517,37 +701,89 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
|
|
|
517
701
|
)}
|
|
518
702
|
{columns.map((col, colIndex) => {
|
|
519
703
|
const columnWidth = columnWidths[col.accessorKey] || col.width;
|
|
704
|
+
const originalValue = row[col.accessorKey];
|
|
705
|
+
const hasPendingChange = rowChanges[col.accessorKey] !== undefined;
|
|
706
|
+
const cellValue = hasPendingChange ? rowChanges[col.accessorKey] : originalValue;
|
|
707
|
+
const isEditing = editingCell?.rowIndex === rowIndex && editingCell?.columnKey === col.accessorKey;
|
|
708
|
+
const isEditable = editable && col.editable !== false;
|
|
709
|
+
|
|
520
710
|
return (
|
|
521
711
|
<TableCell
|
|
522
712
|
key={colIndex}
|
|
523
|
-
className={
|
|
713
|
+
className={cn(
|
|
714
|
+
col.cellClassName,
|
|
715
|
+
col.align === 'right' && 'text-right',
|
|
716
|
+
col.align === 'center' && 'text-center',
|
|
717
|
+
isEditable && !isEditing && "cursor-text hover:bg-muted/50",
|
|
718
|
+
hasPendingChange && "font-semibold text-amber-700 dark:text-amber-400"
|
|
719
|
+
)}
|
|
524
720
|
style={{
|
|
525
721
|
width: columnWidth,
|
|
526
722
|
minWidth: columnWidth,
|
|
527
723
|
maxWidth: columnWidth
|
|
528
724
|
}}
|
|
725
|
+
onDoubleClick={() => isEditable && startEdit(rowIndex, col.accessorKey)}
|
|
726
|
+
onKeyDown={(e) => handleCellKeyDown(e, rowIndex, col.accessorKey)}
|
|
727
|
+
tabIndex={0}
|
|
529
728
|
>
|
|
530
|
-
{
|
|
729
|
+
{isEditing ? (
|
|
730
|
+
<Input
|
|
731
|
+
ref={editInputRef}
|
|
732
|
+
value={editValue}
|
|
733
|
+
onChange={(e) => setEditValue(e.target.value)}
|
|
734
|
+
onKeyDown={handleEditKeyDown}
|
|
735
|
+
className="h-8 px-2 py-1"
|
|
736
|
+
/>
|
|
737
|
+
) : typeof col.cell === 'function' ? (
|
|
738
|
+
col.cell(cellValue, row)
|
|
739
|
+
) : (
|
|
740
|
+
cellValue
|
|
741
|
+
)}
|
|
531
742
|
</TableCell>
|
|
532
743
|
);
|
|
533
744
|
})}
|
|
534
745
|
{rowActions && (
|
|
535
746
|
<TableCell className="text-right">
|
|
536
747
|
<div className="flex items-center justify-end gap-1">
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
748
|
+
{rowHasChanges && (schema.onRowSave || schema.onBatchSave) ? (
|
|
749
|
+
<>
|
|
750
|
+
<Button
|
|
751
|
+
variant="ghost"
|
|
752
|
+
size="icon-sm"
|
|
753
|
+
onClick={() => cancelRowChanges(rowIndex)}
|
|
754
|
+
disabled={isSaving}
|
|
755
|
+
title="Cancel changes"
|
|
756
|
+
>
|
|
757
|
+
<X className="h-4 w-4" />
|
|
758
|
+
</Button>
|
|
759
|
+
<Button
|
|
760
|
+
variant="ghost"
|
|
761
|
+
size="icon-sm"
|
|
762
|
+
onClick={() => saveRow(rowIndex)}
|
|
763
|
+
disabled={isSaving}
|
|
764
|
+
title="Save row"
|
|
765
|
+
>
|
|
766
|
+
<Save className="h-4 w-4 text-green-600" />
|
|
767
|
+
</Button>
|
|
768
|
+
</>
|
|
769
|
+
) : (
|
|
770
|
+
<>
|
|
771
|
+
<Button
|
|
772
|
+
variant="ghost"
|
|
773
|
+
size="icon-sm"
|
|
774
|
+
onClick={() => schema.onRowEdit?.(row)}
|
|
775
|
+
>
|
|
776
|
+
<Edit className="h-4 w-4" />
|
|
777
|
+
</Button>
|
|
778
|
+
<Button
|
|
779
|
+
variant="ghost"
|
|
780
|
+
size="icon-sm"
|
|
781
|
+
onClick={() => schema.onRowDelete?.(row)}
|
|
782
|
+
>
|
|
783
|
+
<Trash2 className="h-4 w-4 text-destructive" />
|
|
784
|
+
</Button>
|
|
785
|
+
</>
|
|
786
|
+
)}
|
|
551
787
|
</div>
|
|
552
788
|
</TableCell>
|
|
553
789
|
)}
|
|
@@ -568,9 +804,9 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
|
|
|
568
804
|
|
|
569
805
|
{/* Pagination */}
|
|
570
806
|
{pagination && sortedData.length > 0 && (
|
|
571
|
-
<div className="flex items-center justify-between">
|
|
807
|
+
<div className="flex flex-col sm:flex-row items-center justify-between gap-2">
|
|
572
808
|
<div className="flex items-center gap-2">
|
|
573
|
-
<span className="text-sm text-muted-foreground">Rows per page:</span>
|
|
809
|
+
<span className="text-xs sm:text-sm text-muted-foreground">Rows per page:</span>
|
|
574
810
|
<Select
|
|
575
811
|
value={pageSize.toString()}
|
|
576
812
|
onValueChange={(value) => {
|
|
@@ -592,8 +828,8 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
|
|
|
592
828
|
</div>
|
|
593
829
|
|
|
594
830
|
<div className="flex items-center gap-2">
|
|
595
|
-
<span className="text-sm text-muted-foreground">
|
|
596
|
-
Page {currentPage} of {totalPages} ({sortedData.length} total)
|
|
831
|
+
<span className="text-xs sm:text-sm text-muted-foreground">
|
|
832
|
+
Page {currentPage} of {totalPages} <span className="hidden sm:inline">({sortedData.length} total)</span>
|
|
597
833
|
</span>
|
|
598
834
|
<div className="flex items-center gap-1">
|
|
599
835
|
<Button
|
|
@@ -17,23 +17,26 @@ import {
|
|
|
17
17
|
import { renderChildren } from '../../lib/utils';
|
|
18
18
|
|
|
19
19
|
ComponentRegistry.register('resizable',
|
|
20
|
-
({ schema, className, ...props }: { schema: ResizableSchema; className?: string; [key: string]: any }) =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
20
|
+
({ schema, className, ...props }: { schema: ResizableSchema; className?: string; [key: string]: any }) => {
|
|
21
|
+
const panels = Array.isArray(schema.panels) ? schema.panels : [];
|
|
22
|
+
return (
|
|
23
|
+
<ResizablePanelGroup
|
|
24
|
+
orientation={(schema.direction || 'horizontal') as "horizontal" | "vertical"}
|
|
25
|
+
className={className}
|
|
26
|
+
{...props}
|
|
27
|
+
style={{ minHeight: schema.minHeight || '200px' }}
|
|
28
|
+
>
|
|
29
|
+
{panels.map((panel: any, index: number) => (
|
|
30
|
+
<React.Fragment key={index}>
|
|
31
|
+
<ResizablePanel defaultSize={panel.defaultSize} minSize={panel.minSize} maxSize={panel.maxSize}>
|
|
32
|
+
{renderChildren(panel.content)}
|
|
33
|
+
</ResizablePanel>
|
|
34
|
+
{index < panels.length - 1 && <ResizableHandle withHandle={schema.withHandle} />}
|
|
35
|
+
</React.Fragment>
|
|
36
|
+
))}
|
|
37
|
+
</ResizablePanelGroup>
|
|
38
|
+
);
|
|
39
|
+
},
|
|
37
40
|
{
|
|
38
41
|
namespace: 'ui',
|
|
39
42
|
label: 'Resizable Panel Group',
|
|
@@ -15,7 +15,7 @@ ComponentRegistry.register('list',
|
|
|
15
15
|
({ schema, className, ...props }: { schema: ListSchema; className?: string; [key: string]: any }) => {
|
|
16
16
|
// Support data binding
|
|
17
17
|
const boundData = useDataScope(schema.bind);
|
|
18
|
-
const items = boundData
|
|
18
|
+
const items = Array.isArray(boundData) ? boundData : Array.isArray(schema.items) ? schema.items : [];
|
|
19
19
|
|
|
20
20
|
// We use 'ol' or 'ul' based on ordered prop
|
|
21
21
|
const ListTag = schema.ordered ? 'ol' : 'ul';
|
|
@@ -23,7 +23,7 @@ export const SimpleTableRenderer = ({ schema, className }: any) => {
|
|
|
23
23
|
// Try to get data from binding first, then fall back to inline data
|
|
24
24
|
const boundData = useDataScope(schema.bind);
|
|
25
25
|
const data = boundData || schema.data || schema.props?.data || [];
|
|
26
|
-
const columns = schema.columns
|
|
26
|
+
const columns = Array.isArray(schema.columns) ? schema.columns : Array.isArray(schema.props?.columns) ? schema.props.columns : [];
|
|
27
27
|
|
|
28
28
|
// If we have data but it's not an array, show error.
|
|
29
29
|
// If data is undefined, we might just be loading or empty.
|
|
@@ -102,7 +102,8 @@ ComponentRegistry.register('tree-view',
|
|
|
102
102
|
|
|
103
103
|
// Support data binding
|
|
104
104
|
const boundData = useDataScope(schema.bind);
|
|
105
|
-
const
|
|
105
|
+
const rawNodes = boundData || schema.nodes || schema.data || [];
|
|
106
|
+
const nodes = Array.isArray(rawNodes) ? rawNodes : [];
|
|
106
107
|
|
|
107
108
|
return (
|
|
108
109
|
<div className={cn(
|
|
@@ -228,9 +228,17 @@ ComponentRegistry.register('form',
|
|
|
228
228
|
disabled: fieldDisabled = false,
|
|
229
229
|
validation = {},
|
|
230
230
|
condition,
|
|
231
|
+
colSpan,
|
|
232
|
+
hidden,
|
|
233
|
+
widget,
|
|
234
|
+
visibleOn,
|
|
235
|
+
readonly,
|
|
231
236
|
...fieldProps
|
|
232
237
|
} = field;
|
|
233
238
|
|
|
239
|
+
// Skip hidden fields
|
|
240
|
+
if (hidden) return null;
|
|
241
|
+
|
|
234
242
|
// Handle conditional rendering with null/undefined safety
|
|
235
243
|
if (condition) {
|
|
236
244
|
const watchField = condition.field;
|
|
@@ -264,6 +272,17 @@ ComponentRegistry.register('form',
|
|
|
264
272
|
// Use field.id or field.name for stable keys (never use index alone)
|
|
265
273
|
const fieldKey = field.id ?? name;
|
|
266
274
|
|
|
275
|
+
// Resolve the component type: prefer widget override, fallback to field type
|
|
276
|
+
const resolvedType = widget || type;
|
|
277
|
+
|
|
278
|
+
// colSpan classes for grid layout
|
|
279
|
+
const colSpanClass = colSpan && colSpan > 1
|
|
280
|
+
? colSpan === 2 ? 'col-span-2'
|
|
281
|
+
: colSpan === 3 ? 'col-span-3'
|
|
282
|
+
: colSpan >= 4 ? 'col-span-4'
|
|
283
|
+
: ''
|
|
284
|
+
: '';
|
|
285
|
+
|
|
267
286
|
return (
|
|
268
287
|
<FormField
|
|
269
288
|
key={fieldKey}
|
|
@@ -271,9 +290,9 @@ ComponentRegistry.register('form',
|
|
|
271
290
|
name={name}
|
|
272
291
|
rules={rules}
|
|
273
292
|
render={({ field: formField }) => (
|
|
274
|
-
<FormItem>
|
|
293
|
+
<FormItem className={colSpanClass || undefined}>
|
|
275
294
|
{label && (
|
|
276
|
-
<FormLabel>
|
|
295
|
+
<FormLabel className="text-xs sm:text-sm">
|
|
277
296
|
{label}
|
|
278
297
|
{required && (
|
|
279
298
|
<span className="text-destructive ml-1" aria-label="required">
|
|
@@ -283,8 +302,8 @@ ComponentRegistry.register('form',
|
|
|
283
302
|
</FormLabel>
|
|
284
303
|
)}
|
|
285
304
|
<FormControl>
|
|
286
|
-
{/* Render the actual field component based on type */}
|
|
287
|
-
{renderFieldComponent(
|
|
305
|
+
{/* Render the actual field component based on resolved type */}
|
|
306
|
+
{renderFieldComponent(resolvedType, {
|
|
288
307
|
...fieldProps,
|
|
289
308
|
// specialized fields needs raw metadata, but we should traverse down if it exists
|
|
290
309
|
// field is the field configuration loop variable
|
|
@@ -293,7 +312,7 @@ ComponentRegistry.register('form',
|
|
|
293
312
|
inputType: fieldProps.inputType,
|
|
294
313
|
options: fieldProps.options,
|
|
295
314
|
placeholder: fieldProps.placeholder,
|
|
296
|
-
disabled: disabled || fieldDisabled || isSubmitting,
|
|
315
|
+
disabled: disabled || fieldDisabled || readonly || isSubmitting,
|
|
297
316
|
})}
|
|
298
317
|
</FormControl>
|
|
299
318
|
{description && (
|
|
@@ -310,13 +329,14 @@ ComponentRegistry.register('form',
|
|
|
310
329
|
|
|
311
330
|
{/* Form Actions */}
|
|
312
331
|
{(schema.showActions !== false) && (
|
|
313
|
-
<div className={`flex gap-2 ${layout === 'horizontal' ? 'justify-end' : 'justify-start'} mt-6`}>
|
|
332
|
+
<div className={`flex flex-col sm:flex-row gap-2 ${layout === 'horizontal' ? 'sm:justify-end' : 'sm:justify-start'} mt-6`}>
|
|
314
333
|
{showCancel && (
|
|
315
334
|
<Button
|
|
316
335
|
type="button"
|
|
317
336
|
variant="outline"
|
|
318
337
|
onClick={handleCancel}
|
|
319
338
|
disabled={isSubmitting || disabled}
|
|
339
|
+
className="w-full sm:w-auto"
|
|
320
340
|
>
|
|
321
341
|
{cancelLabel}
|
|
322
342
|
</Button>
|
|
@@ -325,6 +345,7 @@ ComponentRegistry.register('form',
|
|
|
325
345
|
<Button
|
|
326
346
|
type="submit"
|
|
327
347
|
disabled={isSubmitting || disabled}
|
|
348
|
+
className="w-full sm:w-auto"
|
|
328
349
|
>
|
|
329
350
|
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
330
351
|
{submitLabel}
|
|
@@ -441,12 +462,12 @@ function renderFieldComponent(type: string, props: RenderFieldProps) {
|
|
|
441
462
|
if (inputType === 'file') {
|
|
442
463
|
// File inputs cannot be controlled with value prop
|
|
443
464
|
const { value, ...fileProps } = fieldProps;
|
|
444
|
-
return <Input type="file" placeholder={placeholder} {...fileProps} />;
|
|
465
|
+
return <Input type="file" placeholder={placeholder} className="min-h-[44px] sm:min-h-0" {...fileProps} />;
|
|
445
466
|
}
|
|
446
|
-
return <Input type={inputType || 'text'} placeholder={placeholder} {...fieldProps} value={fieldProps.value ?? ''} />;
|
|
467
|
+
return <Input type={inputType || 'text'} placeholder={placeholder} className="min-h-[44px] sm:min-h-0" {...fieldProps} value={fieldProps.value ?? ''} />;
|
|
447
468
|
|
|
448
469
|
case 'textarea':
|
|
449
|
-
return <Textarea placeholder={placeholder} {...fieldProps} value={fieldProps.value ?? ''} />;
|
|
470
|
+
return <Textarea placeholder={placeholder} className="min-h-[44px] sm:min-h-0" {...fieldProps} value={fieldProps.value ?? ''} />;
|
|
450
471
|
|
|
451
472
|
case 'checkbox': {
|
|
452
473
|
// For checkbox, we need to handle the value differently
|
|
@@ -455,6 +476,7 @@ function renderFieldComponent(type: string, props: RenderFieldProps) {
|
|
|
455
476
|
<Checkbox
|
|
456
477
|
checked={value}
|
|
457
478
|
onCheckedChange={onChange}
|
|
479
|
+
className="min-h-[44px] min-w-[44px] sm:min-h-0 sm:min-w-0"
|
|
458
480
|
{...checkboxProps}
|
|
459
481
|
/>
|
|
460
482
|
);
|
|
@@ -467,6 +489,7 @@ function renderFieldComponent(type: string, props: RenderFieldProps) {
|
|
|
467
489
|
<Switch
|
|
468
490
|
checked={value}
|
|
469
491
|
onCheckedChange={onChange}
|
|
492
|
+
className="min-h-[44px] min-w-[44px] sm:min-h-0 sm:min-w-0"
|
|
470
493
|
{...switchProps}
|
|
471
494
|
/>
|
|
472
495
|
);
|
|
@@ -483,7 +506,7 @@ function renderFieldComponent(type: string, props: RenderFieldProps) {
|
|
|
483
506
|
|
|
484
507
|
return (
|
|
485
508
|
<Select value={selectValue} onValueChange={selectOnChange} {...selectProps}>
|
|
486
|
-
<SelectTrigger>
|
|
509
|
+
<SelectTrigger className="min-h-[44px] sm:min-h-0">
|
|
487
510
|
<SelectValue placeholder={placeholder ?? 'Select an option'} />
|
|
488
511
|
</SelectTrigger>
|
|
489
512
|
<SelectContent>
|
package/src/renderers/index.ts
CHANGED
|
@@ -28,7 +28,7 @@ ComponentRegistry.register('aspect-ratio',
|
|
|
28
28
|
{...{ 'data-obj-id': dataObjId, 'data-obj-type': dataObjType, style }}
|
|
29
29
|
>
|
|
30
30
|
{schema.image ? (
|
|
31
|
-
<img src={schema.image} alt={schema.alt || ''} className="rounded-md object-cover w-full h-full" />
|
|
31
|
+
<img src={schema.image} alt={schema.alt || ''} loading="lazy" className="rounded-md object-cover w-full h-full" />
|
|
32
32
|
) : (
|
|
33
33
|
renderChildren(schema.children || schema.body)
|
|
34
34
|
)}
|