twico-ui 1.36.0 → 1.37.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/dist/index.d.cts CHANGED
@@ -1927,7 +1927,9 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1927
1927
  /** #339: let end-users build their own combined columns at runtime. Adds a **"Combine columns…"** item to
1928
1928
  * each column's ⋮ menu that opens an editor to fold other columns' data into that column (they show in its
1929
1929
  * cell and their own columns hide); layout (inline/stacked) + labels are configurable, and "Uncombine"
1930
- * restores them. The result is the same as a declarative `column.combine` and persists via `stateKey`.
1930
+ * restores them. The result is like a declarative `column.combine` and persists via `stateKey` — except a
1931
+ * runtime combine also applies the source-join value + render over a target's own `valueGetter`/`renderCell`
1932
+ * (so a synthetic getter-backed column can still be a target), which a declarative `combine` defers to (#369).
1931
1933
  * @default false */
1932
1934
  columnCombining?: boolean;
1933
1935
  /** Make all columns editable by default (double-click a cell to edit; per-column `editable` overrides). @default false */
@@ -2179,7 +2181,11 @@ interface DatatableColumn<T = any> {
2179
2181
  * (label-prefixed when `labels`), while the **cell render** additionally applies each source column's
2180
2182
  * `valueFormatter`. The combined value is synthetic, so the column is display-only (never inline-editable).
2181
2183
  * Supplying your own `valueGetter` and/or `renderCell` overrides the derived one(s) independently —
2182
- * override `valueGetter` alone and the cell shows that value (not the source-join). */
2184
+ * override `valueGetter` alone and the cell shows that value (not the source-join). **This precedence is
2185
+ * for a *declarative* `combine` only.** A *runtime* combine the user builds via `columnCombining` (the ⋮ →
2186
+ * "Combine columns…" editor) is an explicit action, so it applies the source-join value + render even when
2187
+ * the target column has its own `valueGetter`/`renderCell` — a synthetic getter-backed column can still be
2188
+ * a combine target (#369). Uncombining restores the column's own value/render. */
2183
2189
  combine?: string[] | { fields: string[]; layout?: "inline" | "stack"; separator?: string; labels?: boolean };
2184
2190
  /** Header label. @default field ("Actions" for actions columns) */
2185
2191
  headerName?: string;
@@ -2909,8 +2915,14 @@ interface DialogProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"
2909
2915
  footer?: React.ReactNode;
2910
2916
  /** `"full"` is a near-fullscreen panel (fills the viewport minus a small margin). @default "md" */
2911
2917
  size?: "sm" | "md" | "lg" | "full";
2912
- /** Keep the header + footer fixed and scroll only the body (for long content). @default false */
2918
+ /** Keep the header + footer fixed and scroll only the body when content exceeds the viewport — so the
2919
+ * action buttons never scroll off-screen (#371). Set `false` for a panel that should scroll as one block
2920
+ * (rare; note a non-portaled child that deliberately overflows the panel would be clipped). @default true */
2913
2921
  scrollBody?: boolean;
2922
+ /** Draw a rule between header/body and body/footer. Suppressed automatically where the region is empty
2923
+ * (no footer; a header with only a close button and no title/description). Set `false` for a panel that
2924
+ * should read as one continuous surface. @default true */
2925
+ dividers?: boolean;
2914
2926
  /** Close when the backdrop is clicked. @default true */
2915
2927
  closeOnBackdrop?: boolean;
2916
2928
  children?: React.ReactNode;
@@ -2956,6 +2968,10 @@ interface DrawerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"
2956
2968
  height?: "sm" | "md" | "lg" | number | string;
2957
2969
  /** Close when the backdrop is clicked. @default true */
2958
2970
  closeOnBackdrop?: boolean;
2971
+ /** Draw a rule between header/body and body/footer (matches `Dialog`). Suppressed automatically where the
2972
+ * region is empty (no footer; a header with only a close button and no title/description). Set `false` for a
2973
+ * panel that should read as one continuous surface. @default true */
2974
+ dividers?: boolean;
2959
2975
  children?: React.ReactNode;
2960
2976
  }
2961
2977
 
package/dist/index.d.ts CHANGED
@@ -1927,7 +1927,9 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1927
1927
  /** #339: let end-users build their own combined columns at runtime. Adds a **"Combine columns…"** item to
1928
1928
  * each column's ⋮ menu that opens an editor to fold other columns' data into that column (they show in its
1929
1929
  * cell and their own columns hide); layout (inline/stacked) + labels are configurable, and "Uncombine"
1930
- * restores them. The result is the same as a declarative `column.combine` and persists via `stateKey`.
1930
+ * restores them. The result is like a declarative `column.combine` and persists via `stateKey` — except a
1931
+ * runtime combine also applies the source-join value + render over a target's own `valueGetter`/`renderCell`
1932
+ * (so a synthetic getter-backed column can still be a target), which a declarative `combine` defers to (#369).
1931
1933
  * @default false */
1932
1934
  columnCombining?: boolean;
1933
1935
  /** Make all columns editable by default (double-click a cell to edit; per-column `editable` overrides). @default false */
@@ -2179,7 +2181,11 @@ interface DatatableColumn<T = any> {
2179
2181
  * (label-prefixed when `labels`), while the **cell render** additionally applies each source column's
2180
2182
  * `valueFormatter`. The combined value is synthetic, so the column is display-only (never inline-editable).
2181
2183
  * Supplying your own `valueGetter` and/or `renderCell` overrides the derived one(s) independently —
2182
- * override `valueGetter` alone and the cell shows that value (not the source-join). */
2184
+ * override `valueGetter` alone and the cell shows that value (not the source-join). **This precedence is
2185
+ * for a *declarative* `combine` only.** A *runtime* combine the user builds via `columnCombining` (the ⋮ →
2186
+ * "Combine columns…" editor) is an explicit action, so it applies the source-join value + render even when
2187
+ * the target column has its own `valueGetter`/`renderCell` — a synthetic getter-backed column can still be
2188
+ * a combine target (#369). Uncombining restores the column's own value/render. */
2183
2189
  combine?: string[] | { fields: string[]; layout?: "inline" | "stack"; separator?: string; labels?: boolean };
2184
2190
  /** Header label. @default field ("Actions" for actions columns) */
2185
2191
  headerName?: string;
@@ -2909,8 +2915,14 @@ interface DialogProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"
2909
2915
  footer?: React.ReactNode;
2910
2916
  /** `"full"` is a near-fullscreen panel (fills the viewport minus a small margin). @default "md" */
2911
2917
  size?: "sm" | "md" | "lg" | "full";
2912
- /** Keep the header + footer fixed and scroll only the body (for long content). @default false */
2918
+ /** Keep the header + footer fixed and scroll only the body when content exceeds the viewport — so the
2919
+ * action buttons never scroll off-screen (#371). Set `false` for a panel that should scroll as one block
2920
+ * (rare; note a non-portaled child that deliberately overflows the panel would be clipped). @default true */
2913
2921
  scrollBody?: boolean;
2922
+ /** Draw a rule between header/body and body/footer. Suppressed automatically where the region is empty
2923
+ * (no footer; a header with only a close button and no title/description). Set `false` for a panel that
2924
+ * should read as one continuous surface. @default true */
2925
+ dividers?: boolean;
2914
2926
  /** Close when the backdrop is clicked. @default true */
2915
2927
  closeOnBackdrop?: boolean;
2916
2928
  children?: React.ReactNode;
@@ -2956,6 +2968,10 @@ interface DrawerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"
2956
2968
  height?: "sm" | "md" | "lg" | number | string;
2957
2969
  /** Close when the backdrop is clicked. @default true */
2958
2970
  closeOnBackdrop?: boolean;
2971
+ /** Draw a rule between header/body and body/footer (matches `Dialog`). Suppressed automatically where the
2972
+ * region is empty (no footer; a header with only a close button and no title/description). Set `false` for a
2973
+ * panel that should read as one continuous surface. @default true */
2974
+ dividers?: boolean;
2959
2975
  children?: React.ReactNode;
2960
2976
  }
2961
2977
 
package/dist/index.mjs CHANGED
@@ -9360,7 +9360,7 @@ function Datatable({
9360
9360
  for (const c of columnsProp) if (c && c.field != null) byField[c.field] = c;
9361
9361
  return [DIFF_OP_COLUMN, ...columnsProp.map((c) => {
9362
9362
  const cfg = combineCfg(userCombine[c.field] || c.combine);
9363
- const base = cfg && !c.valueGetter ? { ...c, valueGetter: combineValueGetter(combineSrcCols(cfg, byField), cfg) } : c;
9363
+ const base = cfg && (!!userCombine[c.field] || !c.valueGetter) ? { ...c, valueGetter: combineValueGetter(combineSrcCols(cfg, byField), cfg) } : c;
9364
9364
  return { ...base, renderCell: (val, row) => renderDiffCell(base, val, row) };
9365
9365
  })];
9366
9366
  },
@@ -9411,15 +9411,17 @@ function Datatable({
9411
9411
  const cfg = combineCfg(userCombine[c.field] || c.combine);
9412
9412
  if (!cfg) continue;
9413
9413
  const srcCols = combineSrcCols(cfg, byField);
9414
+ const explicit = !!userCombine[c.field];
9415
+ const useJoin = explicit || !c.valueGetter;
9414
9416
  out[i] = {
9415
9417
  ...c,
9416
9418
  editable: false,
9417
9419
  // the combined value is synthetic — inline editing would write to a phantom field
9418
- valueGetter: c.valueGetter || combineValueGetter(srcCols, cfg),
9419
- // Consumer renderCell wins. If they overrode only valueGetter, fall back to the DEFAULT cell render
9420
- // (renderCell: undefined) so the cell shows THEIR value — keeping display in sync with what
9421
- // sort/filter/search/export use — rather than the auto source-join. Otherwise render the combined cell.
9422
- renderCell: c.renderCell || (c.valueGetter ? void 0 : ((value, row) => renderCombined(srcCols, row, cfg)))
9420
+ valueGetter: useJoin ? combineValueGetter(srcCols, cfg) : c.valueGetter,
9421
+ // In diff mode the diff `columns` memo already wrapped renderCell in renderDiffCell (over the enriched
9422
+ // combined value) — keep that wrapper, or an EXPLICIT combine's `!explicit` short-circuit would strip the
9423
+ // diff arrows / old→new styling (#369 review). Declarative diff already preserved it (c.renderCell truthy).
9424
+ renderCell: isDiff ? c.renderCell : !explicit && c.renderCell || (useJoin ? ((value, row) => renderCombined(srcCols, row, cfg)) : void 0)
9423
9425
  };
9424
9426
  }
9425
9427
  return out;
@@ -15410,6 +15412,10 @@ var DIALOG_CSS = `
15410
15412
  .twc-dialog__close svg { width: 18px; height: 18px; }
15411
15413
  .twc-dialog__body { padding: var(--space-4) var(--space-5); font-size: var(--text-sm); color: var(--color-text-muted); line-height: var(--leading-normal); }
15412
15414
  .twc-dialog__footer { display: flex; gap: var(--space-2); justify-content: flex-end; padding: 0 var(--space-5) var(--space-5); }
15415
+ /* #372: header/body and body/footer rules (on by default). Per-region data-divider so the rule is
15416
+ suppressed when the region is empty (no footer; a header with only a close button and no title/description). */
15417
+ .twc-dialog__header[data-divider="true"] { padding-bottom: var(--space-4); border-bottom: var(--border-thin) solid var(--color-divider); }
15418
+ .twc-dialog__footer[data-divider="true"] { padding-top: var(--space-4); border-top: var(--border-thin) solid var(--color-divider); }
15413
15419
  `;
15414
15420
  function Dialog({
15415
15421
  open,
@@ -15419,7 +15425,8 @@ function Dialog({
15419
15425
  footer,
15420
15426
  children,
15421
15427
  size = "md",
15422
- scrollBody = false,
15428
+ scrollBody = true,
15429
+ dividers = true,
15423
15430
  closeOnBackdrop = true,
15424
15431
  className = "",
15425
15432
  ...rest
@@ -15460,7 +15467,7 @@ function Dialog({
15460
15467
  }, children: [
15461
15468
  __twcStyles,
15462
15469
  /* @__PURE__ */ jsxs("div", { ref: dialogRef, className: `twc-dialog ${className}`, "data-state": state, "data-size": size, "data-scroll-body": scrollBody ? "true" : void 0, role: "dialog", "aria-modal": "true", tabIndex: -1, "aria-labelledby": title ? titleId : void 0, "aria-label": !title ? "Dialog" : void 0, "aria-describedby": description ? descId : void 0, ...rest, children: [
15463
- title || description || onClose ? /* @__PURE__ */ jsxs("div", { className: "twc-dialog__header", children: [
15470
+ title || description || onClose ? /* @__PURE__ */ jsxs("div", { className: "twc-dialog__header", "data-divider": dividers && (title || description) && (children || footer) ? "true" : void 0, children: [
15464
15471
  /* @__PURE__ */ jsxs("div", { className: "twc-dialog__titles", children: [
15465
15472
  title ? /* @__PURE__ */ jsx("div", { className: "twc-dialog__title", id: titleId, children: title }) : null,
15466
15473
  description ? /* @__PURE__ */ jsx("div", { className: "twc-dialog__desc", id: descId, children: description }) : null
@@ -15468,7 +15475,7 @@ function Dialog({
15468
15475
  onClose ? /* @__PURE__ */ jsx("button", { className: "twc-dialog__close", onClick: onClose, "aria-label": "Close", type: "button", children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12" }) }) }) : null
15469
15476
  ] }) : null,
15470
15477
  children ? /* @__PURE__ */ jsx("div", { className: "twc-dialog__body", children }) : null,
15471
- footer ? /* @__PURE__ */ jsx("div", { className: "twc-dialog__footer", children: footer }) : null
15478
+ footer ? /* @__PURE__ */ jsx("div", { className: "twc-dialog__footer", "data-divider": dividers && children ? "true" : void 0, children: footer }) : null
15472
15479
  ] })
15473
15480
  ] });
15474
15481
  return renderPortal(overlay);
@@ -15568,7 +15575,11 @@ var DRAWER_CSS = `
15568
15575
  .twc-drawer__close:hover { background: var(--color-surface-sunken); color: var(--color-text); }
15569
15576
  .twc-drawer__close svg { width: 18px; height: 18px; }
15570
15577
  .twc-drawer__body { flex: 1; overflow-y: auto; padding: var(--space-2) var(--space-5) var(--space-5); font-size: var(--text-sm); color: var(--color-text-muted); line-height: var(--leading-normal); }
15571
- .twc-drawer__footer { display: flex; gap: var(--space-2); justify-content: flex-end; padding: var(--space-4) var(--space-5); border-top: var(--border-thin) solid var(--color-divider); }
15578
+ .twc-drawer__footer { display: flex; gap: var(--space-2); justify-content: flex-end; padding: var(--space-4) var(--space-5); }
15579
+ /* #372: header/footer rules (on by default), consistent with Dialog. Per-region data-divider so an empty
15580
+ region grows no stray line (no footer; a header with only a close button and no title/description). */
15581
+ .twc-drawer__header[data-divider="true"] { border-bottom: var(--border-thin) solid var(--color-divider); }
15582
+ .twc-drawer__footer[data-divider="true"] { border-top: var(--border-thin) solid var(--color-divider); }
15572
15583
  @media (prefers-reduced-motion: reduce) {
15573
15584
  .twc-drawer__overlay[data-state], .twc-drawer[data-state] { animation-duration: 1ms; }
15574
15585
  }
@@ -15584,6 +15595,7 @@ function Drawer({
15584
15595
  width,
15585
15596
  height,
15586
15597
  closeOnBackdrop = true,
15598
+ dividers = true,
15587
15599
  children,
15588
15600
  className = "",
15589
15601
  style,
@@ -15629,7 +15641,7 @@ function Drawer({
15629
15641
  }, children: [
15630
15642
  __twcStyles,
15631
15643
  /* @__PURE__ */ jsxs("div", { ref: panelRef, className: `twc-drawer ${className}`, "data-side": side, "data-state": state, role: "dialog", "aria-modal": "true", tabIndex: -1, "aria-labelledby": title ? titleId : void 0, "aria-label": !title ? "Drawer" : void 0, "aria-describedby": description ? descId : void 0, style: { ...dim ? sizeVar : null, ...style }, ...rest, children: [
15632
- title || description || onClose ? /* @__PURE__ */ jsxs("div", { className: "twc-drawer__header", children: [
15644
+ title || description || onClose ? /* @__PURE__ */ jsxs("div", { className: "twc-drawer__header", "data-divider": dividers && (title || description) ? "true" : void 0, children: [
15633
15645
  /* @__PURE__ */ jsxs("div", { className: "twc-drawer__titles", children: [
15634
15646
  title ? /* @__PURE__ */ jsx("div", { className: "twc-drawer__title", id: titleId, children: title }) : null,
15635
15647
  description ? /* @__PURE__ */ jsx("div", { className: "twc-drawer__desc", id: descId, children: description }) : null
@@ -15637,7 +15649,7 @@ function Drawer({
15637
15649
  onClose ? /* @__PURE__ */ jsx("button", { className: "twc-drawer__close", onClick: onClose, "aria-label": "Close", type: "button", children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12" }) }) }) : null
15638
15650
  ] }) : null,
15639
15651
  /* @__PURE__ */ jsx("div", { className: "twc-drawer__body", children }),
15640
- footer ? /* @__PURE__ */ jsx("div", { className: "twc-drawer__footer", children: footer }) : null
15652
+ footer ? /* @__PURE__ */ jsx("div", { className: "twc-drawer__footer", "data-divider": dividers ? "true" : void 0, children: footer }) : null
15641
15653
  ] })
15642
15654
  ] });
15643
15655
  return renderPortal(overlay);
@@ -16338,7 +16350,9 @@ var POPOVER_CSS = `
16338
16350
  .twc-popover__arrow { position: absolute; width: 11px; height: 11px; background: var(--color-surface-raised);
16339
16351
  border-left: var(--border-thin) solid var(--color-border); border-top: var(--border-thin) solid var(--color-border);
16340
16352
  transform: rotate(45deg); }
16341
- .twc-popover__inner { padding: var(--space-4); font-size: var(--text-sm); color: var(--color-text-muted); line-height: var(--leading-normal); }
16353
+ /* #373: cap the panel to the room place() measured and scroll INSIDE the inner (not the panel \u2014 the panel
16354
+ must keep overflow visible so the absolutely-positioned arrow isn't clipped) so a tall panel's footer stays reachable. */
16355
+ .twc-popover__inner { padding: var(--space-4); font-size: var(--text-sm); color: var(--color-text-muted); line-height: var(--leading-normal); overflow-y: auto; }
16342
16356
  .twc-popover__title { font-size: var(--text-sm); font-weight: var(--font-bold); color: var(--color-text); margin-bottom: 4px; }
16343
16357
  `;
16344
16358
  function Popover({
@@ -16400,7 +16414,7 @@ function Popover({
16400
16414
  l = Math.max(M, Math.min(l, vw - w - M));
16401
16415
  left = l;
16402
16416
  arrow = { left: Math.min(Math.max(r2.left + r2.width / 2 - l - 5.5, 10), w - 20), [flip ? "bottom" : "top"]: -6 };
16403
- return setPos({ top, bottom, left, width: w, flip, arrow });
16417
+ return setPos({ top, bottom, left, width: w, flip, arrow, maxHeight: flip ? spaceAbove : spaceBelow });
16404
16418
  }
16405
16419
  const ph = popRef.current ? popRef.current.offsetHeight : 120;
16406
16420
  let onRight = placement === "right";
@@ -16412,7 +16426,7 @@ function Popover({
16412
16426
  const cy = r2.top + r2.height / 2;
16413
16427
  top = Math.max(M, Math.min(cy - ph / 2, vh - ph - M));
16414
16428
  arrow = { top: Math.min(Math.max(cy - top - 5.5, 8), ph - 18), [onRight ? "left" : "right"]: -6 };
16415
- setPos({ top, left, width: w, flip: false, arrow });
16429
+ setPos({ top, left, width: w, flip: false, arrow, maxHeight: vh - 2 * M });
16416
16430
  }, [placement, align, width]);
16417
16431
  React51__default.useEffect(() => {
16418
16432
  if (!open) return;
@@ -16478,7 +16492,7 @@ function Popover({
16478
16492
  children: [
16479
16493
  __twcStyles,
16480
16494
  /* @__PURE__ */ jsx("span", { className: "twc-popover__arrow", style: pos.arrow, "aria-hidden": "true" }),
16481
- /* @__PURE__ */ jsxs("div", { className: "twc-popover__inner", children: [
16495
+ /* @__PURE__ */ jsxs("div", { className: "twc-popover__inner", style: { maxHeight: pos.maxHeight }, children: [
16482
16496
  title ? /* @__PURE__ */ jsx("div", { className: "twc-popover__title", id: `${popId}-title`, children: title }) : null,
16483
16497
  children
16484
16498
  ] })