@assure-one/design-system 1.26.0 → 1.28.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.ts CHANGED
@@ -2705,6 +2705,15 @@ interface ProposalPackageCardProps extends Omit<React.HTMLAttributes<HTMLDivElem
2705
2705
  servicesLabel?: ReactNode;
2706
2706
  /** Service rows (`ProposalServiceRow`) or any content. */
2707
2707
  children?: ReactNode;
2708
+ /**
2709
+ * Cap the service list's height (any CSS length, e.g. `"14rem"`). Past it the
2710
+ * list scrolls rather than growing, so a package with many services keeps its
2711
+ * select control in view instead of pushing it below the fold — and cards
2712
+ * sitting in a row stay the same height.
2713
+ *
2714
+ * Omit for the previous behaviour: the list grows to fit.
2715
+ */
2716
+ servicesMaxHeight?: string;
2708
2717
  /** Select / toggle handler — makes the card interactive. */
2709
2718
  onSelect?: () => void;
2710
2719
  disabled?: boolean;
package/dist/index.js CHANGED
@@ -10639,6 +10639,7 @@ var ProposalPackageCard = forwardRef(
10639
10639
  badge,
10640
10640
  servicesLabel = "Included services",
10641
10641
  children,
10642
+ servicesMaxHeight,
10642
10643
  onSelect,
10643
10644
  disabled = false,
10644
10645
  className,
@@ -10674,7 +10675,31 @@ var ProposalPackageCard = forwardRef(
10674
10675
  ] }),
10675
10676
  children && /* @__PURE__ */ jsxs(Fragment, { children: [
10676
10677
  /* @__PURE__ */ jsx("div", { className: "text-fg-3 mt-4 mb-1 text-[11.5px] font-semibold tracking-wide", children: servicesLabel }),
10677
- /* @__PURE__ */ jsx("div", { children })
10678
+ /* @__PURE__ */ jsx(
10679
+ "div",
10680
+ {
10681
+ className: cn(
10682
+ servicesMaxHeight && [
10683
+ "min-h-0 overflow-y-auto",
10684
+ // pr-3 keeps the rail off the service text; -mr-1 gives that
10685
+ // padding back to the card so the rail still sits near its
10686
+ // edge rather than floating in from it.
10687
+ "-mr-1 pr-3",
10688
+ // A 2px hairline. `scrollbar-thin` is 8px, and on
10689
+ // Chrome-for-Windows an unstyled bar also carries stepper
10690
+ // arrows, which is most of what made it read as heavy.
10691
+ "[scrollbar-width:thin]",
10692
+ "[&::-webkit-scrollbar]:w-[2px]",
10693
+ "[&::-webkit-scrollbar-button]:hidden",
10694
+ "[&::-webkit-scrollbar-track]:bg-transparent",
10695
+ "[&::-webkit-scrollbar-thumb]:rounded-full",
10696
+ "[&::-webkit-scrollbar-thumb]:bg-rule-strong"
10697
+ ]
10698
+ ),
10699
+ style: servicesMaxHeight ? { maxHeight: servicesMaxHeight } : void 0,
10700
+ children
10701
+ }
10702
+ )
10678
10703
  ] }),
