@caratlogic/design-system 0.1.37 → 0.1.38

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 (29) 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/PickListFilter/PickListFilter.d.ts.map +1 -1
  8. package/dist/components/molecules/Filters/PickListFilter/PickListFilter.utils.d.ts +2 -1
  9. package/dist/components/molecules/Filters/PickListFilter/PickListFilter.utils.d.ts.map +1 -1
  10. package/dist/components/molecules/Filters/ScaleRangeFilter/ScaleRangeFilter.d.ts +2 -2
  11. package/dist/components/molecules/Filters/ScaleRangeFilter/ScaleRangeFilter.d.ts.map +1 -1
  12. package/dist/components/molecules/Filters/ScaleRangeFilter/ScaleRangeFilter.types.d.ts +4 -1
  13. package/dist/components/molecules/Filters/ScaleRangeFilter/ScaleRangeFilter.types.d.ts.map +1 -1
  14. package/dist/components/molecules/Filters/ScaleRangeFilter/ScaleRangeFilter.utils.d.ts +2 -0
  15. package/dist/components/molecules/Filters/ScaleRangeFilter/ScaleRangeFilter.utils.d.ts.map +1 -1
  16. package/dist/components/molecules/Filters/ScaleRangeFilter/index.d.ts +1 -1
  17. package/dist/components/molecules/Filters/ScaleRangeFilter/index.d.ts.map +1 -1
  18. package/dist/components/organisms/Filters/FilterField/FilterField.d.ts.map +1 -1
  19. package/dist/components/organisms/Filters/FilterSurface/FilterSurface.accessibility.d.ts +1 -2
  20. package/dist/components/organisms/Filters/FilterSurface/FilterSurface.accessibility.d.ts.map +1 -1
  21. package/dist/components/organisms/Filters/FilterSurface/FilterSurface.d.ts +2 -2
  22. package/dist/components/organisms/Filters/FilterSurface/FilterSurface.d.ts.map +1 -1
  23. package/dist/components/organisms/Filters/FilterSurface/FilterSurface.types.d.ts +7 -7
  24. package/dist/components/organisms/Filters/FilterSurface/FilterSurface.types.d.ts.map +1 -1
  25. package/dist/index.cjs.js +116 -87
  26. package/dist/index.cjs.js.map +1 -1
  27. package/dist/index.esm.js +116 -87
  28. package/dist/index.esm.js.map +1 -1
  29. package/package.json +1 -1
package/dist/index.esm.js CHANGED
@@ -15707,7 +15707,7 @@ function getOptionRowClassName(selected = false, className) {
15707
15707
  }
15708
15708
  /** A selectable pill (chip options, range presets); one height and shape everywhere. */
15709
15709
  function getPillClassName(selected) {
15710
- return getSelectableOptionClassName(selected, 'inline-flex h-8 items-center gap-1.5 rounded-full px-3');
15710
+ return getSelectableOptionClassName(selected, 'inline-flex h-8 items-center gap-1.5 rounded-sm px-3');
15711
15711
  }
15712
15712
  /** Search box above a list (pick lists, saved searches); matches the inputs' height and corners. */
15713
15713
  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';
@@ -15888,14 +15888,15 @@ const LIST_COLUMN_MIN_WIDTH = '10rem';
15888
15888
  const getOptionsScrollClassName = (fill) => fill ? OPTIONS_FILL_CLASS_NAME : OPTIONS_SCROLL_CLASS_NAME;
15889
15889
  const LIST_ROW_CLASS_NAME = getOptionRowClassName();
15890
15890
  const COUNT_BADGE_TONES = {
15891
- neutral: 'bg-muted text-muted-foreground',
15892
- tinted: 'bg-primary/10 text-primary',
15893
- solid: 'bg-primary-foreground/20 text-primary-foreground',
15891
+ neutral: 'border-primary/30 bg-primary/5 text-primary',
15892
+ tinted: 'border-primary/30 bg-primary/5 text-primary',
15893
+ solid: 'border-primary-foreground/60 bg-primary-foreground/20 text-primary-foreground',
15894
15894
  };
15895
15895
  /** How many items carry the option, as a small round badge at the right. */
