twico-ui 1.10.0 → 1.11.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,28 @@
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.11.1](https://github.com/EncryptionL/twico-ui/compare/v1.11.0...v1.11.1) (2026-07-09)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **datatable:** pointer cursor on the Columns-panel visibility toggle ([9d30343](https://github.com/EncryptionL/twico-ui/commit/9d303434f51c4a4131cf393e56f8534d53f0a7bb))
11
+ * **tooltip:** grow horizontally + point the arrow at the trigger ([19aa76a](https://github.com/EncryptionL/twico-ui/commit/19aa76a6ffe597d81aff00a4144182661db0971b))
12
+
13
+ # [1.11.0](https://github.com/EncryptionL/twico-ui/compare/v1.10.0...v1.11.0) (2026-07-07)
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * **app-shell:** suppress the focus ring on the tabindex=-1 skip target ([#190](https://github.com/EncryptionL/twico-ui/issues/190)) ([88fd874](https://github.com/EncryptionL/twico-ui/commit/88fd874d659115407492d7ab78997f5b88ee46fa))
19
+ * **styles:** omit CSP nonce from React 19 precedence styles ([#189](https://github.com/EncryptionL/twico-ui/issues/189)) ([c86c7e8](https://github.com/EncryptionL/twico-ui/commit/c86c7e8c09d559f1749431cc8c5075d382f6ca8f))
20
+ * **sx:** normalize box-model shorthands to longhands ([#188](https://github.com/EncryptionL/twico-ui/issues/188)) ([3807718](https://github.com/EncryptionL/twico-ui/commit/38077184aeb58cfe5e4a4989b8cacb10e6068475))
21
+
22
+
23
+ ### Features
24
+
25
+ * **datatable:** expose column visibility to server mode ([#191](https://github.com/EncryptionL/twico-ui/issues/191)) ([1412c13](https://github.com/EncryptionL/twico-ui/commit/1412c135095f091557697ea04a9f700ee0b472cc))
26
+
5
27
  # [1.10.0](https://github.com/EncryptionL/twico-ui/compare/v1.9.0...v1.10.0) (2026-07-05)
6
28
 
7
29
 
package/dist/index.cjs CHANGED
@@ -46,7 +46,7 @@ function useScopedStyles(id, css) {
46
46
  }
47
47
  if (el.textContent !== css) el.textContent = css;
48
48
  }, [id, css, nonce]);
49
- return SUPPORTS_STYLE_HOIST && css ? React51__namespace.createElement("style", { href: id, precedence: "twc-ui", nonce }, css) : null;
49
+ return SUPPORTS_STYLE_HOIST && css ? React51__namespace.createElement("style", { href: id, precedence: "twc-ui" }, css) : null;
50
50
  }
51
51
 
52
52
  // components/TwicoProvider.jsx
@@ -1308,6 +1308,62 @@ function compileSx(obj, selector) {
1308
1308
  out += rules;
1309
1309
  return out;
1310
1310
  }
1311
+ var BOX_EDGE4 = {
1312
+ padding: ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft"],
1313
+ margin: ["marginTop", "marginRight", "marginBottom", "marginLeft"],
1314
+ inset: ["top", "right", "bottom", "left"]
1315
+ };
1316
+ var BOX_EDGE2 = {
1317
+ paddingBlock: ["paddingBlockStart", "paddingBlockEnd"],
1318
+ paddingInline: ["paddingInlineStart", "paddingInlineEnd"],
1319
+ marginBlock: ["marginBlockStart", "marginBlockEnd"],
1320
+ marginInline: ["marginInlineStart", "marginInlineEnd"]
1321
+ };
1322
+ function splitShorthand(value) {
1323
+ const s = String(value).trim();
1324
+ if (!s) return [];
1325
+ const out = [];
1326
+ let depth = 0, cur = "";
1327
+ for (let i = 0; i < s.length; i++) {
1328
+ const c = s[i];
1329
+ if (c === "(") {
1330
+ depth++;
1331
+ cur += c;
1332
+ } else if (c === ")") {
1333
+ depth = Math.max(0, depth - 1);
1334
+ cur += c;
1335
+ } else if (depth === 0 && (c === " " || c === " " || c === "\n" || c === "\r")) {
1336
+ if (cur) {
1337
+ out.push(cur);
1338
+ cur = "";
1339
+ }
1340
+ } else cur += c;
1341
+ }
1342
+ if (cur) out.push(cur);
1343
+ return out;
1344
+ }
1345
+ function expandBoxShorthand(key, value) {
1346
+ const four = BOX_EDGE4[key];
1347
+ const two = BOX_EDGE2[key];
1348
+ if (!four && !two || value == null) return null;
1349
+ if (typeof value === "number") return (four || two).map((edge) => [edge, value]);
1350
+ const p = splitShorthand(value);
1351
+ if (p.length === 0) return null;
1352
+ if (two) return [[two[0], p[0]], [two[1], p.length > 1 ? p[1] : p[0]]];
1353
+ let top, right, bottom, left;
1354
+ if (p.length === 1) top = right = bottom = left = p[0];
1355
+ else if (p.length === 2) {
1356
+ top = bottom = p[0];
1357
+ right = left = p[1];
1358
+ } else if (p.length === 3) {
1359
+ top = p[0];
1360
+ right = left = p[1];
1361
+ bottom = p[2];
1362
+ } else {
1363
+ [top, right, bottom, left] = p;
1364
+ }
1365
+ return [[four[0], top], [four[1], right], [four[2], bottom], [four[3], left]];
1366
+ }
1311
1367
  function buildSx(sx, uid) {
1312
1368
  if (!isObject(sx)) return { flatStyle: void 0, css: "" };
1313
1369
  const scope = `[data-twc-sx="${uid}"]`;
@@ -1325,7 +1381,13 @@ function buildSx(sx, uid) {
1325
1381
  css += compileSx(value, resolveSelector(key, scope));
1326
1382
  }
1327
1383
  } else {
1328
- (flatStyle || (flatStyle = {}))[key] = value;
1384
+ const expanded = expandBoxShorthand(key, value);
1385
+ if (expanded) {
1386
+ flatStyle || (flatStyle = {});
1387
+ for (const [k, v] of expanded) flatStyle[k] = v;
1388
+ } else {
1389
+ (flatStyle || (flatStyle = {}))[key] = value;
1390
+ }
1329
1391
  }
1330
1392
  }
1331
1393
  return { flatStyle, css };
@@ -7776,7 +7838,12 @@ var TOOLTIP_CSS = `
7776
7838
  padding: 6px 10px; border-radius: var(--radius-md);
7777
7839
  background: var(--color-text); color: var(--color-surface);
7778
7840
  font-family: var(--font-sans); font-size: var(--text-xs); font-weight: var(--font-medium);
7779
- line-height: 1.3; white-space: normal; max-width: var(--_tw-maxw, 260px); box-shadow: var(--shadow-md);
7841
+ line-height: 1.3; white-space: normal;
7842
+ /* Grow horizontally to max-width, then wrap. max-content sizes to the content (capped by
7843
+ max-width) and \u2014 unlike shrink-to-fit width:auto \u2014 is NOT constrained by the space between
7844
+ the fixed left edge and the viewport edge, so a tooltip near the right edge no longer
7845
+ collapses to ~1 word per line and stretch vertically. */
7846
+ width: max-content; max-width: var(--_tw-maxw, 320px); box-shadow: var(--shadow-md);
7780
7847
  opacity: 0; transform: scale(0.9); transition: opacity var(--duration-fast) var(--ease-out), transform var(--duration-fast) var(--ease-spring);
7781
7848
  }
7782
7849
  .twc-tooltip[data-show="true"] { pointer-events: auto; }
@@ -7787,10 +7854,13 @@ var TOOLTIP_CSS = `
7787
7854
  .twc-tooltip[data-place="right"] { transform-origin: left center; translate: 0 -50%; }
7788
7855
  .twc-tooltip[data-show="true"][data-place="top"], .twc-tooltip[data-show="true"][data-place="bottom"] { transform: scale(1); }
7789
7856
  .twc-tooltip__arrow { position: absolute; width: 7px; height: 7px; background: var(--color-text); transform: rotate(45deg); }
7790
- .twc-tooltip[data-place="top"] .twc-tooltip__arrow { bottom: -3px; left: 50%; margin-left: -3.5px; }
7791
- .twc-tooltip[data-place="bottom"] .twc-tooltip__arrow { top: -3px; left: 50%; margin-left: -3.5px; }
7792
- .twc-tooltip[data-place="left"] .twc-tooltip__arrow { right: -3px; top: 50%; margin-top: -3.5px; }
7793
- .twc-tooltip[data-place="right"] .twc-tooltip__arrow { left: -3px; top: 50%; margin-top: -3.5px; }
7857
+ /* Arrow tracks the trigger via --_tw-arrow-x/y (px from the tooltip edge) instead of being
7858
+ locked to the centre, so it still points at the trigger when the bubble is clamped near a
7859
+ viewport edge. Falls back to 50% before measurement / SSR. */
7860
+ .twc-tooltip[data-place="top"] .twc-tooltip__arrow { bottom: -3px; left: var(--_tw-arrow-x, 50%); margin-left: -3.5px; }
7861
+ .twc-tooltip[data-place="bottom"] .twc-tooltip__arrow { top: -3px; left: var(--_tw-arrow-x, 50%); margin-left: -3.5px; }
7862
+ .twc-tooltip[data-place="left"] .twc-tooltip__arrow { right: -3px; top: var(--_tw-arrow-y, 50%); margin-top: -3.5px; }
7863
+ .twc-tooltip[data-place="right"] .twc-tooltip__arrow { left: -3px; top: var(--_tw-arrow-y, 50%); margin-top: -3.5px; }
7794
7864
  @media (prefers-reduced-motion: reduce) { .twc-tooltip { transition: opacity var(--duration-fast) linear; transform: none; } }
7795
7865
  `;
7796
7866
  function Tooltip({
@@ -7844,10 +7914,22 @@ function Tooltip({
7844
7914
  else if (placement === "left" && spaceLeft < tw && spaceRight > spaceLeft) side = "right";
7845
7915
  else if (placement === "right" && spaceRight < tw && spaceLeft > spaceRight) side = "left";
7846
7916
  const base = { left: "auto", right: "auto", top: "auto", bottom: "auto", place: side };
7847
- if (side === "bottom") setCoords({ ...base, left: clampX(cx), top: Math.round(r2.bottom + gap) });
7848
- else if (side === "left") setCoords({ ...base, top: clampY(cy), right: Math.round(vw - r2.left + gap) });
7849
- else if (side === "right") setCoords({ ...base, top: clampY(cy), left: Math.round(r2.right + gap) });
7850
- else setCoords({ ...base, left: clampX(cx), bottom: Math.round(vh - r2.top + gap) });
7917
+ const AP = 12;
7918
+ const arrowX = (center) => tw ? Math.round(Math.min(Math.max(cx - (center - tw / 2), AP), tw - AP)) : void 0;
7919
+ const arrowY = (center) => th ? Math.round(Math.min(Math.max(cy - (center - th / 2), AP), th - AP)) : void 0;
7920
+ if (side === "bottom") {
7921
+ const c = clampX(cx);
7922
+ setCoords({ ...base, left: c, top: Math.round(r2.bottom + gap), arrowX: arrowX(c) });
7923
+ } else if (side === "left") {
7924
+ const c = clampY(cy);
7925
+ setCoords({ ...base, top: c, right: Math.round(vw - r2.left + gap), arrowY: arrowY(c) });
7926
+ } else if (side === "right") {
7927
+ const c = clampY(cy);
7928
+ setCoords({ ...base, top: c, left: Math.round(r2.right + gap), arrowY: arrowY(c) });
7929
+ } else {
7930
+ const c = clampX(cx);
7931
+ setCoords({ ...base, left: c, bottom: Math.round(vh - r2.top + gap), arrowX: arrowX(c) });
7932
+ }
7851
7933
  }, [placement]);
7852
7934
  React51__namespace.default.useEffect(() => {
7853
7935
  place();
@@ -7904,7 +7986,15 @@ function Tooltip({
7904
7986
  "aria-hidden": show ? void 0 : "true",
7905
7987
  onMouseEnter: cancelClose,
7906
7988
  onMouseLeave: close,
7907
- style: { position: "fixed", left: coords.left, right: coords.right, top: coords.top, bottom: coords.bottom },
7989
+ style: {
7990
+ position: "fixed",
7991
+ left: coords.left,
7992
+ right: coords.right,
7993
+ top: coords.top,
7994
+ bottom: coords.bottom,
7995
+ "--_tw-arrow-x": coords.arrowX != null ? `${coords.arrowX}px` : void 0,
7996
+ "--_tw-arrow-y": coords.arrowY != null ? `${coords.arrowY}px` : void 0
7997
+ },
7908
7998
  children: [
7909
7999
  label,
7910
8000
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-tooltip__arrow", "aria-hidden": "true" })
@@ -8284,7 +8374,8 @@ th.twc-dt__rownum .twc-dt__th-inner { padding-inline: 8px; gap: 2px; justify-con
8284
8374
  .twc-dt__col-pin:hover { background: var(--color-surface); color: var(--color-text); }
8285
8375
  .twc-dt__col-pin[data-on="true"] { color: var(--color-primary); background: var(--color-primary-subtle); }
8286
8376
  .twc-dt__col-pin svg { width: 14px; height: 14px; }
8287
- .twc-dt__sw { width: 32px; height: 18px; border-radius: var(--radius-full); background: var(--color-border-strong); position: relative; flex: none; transition: background-color var(--duration-base); }
8377
+ .twc-dt__sw { width: 32px; height: 18px; border-radius: var(--radius-full); background: var(--color-border-strong); position: relative; flex: none; cursor: pointer; transition: background-color var(--duration-base); }
8378
+ .twc-dt__sw[aria-disabled="true"] { cursor: default; }
8288
8379
  .twc-dt__sw[data-on="true"] { background: var(--color-primary); }
8289
8380
  .twc-dt__sw::after { content: ""; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: var(--radius-full); background: var(--color-primary-fg); box-shadow: var(--shadow-sm); transition: transform var(--duration-base) var(--ease-spring); }
8290
8381
  .twc-dt__sw:focus-visible { outline: none; box-shadow: var(--ring); }
@@ -8699,6 +8790,7 @@ function Datatable({
8699
8790
  serverMode = false,
8700
8791
  rowCount,
8701
8792
  onServerChange,
8793
+ onColumnVisibilityChange,
8702
8794
  batchActions = [],
8703
8795
  showExport = false,
8704
8796
  showDensity = false,
@@ -9005,6 +9097,19 @@ function Datatable({
9005
9097
  }, [exportOpen]);
9006
9098
  const colByField = React51__namespace.default.useMemo(() => Object.fromEntries(cols.map((c) => [c.field, c])), [cols]);
9007
9099
  const visibleCols = cols.filter((c) => !hidden.has(c.field));
9100
+ const colFieldsKey = JSON.stringify(
9101
+ cols.filter((c) => typeof c.field === "string" && c.type !== "actions").map((c) => c.field)
9102
+ );
9103
+ const colFields = React51__namespace.default.useMemo(() => JSON.parse(colFieldsKey), [colFieldsKey]);
9104
+ const hiddenKey = JSON.stringify([...hidden].filter((f) => typeof f === "string").sort());
9105
+ const visibleColumns = React51__namespace.default.useMemo(() => {
9106
+ const h = new Set(JSON.parse(hiddenKey));
9107
+ return colFields.filter((f) => !h.has(f));
9108
+ }, [colFields, hiddenKey]);
9109
+ const hiddenColumns = React51__namespace.default.useMemo(() => {
9110
+ const h = new Set(JSON.parse(hiddenKey));
9111
+ return colFields.filter((f) => h.has(f));
9112
+ }, [colFields, hiddenKey]);
9008
9113
  const ordered = React51__namespace.default.useMemo(() => {
9009
9114
  const orderIdx = (f) => {
9010
9115
  const i = order.indexOf(f);
@@ -9347,11 +9452,27 @@ function Datatable({
9347
9452
  pageSize: sizeVal,
9348
9453
  sort,
9349
9454
  filters: filters.map(({ id, ...f }) => f),
9350
- quickFilter: quick.trim()
9455
+ quickFilter: quick.trim(),
9456
+ visibleColumns,
9457
+ hiddenColumns
9458
+ // #191: server can project only the visible columns
9351
9459
  });
9352
9460
  }, 250);
9353
9461
  return () => clearTimeout(t);
9354
- }, [serverMode, pageVal, sizeVal, sort, filters, quick]);
9462
+ }, [serverMode, pageVal, sizeVal, sort, filters, quick, visibleColumns, hiddenColumns]);
9463
+ const onColVisRef = React51__namespace.default.useRef(onColumnVisibilityChange);
9464
+ onColVisRef.current = onColumnVisibilityChange;
9465
+ const lastVisibleKey = React51__namespace.default.useRef(null);
9466
+ React51__namespace.default.useEffect(() => {
9467
+ const key = JSON.stringify(visibleColumns);
9468
+ if (lastVisibleKey.current === null) {
9469
+ lastVisibleKey.current = key;
9470
+ return;
9471
+ }
9472
+ if (lastVisibleKey.current === key) return;
9473
+ lastVisibleKey.current = key;
9474
+ onColVisRef.current?.(visibleColumns);
9475
+ }, [visibleColumns]);
9355
9476
  function cycleSort(field) {
9356
9477
  setSort((s) => !s || s.field !== field ? { field, dir: "asc" } : s.dir === "asc" ? { field, dir: "desc" } : null);
9357
9478
  }
@@ -15374,6 +15495,12 @@ var SHELL_CSS = `
15374
15495
  min-height: 60px; padding-inline: var(--space-5); border-bottom: var(--border-thin) solid var(--color-border); background: var(--color-surface); }
15375
15496
  .twc-shell__content { flex: 1 1 auto; min-height: 0; overflow: auto; }
15376
15497
  .twc-shell__content[data-padded="true"] { padding: var(--space-6) var(--space-5); }
15498
+ /* #190: <main> is the skip-link target (tabindex="-1") \u2014 a programmatic focus target,
15499
+ not tab-reachable and not an interactive control. Clicking content mouse-focuses it,
15500
+ then the first keypress flips to keyboard modality and :focus-visible would draw a UA
15501
+ outline around the ENTIRE content area. Suppress it \u2014 the skip LINK keeps its own
15502
+ visible ring (.twc-shell__skip:focus-visible) and SR focus announcement is unaffected. */
15503
+ .twc-shell__content:focus, .twc-shell__content:focus-visible { outline: none; }
15377
15504
  /* Mobile: stack the shell so the sidebar (rendered as an off-canvas overlay) no
15378
15505
  longer reserves a column \u2014 the main content spans the full width. */
15379
15506
  @media (max-width: 720px) {
@@ -15508,8 +15635,10 @@ var Container = React51__namespace.default.forwardRef(function Container2({
15508
15635
  style: {
15509
15636
  width: "100%",
15510
15637
  maxWidth: typeof max === "number" ? `${max}px` : max,
15511
- marginInline: "auto",
15512
- paddingInline: padded ? "var(--space-5)" : void 0,
15638
+ marginInlineStart: "auto",
15639
+ marginInlineEnd: "auto",
15640
+ paddingInlineStart: padded ? "var(--space-5)" : void 0,
15641
+ paddingInlineEnd: padded ? "var(--space-5)" : void 0,
15513
15642
  ...style,
15514
15643
  ...flatStyle
15515
15644
  },
@@ -15759,7 +15888,10 @@ var Heading = React51__namespace.default.forwardRef(function Heading2({
15759
15888
  className: `twc-heading ${className}`.trim(),
15760
15889
  "data-twc-sx": sxAttr,
15761
15890
  style: {
15762
- margin: 0,
15891
+ marginTop: 0,
15892
+ marginRight: 0,
15893
+ marginBottom: 0,
15894
+ marginLeft: 0,
15763
15895
  fontFamily: "var(--font-sans)",
15764
15896
  fontWeight: "var(--font-bold)",
15765
15897
  color: "var(--color-text)",
@@ -15824,7 +15956,10 @@ var Text = React51__namespace.default.forwardRef(function Text2({
15824
15956
  className: `twc-text ${className}`.trim(),
15825
15957
  "data-twc-sx": sxAttr,
15826
15958
  style: {
15827
- margin: 0,
15959
+ marginTop: 0,
15960
+ marginRight: 0,
15961
+ marginBottom: 0,
15962
+ marginLeft: 0,
15828
15963
  fontFamily: "var(--font-sans)",
15829
15964
  lineHeight: 1.6,
15830
15965
  fontSize: `var(--text-${size})`,