@caratlogic/design-system 0.1.37 → 0.1.39

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.
Files changed (31) hide show
  1. package/dist/components/molecules/Filters/ActiveFilterChips/ActiveFilterChips.d.ts +1 -1
  2. package/dist/components/molecules/Filters/ActiveFilterChips/ActiveFilterChips.d.ts.map +1 -1
  3. package/dist/components/molecules/Filters/ActiveFilterChips/ActiveFilterChips.types.d.ts +2 -0
  4. package/dist/components/molecules/Filters/ActiveFilterChips/ActiveFilterChips.types.d.ts.map +1 -1
  5. package/dist/components/molecules/Filters/ActiveFilterChips/ActiveFilterChips.utils.d.ts +1 -0
  6. package/dist/components/molecules/Filters/ActiveFilterChips/ActiveFilterChips.utils.d.ts.map +1 -1
  7. package/dist/components/molecules/Filters/FilterPanel/FilterPanel.utils.d.ts.map +1 -1
  8. package/dist/components/molecules/Filters/Filters.utils.d.ts.map +1 -1
  9. package/dist/components/molecules/Filters/PickListFilter/PickListFilter.d.ts.map +1 -1
  10. package/dist/components/molecules/Filters/PickListFilter/PickListFilter.utils.d.ts +2 -1
  11. package/dist/components/molecules/Filters/PickListFilter/PickListFilter.utils.d.ts.map +1 -1
  12. package/dist/components/molecules/Filters/ScaleRangeFilter/ScaleRangeFilter.d.ts +2 -2
  13. package/dist/components/molecules/Filters/ScaleRangeFilter/ScaleRangeFilter.d.ts.map +1 -1
  14. package/dist/components/molecules/Filters/ScaleRangeFilter/ScaleRangeFilter.types.d.ts +4 -1
  15. package/dist/components/molecules/Filters/ScaleRangeFilter/ScaleRangeFilter.types.d.ts.map +1 -1
  16. package/dist/components/molecules/Filters/ScaleRangeFilter/ScaleRangeFilter.utils.d.ts +2 -0
  17. package/dist/components/molecules/Filters/ScaleRangeFilter/ScaleRangeFilter.utils.d.ts.map +1 -1
  18. package/dist/components/molecules/Filters/ScaleRangeFilter/index.d.ts +1 -1
  19. package/dist/components/molecules/Filters/ScaleRangeFilter/index.d.ts.map +1 -1
  20. package/dist/components/organisms/Filters/FilterField/FilterField.d.ts.map +1 -1
  21. package/dist/components/organisms/Filters/FilterSurface/FilterSurface.accessibility.d.ts +1 -2
  22. package/dist/components/organisms/Filters/FilterSurface/FilterSurface.accessibility.d.ts.map +1 -1
  23. package/dist/components/organisms/Filters/FilterSurface/FilterSurface.d.ts +2 -2
  24. package/dist/components/organisms/Filters/FilterSurface/FilterSurface.d.ts.map +1 -1
  25. package/dist/components/organisms/Filters/FilterSurface/FilterSurface.types.d.ts +7 -7
  26. package/dist/components/organisms/Filters/FilterSurface/FilterSurface.types.d.ts.map +1 -1
  27. package/dist/index.cjs.js +124 -90
  28. package/dist/index.cjs.js.map +1 -1
  29. package/dist/index.esm.js +124 -90
  30. package/dist/index.esm.js.map +1 -1
  31. package/package.json +1 -1
package/dist/index.cjs.js CHANGED
@@ -15514,7 +15514,10 @@ function formatRuleValue(value, unit) {
15514
15514
  const number = value.toLocaleString('en-US', { maximumFractionDigits: 2 });
15515
15515
  if (!unit)
15516
15516
  return number;
15517
- return unit === '%' ? `${number}%` : `${number} ${unit}`;
15517
+ // Symbols sit right after the number (`60%`, `34°`); words take a space (`2 ct`).
15518
+ return unit === '%' || unit === '°'
15519
+ ? `${number}${unit}`
15520
+ : `${number} ${unit}`;
15518
15521
  }
15519
15522
  /** The help popover's rule list for a column's typical range; rules use short numbers (`1k`) that the field accepts as typed. */