15896
- 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]);
15896
+ 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]);
15897
15897
  const COUNT_BADGE_CLASS_NAME = getCountBadgeClassName('neutral');
15898
- const formatOptionCount = (count) => count.toLocaleString('en-US');
15898
+ /** Plain digits (no thousands separators), so the badge stays compact. */
15899
+ const formatOptionCount = (count) => String(count);
15899
15900
  /** Picking a value: a single-choice list replaces (or clears) it, a multi-choice list toggles it. */
15900
15901
  const pickValue = (values, value, single) => {
15901
15902
  if (!single)
@@ -15951,7 +15952,7 @@ function PickListFilter({ options, value, onChange, ariaLabel, layout = 'list',
15951
15952
  if (layout === 'tiles') {
15952
15953
  return (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) => (jsxs("button", { type: "button", "aria-pressed": value.includes(option.value), onClick: () => toggle(option), className: getTileClassName(value.includes(option.value), option.count !== undefined), children: [jsx(TruncatedLabel, { text: option.label }), option.count !== undefined && (jsx("span", { className: getCountBadgeClassName(value.includes(option.value) ? 'tinted' : 'neutral'), children: formatOptionCount(option.count) }))] }, option.value))) }));
15953
15954
  }
15954
- return (jsx(ScrollFade, { role: "group", "aria-label": ariaLabel, wrapperClassName: wrapperClassName, className: cn('flex flex-wrap content-start gap-1.5', scrollClassName), children: visibleOptions.map((option) => (jsxs("button", { type: "button", "aria-pressed": value.includes(option.value), onClick: () => toggle(option), className: getChipClassName(value.includes(option.value)), children: [jsx(OptionSwatch, { color: option.swatch, round: true }), jsx("span", { children: option.label }), option.count !== undefined && (jsx("span", { className: "text-xs font-normal tabular-nums text-muted-foreground", children: formatOptionCount(option.count) }))] }, option.value))) }));
15955
+ return (jsx(ScrollFade, { role: "group", "aria-label": ariaLabel, wrapperClassName: wrapperClassName, className: cn('flex flex-wrap content-start gap-1.5', scrollClassName), children: visibleOptions.map((option) => (jsxs("button", { type: "button", "aria-pressed": value.includes(option.value), onClick: () => toggle(option), className: getChipClassName(value.includes(option.value)), children: [jsx(OptionSwatch, { color: option.swatch, round: true }), jsx("span", { children: option.label }), option.count !== undefined && (jsx("span", { className: getCountBadgeClassName(value.includes(option.value) ? 'tinted' : 'neutral'), children: formatOptionCount(option.count) }))] }, option.value))) }));
15955
15956
  }
15956
15957
 
15957
15958
  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';
@@ -38566,6 +38567,70 @@ function fieldTypeIcon(type) {
38566
38567
  return FIELD_TYPE_ICONS[type] ?? Type;
38567
38568
  }
38568
38569
 
