twico-ui 1.36.1 → 1.37.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/dist/index.d.cts CHANGED
@@ -122,7 +122,9 @@ interface ImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "al
122
122
  src?: string;
123
123
  /** Alternative text (required for accessibility; pass "" for decorative images). */
124
124
  alt: string;
125
- /** object-fit value. */
125
+ /** object-fit value. When set, the image fills its container by default (`width`/`height: 100%`); pass an
126
+ * explicit `width`/`height` (or size the container / `style`) to give it a fixed box that `fit` crops within
127
+ * — an explicit `width`/`height` now wins over the fill (#378). */
126
128
  fit?: "cover" | "contain" | "fill" | "none" | "scale-down";
127
129
  /** Border radius token (e.g. "md", "lg", "full"). */
128
130
  radius?: string;
@@ -2915,8 +2917,14 @@ interface DialogProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"
2915
2917
  footer?: React.ReactNode;
2916
2918
  /** `"full"` is a near-fullscreen panel (fills the viewport minus a small margin). @default "md" */
2917
2919
  size?: "sm" | "md" | "lg" | "full";
2918
- /** Keep the header + footer fixed and scroll only the body (for long content). @default false */
2920
+ /** Keep the header + footer fixed and scroll only the body when content exceeds the viewport — so the
2921
+ * action buttons never scroll off-screen (#371). Set `false` for a panel that should scroll as one block
2922
+ * (rare; note a non-portaled child that deliberately overflows the panel would be clipped). @default true */
2919
2923
  scrollBody?: boolean;
2924
+ /** Draw a rule between header/body and body/footer. Suppressed automatically where the region is empty
2925
+ * (no footer; a header with only a close button and no title/description). Set `false` for a panel that
2926
+ * should read as one continuous surface. @default true */
2927
+ dividers?: boolean;
2920
2928
  /** Close when the backdrop is clicked. @default true */
2921
2929
  closeOnBackdrop?: boolean;
2922
2930
  children?: React.ReactNode;
@@ -2962,6 +2970,10 @@ interface DrawerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"
2962
2970
  height?: "sm" | "md" | "lg" | number | string;
2963
2971
  /** Close when the backdrop is clicked. @default true */
2964
2972
  closeOnBackdrop?: boolean;
2973
+ /** Draw a rule between header/body and body/footer (matches `Dialog`). Suppressed automatically where the
2974
+ * region is empty (no footer; a header with only a close button and no title/description). Set `false` for a
2975
+ * panel that should read as one continuous surface. @default true */
2976
+ dividers?: boolean;
2965
2977
  children?: React.ReactNode;
2966
2978
  }
2967
2979
 
package/dist/index.d.ts CHANGED
@@ -122,7 +122,9 @@ interface ImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "al
122
122
  src?: string;
123
123
  /** Alternative text (required for accessibility; pass "" for decorative images). */
124
124
  alt: string;
125
- /** object-fit value. */
125
+ /** object-fit value. When set, the image fills its container by default (`width`/`height: 100%`); pass an
126
+ * explicit `width`/`height` (or size the container / `style`) to give it a fixed box that `fit` crops within
127
+ * — an explicit `width`/`height` now wins over the fill (#378). */
126
128
  fit?: "cover" | "contain" | "fill" | "none" | "scale-down";
127
129
  /** Border radius token (e.g. "md", "lg", "full"). */
128
130
  radius?: string;
@@ -2915,8 +2917,14 @@ interface DialogProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"
2915
2917
  footer?: React.ReactNode;
2916
2918
  /** `"full"` is a near-fullscreen panel (fills the viewport minus a small margin). @default "md" */
2917
2919
  size?: "sm" | "md" | "lg" | "full";
2918
- /** Keep the header + footer fixed and scroll only the body (for long content). @default false */
2920
+ /** Keep the header + footer fixed and scroll only the body when content exceeds the viewport — so the
2921
+ * action buttons never scroll off-screen (#371). Set `false` for a panel that should scroll as one block
2922
+ * (rare; note a non-portaled child that deliberately overflows the panel would be clipped). @default true */
2919
2923
  scrollBody?: boolean;
