@caratlogic/design-system 0.1.34 → 0.1.36
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/components/molecules/Filters/DateRangeFilter/DateRangeFilter.d.ts +2 -2
- package/dist/components/molecules/Filters/DateRangeFilter/DateRangeFilter.d.ts.map +1 -1
- package/dist/components/molecules/Filters/DateRangeFilter/DateRangeFilter.hooks.d.ts +0 -1
- package/dist/components/molecules/Filters/DateRangeFilter/DateRangeFilter.hooks.d.ts.map +1 -1
- package/dist/components/molecules/Filters/DateRangeFilter/DateRangeFilter.types.d.ts +2 -0
- package/dist/components/molecules/Filters/DateRangeFilter/DateRangeFilter.types.d.ts.map +1 -1
- package/dist/components/molecules/Filters/Filters.utils.d.ts.map +1 -1
- package/dist/components/molecules/Filters/NumberRangeInputs/NumberRangeInputs.d.ts.map +1 -1
- package/dist/components/molecules/Filters/NumberRangeInputs/NumberRangeInputs.utils.d.ts +2 -3
- package/dist/components/molecules/Filters/NumberRangeInputs/NumberRangeInputs.utils.d.ts.map +1 -1
- package/dist/components/molecules/Filters/PickListDropdown/PickListDropdown.accessibility.d.ts +2 -0
- package/dist/components/molecules/Filters/PickListDropdown/PickListDropdown.accessibility.d.ts.map +1 -0
- package/dist/components/molecules/Filters/PickListDropdown/PickListDropdown.d.ts +6 -0
- package/dist/components/molecules/Filters/PickListDropdown/PickListDropdown.d.ts.map +1 -0
- package/dist/components/molecules/Filters/PickListDropdown/PickListDropdown.hooks.d.ts +4 -0
- package/dist/components/molecules/Filters/PickListDropdown/PickListDropdown.hooks.d.ts.map +1 -0
- package/dist/components/molecules/Filters/PickListDropdown/PickListDropdown.types.d.ts +22 -0
- package/dist/components/molecules/Filters/PickListDropdown/PickListDropdown.types.d.ts.map +1 -0
- package/dist/components/molecules/Filters/PickListDropdown/PickListDropdown.utils.d.ts +6 -0
- package/dist/components/molecules/Filters/PickListDropdown/PickListDropdown.utils.d.ts.map +1 -0
- package/dist/components/molecules/Filters/PickListDropdown/index.d.ts +3 -0
- package/dist/components/molecules/Filters/PickListDropdown/index.d.ts.map +1 -0
- package/dist/components/molecules/Filters/PickListFilter/PickListFilter.d.ts.map +1 -1
- package/dist/components/molecules/Filters/PickListFilter/PickListFilter.utils.d.ts +8 -1
- package/dist/components/molecules/Filters/PickListFilter/PickListFilter.utils.d.ts.map +1 -1
- package/dist/components/molecules/Filters/ScaleRangeFilter/ScaleRangeFilter.d.ts.map +1 -1
- package/dist/components/molecules/Filters/ScaleRangeFilter/ScaleRangeFilter.utils.d.ts +4 -1
- package/dist/components/molecules/Filters/ScaleRangeFilter/ScaleRangeFilter.utils.d.ts.map +1 -1
- package/dist/components/molecules/Filters/TriStateFilter/TriStateFilter.d.ts +1 -1
- package/dist/components/molecules/Filters/TriStateFilter/TriStateFilter.d.ts.map +1 -1
- package/dist/components/molecules/Filters/TriStateFilter/TriStateFilter.types.d.ts +2 -0
- package/dist/components/molecules/Filters/TriStateFilter/TriStateFilter.types.d.ts.map +1 -1
- package/dist/components/molecules/Filters/index.d.ts +1 -0
- package/dist/components/molecules/Filters/index.d.ts.map +1 -1
- package/dist/components/molecules/SavedSearches/SaveSearchDialog/SaveSearchDialog.parts.d.ts +2 -2
- package/dist/components/molecules/SavedSearches/SaveSearchDialog/SaveSearchDialog.parts.d.ts.map +1 -1
- package/dist/components/molecules/SavedSearches/SaveSearchDialog/SaveSearchDialog.types.d.ts +2 -0
- package/dist/components/molecules/SavedSearches/SaveSearchDialog/SaveSearchDialog.types.d.ts.map +1 -1
- package/dist/components/molecules/SavedSearches/SaveSearchDialog/index.d.ts +1 -0
- package/dist/components/molecules/SavedSearches/SaveSearchDialog/index.d.ts.map +1 -1
- package/dist/components/organisms/Filters/FilterField/FilterField.d.ts.map +1 -1
- package/dist/components/organisms/Filters/FilterField/FilterField.types.d.ts +4 -4
- package/dist/components/organisms/Filters/FilterField/FilterField.types.d.ts.map +1 -1
- package/dist/components/organisms/Filters/FilterField/FilterField.utils.d.ts +4 -4
- package/dist/components/organisms/Filters/FilterField/FilterField.utils.d.ts.map +1 -1
- package/dist/components/organisms/Filters/FilterSurface/FilterSurface.d.ts +2 -2
- package/dist/components/organisms/Filters/FilterSurface/FilterSurface.d.ts.map +1 -1
- package/dist/components/organisms/Filters/FilterSurface/FilterSurface.hooks.d.ts +5 -0
- package/dist/components/organisms/Filters/FilterSurface/FilterSurface.hooks.d.ts.map +1 -1
- package/dist/components/organisms/Filters/FilterSurface/FilterSurface.parts.d.ts +3 -1
- package/dist/components/organisms/Filters/FilterSurface/FilterSurface.parts.d.ts.map +1 -1
- package/dist/components/organisms/Filters/FilterSurface/FilterSurface.types.d.ts +8 -0
- package/dist/components/organisms/Filters/FilterSurface/FilterSurface.types.d.ts.map +1 -1
- package/dist/components/organisms/Filters/FilterSurface/FilterSurface.utils.d.ts +6 -0
- package/dist/components/organisms/Filters/FilterSurface/FilterSurface.utils.d.ts.map +1 -1
- package/dist/index.cjs.js +138 -51
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +137 -52
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs.js
CHANGED
|
@@ -15476,7 +15476,8 @@ function ApiComboBox({ contactType, selectedItem, onSelect, fetchBusinessContact
|
|
|
15476
15476
|
}
|
|
15477
15477
|
|
|
15478
15478
|
// A number with an optional `k` (thousand), `M` (million) or `B` (billion), any case, e.g. `1.5`, `2k`, `1.2M`.
|
|
15479
|
-
|
|
15479
|
+
// An optional minus sign allows negative values (e.g. a Rapnet discount of -35); `-30--10` still reads as a range.
|
|
15480
|
+
const NUMBER = String.raw `(-?(?:\d+(?:\.\d+)?|\.\d+)[kKmMbB]?)`;
|
|
15480
15481
|
const SUFFIX_MULTIPLIER = {
|
|
15481
15482
|
k: 1000,
|
|
15482
15483
|
m: 1000000,
|
|
@@ -15901,8 +15902,9 @@ function filterOptionsByQuery(options, query) {
|
|
|
15901
15902
|
return options.filter((option) => option.label.toLowerCase().includes(needle));
|
|
15902
15903
|
}
|
|
15903
15904
|
const getChipClassName = getPillClassName;
|
|
15904
|
-
|
|
15905
|
-
|
|
15905
|
+
/** A tile with a count reads like a list row: name on the left, count badge on the right. */
|
|
15906
|
+
function getTileClassName(selected, hasCount = false) {
|
|
15907
|
+
return getSelectableOptionClassName(selected, cn('flex min-h-12 min-w-0 items-center gap-2 rounded-md px-3 py-1.5', hasCount ? 'justify-between text-left' : 'justify-center text-center'));
|
|
15906
15908
|
}
|
|
15907
15909
|
/** The list's search box only appears once there are enough options to need it. */
|
|
15908
15910
|
const SEARCH_MIN_OPTIONS = 9;
|
|
@@ -15915,6 +15917,15 @@ const FILL_WRAPPER_CLASS_NAME = 'flex min-h-0 flex-1 flex-col';
|
|
|
15915
15917
|
const LIST_COLUMN_MIN_WIDTH = '10rem';
|
|
15916
15918
|
const getOptionsScrollClassName = (fill) => fill ? OPTIONS_FILL_CLASS_NAME : OPTIONS_SCROLL_CLASS_NAME;
|
|
15917
15919
|
const LIST_ROW_CLASS_NAME = getOptionRowClassName();
|
|
15920
|
+
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',
|
|
15924
|
+
};
|
|
15925
|
+
/** 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]);
|
|
15927
|
+
const COUNT_BADGE_CLASS_NAME = getCountBadgeClassName('neutral');
|
|
15928
|
+
const formatOptionCount = (count) => count.toLocaleString('en-US');
|
|
15918
15929
|
/** Picking a value: a single-choice list replaces (or clears) it, a multi-choice list toggles it. */
|
|
15919
15930
|
const pickValue = (values, value, single) => {
|
|
15920
15931
|
if (!single)
|
|
@@ -15963,12 +15974,12 @@ function PickListFilter({ options, value, onChange, ariaLabel, layout = 'list',
|
|
|
15963
15974
|
? getGridColumnsStyle(undefined, LIST_COLUMN_MIN_WIDTH)
|
|
15964
15975
|
: undefined, wrapperClassName: wrapperClassName, children: visibleOptions.map((option) => (jsxRuntime.jsxs("label", { className: LIST_ROW_CLASS_NAME, children: [single ? (jsxRuntime.jsx("input", { type: "radio", name: radioName, checked: value.includes(option.value),
|
|
15965
15976
|
// A click on the picked row clears it (radios can't be unticked natively).
|
|
15966
|
-
onClick: () => toggle(option), onChange: () => undefined, className: LIST_RADIO_CLASS_NAME })) : (jsxRuntime.jsx(Checkbox, { className: LIST_CHECKBOX_CLASS_NAME, checked: value.includes(option.value), onCheckedChange: () => toggle(option) })), option.content ? (jsxRuntime.jsx("span", { className: "min-w-0 flex-1", children: option.content })) : (jsxRuntime.jsx(TruncatedLabel, { text: option.label, className: "flex-1" })),
|
|
15977
|
+
onClick: () => toggle(option), onChange: () => undefined, className: LIST_RADIO_CLASS_NAME })) : (jsxRuntime.jsx(Checkbox, { className: LIST_CHECKBOX_CLASS_NAME, checked: value.includes(option.value), onCheckedChange: () => toggle(option) })), option.content ? (jsxRuntime.jsx("span", { className: "min-w-0 flex-1", children: option.content })) : (jsxRuntime.jsx(TruncatedLabel, { text: option.label, className: "flex-1" })), option.hint && (jsxRuntime.jsx("span", { className: "shrink-0 text-muted-foreground", children: option.hint })), jsxRuntime.jsx(OptionSwatch, { color: option.swatch }), option.count !== undefined && (jsxRuntime.jsx("span", { className: COUNT_BADGE_CLASS_NAME, children: formatOptionCount(option.count) }))] }, option.value))) })] }));
|
|
15967
15978
|
}
|
|
15968
15979
|
if (layout === 'tiles') {
|
|
15969
|
-
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)), children: [jsxRuntime.jsx(TruncatedLabel, { text: option.label }), option.count !== undefined && (jsxRuntime.jsx("span", { className:
|
|
15980
|
+
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))) }));
|
|
15970
15981
|
}
|
|
15971
|
-
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: "font-normal text-muted-foreground", children: option.count }))] }, option.value))) }));
|
|
15982
|
+
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))) }));
|
|
15972
15983
|
}
|
|
15973
15984
|
|
|
15974
15985
|
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';
|
|
@@ -38728,18 +38739,20 @@ function useDateRangeFilter(value, onChange, today, fiscalYearStartMonth) {
|
|
|
38728
38739
|
selectPreset: (preset) => onChange(togglePreset(value, preset)),
|
|
38729
38740
|
handleFromChange: (date) => onChange(buildCustomRange(toIsoOrEmpty(date), to)),
|
|
38730
38741
|
handleToChange: (date) => onChange(buildCustomRange(from, toIsoOrEmpty(date))),
|
|
38731
|
-
clear: () => onChange(undefined),
|
|
38732
38742
|
};
|
|
38733
38743
|
}
|
|
38734
38744
|
|
|
38735
|
-
/** Date filter body: presets on the left, a From / To range on the right. */
|
|
38736
|
-
function DateRangeFilter({ value, onChange, ariaLabel, fiscalYearStartMonth = 1, today = new Date(), }) {
|
|
38737
|
-
const { from, to, selectPreset, handleFromChange, handleToChange
|
|
38745
|
+
/** Date filter body: presets on the left, a From / To range on the right. Clearing is the surrounding panel's job. */
|
|
38746
|
+
function DateRangeFilter({ value, onChange, ariaLabel, fiscalYearStartMonth = 1, today = new Date(), compact = false, }) {
|
|
38747
|
+
const { from, to, selectPreset, handleFromChange, handleToChange } = useDateRangeFilter(value, onChange, today, fiscalYearStartMonth);
|
|
38748
|
+
if (compact) {
|
|
38749
|
+
return (jsxRuntime.jsxs("div", { className: "flex flex-col gap-2.5 text-xs", children: [jsxRuntime.jsx("div", { role: "group", "aria-label": getPresetGroupLabel(ariaLabel), className: "flex flex-wrap gap-1.5", children: DATE_PRESET_OPTIONS.map((preset) => (jsxRuntime.jsx("button", { type: "button", "aria-pressed": value?.preset === preset.value, onClick: () => selectPreset(preset.value), className: getPillClassName(value?.preset === preset.value), children: preset.label }, preset.value))) }), jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-2", children: [jsxRuntime.jsx(DateInput, { value: fromIsoDate(from), max: to || undefined, placeholder: "From", ariaLabel: getDateInputLabel(ariaLabel, 'from'), onChange: handleFromChange }), jsxRuntime.jsx(DateInput, { value: fromIsoDate(to), min: from || undefined, placeholder: "To", ariaLabel: getDateInputLabel(ariaLabel, 'to'), onChange: handleToChange, align: "end" })] })] }));
|
|
38750
|
+
}
|
|
38738
38751
|
return (jsxRuntime.jsxs("div", { className: "grid grid-cols-[auto_minmax(0,1fr)] divide-x divide-border text-xs", children: [jsxRuntime.jsx("div", { role: "group", "aria-label": getPresetGroupLabel(ariaLabel), className: "flex flex-col gap-1 pr-3", children: DATE_PRESET_OPTIONS.map((preset) => (jsxRuntime.jsx("button", { type: "button", "aria-pressed": value?.preset === preset.value, onClick: () => selectPreset(preset.value), className: getPresetClassName$1(value?.preset === preset.value), children: preset.label }, preset.value))) }), jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-col gap-3 pl-3", children: [jsxRuntime.jsxs("label", { className: "flex min-w-0 flex-col gap-1 text-muted-foreground", children: ["From", jsxRuntime.jsx(DateInput
|
|
38739
38752
|
// The inputs sit on the card's right; an end-aligned calendar stays under them.
|
|
38740
38753
|
, {
|
|
38741
38754
|
// The inputs sit on the card's right; an end-aligned calendar stays under them.
|
|
38742
|
-
align: "end", value: fromIsoDate(from), max: to || undefined, ariaLabel: getDateInputLabel(ariaLabel, 'from'), onChange: handleFromChange })] }), jsxRuntime.jsxs("label", { className: "flex min-w-0 flex-col gap-1 text-muted-foreground", children: ["To", jsxRuntime.jsx(DateInput, { value: fromIsoDate(to), min: from || undefined, ariaLabel: getDateInputLabel(ariaLabel, 'to'), onChange: handleToChange, align: "end" })] })
|
|
38755
|
+
align: "end", value: fromIsoDate(from), max: to || undefined, ariaLabel: getDateInputLabel(ariaLabel, 'from'), onChange: handleFromChange })] }), jsxRuntime.jsxs("label", { className: "flex min-w-0 flex-col gap-1 text-muted-foreground", children: ["To", jsxRuntime.jsx(DateInput, { value: fromIsoDate(to), min: from || undefined, ariaLabel: getDateInputLabel(ariaLabel, 'to'), onChange: handleToChange, align: "end" })] })] })] }));
|
|
38743
38756
|
}
|
|
38744
38757
|
|
|
38745
38758
|
const getClearButtonLabel = (ariaLabel) => `Clear ${ariaLabel}`;
|
|
@@ -38799,13 +38812,17 @@ function getNextScaleRange(anchor, option) {
|
|
|
38799
38812
|
? { from: option, to: option }
|
|
38800
38813
|
: { from: anchor, to: option };
|
|
38801
38814
|
}
|
|
38802
|
-
function getScaleButtonClassName(position, fill = false) {
|
|
38803
|
-
return cn('flex min-w-0 cursor-pointer
|
|
38815
|
+
function getScaleButtonClassName(position, fill = false, hasCount = false) {
|
|
38816
|
+
return cn('flex min-w-0 cursor-pointer items-center gap-2 rounded-md border transition-colors',
|
|
38817
|
+
// With a count: name left, badge right (like a list row); without: name centred.
|
|
38818
|
+
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,
|
|
38804
38819
|
// Unselected options look exactly like unselected pick-list tiles.
|
|
38805
38820
|
position === 'outside' && getSelectableOptionClassName(false), position === 'end' &&
|
|
38806
38821
|
'border-primary bg-primary font-medium text-primary-foreground', position === 'between' &&
|
|
38807
38822
|
'border-primary/30 bg-primary/10 font-medium text-primary');
|
|
38808
38823
|
}
|
|
38824
|
+
/** The count badge follows the tile: plain, tinted inside the range, solid on an end. */
|
|
38825
|
+
const getScaleCountTone = (position) => position === 'end' ? 'solid' : position === 'between' ? 'tinted' : 'neutral';
|
|
38809
38826
|
|
|
38810
38827
|
/** First click picks the start option, the next click the end option; a click after that starts over. */
|
|
38811
38828
|
function useScaleRangeFilter({ value, onChange, }) {
|
|
@@ -38826,7 +38843,7 @@ function ScaleRangeFilter({ options, value, onChange, ariaLabel, columns, fill =
|
|
|
38826
38843
|
const { selectOption } = useScaleRangeFilter({ value, onChange });
|
|
38827
38844
|
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) => {
|
|
38828
38845
|
const position = getScalePosition(options, value, option.value);
|
|
38829
|
-
return (jsxRuntime.
|
|
38846
|
+
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));
|
|
38830
38847
|
}) }));
|
|
38831
38848
|
}
|
|
38832
38849
|
|
|
@@ -38860,6 +38877,43 @@ function FilterCardSkeleton({ variant = 'list', className, }) {
|
|
|
38860
38877
|
return (jsxRuntime.jsxs("div", { "aria-hidden": "true", className: cn(CARD_CLASS_NAME, className), children: [jsxRuntime.jsx(SquareSkeleton, { height: 16, width: 96, rounded: "sm" }), jsxRuntime.jsx(Body, { variant: variant })] }));
|
|
38861
38878
|
}
|
|
38862
38879
|
|
|
38880
|
+
const getRemoveLabel = (label) => `Remove ${label}`;
|
|
38881
|
+
|
|
38882
|
+
function usePickListDropdown(options, value, onChange, single) {
|
|
38883
|
+
const [open, setOpen] = React.useState(false);
|
|
38884
|
+
const selected = React.useMemo(() => options.filter((option) => value.includes(option.value)), [options, value]);
|
|
38885
|
+
return {
|
|
38886
|
+
open,
|
|
38887
|
+
setOpen,
|
|
38888
|
+
selected,
|
|
38889
|
+
handleChange: (values) => {
|
|
38890
|
+
onChange(values);
|
|
38891
|
+
if (single)
|
|
38892
|
+
setOpen(false);
|
|
38893
|
+
},
|
|
38894
|
+
remove: (removed) => onChange(value.filter((current) => current !== removed)),
|
|
38895
|
+
};
|
|
38896
|
+
}
|
|
38897
|
+
|
|
38898
|
+
const TRIGGER_CLASS_NAME = cn('flex h-9 w-full min-w-0 cursor-pointer items-center justify-between gap-2 rounded-md border border-input bg-card px-3 text-left text-sm transition-colors hover:bg-muted/40', FILTER_FOCUS_RING);
|
|
38899
|
+
const CHIP_CLASS_NAME = 'inline-flex h-7 max-w-full items-center gap-1 rounded-full border border-primary/30 bg-primary/5 pr-1 pl-2.5 text-xs text-foreground';
|
|
38900
|
+
const CHIP_REMOVE_CLASS_NAME = cn('inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-full text-muted-foreground hover:bg-primary/10 hover:text-foreground', FILTER_FOCUS_RING);
|
|
38901
|
+
/** `Ruby` for one, `3 selected` for several; the pills below name them. */
|
|
38902
|
+
function getTriggerText(selectedLabels, placeholder) {
|
|
38903
|
+
if (selectedLabels.length === 0)
|
|
38904
|
+
return placeholder;
|
|
38905
|
+
if (selectedLabels.length === 1)
|
|
38906
|
+
return selectedLabels[0];
|
|
38907
|
+
return `${selectedLabels.length} selected`;
|
|
38908
|
+
}
|
|
38909
|
+
|
|
38910
|
+
/** A long pick list as a compact select: the searchable list opens on demand, picks show as removable pills. */
|
|
38911
|
+
function PickListDropdown({ options, value, onChange, ariaLabel, placeholder, searchPlaceholder, single = false, }) {
|
|
38912
|
+
const { open, setOpen, selected, handleChange, remove } = usePickListDropdown(options, value, onChange, single);
|
|
38913
|
+
const labels = selected.map((option) => option.label);
|
|
38914
|
+
return (jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [jsxRuntime.jsxs(Popover, { open: open, onOpenChange: setOpen, children: [jsxRuntime.jsx(PopoverTrigger, { asChild: true, children: jsxRuntime.jsxs("button", { type: "button", "aria-label": ariaLabel, className: TRIGGER_CLASS_NAME, children: [jsxRuntime.jsx("span", { className: cn('min-w-0 flex-1', labels.length === 0 && 'text-placeholder'), children: jsxRuntime.jsx(TruncatedLabel, { text: getTriggerText(labels, placeholder) }) }), jsxRuntime.jsx(ChevronDown, { "aria-hidden": "true", className: "size-4 shrink-0 text-muted-foreground" })] }) }), jsxRuntime.jsx(PopoverContent, { align: "start", sideOffset: 4, className: "w-[var(--radix-popover-trigger-width)] min-w-64 p-3", children: jsxRuntime.jsx(PickListFilter, { ariaLabel: ariaLabel, options: options, value: value, onChange: handleChange, layout: "list", searchPlaceholder: searchPlaceholder ?? 'Search', alwaysSearchable: true, single: single }) })] }), !single && selected.length > 0 && (jsxRuntime.jsx("ul", { className: "flex flex-wrap gap-1.5", children: selected.map((option) => (jsxRuntime.jsxs("li", { className: CHIP_CLASS_NAME, children: [jsxRuntime.jsx(TruncatedLabel, { text: option.label }), jsxRuntime.jsx("button", { type: "button", "aria-label": getRemoveLabel(option.label), onClick: () => remove(option.value), className: CHIP_REMOVE_CLASS_NAME, children: jsxRuntime.jsx(X$2, { "aria-hidden": "true", className: "size-3" }) })] }, option.value))) }))] }));
|
|
38915
|
+
}
|
|
38916
|
+
|
|
38863
38917
|
const getClearIdsLabel = (ariaLabel) => `Clear ${ariaLabel}`;
|
|
38864
38918
|
|
|
38865
38919
|
const DEFAULT_ID_HELPER_TEXT = 'Paste from Excel or an email. Commas, spaces and new lines all split IDs.';
|
|
@@ -38998,13 +39052,13 @@ function NumberExpressionFilter({ value, onChange, ariaLabel, placeholder, note,
|
|
|
38998
39052
|
const REVERSED_RANGE_MESSAGE = 'From must not be more than To.';
|
|
38999
39053
|
const getBoundLabel = (ariaLabel, bound) => `${ariaLabel} ${bound}`;
|
|
39000
39054
|
|
|
39001
|
-
const DECIMAL_DRAFT_RE =
|
|
39002
|
-
/** Allows partial decimals (`1.`) while typing; rejects anything else. */
|
|
39055
|
+
const DECIMAL_DRAFT_RE = /^-?\d*\.?\d*$/;
|
|
39056
|
+
/** Allows partial decimals (`1.`, `-`, `-0.`) while typing; rejects anything else. */
|
|
39003
39057
|
function isDecimalDraft(text) {
|
|
39004
39058
|
return DECIMAL_DRAFT_RE.test(text);
|
|
39005
39059
|
}
|
|
39006
39060
|
function toBound(text) {
|
|
39007
|
-
if (!text || text === '.')
|
|
39061
|
+
if (!text || text === '.' || text === '-' || text === '-.')
|
|
39008
39062
|
return undefined;
|
|
39009
39063
|
return Number(text);
|
|
39010
39064
|
}
|
|
@@ -39033,7 +39087,7 @@ function isSameRange(a, b) {
|
|
|
39033
39087
|
function getBoundInputClassName(filled) {
|
|
39034
39088
|
return cn('h-9 min-w-0 flex-1 text-sm', filled && 'border-primary bg-primary/5');
|
|
39035
39089
|
}
|
|
39036
|
-
const getPresetClassName = getPillClassName;
|
|
39090
|
+
const getPresetClassName = (selected) => cn(getPillClassName(selected), 'shrink-0 whitespace-nowrap');
|
|
39037
39091
|
|
|
39038
39092
|
function useNumberRangeInputs(value, onChange) {
|
|
39039
39093
|
const minText = toBoundText(value?.min);
|
|
@@ -39067,7 +39121,7 @@ function useNumberRangeInputs(value, onChange) {
|
|
|
39067
39121
|
/** From – To number inputs for full-page filter cards, with optional quick-pick presets. */
|
|
39068
39122
|
function NumberRangeInputs({ value, onChange, ariaLabel, fromPlaceholder = 'From', toPlaceholder = 'To', presets, presetsLabel, }) {
|
|
39069
39123
|
const { fromDraft, toDraft, reversed, handleFromChange, handleToChange, isPresetSelected, togglePreset, } = useNumberRangeInputs(value, onChange);
|
|
39070
|
-
return (jsxRuntime.jsxs("div", { className: "flex flex-col gap-3", children: [jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [jsxRuntime.jsx(Input, { inputMode: "decimal", value: fromDraft, placeholder: fromPlaceholder, "aria-label": getBoundLabel(ariaLabel, 'from'), "aria-invalid": reversed || undefined, onChange: handleFromChange, className: getBoundInputClassName(fromDraft !== '') }), jsxRuntime.jsx("span", { "aria-hidden": "true", className: "text-muted-foreground", children: "\u2013" }), jsxRuntime.jsx(Input, { inputMode: "decimal", value: toDraft, placeholder: toPlaceholder, "aria-label": getBoundLabel(ariaLabel, 'to'), "aria-invalid": reversed || undefined, onChange: handleToChange, className: getBoundInputClassName(toDraft !== '') })] }), reversed && (jsxRuntime.jsx("p", { role: "alert", className: "text-xs text-destructive", children: REVERSED_RANGE_MESSAGE })), presets && presets.length > 0 && (jsxRuntime.jsxs("div", { className: "flex flex-col gap-2", children: [presetsLabel && (jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: presetsLabel })), jsxRuntime.jsx("div", { className: "flex
|
|
39124
|
+
return (jsxRuntime.jsxs("div", { className: "flex flex-col gap-3", children: [jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [jsxRuntime.jsx(Input, { inputMode: "decimal", value: fromDraft, placeholder: fromPlaceholder, "aria-label": getBoundLabel(ariaLabel, 'from'), "aria-invalid": reversed || undefined, onChange: handleFromChange, className: getBoundInputClassName(fromDraft !== '') }), jsxRuntime.jsx("span", { "aria-hidden": "true", className: "text-muted-foreground", children: "\u2013" }), jsxRuntime.jsx(Input, { inputMode: "decimal", value: toDraft, placeholder: toPlaceholder, "aria-label": getBoundLabel(ariaLabel, 'to'), "aria-invalid": reversed || undefined, onChange: handleToChange, className: getBoundInputClassName(toDraft !== '') })] }), reversed && (jsxRuntime.jsx("p", { role: "alert", className: "text-xs text-destructive", children: REVERSED_RANGE_MESSAGE })), presets && presets.length > 0 && (jsxRuntime.jsxs("div", { className: "flex flex-col gap-2", children: [presetsLabel && (jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: presetsLabel })), jsxRuntime.jsx("div", { className: "flex gap-1.5 overflow-x-auto pb-1 [scrollbar-width:thin]", children: presets.map((preset) => (jsxRuntime.jsx("button", { type: "button", "aria-pressed": isPresetSelected(preset), onClick: () => togglePreset(preset), className: getPresetClassName(isPresetSelected(preset)), children: preset.label }, preset.label))) })] }))] }));
|
|
39071
39125
|
}
|
|
39072
39126
|
|
|
39073
39127
|
const NEXT_KEYS = new Set(['ArrowDown', 'ArrowRight']);
|
|
@@ -39100,7 +39154,10 @@ function getRadioCircleClassName(checked) {
|
|
|
39100
39154
|
}
|
|
39101
39155
|
|
|
39102
39156
|
/** Any / Yes / No radio group for boolean columns (e.g. Active). */
|
|
39103
|
-
function TriStateFilter({ value, onChange, ariaLabel, }) {
|
|
39157
|
+
function TriStateFilter({ value, onChange, ariaLabel, compact = false, }) {
|
|
39158
|
+
if (compact) {
|
|
39159
|
+
return (jsxRuntime.jsx("div", { role: "radiogroup", "aria-label": ariaLabel, onKeyDown: createRadioGroupKeyDownHandler(value, onChange), className: "flex flex-wrap gap-1.5", children: TRI_STATE_OPTIONS.map((option) => (jsxRuntime.jsx("button", { type: "button", role: "radio", "aria-checked": option.value === value, tabIndex: option.value === value ? 0 : -1, "data-value": option.value, onClick: () => onChange(option.value), className: getPillClassName(option.value === value), children: option.label }, option.value))) }));
|
|
39160
|
+
}
|
|
39104
39161
|
return (jsxRuntime.jsx("div", { role: "radiogroup", "aria-label": ariaLabel, onKeyDown: createRadioGroupKeyDownHandler(value, onChange), className: "flex flex-col gap-0.5", children: TRI_STATE_OPTIONS.map((option) => {
|
|
39105
39162
|
const checked = option.value === value;
|
|
39106
39163
|
return (jsxRuntime.jsxs("button", { type: "button", role: "radio", "aria-checked": checked, tabIndex: checked ? 0 : -1, "data-value": option.value, onClick: () => onChange(option.value), className: getRadioRowClassName(checked), children: [jsxRuntime.jsx("span", { "aria-hidden": "true", className: getRadioCircleClassName(checked), children: checked && jsxRuntime.jsx("span", { className: "size-2 rounded-full bg-primary" }) }), option.label] }, option.value));
|
|
@@ -39370,9 +39427,9 @@ function useSaveSearchDialog({ open, defaultName, onSave, saving, existingNames,
|
|
|
39370
39427
|
};
|
|
39371
39428
|
}
|
|
39372
39429
|
|
|
39373
|
-
/** The search a
|
|
39374
|
-
function
|
|
39375
|
-
return (jsxRuntime.jsxs("div", { className: WARNING_DETAIL_CLASS_NAME, children: [jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: label }), jsxRuntime.jsx(TruncatedLabel, { text: value, className: "text-sm font-semibold text-foreground" })] }));
|
|
39430
|
+
/** The search a dialog is about, in its body (long names stay on one line, full text on hover). */
|
|
39431
|
+
function SavedSearchDetail({ label, value, chips, }) {
|
|
39432
|
+
return (jsxRuntime.jsxs("div", { className: WARNING_DETAIL_CLASS_NAME, children: [jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: label }), jsxRuntime.jsx(TruncatedLabel, { text: value, className: "text-sm font-semibold text-foreground" }), chips && chips.length > 0 && (jsxRuntime.jsx(SavedSearchChipList, { chips: chips, collapsible: true, className: "mt-1.5" }))] }));
|
|
39376
39433
|
}
|
|
39377
39434
|
|
|
39378
39435
|
/** Names the current filters and saves them; also used to rename a saved search. */
|
|
@@ -39398,7 +39455,7 @@ function SaveSearchDialog({ open, onClose, onSave, defaultName = '', chips, titl
|
|
|
39398
39455
|
open: open && step === 'form', onClose: onClose, title: title, icon: icon, footer: jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Button, { appearance: "outline", color: "secondary", size: "md", onClick: onClose, children: "Cancel" }), jsxRuntime.jsx(Button, { appearance: "solid", color: "primary", size: "md", type: "submit", form: formId, loading: saving, children: submitLabel })] }), children: jsxRuntime.jsxs("form", { id: formId, noValidate: true, className: "flex flex-col gap-4 px-6 pb-5", onSubmit: (event) => {
|
|
39399
39456
|
event.preventDefault();
|
|
39400
39457
|
submit();
|
|
39401
|
-
}, children: [jsxRuntime.jsx(Field, { label: NAME_LABEL, required: true, error: error, input: jsxRuntime.jsx(Input, { id: inputId, "aria-label": NAME_LABEL, "aria-invalid": Boolean(error) || undefined, value: name, maxLength: SAVED_SEARCH_NAME_MAX_LENGTH, onChange: (event) => setName(event.target.value), autoFocus: true }) }), chips && (jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [jsxRuntime.jsx("span", { className: "text-[13px] text-label", children: FILTERS_LABEL }), jsxRuntime.jsx(SavedSearchChipList, { chips: chips, className: FILTERS_PANEL_CLASS_NAME })] })), note && (jsxRuntime.jsxs("p", { className: "flex items-start gap-1.5 text-xs text-muted-foreground", children: [jsxRuntime.jsx(Info, { "aria-hidden": "true", className: "mt-px size-3.5 shrink-0" }), jsxRuntime.jsx("span", { children: note })] }))] }) }), jsxRuntime.jsx(ConfirmDialog, { open: open && step !== 'form', onOpenChange: (next) => !next && !saving && cancelWarning(), icon: TriangleAlert, title: warning?.title ?? '', description: warning?.description, cancelLabel: getWarningCancelLabel(step), confirmLabel: warning?.confirmLabel ?? SAVE_ANYWAY_LABEL, confirmVariant: "default", disabled: saving, onConfirm: confirmWarning, children: warning?.detail && jsxRuntime.jsx(
|
|
39458
|
+
}, children: [jsxRuntime.jsx(Field, { label: NAME_LABEL, required: true, error: error, input: jsxRuntime.jsx(Input, { id: inputId, "aria-label": NAME_LABEL, "aria-invalid": Boolean(error) || undefined, value: name, maxLength: SAVED_SEARCH_NAME_MAX_LENGTH, onChange: (event) => setName(event.target.value), autoFocus: true }) }), chips && (jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [jsxRuntime.jsx("span", { className: "text-[13px] text-label", children: FILTERS_LABEL }), jsxRuntime.jsx(SavedSearchChipList, { chips: chips, className: FILTERS_PANEL_CLASS_NAME })] })), note && (jsxRuntime.jsxs("p", { className: "flex items-start gap-1.5 text-xs text-muted-foreground", children: [jsxRuntime.jsx(Info, { "aria-hidden": "true", className: "mt-px size-3.5 shrink-0" }), jsxRuntime.jsx("span", { children: note })] }))] }) }), jsxRuntime.jsx(ConfirmDialog, { open: open && step !== 'form', onOpenChange: (next) => !next && !saving && cancelWarning(), icon: TriangleAlert, title: warning?.title ?? '', description: warning?.description, cancelLabel: getWarningCancelLabel(step), confirmLabel: warning?.confirmLabel ?? SAVE_ANYWAY_LABEL, confirmVariant: "default", disabled: saving, onConfirm: confirmWarning, children: warning?.detail && jsxRuntime.jsx(SavedSearchDetail, { ...warning.detail }) })] }));
|
|
39402
39459
|
}
|
|
39403
39460
|
|
|
39404
39461
|
/**
|
|
@@ -76982,16 +77039,17 @@ function getPopoverWidthClassName(field) {
|
|
|
76982
77039
|
}
|
|
76983
77040
|
const POPOVER_TILE_COLUMNS = 2;
|
|
76984
77041
|
/** Above this many options, chips / tiles give way to the searchable checkbox list. */
|
|
76985
|
-
const MAX_PILL_OPTIONS =
|
|
77042
|
+
const MAX_PILL_OPTIONS = 4;
|
|
76986
77043
|
/** The list layout actually used: the configured one, or the checkbox list once there are too many options for pills. */
|
|
76987
77044
|
function resolveListLayout(field) {
|
|
76988
77045
|
return (field.options?.length ?? 0) > MAX_PILL_OPTIONS
|
|
76989
77046
|
? 'list'
|
|
76990
77047
|
: (field.layout ?? 'chips');
|
|
76991
77048
|
}
|
|
76992
|
-
/**
|
|
76993
|
-
const
|
|
76994
|
-
|
|
77049
|
+
/** A card dropdown with nothing picked: `inputPlaceholder`, else `Any <label>` (single) or `Select <label>` (set `inputPlaceholder` for acronyms). */
|
|
77050
|
+
const getDropdownPlaceholder = (field) => field.inputPlaceholder ??
|
|
77051
|
+
`${field.single ? 'Any' : 'Select'} ${field.label.toLowerCase()}`;
|
|
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. */
|
|
76995
77053
|
function toPopoverField(field) {
|
|
76996
77054
|
if (field.type === 'scale' ||
|
|
76997
77055
|
(field.type === 'list' && resolveListLayout(field) === 'tiles')) {
|
|
@@ -77013,21 +77071,17 @@ function getPopoverStatus(field, value) {
|
|
|
77013
77071
|
return describeFilterValue(field, value) || undefined;
|
|
77014
77072
|
}
|
|
77015
77073
|
|
|
77016
|
-
/** Single-choice list on a card, as the design's dropdown ("Any supplier"); the filter row lists the same options as a pick-one list. */
|
|
77017
|
-
function SingleSelectFilter({ field, selected, onSelect, }) {
|
|
77018
|
-
const options = field.options ?? [];
|
|
77019
|
-
return (jsxRuntime.jsx(SearchableSelect, { options: options, selectedItem: options.find((option) => option.value === selected) ?? null, onChange: (option) => onSelect(option.value), getOptionKey: (option) => option.value, getOptionLabel: (option) => option.label, searchPlaceholder: getSingleSelectPlaceholder(field), hideTriggerSearchIcon: true }));
|
|
77020
|
-
}
|
|
77021
77074
|
/** The control alone — identical in a filter-row popover and on a card, except the list layout. Compose it into custom cards. */
|
|
77022
77075
|
function FilterFieldControl({ field, value, onChange, fiscalYearStartMonth, fill, inPopover, }) {
|
|
77023
77076
|
const ariaLabel = `${field.label} filter`;
|
|
77024
77077
|
switch (field.type) {
|
|
77025
77078
|
case 'list': {
|
|
77026
|
-
if (field.single && !inPopover) {
|
|
77027
|
-
return (jsxRuntime.jsx(SingleSelectFilter, { field: field, selected: valueOfType(value, 'list')?.values[0], onSelect: (next) => onChange(toListValue([next])) }));
|
|
77028
|
-
}
|
|
77029
77079
|
const layout = resolveListLayout(field);
|
|
77030
|
-
|
|
77080
|
+
// On a card a long list is a compact dropdown with its picks as pills; the filter row's popover already is one.
|
|
77081
|
+
if (!inPopover && layout === 'list') {
|
|
77082
|
+
return (jsxRuntime.jsx(PickListDropdown, { ariaLabel: ariaLabel, options: field.options ?? [], value: valueOfType(value, 'list')?.values ?? [], onChange: (values) => onChange(toListValue(values)), placeholder: getDropdownPlaceholder(field), searchPlaceholder: field.searchPlaceholder, single: field.single }));
|
|
77083
|
+
}
|
|
77084
|
+
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 }));
|
|
77031
77085
|
}
|
|
77032
77086
|
case 'scale':
|
|
77033
77087
|
return (jsxRuntime.jsx(ScaleRangeFilter, { ariaLabel: ariaLabel, options: field.options ?? [], value: valueOfType(value, 'scale')?.range, onChange: (range) => onChange(toScaleValue(range)), columns: field.columns, fill: fill }));
|
|
@@ -77040,9 +77094,9 @@ function FilterFieldControl({ field, value, onChange, fiscalYearStartMonth, fill
|
|
|
77040
77094
|
case 'contains':
|
|
77041
77095
|
return (jsxRuntime.jsx(ContainsFilter, { ariaLabel: ariaLabel, size: "field", placeholder: field.inputPlaceholder, value: valueOfType(value, 'contains')?.text ?? '', onChange: (text) => onChange(toContainsValue(text)) }));
|
|
77042
77096
|
case 'date':
|
|
77043
|
-
return (jsxRuntime.jsx(DateRangeFilter, { ariaLabel: ariaLabel, value: valueOfType(value, 'date')?.range, onChange: (range) => onChange(toDateValue(range)), fiscalYearStartMonth: fiscalYearStartMonth }));
|
|
77097
|
+
return (jsxRuntime.jsx(DateRangeFilter, { ariaLabel: ariaLabel, value: valueOfType(value, 'date')?.range, onChange: (range) => onChange(toDateValue(range)), fiscalYearStartMonth: fiscalYearStartMonth, compact: !inPopover }));
|
|
77044
77098
|
case 'bool':
|
|
77045
|
-
return (jsxRuntime.jsx(TriStateFilter, { ariaLabel: ariaLabel, value: valueOfType(value, 'bool')?.value ?? 'any', onChange: (next) => onChange(toBoolValue(next)) }));
|
|
77099
|
+
return (jsxRuntime.jsx(TriStateFilter, { ariaLabel: ariaLabel, value: valueOfType(value, 'bool')?.value ?? 'any', onChange: (next) => onChange(toBoolValue(next)), compact: !inPopover }));
|
|
77046
77100
|
}
|
|
77047
77101
|
}
|
|
77048
77102
|
/** Filter-row cell: inline input for number/contains, otherwise a trigger that opens the control in a popover. */
|
|
@@ -77087,16 +77141,12 @@ const RETRY_LABEL = 'Try again';
|
|
|
77087
77141
|
/** Placeholder cards while the filters load; roughly the first rows of a card grid. */
|
|
77088
77142
|
const SKELETON_CARD_COUNT = 8;
|
|
77089
77143
|
const SKELETON_GRID_CLASS_NAME = 'grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4';
|
|
77090
|
-
|
|
77091
|
-
const
|
|
77092
|
-
/**
|
|
77093
|
-
|
|
77094
|
-
|
|
77095
|
-
|
|
77096
|
-
/** Shown in place of the cards when the filters couldn't load. */
|
|
77097
|
-
function FilterSurfaceError({ message, onRetry, }) {
|
|
77098
|
-
return (jsxRuntime.jsxs("div", { role: "alert", className: "flex flex-col items-center rounded-xl border border-border bg-card px-6 py-12 text-center", children: [jsxRuntime.jsx("span", { className: "flex size-10 items-center justify-center rounded-full bg-destructive/10 text-destructive", children: jsxRuntime.jsx(TriangleAlert, { "aria-hidden": "true", className: "size-5" }) }), jsxRuntime.jsx("p", { className: "mt-4 text-base font-semibold text-foreground", children: ERROR_TITLE }), jsxRuntime.jsx("p", { className: "mt-1.5 max-w-sm text-sm text-muted-foreground", children: message }), onRetry && (jsxRuntime.jsx(Button, { appearance: "outline", color: "secondary", size: "md", leftIcon: jsxRuntime.jsx(RotateCcw, {}), onClick: onRetry, className: "mt-5", children: RETRY_LABEL }))] }));
|
|
77099
|
-
}
|
|
77144
|
+
/** Masonry grid rows are this tall; a card spans as many as its height needs. */
|
|
77145
|
+
const MASONRY_ROW_PX = 4;
|
|
77146
|
+
/** Rows a card of `height` px (including its bottom gap) spans; 0 until measured. */
|
|
77147
|
+
const getMasonrySpan = (height) => height > 0 ? Math.ceil(height / MASONRY_ROW_PX) : 0;
|
|
77148
|
+
/** Dense 4px rows; the vertical gap comes from each item's bottom padding. */
|
|
77149
|
+
const MASONRY_GRID_CLASS_NAME = 'grid-flow-row-dense auto-rows-[4px] gap-x-3';
|
|
77100
77150
|
|
|
77101
77151
|
const NESTED_LAYER_SELECTOR = '[data-slot="popover-content"], [role="dialog"], [role="menu"], [role="listbox"]';
|
|
77102
77152
|
function useCloseOnEscape(onClose) {
|
|
@@ -77118,14 +77168,49 @@ function useFilterSurfaceMore(defaultExpanded = false) {
|
|
|
77118
77168
|
const [expanded, setExpanded] = React.useState(defaultExpanded);
|
|
77119
77169
|
return { expanded, toggle: () => setExpanded((current) => !current) };
|
|
77120
77170
|
}
|
|
77171
|
+
/** Measures a masonry card and keeps its row span in step with its height (content opening, wrapping…). */
|
|
77172
|
+
function useMasonrySpan() {
|
|
77173
|
+
const [node, setNode] = React.useState(null);
|
|
77174
|
+
const [span, setSpan] = React.useState(0);
|
|
77175
|
+
React.useEffect(() => {
|
|
77176
|
+
if (!node)
|
|
77177
|
+
return;
|
|
77178
|
+
const measure = () => {
|
|
77179
|
+
const next = getMasonrySpan(node.offsetHeight);
|
|
77180
|
+
setSpan((current) => (current === next ? current : next));
|
|
77181
|
+
};
|
|
77182
|
+
measure();
|
|
77183
|
+
const observer = new ResizeObserver(measure);
|
|
77184
|
+
observer.observe(node);
|
|
77185
|
+
return () => observer.disconnect();
|
|
77186
|
+
}, [node]);
|
|
77187
|
+
return { ref: React.useCallback((next) => setNode(next), []), span };
|
|
77188
|
+
}
|
|
77189
|
+
|
|
77190
|
+
const DEFAULT_SKELETON_CARDS = Array.from({ length: SKELETON_CARD_COUNT }, (_, index) => `card-${index}`);
|
|
77191
|
+
/** Loading placeholder: the page's own skeleton, or generic list cards. */
|
|
77192
|
+
function FilterSurfaceSkeleton({ children, }) {
|
|
77193
|
+
return (jsxRuntime.jsx("div", { role: "status", "aria-label": LOADING_LABEL, children: children ?? (jsxRuntime.jsx("div", { className: SKELETON_GRID_CLASS_NAME, children: DEFAULT_SKELETON_CARDS.map((id) => (jsxRuntime.jsx(FilterCardSkeleton, {}, id))) })) }));
|
|
77194
|
+
}
|
|
77195
|
+
/** Shown in place of the cards when the filters couldn't load. */
|
|
77196
|
+
function FilterSurfaceError({ message, onRetry, }) {
|
|
77197
|
+
return (jsxRuntime.jsxs("div", { role: "alert", className: "flex flex-col items-center rounded-xl border border-border bg-card px-6 py-12 text-center", children: [jsxRuntime.jsx("span", { className: "flex size-10 items-center justify-center rounded-full bg-destructive/10 text-destructive", children: jsxRuntime.jsx(TriangleAlert, { "aria-hidden": "true", className: "size-5" }) }), jsxRuntime.jsx("p", { className: "mt-4 text-base font-semibold text-foreground", children: ERROR_TITLE }), jsxRuntime.jsx("p", { className: "mt-1.5 max-w-sm text-sm text-muted-foreground", children: message }), onRetry && (jsxRuntime.jsx(Button, { appearance: "outline", color: "secondary", size: "md", leftIcon: jsxRuntime.jsx(RotateCcw, {}), onClick: onRetry, className: "mt-5", children: RETRY_LABEL }))] }));
|
|
77198
|
+
}
|
|
77199
|
+
/** One masonry cell: spans the rows its card needs; the bottom padding is the gap to the next card. */
|
|
77200
|
+
function MasonryItem({ children }) {
|
|
77201
|
+
const { ref, span } = useMasonrySpan();
|
|
77202
|
+
return (jsxRuntime.jsx("div", { style: span > 0 ? { gridRowEnd: `span ${span}` } : undefined, children: jsxRuntime.jsx("div", { ref: ref, className: "pb-3", children: children }) }));
|
|
77203
|
+
}
|
|
77121
77204
|
|
|
77122
77205
|
/** Full-page filter screen (e.g. an advanced search): header, scrolling card grid, sticky apply footer. */
|
|
77123
77206
|
function FilterSurface({ title, headerActions, footerChips, footerActions, onClose, status = 'ready', errorMessage, onRetry, skeleton, children, }) {
|
|
77124
77207
|
useCloseOnEscape(onClose);
|
|
77125
77208
|
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 })] }))] }));
|
|
77126
77209
|
}
|
|
77127
|
-
function FilterSurfaceGroup({ title, className, children, }) {
|
|
77128
|
-
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
|
|
77210
|
+
function FilterSurfaceGroup({ title, masonry = false, className, children, }) {
|
|
77211
|
+
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
|
|
77212
|
+
? React.Children.map(children, (child) => child ? jsxRuntime.jsx(MasonryItem, { children: child }) : null)
|
|
77213
|
+
: children })] }));
|
|
77129
77214
|
}
|
|
77130
77215
|
/** Collapsed "More filters" divider that reveals extra cards. */
|
|
77131
77216
|
function FilterSurfaceMore({ label, defaultExpanded, children, }) {
|
|
@@ -79853,6 +79938,7 @@ exports.PageTabs = PageTabs;
|
|
|
79853
79938
|
exports.Pagination = Pagination;
|
|
79854
79939
|
exports.PaginationSelect = PaginationSelect;
|
|
79855
79940
|
exports.PartnerShareCard = PartnerShareCard;
|
|
79941
|
+
exports.PickListDropdown = PickListDropdown;
|
|
79856
79942
|
exports.PickListFilter = PickListFilter;
|
|
79857
79943
|
exports.PriceInput = PriceInput;
|
|
79858
79944
|
exports.Protected = Protected;
|
|
@@ -79867,6 +79953,7 @@ exports.SEARCHABLE_SELECT_PAGE_SIZE = SEARCHABLE_SELECT_PAGE_SIZE;
|
|
|
79867
79953
|
exports.SafeImage = SafeImage;
|
|
79868
79954
|
exports.SaveSearchDialog = SaveSearchDialog;
|
|
79869
79955
|
exports.SavedSearchChipList = SavedSearchChipList;
|
|
79956
|
+
exports.SavedSearchDetail = SavedSearchDetail;
|
|
79870
79957
|
exports.SavedSearchesMenu = SavedSearchesMenu;
|
|
79871
79958
|
exports.ScaleRangeFilter = ScaleRangeFilter;
|
|
79872
79959
|
exports.ScrollFade = ScrollFade;
|