twico-ui 1.40.0 → 1.41.1
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 +63 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -4
- package/dist/index.d.ts +20 -4
- package/dist/index.mjs +63 -17
- package/dist/index.mjs.map +1 -1
- package/llms.txt +3 -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.41.1](https://github.com/EncryptionL/twico-ui/compare/v1.41.0...v1.41.1) (2026-09-24)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **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)
|
|
11
|
+
* **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)
|
|
12
|
+
* **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))
|
|
13
|
+
|
|
14
|
+
# [1.41.0](https://github.com/EncryptionL/twico-ui/compare/v1.40.0...v1.41.0) (2026-09-23)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* **combobox:** keep active option in sync with a filtering defaultQuery ([2d13c08](https://github.com/EncryptionL/twico-ui/commit/2d13c0813cad21c5cda267f5344b2213715b48e2)), closes [#425](https://github.com/EncryptionL/twico-ui/issues/425)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* **combobox:** add defaultQuery + onOpenChange for cell-editor use ([4d65be7](https://github.com/EncryptionL/twico-ui/commit/4d65be7f0062cf77abd438a1b342393e944f5562))
|
|
25
|
+
|
|
5
26
|
# [1.40.0](https://github.com/EncryptionL/twico-ui/compare/v1.39.0...v1.40.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"). */
|
|
@@ -6349,6 +6356,7 @@ function Combobox({
|
|
|
6349
6356
|
options,
|
|
6350
6357
|
value,
|
|
6351
6358
|
defaultValue = null,
|
|
6359
|
+
defaultQuery,
|
|
6352
6360
|
onChange,
|
|
6353
6361
|
clearable = false,
|
|
6354
6362
|
disabled = false,
|
|
@@ -6356,6 +6364,7 @@ function Combobox({
|
|
|
6356
6364
|
portal = true,
|
|
6357
6365
|
minWidth = 0,
|
|
6358
6366
|
onInputChange,
|
|
6367
|
+
onOpenChange,
|
|
6359
6368
|
filter,
|
|
6360
6369
|
loading = false,
|
|
6361
6370
|
emptyText = "No results found",
|
|
@@ -6472,9 +6481,18 @@ function Combobox({
|
|
|
6472
6481
|
React53__namespace.default.useEffect(() => {
|
|
6473
6482
|
setActive(0);
|
|
6474
6483
|
}, [query]);
|
|
6484
|
+
const onOpenChangeRef = React53__namespace.default.useRef(onOpenChange);
|
|
6485
|
+
onOpenChangeRef.current = onOpenChange;
|
|
6486
|
+
const prevOpenRef = React53__namespace.default.useRef(open);
|
|
6487
|
+
React53__namespace.default.useEffect(() => {
|
|
6488
|
+
if (prevOpenRef.current !== open) {
|
|
6489
|
+
prevOpenRef.current = open;
|
|
6490
|
+
onOpenChangeRef.current?.(open);
|
|
6491
|
+
}
|
|
6492
|
+
}, [open]);
|
|
6475
6493
|
React53__namespace.default.useEffect(() => {
|
|
6476
6494
|
if (!open) return;
|
|
6477
|
-
const idx =
|
|
6495
|
+
const idx = visible.findIndex((o) => o.value === current);
|
|
6478
6496
|
setActive(idx >= 0 ? idx : 0);
|
|
6479
6497
|
}, [open]);
|
|
6480
6498
|
React53__namespace.default.useEffect(() => {
|
|
@@ -6501,8 +6519,13 @@ function Combobox({
|
|
|
6501
6519
|
}, [active, open, virtualized, rows, listH]);
|
|
6502
6520
|
function openMenu() {
|
|
6503
6521
|
if (disabled) return;
|
|
6504
|
-
|
|
6522
|
+
const seed = defaultQuery || "";
|
|
6523
|
+
setQuery(seed);
|
|
6505
6524
|
setOpen(true);
|
|
6525
|
+
if (seed) requestAnimationFrame(() => {
|
|
6526
|
+
const el = inputRef.current;
|
|
6527
|
+
if (el) el.setSelectionRange(seed.length, seed.length);
|
|
6528
|
+
});
|
|
6506
6529
|
}
|
|
6507
6530
|
function close() {
|
|
6508
6531
|
setOpen(false);
|
|
@@ -10467,7 +10490,7 @@ function Datatable({
|
|
|
10467
10490
|
);
|
|
10468
10491
|
const effectiveHidden = React53__namespace.default.useMemo(() => combineSources.size ? /* @__PURE__ */ new Set([...hidden, ...combineSources]) : hidden, [hidden, combineSources]);
|
|
10469
10492
|
const effectiveWrapped = React53__namespace.default.useMemo(() => stackCombineTargets.size ? /* @__PURE__ */ new Set([...wrapped, ...stackCombineTargets]) : wrapped, [wrapped, stackCombineTargets]);
|
|
10470
|
-
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]);
|
|
10471
10494
|
const colFieldsKey = JSON.stringify(
|
|
10472
10495
|
cols.filter((c) => typeof c.field === "string" && c.type !== "actions").map((c) => c.field)
|
|
10473
10496
|
);
|
|
@@ -11342,12 +11365,18 @@ function Datatable({
|
|
|
11342
11365
|
const revealOn = !!scrollActiveCellIntoView;
|
|
11343
11366
|
const revealBlock = scrollActiveCellIntoView && typeof scrollActiveCellIntoView === "object" && scrollActiveCellIntoView.block === "center" ? "center" : "nearest";
|
|
11344
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;
|
|
11345
11374
|
React53__namespace.default.useEffect(() => {
|
|
11346
11375
|
if (!revealOn || !activeCellVal) return void 0;
|
|
11347
11376
|
const block = revealBlock, inline = revealInline;
|
|
11348
|
-
const ri =
|
|
11377
|
+
const ri = revealRi, ci = revealCi;
|
|
11349
11378
|
if (ri == null || ci < 0) return void 0;
|
|
11350
|
-
let raf = 0;
|
|
11379
|
+
let raf = 0, tries = 0;
|
|
11351
11380
|
const reveal = () => {
|
|
11352
11381
|
const sc = scrollRef.current;
|
|
11353
11382
|
if (!sc) return;
|
|
@@ -11373,10 +11402,12 @@ function Datatable({
|
|
|
11373
11402
|
} else if (tR.left < scR.left + leftInset) sc.scrollLeft -= scR.left + leftInset - tR.left;
|
|
11374
11403
|
else if (tR.right > scR.right - rightInset) sc.scrollLeft += tR.right - (scR.right - rightInset);
|
|
11375
11404
|
}
|
|
11376
|
-
} else if (virtualizing &&
|
|
11377
|
-
const
|
|
11378
|
-
|
|
11379
|
-
|
|
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));
|
|
11380
11411
|
raf = requestAnimationFrame(reveal);
|
|
11381
11412
|
}
|
|
11382
11413
|
}
|
|
@@ -11385,7 +11416,7 @@ function Datatable({
|
|
|
11385
11416
|
return () => {
|
|
11386
11417
|
if (raf) cancelAnimationFrame(raf);
|
|
11387
11418
|
};
|
|
11388
|
-
}, [activeCellVal?.key, activeCellVal?.field, revealOn, revealBlock, revealInline,
|
|
11419
|
+
}, [activeCellVal?.key, activeCellVal?.field, revealOn, revealBlock, revealInline, revealRi, revealCi, virtualizing, headH]);
|
|
11389
11420
|
React53__namespace.default.useEffect(() => {
|
|
11390
11421
|
if (!activeCellControlled || !activeCellVal) {
|
|
11391
11422
|
jumpAppliedRef.current = null;
|
|
@@ -11477,6 +11508,10 @@ function Datatable({
|
|
|
11477
11508
|
[cols, editMode, batchEditKey]
|
|
11478
11509
|
// eslint-disable-line react-hooks/exhaustive-deps
|
|
11479
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
|
+
);
|
|
11480
11515
|
const hasBatchEditor = showBatchEdit && batchEditableCols.length > 0;
|
|
11481
11516
|
const [batchEdit, setBatchEdit] = React53__namespace.default.useState(null);
|
|
11482
11517
|
const [batchEditPos, openBatchEdit, closeBatchEdit] = useFloating();
|
|
@@ -11511,7 +11546,7 @@ function Datatable({
|
|
|
11511
11546
|
}, [batchEdit, selected.size, closeBatchEdit]);
|
|
11512
11547
|
function applyBatchEdit() {
|
|
11513
11548
|
if (!batchEdit) return;
|
|
11514
|
-
const active =
|
|
11549
|
+
const active = batchEditableColsSel.filter((c) => batchEdit.fields[c.field]);
|
|
11515
11550
|
if (!active.length) {
|
|
11516
11551
|
setBatchEdit(null);
|
|
11517
11552
|
closeBatchEdit();
|
|
@@ -11527,20 +11562,27 @@ function Datatable({
|
|
|
11527
11562
|
patch[c.field] = v;
|
|
11528
11563
|
});
|
|
11529
11564
|
const selKeys2 = new Set(selected);
|
|
11565
|
+
const loadedSel = /* @__PURE__ */ new Set();
|
|
11530
11566
|
const changedRows = [];
|
|
11567
|
+
const safeKeys = [];
|
|
11531
11568
|
const nextAll = rows.map((r2, i) => {
|
|
11532
11569
|
const k = keyOf(r2, i);
|
|
11533
11570
|
if (!selKeys2.has(k)) return r2;
|
|
11571
|
+
loadedSel.add(k);
|
|
11534
11572
|
const rowPatch = {};
|
|
11573
|
+
let allEditable = true;
|
|
11535
11574
|
for (const c of active) {
|
|
11536
11575
|
if (typeof c.editable !== "function" || c.editable(r2, { field: c.field })) rowPatch[c.field] = patch[c.field];
|
|
11576
|
+
else allEditable = false;
|
|
11537
11577
|
}
|
|
11578
|
+
if (allEditable) safeKeys.push(k);
|
|
11538
11579
|
if (!Object.keys(rowPatch).length) return r2;
|
|
11539
11580
|
const updated = { ...r2, ...rowPatch };
|
|
11540
11581
|
changedRows.push(updated);
|
|
11541
11582
|
return updated;
|
|
11542
11583
|
});
|
|
11543
|
-
|
|
11584
|
+
for (const k of selKeys2) if (!loadedSel.has(k)) safeKeys.push(k);
|
|
11585
|
+
onBatchUpdate?.(changedRows, patch, safeKeys);
|
|
11544
11586
|
onRowsChange?.(nextAll);
|
|
11545
11587
|
setBatchEdit(null);
|
|
11546
11588
|
closeBatchEdit();
|
|
@@ -13083,8 +13125,8 @@ function Datatable({
|
|
|
13083
13125
|
}
|
|
13084
13126
|
) : null,
|
|
13085
13127
|
batchEdit && batchEditPos ? (() => {
|
|
13086
|
-
const pickedCols = Object.keys(batchEdit.fields).filter((f) => batchEdit.fields[f]).map((f) =>
|
|
13087
|
-
const unpicked =
|
|
13128
|
+
const pickedCols = Object.keys(batchEdit.fields).filter((f) => batchEdit.fields[f]).map((f) => batchEditableColsSel.find((c) => c.field === f)).filter(Boolean);
|
|
13129
|
+
const unpicked = batchEditableColsSel.filter((c) => !batchEdit.fields[c.field]);
|
|
13088
13130
|
const addField = (f) => {
|
|
13089
13131
|
if (f) setBatchEdit((b) => ({ ...b, fields: { ...b.fields, [f]: true }, values: { ...b.values, [f]: b.values[f] ?? "" } }));
|
|
13090
13132
|
};
|
|
@@ -13116,7 +13158,11 @@ function Datatable({
|
|
|
13116
13158
|
onChange: addField
|
|
13117
13159
|
}
|
|
13118
13160
|
) }) : null,
|
|
13119
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-dt__cfg-list", children: pickedCols.length === 0 ?
|
|
13161
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-dt__cfg-list", children: pickedCols.length === 0 ? (
|
|
13162
|
+
// #428: when a predicate leaves no column editable for the current selection, say so instead of
|
|
13163
|
+
// inviting a pick that can't be made (the "Add a column…" picker is hidden when unpicked is empty).
|
|
13164
|
+
/* @__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." })
|
|
13165
|
+
) : pickedCols.map((c, i) => {
|
|
13120
13166
|
const opts = c.valueOptions ? c.valueOptions.map((o) => typeof o === "string" ? { value: o, label: o } : o) : null;
|
|
13121
13167
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "twc-dt__be-row", children: [
|
|
13122
13168
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "twc-dt__be-name", title: colLabel(c), children: [
|