38570
+ 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';
38571
+ /** Children laid out below the first one's line. */
38572
+ function countWrapped(items) {
38573
+ if (items.length === 0)
38574
+ return 0;
38575
+ const firstTop = items[0].offsetTop;
38576
+ return items.filter((item) => item.offsetTop > firstTop).length;
38577
+ }
38578
+ const getShowMoreLabel = (hiddenCount) => `+${hiddenCount} more`;
38579
+
38580
+ /**
38581
+ * Fits chips on one line with the "+N more" link right after the last one. Renders every chip, counts
38582
+ * how many wrap, then drops chips until the link itself fits; a resize starts the measurement over.
38583
+ */
38584
+ function useChipOverflow(chips, enabled) {
38585
+ const [list, setList] = useState(null);
38586
+ const [expanded, setExpanded] = useState(false);
38587
+ const [visibleCount, setVisibleCount] = useState(chips.length);
38588
+ const collapsed = enabled && !expanded;
38589
+ const [measuredLength, setMeasuredLength] = useState(chips.length);
38590
+ if (measuredLength !== chips.length) {
38591
+ setMeasuredLength(chips.length);
38592
+ setVisibleCount(chips.length);
38593
+ }
38594
+ useLayoutEffect(() => {
38595
+ if (!collapsed || !list)
38596
+ return;
38597
+ const items = Array.from(list.children);
38598
+ if (visibleCount >= chips.length) {
38599
+ const wrapped = countWrapped(items);
38600
+ if (wrapped > 0)
38601
+ setVisibleCount(chips.length - wrapped);
38602
+ return;
38603
+ }
38604
+ // The last child is the "+N more" link; if it wrapped, give it one more chip's room.
38605
+ if (countWrapped(items) > 0 && visibleCount > 1)
38606
+ setVisibleCount(visibleCount - 1);
38607
+ }, [collapsed, list, visibleCount, chips.length]);
38608
+ useEffect(() => {
38609
+ if (!collapsed || !list)
38610
+ return;
38611
+ let width = list.clientWidth;
38612
+ const observer = new ResizeObserver(() => {
38613
+ if (list.clientWidth === width)
38614
+ return;
38615
+ width = list.clientWidth;
38616
+ setVisibleCount(chips.length);
38617
+ });
38618
+ observer.observe(list);
38619
+ return () => observer.disconnect();
38620
+ }, [collapsed, list, chips.length]);
38621
+ const shown = collapsed ? Math.min(visibleCount, chips.length) : chips.length;
38622
+ return {
38623
+ listRef: useCallback((node) => setList(node), []),
38624
+ expanded,
38625
+ toggle: () => {
38626
+ setExpanded((current) => !current);
38627
+ setVisibleCount(chips.length);
38628
+ },
38629
+ visibleChips: chips.slice(0, shown),
38630
+ hiddenCount: chips.length - shown,
38631
+ };
38632
+ }
38633
+
38569
38634
  const getRemoveChipLabel = (label) => `Remove ${label} filter`;
38570
38635
  const FILTERED_BY_LABEL = 'Filtered by';
38571
38636
 
@@ -38573,16 +38638,33 @@ const FILTERED_BY_LABEL = 'Filtered by';
38573
38638
  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';
38574
38639
  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';
38575
38640
  const CLEAR_ALL_CLASS = cn(FILTER_LINK_CLASS_NAME, 'underline');
38641
+ const SHOW_MORE_CLASS = cn(FILTER_LINK_CLASS_NAME, 'whitespace-nowrap px-1 underline underline-offset-2');
38576
38642
  function getChipLabelText(label, variant) {
38577
38643
  return variant === 'inline' ? `${label}:` : label;
38578
38644
  }
38579
38645
 
38646
+ function Chip({ chip, variant, onRemove }) {
38647
+ return (jsxs(Fragment$2, { children: [jsx("span", { className: "shrink-0 font-semibold", children: getChipLabelText(chip.label, variant) }), jsx(TruncatedLabel, { text: chip.value }), onRemove && (jsx("button", { type: "button", "aria-label": getRemoveChipLabel(chip.label), onClick: () => onRemove(chip.id), className: REMOVE_BUTTON_CLASS, children: jsx(X$2, { "aria-hidden": "true", className: "size-3" }) }))] }));
38648
+ }
38649
+ /** One line of chips ending in "+N more"; Clear all and the actions stay at the right end. */
38650
+ function CollapsibleChips({ chips, onRemove, variant = 'inline', clearAll, actions, className, }) {
38651
+ const { listRef, expanded, toggle, visibleChips, hiddenCount } = useChipOverflow(chips.map((chip) => chip.id), true);
38652
+ const visible = new Set(visibleChips);
38653
+ return (
38654
+ // Centred, so the label and Clear all line up with buttons beside the row even when the chips wrap.
38655
+ jsxs("div", { className: cn('flex items-center gap-1.5', className), children: [variant === 'inline' && (jsx("span", { className: "mr-0.5 flex h-7 shrink-0 items-center text-xs text-muted-foreground", children: FILTERED_BY_LABEL })), jsxs("ul", { ref: listRef, className: "flex min-w-0 flex-1 flex-wrap items-center gap-1.5", children: [chips
38656
+ .filter((chip) => visible.has(chip.id))
38657
+ .map((chip) => (jsx("li", { className: CHIP_CLASS, children: jsx(Chip, { chip: chip, variant: variant, onRemove: onRemove }) }, chip.id))), (expanded || hiddenCount > 0) && (jsx("li", { className: "flex h-7 shrink-0 items-center", children: jsx("button", { type: "button", "aria-expanded": expanded, onClick: toggle, className: SHOW_MORE_CLASS, children: expanded ? 'Show less' : getShowMoreLabel(hiddenCount) }) }))] }), (clearAll || actions) && (jsxs("div", { className: "ml-1.5 flex h-7 shrink-0 items-center gap-3", children: [clearAll, actions] }))] }));
38658
+ }
38580
38659
  /** Applied filters as removable chips, with an optional Clear all. */