2924
+ /** Draw a rule between header/body and body/footer. Suppressed automatically where the region is empty
2925
+ * (no footer; a header with only a close button and no title/description). Set `false` for a panel that
2926
+ * should read as one continuous surface. @default true */
2927
+ dividers?: boolean;
2920
2928
  /** Close when the backdrop is clicked. @default true */
2921
2929
  closeOnBackdrop?: boolean;
2922
2930
  children?: React.ReactNode;
@@ -2962,6 +2970,10 @@ interface DrawerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"
2962
2970
  height?: "sm" | "md" | "lg" | number | string;
2963
2971
  /** Close when the backdrop is clicked. @default true */
2964
2972
  closeOnBackdrop?: boolean;
2973
+ /** Draw a rule between header/body and body/footer (matches `Dialog`). Suppressed automatically where the
2974
+ * region is empty (no footer; a header with only a close button and no title/description). Set `false` for a
2975
+ * panel that should read as one continuous surface. @default true */
2976
+ dividers?: boolean;
2965
2977
  children?: React.ReactNode;
2966
2978
  }
2967
2979
 
package/dist/index.mjs CHANGED
@@ -304,6 +304,8 @@ var Image = React51__default.forwardRef(function Image2({
304
304
  className = "",
305
305
  style,
306
306
  onError,
307
+ width,
308
+ height,
307
309
  ...rest
308
310
  }, ref) {
309
311
  const __twcStyles = useScopedStyles("twc-image-styles", IMAGE_CSS);
@@ -313,6 +315,14 @@ var Image = React51__default.forwardRef(function Image2({
313
315
  objectFit: fit,
314
316
  borderRadius: radius ? `var(--radius-${radius})` : void 0,
315
317
  aspectRatio,
318
+ // #378: only when `fit` is set, also emit explicit width/height as inline style — that's the sole case
319
+ // where the `[data-fit] { width/height: 100% }` rule overrides the width/height ATTRIBUTES (a
320
+ // presentational attr loses to a real CSS rule), silently sizing `<Image width height fit>` to the
321
+ // container/intrinsic size. Inline style beats that rule so the props mean what they say (object-fit
322
+ // still crops within the box), placed before `...style` so a caller's own `style` still wins. Gated on
323
+ // `fit` so the no-fit path stays attribute-only — a consumer's own CSS/className can still override it.
324
+ ...fit && width != null ? { width } : null,
325
+ ...fit && height != null ? { height } : null,
316
326
  ...style
317
327
  };
318
328
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -326,6 +336,8 @@ var Image = React51__default.forwardRef(function Image2({
326
336
  loading,
327
337
  className: `twc-image ${className}`.trim(),
328
338
  "data-fit": fit || void 0,
339
+ width,
340
+ height,
329
341
  style: s,
330
342
  onError: (e) => {
331
343
  onError?.(e);
@@ -15412,6 +15424,10 @@ var DIALOG_CSS = `
15412
15424
  .twc-dialog__close svg { width: 18px; height: 18px; }
15413
15425
  .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); }
15414
15426
  .twc-dialog__footer { display: flex; gap: var(--space-2); justify-content: flex-end; padding: 0 var(--space-5) var(--space-5); }
15427
+ /* #372: header/body and body/footer rules (on by default). Per-region data-divider so the rule is
15428
+ suppressed when the region is empty (no footer; a header with only a close button and no title/description). */
15429
+ .twc-dialog__header[data-divider="true"] { padding-bottom: var(--space-4); border-bottom: var(--border-thin) solid var(--color-divider); }
15430
+ .twc-dialog__footer[data-divider="true"] { padding-top: var(--space-4); border-top: var(--border-thin) solid var(--color-divider); }
15415
15431
  `;
15416
15432
  function Dialog({
15417
15433
  open,
@@ -15421,7 +15437,8 @@ function Dialog({
15421
15437
  footer,
15422
15438
  children,
15423
15439
  size = "md",
15424
- scrollBody = false,
15440
+ scrollBody = true,
15441
+ dividers = true,
15425
15442
  closeOnBackdrop = true,
15426
15443
  className = "",
15427
15444
  ...rest
@@ -15462,7 +15479,7 @@ function Dialog({
15462
15479
  }, children: [
15463
15480
  __twcStyles,
15464
15481
  /* @__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: [
15465
- title || description || onClose ? /* @__PURE__ */ jsxs("div", { className: "twc-dialog__header", children: [
15482
+ title || description || onClose ? /* @__PURE__ */ jsxs("div", { className: "twc-dialog__header", "data-divider": dividers && (title || description) && (children || footer) ? "true" : void 0, children: [
15466
15483
  /* @__PURE__ */ jsxs("div", { className: "twc-dialog__titles", children: [
15467
15484
  title ? /* @__PURE__ */ jsx("div", { className: "twc-dialog__title", id: titleId, children: title }) : null,
15468
15485
  description ? /* @__PURE__ */ jsx("div", { className: "twc-dialog__desc", id: descId, children: description }) : null
@@ -15470,7 +15487,7 @@ function Dialog({
15470
15487
  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
15471
15488
  ] }) : null,
15472
15489
  children ? /* @__PURE__ */ jsx("div", { className: "twc-dialog__body", children }) : null,
15473
- footer ? /* @__PURE__ */ jsx("div", { className: "twc-dialog__footer", children: footer }) : null
15490
+ footer ? /* @__PURE__ */ jsx("div", { className: "twc-dialog__footer", "data-divider": dividers && children ? "true" : void 0, children: footer }) : null
15474
15491
  ] })
15475
15492
  ] });
15476
15493
  return renderPortal(overlay);
@@ -15570,7 +15587,11 @@ var DRAWER_CSS = `
15570
15587
  .twc-drawer__close:hover { background: var(--color-surface-sunken); color: var(--color-text); }
15571
15588
  .twc-drawer__close svg { width: 18px; height: 18px; }
15572
15589
  .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); }
15573
- .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); }
15590
+ .twc-drawer__footer { display: flex; gap: var(--space-2); justify-content: flex-end; padding: var(--space-4) var(--space-5); }
15591
+ /* #372: header/footer rules (on by default), consistent with Dialog. Per-region data-divider so an empty
15592
+ region grows no stray line (no footer; a header with only a close button and no title/description). */
15593
+ .twc-drawer__header[data-divider="true"] { border-bottom: var(--border-thin) solid var(--color-divider); }
15594
+ .twc-drawer__footer[data-divider="true"] { border-top: var(--border-thin) solid var(--color-divider); }
15574
15595
  @media (prefers-reduced-motion: reduce) {
15575
15596
  .twc-drawer__overlay[data-state], .twc-drawer[data-state] { animation-duration: 1ms; }
15576
15597
  }
@@ -15586,6 +15607,7 @@ function Drawer({
15586
15607
  width,
15587
15608
  height,
15588
15609
  closeOnBackdrop = true,
15610
+ dividers = true,
15589
15611
  children,
15590
15612
  className = "",
15591
15613
  style,
@@ -15631,7 +15653,7 @@ function Drawer({
15631
15653
  }, children: [
15632
15654
  __twcStyles,
15633
15655
  /* @__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: [
15634
- title || description || onClose ? /* @__PURE__ */ jsxs("div", { className: "twc-drawer__header", children: [
15656
+ title || description || onClose ? /* @__PURE__ */ jsxs("div", { className: "twc-drawer__header", "data-divider": dividers && (title || description) ? "true" : void 0, children: [
15635
15657
  /* @__PURE__ */ jsxs("div", { className: "twc-drawer__titles", children: [
15636
15658
  title ? /* @__PURE__ */ jsx("div", { className: "twc-drawer__title", id: titleId, children: title }) : null,
15637
15659
  description ? /* @__PURE__ */ jsx("div", { className: "twc-drawer__desc", id: descId, children: description }) : null
@@ -15639,7 +15661,7 @@ function Drawer({
15639
15661
  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
15640
15662
  ] }) : null,
15641
15663
  /* @__PURE__ */ jsx("div", { className: "twc-drawer__body", children }),
15642
- footer ? /* @__PURE__ */ jsx("div", { className: "twc-drawer__footer", children: footer }) : null
15664
+ footer ? /* @__PURE__ */ jsx("div", { className: "twc-drawer__footer", "data-divider": dividers ? "true" : void 0, children: footer }) : null
15643
15665
  ] })
15644
15666
  ] });
15645
15667
  return renderPortal(overlay);
@@ -16340,7 +16362,9 @@ var POPOVER_CSS = `
16340
16362
  .twc-popover__arrow { position: absolute; width: 11px; height: 11px; background: var(--color-surface-raised);
16341
16363
  border-left: var(--border-thin) solid var(--color-border); border-top: var(--border-thin) solid var(--color-border);
16342
16364
  transform: rotate(45deg); }
16343
- .twc-popover__inner { padding: var(--space-4); font-size: var(--text-sm); color: var(--color-text-muted); line-height: var(--leading-normal); }
16365
+ /* #373: cap the panel to the room place() measured and scroll INSIDE the inner (not the panel \u2014 the panel
16366
+ must keep overflow visible so the absolutely-positioned arrow isn't clipped) so a tall panel's footer stays reachable. */
16367
+ .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; }
16344
16368
  .twc-popover__title { font-size: var(--text-sm); font-weight: var(--font-bold); color: var(--color-text); margin-bottom: 4px; }
16345
16369
  `;
16346
16370
  function Popover({
@@ -16402,7 +16426,7 @@ function Popover({
16402
16426
  l = Math.max(M, Math.min(l, vw - w - M));
16403
16427
  left = l;
16404
16428
  arrow = { left: Math.min(Math.max(r2.left + r2.width / 2 - l - 5.5, 10), w - 20), [flip ? "bottom" : "top"]: -6 };
16405
- return setPos({ top, bottom, left, width: w, flip, arrow });
16429
+ return setPos({ top, bottom, left, width: w, flip, arrow, maxHeight: flip ? spaceAbove : spaceBelow });
16406
16430
  }
16407
16431
  const ph = popRef.current ? popRef.current.offsetHeight : 120;
16408
16432
  let onRight = placement === "right";
@@ -16414,7 +16438,7 @@ function Popover({
16414
16438
  const cy = r2.top + r2.height / 2;
16415
16439
  top = Math.max(M, Math.min(cy - ph / 2, vh - ph - M));
16416
16440
  arrow = { top: Math.min(Math.max(cy - top - 5.5, 8), ph - 18), [onRight ? "left" : "right"]: -6 };
16417
- setPos({ top, left, width: w, flip: false, arrow });
16441
+ setPos({ top, left, width: w, flip: false, arrow, maxHeight: vh - 2 * M });
16418
16442
  }, [placement, align, width]);
16419
16443
  React51__default.useEffect(() => {
16420
16444
  if (!open) return;
@@ -16480,7 +16504,7 @@ function Popover({
16480
16504
  children: [
16481
16505
  __twcStyles,
16482
16506
  /* @__PURE__ */ jsx("span", { className: "twc-popover__arrow", style: pos.arrow, "aria-hidden": "true" }),
16483
- /* @__PURE__ */ jsxs("div", { className: "twc-popover__inner", children: [
16507
+ /* @__PURE__ */ jsxs("div", { className: "twc-popover__inner", style: { maxHeight: pos.maxHeight }, children: [
16484
16508
  title ? /* @__PURE__ */ jsx("div", { className: "twc-popover__title", id: `${popId}-title`, children: title }) : null,
16485
16509
  children
16486
16510
  ] })