hazo_ui 6.16.0 → 6.18.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/CHANGE_LOG.md +74 -0
- package/README.md +13 -0
- package/dist/index.cjs +506 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -2
- package/dist/index.d.ts +33 -2
- package/dist/index.js +509 -5
- package/dist/index.js.map +1 -1
- package/dist/index.locale-picker.cjs +5 -1
- package/dist/index.locale-picker.cjs.map +1 -1
- package/dist/index.locale-picker.js +5 -1
- package/dist/index.locale-picker.js.map +1 -1
- package/package.json +3 -3
package/CHANGE_LOG.md
CHANGED
|
@@ -5,6 +5,80 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## v6.18.0 (2026-10-19)
|
|
9
|
+
|
|
10
|
+
### Changed — widened `hazo_i18n` peer range
|
|
11
|
+
|
|
12
|
+
- **`hazo_i18n` peer range widened to `^0.9.0 || ^0.15.0` (was `^0.9.0`).** This package
|
|
13
|
+
never imports `hazo_i18n` directly — only via `hazo_core/i18n`'s stable `registerCatalog()`
|
|
14
|
+
probe — so there is no real compatibility change. npm's caret on a `0.x` version only
|
|
15
|
+
matches that exact minor line (`^0.9.0` excludes `0.15.x`), so both lines must be listed
|
|
16
|
+
explicitly for a consumer installing `hazo_i18n` 0.15.x (e.g. Kinstripe's
|
|
17
|
+
dependency-hygiene cleanup) to see a valid peer.
|
|
18
|
+
|
|
19
|
+
### Added — EditableDataTable (paste, Enter-down navigation, header styling)
|
|
20
|
+
|
|
21
|
+
- **Paste support** — pasting TSV (`\t`-separated) or CSV (`,`-separated)
|
|
22
|
+
clipboard text into a text/number cell now expands across the editable
|
|
23
|
+
columns to the right of the pasted-into cell and down into as many rows
|
|
24
|
+
as the clipboard text has lines, creating new rows via `createRow()`
|
|
25
|
+
when the paste runs past the end of the current `rows` array. Only
|
|
26
|
+
targets `editable !== false` text/number columns — select/color
|
|
27
|
+
columns are left untouched by a paste that starts on or crosses one of
|
|
28
|
+
them, same restriction already in place for the Tab traversal. A
|
|
29
|
+
single-cell paste (no tab/comma/newline) is left to the browser's
|
|
30
|
+
native paste-into-input behavior, unchanged from before.
|
|
31
|
+
- **Enter moves focus down one row** — Enter still commits the current
|
|
32
|
+
cell (unchanged), and now also moves the active-edit target one row
|
|
33
|
+
down in the *same* column (spreadsheet-style), reusing the existing
|
|
34
|
+
`activeEditCell`/`forceEdit` cross-row focus mechanism that Tab already
|
|
35
|
+
relies on. On the last row there's no next row to move into; Enter just
|
|
36
|
+
commits in place, same as before this feature existed.
|
|
37
|
+
- **Clearer column headers** — the header `<TableRow>` now gets a
|
|
38
|
+
`bg-muted/60` background and a 2px bottom border, and each `<TableHead>`
|
|
39
|
+
label is bold, uppercase, and tracked (`font-semibold uppercase
|
|
40
|
+
tracking-wide text-[11px]`), so the header reads as a distinct visual
|
|
41
|
+
tier instead of blending into the first data row. No layout/API change.
|
|
42
|
+
- Tab/Shift+Tab navigation, inline-edit, add-row, delete-row, and
|
|
43
|
+
drag-reorder are all unaffected — existing tests extended, none
|
|
44
|
+
weakened. One pre-existing render-isolation test (100+ rows, "only the
|
|
45
|
+
edited row re-renders") was updated to also expect the Enter-down
|
|
46
|
+
target row (the row directly below the edited one) to re-render,
|
|
47
|
+
since forcing that row into edit mode is the whole point of the new
|
|
48
|
+
feature.
|
|
49
|
+
|
|
50
|
+
## v6.17.0 (2026-10-18)
|
|
51
|
+
|
|
52
|
+
### Added — EditableDataTable
|
|
53
|
+
|
|
54
|
+
- **`EditableDataTable`** — inline-editable data grid for small datasets.
|
|
55
|
+
Click-to-edit cells (text, number, select, color), add/delete rows, row
|
|
56
|
+
drag-reorder via @dnd-kit. Built on shadcn Table primitives. Separate from
|
|
57
|
+
`HazoUiTable` (read-only display with sort/filter/pagination). See README
|
|
58
|
+
for comparison.
|
|
59
|
+
- Test-app demo page at `/editable-table`.
|
|
60
|
+
|
|
61
|
+
### Fixed — EditableDataTable (final-review fixes)
|
|
62
|
+
|
|
63
|
+
- **Tab navigation sent focus to `document.body` in any table mixing
|
|
64
|
+
text/number columns with select/color columns.** The cross-row Tab
|
|
65
|
+
traversal (`activeEditCell`/`forceEdit`/`onTabOut`) included `select`/
|
|
66
|
+
`color` columns as possible targets, but `EditableCell`'s `forceEdit`
|
|
67
|
+
mechanism only does anything observable for `text`/`number` cells — for
|
|
68
|
+
`select`/`color` it was a no-op, so the computed target could never
|
|
69
|
+
actually receive focus and focus fell through to `document.body`
|
|
70
|
+
instead of reaching the next row. `select`/`color` columns are now
|
|
71
|
+
excluded from this traversal entirely (same as the delete/add buttons
|
|
72
|
+
already were); they remain reachable via native Tab/click, unchanged.
|
|
73
|
+
Affects every consumer whose columns mix types — e.g. hazo_canvas's
|
|
74
|
+
Sankey/Dagre/Network node tables (which end in a color column) and
|
|
75
|
+
Sankey's link table (two select columns before its number column).
|
|
76
|
+
- **Editing one cell in a 100+ row table re-rendered every row.**
|
|
77
|
+
`SortableRow` is now wrapped in `React.memo`, and row rendering is
|
|
78
|
+
restructured into a new, module-scope `React.memo`'d row component with
|
|
79
|
+
stable, ref-backed callbacks — so editing one row no longer forces every
|
|
80
|
+
other row's cells to re-render. No API change.
|
|
81
|
+
|
|
8
82
|
## v6.16.0 (2026-10-15)
|
|
9
83
|
|
|
10
84
|
### Added — Tailwind v3 preset + responsive AppHeader
|
package/README.md
CHANGED
|
@@ -3932,6 +3932,19 @@ display states automatically: loading skeleton, empty state, and populated list.
|
|
|
3932
3932
|
|
|
3933
3933
|
---
|
|
3934
3934
|
|
|
3935
|
+
## Table Components
|
|
3936
|
+
|
|
3937
|
+
hazo_ui ships two table components for different use cases:
|
|
3938
|
+
|
|
3939
|
+
| Component | Purpose | Editing | Pagination | Server-side data |
|
|
3940
|
+
|-----------|---------|---------|------------|-----------------|
|
|
3941
|
+
| `HazoUiTable` | Read-only data display | No | Yes | Yes (`onLoad`) |
|
|
3942
|
+
| `EditableDataTable` | Inline data editing | Click-to-edit cells, add/delete/reorder rows, Tab/Shift+Tab + Enter navigation, TSV/CSV paste | No | No |
|
|
3943
|
+
|
|
3944
|
+
**Use `HazoUiTable`** for dashboards, admin lists, and any table where users browse but don't modify data in-place.
|
|
3945
|
+
|
|
3946
|
+
**Use `EditableDataTable`** for forms, configuration panels, and data editors where users need to create, modify, and reorder small datasets (typically <100 rows) with immediate feedback.
|
|
3947
|
+
|
|
3935
3948
|
## HazoUiTable — Column-config-driven Data Table (v2.14.0+, v2.15 additions)
|
|
3936
3949
|
|
|
3937
3950
|
A higher-level data table that composes the shadcn `Table` primitive
|
package/dist/index.cjs
CHANGED
|
@@ -936,7 +936,11 @@ var en_default = {
|
|
|
936
936
|
"charts.pct_of_first": "% of first: {{pct}}%",
|
|
937
937
|
"charts.pct_of_prev": "% of prev: {{pct}}%",
|
|
938
938
|
"charts.dropoff": "Drop-off: \u2212{{value}}",
|
|
939
|
-
"toast.missing_prefix": "Missing: {{items}}"
|
|
939
|
+
"toast.missing_prefix": "Missing: {{items}}",
|
|
940
|
+
"editable_data_table.add": "Add",
|
|
941
|
+
"editable_data_table.delete": "Delete row",
|
|
942
|
+
"editable_data_table.empty": "No rows yet. Click Add to start.",
|
|
943
|
+
"editable_data_table.drag_to_reorder": "Drag to reorder"
|
|
940
944
|
};
|
|
941
945
|
var default_t = i18n.createPackageT("hazo_ui", en_default);
|
|
942
946
|
var defaultHazoUiT = default_t;
|
|
@@ -13224,6 +13228,506 @@ function useHazoEvent(pkg, event, handler) {
|
|
|
13224
13228
|
return () => window.removeEventListener(event_name, listener);
|
|
13225
13229
|
}, [pkg, event]);
|
|
13226
13230
|
}
|
|
13231
|
+
function EditableCell({
|
|
13232
|
+
value,
|
|
13233
|
+
type,
|
|
13234
|
+
options,
|
|
13235
|
+
defaultColorValue,
|
|
13236
|
+
clearable,
|
|
13237
|
+
onCommit,
|
|
13238
|
+
editable,
|
|
13239
|
+
forceEdit,
|
|
13240
|
+
onTabOut,
|
|
13241
|
+
onEnterDown,
|
|
13242
|
+
onPaste,
|
|
13243
|
+
onActivated
|
|
13244
|
+
}) {
|
|
13245
|
+
const [editing, setEditing] = React27.useState(false);
|
|
13246
|
+
const [draft, setDraft] = React27.useState(String(value ?? ""));
|
|
13247
|
+
const inputRef = React27.useRef(null);
|
|
13248
|
+
React27.useEffect(() => {
|
|
13249
|
+
if (forceEdit) {
|
|
13250
|
+
setDraft(String(value ?? ""));
|
|
13251
|
+
setEditing(true);
|
|
13252
|
+
}
|
|
13253
|
+
}, [forceEdit]);
|
|
13254
|
+
React27.useEffect(() => {
|
|
13255
|
+
if (editing && inputRef.current) {
|
|
13256
|
+
inputRef.current.focus();
|
|
13257
|
+
inputRef.current.select();
|
|
13258
|
+
if (forceEdit) {
|
|
13259
|
+
onActivated?.();
|
|
13260
|
+
}
|
|
13261
|
+
}
|
|
13262
|
+
}, [editing]);
|
|
13263
|
+
const commit = React27.useCallback(() => {
|
|
13264
|
+
setEditing(false);
|
|
13265
|
+
if (type === "number") {
|
|
13266
|
+
const n = Number(draft);
|
|
13267
|
+
onCommit(Number.isNaN(n) || draft.trim() === "" ? 0 : n);
|
|
13268
|
+
} else {
|
|
13269
|
+
onCommit(draft);
|
|
13270
|
+
}
|
|
13271
|
+
}, [draft, onCommit, type]);
|
|
13272
|
+
const cancel = React27.useCallback(() => {
|
|
13273
|
+
setEditing(false);
|
|
13274
|
+
setDraft(String(value ?? ""));
|
|
13275
|
+
}, [value]);
|
|
13276
|
+
const handleKeyDown = React27.useCallback(
|
|
13277
|
+
(e) => {
|
|
13278
|
+
if (e.key === "Enter") {
|
|
13279
|
+
e.preventDefault();
|
|
13280
|
+
onEnterDown?.();
|
|
13281
|
+
commit();
|
|
13282
|
+
} else if (e.key === "Escape") {
|
|
13283
|
+
e.preventDefault();
|
|
13284
|
+
cancel();
|
|
13285
|
+
} else if (e.key === "Tab") {
|
|
13286
|
+
const direction = e.shiftKey ? -1 : 1;
|
|
13287
|
+
const handled = onTabOut?.(direction) ?? false;
|
|
13288
|
+
if (handled) {
|
|
13289
|
+
e.preventDefault();
|
|
13290
|
+
commit();
|
|
13291
|
+
}
|
|
13292
|
+
}
|
|
13293
|
+
},
|
|
13294
|
+
[commit, cancel, onTabOut, onEnterDown]
|
|
13295
|
+
);
|
|
13296
|
+
if (!editable) {
|
|
13297
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs truncate block px-1 py-1.5", children: String(value ?? "") });
|
|
13298
|
+
}
|
|
13299
|
+
if (type === "color") {
|
|
13300
|
+
const colorValue = String(
|
|
13301
|
+
value ?? defaultColorValue ?? "#94a3b8"
|
|
13302
|
+
);
|
|
13303
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5", children: [
|
|
13304
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
13305
|
+
"input",
|
|
13306
|
+
{
|
|
13307
|
+
type: "color",
|
|
13308
|
+
className: "h-7 w-8 cursor-pointer border border-border rounded p-0.5",
|
|
13309
|
+
value: colorValue,
|
|
13310
|
+
onChange: (e) => onCommit(e.target.value)
|
|
13311
|
+
}
|
|
13312
|
+
),
|
|
13313
|
+
clearable && value != null && /* @__PURE__ */ jsxRuntime.jsx(
|
|
13314
|
+
"button",
|
|
13315
|
+
{
|
|
13316
|
+
type: "button",
|
|
13317
|
+
className: "text-xs leading-none text-muted-foreground hover:text-foreground px-0.5",
|
|
13318
|
+
onClick: () => onCommit(void 0),
|
|
13319
|
+
children: "\xD7"
|
|
13320
|
+
}
|
|
13321
|
+
)
|
|
13322
|
+
] });
|
|
13323
|
+
}
|
|
13324
|
+
if (type === "select") {
|
|
13325
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
13326
|
+
Select,
|
|
13327
|
+
{
|
|
13328
|
+
value: String(value ?? ""),
|
|
13329
|
+
onValueChange: (v) => onCommit(v),
|
|
13330
|
+
children: [
|
|
13331
|
+
/* @__PURE__ */ jsxRuntime.jsx(SelectTrigger, { className: "h-7 text-xs", children: /* @__PURE__ */ jsxRuntime.jsx(SelectValue, {}) }),
|
|
13332
|
+
/* @__PURE__ */ jsxRuntime.jsx(SelectContent, { children: (options ?? []).map((opt) => /* @__PURE__ */ jsxRuntime.jsx(SelectItem, { value: opt, children: opt }, opt)) })
|
|
13333
|
+
]
|
|
13334
|
+
}
|
|
13335
|
+
);
|
|
13336
|
+
}
|
|
13337
|
+
if (!editing) {
|
|
13338
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
13339
|
+
"span",
|
|
13340
|
+
{
|
|
13341
|
+
className: "text-xs truncate block px-1 py-1.5 cursor-text hover:bg-muted/50 rounded",
|
|
13342
|
+
onClick: () => {
|
|
13343
|
+
setDraft(String(value ?? ""));
|
|
13344
|
+
setEditing(true);
|
|
13345
|
+
},
|
|
13346
|
+
role: "button",
|
|
13347
|
+
tabIndex: 0,
|
|
13348
|
+
onKeyDown: (e) => {
|
|
13349
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
13350
|
+
setDraft(String(value ?? ""));
|
|
13351
|
+
setEditing(true);
|
|
13352
|
+
}
|
|
13353
|
+
},
|
|
13354
|
+
children: String(value ?? "")
|
|
13355
|
+
}
|
|
13356
|
+
);
|
|
13357
|
+
}
|
|
13358
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
13359
|
+
"input",
|
|
13360
|
+
{
|
|
13361
|
+
ref: inputRef,
|
|
13362
|
+
type: type === "number" ? "number" : "text",
|
|
13363
|
+
className: "h-7 text-xs border border-input rounded-md px-2 bg-background focus:outline-none focus:ring-1 focus:ring-ring w-full",
|
|
13364
|
+
value: draft,
|
|
13365
|
+
onChange: (e) => setDraft(e.target.value),
|
|
13366
|
+
onKeyDown: handleKeyDown,
|
|
13367
|
+
onPaste: (e) => {
|
|
13368
|
+
const text = e.clipboardData.getData("text/plain") || e.clipboardData.getData("text");
|
|
13369
|
+
if (!text) return;
|
|
13370
|
+
const handled = onPaste?.(text) ?? false;
|
|
13371
|
+
if (handled) {
|
|
13372
|
+
e.preventDefault();
|
|
13373
|
+
setEditing(false);
|
|
13374
|
+
}
|
|
13375
|
+
},
|
|
13376
|
+
onBlur: commit
|
|
13377
|
+
}
|
|
13378
|
+
);
|
|
13379
|
+
}
|
|
13380
|
+
var SortableRow = React27__namespace.default.memo(function SortableRow2({
|
|
13381
|
+
id,
|
|
13382
|
+
highlighted,
|
|
13383
|
+
children
|
|
13384
|
+
}) {
|
|
13385
|
+
const t = useHazoUiT();
|
|
13386
|
+
const {
|
|
13387
|
+
attributes,
|
|
13388
|
+
listeners,
|
|
13389
|
+
setNodeRef,
|
|
13390
|
+
setActivatorNodeRef,
|
|
13391
|
+
transform,
|
|
13392
|
+
transition,
|
|
13393
|
+
isDragging
|
|
13394
|
+
} = sortable.useSortable({ id });
|
|
13395
|
+
const style = {
|
|
13396
|
+
transform: utilities.CSS.Transform.toString(transform),
|
|
13397
|
+
transition,
|
|
13398
|
+
opacity: isDragging ? 0.5 : 1
|
|
13399
|
+
};
|
|
13400
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
13401
|
+
TableRow2,
|
|
13402
|
+
{
|
|
13403
|
+
ref: setNodeRef,
|
|
13404
|
+
style,
|
|
13405
|
+
"data-sortable-id": id,
|
|
13406
|
+
className: cn(highlighted && "bg-blue-50 ring-1 ring-blue-300"),
|
|
13407
|
+
children: [
|
|
13408
|
+
/* @__PURE__ */ jsxRuntime.jsx(TableCell2, { className: "px-0 py-0.5 w-6", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
13409
|
+
"div",
|
|
13410
|
+
{
|
|
13411
|
+
ref: setActivatorNodeRef,
|
|
13412
|
+
className: "cursor-grab active:cursor-grabbing text-muted-foreground hover:text-foreground flex items-center justify-center",
|
|
13413
|
+
title: t("editable_data_table.drag_to_reorder"),
|
|
13414
|
+
...attributes,
|
|
13415
|
+
...listeners,
|
|
13416
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.GripVertical, { className: "h-3.5 w-3.5" })
|
|
13417
|
+
}
|
|
13418
|
+
) }),
|
|
13419
|
+
children
|
|
13420
|
+
]
|
|
13421
|
+
}
|
|
13422
|
+
);
|
|
13423
|
+
});
|
|
13424
|
+
SortableRow.displayName = "SortableRow";
|
|
13425
|
+
var EditableTableRow = React27__namespace.default.memo(function EditableTableRow2({
|
|
13426
|
+
row,
|
|
13427
|
+
rowIndex,
|
|
13428
|
+
rowKey,
|
|
13429
|
+
isHighlighted,
|
|
13430
|
+
columns,
|
|
13431
|
+
activeEditColKey,
|
|
13432
|
+
deleteLabel,
|
|
13433
|
+
onCellCommit,
|
|
13434
|
+
onDeleteRow,
|
|
13435
|
+
onTabOutCell,
|
|
13436
|
+
onEnterDownCell,
|
|
13437
|
+
onPasteCell,
|
|
13438
|
+
onActivated
|
|
13439
|
+
}) {
|
|
13440
|
+
const renderCountRef = React27.useRef(0);
|
|
13441
|
+
renderCountRef.current += 1;
|
|
13442
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(SortableRow, { id: rowKey, highlighted: isHighlighted, children: [
|
|
13443
|
+
columns.map((col, colIndex) => {
|
|
13444
|
+
const resolvedOptions = typeof col.options === "function" ? col.options(row) : col.options;
|
|
13445
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
13446
|
+
TableCell2,
|
|
13447
|
+
{
|
|
13448
|
+
className: cn("px-1 py-0.5", col.width),
|
|
13449
|
+
"data-row-render-count": colIndex === 0 ? renderCountRef.current : void 0,
|
|
13450
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
13451
|
+
EditableCell,
|
|
13452
|
+
{
|
|
13453
|
+
value: row[col.key],
|
|
13454
|
+
type: col.type ?? "text",
|
|
13455
|
+
options: resolvedOptions,
|
|
13456
|
+
defaultColorValue: col.defaultValue?.(rowIndex),
|
|
13457
|
+
clearable: col.clearable,
|
|
13458
|
+
editable: col.editable !== false,
|
|
13459
|
+
forceEdit: activeEditColKey === col.key,
|
|
13460
|
+
onTabOut: (direction) => onTabOutCell(rowIndex, col.key, direction),
|
|
13461
|
+
onEnterDown: () => onEnterDownCell(rowIndex, col.key),
|
|
13462
|
+
onPaste: (text) => onPasteCell(rowIndex, col.key, text),
|
|
13463
|
+
onActivated,
|
|
13464
|
+
onCommit: (v) => onCellCommit(rowIndex, col.key, v)
|
|
13465
|
+
}
|
|
13466
|
+
)
|
|
13467
|
+
},
|
|
13468
|
+
col.key
|
|
13469
|
+
);
|
|
13470
|
+
}),
|
|
13471
|
+
/* @__PURE__ */ jsxRuntime.jsx(TableCell2, { className: "px-0 py-0.5 w-8", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
13472
|
+
Button,
|
|
13473
|
+
{
|
|
13474
|
+
variant: "ghost",
|
|
13475
|
+
size: "icon",
|
|
13476
|
+
className: "h-7 w-7 text-muted-foreground hover:text-destructive",
|
|
13477
|
+
onClick: () => onDeleteRow(rowIndex),
|
|
13478
|
+
"aria-label": deleteLabel,
|
|
13479
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-3 w-3" })
|
|
13480
|
+
}
|
|
13481
|
+
) })
|
|
13482
|
+
] });
|
|
13483
|
+
});
|
|
13484
|
+
EditableTableRow.displayName = "EditableTableRow";
|
|
13485
|
+
function EditableDataTable({
|
|
13486
|
+
columns,
|
|
13487
|
+
rows,
|
|
13488
|
+
onChange,
|
|
13489
|
+
getRowKey,
|
|
13490
|
+
createRow,
|
|
13491
|
+
highlightedKey,
|
|
13492
|
+
className,
|
|
13493
|
+
label
|
|
13494
|
+
}) {
|
|
13495
|
+
const t = useHazoUiT();
|
|
13496
|
+
const editableColumns = React27.useMemo(
|
|
13497
|
+
() => columns.filter(
|
|
13498
|
+
(col) => col.editable !== false && (col.type ?? "text") !== "select" && (col.type ?? "text") !== "color"
|
|
13499
|
+
).map((col) => col.key),
|
|
13500
|
+
[columns]
|
|
13501
|
+
);
|
|
13502
|
+
const [activeEditCell, setActiveEditCell] = React27.useState(null);
|
|
13503
|
+
const rowsRef = React27.useRef(rows);
|
|
13504
|
+
rowsRef.current = rows;
|
|
13505
|
+
const onChangeRef = React27.useRef(onChange);
|
|
13506
|
+
onChangeRef.current = onChange;
|
|
13507
|
+
const handleTabOut = React27.useCallback(
|
|
13508
|
+
(rowIndex, colKey, direction) => {
|
|
13509
|
+
const idx = editableColumns.indexOf(colKey);
|
|
13510
|
+
if (idx === -1) return false;
|
|
13511
|
+
if (direction === 1) {
|
|
13512
|
+
if (idx < editableColumns.length - 1) {
|
|
13513
|
+
setActiveEditCell({ row: rowIndex, col: editableColumns[idx + 1] });
|
|
13514
|
+
return true;
|
|
13515
|
+
}
|
|
13516
|
+
if (rowIndex + 1 < rows.length) {
|
|
13517
|
+
setActiveEditCell({ row: rowIndex + 1, col: editableColumns[0] });
|
|
13518
|
+
return true;
|
|
13519
|
+
}
|
|
13520
|
+
return false;
|
|
13521
|
+
}
|
|
13522
|
+
if (idx > 0) {
|
|
13523
|
+
setActiveEditCell({ row: rowIndex, col: editableColumns[idx - 1] });
|
|
13524
|
+
return true;
|
|
13525
|
+
}
|
|
13526
|
+
if (rowIndex - 1 >= 0) {
|
|
13527
|
+
setActiveEditCell({
|
|
13528
|
+
row: rowIndex - 1,
|
|
13529
|
+
col: editableColumns[editableColumns.length - 1]
|
|
13530
|
+
});
|
|
13531
|
+
return true;
|
|
13532
|
+
}
|
|
13533
|
+
return false;
|
|
13534
|
+
},
|
|
13535
|
+
[editableColumns, rows.length]
|
|
13536
|
+
);
|
|
13537
|
+
const handleEnterDown = React27.useCallback(
|
|
13538
|
+
(rowIndex, colKey) => {
|
|
13539
|
+
if (rowIndex + 1 < rows.length) {
|
|
13540
|
+
setActiveEditCell({ row: rowIndex + 1, col: colKey });
|
|
13541
|
+
return true;
|
|
13542
|
+
}
|
|
13543
|
+
return false;
|
|
13544
|
+
},
|
|
13545
|
+
[rows.length]
|
|
13546
|
+
);
|
|
13547
|
+
const handlePaste = React27.useCallback(
|
|
13548
|
+
(rowIndex, colKey, text) => {
|
|
13549
|
+
const startColIdx = editableColumns.indexOf(
|
|
13550
|
+
colKey
|
|
13551
|
+
);
|
|
13552
|
+
if (startColIdx === -1) return false;
|
|
13553
|
+
const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
13554
|
+
const lines = normalized.split("\n");
|
|
13555
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") {
|
|
13556
|
+
lines.pop();
|
|
13557
|
+
}
|
|
13558
|
+
const hasTab = normalized.includes(" ");
|
|
13559
|
+
const delimiter = hasTab ? " " : ",";
|
|
13560
|
+
const grid = lines.map((line) => line.split(delimiter));
|
|
13561
|
+
const isSingleCell = grid.length === 1 && grid[0].length === 1;
|
|
13562
|
+
if (isSingleCell) return false;
|
|
13563
|
+
const currentRows = rowsRef.current;
|
|
13564
|
+
const nextRows = currentRows.slice();
|
|
13565
|
+
grid.forEach((lineCells, rowOffset) => {
|
|
13566
|
+
const targetRowIndex = rowIndex + rowOffset;
|
|
13567
|
+
while (targetRowIndex >= nextRows.length) {
|
|
13568
|
+
nextRows.push(createRow());
|
|
13569
|
+
}
|
|
13570
|
+
const targetRow = {
|
|
13571
|
+
...nextRows[targetRowIndex]
|
|
13572
|
+
};
|
|
13573
|
+
lineCells.forEach((cellValue, colOffset) => {
|
|
13574
|
+
const targetColIdx = startColIdx + colOffset;
|
|
13575
|
+
if (targetColIdx >= editableColumns.length) return;
|
|
13576
|
+
const targetColKey = editableColumns[targetColIdx];
|
|
13577
|
+
const col = columns.find((c) => c.key === targetColKey);
|
|
13578
|
+
if (col?.type === "number") {
|
|
13579
|
+
const n = Number(cellValue);
|
|
13580
|
+
targetRow[targetColKey] = Number.isNaN(n) || cellValue.trim() === "" ? 0 : n;
|
|
13581
|
+
} else {
|
|
13582
|
+
targetRow[targetColKey] = cellValue;
|
|
13583
|
+
}
|
|
13584
|
+
});
|
|
13585
|
+
nextRows[targetRowIndex] = targetRow;
|
|
13586
|
+
});
|
|
13587
|
+
onChangeRef.current(nextRows);
|
|
13588
|
+
return true;
|
|
13589
|
+
},
|
|
13590
|
+
[editableColumns, columns, createRow]
|
|
13591
|
+
);
|
|
13592
|
+
const handleCellChange = React27.useCallback(
|
|
13593
|
+
(rowIndex, colKey, value) => {
|
|
13594
|
+
const currentRows = rowsRef.current;
|
|
13595
|
+
const next = currentRows.map(
|
|
13596
|
+
(row, i) => i === rowIndex ? { ...row, [colKey]: value } : row
|
|
13597
|
+
);
|
|
13598
|
+
onChangeRef.current(next);
|
|
13599
|
+
},
|
|
13600
|
+
[]
|
|
13601
|
+
);
|
|
13602
|
+
const handleDelete = React27.useCallback((rowIndex) => {
|
|
13603
|
+
const currentRows = rowsRef.current;
|
|
13604
|
+
onChangeRef.current(currentRows.filter((_, i) => i !== rowIndex));
|
|
13605
|
+
}, []);
|
|
13606
|
+
const onCellActivated = React27.useCallback(() => setActiveEditCell(null), []);
|
|
13607
|
+
const handleAdd = () => {
|
|
13608
|
+
onChange([...rows, createRow()]);
|
|
13609
|
+
};
|
|
13610
|
+
const sensors = core$1.useSensors(
|
|
13611
|
+
core$1.useSensor(core$1.PointerSensor),
|
|
13612
|
+
core$1.useSensor(core$1.KeyboardSensor, {
|
|
13613
|
+
coordinateGetter: sortable.sortableKeyboardCoordinates
|
|
13614
|
+
})
|
|
13615
|
+
);
|
|
13616
|
+
const rowKeys = React27.useMemo(
|
|
13617
|
+
() => rows.map((row, i) => getRowKey(row, i)),
|
|
13618
|
+
[rows, getRowKey]
|
|
13619
|
+
);
|
|
13620
|
+
const handleDragEnd = React27.useCallback(
|
|
13621
|
+
(event) => {
|
|
13622
|
+
const { active, over } = event;
|
|
13623
|
+
if (!over || active.id === over.id) return;
|
|
13624
|
+
const oldIndex = rows.findIndex(
|
|
13625
|
+
(row, i) => getRowKey(row, i) === active.id
|
|
13626
|
+
);
|
|
13627
|
+
const newIndex = rows.findIndex(
|
|
13628
|
+
(row, i) => getRowKey(row, i) === over.id
|
|
13629
|
+
);
|
|
13630
|
+
if (oldIndex === -1 || newIndex === -1) return;
|
|
13631
|
+
onChange(sortable.arrayMove(rows, oldIndex, newIndex));
|
|
13632
|
+
},
|
|
13633
|
+
[rows, getRowKey, onChange]
|
|
13634
|
+
);
|
|
13635
|
+
return (
|
|
13636
|
+
// DndContext wraps the whole component rather than just <TableBody> —
|
|
13637
|
+
// it renders its own hidden accessibility helpers (RestoreFocus,
|
|
13638
|
+
// live-region announcer) as DOM siblings of its children. Nesting it
|
|
13639
|
+
// directly inside <tbody> would make those helpers invalid children of
|
|
13640
|
+
// <tbody> (only <tr> is valid there); wrapping the outer <div> instead
|
|
13641
|
+
// gives them a valid home. <SortableContext> is a plain context
|
|
13642
|
+
// provider with no DOM output of its own, so it can stay right inside
|
|
13643
|
+
// <TableBody>, wrapping only the actual <tr> rows.
|
|
13644
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
13645
|
+
core$1.DndContext,
|
|
13646
|
+
{
|
|
13647
|
+
sensors,
|
|
13648
|
+
collisionDetection: core$1.closestCenter,
|
|
13649
|
+
onDragEnd: handleDragEnd,
|
|
13650
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-editable-data-table": true, className: cn("flex flex-col gap-1", className), children: [
|
|
13651
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-1", children: [
|
|
13652
|
+
label && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-semibold text-muted-foreground uppercase tracking-wide", children: label }),
|
|
13653
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
13654
|
+
Button,
|
|
13655
|
+
{
|
|
13656
|
+
variant: "ghost",
|
|
13657
|
+
size: "sm",
|
|
13658
|
+
className: "h-6 px-2 text-xs ml-auto",
|
|
13659
|
+
onClick: handleAdd,
|
|
13660
|
+
"aria-label": t("editable_data_table.add"),
|
|
13661
|
+
children: [
|
|
13662
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3 w-3 mr-1" }),
|
|
13663
|
+
t("editable_data_table.add")
|
|
13664
|
+
]
|
|
13665
|
+
}
|
|
13666
|
+
)
|
|
13667
|
+
] }),
|
|
13668
|
+
/* @__PURE__ */ jsxRuntime.jsxs(Table2, { children: [
|
|
13669
|
+
/* @__PURE__ */ jsxRuntime.jsx(TableHeader2, { children: /* @__PURE__ */ jsxRuntime.jsxs(TableRow2, { className: "bg-muted/60 hover:bg-muted/60 border-b-2 border-border", children: [
|
|
13670
|
+
/* @__PURE__ */ jsxRuntime.jsx(TableHead, { className: "w-6 px-0" }),
|
|
13671
|
+
columns.map((col) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
13672
|
+
TableHead,
|
|
13673
|
+
{
|
|
13674
|
+
className: cn(
|
|
13675
|
+
"text-[11px] font-semibold uppercase tracking-wide text-foreground px-1 py-1 h-8",
|
|
13676
|
+
col.width
|
|
13677
|
+
),
|
|
13678
|
+
children: col.label
|
|
13679
|
+
},
|
|
13680
|
+
col.key
|
|
13681
|
+
)),
|
|
13682
|
+
/* @__PURE__ */ jsxRuntime.jsx(TableHead, { className: "w-8 px-0" })
|
|
13683
|
+
] }) }),
|
|
13684
|
+
/* @__PURE__ */ jsxRuntime.jsxs(TableBody, { children: [
|
|
13685
|
+
rows.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(TableRow2, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
13686
|
+
TableCell2,
|
|
13687
|
+
{
|
|
13688
|
+
colSpan: columns.length + 2,
|
|
13689
|
+
className: "text-center text-xs text-muted-foreground italic py-4",
|
|
13690
|
+
children: t("editable_data_table.empty")
|
|
13691
|
+
}
|
|
13692
|
+
) }),
|
|
13693
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
13694
|
+
sortable.SortableContext,
|
|
13695
|
+
{
|
|
13696
|
+
items: rowKeys,
|
|
13697
|
+
strategy: sortable.verticalListSortingStrategy,
|
|
13698
|
+
children: rows.map((row, rowIndex) => {
|
|
13699
|
+
const key = rowKeys[rowIndex];
|
|
13700
|
+
const isHighlighted = highlightedKey != null && key === highlightedKey;
|
|
13701
|
+
const activeEditColKey = activeEditCell?.row === rowIndex ? activeEditCell.col : null;
|
|
13702
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
13703
|
+
EditableTableRow,
|
|
13704
|
+
{
|
|
13705
|
+
row,
|
|
13706
|
+
rowIndex,
|
|
13707
|
+
rowKey: key,
|
|
13708
|
+
isHighlighted,
|
|
13709
|
+
columns,
|
|
13710
|
+
activeEditColKey,
|
|
13711
|
+
deleteLabel: t("editable_data_table.delete"),
|
|
13712
|
+
onCellCommit: handleCellChange,
|
|
13713
|
+
onDeleteRow: handleDelete,
|
|
13714
|
+
onTabOutCell: handleTabOut,
|
|
13715
|
+
onEnterDownCell: handleEnterDown,
|
|
13716
|
+
onPasteCell: handlePaste,
|
|
13717
|
+
onActivated: onCellActivated
|
|
13718
|
+
},
|
|
13719
|
+
key
|
|
13720
|
+
);
|
|
13721
|
+
})
|
|
13722
|
+
}
|
|
13723
|
+
)
|
|
13724
|
+
] })
|
|
13725
|
+
] })
|
|
13726
|
+
] })
|
|
13727
|
+
}
|
|
13728
|
+
)
|
|
13729
|
+
);
|
|
13730
|
+
}
|
|
13227
13731
|
|
|
13228
13732
|
Object.defineProperty(exports, "rawToast", {
|
|
13229
13733
|
enumerable: true,
|
|
@@ -13316,6 +13820,7 @@ exports.DropdownMenuSub = DropdownMenuSub;
|
|
|
13316
13820
|
exports.DropdownMenuSubContent = DropdownMenuSubContent;
|
|
13317
13821
|
exports.DropdownMenuSubTrigger = DropdownMenuSubTrigger;
|
|
13318
13822
|
exports.DropdownMenuTrigger = DropdownMenuTrigger;
|
|
13823
|
+
exports.EditableDataTable = EditableDataTable;
|
|
13319
13824
|
exports.EmptyState = EmptyState;
|
|
13320
13825
|
exports.ErrorBanner = ErrorBanner;
|
|
13321
13826
|
exports.ErrorPage = ErrorPage;
|