twico-ui 1.30.0 → 1.31.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,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.31.1](https://github.com/EncryptionL/twico-ui/compare/v1.31.0...v1.31.1) (2026-08-04)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **datatable:** make the filter And/Or connector a resizable field (no more truncation) ([15698fd](https://github.com/EncryptionL/twico-ui/commit/15698fdee0191e178cfb0a65cb08775c25d20b91)), closes [#289](https://github.com/EncryptionL/twico-ui/issues/289) [#292](https://github.com/EncryptionL/twico-ui/issues/292)
11
+
12
+ # [1.31.0](https://github.com/EncryptionL/twico-ui/compare/v1.30.0...v1.31.0) (2026-08-04)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **datatable:** keep DT_CSS a plain literal so single-component bundles tree-shake ([9574c2b](https://github.com/EncryptionL/twico-ui/commit/9574c2b52be39bdfcd27241a381b463eb7002c11)), closes [#330](https://github.com/EncryptionL/twico-ui/issues/330)
18
+
19
+
20
+ ### Features
21
+
22
+ * **datatable:** MUI DataGrid-style per-row And/Or filter connector + controllable filterLogic ([3d79a1f](https://github.com/EncryptionL/twico-ui/commit/3d79a1f1c2e8f55d439e1bfeb37c0258b37cf5bc)), closes [#303](https://github.com/EncryptionL/twico-ui/issues/303) [#330](https://github.com/EncryptionL/twico-ui/issues/330)
23
+
5
24
  # [1.30.0](https://github.com/EncryptionL/twico-ui/compare/v1.29.0...v1.30.0) (2026-08-04)
6
25
 
7
26
 
package/dist/index.cjs CHANGED
@@ -8211,6 +8211,7 @@ function classifyDiff(from = [], to = [], columns = [], rowKey, opts = {}) {
8211
8211
  });
8212
8212
  return { rows, counts };
8213
8213
  }
8214
+ var DT_FILTER_PANEL_W = 620;
8214
8215
  var DT_CSS = `
8215
8216
  .twc-dt { display: flex; flex-direction: column; font-family: var(--font-sans); color: var(--color-text);
8216
8217
  border: var(--border-thin) solid var(--color-border); border-radius: var(--radius-lg);
@@ -8633,13 +8634,14 @@ th.twc-dt__rownum .twc-dt__th-inner { padding-inline: 8px; gap: 2px; justify-con
8633
8634
  .twc-dt__panel-count { font-weight: 600; color: var(--color-text-muted); text-transform: none; letter-spacing: 0; margin-inline-start: 6px; font-variant-numeric: tabular-nums; }
8634
8635
  .twc-dt__link { border: none; background: transparent; color: var(--color-primary); font-family: inherit; font-size: var(--text-xs); font-weight: 600; cursor: pointer; padding: 2px 4px; border-radius: var(--radius-sm); }
8635
8636
  .twc-dt__link:hover { background: var(--color-primary-subtle); }
8636
- /* #303: AND/OR connective toggle (segmented pill) in the Filters panel head. */
8637
- .twc-dt__flogic { display: inline-flex; border: var(--border-thin) solid var(--color-border); border-radius: var(--radius-md); overflow: hidden; }
8638
- .twc-dt__flogic-btn { border: none; background: transparent; color: var(--color-text-muted); font-family: inherit; font-size: 10px; font-weight: 700; letter-spacing: var(--tracking-wide); cursor: pointer; padding: 2px 8px; }
8639
- .twc-dt__flogic-btn + .twc-dt__flogic-btn { border-inline-start: var(--border-thin) solid var(--color-border); }
8640
- .twc-dt__flogic-btn:hover { background: var(--color-surface-sunken); }
8641
- .twc-dt__flogic-btn[data-active] { background: var(--color-primary); color: var(--color-primary-fg); }
8642
- .twc-dt__flogic-btn[data-active]:hover { background: var(--color-primary); }
8637
+ /* #330: MUI DataGrid-style per-row And/Or connector. Row 1 shows a static "Where"; the first connector
8638
+ row (row 2) hosts an editable And/Or Select bound to the single filterLogic; rows 3+ echo it as static
8639
+ text. Leading cell aligns the Column/Operator/Value cells across rows. It is a first-class resizable
8640
+ filter field like col/op: auto-fit to its content (-fit, from the #289 measurer) with a user drag tier
8641
+ (-usr) winning in the clamp cascade, so it no longer truncates and the width can be adjusted manually. */
8642
+ .twc-dt__f-logic { position: relative; flex: none; min-width: 84px;
8643
+ width: clamp(84px, var(--twc-dt-flogic-usr, var(--twc-dt-flogic-fit, 84px)), var(--twc-dt-flogic-cap, 180px)); }
8644
+ .twc-dt__f-where { display: inline-flex; align-items: center; height: 38px; padding-inline: 6px; color: var(--color-text-muted); font-size: var(--text-sm); font-weight: var(--font-medium); }
8643
8645
 
8644
8646
  /* Filter panel \u2014 user-resizable (#292): panel size + per-field widths resolve through a two-tier CSS var
8645
8647
  cascade \u2014 user drag (-usr) > #289 measured auto-fit (-fit) > 118px fallback \u2014 entirely in clamp(), so
@@ -8649,7 +8651,7 @@ th.twc-dt__rownum .twc-dt__th-inner { padding-inline: 8px; gap: 2px; justify-con
8649
8651
  equal-specificity 'position: relative' defined later would clobber it, dropping the popover into normal
8650
8652
  flow below the table. The panel stays position: fixed, which already anchors the absolutely-positioned
8651
8653
  .twc-dt__pop-grip; per-field handles anchor to their own position: relative .twc-dt__f-* fields. */
8652
- width: var(--twc-dt-panel-w, 580px); max-width: calc(100vw - 32px);
8654
+ width: var(--twc-dt-panel-w, 620px); max-width: calc(100vw - 32px);
8653
8655
  height: var(--twc-dt-panel-h, auto); max-height: calc(100vh - 32px);
8654
8656
  display: flex; flex-direction: column;
8655
8657
  }
@@ -9261,6 +9263,9 @@ function Datatable({
9261
9263
  searchable = true,
9262
9264
  quickFilter,
9263
9265
  onQuickFilterChange,
9266
+ filterLogic: filterLogicProp,
9267
+ defaultFilterLogic = "and",
9268
+ onFilterLogicChange,
9264
9269
  toolbarActions,
9265
9270
  resizableFilters = true,
9266
9271
  resizablePopovers = true,
@@ -9354,7 +9359,14 @@ function Datatable({
9354
9359
  });
9355
9360
  const [sort, setSort] = React51__namespace.default.useState(null);
9356
9361
  const [filters, setFilters] = React51__namespace.default.useState([]);
9357
- const [filterLogic, setFilterLogic] = React51__namespace.default.useState("and");
9362
+ const filterLogicControlled = filterLogicProp !== void 0;
9363
+ const [filterLogicInternal, setFilterLogicInternal] = React51__namespace.default.useState(defaultFilterLogic === "or" ? "or" : "and");
9364
+ const filterLogic = filterLogicControlled ? filterLogicProp === "or" ? "or" : "and" : filterLogicInternal;
9365
+ const setFilterLogic = (next) => {
9366
+ const v = next === "or" ? "or" : "and";
9367
+ if (!filterLogicControlled) setFilterLogicInternal(v);
9368
+ onFilterLogicChange?.(v);
9369
+ };
9358
9370
  const [hidden, setHidden] = React51__namespace.default.useState(() => /* @__PURE__ */ new Set());
9359
9371
  const [showRowNum, setShowRowNum] = React51__namespace.default.useState(rowNumbers);
9360
9372
  const [wrapped, setWrapped] = React51__namespace.default.useState(() => new Set((columns || []).filter((c) => c.wrapText).map((c) => c.field)));
@@ -9442,7 +9454,7 @@ function Datatable({
9442
9454
  density,
9443
9455
  // #292: additive, optional — user-resized filter panel + field widths (panel-global, not per column).
9444
9456
  filterPanelSize: fPanelSize || void 0,
9445
- filterFieldWidths: fWidths.col != null || fWidths.op != null || fWidths.val != null ? fWidths : void 0,
9457
+ filterFieldWidths: fWidths.col != null || fWidths.op != null || fWidths.val != null || fWidths.logic != null ? fWidths : void 0,
9446
9458
  // #304: additive, optional — user-resized sizes of the other toolbar popovers, keyed by popover id.
9447
9459
  popoverSizes: Object.keys(popSizes).length ? popSizes : void 0
9448
9460
  });
@@ -9450,7 +9462,7 @@ function Datatable({
9450
9462
  if (!s || typeof s !== "object") return;
9451
9463
  const known = new Set(cols.map((c) => c.field).filter(Boolean));
9452
9464
  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 })));
9453
- if (s.filterLogic === "or" || s.filterLogic === "and") setFilterLogic(s.filterLogic);
9465
+ if ((s.filterLogic === "or" || s.filterLogic === "and") && !filterLogicControlled) setFilterLogicInternal(s.filterLogic);
9454
9466
  if ("sort" in s) setSort(s.sort && known.has(s.sort.field) ? { field: s.sort.field, dir: s.sort.dir === "desc" ? "desc" : "asc" } : null);
9455
9467
  if (typeof s.quickFilter === "string" && quickFilter === void 0) setInternalQuick(s.quickFilter);
9456
9468
  if (typeof s.page === "number" && !pageControlled) setInternalPage(Math.max(0, Math.floor(s.page)));
@@ -9472,7 +9484,7 @@ function Datatable({
9472
9484
  if (s.density === "compact" || s.density === "standard" || s.density === "comfortable") setDensity(s.density);
9473
9485
  if (s.filterFieldWidths && typeof s.filterFieldWidths === "object") {
9474
9486
  const fw = {};
9475
- for (const k of ["col", "op", "val"]) if (typeof s.filterFieldWidths[k] === "number") fw[k] = s.filterFieldWidths[k];
9487
+ for (const k of ["col", "op", "val", "logic"]) if (typeof s.filterFieldWidths[k] === "number") fw[k] = s.filterFieldWidths[k];
9476
9488
  if (Object.keys(fw).length) setFWidths(fw);
9477
9489
  }
9478
9490
  if (s.filterPanelSize && typeof s.filterPanelSize === "object") {
@@ -10843,9 +10855,11 @@ function Datatable({
10843
10855
  const pad2 = trigger ? (parseFloat(cs.paddingLeft) || 0) + (parseFloat(cs.paddingRight) || 0) : 24;
10844
10856
  const chrome = pad2 + 8 + 16 + 4;
10845
10857
  const widest = (labels) => labels.reduce((m, l) => Math.max(m, ctx.measureText(String(l)).width), 0);
10846
- const colCap = Math.max(118, Math.min(210, el.clientWidth - 118 - 30 - 140 - 3 * 6 - 2 * 4));
10858
+ const logicW = Math.min(180, Math.max(84, Math.ceil(widest(["And", "Or"]) + chrome)));
10859
+ const colCap = Math.max(118, Math.min(210, el.clientWidth - logicW - 118 - 30 - 30 - 140 - 5 * 6 - 2 * 4));
10847
10860
  const colW = Math.min(colCap, Math.max(118, Math.ceil(widest(filterableCols.map((c) => c.headerName ?? c.field)) + chrome)));
10848
10861
  const opW = Math.min(170, Math.max(118, Math.ceil(widest(ALL_OP_LABELS) + chrome)));
10862
+ if (Number.isFinite(logicW)) el.style.setProperty("--twc-dt-flogic-fit", `${logicW}px`);
10849
10863
  if (Number.isFinite(colW)) el.style.setProperty("--twc-dt-fcol-fit", `${colW}px`);
10850
10864
  if (Number.isFinite(opW)) el.style.setProperty("--twc-dt-fop-fit", `${opW}px`);
10851
10865
  };
@@ -10859,10 +10873,10 @@ function Datatable({
10859
10873
  }
10860
10874
  return () => ro?.disconnect();
10861
10875
  }, [panel, filterableCols, filters.length]);
10862
- const F_MIN = { col: 118, op: 118, val: 140 };
10863
- const F_VAR = { col: "--twc-dt-fcol-usr", op: "--twc-dt-fop-usr", val: "--twc-dt-fval-usr" };
10864
- const F_SEL = { col: ".twc-dt__f-col", op: ".twc-dt__f-op", val: ".twc-dt__f-val" };
10865
- const F_LABEL = { col: "column", op: "operator", val: "value" };
10876
+ const F_MIN = { col: 118, op: 118, val: 140, logic: 84 };
10877
+ const F_VAR = { col: "--twc-dt-fcol-usr", op: "--twc-dt-fop-usr", val: "--twc-dt-fval-usr", logic: "--twc-dt-flogic-usr" };
10878
+ const F_SEL = { col: ".twc-dt__f-col", op: ".twc-dt__f-op", val: ".twc-dt__f-val", logic: ".twc-dt__f-logic" };
10879
+ const F_LABEL = { col: "column", op: "operator", val: "value", logic: "connector" };
10866
10880
  const fMaxW = () => typeof window === "undefined" ? 9999 : window.innerWidth - 32;
10867
10881
  const fMaxH = () => typeof window === "undefined" ? 9999 : window.innerHeight - 32;
10868
10882
  const clampNum = (v, lo, hi) => Math.max(lo, Math.min(hi, Math.round(v)));
@@ -10871,11 +10885,12 @@ function Datatable({
10871
10885
  return el ? el.getBoundingClientRect().width : F_MIN[field];
10872
10886
  };
10873
10887
  const fCap = (field) => {
10888
+ if (field === "logic") return 180;
10874
10889
  if (field === "col") return filterFieldMaxWidth || 360;
10875
10890
  if (field === "op") return filterFieldMaxWidth || 260;
10876
10891
  const p = filterPanelRef.current;
10877
10892
  if (!p) return 640;
10878
- return clampNum(p.clientWidth - fLiveW("col") - fLiveW("op") - 30 - 24, F_MIN.val, 640);
10893
+ return clampNum(p.clientWidth - fLiveW("logic") - fLiveW("col") - fLiveW("op") - 30 - 30 - 5 * 6 - 2 * 4, F_MIN.val, 640);
10879
10894
  };
10880
10895
  const isRtl = () => {
10881
10896
  const p = filterPanelRef.current;
@@ -11008,12 +11023,12 @@ function Datatable({
11008
11023
  setFPanelSize(null);
11009
11024
  const p = filterPanelRef.current;
11010
11025
  if (p) {
11011
- ["--twc-dt-fcol-usr", "--twc-dt-fop-usr", "--twc-dt-fval-usr", "--twc-dt-panel-w", "--twc-dt-panel-h"].forEach((v) => p.style.removeProperty(v));
11026
+ ["--twc-dt-fcol-usr", "--twc-dt-fop-usr", "--twc-dt-fval-usr", "--twc-dt-flogic-usr", "--twc-dt-panel-w", "--twc-dt-panel-h"].forEach((v) => p.style.removeProperty(v));
11012
11027
  p.removeAttribute("data-val-fixed");
11013
11028
  p.removeAttribute("data-panel-sized");
11014
11029
  }
11015
11030
  };
11016
- const hasFilterSizeOverride = fWidths.col != null || fWidths.op != null || fWidths.val != null || fPanelSize != null;
11031
+ const hasFilterSizeOverride = fWidths.col != null || fWidths.op != null || fWidths.val != null || fWidths.logic != null || fPanelSize != null;
11017
11032
  const POP_MIN_W = { columns: 240, agg: 240, pivot: 260, batchedit: 260 };
11018
11033
  const popMinW = (id) => POP_MIN_W[id] || 200;
11019
11034
  const clearPop = (id, node) => {
@@ -11572,7 +11587,7 @@ function Datatable({
11572
11587
  } else {
11573
11588
  setPanel("filters");
11574
11589
  setColMenu(null);
11575
- openPanel(e.currentTarget, "left", 580);
11590
+ openPanel(e.currentTarget, "left", DT_FILTER_PANEL_W);
11576
11591
  }
11577
11592
  },
11578
11593
  children: [
@@ -12140,7 +12155,7 @@ function Datatable({
12140
12155
  closeMenu();
12141
12156
  restoreTriggerFocus();
12142
12157
  setPanel("filters");
12143
- openPanel(document.querySelector(".twc-dt__toolbar .twc-dt__tbtn:nth-child(2)"), "left", 580);
12158
+ openPanel(document.querySelector(".twc-dt__toolbar .twc-dt__tbtn:nth-child(2)"), "left", DT_FILTER_PANEL_W);
12144
12159
  }, children: [
12145
12160
  /* @__PURE__ */ jsxRuntime.jsx(Svg, { d: I.filter }),
12146
12161
  " Filter"
@@ -12563,6 +12578,7 @@ function Datatable({
12563
12578
  "--twc-dt-fcol-usr": fWidths.col != null ? `${fWidths.col}px` : void 0,
12564
12579
  "--twc-dt-fop-usr": fWidths.op != null ? `${fWidths.op}px` : void 0,
12565
12580
  "--twc-dt-fval-usr": fWidths.val != null ? `${fWidths.val}px` : void 0,
12581
+ "--twc-dt-flogic-usr": fWidths.logic != null ? `${fWidths.logic}px` : void 0,
12566
12582
  "--twc-dt-panel-w": fPanelSize?.w != null ? `${fPanelSize.w}px` : void 0,
12567
12583
  "--twc-dt-panel-h": fPanelSize?.h != null ? `${fPanelSize.h}px` : void 0,
12568
12584
  "--twc-dt-fcol-cap": filterFieldMaxWidth ? `${filterFieldMaxWidth}px` : void 0,
@@ -12574,10 +12590,6 @@ function Datatable({
12574
12590
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "twc-dt__panel-head", children: [
12575
12591
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-dt__panel-title", children: "Filters" }),
12576
12592
  /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: 6 }, children: [
12577
- filters.length > 1 ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "twc-dt__flogic", role: "group", "aria-label": "Combine filters with", children: [
12578
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "twc-dt__flogic-btn", "data-active": filterLogic === "and" || void 0, "aria-pressed": filterLogic === "and", onClick: () => setFilterLogic("and"), children: "AND" }),
12579
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "twc-dt__flogic-btn", "data-active": filterLogic === "or" || void 0, "aria-pressed": filterLogic === "or", onClick: () => setFilterLogic("or"), children: "OR" })
12580
- ] }) : null,
12581
12593
  resizableFilters && hasFilterSizeOverride ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "twc-dt__link", onClick: resetFilterSizes, children: "Reset sizes" }) : null,
12582
12594
  filters.length ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "twc-dt__link", onClick: () => setFilters([]), children: "Clear all" }) : null
12583
12595
  ] })
@@ -12587,6 +12599,20 @@ function Datatable({
12587
12599
  const ops = opsFor(filterTypeOf(col));
12588
12600
  const op = ops.find((o) => o.value === f.op) || ops[0];
12589
12601
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "twc-dt__frow", children: [
12602
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "twc-dt__f-logic", children: [
12603
+ i === 0 ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-dt__f-where", children: "Where" }) : i === 1 ? /* @__PURE__ */ jsxRuntime.jsx(
12604
+ Select,
12605
+ {
12606
+ size: "sm",
12607
+ portal: true,
12608
+ "aria-label": "Combine with the other filters",
12609
+ value: filterLogic,
12610
+ options: [{ value: "and", label: "And" }, { value: "or", label: "Or" }],
12611
+ onChange: (v) => setFilterLogic(v)
12612
+ }
12613
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-dt__f-where", children: filterLogic === "or" ? "Or" : "And" }),
12614
+ fieldHandle("logic", i)
12615
+ ] }),
12590
12616
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "twc-dt__f-col", children: [
12591
12617
  /* @__PURE__ */ jsxRuntime.jsx(
12592
12618
  Select,
@@ -12666,7 +12692,7 @@ function Datatable({
12666
12692
  role: "slider",
12667
12693
  tabIndex: 0,
12668
12694
  "aria-label": "Resize filters panel (Enter to reset)",
12669
- "aria-valuetext": `Width ${Math.round(fPanelSize?.w ?? 580)} pixels, height ${fPanelSize?.h != null ? Math.round(fPanelSize.h) + " pixels" : "auto"}`,
12695
+ "aria-valuetext": `Width ${Math.round(fPanelSize?.w ?? DT_FILTER_PANEL_W)} pixels, height ${fPanelSize?.h != null ? Math.round(fPanelSize.h) + " pixels" : "auto"}`,
12670
12696
  onPointerDown: startPanelResize,
12671
12697
  onKeyDown: onPanelGripKey,
12672
12698
  onDoubleClick: (e) => {