twico-ui 1.43.0 → 1.44.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 CHANGED
@@ -2,6 +2,13 @@
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.44.0](https://github.com/EncryptionL/twico-ui/compare/v1.43.0...v1.44.0) (2026-10-16)
6
+
7
+
8
+ ### Features
9
+
10
+ * **cardgrid,datatable:** selection + batch actions on CardGrid, disabledReason on batch actions ([dac8b30](https://github.com/EncryptionL/twico-ui/commit/dac8b30a36ad3387e09678435803dfaf474e02dd)), closes [#478](https://github.com/EncryptionL/twico-ui/issues/478) [#479](https://github.com/EncryptionL/twico-ui/issues/479) [#322](https://github.com/EncryptionL/twico-ui/issues/322) [399/#419](https://github.com/EncryptionL/twico-ui/issues/419) [pre-#460](https://github.com/pre-/issues/460)
11
+
5
12
  # [1.43.0](https://github.com/EncryptionL/twico-ui/compare/v1.42.1...v1.43.0) (2026-10-07)
6
13
 
7
14
 
package/dist/index.cjs CHANGED
@@ -7248,8 +7248,8 @@ function Currency({
7248
7248
  }
7249
7249
  function useFallbackName(ref, hasOwnName) {
7250
7250
  const [needsFallback, setNeedsFallback] = React54__namespace.default.useState(false);
7251
- const useIso3 = typeof document !== "undefined" ? React54__namespace.default.useLayoutEffect : React54__namespace.default.useEffect;
7252
- useIso3(() => {
7251
+ const useIso2 = typeof document !== "undefined" ? React54__namespace.default.useLayoutEffect : React54__namespace.default.useEffect;
7252
+ useIso2(() => {
7253
7253
  if (hasOwnName) {
7254
7254
  setNeedsFallback(false);
7255
7255
  return;
@@ -7265,7 +7265,6 @@ function useFallbackName(ref, hasOwnName) {
7265
7265
  });
7266
7266
  return !hasOwnName && needsFallback;
7267
7267
  }
7268
- typeof document !== "undefined" ? React54__namespace.default.useLayoutEffect : React54__namespace.default.useEffect;
7269
7268
  var SELECT_CSS = `
7270
7269
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
7271
7270
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
@@ -12894,14 +12893,20 @@ function Datatable({
12894
12893
  const isHidden = typeof a.hidden === "function" ? a.hidden(selKeys, selectedRows) : !!a.hidden;
12895
12894
  if (isHidden) return null;
12896
12895
  const isDisabled = typeof a.disabled === "function" ? a.disabled(selKeys, selectedRows) : !!a.disabled;
12897
- return /* @__PURE__ */ jsxRuntime.jsxs(
12896
+ const reason = isDisabled && a.disabledReason != null ? typeof a.disabledReason === "function" ? a.disabledReason(selKeys, selectedRows) : a.disabledReason : null;
12897
+ const soft = isDisabled && reason != null;
12898
+ const btn = /* @__PURE__ */ jsxRuntime.jsxs(
12898
12899
  "button",
12899
12900
  {
12900
12901
  type: "button",
12901
12902
  className: "twc-dt__batch-btn",
12902
12903
  "data-danger": a.danger || void 0,
12903
- disabled: isDisabled,
12904
- onClick: (e) => a.onClick?.(selKeys, selectedRows, clearSelection, { anchorEl: e.currentTarget }),
12904
+ disabled: isDisabled && !soft,
12905
+ "aria-disabled": soft || void 0,
12906
+ onClick: (e) => {
12907
+ if (isDisabled) return;
12908
+ a.onClick?.(selKeys, selectedRows, clearSelection, { anchorEl: e.currentTarget });
12909
+ },
12905
12910
  children: [
12906
12911
  a.icon,
12907
12912
  a.label
@@ -12909,6 +12914,7 @@ function Datatable({
12909
12914
  },
12910
12915
  i
12911
12916
  );
12917
+ return reason != null ? /* @__PURE__ */ jsxRuntime.jsx(Tooltip, { label: reason, placement: "top", children: btn }, i) : btn;
12912
12918
  })
12913
12919
  ] })
12914
12920
  ] }) : null,
@@ -14339,6 +14345,42 @@ var CARDGRID_CSS = `
14339
14345
  .twc-cardgrid__footer { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2); }
14340
14346
  .twc-cardgrid__count { font-size: var(--text-sm); color: var(--color-text-muted); }
14341
14347
  .twc-cardgrid__perpage { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); color: var(--color-text-muted); }
14348
+
14349
+ /* #478: selection. The card's selected state is a RING, not a background tint: a card is usually a
14350
+ full-bleed thumbnail, so a tint is invisible behind the image. The ring is drawn with box-shadow on
14351
+ a wrapper so it never participates in layout (no reflow when a card becomes selected) and it reads
14352
+ in both themes, --color-primary clearing 3:1 against both surfaces as a non-text indicator. */
14353
+ .twc-cardgrid__item { position: relative; border-radius: var(--radius-lg); transition: box-shadow var(--duration-fast) var(--ease-standard); }
14354
+ .twc-cardgrid__item[data-selected="true"] { box-shadow: 0 0 0 var(--border-medium) var(--color-primary); }
14355
+ /* The overlay is the GRID's checkbox, not the card's: renderCard owns the whole body, so a
14356
+ consumer-rendered checkbox would compete with the card's own click target and have to be positioned
14357
+ against a thumbnail whose size only the consumer knows. Placement is configurable, and a consumer who
14358
+ wants it inline instead uses the third renderCard argument and sets selectionPlacement="none". */
14359
+ .twc-cardgrid__pick { position: absolute; z-index: 2; display: inline-flex; padding: var(--space-1);
14360
+ background: color-mix(in srgb, var(--color-surface) 82%, transparent); border-radius: var(--radius-md);
14361
+ backdrop-filter: blur(4px); }
14362
+ .twc-cardgrid__item[data-place="top-start"] .twc-cardgrid__pick { top: var(--space-2); inset-inline-start: var(--space-2); }
14363
+ .twc-cardgrid__item[data-place="top-end"] .twc-cardgrid__pick { top: var(--space-2); inset-inline-end: var(--space-2); }
14364
+ .twc-cardgrid__item[data-place="bottom-start"] .twc-cardgrid__pick { bottom: var(--space-2); inset-inline-start: var(--space-2); }
14365
+ .twc-cardgrid__item[data-place="bottom-end"] .twc-cardgrid__pick { bottom: var(--space-2); inset-inline-end: var(--space-2); }
14366
+ .twc-cardgrid__batch { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2);
14367
+ padding: var(--space-2) var(--space-3); border: var(--border-thin) solid var(--color-primary-border);
14368
+ background: var(--color-primary-subtle); border-radius: var(--radius-md); }
14369
+ .twc-cardgrid__batch-count { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-primary-subtle-fg); }
14370
+ .twc-cardgrid__batch-actions { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-inline-start: auto; }
14371
+ .twc-cardgrid__batch-btn { display: inline-flex; align-items: center; gap: var(--space-2); height: var(--control-h-sm);
14372
+ padding-inline: var(--space-3); border: var(--border-thin) solid var(--color-border); background: var(--color-surface);
14373
+ color: var(--color-text); font-family: inherit; font-size: var(--text-xs); font-weight: var(--font-semibold);
14374
+ border-radius: var(--radius-md); cursor: pointer; }
14375
+ .twc-cardgrid__batch-btn:hover:not(:disabled):not([aria-disabled="true"]) { border-color: var(--color-primary-subtle-fg); color: var(--color-primary-subtle-fg); }
14376
+ .twc-cardgrid__batch-btn:disabled, .twc-cardgrid__batch-btn[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }
14377
+ .twc-cardgrid__batch-btn[data-danger="true"] { color: var(--color-danger-subtle-fg); }
14378
+ .twc-cardgrid__batch-btn svg { width: 15px; height: 15px; }
14379
+ .twc-cardgrid__batch-x { display: inline-grid; place-items: center; width: 26px; height: 26px; flex: none;
14380
+ border: none; background: transparent; color: var(--color-primary-subtle-fg); cursor: pointer; border-radius: var(--radius-sm); }
14381
+ .twc-cardgrid__batch-x:hover { background: color-mix(in srgb, var(--color-primary) 14%, transparent); }
14382
+ .twc-cardgrid__batch-x svg { width: 15px; height: 15px; }
14383
+ .twc-cardgrid__selall { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); color: var(--color-text-muted); }
14342
14384
  `;
14343
14385
  function CardGrid({
14344
14386
  rows = [],
@@ -14375,6 +14417,16 @@ function CardGrid({
14375
14417
  sortMinWidth,
14376
14418
  // slots / states
14377
14419
  toolbar,
14420
+ // #478: selection + batch actions, deliberately the SAME vocabulary as Datatable rather than a new
14421
+ // one - a card grid is the surface where "pick several of these and do one thing to them" is close to
14422
+ // the whole point, and it was the only Datatable affordance CardGrid lacked.
14423
+ checkboxSelection = false,
14424
+ batchActions = [],
14425
+ onRowSelectionChange,
14426
+ selectionPlacement = "top-start",
14427
+ selectionLabel,
14428
+ showSelectAll = true,
14429
+ retainSelectedRows = false,
14378
14430
  emptyState = "No results.",
14379
14431
  label,
14380
14432
  id,
@@ -14451,6 +14503,62 @@ function CardGrid({
14451
14503
  return () => clearTimeout(t);
14452
14504
  }, [serverMode, queryKey]);
14453
14505
  const keyOf = typeof rowKey === "function" ? rowKey : (r2) => r2 && r2[rowKey];
14506
+ const [selected, setSelected] = React54__namespace.default.useState(() => /* @__PURE__ */ new Set());
14507
+ const retainCap = retainSelectedRows === true ? 1e3 : retainSelectedRows || 0;
14508
+ const retainedRef = React54__namespace.default.useRef(/* @__PURE__ */ new Map());
14509
+ const selKeys = React54__namespace.default.useMemo(() => [...selected], [selected]);
14510
+ const selectedRows = React54__namespace.default.useMemo(() => {
14511
+ const onPage = new Map(pageRows.map((r2, i) => [keyOf(r2, i), r2]));
14512
+ return selKeys.map((k) => onPage.get(k) ?? retainedRef.current.get(k)).filter((r2) => r2 !== void 0);
14513
+ }, [selKeys, pageRows]);
14514
+ const clearSelection = React54__namespace.default.useCallback(() => {
14515
+ retainedRef.current.clear();
14516
+ setSelected(/* @__PURE__ */ new Set());
14517
+ }, []);
14518
+ const toggleKey = React54__namespace.default.useCallback((key, row) => {
14519
+ setSelected((prev) => {
14520
+ const next = new Set(prev);
14521
+ if (next.has(key)) {
14522
+ next.delete(key);
14523
+ retainedRef.current.delete(key);
14524
+ } else {
14525
+ next.add(key);
14526
+ if (retainCap > 0 && row !== void 0 && retainedRef.current.size < retainCap) retainedRef.current.set(key, row);
14527
+ }
14528
+ return next;
14529
+ });
14530
+ }, [retainCap]);
14531
+ const pageKeys = pageRows.map((r2, i) => keyOf(r2, i));
14532
+ const pageSelectedCount = pageKeys.filter((k) => selected.has(k)).length;
14533
+ const allPageSelected = pageKeys.length > 0 && pageSelectedCount === pageKeys.length;
14534
+ const somePageSelected = pageSelectedCount > 0 && !allPageSelected;
14535
+ const togglePage = () => {
14536
+ setSelected((prev) => {
14537
+ const next = new Set(prev);
14538
+ pageRows.forEach((r2, i) => {
14539
+ const k = keyOf(r2, i);
14540
+ if (allPageSelected) {
14541
+ next.delete(k);
14542
+ retainedRef.current.delete(k);
14543
+ } else {
14544
+ next.add(k);
14545
+ if (retainCap > 0 && retainedRef.current.size < retainCap) retainedRef.current.set(k, r2);
14546
+ }
14547
+ });
14548
+ return next;
14549
+ });
14550
+ };
14551
+ const onSelRef = React54__namespace.default.useRef(onRowSelectionChange);
14552
+ onSelRef.current = onRowSelectionChange;
14553
+ const prevSelSigRef = React54__namespace.default.useRef(null);
14554
+ React54__namespace.default.useEffect(() => {
14555
+ const cb = onSelRef.current;
14556
+ if (!cb) return;
14557
+ const sig = selKeys.join(",");
14558
+ if (sig === prevSelSigRef.current) return;
14559
+ prevSelSigRef.current = sig;
14560
+ cb(selKeys, selectedRows);
14561
+ }, [selKeys, selectedRows]);
14454
14562
  const from = total === 0 ? 0 : Math.min(shownPage * sizeVal + 1, total);
14455
14563
  const to = paginated ? Math.min((shownPage + 1) * sizeVal, total) : total;
14456
14564
  const rppOptions = Array.from(new Set([...pageSizeOptions || [], pageSize].filter((n) => n > 0))).sort((a, b) => a - b).map((n) => ({ value: String(n), label: String(n) }));
@@ -14503,6 +14611,64 @@ function CardGrid({
14503
14611
  )
14504
14612
  ] }) : null
14505
14613
  ] }) : null,
14614
+ checkboxSelection && showSelectAll && pageRows.length > 0 && selected.size === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-cardgrid__selall", children: [
14615
+ /* @__PURE__ */ jsxRuntime.jsx(
14616
+ Checkbox,
14617
+ {
14618
+ checked: allPageSelected,
14619
+ indeterminate: somePageSelected,
14620
+ onChange: togglePage,
14621
+ "aria-label": "Select all on this page"
14622
+ }
14623
+ ),
14624
+ "Select all on this page"
14625
+ ] }) : null,
14626
+ checkboxSelection && selected.size > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "twc-cardgrid__batch", children: [
14627
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "twc-cardgrid__batch-x", onClick: clearSelection, "aria-label": "Clear selection", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.4", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M18 6 6 18M6 6l12 12" }) }) }),
14628
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "twc-cardgrid__batch-count", children: [
14629
+ selected.size,
14630
+ " selected"
14631
+ ] }),
14632
+ showSelectAll && pageRows.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-cardgrid__selall", children: [
14633
+ /* @__PURE__ */ jsxRuntime.jsx(
14634
+ Checkbox,
14635
+ {
14636
+ checked: allPageSelected,
14637
+ indeterminate: somePageSelected,
14638
+ onChange: togglePage,
14639
+ "aria-label": "Select all on this page"
14640
+ }
14641
+ ),
14642
+ "This page"
14643
+ ] }) : null,
14644
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-cardgrid__batch-actions", children: batchActions.map((a, i) => {
14645
+ const isHidden = typeof a.hidden === "function" ? a.hidden(selKeys, selectedRows) : !!a.hidden;
14646
+ if (isHidden) return null;
14647
+ const isDisabled = typeof a.disabled === "function" ? a.disabled(selKeys, selectedRows) : !!a.disabled;
14648
+ const reason = isDisabled && a.disabledReason != null ? typeof a.disabledReason === "function" ? a.disabledReason(selKeys, selectedRows) : a.disabledReason : null;
14649
+ const soft = isDisabled && reason != null;
14650
+ const btn = /* @__PURE__ */ jsxRuntime.jsxs(
14651
+ "button",
14652
+ {
14653
+ type: "button",
14654
+ className: "twc-cardgrid__batch-btn",
14655
+ "data-danger": a.danger || void 0,
14656
+ disabled: isDisabled && !soft,
14657
+ "aria-disabled": soft || void 0,
14658
+ onClick: (e) => {
14659
+ if (isDisabled) return;
14660
+ a.onClick?.(selKeys, selectedRows, clearSelection, { anchorEl: e.currentTarget });
14661
+ },
14662
+ children: [
14663
+ a.icon,
14664
+ a.label
14665
+ ]
14666
+ },
14667
+ i
14668
+ );
14669
+ return reason != null ? /* @__PURE__ */ jsxRuntime.jsx(Tooltip, { label: reason, placement: "top", children: btn }, i) : btn;
14670
+ }) })
14671
+ ] }) : null,
14506
14672
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "twc-cardgrid__body", "data-loading": loading || void 0, "aria-busy": loading || void 0, children: [
14507
14673
  loading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-cardgrid__overlay", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-cardgrid__spinner", role: "status", "aria-label": "Loading" }) }) : null,
14508
14674
  pageRows.length === 0 && !loading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-cardgrid__empty", children: emptyState }) : /* @__PURE__ */ jsxRuntime.jsx(
@@ -14514,7 +14680,31 @@ function CardGrid({
14514
14680
  gap,
14515
14681
  role: "list",
14516
14682
  "aria-labelledby": label ? `${fieldId}-label` : void 0,
14517
- children: pageRows.map((row, i) => /* @__PURE__ */ jsxRuntime.jsx("div", { role: "listitem", children: renderCard(row, i) }, keyOf(row, i) ?? i))
14683
+ children: pageRows.map((row, i) => {
14684
+ const k = keyOf(row, i);
14685
+ const isSel = checkboxSelection && selected.has(k);
14686
+ return /* @__PURE__ */ jsxRuntime.jsxs(
14687
+ "div",
14688
+ {
14689
+ role: "listitem",
14690
+ className: "twc-cardgrid__item",
14691
+ "data-selected": isSel || void 0,
14692
+ "data-place": checkboxSelection ? selectionPlacement : void 0,
14693
+ children: [
14694
+ checkboxSelection && selectionPlacement !== "none" ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-cardgrid__pick", children: /* @__PURE__ */ jsxRuntime.jsx(
14695
+ Checkbox,
14696
+ {
14697
+ checked: isSel,
14698
+ onChange: () => toggleKey(k, row),
14699
+ "aria-label": selectionLabel ? selectionLabel(row, i) : "Select item"
14700
+ }
14701
+ ) }) : null,
14702
+ renderCard(row, i, checkboxSelection ? { selected: isSel, toggle: () => toggleKey(k, row) } : void 0)
14703
+ ]
14704
+ },
14705
+ k ?? i
14706
+ );
14707
+ })
14518
14708
  }
14519
14709
  )
14520
14710
  ] }),
@@ -19531,7 +19721,7 @@ function Tag({
19531
19721
  }, "aria-label": _removeLabel, type: "button", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.4", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M18 6 6 18M6 6l12 12" }) }) }) : null
19532
19722
  ] });
19533
19723
  }
19534
- var useIso2 = typeof document !== "undefined" ? React54__namespace.default.useLayoutEffect : React54__namespace.default.useEffect;
19724
+ var useIso = typeof document !== "undefined" ? React54__namespace.default.useLayoutEffect : React54__namespace.default.useEffect;
19535
19725
  var TEXTAREA_CSS = `
19536
19726
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
19537
19727
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
@@ -19630,7 +19820,7 @@ var Textarea = React54__namespace.default.forwardRef(function Textarea2({
19630
19820
  el.style.height = `${next}px`;
19631
19821
  el.style.overflowY = el.scrollHeight + vBorder > maxH ? "auto" : "hidden";
19632
19822
  };
19633
- useIso2(() => {
19823
+ useIso(() => {
19634
19824
  if (autosize && innerRef.current) resize();
19635
19825
  }, [autosize, rest.value, minRows, maxRows, rows]);
19636
19826
  const handleInput = (e) => {