38581
- function ActiveFilterChips({ chips, onRemove, onClearAll, variant = 'inline', actions, className, }) {
38660
+ function ActiveFilterChips({ chips, onRemove, onClearAll, variant = 'inline', actions, collapsible = false, className, }) {
38582
38661
  if (chips.length === 0)
38583
38662
  return null;
38584
38663
  const clearAll = onClearAll && (jsx("button", { type: "button", onClick: onClearAll, className: CLEAR_ALL_CLASS, children: "Clear all" }));
38585
- return (jsxs("div", { className: cn('flex flex-wrap items-center gap-1.5', className), children: [variant === 'inline' && (jsx("span", { className: "mr-0.5 text-xs text-muted-foreground", children: FILTERED_BY_LABEL })), chips.map((chip) => (jsxs("span", { className: CHIP_CLASS, children: [jsx("span", { className: "shrink-0 font-semibold", children: getChipLabelText(chip.label, variant) }), jsx(TruncatedLabel, { text: chip.value }), onRemove && (jsx("button", { type: "button", "aria-label": getRemoveChipLabel(chip.label), onClick: () => onRemove(chip.id), className: REMOVE_BUTTON_CLASS, children: jsx(X$2, { "aria-hidden": "true", className: "size-3" }) }))] }, chip.id))), actions ? (
38664
+ if (collapsible) {
38665
+ return (jsx(CollapsibleChips, { chips: chips, onRemove: onRemove, variant: variant, clearAll: clearAll, actions: actions, className: className }));
38666
+ }
38667
+ return (jsxs("div", { className: cn('flex flex-wrap items-center gap-1.5', className), children: [variant === 'inline' && (jsx("span", { className: "mr-0.5 text-xs text-muted-foreground", children: FILTERED_BY_LABEL })), chips.map((chip) => (jsx("span", { className: CHIP_CLASS, children: jsx(Chip, { chip: chip, variant: variant, onRemove: onRemove }) }, chip.id))), actions ? (
38586
38668
  // With actions, Clear all joins them at the row's right end.
38587
38669
  jsxs("div", { className: "ml-auto flex items-center gap-3 pl-3", children: [clearAll, actions] })) : (clearAll)] }));
38588
38670
  }
@@ -38784,14 +38866,18 @@ function getNextScaleRange(anchor, option) {
38784
38866
  ? { from: option, to: option }
38785
38867
  : { from: anchor, to: option };
38786
38868
  }
38869
+ const SCALE_POSITION_CLASS_NAMES = {
38870
+ // Unselected options look exactly like unselected pick-list options.
38871
+ outside: getSelectableOptionClassName(false),
38872
+ end: 'border-primary bg-primary font-medium text-primary-foreground',
38873
+ between: 'border-primary/30 bg-primary/10 font-medium text-primary',
38874
+ };
38875
+ /** A pill like the other pick lists' (`Single`, `Parcel`), coloured by where it sits in the range. */
38876
+ 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]);
38787
38877
  function getScaleButtonClassName(position, fill = false, hasCount = false) {
38788
38878
  return cn('flex min-w-0 cursor-pointer items-center gap-2 rounded-md border transition-colors',
38789
38879
  // With a count: name left, badge right (like a list row); without: name centred.
38790
- 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,
38791
- // Unselected options look exactly like unselected pick-list tiles.
38792
- position === 'outside' && getSelectableOptionClassName(false), position === 'end' &&
38793
- 'border-primary bg-primary font-medium text-primary-foreground', position === 'between' &&
38794
- 'border-primary/30 bg-primary/10 font-medium text-primary');
38880
+ 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]);
38795
38881
  }
38796
38882
  /** The count badge follows the tile: plain, tinted inside the range, solid on an end. */
38797
38883
  const getScaleCountTone = (position) => position === 'end' ? 'solid' : position === 'between' ? 'tinted' : 'neutral';
@@ -38811,8 +38897,14 @@ function useScaleRangeFilter({ value, onChange, }) {
38811
38897
  }
38812
38898
 
38813
38899
  /** Ordered-scale filter: click a start option, then an end option. */
38814
- function ScaleRangeFilter({ options, value, onChange, ariaLabel, columns, fill = false, }) {
38900
+ function ScaleRangeFilter({ options, value, onChange, ariaLabel, layout = 'pills', columns, fill = false, }) {
38815
38901
  const { selectOption } = useScaleRangeFilter({ value, onChange });
38902
+ if (layout === 'pills') {
38903
+ return (jsx("div", { role: "group", "aria-label": ariaLabel, className: "flex flex-wrap content-start gap-1.5", children: options.map((option) => {
38904
+ const position = getScalePosition(options, value, option.value);
38905
+ return (jsxs("button", { type: "button", "aria-pressed": position !== 'outside', "aria-label": option.label, onClick: () => selectOption(option.value), className: getScalePillClassName(position), children: [jsx("span", { children: option.label }), option.count !== undefined && (jsx("span", { className: getCountBadgeClassName(getScaleCountTone(position)), children: formatOptionCount(option.count) }))] }, option.value));
38906
+ }) }));
38907
+ }
38816
38908
  return (jsx("div", { role: "group", "aria-label": ariaLabel, className: fill ? 'grid gap-2' : 'grid gap-1.5', style: getGridColumnsStyle(columns), children: options.map((option) => {
38817
38909
  const position = getScalePosition(options, value, option.value);
38818
38910
  return (jsxs("button", { type: "button", "aria-pressed": position !== 'outside', "aria-label": option.label, onClick: () => selectOption(option.value), className: getScaleButtonClassName(position, fill, option.count !== undefined), children: [jsx(TruncatedLabel, { text: option.label }), option.count !== undefined && (jsx("span", { className: getCountBadgeClassName(getScaleCountTone(position)), children: formatOptionCount(option.count) }))] }, option.value));
@@ -38826,7 +38918,7 @@ const TILE_KEYS = ['a', 'b', 'c', 'd'];
38826
38918
  const CARD_CLASS_NAME = 'flex h-full min-w-0 flex-col gap-3 rounded-xl border border-border bg-card p-4';
38827
38919
 
38828
38920
  function Pills({ count }) {
38829
- return (jsx("div", { className: "flex flex-wrap gap-1.5", children: PILL_WIDTHS.slice(0, count).map((width) => (jsx(SquareSkeleton, { height: 32, width: width, rounded: "full" }, width))) }));
38921
+ return (jsx("div", { className: "flex flex-wrap gap-1.5", children: PILL_WIDTHS.slice(0, count).map((width) => (jsx(SquareSkeleton, { height: 32, width: width, rounded: "sm" }, width))) }));
38830
38922
  }
38831
38923
  function Body({ variant, }) {
38832
38924
  switch (variant) {
@@ -39136,70 +39228,6 @@ function TriStateFilter({ value, onChange, ariaLabel, compact = false, }) {
39136
39228
  }) }));
39137
39229
  }
39138
39230
 
39139
- 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';
39140
- /** Children laid out below the first one's line. */
39141
- function countWrapped(items) {
39142
- if (items.length === 0)
39143
- return 0;
39144
- const firstTop = items[0].offsetTop;
39145
- return items.filter((item) => item.offsetTop > firstTop).length;
39146
- }
39147
- const getShowMoreLabel = (hiddenCount) => `+${hiddenCount} more`;
39148
-
39149
- /**
39150
- * Fits chips on one line with the "+N more" link right after the last one. Renders every chip, counts
39151
- * how many wrap, then drops chips until the link itself fits; a resize starts the measurement over.
39152
- */
39153
- function useChipOverflow(chips, enabled) {
39154
- const [list, setList] = useState(null);
39155
- const [expanded, setExpanded] = useState(false);
39156
- const [visibleCount, setVisibleCount] = useState(chips.length);
39157
- const collapsed = enabled && !expanded;
39158
- const [measuredLength, setMeasuredLength] = useState(chips.length);
39159
- if (measuredLength !== chips.length) {
39160
- setMeasuredLength(chips.length);
39161
- setVisibleCount(chips.length);
39162
- }
39163
- useLayoutEffect(() => {
39164
- if (!collapsed || !list)
39165
- return;
39166
- const items = Array.from(list.children);
39167
- if (visibleCount >= chips.length) {
39168
- const wrapped = countWrapped(items);
39169
- if (wrapped > 0)
39170
- setVisibleCount(chips.length - wrapped);
39171
- return;
39172
- }
39173
- // The last child is the "+N more" link; if it wrapped, give it one more chip's room.
39174
- if (countWrapped(items) > 0 && visibleCount > 1)
39175
- setVisibleCount(visibleCount - 1);
39176
- }, [collapsed, list, visibleCount, chips.length]);
39177
- useEffect(() => {
39178
- if (!collapsed || !list)
39179
- return;
39180
- let width = list.clientWidth;
39181
- const observer = new ResizeObserver(() => {
39182
- if (list.clientWidth === width)
39183
- return;
39184
- width = list.clientWidth;
39185
- setVisibleCount(chips.length);
39186
- });
39187
- observer.observe(list);
39188
- return () => observer.disconnect();
39189
- }, [collapsed, list, chips.length]);
39190
- const shown = collapsed ? Math.min(visibleCount, chips.length) : chips.length;
39191
- return {
39192
- listRef: useCallback((node) => setList(node), []),
39193
- expanded,
39194
- toggle: () => {
39195
- setExpanded((current) => !current);
39196
- setVisibleCount(chips.length);
39197
- },
39198
- visibleChips: chips.slice(0, shown),
39199
- hiddenCount: chips.length - shown,
39200
- };
39201
- }
39202
-
39203
39231
  /** Read-only filter summaries of a saved search, as pills; optionally one line ending in "+N more". */
39204
39232
  function SavedSearchChipList({ chips, emptyText = 'No filters', collapsible = false, className, }) {
39205
39233
  const { listRef, expanded, toggle, visibleChips, hiddenCount } = useChipOverflow(chips, collapsible);
@@ -77023,7 +77051,7 @@ const getDropdownPlaceholder = (field) => field.inputPlaceholder ??
77023
77051
  `${field.single ? 'Any' : 'Select'} ${field.label.toLowerCase()}`;
77024
77052
  /** Popover variant: the same control and layout as the card (pills up to 4 options, then the list); tile grids drop to two columns. */
77025
77053
  function toPopoverField(field) {
77026
- if (field.type === 'scale' ||
77054
+ if ((field.type === 'scale' && field.layout === 'tiles') ||
77027
77055
  (field.type === 'list' && resolveListLayout(field) === 'tiles')) {
77028
77056
  return { ...field, columns: POPOVER_TILE_COLUMNS };
77029
77057
  }
@@ -77056,7 +77084,7 @@ function FilterFieldControl({ field, value, onChange, fiscalYearStartMonth, fill
77056
77084
  return (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 }));
77057
77085
  }
77058
77086
  case 'scale':
77059
- return (jsx(ScaleRangeFilter, { ariaLabel: ariaLabel, options: field.options ?? [], value: valueOfType(value, 'scale')?.range, onChange: (range) => onChange(toScaleValue(range)), columns: field.columns, fill: fill }));
77087
+ return (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 }));
77060
77088
  case 'number':
77061
77089
  return (jsx(NumberRangeInputs, { ariaLabel: field.label, value: valueOfType(value, 'number')?.range, onChange: (range) => onChange(toNumberValue(range)), presets: field.presets, presetsLabel: field.presetsLabel }));
77062
77090
  case 'ids':
@@ -77102,9 +77130,8 @@ function FilterField(props) {
77102
77130
  return mode === 'cell' ? (jsx(FilterFieldCell, { ...rest })) : (jsx(FilterFieldCard, { mode: mode, ...rest }));
77103
77131
  }
77104
77132
 
77105
- /** Both leave without applying — the draft is discarded. */
77133
+ /** Leaves without applying; the draft is discarded. */
77106
77134
  const BACK_LABEL = 'Back without applying';
77107
- const CLOSE_LABEL = 'Close without applying';
77108
77135
  const LOADING_LABEL = 'Loading filters';
77109
77136
  const ERROR_TITLE = "Couldn't load filters";
77110
77137
  const DEFAULT_ERROR_MESSAGE = 'Something went wrong while loading the filters. Check your connection and try again.';
@@ -77174,10 +77201,12 @@ function MasonryItem({ children }) {
77174
77201
  return (jsx("div", { style: span > 0 ? { gridRowEnd: `span ${span}` } : undefined, children: jsx("div", { ref: ref, className: "pb-3", children: children }) }));
77175
77202
  }
77176
77203
 
77177
- /** Full-page filter screen (e.g. an advanced search): header, scrolling card grid, sticky apply footer. */
77178
- function FilterSurface({ title, headerActions, footerChips, footerActions, onClose, status = 'ready', errorMessage, onRetry, skeleton, children, }) {
77204
+ /** Full-page filter screen (e.g. an advanced search): one sticky header with every action, then the scrolling card grid. */
77205
+ function FilterSurface({ title, headerActions, primaryActions, chips, onClose, status = 'ready', errorMessage, onRetry, skeleton, children, }) {
77179
77206
  useCloseOnEscape(onClose);
77180
- return (jsxs("div", { role: "region", "aria-label": title, className: "flex h-full min-h-0 flex-col bg-background", children: [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: [jsx("div", { className: "flex flex-col gap-1", children: jsxs("div", { className: "flex items-center gap-2", children: [jsx(IconButton, { appearance: "ghost", size: "sm", "aria-label": BACK_LABEL, title: BACK_LABEL, onClick: onClose, children: jsx(ArrowLeft, {}) }), jsx("h1", { className: "text-xl font-semibold text-foreground", children: title })] }) }), jsxs("div", { className: "flex items-center gap-2", children: [headerActions, jsx(IconButton, { appearance: "outline", size: "md", "aria-label": CLOSE_LABEL, title: CLOSE_LABEL, onClick: onClose, children: jsx(X$2, {}) })] })] }), jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-3 pb-3", children: status === 'loading' ? (jsx(FilterSurfaceSkeleton, { children: skeleton })) : status === 'error' ? (jsx(FilterSurfaceError, { message: errorMessage ?? DEFAULT_ERROR_MESSAGE, onRetry: onRetry })) : (jsx("div", { className: "flex flex-col gap-3", children: children })) }), status === 'ready' && (footerChips || footerActions) && (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: [jsx("div", { className: "min-w-0 flex-1", children: footerChips }), jsx("div", { className: "flex shrink-0 items-center gap-2", children: footerActions })] }))] }));
77207
+ const ready = status === 'ready';
77208
+ const actions = ready ? primaryActions : undefined;
77209
+ return (jsxs("div", { role: "region", "aria-label": title, className: "flex h-full min-h-0 flex-col bg-background", children: [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: [jsx(IconButton, { appearance: "ghost", size: "sm", "aria-label": BACK_LABEL, title: BACK_LABEL, onClick: onClose, children: jsx(ArrowLeft, {}) }), jsx("h1", { className: "sr-only", children: title }), jsx("div", { className: "min-w-0 flex-1", children: ready && chips }), jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [headerActions, actions] })] }), jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-3 pb-3", children: status === 'loading' ? (jsx(FilterSurfaceSkeleton, { children: skeleton })) : status === 'error' ? (jsx(FilterSurfaceError, { message: errorMessage ?? DEFAULT_ERROR_MESSAGE, onRetry: onRetry })) : (jsx("div", { className: "flex flex-col gap-3", children: children })) })] }));
77181
77210
  }
77182
77211
  function FilterSurfaceGroup({ title, masonry = false, className, children, }) {
77183
77212
  return (jsxs("section", { className: "flex flex-col gap-3", children: [title && (jsx("h2", { className: "text-xs font-semibold uppercase tracking-wide text-label", children: title })), jsx("div", { className: cn('grid grid-cols-1 items-start lg:grid-cols-6', masonry ? MASONRY_GRID_CLASS_NAME : 'gap-3', className), children: masonry