twico-ui 1.37.1 → 1.38.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,21 @@
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.38.0](https://github.com/EncryptionL/twico-ui/compare/v1.37.2...v1.38.0) (2026-09-22)
6
+
7
+
8
+ ### Features
9
+
10
+ * **datatable,tooltip:** toolbar/group-bar controls + rich tooltip labels ([#385](https://github.com/EncryptionL/twico-ui/issues/385), [#386](https://github.com/EncryptionL/twico-ui/issues/386), [#387](https://github.com/EncryptionL/twico-ui/issues/387)) ([a89fb1f](https://github.com/EncryptionL/twico-ui/commit/a89fb1f4885bc51f2383b4c3437eb502aa574a37))
11
+
12
+ ## [1.37.2](https://github.com/EncryptionL/twico-ui/compare/v1.37.1...v1.37.2) (2026-09-18)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **datatable,code:** clipboard paste/copy work outside a secure context ([#380](https://github.com/EncryptionL/twico-ui/issues/380)) ([29aa913](https://github.com/EncryptionL/twico-ui/commit/29aa91382df6df40333ec49132b8883ec6f56b9f)), closes [#343](https://github.com/EncryptionL/twico-ui/issues/343)
18
+ * **datatable,code:** harden [#380](https://github.com/EncryptionL/twico-ui/issues/380) clipboard fix (review follow-ups) ([849c9f8](https://github.com/EncryptionL/twico-ui/commit/849c9f88d331e38d5fd1faf41401b8424e904541))
19
+
5
20
  ## [1.37.1](https://github.com/EncryptionL/twico-ui/compare/v1.37.0...v1.37.1) (2026-09-14)
6
21
 
7
22
 
package/dist/index.cjs CHANGED
@@ -7979,7 +7979,11 @@ var TOOLTIP_CSS = `
7979
7979
  (opacity:0) nor a shown bubble can block neighbouring cells/buttons (matches MUI/Radix tooltips). */
7980
7980
  pointer-events: none;
7981
7981
  padding: 6px 10px; border-radius: var(--radius-md);
7982
- background: var(--color-text); color: var(--color-surface);
7982
+ /* #385: capture the ink as the bubble bg via a private var BEFORE the content wrapper re-scopes
7983
+ --color-text, so a token-driven child (Text/Heading) reads the bubble's fg \u2014 not the page ink,
7984
+ which is this very background \u2014 and the re-scope can't feed back into the bg. */
7985
+ --_tt-bg: var(--color-text); --_tt-fg: var(--color-surface);
7986
+ background: var(--_tt-bg); color: var(--_tt-fg);
7983
7987
  font-family: var(--font-sans); font-size: var(--text-xs); font-weight: var(--font-medium);
7984
7988
  line-height: 1.3; white-space: normal;
7985
7989
  /* Grow horizontally to max-width, then wrap. max-content sizes to the content (capped by
@@ -7995,7 +7999,13 @@ var TOOLTIP_CSS = `
7995
7999
  .twc-tooltip[data-place="left"] { transform-origin: right center; translate: 0 -50%; }
7996
8000
  .twc-tooltip[data-place="right"] { transform-origin: left center; translate: 0 -50%; }
7997
8001
  .twc-tooltip[data-show="true"][data-place="top"], .twc-tooltip[data-show="true"][data-place="bottom"] { transform: scale(1); }
7998
- .twc-tooltip__arrow { position: absolute; width: 7px; height: 7px; background: var(--color-text); transform: rotate(45deg); }
8002
+ .twc-tooltip__arrow { position: absolute; width: 7px; height: 7px; background: var(--_tt-bg); transform: rotate(45deg); }
8003
+ /* #385: re-scope token ink for the label subtree so Text/Heading (which set --color-text as an inline
8004
+ style) render in the bubble's fg instead of the page ink. display:contents adds no box, so the label's
8005
+ own flex/block layout is unchanged; custom properties still inherit through it. */
8006
+ .twc-tooltip__content { display: contents; --color-text: var(--_tt-fg);
8007
+ --color-text-muted: color-mix(in srgb, var(--_tt-fg) 72%, var(--_tt-bg));
8008
+ --color-text-subtle: color-mix(in srgb, var(--_tt-fg) 55%, var(--_tt-bg)); }
7999
8009
  /* Arrow tracks the trigger via --_tw-arrow-x/y (px from the tooltip edge) instead of being
8000
8010
  locked to the centre, so it still points at the trigger when the bubble is clamped near a
8001
8011
  viewport edge. Falls back to 50% before measurement / SSR. */
@@ -8120,7 +8130,7 @@ function Tooltip({
8120
8130
  "--_tw-arrow-y": coords.arrowY != null ? `${coords.arrowY}px` : void 0
8121
8131
  },
8122
8132
  children: [
8123
- label,
8133
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-tooltip__content", children: label }),
8124
8134
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-tooltip__arrow", "aria-hidden": "true" })
8125
8135
  ]
8126
8136
  }
@@ -9300,6 +9310,8 @@ function Datatable({
9300
9310
  showExport = false,
9301
9311
  showDensity = false,
9302
9312
  showPivot = false,
9313
+ showColumns,
9314
+ showFilters,
9303
9315
  exportFilename = "export",
9304
9316
  aggregationValues = null,
9305
9317
  disableColumnReorder = false,
@@ -9331,6 +9343,8 @@ function Datatable({
9331
9343
  ariaLabel = "Data table",
9332
9344
  "aria-label": ariaLabelAttr,
9333
9345
  rowGrouping = [],
9346
+ showGroupBar = true,
9347
+ renderGroupLabel,
9334
9348
  rowNumbers = false,
9335
9349
  searchFields = null,
9336
9350
  searchable = true,
@@ -10667,8 +10681,10 @@ function Datatable({
10667
10681
  return;
10668
10682
  }
10669
10683
  if (k === "v") {
10670
- e.preventDefault();
10671
- pasteSelection();
10684
+ if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.readText) {
10685
+ e.preventDefault();
10686
+ pasteSelection();
10687
+ }
10672
10688
  return;
10673
10689
  }
10674
10690
  }
@@ -10888,19 +10904,19 @@ function Datatable({
10888
10904
  const copyTypeOf = (col) => col && col.copyType != null ? col.copyType : col && col.type === "number" ? "number" : "text";
10889
10905
  const writeClipboard = (text) => {
10890
10906
  const execFallback = () => {
10907
+ if (typeof document === "undefined") return;
10908
+ const ta = document.createElement("textarea");
10909
+ ta.value = text;
10910
+ ta.style.position = "fixed";
10911
+ ta.style.opacity = "0";
10912
+ document.body.appendChild(ta);
10913
+ ta.focus();
10914
+ ta.select();
10891
10915
  try {
10892
- if (typeof document !== "undefined") {
10893
- const ta = document.createElement("textarea");
10894
- ta.value = text;
10895
- ta.style.position = "fixed";
10896
- ta.style.opacity = "0";
10897
- document.body.appendChild(ta);
10898
- ta.focus();
10899
- ta.select();
10900
- document.execCommand("copy");
10901
- document.body.removeChild(ta);
10902
- }
10916
+ document.execCommand("copy");
10903
10917
  } catch {
10918
+ } finally {
10919
+ if (ta.parentNode) ta.parentNode.removeChild(ta);
10904
10920
  }
10905
10921
  };
10906
10922
  try {
@@ -10971,19 +10987,8 @@ function Datatable({
10971
10987
  announceClip(`Copied ${n} cell${n === 1 ? "" : "s"}`, cellRect);
10972
10988
  }
10973
10989
  };
10974
- const pasteSelection = async () => {
10975
- if (!cellRect) return;
10976
- let text = null;
10977
- try {
10978
- if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.readText) text = await navigator.clipboard.readText();
10979
- } catch {
10980
- setClipboardMsg("Paste unavailable \u2014 clipboard permission denied");
10981
- return;
10982
- }
10983
- if (text == null) {
10984
- setClipboardMsg("Nothing to paste");
10985
- return;
10986
- }
10990
+ const applyPastedText = (text) => {
10991
+ if (text == null || !cellRect) return;
10987
10992
  const inApp = clipboardRef.current && clipboardRef.current.tsv === text ? clipboardRef.current : null;
10988
10993
  let matrix;
10989
10994
  if (inApp) matrix = inApp.matrix;
@@ -11028,6 +11033,31 @@ function Datatable({
11028
11033
  const pasteRect = written ? { r0: cellRect.r0, c0: cellRect.c0, r1: Math.min(cellRect.r0 + matrix.length - 1, leafRows.length - 1), c1: Math.min(cellRect.c0 + maxCols - 1, ordered.length - 1) } : null;
11029
11034
  announceClip(`Pasted ${written} cell${written === 1 ? "" : "s"}${skipped ? `, ${skipped} skipped (incompatible column)` : ""}`, pasteRect);
11030
11035
  };
11036
+ const pasteSelection = async () => {
11037
+ if (!cellRect) return;
11038
+ let text = null;
11039
+ try {
11040
+ if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.readText) text = await navigator.clipboard.readText();
11041
+ } catch {
11042
+ setClipboardMsg("Paste unavailable \u2014 clipboard permission denied");
11043
+ return;
11044
+ }
11045
+ if (text == null) {
11046
+ setClipboardMsg("Nothing to paste");
11047
+ return;
11048
+ }
11049
+ applyPastedText(text);
11050
+ };
11051
+ const onGridPaste = (e) => {
11052
+ if (e.target.closest("input, textarea, select, [contenteditable='true']") || editing) return;
11053
+ if (!e.target.closest(".twc-dt__td[data-r]")) return;
11054
+ if (!(enableClipboard && selectionMode === "cell") || !cellRect) return;
11055
+ if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.readText) return;
11056
+ const text = e.clipboardData ? e.clipboardData.getData("text/plain") : null;
11057
+ if (text == null || text === "") return;
11058
+ e.preventDefault();
11059
+ applyPastedText(text);
11060
+ };
11031
11061
  React51__namespace.default.useEffect(() => {
11032
11062
  if (!editing) return;
11033
11063
  const onDown = (e) => {
@@ -11094,6 +11124,8 @@ function Datatable({
11094
11124
  }
11095
11125
  const tableMinWidth = leadW + ordered.reduce((a, c) => a + widthOf(c), 0);
11096
11126
  const filterableCols = React51__namespace.default.useMemo(() => cols.filter((c) => c.filterable), [cols]);
11127
+ const columnsShown = showColumns !== void 0 ? showColumns : cols.some((c) => c.hideable) || rowNumbers;
11128
+ const filtersShown = showFilters !== void 0 ? showFilters : filterableCols.length > 0;
11097
11129
  React51__namespace.default.useEffect(() => {
11098
11130
  const el = filterPanelRef.current;
11099
11131
  if (panel !== "filters" || !el || typeof document === "undefined") return void 0;
@@ -11480,6 +11512,7 @@ function Datatable({
11480
11512
  const totalCols = ordered.length + (checkboxSelection ? 1 : 0) + (showRowNum ? 1 : 0) + (hasExpandCol ? 1 : 0);
11481
11513
  function renderGroupRow(item) {
11482
11514
  const subs = aggOn ? subtotalText(item.rows) : [];
11515
+ const custom = renderGroupLabel ? renderGroupLabel({ field: item.field, value: item.value, count: item.count, rows: item.rows }) : null;
11483
11516
  return /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "twc-dt__group-row", role: "row", children: /* @__PURE__ */ jsxRuntime.jsxs("td", { className: "twc-dt__group-cell", role: "gridcell", colSpan: totalCols, style: { maxWidth: "none" }, children: [
11484
11517
  /* @__PURE__ */ jsxRuntime.jsxs(
11485
11518
  "button",
@@ -11491,12 +11524,14 @@ function Datatable({
11491
11524
  onClick: () => toggleGroup(item.key),
11492
11525
  children: [
11493
11526
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-dt__group-chev", "data-open": !item.collapsed || void 0, children: /* @__PURE__ */ jsxRuntime.jsx(Svg, { d: I.chevDown }) }),
11494
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "twc-dt__group-name", children: [
11495
- fieldLabel(item.field),
11496
- ":"
11497
- ] }),
11498
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-dt__group-val", children: String(item.value) }),
11499
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-dt__group-count", children: item.count })
11527
+ renderGroupLabel ? custom : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11528
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "twc-dt__group-name", children: [
11529
+ fieldLabel(item.field),
11530
+ ":"
11531
+ ] }),
11532
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-dt__group-val", children: String(item.value) }),
11533
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-dt__group-count", children: item.count })
11534
+ ] })
11500
11535
  ]
11501
11536
  }
11502
11537
  ),
@@ -11861,11 +11896,12 @@ function Datatable({
11861
11896
  /* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", checked: diffOnly, onChange: (e) => setDiffOnly(e.target.checked) }),
11862
11897
  diff.toggleLabel != null ? diff.toggleLabel : "Only changed"
11863
11898
  ] }) : null,
11864
- /* @__PURE__ */ jsxRuntime.jsxs(
11899
+ columnsShown ? /* @__PURE__ */ jsxRuntime.jsxs(
11865
11900
  "button",
11866
11901
  {
11867
11902
  type: "button",
11868
11903
  className: "twc-dt__tbtn",
11904
+ "data-tbtn": "columns",
11869
11905
  "data-active": panel === "columns" || void 0,
11870
11906
  "data-tip": "Show or hide columns",
11871
11907
  onClick: (e) => {
@@ -11885,12 +11921,13 @@ function Datatable({
11885
11921
  hidden.size ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-dt__tbadge", children: cols.length - hidden.size }) : null
11886
11922
  ]
11887
11923
  }
11888
- ),
11889
- /* @__PURE__ */ jsxRuntime.jsxs(
11924
+ ) : null,
11925
+ filtersShown ? /* @__PURE__ */ jsxRuntime.jsxs(
11890
11926
  "button",
11891
11927
  {
11892
11928
  type: "button",
11893
11929
  className: "twc-dt__tbtn",
11930
+ "data-tbtn": "filters",
11894
11931
  "data-active": panel === "filters" || void 0,
11895
11932
  "data-tip": "Filter rows",
11896
11933
  onClick: (e) => {
@@ -11909,7 +11946,7 @@ function Datatable({
11909
11946
  filters.length ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-dt__tbadge", children: filters.length }) : null
11910
11947
  ]
11911
11948
  }
11912
- ),
11949
+ ) : null,
11913
11950
  showDensity ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "twc-dt__tbtn", "data-tip": "Change row density", onClick: () => setDensity((d) => d === "compact" ? "standard" : d === "standard" ? "comfortable" : "compact"), children: [
11914
11951
  /* @__PURE__ */ jsxRuntime.jsx(Svg, { d: I.density }),
11915
11952
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-dt__tlabel", children: density[0].toUpperCase() + density.slice(1) })
@@ -12006,7 +12043,7 @@ function Datatable({
12006
12043
  }, children: /* @__PURE__ */ jsxRuntime.jsx(Svg, { d: I.x }) }) : null
12007
12044
  ] }) : null
12008
12045
  ] }),
12009
- !pivotActive && activeGroupBy.length ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "twc-dt__groupbar", children: [
12046
+ !pivotActive && activeGroupBy.length && showGroupBar ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "twc-dt__groupbar", children: [
12010
12047
  /* @__PURE__ */ jsxRuntime.jsx(Svg, { d: I.group }),
12011
12048
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-dt__groupbar-label", children: "Grouped by" }),
12012
12049
  activeGroupBy.map((f) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "twc-dt__groupchip", children: [
@@ -12043,6 +12080,7 @@ function Datatable({
12043
12080
  "aria-activedescendant": activeCellId,
12044
12081
  "aria-busy": loading || void 0,
12045
12082
  onKeyDown: onGridKeyDown,
12083
+ onPaste: onGridPaste,
12046
12084
  children: [
12047
12085
  /* @__PURE__ */ jsxRuntime.jsx("thead", { ref: theadRef, children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { role: "row", "aria-rowindex": 1, children: [
12048
12086
  hasExpandCol ? /* @__PURE__ */ jsxRuntime.jsx("th", { className: "twc-dt__th twc-dt__expand-cell", role: "columnheader", "aria-label": "Expand", "data-pin": "left", "data-pin-edge": checkboxSelection || showRowNum || visLeft.length ? void 0 : "left", style: { insetInlineStart: 0, width: EXP_W, minWidth: EXP_W } }) : null,
@@ -12480,13 +12518,13 @@ function Datatable({
12480
12518
  " Sort descending"
12481
12519
  ] })
12482
12520
  ] }) : null,
12483
- c.filterable ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", role: "menuitem", className: "twc-dt__mi", onClick: (e) => {
12521
+ c.filterable && filtersShown ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", role: "menuitem", className: "twc-dt__mi", onClick: (e) => {
12484
12522
  addFilter(c.field);
12485
12523
  setColMenu(null);
12486
12524
  closeMenu();
12487
12525
  restoreTriggerFocus();
12488
12526
  setPanel("filters");
12489
- openPanel(document.querySelector(".twc-dt__toolbar .twc-dt__tbtn:nth-child(2)"), "left", DT_FILTER_PANEL_W);
12527
+ openPanel(document.querySelector('.twc-dt__toolbar .twc-dt__tbtn[data-tbtn="filters"]'), "left", DT_FILTER_PANEL_W);
12490
12528
  }, children: [
12491
12529
  /* @__PURE__ */ jsxRuntime.jsx(Svg, { d: I.filter }),
12492
12530
  " Filter"
@@ -12559,7 +12597,7 @@ function Datatable({
12559
12597
  " ",
12560
12598
  wrapped.has(c.field) ? "Unwrap text" : "Wrap text"
12561
12599
  ] }) : null,
12562
- c.hideable ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", role: "menuitem", className: "twc-dt__mi", onClick: () => {
12600
+ c.hideable && columnsShown ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", role: "menuitem", className: "twc-dt__mi", onClick: () => {
12563
12601
  setHidden((h) => new Set(h).add(c.field));
12564
12602
  close();
12565
12603
  }, children: [
@@ -19117,14 +19155,36 @@ function Code({
19117
19155
  const timerRef = React51__namespace.default.useRef(null);
19118
19156
  React51__namespace.default.useEffect(() => () => clearTimeout(timerRef.current), []);
19119
19157
  const doCopy = () => {
19120
- if (typeof navigator === "undefined" || !navigator.clipboard) return;
19121
19158
  const text = typeof children === "string" ? children : rootRef.current?.textContent ?? "";
19122
- navigator.clipboard.writeText(text).then(() => {
19159
+ const flash = () => {
19123
19160
  setCopied(true);
19124
19161
  clearTimeout(timerRef.current);
19125
19162
  timerRef.current = setTimeout(() => setCopied(false), 2e3);
19126
- }).catch(() => {
19127
- });
19163
+ };
19164
+ const execFallback = () => {
19165
+ if (typeof document === "undefined") return;
19166
+ const ta = document.createElement("textarea");
19167
+ ta.value = text;
19168
+ ta.style.position = "fixed";
19169
+ ta.style.opacity = "0";
19170
+ document.body.appendChild(ta);
19171
+ ta.focus();
19172
+ ta.select();
19173
+ try {
19174
+ if (document.execCommand("copy")) flash();
19175
+ } catch {
19176
+ } finally {
19177
+ if (ta.parentNode) ta.parentNode.removeChild(ta);
19178
+ }
19179
+ };
19180
+ try {
19181
+ if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) {
19182
+ navigator.clipboard.writeText(text).then(flash).catch(execFallback);
19183
+ return;
19184
+ }
19185
+ } catch {
19186
+ }
19187
+ execFallback();
19128
19188
  };
19129
19189
  return /* @__PURE__ */ jsxRuntime.jsxs(Tag2, { ref: rootRef, className: `twc-code ${className}`.trim(), "data-block": block || void 0, ...rest, children: [
19130
19190
  __twcStyles,