twico-ui 1.41.0 → 1.42.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 +21 -0
- package/dist/index.cjs +67 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -6
- package/dist/index.d.ts +40 -6
- package/dist/index.mjs +67 -16
- package/dist/index.mjs.map +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
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.42.0](https://github.com/EncryptionL/twico-ui/compare/v1.41.1...v1.42.0) (2026-09-24)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **datatable:** gate row-tree sub-rows in the batch write + document the 4-arg onBatchUpdate ([1cfb163](https://github.com/EncryptionL/twico-ui/commit/1cfb1638aca476b3234800fb547cbcd78662807d)), closes [#432](https://github.com/EncryptionL/twico-ui/issues/432) [#359](https://github.com/EncryptionL/twico-ui/issues/359) [#428](https://github.com/EncryptionL/twico-ui/issues/428) [#428](https://github.com/EncryptionL/twico-ui/issues/428)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **datatable:** report rowPatches + skippedKeys from onBatchUpdate ([f15be7f](https://github.com/EncryptionL/twico-ui/commit/f15be7f6c9611d099f50df850acc47c3bb769bfa))
|
|
16
|
+
|
|
17
|
+
## [1.41.1](https://github.com/EncryptionL/twico-ui/compare/v1.41.0...v1.41.1) (2026-09-24)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **button:** make the neutral pressed tone legible in dark mode ([7cd0cf0](https://github.com/EncryptionL/twico-ui/commit/7cd0cf019b44be2246b2dde870325fffc2c9228c)), closes [#411](https://github.com/EncryptionL/twico-ui/issues/411) [#0b1222](https://github.com/EncryptionL/twico-ui/issues/0b1222) [#0f172a](https://github.com/EncryptionL/twico-ui/issues/0f172a) [#411](https://github.com/EncryptionL/twico-ui/issues/411)
|
|
23
|
+
* **datatable:** apply per-row editable to batch write ([#428](https://github.com/EncryptionL/twico-ui/issues/428)) + reveal once per activeCell ([#429](https://github.com/EncryptionL/twico-ui/issues/429)) ([363044f](https://github.com/EncryptionL/twico-ui/commit/363044f10c4dafcc90caf68b5d8464a7ccff7e41)), closes [#424](https://github.com/EncryptionL/twico-ui/issues/424)
|
|
24
|
+
* **datatable:** make batch keys×patch safe for mixed columns ([#428](https://github.com/EncryptionL/twico-ui/issues/428)) + converge virtualized reveal ([#429](https://github.com/EncryptionL/twico-ui/issues/429)) ([394a79c](https://github.com/EncryptionL/twico-ui/commit/394a79caacc8136403fa6b5386a53cd6b1d64021))
|
|
25
|
+
|
|
5
26
|
# [1.41.0](https://github.com/EncryptionL/twico-ui/compare/v1.40.0...v1.41.0) (2026-09-23)
|
|
6
27
|
|
|
7
28
|
|
package/dist/index.cjs
CHANGED
|
@@ -441,7 +441,10 @@ var ICONBTN_CSS = `
|
|
|
441
441
|
.twc-iconbtn[aria-pressed="true"][data-pressed-tone="warning"] { --_accent: var(--color-warning); --_accent-subtle: var(--color-warning-subtle); --_accent-subtle-fg: var(--color-warning-subtle-fg); }
|
|
442
442
|
.twc-iconbtn[aria-pressed="true"][data-pressed-tone="info"] { --_accent: var(--color-info); --_accent-subtle: var(--color-info-subtle); --_accent-subtle-fg: var(--color-info-subtle-fg); }
|
|
443
443
|
.twc-iconbtn[aria-pressed="true"][data-pressed-tone="danger"] { --_accent: var(--color-danger); --_accent-subtle: var(--color-danger-subtle); --_accent-subtle-fg: var(--color-danger-subtle-fg); }
|
|
444
|
-
|
|
444
|
+
/* #430: color-surface-sunken steps toward the page background in dark mode, so a neutral pressed fill was
|
|
445
|
+
invisible on a surface panel. Fill with color-border (steps away from the background in both themes) + a
|
|
446
|
+
stronger color-text-subtle edge so the outline also carries the "on" state. Mirrors Button. */
|
|
447
|
+
.twc-iconbtn[aria-pressed="true"][data-pressed-tone="neutral"] { --_accent: var(--color-text-subtle); --_accent-subtle: var(--color-border); --_accent-subtle-fg: var(--color-text); }
|
|
445
448
|
/* #418: keep the pressed look on hover (matching-specificity, after the variant hover rules). */
|
|
446
449
|
.twc-iconbtn[aria-pressed="true"]:hover:not(:disabled) { background: var(--_accent-subtle); color: var(--_accent-subtle-fg); border-color: var(--_accent); filter: brightness(0.97); }
|
|
447
450
|
.dark .twc-iconbtn[aria-pressed="true"]:hover:not(:disabled) { filter: brightness(1.25); }
|
|
@@ -1739,7 +1742,11 @@ var CSS2 = `
|
|
|
1739
1742
|
.twc-btn[aria-pressed="true"][data-pressed-tone="warning"] { --_accent: var(--color-warning); --_accent-subtle: var(--color-warning-subtle); --_accent-subtle-fg: var(--color-warning-subtle-fg); }
|
|
1740
1743
|
.twc-btn[aria-pressed="true"][data-pressed-tone="info"] { --_accent: var(--color-info); --_accent-subtle: var(--color-info-subtle); --_accent-subtle-fg: var(--color-info-subtle-fg); }
|
|
1741
1744
|
.twc-btn[aria-pressed="true"][data-pressed-tone="danger"] { --_accent: var(--color-danger); --_accent-subtle: var(--color-danger-subtle); --_accent-subtle-fg: var(--color-danger-subtle-fg); }
|
|
1742
|
-
|
|
1745
|
+
/* #430: color-surface-sunken steps TOWARD the page background in dark mode (0b1222 under 0f172a), so a neutral
|
|
1746
|
+
pressed fill was invisible on a surface panel. Fill with color-border instead \u2014 it steps AWAY from the
|
|
1747
|
+
background in both themes (lighter in dark, darker in light) \u2014 and strengthen the edge with color-text-subtle
|
|
1748
|
+
so the outline carries the "on" state too (neutral has no meaning-colour to recolour the label). */
|
|
1749
|
+
.twc-btn[aria-pressed="true"][data-pressed-tone="neutral"] { --_accent: var(--color-text-subtle); --_accent-subtle: var(--color-border); --_accent-subtle-fg: var(--color-text); }
|
|
1743
1750
|
/* #418: keep the pressed look under the pointer \u2014 the variant hover rules (0,4,0) otherwise beat the base
|
|
1744
1751
|
pressed rule (0,2,0). This matching-specificity rule, source-ordered after them, restores the fill/fg/border
|
|
1745
1752
|
(resetting the text colour too, so solid stays legible \u2014 incl. tone="danger"). */
|
|
@@ -10483,7 +10490,7 @@ function Datatable({
|
|
|
10483
10490
|
);
|
|
10484
10491
|
const effectiveHidden = React53__namespace.default.useMemo(() => combineSources.size ? /* @__PURE__ */ new Set([...hidden, ...combineSources]) : hidden, [hidden, combineSources]);
|
|
10485
10492
|
const effectiveWrapped = React53__namespace.default.useMemo(() => stackCombineTargets.size ? /* @__PURE__ */ new Set([...wrapped, ...stackCombineTargets]) : wrapped, [wrapped, stackCombineTargets]);
|
|
10486
|
-
const visibleCols = cols.filter((c) => !effectiveHidden.has(c.field));
|
|
10493
|
+
const visibleCols = React53__namespace.default.useMemo(() => cols.filter((c) => !effectiveHidden.has(c.field)), [cols, effectiveHidden]);
|
|
10487
10494
|
const colFieldsKey = JSON.stringify(
|
|
10488
10495
|
cols.filter((c) => typeof c.field === "string" && c.type !== "actions").map((c) => c.field)
|
|
10489
10496
|
);
|
|
@@ -11358,12 +11365,18 @@ function Datatable({
|
|
|
11358
11365
|
const revealOn = !!scrollActiveCellIntoView;
|
|
11359
11366
|
const revealBlock = scrollActiveCellIntoView && typeof scrollActiveCellIntoView === "object" && scrollActiveCellIntoView.block === "center" ? "center" : "nearest";
|
|
11360
11367
|
const revealInline = scrollActiveCellIntoView && typeof scrollActiveCellIntoView === "object" && scrollActiveCellIntoView.inline === "center" ? "center" : "nearest";
|
|
11368
|
+
const revealRi = activeCellVal ? keyIndex.get(activeCellVal.key) : void 0;
|
|
11369
|
+
const revealCi = activeCellVal ? cellColIndex(activeCellVal.field) : -1;
|
|
11370
|
+
const revealOffsetsRef = React53__namespace.default.useRef(offsets);
|
|
11371
|
+
revealOffsetsRef.current = offsets;
|
|
11372
|
+
const revealMidRef = React53__namespace.default.useRef(keyIndexMid);
|
|
11373
|
+
revealMidRef.current = keyIndexMid;
|
|
11361
11374
|
React53__namespace.default.useEffect(() => {
|
|
11362
11375
|
if (!revealOn || !activeCellVal) return void 0;
|
|
11363
11376
|
const block = revealBlock, inline = revealInline;
|
|
11364
|
-
const ri =
|
|
11377
|
+
const ri = revealRi, ci = revealCi;
|
|
11365
11378
|
if (ri == null || ci < 0) return void 0;
|
|
11366
|
-
let raf = 0;
|
|
11379
|
+
let raf = 0, tries = 0;
|
|
11367
11380
|
const reveal = () => {
|
|
11368
11381
|
const sc = scrollRef.current;
|
|
11369
11382
|
if (!sc) return;
|
|
@@ -11389,10 +11402,12 @@ function Datatable({
|
|
|
11389
11402
|
} else if (tR.left < scR.left + leftInset) sc.scrollLeft -= scR.left + leftInset - tR.left;
|
|
11390
11403
|
else if (tR.right > scR.right - rightInset) sc.scrollLeft += tR.right - (scR.right - rightInset);
|
|
11391
11404
|
}
|
|
11392
|
-
} else if (virtualizing &&
|
|
11393
|
-
const
|
|
11394
|
-
|
|
11395
|
-
|
|
11405
|
+
} else if (virtualizing && revealOffsetsRef.current) {
|
|
11406
|
+
const off = revealOffsetsRef.current;
|
|
11407
|
+
const mi = revealMidRef.current.get(activeCellVal.key);
|
|
11408
|
+
if (mi != null && off[mi] != null && tries < 30) {
|
|
11409
|
+
tries += 1;
|
|
11410
|
+
sc.scrollTop = Math.max(0, off[mi] - (block === "center" ? sc.clientHeight / 2 : headH));
|
|
11396
11411
|
raf = requestAnimationFrame(reveal);
|
|
11397
11412
|
}
|
|
11398
11413
|
}
|
|
@@ -11401,7 +11416,7 @@ function Datatable({
|
|
|
11401
11416
|
return () => {
|
|
11402
11417
|
if (raf) cancelAnimationFrame(raf);
|
|
11403
11418
|
};
|
|
11404
|
-
}, [activeCellVal?.key, activeCellVal?.field, revealOn, revealBlock, revealInline,
|
|
11419
|
+
}, [activeCellVal?.key, activeCellVal?.field, revealOn, revealBlock, revealInline, revealRi, revealCi, virtualizing, headH]);
|
|
11405
11420
|
React53__namespace.default.useEffect(() => {
|
|
11406
11421
|
if (!activeCellControlled || !activeCellVal) {
|
|
11407
11422
|
jumpAppliedRef.current = null;
|
|
@@ -11493,6 +11508,10 @@ function Datatable({
|
|
|
11493
11508
|
[cols, editMode, batchEditKey]
|
|
11494
11509
|
// eslint-disable-line react-hooks/exhaustive-deps
|
|
11495
11510
|
);
|
|
11511
|
+
const batchEditableColsSel = React53__namespace.default.useMemo(
|
|
11512
|
+
() => batchEditableCols.filter((c) => typeof c.editable !== "function" || selectedRows.some((r2) => c.editable(r2, { field: c.field }))),
|
|
11513
|
+
[batchEditableCols, selectedRows]
|
|
11514
|
+
);
|
|
11496
11515
|
const hasBatchEditor = showBatchEdit && batchEditableCols.length > 0;
|
|
11497
11516
|
const [batchEdit, setBatchEdit] = React53__namespace.default.useState(null);
|
|
11498
11517
|
const [batchEditPos, openBatchEdit, closeBatchEdit] = useFloating();
|
|
@@ -11527,7 +11546,7 @@ function Datatable({
|
|
|
11527
11546
|
}, [batchEdit, selected.size, closeBatchEdit]);
|
|
11528
11547
|
function applyBatchEdit() {
|
|
11529
11548
|
if (!batchEdit) return;
|
|
11530
|
-
const active =
|
|
11549
|
+
const active = batchEditableColsSel.filter((c) => batchEdit.fields[c.field]);
|
|
11531
11550
|
if (!active.length) {
|
|
11532
11551
|
setBatchEdit(null);
|
|
11533
11552
|
closeBatchEdit();
|
|
@@ -11543,20 +11562,48 @@ function Datatable({
|
|
|
11543
11562
|
patch[c.field] = v;
|
|
11544
11563
|
});
|
|
11545
11564
|
const selKeys2 = new Set(selected);
|
|
11565
|
+
const loadedSel = /* @__PURE__ */ new Set();
|
|
11546
11566
|
const changedRows = [];
|
|
11567
|
+
const safeKeys = [];
|
|
11568
|
+
const rowPatches = [];
|
|
11569
|
+
const skippedKeys = [];
|
|
11547
11570
|
const nextAll = rows.map((r2, i) => {
|
|
11548
11571
|
const k = keyOf(r2, i);
|
|
11549
11572
|
if (!selKeys2.has(k)) return r2;
|
|
11573
|
+
loadedSel.add(k);
|
|
11550
11574
|
const rowPatch = {};
|
|
11575
|
+
let allEditable = true;
|
|
11551
11576
|
for (const c of active) {
|
|
11552
11577
|
if (typeof c.editable !== "function" || c.editable(r2, { field: c.field })) rowPatch[c.field] = patch[c.field];
|
|
11578
|
+
else allEditable = false;
|
|
11579
|
+
}
|
|
11580
|
+
if (allEditable) safeKeys.push(k);
|
|
11581
|
+
if (!Object.keys(rowPatch).length) {
|
|
11582
|
+
skippedKeys.push(k);
|
|
11583
|
+
return r2;
|
|
11553
11584
|
}
|
|
11554
|
-
if (!Object.keys(rowPatch).length) return r2;
|
|
11555
11585
|
const updated = { ...r2, ...rowPatch };
|
|
11556
11586
|
changedRows.push(updated);
|
|
11587
|
+
rowPatches.push({ key: k, patch: rowPatch });
|
|
11557
11588
|
return updated;
|
|
11558
11589
|
});
|
|
11559
|
-
|
|
11590
|
+
for (const k of selKeys2) {
|
|
11591
|
+
if (loadedSel.has(k)) continue;
|
|
11592
|
+
const li = keyIndex.get(k);
|
|
11593
|
+
const sub = li == null ? null : leafRows[li];
|
|
11594
|
+
if (!sub) {
|
|
11595
|
+
safeKeys.push(k);
|
|
11596
|
+
continue;
|
|
11597
|
+
}
|
|
11598
|
+
let allEditable = true, anyEditable = false;
|
|
11599
|
+
for (const c of active) {
|
|
11600
|
+
if (typeof c.editable !== "function" || c.editable(sub, { field: c.field })) anyEditable = true;
|
|
11601
|
+
else allEditable = false;
|
|
11602
|
+
}
|
|
11603
|
+
if (allEditable) safeKeys.push(k);
|
|
11604
|
+
if (!anyEditable) skippedKeys.push(k);
|
|
11605
|
+
}
|
|
11606
|
+
onBatchUpdate?.(changedRows, patch, safeKeys, { rowPatches, skippedKeys });
|
|
11560
11607
|
onRowsChange?.(nextAll);
|
|
11561
11608
|
setBatchEdit(null);
|
|
11562
11609
|
closeBatchEdit();
|
|
@@ -13099,8 +13146,8 @@ function Datatable({
|
|
|
13099
13146
|
}
|
|
13100
13147
|
) : null,
|
|
13101
13148
|
batchEdit && batchEditPos ? (() => {
|
|
13102
|
-
const pickedCols = Object.keys(batchEdit.fields).filter((f) => batchEdit.fields[f]).map((f) =>
|
|
13103
|
-
const unpicked =
|
|
13149
|
+
const pickedCols = Object.keys(batchEdit.fields).filter((f) => batchEdit.fields[f]).map((f) => batchEditableColsSel.find((c) => c.field === f)).filter(Boolean);
|
|
13150
|
+
const unpicked = batchEditableColsSel.filter((c) => !batchEdit.fields[c.field]);
|
|
13104
13151
|
const addField = (f) => {
|
|
13105
13152
|
if (f) setBatchEdit((b) => ({ ...b, fields: { ...b.fields, [f]: true }, values: { ...b.values, [f]: b.values[f] ?? "" } }));
|
|
13106
13153
|
};
|
|
@@ -13132,7 +13179,11 @@ function Datatable({
|
|
|
13132
13179
|
onChange: addField
|
|
13133
13180
|
}
|
|
13134
13181
|
) }) : null,
|
|
13135
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-dt__cfg-list", children: pickedCols.length === 0 ?
|
|
13182
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-dt__cfg-list", children: pickedCols.length === 0 ? (
|
|
13183
|
+
// #428: when a predicate leaves no column editable for the current selection, say so instead of
|
|
13184
|
+
// inviting a pick that can't be made (the "Add a column…" picker is hidden when unpicked is empty).
|
|
13185
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-dt__be-empty", children: unpicked.length ? "Pick a column to set its value on all selected rows." : "No columns are editable for the current selection." })
|
|
13186
|
+
) : pickedCols.map((c, i) => {
|
|
13136
13187
|
const opts = c.valueOptions ? c.valueOptions.map((o) => typeof o === "string" ? { value: o, label: o } : o) : null;
|
|
13137
13188
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "twc-dt__be-row", children: [
|
|
13138
13189
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "twc-dt__be-name", title: colLabel(c), children: [
|