15520
15523
  function buildNumberRuleHelp({ low, high, exact, unit, } = DEFAULT_NUMBER_RULE_EXAMPLES) {
@@ -15737,7 +15740,7 @@ function getOptionRowClassName(selected = false, className) {
15737
15740
  }
15738
15741
  /** A selectable pill (chip options, range presets); one height and shape everywhere. */
15739
15742
  function getPillClassName(selected) {
15740
- return getSelectableOptionClassName(selected, 'inline-flex h-8 items-center gap-1.5 rounded-full px-3');
15743
+ return getSelectableOptionClassName(selected, 'inline-flex h-8 items-center gap-1.5 rounded-sm px-3');
15741
15744
  }
15742
15745
  /** Search box above a list (pick lists, saved searches); matches the inputs' height and corners. */
15743
15746
  const FILTER_SEARCH_BOX_CLASS_NAME = 'flex h-9 items-center gap-2 rounded-md border border-input bg-card px-3 focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/30';
@@ -15769,8 +15772,10 @@ function getPanelClassName(variant, active, className) {
15769
15772
  return cn('flex min-w-0 flex-col gap-3', variant === 'card' && 'rounded-xl border bg-card p-4', variant === 'card' && (active ? 'border-primary' : 'border-border'), variant === 'popover' && 'p-3', className);
15770
15773
  }
15771
15774
  function getTitleClassName(variant) {
15772
- return cn('flex items-center gap-2 text-foreground', variant === 'section'
15773
- ? 'text-xs font-medium text-label'
15775
+ return cn('flex items-center gap-2 text-foreground',
15776
+ // A section title is a field name: between the card title and grey helper text.
15777
+ variant === 'section'
15778
+ ? 'text-xs font-medium text-foreground/70'
15774
15779
  : 'text-sm font-semibold');
15775
15780
  }
15776
15781
 
@@ -15918,14 +15923,15 @@ const LIST_COLUMN_MIN_WIDTH = '10rem';
15918
15923
  const getOptionsScrollClassName = (fill) => fill ? OPTIONS_FILL_CLASS_NAME : OPTIONS_SCROLL_CLASS_NAME;
15919
15924
  const LIST_ROW_CLASS_NAME = getOptionRowClassName();
15920
15925
  const COUNT_BADGE_TONES = {
15921
- neutral: 'bg-muted text-muted-foreground',
15922
- tinted: 'bg-primary/10 text-primary',
15923
- solid: 'bg-primary-foreground/20 text-primary-foreground',
15926
+ neutral: 'border-primary/30 bg-primary/5 text-primary',
15927
+ tinted: 'border-primary/30 bg-primary/5 text-primary',
15928
+ solid: 'border-primary-foreground/60 bg-primary-foreground/20 text-primary-foreground',
15924
15929
  };
15925
15930
  /** How many items carry the option, as a small round badge at the right. */
15926
- const getCountBadgeClassName = (tone) => cn('inline-flex h-5 min-w-5 shrink-0 items-center justify-center rounded-full px-1.5 text-xs font-medium tabular-nums', COUNT_BADGE_TONES[tone]);
15931
+ const getCountBadgeClassName = (tone) => cn('inline-flex h-5 min-w-7 shrink-0 items-center justify-center rounded-full border px-1.5 text-xs font-medium tabular-nums', COUNT_BADGE_TONES[tone]);
15927
15932
  const COUNT_BADGE_CLASS_NAME = getCountBadgeClassName('neutral');
15928
- const formatOptionCount = (count) => count.toLocaleString('en-US');
15933
+ /** Plain digits (no thousands separators), so the badge stays compact. */
15934
+ const formatOptionCount = (count) => String(count);
15929
15935
  /** Picking a value: a single-choice list replaces (or clears) it, a multi-choice list toggles it. */
15930
15936
  const pickValue = (values, value, single) => {
15931
15937
  if (!single)
@@ -15981,7 +15987,7 @@ function PickListFilter({ options, value, onChange, ariaLabel, layout = 'list',
15981
15987
  if (layout === 'tiles') {
15982
15988
  return (jsxRuntime.jsx(ScrollFade, { role: "group", "aria-label": ariaLabel, wrapperClassName: wrapperClassName, className: cn('grid content-start gap-1.5', scrollClassName), style: getGridColumnsStyle(columns), children: visibleOptions.map((option) => (jsxRuntime.jsxs("button", { type: "button", "aria-pressed": value.includes(option.value), onClick: () => toggle(option), className: getTileClassName(value.includes(option.value), option.count !== undefined), children: [jsxRuntime.jsx(TruncatedLabel, { text: option.label }), option.count !== undefined && (jsxRuntime.jsx("span", { className: getCountBadgeClassName(value.includes(option.value) ? 'tinted' : 'neutral'), children: formatOptionCount(option.count) }))] }, option.value))) }));
15983
15989
  }
15984
- return (jsxRuntime.jsx(ScrollFade, { role: "group", "aria-label": ariaLabel, wrapperClassName: wrapperClassName, className: cn('flex flex-wrap content-start gap-1.5', scrollClassName), children: visibleOptions.map((option) => (jsxRuntime.jsxs("button", { type: "button", "aria-pressed": value.includes(option.value), onClick: () => toggle(option), className: getChipClassName(value.includes(option.value)), children: [jsxRuntime.jsx(OptionSwatch, { color: option.swatch, round: true }), jsxRuntime.jsx("span", { children: option.label }), option.count !== undefined && (jsxRuntime.jsx("span", { className: "text-xs font-normal tabular-nums text-muted-foreground", children: formatOptionCount(option.count) }))] }, option.value))) }));
15990
+ return (jsxRuntime.jsx(ScrollFade, { role: "group", "aria-label": ariaLabel, wrapperClassName: wrapperClassName, className: cn('flex flex-wrap content-start gap-1.5', scrollClassName), children: visibleOptions.map((option) => (jsxRuntime.jsxs("button", { type: "button", "aria-pressed": value.includes(option.value), onClick: () => toggle(option), className: getChipClassName(value.includes(option.value)), children: [jsxRuntime.jsx(OptionSwatch, { color: option.swatch, round: true }), jsxRuntime.jsx("span", { children: option.label }), option.count !== undefined && (jsxRuntime.jsx("span", { className: getCountBadgeClassName(value.includes(option.value) ? 'tinted' : 'neutral'), children: formatOptionCount(option.count) }))] }, option.value))) }));
15985
15991
  }
15986
15992
 
15987
15993
  const TRIGGER_BASE = 'h-9 !w-fit shadow-[0_1px_2px_0_rgba(0,0,0,0.05)] gap-2 text-foreground bg-card hover:bg-muted/40 border border-border rounded-lg text-sm font-medium px-3';
@@ -38596,6 +38602,70 @@ function fieldTypeIcon(type) {
38596
38602
  return FIELD_TYPE_ICONS[type] ?? Type;
38597
38603
  }
38598
38604
 
38605
+ const SAVED_SEARCH_CHIP_CLASS_NAME = 'inline-flex h-6 min-w-0 max-w-full items-center rounded-full border border-primary/30 bg-primary/5 px-2 text-xs text-foreground';
38606
+ /** Children laid out below the first one's line. */
38607
+ function countWrapped(items) {
38608
+ if (items.length === 0)
38609
+ return 0;
38610
+ const firstTop = items[0].offsetTop;
38611
+ return items.filter((item) => item.offsetTop > firstTop).length;
38612
+ }
38613
+ const getShowMoreLabel = (hiddenCount) => `+${hiddenCount} more`;
38614
+
38615
+ /**
38616
+ * Fits chips on one line with the "+N more" link right after the last one. Renders every chip, counts
38617
+ * how many wrap, then drops chips until the link itself fits; a resize starts the measurement over.
38618
+ */
38619
+ function useChipOverflow(chips, enabled) {
38620
+ const [list, setList] = React.useState(null);
38621
+ const [expanded, setExpanded] = React.useState(false);
38622
+ const [visibleCount, setVisibleCount] = React.useState(chips.length);
38623
+ const collapsed = enabled && !expanded;
38624
+ const [measuredLength, setMeasuredLength] = React.useState(chips.length);
38625
+ if (measuredLength !== chips.length) {
38626
+ setMeasuredLength(chips.length);
38627
+ setVisibleCount(chips.length);
38628
+ }
38629
+ React.useLayoutEffect(() => {
38630
+ if (!collapsed || !list)
38631
+ return;
38632
+ const items = Array.from(list.children);
38633
+ if (visibleCount >= chips.length) {
38634
+ const wrapped = countWrapped(items);
38635
+ if (wrapped > 0)
38636
+ setVisibleCount(chips.length - wrapped);
38637
+ return;
38638
+ }
38639
+ // The last child is the "+N more" link; if it wrapped, give it one more chip's room.
38640
+ if (countWrapped(items) > 0 && visibleCount > 1)
38641
+ setVisibleCount(visibleCount - 1);
38642
+ }, [collapsed, list, visibleCount, chips.length]);
38643
+ React.useEffect(() => {
38644
+ if (!collapsed || !list)
38645
+ return;
38646
+ let width = list.clientWidth;
38647
+ const observer = new ResizeObserver(() => {
38648
+ if (list.clientWidth === width)
38649
+ return;
38650
+ width = list.clientWidth;
38651
+ setVisibleCount(chips.length);
38652
+ });
38653
+ observer.observe(list);
38654
+ return () => observer.disconnect();
38655
+ }, [collapsed, list, chips.length]);
38656
+ const shown = collapsed ? Math.min(visibleCount, chips.length) : chips.length;
38657
+ return {
38658
+ listRef: React.useCallback((node) => setList(node), []),
38659
+ expanded,
38660
+ toggle: () => {
38661
+ setExpanded((current) => !current);
38662
+ setVisibleCount(chips.length);
38663
+ },
38664
+ visibleChips: chips.slice(0, shown),
38665
+ hiddenCount: chips.length - shown,
38666
+ };
38667
+ }
38668
+
38599
38669
  const getRemoveChipLabel = (label) => `Remove ${label} filter`;
38600
38670
  const FILTERED_BY_LABEL = 'Filtered by';
38601
38671
 
@@ -38603,16 +38673,33 @@ const FILTERED_BY_LABEL = 'Filtered by';
38603
38673
  const CHIP_CLASS = 'inline-flex h-7 max-w-full items-center gap-1 rounded-md border border-primary/30 bg-primary/5 px-2.5 text-xs text-foreground';
38604
38674
  const REMOVE_BUTTON_CLASS = 'inline-flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-full text-muted-foreground outline-none hover:bg-primary/10 hover:text-foreground focus-visible:ring-2 focus-visible:ring-primary/30';
38605
38675
  const CLEAR_ALL_CLASS = cn(FILTER_LINK_CLASS_NAME, 'underline');
38676
+ const SHOW_MORE_CLASS = cn(FILTER_LINK_CLASS_NAME, 'whitespace-nowrap px-1 underline underline-offset-2');
38606
38677
  function getChipLabelText(label, variant) {
38607
38678
  return variant === 'inline' ? `${label}:` : label;
38608
38679
  }
38609
38680
 
38681
+ function Chip({ chip, variant, onRemove }) {
38682
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { className: "shrink-0 font-semibold", children: getChipLabelText(chip.label, variant) }), jsxRuntime.jsx(TruncatedLabel, { text: chip.value }), onRemove && (jsxRuntime.jsx("button", { type: "button", "aria-label": getRemoveChipLabel(chip.label), onClick: () => onRemove(chip.id), className: REMOVE_BUTTON_CLASS, children: jsxRuntime.jsx(X$2, { "aria-hidden": "true", className: "size-3" }) }))] }));
38683
+ }
38684
+ /** One line of chips ending in "+N more"; Clear all and the actions stay at the right end. */
38685
+ function CollapsibleChips({ chips, onRemove, variant = 'inline', clearAll, actions, className, }) {
38686
+ const { listRef, expanded, toggle, visibleChips, hiddenCount } = useChipOverflow(chips.map((chip) => chip.id), true);
38687
+ const visible = new Set(visibleChips);
38688
+ return (
38689
+ // Centred, so the label and Clear all line up with buttons beside the row even when the chips wrap.
38690
+ jsxRuntime.jsxs("div", { className: cn('flex items-center gap-1.5', className), children: [variant === 'inline' && (jsxRuntime.jsx("span", { className: "mr-0.5 flex h-7 shrink-0 items-center text-xs text-muted-foreground", children: FILTERED_BY_LABEL })), jsxRuntime.jsxs("ul", { ref: listRef, className: "flex min-w-0 flex-1 flex-wrap items-center gap-1.5", children: [chips
38691
+ .filter((chip) => visible.has(chip.id))
38692
+ .map((chip) => (jsxRuntime.jsx("li", { className: CHIP_CLASS, children: jsxRuntime.jsx(Chip, { chip: chip, variant: variant, onRemove: onRemove }) }, chip.id))), (expanded || hiddenCount > 0) && (jsxRuntime.jsx("li", { className: "flex h-7 shrink-0 items-center", children: jsxRuntime.jsx("button", { type: "button", "aria-expanded": expanded, onClick: toggle, className: SHOW_MORE_CLASS, children: expanded ? 'Show less' : getShowMoreLabel(hiddenCount) }) }))] }), (clearAll || actions) && (jsxRuntime.jsxs("div", { className: "ml-1.5 flex h-7 shrink-0 items-center gap-3", children: [clearAll, actions] }))] }));
38693
+ }
38610
38694
  /** Applied filters as removable chips, with an optional Clear all. */
