twico-ui 1.17.1 → 1.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/CHANGELOG.md +19 -0
- package/dist/index.cjs +122 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -2
- package/dist/index.d.ts +48 -2
- package/dist/index.mjs +122 -14
- package/dist/index.mjs.map +1 -1
- package/llms.txt +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Twico UI are documented here (newest first). This file is generated automatically on each release by [semantic-release](https://semantic-release.gitbook.io/) from [Conventional Commits](https://www.conventionalcommits.org/) — do not edit it by hand. The same notes appear on each [GitHub Release](https://github.com/EncryptionL/twico-ui/releases).
|
|
4
4
|
|
|
5
|
+
# [1.18.0](https://github.com/EncryptionL/twico-ui/compare/v1.17.2...v1.18.0) (2026-07-17)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **hooks:** copy off a secure origin via execCommand fallback ([2786e74](https://github.com/EncryptionL/twico-ui/commit/2786e7414aec19832eacb8277830f72ae935307e)), closes [#255](https://github.com/EncryptionL/twico-ui/issues/255)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **datatable:** persist & restore view state via stateKey/initialState/onStateChange ([0830d59](https://github.com/EncryptionL/twico-ui/commit/0830d591e9bfb21395efa72ad67286326471371b)), closes [#45](https://github.com/EncryptionL/twico-ui/issues/45) [#259](https://github.com/EncryptionL/twico-ui/issues/259)
|
|
16
|
+
|
|
17
|
+
## [1.17.2](https://github.com/EncryptionL/twico-ui/compare/v1.17.1...v1.17.2) (2026-07-17)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **datatable:** clamp footer range + title on truncated cells/headers ([#252](https://github.com/EncryptionL/twico-ui/issues/252), [#253](https://github.com/EncryptionL/twico-ui/issues/253)) ([21a023e](https://github.com/EncryptionL/twico-ui/commit/21a023eb20aa8d05b5413ea752c18e32e15a03dc))
|
|
23
|
+
|
|
5
24
|
## [1.17.1](https://github.com/EncryptionL/twico-ui/compare/v1.17.0...v1.17.1) (2026-07-15)
|
|
6
25
|
|
|
7
26
|
|
package/dist/index.cjs
CHANGED
|
@@ -9011,6 +9011,9 @@ function Datatable({
|
|
|
9011
9011
|
onBatchUpdate,
|
|
9012
9012
|
showBatchEdit = true,
|
|
9013
9013
|
batchEditFields = null,
|
|
9014
|
+
stateKey,
|
|
9015
|
+
initialState,
|
|
9016
|
+
onStateChange,
|
|
9014
9017
|
showPageJumper = true,
|
|
9015
9018
|
selectionMode = "none",
|
|
9016
9019
|
onRowClick,
|
|
@@ -9173,6 +9176,75 @@ function Datatable({
|
|
|
9173
9176
|
const [rowGrab, setRowGrab] = React51__namespace.default.useState(null);
|
|
9174
9177
|
const [reorderMsg, setReorderMsg] = React51__namespace.default.useState("");
|
|
9175
9178
|
const rowFocusRef = React51__namespace.default.useRef(null);
|
|
9179
|
+
const serializeState = () => ({
|
|
9180
|
+
filters: filters.map(({ id, ...f }) => f),
|
|
9181
|
+
// strip the internal row id
|
|
9182
|
+
sort,
|
|
9183
|
+
quickFilter: quick,
|
|
9184
|
+
page: pageVal,
|
|
9185
|
+
pageSize: sizeVal,
|
|
9186
|
+
columnOrder: order,
|
|
9187
|
+
columnWidths: widths,
|
|
9188
|
+
columnVisibility: Object.fromEntries([...hidden].map((f) => [f, false])),
|
|
9189
|
+
// absent = visible
|
|
9190
|
+
columnPinning: Object.fromEntries([...pins.left.map((f) => [f, "left"]), ...pins.right.map((f) => [f, "right"])]),
|
|
9191
|
+
density
|
|
9192
|
+
});
|
|
9193
|
+
const applyState = (s) => {
|
|
9194
|
+
if (!s || typeof s !== "object") return;
|
|
9195
|
+
const known = new Set(cols.map((c) => c.field).filter(Boolean));
|
|
9196
|
+
if (Array.isArray(s.filters)) setFilters(s.filters.filter((f) => f && known.has(f.field)).map((f, i) => ({ id: Date.now() + Math.random() + i, field: f.field, op: f.op, value: f.value })));
|
|
9197
|
+
if ("sort" in s) setSort(s.sort && known.has(s.sort.field) ? { field: s.sort.field, dir: s.sort.dir === "desc" ? "desc" : "asc" } : null);
|
|
9198
|
+
if (typeof s.quickFilter === "string" && quickFilter === void 0) setInternalQuick(s.quickFilter);
|
|
9199
|
+
if (typeof s.page === "number" && !pageControlled) setInternalPage(Math.max(0, Math.floor(s.page)));
|
|
9200
|
+
if (typeof s.pageSize === "number" && !pageSizeControlled) setInternalRpp(s.pageSize > 0 ? s.pageSize : 10);
|
|
9201
|
+
if (Array.isArray(s.columnOrder)) {
|
|
9202
|
+
const savedOrder = s.columnOrder.filter((f) => known.has(f));
|
|
9203
|
+
const missing = cols.map((c) => c.field).filter((f) => f && !savedOrder.includes(f));
|
|
9204
|
+
setOrder([...savedOrder, ...missing]);
|
|
9205
|
+
}
|
|
9206
|
+
if (s.columnWidths && typeof s.columnWidths === "object") setWidths(Object.fromEntries(Object.entries(s.columnWidths).filter(([f, w]) => known.has(f) && typeof w === "number")));
|
|
9207
|
+
if (s.columnVisibility && typeof s.columnVisibility === "object") setHidden(new Set(Object.keys(s.columnVisibility).filter((f) => known.has(f) && s.columnVisibility[f] === false)));
|
|
9208
|
+
if (s.columnPinning && typeof s.columnPinning === "object") {
|
|
9209
|
+
const l = [], r2 = [];
|
|
9210
|
+
for (const [f, side] of Object.entries(s.columnPinning)) {
|
|
9211
|
+
if (!known.has(f)) continue;
|
|
9212
|
+
if (side === "left") l.push(f);
|
|
9213
|
+
else if (side === "right") r2.push(f);
|
|
9214
|
+
}
|
|
9215
|
+
setPins({ left: l, right: r2 });
|
|
9216
|
+
}
|
|
9217
|
+
if (s.density === "compact" || s.density === "standard" || s.density === "comfortable") setDensity(s.density);
|
|
9218
|
+
};
|
|
9219
|
+
const stateReadyRef = React51__namespace.default.useRef(false);
|
|
9220
|
+
const onStateChangeRef = React51__namespace.default.useRef(onStateChange);
|
|
9221
|
+
onStateChangeRef.current = onStateChange;
|
|
9222
|
+
React51__namespace.default.useEffect(() => {
|
|
9223
|
+
if (!stateReadyRef.current) return void 0;
|
|
9224
|
+
if (!stateKey && !onStateChange) return void 0;
|
|
9225
|
+
const state = serializeState();
|
|
9226
|
+
onStateChangeRef.current?.(state);
|
|
9227
|
+
if (stateKey) {
|
|
9228
|
+
try {
|
|
9229
|
+
window.localStorage.setItem(stateKey, JSON.stringify(state));
|
|
9230
|
+
} catch {
|
|
9231
|
+
}
|
|
9232
|
+
}
|
|
9233
|
+
return void 0;
|
|
9234
|
+
}, [filters, sort, quick, pageVal, sizeVal, order, widths, hidden, pins, density, stateKey]);
|
|
9235
|
+
React51__namespace.default.useEffect(() => {
|
|
9236
|
+
let saved = null;
|
|
9237
|
+
if (stateKey) {
|
|
9238
|
+
try {
|
|
9239
|
+
const raw = window.localStorage.getItem(stateKey);
|
|
9240
|
+
if (raw) saved = JSON.parse(raw);
|
|
9241
|
+
} catch {
|
|
9242
|
+
}
|
|
9243
|
+
}
|
|
9244
|
+
if (!saved && initialState) saved = initialState;
|
|
9245
|
+
if (saved) applyState(saved);
|
|
9246
|
+
stateReadyRef.current = true;
|
|
9247
|
+
}, []);
|
|
9176
9248
|
const rootRef = React51__namespace.default.useRef(null);
|
|
9177
9249
|
const [compact, setCompact] = React51__namespace.default.useState(false);
|
|
9178
9250
|
React51__namespace.default.useEffect(() => {
|
|
@@ -9214,13 +9286,20 @@ function Datatable({
|
|
|
9214
9286
|
return [...kept, ...added];
|
|
9215
9287
|
});
|
|
9216
9288
|
}, [columns]);
|
|
9289
|
+
const densitySyncedRef = React51__namespace.default.useRef(false);
|
|
9217
9290
|
React51__namespace.default.useEffect(() => {
|
|
9218
|
-
setDensity(densityProp);
|
|
9291
|
+
if (densitySyncedRef.current) setDensity(densityProp);
|
|
9292
|
+
else densitySyncedRef.current = true;
|
|
9219
9293
|
}, [densityProp]);
|
|
9220
9294
|
React51__namespace.default.useEffect(() => {
|
|
9221
9295
|
setAggOn(showAggregation);
|
|
9222
9296
|
}, [showAggregation]);
|
|
9297
|
+
const pageSizeSyncedRef = React51__namespace.default.useRef(false);
|
|
9223
9298
|
React51__namespace.default.useEffect(() => {
|
|
9299
|
+
if (!pageSizeSyncedRef.current) {
|
|
9300
|
+
pageSizeSyncedRef.current = true;
|
|
9301
|
+
return;
|
|
9302
|
+
}
|
|
9224
9303
|
if (pageSizeControlled) return;
|
|
9225
9304
|
setInternalRpp(pageSize > 0 ? pageSize : 10);
|
|
9226
9305
|
setInternalPage(0);
|
|
@@ -10328,6 +10407,8 @@ function Datatable({
|
|
|
10328
10407
|
const editable = isColEditable(c);
|
|
10329
10408
|
const isEditing = editing && editing.key === k && editing.field === c.field;
|
|
10330
10409
|
const cellActive = selectionMode === "cell" && activeCell && activeCell.key === k && activeCell.field === c.field;
|
|
10410
|
+
const display = c.valueFormatter ? c.valueFormatter(val, row) : val;
|
|
10411
|
+
const cellTitle = !isActions && !c.renderCell && !isEditing && (typeof display === "string" || typeof display === "number") ? String(display) : void 0;
|
|
10331
10412
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
10332
10413
|
"td",
|
|
10333
10414
|
{
|
|
@@ -10346,14 +10427,14 @@ function Datatable({
|
|
|
10346
10427
|
"data-pin": st.pin,
|
|
10347
10428
|
"data-pin-edge": st.edge,
|
|
10348
10429
|
style: { width: widthOf(c), ...st.style },
|
|
10349
|
-
title:
|
|
10430
|
+
title: cellTitle,
|
|
10350
10431
|
onClick: selectionMode === "cell" ? (e) => handleCellClick(e, k, row, c) : void 0,
|
|
10351
10432
|
onFocus: () => setFocus((f) => f.r === ri && f.c === ci ? f : { r: ri, c: ci }),
|
|
10352
10433
|
onDoubleClick: editable ? () => beginEdit(k, c, row) : void 0,
|
|
10353
10434
|
children: [
|
|
10354
10435
|
!checkboxSelection && reorderable && ci === 0 && !isEditing ? /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: 6, maxWidth: "100%" }, children: [
|
|
10355
10436
|
rowHandle(k, midIdx, row),
|
|
10356
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { style: { overflow: "hidden", textOverflow: "ellipsis" }, children: isActions ? renderActions(c, row) : c.renderCell ? c.renderCell(val, row) :
|
|
10437
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: { overflow: "hidden", textOverflow: "ellipsis" }, children: isActions ? renderActions(c, row) : c.renderCell ? c.renderCell(val, row) : display })
|
|
10357
10438
|
] }) : isEditing ? c.renderEditCell ? (
|
|
10358
10439
|
// #236: full escape hatch — the host drops in its own editor (searchable/creatable/
|
|
10359
10440
|
// async Combobox, …) and drives the lifecycle via commit/cancel. Wrapped in
|
|
@@ -10372,7 +10453,7 @@ function Datatable({
|
|
|
10372
10453
|
onCancel: cancelEdit,
|
|
10373
10454
|
onKeyDown: onEditKey
|
|
10374
10455
|
}
|
|
10375
|
-
) : isActions ? renderActions(c, row) : c.renderCell ? c.renderCell(val, row) :
|
|
10456
|
+
) : isActions ? renderActions(c, row) : c.renderCell ? c.renderCell(val, row) : display,
|
|
10376
10457
|
editable && !isEditing ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-dt__edit-hint", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx(Svg, { d: I.pencil }) }) : null
|
|
10377
10458
|
]
|
|
10378
10459
|
},
|
|
@@ -10788,6 +10869,7 @@ function Datatable({
|
|
|
10788
10869
|
className: "twc-dt__th-label",
|
|
10789
10870
|
role: c.sortable ? "button" : void 0,
|
|
10790
10871
|
tabIndex: c.sortable ? 0 : void 0,
|
|
10872
|
+
title: typeof c.headerName === "string" ? c.headerName : void 0,
|
|
10791
10873
|
"aria-label": c.sortable ? `${c.headerName}, sort` : void 0,
|
|
10792
10874
|
draggable: reorderable || void 0,
|
|
10793
10875
|
onDragStart: reorderable ? (e) => {
|
|
@@ -10919,8 +11001,8 @@ function Datatable({
|
|
|
10919
11001
|
const nf = (n) => n.toLocaleString();
|
|
10920
11002
|
if (totalRows === 0) return "No rows";
|
|
10921
11003
|
const shown = paged.length;
|
|
10922
|
-
const start = paginated ? pageVal * sizeVal + 1 : 1;
|
|
10923
|
-
const end = paginated ? start + shown - 1 : shown;
|
|
11004
|
+
const start = paginated ? Math.min(pageVal * sizeVal + 1, totalRows) : 1;
|
|
11005
|
+
const end = paginated ? Math.min(start + shown - 1, totalRows) : shown;
|
|
10924
11006
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
10925
11007
|
paginated ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
10926
11008
|
"Showing ",
|
|
@@ -17806,21 +17888,47 @@ function useLocalStorage(key, initialValue) {
|
|
|
17806
17888
|
);
|
|
17807
17889
|
return [stored, setValue];
|
|
17808
17890
|
}
|
|
17891
|
+
function legacyCopy(text) {
|
|
17892
|
+
try {
|
|
17893
|
+
const ta = document.createElement("textarea");
|
|
17894
|
+
ta.value = text;
|
|
17895
|
+
ta.setAttribute("readonly", "");
|
|
17896
|
+
ta.style.cssText = "position:fixed;top:0;left:0;width:1px;height:1px;padding:0;border:none;opacity:0;pointer-events:none;";
|
|
17897
|
+
document.body.appendChild(ta);
|
|
17898
|
+
const sel = document.getSelection();
|
|
17899
|
+
const prev = sel && sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
|
|
17900
|
+
ta.focus();
|
|
17901
|
+
ta.select();
|
|
17902
|
+
ta.setSelectionRange(0, ta.value.length);
|
|
17903
|
+
const ok = document.execCommand("copy");
|
|
17904
|
+
document.body.removeChild(ta);
|
|
17905
|
+
if (prev && sel) {
|
|
17906
|
+
sel.removeAllRanges();
|
|
17907
|
+
sel.addRange(prev);
|
|
17908
|
+
}
|
|
17909
|
+
return ok;
|
|
17910
|
+
} catch {
|
|
17911
|
+
return false;
|
|
17912
|
+
}
|
|
17913
|
+
}
|
|
17809
17914
|
function useCopyToClipboard(timeout = 1500) {
|
|
17810
17915
|
const [copied, setCopied] = React51__namespace.default.useState(false);
|
|
17811
17916
|
const copy = React51__namespace.default.useCallback(async (text) => {
|
|
17812
|
-
if (!canUseDOM2
|
|
17917
|
+
if (!canUseDOM2) {
|
|
17813
17918
|
setCopied(false);
|
|
17814
17919
|
return false;
|
|
17815
17920
|
}
|
|
17816
|
-
|
|
17817
|
-
|
|
17818
|
-
|
|
17819
|
-
|
|
17820
|
-
|
|
17821
|
-
|
|
17822
|
-
|
|
17921
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
17922
|
+
try {
|
|
17923
|
+
await navigator.clipboard.writeText(text);
|
|
17924
|
+
setCopied(true);
|
|
17925
|
+
return true;
|
|
17926
|
+
} catch {
|
|
17927
|
+
}
|
|
17823
17928
|
}
|
|
17929
|
+
const ok = legacyCopy(String(text ?? ""));
|
|
17930
|
+
setCopied(ok);
|
|
17931
|
+
return ok;
|
|
17824
17932
|
}, []);
|
|
17825
17933
|
React51__namespace.default.useEffect(() => {
|
|
17826
17934
|
if (!copied) return void 0;
|