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 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
- .twc-iconbtn[aria-pressed="true"][data-pressed-tone="neutral"] { --_accent: var(--color-border-strong); --_accent-subtle: var(--color-surface-sunken); --_accent-subtle-fg: var(--color-text); }
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
- .twc-btn[aria-pressed="true"][data-pressed-tone="neutral"] { --_accent: var(--color-border-strong); --_accent-subtle: var(--color-surface-sunken); --_accent-subtle-fg: var(--color-text); }
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 = flat.findIndex((o) => o.value === current);
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
- setQuery("");
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 = keyIndex.get(activeCellVal.key), ci = cellColIndex(activeCellVal.field);
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 && offsets) {
11377
- const mi = keyIndexMid.get(activeCellVal.key);
11378
- if (mi != null && offsets[mi] != null) {
11379
- sc.scrollTop = Math.max(0, offsets[mi] - (block === "center" ? sc.clientHeight / 2 : headH));
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, keyIndex, cellColIndex, virtualizing, offsets, keyIndexMid, headH]);
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 = batchEditableCols.filter((c) => batchEdit.fields[c.field]);
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
- onBatchUpdate?.(changedRows, patch, [...selected]);
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) => batchEditableCols.find((c) => c.field === f)).filter(Boolean);
13087
- const unpicked = batchEditableCols.filter((c) => !batchEdit.fields[c.field]);
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 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-dt__be-empty", children: "Pick a column to set its value on all selected rows." }) : pickedCols.map((c, i) => {
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: [