twico-ui 1.30.0 → 1.31.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,18 @@
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.0](https://github.com/EncryptionL/twico-ui/compare/v1.30.0...v1.31.0) (2026-08-04)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **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)
11
+
12
+
13
+ ### Features
14
+
15
+ * **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)
16
+
5
17
  # [1.30.0](https://github.com/EncryptionL/twico-ui/compare/v1.29.0...v1.30.0) (2026-08-04)
6
18
 
7
19
 
package/dist/index.cjs CHANGED
@@ -8211,6 +8211,8 @@ function classifyDiff(from = [], to = [], columns = [], rowKey, opts = {}) {
8211
8211
  });
8212
8212
  return { rows, counts };
8213
8213
  }
8214
+ var DT_FILTER_PANEL_W = 620;
8215
+ var DT_FILTER_LOGIC_W = 68;
8214
8216
  var DT_CSS = `
8215
8217
  .twc-dt { display: flex; flex-direction: column; font-family: var(--font-sans); color: var(--color-text);
8216
8218
  border: var(--border-thin) solid var(--color-border); border-radius: var(--radius-lg);
@@ -8633,13 +8635,11 @@ th.twc-dt__rownum .twc-dt__th-inner { padding-inline: 8px; gap: 2px; justify-con
8633
8635
  .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
8636
  .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
8637
  .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); }
8638
+ /* #330: MUI DataGrid-style per-row And/Or connector. Row 1 shows a static "Where"; the first connector
8639
+ row (row 2) hosts an editable And/Or Select bound to the single filterLogic; rows 3+ echo it as static
8640
+ text. Fixed-width leading cell so the Column/Operator/Value cells stay aligned across rows. */
8641
+ .twc-dt__f-logic { position: relative; flex: none; width: 68px; }
8642
+ .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
8643
 
8644
8644
  /* Filter panel \u2014 user-resizable (#292): panel size + per-field widths resolve through a two-tier CSS var
8645
8645
  cascade \u2014 user drag (-usr) > #289 measured auto-fit (-fit) > 118px fallback \u2014 entirely in clamp(), so
@@ -8649,7 +8649,7 @@ th.twc-dt__rownum .twc-dt__th-inner { padding-inline: 8px; gap: 2px; justify-con
8649
8649
  equal-specificity 'position: relative' defined later would clobber it, dropping the popover into normal
8650
8650
  flow below the table. The panel stays position: fixed, which already anchors the absolutely-positioned
8651
8651
  .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);
8652
+ width: var(--twc-dt-panel-w, 620px); max-width: calc(100vw - 32px);
8653
8653
  height: var(--twc-dt-panel-h, auto); max-height: calc(100vh - 32px);
8654
8654
  display: flex; flex-direction: column;
8655
8655
  }
@@ -9261,6 +9261,9 @@ function Datatable({
9261
9261
  searchable = true,
9262
9262
  quickFilter,
9263
9263
  onQuickFilterChange,
9264
+ filterLogic: filterLogicProp,
9265
+ defaultFilterLogic = "and",
9266
+ onFilterLogicChange,
9264
9267
  toolbarActions,
9265
9268
  resizableFilters = true,
9266
9269
  resizablePopovers = true,
@@ -9354,7 +9357,14 @@ function Datatable({
9354
9357
  });
9355
9358
  const [sort, setSort] = React51__namespace.default.useState(null);
9356
9359
  const [filters, setFilters] = React51__namespace.default.useState([]);
9357
- const [filterLogic, setFilterLogic] = React51__namespace.default.useState("and");
9360
+ const filterLogicControlled = filterLogicProp !== void 0;
9361
+ const [filterLogicInternal, setFilterLogicInternal] = React51__namespace.default.useState(defaultFilterLogic === "or" ? "or" : "and");
9362
+ const filterLogic = filterLogicControlled ? filterLogicProp === "or" ? "or" : "and" : filterLogicInternal;
9363
+ const setFilterLogic = (next) => {
9364
+ const v = next === "or" ? "or" : "and";
9365
+ if (!filterLogicControlled) setFilterLogicInternal(v);
9366
+ onFilterLogicChange?.(v);
9367
+ };
9358
9368
  const [hidden, setHidden] = React51__namespace.default.useState(() => /* @__PURE__ */ new Set());
9359
9369
  const [showRowNum, setShowRowNum] = React51__namespace.default.useState(rowNumbers);
9360
9370
  const [wrapped, setWrapped] = React51__namespace.default.useState(() => new Set((columns || []).filter((c) => c.wrapText).map((c) => c.field)));
@@ -9450,7 +9460,7 @@ function Datatable({
9450
9460
  if (!s || typeof s !== "object") return;
9451
9461
  const known = new Set(cols.map((c) => c.field).filter(Boolean));
9452
9462
  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);
9463
+ if ((s.filterLogic === "or" || s.filterLogic === "and") && !filterLogicControlled) setFilterLogicInternal(s.filterLogic);
9454
9464
  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
9465
  if (typeof s.quickFilter === "string" && quickFilter === void 0) setInternalQuick(s.quickFilter);
9456
9466
  if (typeof s.page === "number" && !pageControlled) setInternalPage(Math.max(0, Math.floor(s.page)));
@@ -10843,7 +10853,7 @@ function Datatable({
10843
10853
  const pad2 = trigger ? (parseFloat(cs.paddingLeft) || 0) + (parseFloat(cs.paddingRight) || 0) : 24;
10844
10854
  const chrome = pad2 + 8 + 16 + 4;
10845
10855
  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));
10856
+ const colCap = Math.max(118, Math.min(210, el.clientWidth - DT_FILTER_LOGIC_W - 118 - 30 - 30 - 140 - 5 * 6 - 2 * 4));
10847
10857
  const colW = Math.min(colCap, Math.max(118, Math.ceil(widest(filterableCols.map((c) => c.headerName ?? c.field)) + chrome)));
10848
10858
  const opW = Math.min(170, Math.max(118, Math.ceil(widest(ALL_OP_LABELS) + chrome)));
10849
10859
  if (Number.isFinite(colW)) el.style.setProperty("--twc-dt-fcol-fit", `${colW}px`);
@@ -10875,7 +10885,7 @@ function Datatable({
10875
10885
  if (field === "op") return filterFieldMaxWidth || 260;
10876
10886
  const p = filterPanelRef.current;
10877
10887
  if (!p) return 640;
10878
- return clampNum(p.clientWidth - fLiveW("col") - fLiveW("op") - 30 - 24, F_MIN.val, 640);
10888
+ return clampNum(p.clientWidth - DT_FILTER_LOGIC_W - fLiveW("col") - fLiveW("op") - 30 - 30 - 5 * 6 - 2 * 4, F_MIN.val, 640);
10879
10889
  };
10880
10890
  const isRtl = () => {
10881
10891
  const p = filterPanelRef.current;
@@ -11572,7 +11582,7 @@ function Datatable({
11572
11582
  } else {
11573
11583
  setPanel("filters");
11574
11584
  setColMenu(null);
11575
- openPanel(e.currentTarget, "left", 580);
11585
+ openPanel(e.currentTarget, "left", DT_FILTER_PANEL_W);
11576
11586
  }
11577
11587
  },
11578
11588
  children: [
@@ -12140,7 +12150,7 @@ function Datatable({
12140
12150
  closeMenu();
12141
12151
  restoreTriggerFocus();
12142
12152
  setPanel("filters");
12143
- openPanel(document.querySelector(".twc-dt__toolbar .twc-dt__tbtn:nth-child(2)"), "left", 580);
12153
+ openPanel(document.querySelector(".twc-dt__toolbar .twc-dt__tbtn:nth-child(2)"), "left", DT_FILTER_PANEL_W);
12144
12154
  }, children: [
12145
12155
  /* @__PURE__ */ jsxRuntime.jsx(Svg, { d: I.filter }),
12146
12156
  " Filter"
@@ -12574,10 +12584,6 @@ function Datatable({
12574
12584
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "twc-dt__panel-head", children: [
12575
12585
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-dt__panel-title", children: "Filters" }),
12576
12586
  /* @__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
12587
  resizableFilters && hasFilterSizeOverride ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "twc-dt__link", onClick: resetFilterSizes, children: "Reset sizes" }) : null,
12582
12588
  filters.length ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "twc-dt__link", onClick: () => setFilters([]), children: "Clear all" }) : null
12583
12589
  ] })
@@ -12587,6 +12593,17 @@ function Datatable({
12587
12593
  const ops = opsFor(filterTypeOf(col));
12588
12594
  const op = ops.find((o) => o.value === f.op) || ops[0];
12589
12595
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "twc-dt__frow", children: [
12596
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-dt__f-logic", children: i === 0 ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-dt__f-where", children: "Where" }) : i === 1 ? /* @__PURE__ */ jsxRuntime.jsx(
12597
+ Select,
12598
+ {
12599
+ size: "sm",
12600
+ portal: true,
12601
+ "aria-label": "Combine with the other filters",
12602
+ value: filterLogic,
12603
+ options: [{ value: "and", label: "And" }, { value: "or", label: "Or" }],
12604
+ onChange: (v) => setFilterLogic(v)
12605
+ }
12606
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-dt__f-where", children: filterLogic === "or" ? "Or" : "And" }) }),
12590
12607
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "twc-dt__f-col", children: [
12591
12608
  /* @__PURE__ */ jsxRuntime.jsx(
12592
12609
  Select,
@@ -12666,7 +12683,7 @@ function Datatable({
12666
12683
  role: "slider",
12667
12684
  tabIndex: 0,
12668
12685
  "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"}`,
12686
+ "aria-valuetext": `Width ${Math.round(fPanelSize?.w ?? DT_FILTER_PANEL_W)} pixels, height ${fPanelSize?.h != null ? Math.round(fPanelSize.h) + " pixels" : "auto"}`,
12670
12687
  onPointerDown: startPanelResize,
12671
12688
  onKeyDown: onPanelGripKey,
12672
12689
  onDoubleClick: (e) => {