10679
10704
  /* @__PURE__ */ jsx("div", { className: "mt-auto pt-4", children: interactive ? /* @__PURE__ */ jsx(
10680
10705
  "button",
@@ -18249,6 +18274,8 @@ var SEARCH_THRESHOLD = 8;
18249
18274
  var HOVER_OPEN_MS = 90;
18250
18275
  var HOVER_CLOSE_MS = 220;
18251
18276
  var FLYOUT_WIDTH = 224;
18277
+ var MIN_FLYOUT_HEIGHT = 120;
18278
+ var VIEWPORT_MARGIN = 24;
18252
18279
  var DEFAULT_GUTTER_PX = 24;
18253
18280
  function DisplayMenu({
18254
18281
  sections,
@@ -18265,7 +18292,8 @@ function DisplayMenu({
18265
18292
  const [openKey, setOpenKey] = useState(null);
18266
18293
  const [flyout, setFlyout] = useState({
18267
18294
  top: 0,
18268
- side: "left"
18295
+ side: "left",
18296
+ maxHeight: 0
18269
18297
  });
18270
18298
  const [panelShift, setPanelShift] = useState(0);
18271
18299
  const [rootQuery, setRootQuery] = useState("");
@@ -18289,10 +18317,14 @@ function DisplayMenu({
18289
18317
  if (!panel || !row) return;
18290
18318
  const panelRect = panel.getBoundingClientRect();
18291
18319
  const rowRect = row.getBoundingClientRect();
18292
- const maxTop = Math.max(0, window.innerHeight - panelRect.top - 24 - 120);
18320
+ const maxTop = Math.max(0, window.innerHeight - panelRect.top - 24 - MIN_FLYOUT_HEIGHT);
18293
18321
  const top = Math.min(Math.max(rowRect.top - panelRect.top - 4, 0), maxTop);
18322
+ const maxHeight = Math.max(
18323
+ MIN_FLYOUT_HEIGHT,
18324
+ window.innerHeight - (panelRect.top + top) - VIEWPORT_MARGIN
18325
+ );
18294
18326
  const side = panelRect.left >= FLYOUT_WIDTH + 16 ? "left" : "right";
18295
- setFlyout({ top, side });
18327
+ setFlyout({ top, side, maxHeight });
18296
18328
  }, []);
18297
18329
  const openSection = useCallback(
18298
18330
  (key) => {
@@ -18423,100 +18455,108 @@ function DisplayMenu({
18423
18455
  }
18424
18456
  )
18425
18457
  ] }),
18426
- /* @__PURE__ */ jsx("div", { role: "menu", "aria-label": triggerLabel, className: "max-h-80 overflow-y-auto p-1", children: rootRows.length === 0 ? /* @__PURE__ */ jsx(EmptyRow, {}) : rootRows.map((row) => {
18427
- if (row.kind === "option") {
18428
- const { section: section2, option } = row;
18429
- const isActive = section2.selected.has(option.key);
18430
- const isMulti = section2.mode === "multi";
18431
- return /* @__PURE__ */ jsxs(
18432
- "button",
18433
- {
18434
- type: "button",
18435
- role: isMulti ? "menuitemcheckbox" : "menuitemradio",
18436
- "aria-checked": isActive,
18437
- onMouseEnter: () => {
18438
- clearTimers();
18439
- setOpenKey(null);
18440
- },
18441
- onClick: () => section2.onSelect(option.key),
18442
- className: "flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-[13px] transition-colors hover:bg-muted/60 focus-visible:bg-muted/60 focus-visible:outline-none",
18443
- children: [
18444
- section2.icon && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground", "aria-hidden": "true", children: section2.icon }),
18445
- /* @__PURE__ */ jsx("span", { className: "shrink-0 truncate text-muted-foreground", children: section2.label }),
18446
- /* @__PURE__ */ jsx(
18447
- ChevronRightIcon,
18448
- {
18449
- size: 12,
18450
- className: "shrink-0 text-muted-foreground",
18451
- "aria-hidden": "true"
18452
- }
18453
- ),
18454
- /* @__PURE__ */ jsx("span", { className: "flex-1 truncate text-foreground", children: option.label }),
18455
- isActive && /* @__PURE__ */ jsx(CheckIcon, { size: 14, className: "shrink-0 text-brand" })
18456
- ]
18457
- },
18458
- `${section2.key}:${option.key}`
18459
- );
18460
- }
18461
- const { section } = row;
18462
- const count = section.selected.size;
18463
- const valueLabel = section.summary ?? (section.mode === "single" ? section.options.find((o) => section.selected.has(o.key))?.label ?? "" : "");
18464
- const isOpen = section.key === openKey;
18465
- return (
18466
- /* Hover handlers sit on the wrapper so the cursor can cross
18467
- * the gap to `section.trailing` without closing the flyout.
18468
- * The wrapper is not itself a control — the button inside is,
18469
- * and it carries the keyboard equivalent (ArrowRight / Enter /
18470
- * Space open the same flyout). Nothing here is mouse-only. */
18471
- // eslint-disable-next-line jsx-a11y/no-static-element-interactions
18472
- /* @__PURE__ */ jsxs(
18473
- "div",
18474
- {
18475
- ref: (el) => {
18476
- if (el) rowRefs.current.set(section.key, el);
18477
- else rowRefs.current.delete(section.key);
18478
- },
18479
- className: "flex items-center gap-0.5",
18480
- onMouseEnter: () => scheduleOpen(section.key),
18481
- onMouseLeave: scheduleClose,
18482
- children: [
18483
- /* @__PURE__ */ jsxs(
18484
- "button",
18485
- {
18486
- type: "button",
18487
- role: "menuitem",
18488
- "aria-haspopup": "menu",
18489
- "aria-expanded": isOpen,
18490
- onClick: () => openSection(section.key),
18491
- onKeyDown: (e) => {
18492
- if (e.key === "ArrowRight" || e.key === "Enter" || e.key === " ") {
18493
- e.preventDefault();
18494
- openSection(section.key);
18495
- requestAnimationFrame(
18496
- () => flyoutRef.current?.querySelector("input, [role='menuitemcheckbox'], [role='menuitemradio']")?.focus()
18497
- );
18458
+ /* @__PURE__ */ jsx(
18459
+ "div",
18460
+ {
18461
+ role: "menu",
18462
+ "aria-label": triggerLabel,
18463
+ className: "scrollbar-thin max-h-[min(32rem,70vh)] overflow-y-auto p-1",
18464
+ children: rootRows.length === 0 ? /* @__PURE__ */ jsx(EmptyRow, {}) : rootRows.map((row) => {
18465
+ if (row.kind === "option") {
18466
+ const { section: section2, option } = row;
18467
+ const isActive = section2.selected.has(option.key);
18468
+ const isMulti = section2.mode === "multi";
18469
+ return /* @__PURE__ */ jsxs(
18470
+ "button",
18471
+ {
18472
+ type: "button",
18473
+ role: isMulti ? "menuitemcheckbox" : "menuitemradio",
18474
+ "aria-checked": isActive,
18475
+ onMouseEnter: () => {
18476
+ clearTimers();
18477
+ setOpenKey(null);
18478
+ },
18479
+ onClick: () => section2.onSelect(option.key),
18480
+ className: "flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-[13px] transition-colors hover:bg-muted/60 focus-visible:bg-muted/60 focus-visible:outline-none",
18481
+ children: [
18482
+ section2.icon && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground", "aria-hidden": "true", children: section2.icon }),
18483
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 truncate text-muted-foreground", children: section2.label }),
18484
+ /* @__PURE__ */ jsx(
18485
+ ChevronRightIcon,
18486
+ {
18487
+ size: 12,
18488
+ className: "shrink-0 text-muted-foreground",
18489
+ "aria-hidden": "true"
18498
18490
  }
18499
- },
18500
- className: cn(
18501
- "flex min-w-0 flex-1 items-center gap-2 rounded-md px-2 py-1.5 text-left text-[13px] text-foreground transition-colors hover:bg-muted/60 focus-visible:bg-muted/60 focus-visible:outline-none",
18502
- isOpen && "bg-muted/60"
18503
18491
  ),
18504
- children: [
18505
- section.icon && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground", "aria-hidden": "true", children: section.icon }),
18506
- /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: section.label }),
18507
- section.mode === "multi" && count > 0 && /* @__PURE__ */ jsx("span", { className: "shrink-0 rounded-full bg-brand-bg px-1.5 text-[11px] font-medium text-brand", children: count }),
18508
- section.mode !== "multi" && valueLabel && /* @__PURE__ */ jsx("span", { className: "max-w-24 shrink-0 truncate text-[13px] text-muted-foreground", children: valueLabel }),
18509
- /* @__PURE__ */ jsx(ChevronRightIcon, { size: 13, className: "shrink-0 text-muted-foreground" })
18510
- ]
18511
- }
18512
- ),
18513
- section.trailing
18514
- ]
18515
- },
18516
- section.key
18517
- )
18518
- );
18519
- }) }),
18492
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate text-foreground", children: option.label }),
18493
+ isActive && /* @__PURE__ */ jsx(CheckIcon, { size: 14, className: "shrink-0 text-brand" })
18494
+ ]
18495
+ },
18496
+ `${section2.key}:${option.key}`
18497
+ );
18498
+ }
18499
+ const { section } = row;
18500
+ const count = section.selected.size;
18501
+ const valueLabel = section.summary ?? (section.mode === "single" ? section.options.find((o) => section.selected.has(o.key))?.label ?? "" : "");
18502
+ const isOpen = section.key === openKey;
18503
+ return (
18504
+ /* Hover handlers sit on the wrapper so the cursor can cross
18505
+ * the gap to `section.trailing` without closing the flyout.
18506
+ * The wrapper is not itself a control — the button inside is,
18507
+ * and it carries the keyboard equivalent (ArrowRight / Enter /
18508
+ * Space open the same flyout). Nothing here is mouse-only. */
18509
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions
18510
+ /* @__PURE__ */ jsxs(
18511
+ "div",
18512
+ {
18513
+ ref: (el) => {
18514
+ if (el) rowRefs.current.set(section.key, el);
18515
+ else rowRefs.current.delete(section.key);
18516
+ },
18517
+ className: "flex items-center gap-0.5",
18518
+ onMouseEnter: () => scheduleOpen(section.key),
18519
+ onMouseLeave: scheduleClose,
18520
+ children: [
18521
+ /* @__PURE__ */ jsxs(
18522
+ "button",
18523
+ {
18524
+ type: "button",
18525
+ role: "menuitem",
18526
+ "aria-haspopup": "menu",
18527
+ "aria-expanded": isOpen,
18528
+ onClick: () => openSection(section.key),
18529
+ onKeyDown: (e) => {
18530
+ if (e.key === "ArrowRight" || e.key === "Enter" || e.key === " ") {
18531
+ e.preventDefault();
18532
+ openSection(section.key);
18533
+ requestAnimationFrame(
18534
+ () => flyoutRef.current?.querySelector("input, [role='menuitemcheckbox'], [role='menuitemradio']")?.focus()
18535
+ );
18536
+ }
18537
+ },
18538
+ className: cn(
18539
+ "flex min-w-0 flex-1 items-center gap-2 rounded-md px-2 py-1.5 text-left text-[13px] text-foreground transition-colors hover:bg-muted/60 focus-visible:bg-muted/60 focus-visible:outline-none",
18540
+ isOpen && "bg-muted/60"
18541
+ ),
18542
+ children: [
18543
+ section.icon && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground", "aria-hidden": "true", children: section.icon }),
18544
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: section.label }),
18545
+ section.mode === "multi" && count > 0 && /* @__PURE__ */ jsx("span", { className: "shrink-0 rounded-full bg-brand-bg px-1.5 text-[11px] font-medium text-brand", children: count }),
18546
+ section.mode !== "multi" && valueLabel && /* @__PURE__ */ jsx("span", { className: "max-w-24 shrink-0 truncate text-[13px] text-muted-foreground", children: valueLabel }),
18547
+ /* @__PURE__ */ jsx(ChevronRightIcon, { size: 13, className: "shrink-0 text-muted-foreground" })
18548
+ ]
18549
+ }
18550
+ ),
18551
+ section.trailing
18552
+ ]
18553
+ },
18554
+ section.key
18555
+ )
18556
+ );
18557
+ })
18558
+ }
18559
+ ),
18520
18560
  /* @__PURE__ */ jsx("div", { className: "border-t border-border p-1", children: /* @__PURE__ */ jsxs(
18521
18561
  Button,
18522
18562
  {
@@ -18538,9 +18578,12 @@ function DisplayMenu({
18538
18578
  role: "menu",
18539
18579
  "aria-label": activeSection.label,
18540
18580
  tabIndex: -1,
18541
- style: { top: flyout.top, width: FLYOUT_WIDTH },
18581
+ style: { top: flyout.top, width: FLYOUT_WIDTH, maxHeight: flyout.maxHeight },
18542
18582
  className: cn(
18543
- "absolute z-50 rounded-lg border border-border bg-popover shadow-md",
18583
+ // Column + overflow-hidden so the option list (the only flexible
18584
+ // part) absorbs the height cap, while the search box and Clear
18585
+ // footer stay pinned and fully visible.
18586
+ "absolute z-50 flex flex-col overflow-hidden rounded-lg border border-border bg-popover shadow-md",
18544
18587
  // -mx-1 lets the flyout tuck against the panel so there's no
18545
18588
  // dead gap for the cursor to fall through on the way over.
18546
18589
  flyout.side === "left" ? "right-full -mr-1" : "left-full -ml-1"
@@ -18548,7 +18591,7 @@ function DisplayMenu({
18548
18591
  onMouseEnter: clearTimers,
18549
18592
  onMouseLeave: scheduleClose,
18550
18593
  children: [
18551
- showFlyoutSearch && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 border-b border-border px-2.5 py-1.5", children: [
18594
+ showFlyoutSearch && /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-1.5 border-b border-border px-2.5 py-1.5", children: [
18552
18595
  /* @__PURE__ */ jsx(SearchIcon, { size: 13, className: "shrink-0 text-muted-foreground", "aria-hidden": "true" }),
18553
18596
  /* @__PURE__ */ jsx(
18554
18597
  "input",
@@ -18562,7 +18605,7 @@ function DisplayMenu({
18562
18605
  }
18563
18606
  )
18564
18607
  ] }),
18565
- /* @__PURE__ */ jsx("div", { className: "max-h-64 overflow-y-auto p-1", children: activeSection.content ? (
18608
+ /* @__PURE__ */ jsx("div", { className: "scrollbar-thin min-h-0 flex-1 max-h-[min(24rem,60vh)] overflow-y-auto p-1", children: activeSection.content ? (
18566
18609
  /* Consumer-supplied controls (date range, numeric bound).
18567
18610
  * Rendered raw — the section owns its own labelling and
18568
18611
  * layout; the flyout only provides the surface. */
@@ -18615,7 +18658,7 @@ function DisplayMenu({
18615
18658
  option.key
18616
18659
  );
18617
18660
  }) }),
18618
- activeSection.mode === "multi" && activeSection.selected.size > 0 && activeSection.onClear && /* @__PURE__ */ jsx("div", { className: "border-t border-border p-1", children: /* @__PURE__ */ jsxs(
18661
+ activeSection.mode === "multi" && activeSection.selected.size > 0 && activeSection.onClear && /* @__PURE__ */ jsx("div", { className: "shrink-0 border-t border-border p-1", children: /* @__PURE__ */ jsxs(
18619
18662
  Button,
18620
18663
  {
18621
18664
  variant: "ghost",