38611
- function ActiveFilterChips({ chips, onRemove, onClearAll, variant = 'inline', actions, className, }) {
38695
+ function ActiveFilterChips({ chips, onRemove, onClearAll, variant = 'inline', actions, collapsible = false, className, }) {
38612
38696
  if (chips.length === 0)
38613
38697
  return null;
38614
38698
  const clearAll = onClearAll && (jsxRuntime.jsx("button", { type: "button", onClick: onClearAll, className: CLEAR_ALL_CLASS, children: "Clear all" }));
38615
- return (jsxRuntime.jsxs("div", { className: cn('flex flex-wrap items-center gap-1.5', className), children: [variant === 'inline' && (jsxRuntime.jsx("span", { className: "mr-0.5 text-xs text-muted-foreground", children: FILTERED_BY_LABEL })), chips.map((chip) => (jsxRuntime.jsxs("span", { className: CHIP_CLASS, children: [jsxRuntime.jsx("span", { className: "shrink-0 font-semibold", children: getChipLabelText(chip.label, variant) }), jsxRuntime.jsx(TruncatedLabel, { text: chip.value }), onRemove && (jsxRuntime.jsx("button", { type: "button", "aria-label": getRemoveChipLabel(chip.label), onClick: () => onRemove(chip.id), className: REMOVE_BUTTON_CLASS, children: jsxRuntime.jsx(X$2, { "aria-hidden": "true", className: "size-3" }) }))] }, chip.id))), actions ? (
38699
+ if (collapsible) {
38700
+ return (jsxRuntime.jsx(CollapsibleChips, { chips: chips, onRemove: onRemove, variant: variant, clearAll: clearAll, actions: actions, className: className }));
38701
+ }
38702
+ return (jsxRuntime.jsxs("div", { className: cn('flex flex-wrap items-center gap-1.5', className), children: [variant === 'inline' && (jsxRuntime.jsx("span", { className: "mr-0.5 text-xs text-muted-foreground", children: FILTERED_BY_LABEL })), chips.map((chip) => (jsxRuntime.jsx("span", { className: CHIP_CLASS, children: jsxRuntime.jsx(Chip, { chip: chip, variant: variant, onRemove: onRemove }) }, chip.id))), actions ? (
38616
38703
  // With actions, Clear all joins them at the row's right end.
38617
38704
  jsxRuntime.jsxs("div", { className: "ml-auto flex items-center gap-3 pl-3", children: [clearAll, actions] })) : (clearAll)] }));
38618
38705
  }
@@ -38814,14 +38901,18 @@ function getNextScaleRange(anchor, option) {
38814
38901
  ? { from: option, to: option }
38815
38902
  : { from: anchor, to: option };
38816
38903
  }
38904
+ const SCALE_POSITION_CLASS_NAMES = {
38905
+ // Unselected options look exactly like unselected pick-list options.
38906
+ outside: getSelectableOptionClassName(false),
38907
+ end: 'border-primary bg-primary font-medium text-primary-foreground',
38908
+ between: 'border-primary/30 bg-primary/10 font-medium text-primary',
38909
+ };
38910
+ /** A pill like the other pick lists' (`Single`, `Parcel`), coloured by where it sits in the range. */
38911
+ const getScalePillClassName = (position) => cn('inline-flex h-8 cursor-pointer items-center gap-1.5 rounded-sm border px-3 text-xs transition-colors', FILTER_FOCUS_RING, SCALE_POSITION_CLASS_NAMES[position]);
38817
38912
  function getScaleButtonClassName(position, fill = false, hasCount = false) {
38818
38913
  return cn('flex min-w-0 cursor-pointer items-center gap-2 rounded-md border transition-colors',
38819
38914
  // With a count: name left, badge right (like a list row); without: name centred.
38820
- hasCount ? 'justify-between text-left' : 'justify-center text-center', fill ? 'min-h-12 px-2 py-1.5 text-sm' : 'min-h-10 px-1.5 py-1 text-xs', FILTER_FOCUS_RING,
38821
- // Unselected options look exactly like unselected pick-list tiles.
38822
- position === 'outside' && getSelectableOptionClassName(false), position === 'end' &&
38823
- 'border-primary bg-primary font-medium text-primary-foreground', position === 'between' &&
38824
- 'border-primary/30 bg-primary/10 font-medium text-primary');
38915
+ hasCount ? 'justify-between text-left' : 'justify-center text-center', fill ? 'min-h-12 px-2 py-1.5 text-sm' : 'min-h-10 px-1.5 py-1 text-xs', FILTER_FOCUS_RING, SCALE_POSITION_CLASS_NAMES[position]);
38825
38916
  }
38826
38917
  /** The count badge follows the tile: plain, tinted inside the range, solid on an end. */
38827
38918
  const getScaleCountTone = (position) => position === 'end' ? 'solid' : position === 'between' ? 'tinted' : 'neutral';
@@ -38841,8 +38932,14 @@ function useScaleRangeFilter({ value, onChange, }) {
38841
38932
  }
38842
38933
 
38843
38934
  /** Ordered-scale filter: click a start option, then an end option. */
38844
- function ScaleRangeFilter({ options, value, onChange, ariaLabel, columns, fill = false, }) {
38935
+ function ScaleRangeFilter({ options, value, onChange, ariaLabel, layout = 'pills', columns, fill = false, }) {
38845
38936
  const { selectOption } = useScaleRangeFilter({ value, onChange });
38937
+ if (layout === 'pills') {
38938
+ return (jsxRuntime.jsx("div", { role: "group", "aria-label": ariaLabel, className: "flex flex-wrap content-start gap-1.5", children: options.map((option) => {
38939
+ const position = getScalePosition(options, value, option.value);
38940
+ return (jsxRuntime.jsxs("button", { type: "button", "aria-pressed": position !== 'outside', "aria-label": option.label, onClick: () => selectOption(option.value), className: getScalePillClassName(position), children: [jsxRuntime.jsx("span", { children: option.label }), option.count !== undefined && (jsxRuntime.jsx("span", { className: getCountBadgeClassName(getScaleCountTone(position)), children: formatOptionCount(option.count) }))] }, option.value));
38941
+ }) }));
38942
+ }
38846
38943
  return (jsxRuntime.jsx("div", { role: "group", "aria-label": ariaLabel, className: fill ? 'grid gap-2' : 'grid gap-1.5', style: getGridColumnsStyle(columns), children: options.map((option) => {
38847
38944
  const position = getScalePosition(options, value, option.value);
38848
38945
  return (jsxRuntime.jsxs("button", { type: "button", "aria-pressed": position !== 'outside', "aria-label": option.label, onClick: () => selectOption(option.value), className: getScaleButtonClassName(position, fill, option.count !== undefined), children: [jsxRuntime.jsx(TruncatedLabel, { text: option.label }), option.count !== undefined && (jsxRuntime.jsx("span", { className: getCountBadgeClassName(getScaleCountTone(position)), children: formatOptionCount(option.count) }))] }, option.value));
@@ -38856,7 +38953,7 @@ const TILE_KEYS = ['a', 'b', 'c', 'd'];
38856
38953
  const CARD_CLASS_NAME = 'flex h-full min-w-0 flex-col gap-3 rounded-xl border border-border bg-card p-4';
38857
38954
 
38858
38955
  function Pills({ count }) {
38859
- return (jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1.5", children: PILL_WIDTHS.slice(0, count).map((width) => (jsxRuntime.jsx(SquareSkeleton, { height: 32, width: width, rounded: "full" }, width))) }));
38956
+ return (jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1.5", children: PILL_WIDTHS.slice(0, count).map((width) => (jsxRuntime.jsx(SquareSkeleton, { height: 32, width: width, rounded: "sm" }, width))) }));
38860
38957
  }
38861
38958
  function Body({ variant, }) {
38862
38959
  switch (variant) {
@@ -39166,70 +39263,6 @@ function TriStateFilter({ value, onChange, ariaLabel, compact = false, }) {
39166
39263
  }) }));
39167
39264
  }
39168
39265
 
39169
- const SAVED_SEARCH_CHIP_CLASS_NAME = 'inline-flex h-6 min-w-0 max-w-full items-center rounded-full border border-primary/30 bg-primary/5 px-2 text-xs text-foreground';
39170
- /** Children laid out below the first one's line. */
39171
- function countWrapped(items) {
39172
- if (items.length === 0)
39173
- return 0;
39174
- const firstTop = items[0].offsetTop;
39175
- return items.filter((item) => item.offsetTop > firstTop).length;
39176
- }
39177
- const getShowMoreLabel = (hiddenCount) => `+${hiddenCount} more`;
39178
-
39179
- /**
39180
- * Fits chips on one line with the "+N more" link right after the last one. Renders every chip, counts
39181
- * how many wrap, then drops chips until the link itself fits; a resize starts the measurement over.
39182
- */
39183
- function useChipOverflow(chips, enabled) {
39184
- const [list, setList] = React.useState(null);
39185
- const [expanded, setExpanded] = React.useState(false);
39186
- const [visibleCount, setVisibleCount] = React.useState(chips.length);
39187
- const collapsed = enabled && !expanded;
39188
- const [measuredLength, setMeasuredLength] = React.useState(chips.length);
39189
- if (measuredLength !== chips.length) {
39190
- setMeasuredLength(chips.length);
39191
- setVisibleCount(chips.length);
39192
- }
39193
- React.useLayoutEffect(() => {
39194
- if (!collapsed || !list)
39195
- return;
39196
- const items = Array.from(list.children);
39197
- if (visibleCount >= chips.length) {
39198
- const wrapped = countWrapped(items);
39199
- if (wrapped > 0)
39200
- setVisibleCount(chips.length - wrapped);
39201
- return;
39202
- }
39203
- // The last child is the "+N more" link; if it wrapped, give it one more chip's room.
39204
- if (countWrapped(items) > 0 && visibleCount > 1)
39205
- setVisibleCount(visibleCount - 1);
39206
- }, [collapsed, list, visibleCount, chips.length]);
39207
- React.useEffect(() => {
39208
- if (!collapsed || !list)
39209
- return;
39210
- let width = list.clientWidth;
39211
- const observer = new ResizeObserver(() => {
39212
- if (list.clientWidth === width)
39213
- return;
39214
- width = list.clientWidth;
39215
- setVisibleCount(chips.length);
39216
- });
39217
- observer.observe(list);
39218
- return () => observer.disconnect();
39219
- }, [collapsed, list, chips.length]);
39220
- const shown = collapsed ? Math.min(visibleCount, chips.length) : chips.length;
39221
- return {
39222
- listRef: React.useCallback((node) => setList(node), []),
39223
- expanded,
39224
- toggle: () => {
39225
- setExpanded((current) => !current);
39226
- setVisibleCount(chips.length);
39227
- },
39228
- visibleChips: chips.slice(0, shown),
39229
- hiddenCount: chips.length - shown,
39230
- };
39231
- }
39232
-
39233
39266
  /** Read-only filter summaries of a saved search, as pills; optionally one line ending in "+N more". */
39234
39267
  function SavedSearchChipList({ chips, emptyText = 'No filters', collapsible = false, className, }) {
39235
39268
  const { listRef, expanded, toggle, visibleChips, hiddenCount } = useChipOverflow(chips, collapsible);
@@ -77053,7 +77086,7 @@ const getDropdownPlaceholder = (field) => field.inputPlaceholder ??
77053
77086
  `${field.single ? 'Any' : 'Select'} ${field.label.toLowerCase()}`;
77054
77087
  /** Popover variant: the same control and layout as the card (pills up to 4 options, then the list); tile grids drop to two columns. */
77055
77088
  function toPopoverField(field) {
77056
- if (field.type === 'scale' ||
77089
+ if ((field.type === 'scale' && field.layout === 'tiles') ||
77057
77090
  (field.type === 'list' && resolveListLayout(field) === 'tiles')) {
77058
77091
  return { ...field, columns: POPOVER_TILE_COLUMNS };
77059
77092
  }
@@ -77086,7 +77119,7 @@ function FilterFieldControl({ field, value, onChange, fiscalYearStartMonth, fill
77086
77119
  return (jsxRuntime.jsx(PickListFilter, { ariaLabel: ariaLabel, options: field.options ?? [], value: valueOfType(value, 'list')?.values ?? [], onChange: (values) => onChange(toListValue(values)), layout: layout, searchPlaceholder: layout === 'list' ? field.searchPlaceholder : undefined, columns: field.columns, single: field.single }));
77087
77120
  }
77088
77121
  case 'scale':
77089
- return (jsxRuntime.jsx(ScaleRangeFilter, { ariaLabel: ariaLabel, options: field.options ?? [], value: valueOfType(value, 'scale')?.range, onChange: (range) => onChange(toScaleValue(range)), columns: field.columns, fill: fill }));
77122
+ return (jsxRuntime.jsx(ScaleRangeFilter, { ariaLabel: ariaLabel, options: field.options ?? [], value: valueOfType(value, 'scale')?.range, onChange: (range) => onChange(toScaleValue(range)), layout: field.layout === 'tiles' ? 'tiles' : 'pills', columns: field.columns, fill: fill }));
77090
77123
  case 'number':
77091
77124
  return (jsxRuntime.jsx(NumberRangeInputs, { ariaLabel: field.label, value: valueOfType(value, 'number')?.range, onChange: (range) => onChange(toNumberValue(range)), presets: field.presets, presetsLabel: field.presetsLabel }));
77092
77125
  case 'ids':
@@ -77132,9 +77165,8 @@ function FilterField(props) {
77132
77165
  return mode === 'cell' ? (jsxRuntime.jsx(FilterFieldCell, { ...rest })) : (jsxRuntime.jsx(FilterFieldCard, { mode: mode, ...rest }));
77133
77166
  }
77134
77167
 
77135
- /** Both leave without applying — the draft is discarded. */
77168
+ /** Leaves without applying; the draft is discarded. */
77136
77169
  const BACK_LABEL = 'Back without applying';
77137
- const CLOSE_LABEL = 'Close without applying';
77138
77170
  const LOADING_LABEL = 'Loading filters';
77139
77171
  const ERROR_TITLE = "Couldn't load filters";
77140
77172
  const DEFAULT_ERROR_MESSAGE = 'Something went wrong while loading the filters. Check your connection and try again.';
@@ -77204,10 +77236,12 @@ function MasonryItem({ children }) {
77204
77236
  return (jsxRuntime.jsx("div", { style: span > 0 ? { gridRowEnd: `span ${span}` } : undefined, children: jsxRuntime.jsx("div", { ref: ref, className: "pb-3", children: children }) }));
77205
77237
  }
77206
77238
 
77207
- /** Full-page filter screen (e.g. an advanced search): header, scrolling card grid, sticky apply footer. */
77208
- function FilterSurface({ title, headerActions, footerChips, footerActions, onClose, status = 'ready', errorMessage, onRetry, skeleton, children, }) {
77239
+ /** Full-page filter screen (e.g. an advanced search): one sticky header with every action, then the scrolling card grid. */
77240
+ function FilterSurface({ title, headerActions, primaryActions, chips, onClose, status = 'ready', errorMessage, onRetry, skeleton, children, }) {
77209
77241
  useCloseOnEscape(onClose);
77210
- return (jsxRuntime.jsxs("div", { role: "region", "aria-label": title, className: "flex h-full min-h-0 flex-col bg-background", children: [jsxRuntime.jsxs("header", { className: "sticky top-0 z-20 flex shrink-0 flex-wrap items-end justify-between gap-3 bg-background px-3 pt-2 pb-3", children: [jsxRuntime.jsx("div", { className: "flex flex-col gap-1", children: jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [jsxRuntime.jsx(IconButton, { appearance: "ghost", size: "sm", "aria-label": BACK_LABEL, title: BACK_LABEL, onClick: onClose, children: jsxRuntime.jsx(ArrowLeft, {}) }), jsxRuntime.jsx("h1", { className: "text-xl font-semibold text-foreground", children: title })] }) }), jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [headerActions, jsxRuntime.jsx(IconButton, { appearance: "outline", size: "md", "aria-label": CLOSE_LABEL, title: CLOSE_LABEL, onClick: onClose, children: jsxRuntime.jsx(X$2, {}) })] })] }), jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-3 pb-3", children: status === 'loading' ? (jsxRuntime.jsx(FilterSurfaceSkeleton, { children: skeleton })) : status === 'error' ? (jsxRuntime.jsx(FilterSurfaceError, { message: errorMessage ?? DEFAULT_ERROR_MESSAGE, onRetry: onRetry })) : (jsxRuntime.jsx("div", { className: "flex flex-col gap-3", children: children })) }), status === 'ready' && (footerChips || footerActions) && (jsxRuntime.jsxs("footer", { className: "sticky bottom-0 z-20 flex flex-wrap items-center justify-between gap-3 border-t border-border bg-card px-3 py-2", children: [jsxRuntime.jsx("div", { className: "min-w-0 flex-1", children: footerChips }), jsxRuntime.jsx("div", { className: "flex shrink-0 items-center gap-2", children: footerActions })] }))] }));
77242
+ const ready = status === 'ready';
77243
+ const actions = ready ? primaryActions : undefined;
77244
+ return (jsxRuntime.jsxs("div", { role: "region", "aria-label": title, className: "flex h-full min-h-0 flex-col bg-background", children: [jsxRuntime.jsxs("header", { className: "sticky top-0 z-20 flex shrink-0 flex-wrap items-center gap-x-3 gap-y-2 bg-background px-3 py-2.5", children: [jsxRuntime.jsx(IconButton, { appearance: "ghost", size: "sm", "aria-label": BACK_LABEL, title: BACK_LABEL, onClick: onClose, children: jsxRuntime.jsx(ArrowLeft, {}) }), jsxRuntime.jsx("h1", { className: "sr-only", children: title }), jsxRuntime.jsx("div", { className: "min-w-0 flex-1", children: ready && chips }), jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [headerActions, actions] })] }), jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-3 pb-3", children: status === 'loading' ? (jsxRuntime.jsx(FilterSurfaceSkeleton, { children: skeleton })) : status === 'error' ? (jsxRuntime.jsx(FilterSurfaceError, { message: errorMessage ?? DEFAULT_ERROR_MESSAGE, onRetry: onRetry })) : (jsxRuntime.jsx("div", { className: "flex flex-col gap-3", children: children })) })] }));
77211
77245
  }
77212
77246
  function FilterSurfaceGroup({ title, masonry = false, className, children, }) {
77213
77247
  return (jsxRuntime.jsxs("section", { className: "flex flex-col gap-3", children: [title && (jsxRuntime.jsx("h2", { className: "text-xs font-semibold uppercase tracking-wide text-label", children: title })), jsxRuntime.jsx("div", { className: cn('grid grid-cols-1 items-start lg:grid-cols-6', masonry ? MASONRY_GRID_CLASS_NAME : 'gap-3', className), children: masonry