@ashwa-io/ui 0.0.4 → 0.0.6

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 (51) hide show
  1. package/dist/components/atomic/Button/buttonLook.js +18 -0
  2. package/dist/components/atomic/Tooltip/Tooltip.d.ts +14 -0
  3. package/dist/components/atomic/Tooltip/Tooltip.js +14 -0
  4. package/dist/components/atomic/Tooltip/index.d.ts +2 -0
  5. package/dist/components/molecule/ConfirmDialog/ConfirmDialog.d.ts +17 -0
  6. package/dist/components/molecule/ConfirmDialog/ConfirmDialog.js +28 -0
  7. package/dist/components/molecule/ConfirmDialog/index.d.ts +2 -0
  8. package/dist/components/molecule/DropdownMenu/DropdownMenu.d.ts +28 -0
  9. package/dist/components/molecule/DropdownMenu/DropdownMenu.js +53 -0
  10. package/dist/components/molecule/DropdownMenu/index.d.ts +2 -0
  11. package/dist/components/molecule/FacetFilter/FacetFilter.d.ts +14 -0
  12. package/dist/components/molecule/FacetFilter/FacetFilter.js +30 -0
  13. package/dist/components/molecule/FacetFilter/FacetList.d.ts +10 -0
  14. package/dist/components/molecule/FacetFilter/FacetList.js +103 -0
  15. package/dist/components/molecule/FacetFilter/FacetTrigger.d.ts +2 -0
  16. package/dist/components/molecule/FacetFilter/FacetTrigger.js +35 -0
  17. package/dist/components/molecule/FacetFilter/index.d.ts +3 -0
  18. package/dist/components/molecule/FacetFilterNumber/FacetFilterNumber.d.ts +29 -0
  19. package/dist/components/molecule/FacetFilterNumber/FacetFilterNumber.js +93 -0
  20. package/dist/components/molecule/FacetFilterNumber/index.d.ts +2 -0
  21. package/dist/components/molecule/FacetFilterSelect/FacetFilterSelect.d.ts +15 -0
  22. package/dist/components/molecule/FacetFilterSelect/FacetFilterSelect.js +41 -0
  23. package/dist/components/molecule/FacetFilterSelect/index.d.ts +2 -0
  24. package/dist/components/molecule/FilterMenu/FilterMenu.d.ts +23 -0
  25. package/dist/components/molecule/FilterMenu/FilterMenu.js +131 -0
  26. package/dist/components/molecule/FilterMenu/index.d.ts +2 -0
  27. package/dist/components/molecule/Modal/Modal.d.ts +22 -0
  28. package/dist/components/molecule/Modal/Modal.js +48 -0
  29. package/dist/components/molecule/Modal/index.d.ts +2 -0
  30. package/dist/components/molecule/Popover/Popover.d.ts +24 -0
  31. package/dist/components/molecule/Popover/Popover.js +38 -0
  32. package/dist/components/molecule/Popover/index.d.ts +2 -0
  33. package/dist/components/molecule/Toaster/Toaster.d.ts +3 -0
  34. package/dist/components/molecule/Toaster/Toaster.js +19 -0
  35. package/dist/components/molecule/Toaster/index.d.ts +3 -0
  36. package/dist/components/molecule/Toaster/toast.d.ts +13 -0
  37. package/dist/components/molecule/Toaster/toast.js +18 -0
  38. package/dist/components/ui/alert-dialog.js +61 -0
  39. package/dist/components/ui/combobox.js +78 -0
  40. package/dist/components/ui/dialog.js +78 -0
  41. package/dist/components/ui/dropdown-menu.js +42 -0
  42. package/dist/components/ui/input-group.js +63 -0
  43. package/dist/components/ui/popover.js +32 -0
  44. package/dist/components/ui/tooltip.js +39 -0
  45. package/dist/index.d.ts +24 -2
  46. package/dist/index.js +12 -1
  47. package/dist/lib/types.d.ts +4 -0
  48. package/dist/theme/brandTheme.d.ts +1 -1
  49. package/dist/theme/brandTheme.js +10 -8
  50. package/package.json +2 -1
  51. package/src/styles/index.css +45 -43
@@ -0,0 +1,18 @@
1
+ import { BUTTON_VARIANT } from "./buttonVariant.js";
2
+ // src/components/atomic/Button/buttonLook.ts
3
+ const TEXT_SIZE = {
4
+ sm: "sm",
5
+ md: "default",
6
+ lg: "lg"
7
+ };
8
+ const ICON_SIZE = {
9
+ sm: "icon-sm",
10
+ md: "icon",
11
+ lg: "icon-lg"
12
+ };
13
+ /** Primitive props for an overlay trigger drawn in the public Button look. */
14
+ const buttonLook = (variant, size, iconOnly) => ({
15
+ variant: BUTTON_VARIANT[variant],
16
+ size: (iconOnly ? ICON_SIZE : TEXT_SIZE)[size]
17
+ });
18
+ export { buttonLook };
@@ -0,0 +1,14 @@
1
+ import { OverlaySide } from "../../../lib/types.js";
2
+ import React from "react";
3
+ export interface TooltipProps {
4
+ /**
5
+ * Short plain text, a visual hint only: screen readers don't get it, and it needs a focusable
6
+ * child to appear for keyboards. Never put essential information only here.
7
+ */
8
+ content: string;
9
+ children: React.ReactNode;
10
+ side?: OverlaySide;
11
+ /** Lets a full-width child (e.g. `<Button fullWidth>`) fill its container. */
12
+ fullWidth?: boolean;
13
+ }
14
+ export declare const Tooltip: React.FC<TooltipProps>;
@@ -0,0 +1,14 @@
1
+ import { Tooltip as Tooltip$1, TooltipContent, TooltipProvider, TooltipTrigger } from "../../ui/tooltip.js";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ // src/components/atomic/Tooltip/Tooltip.tsx
4
+ const Tooltip = (props) => {
5
+ const { content, children, side = "top", fullWidth = false } = props;
6
+ return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(Tooltip$1, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
7
+ render: /* @__PURE__ */ jsx("span", { className: fullWidth ? "flex w-full" : "inline-flex max-w-full min-w-0" }),
8
+ children
9
+ }), /* @__PURE__ */ jsx(TooltipContent, {
10
+ side,
11
+ children: content
12
+ })] }) });
13
+ };
14
+ export { Tooltip };
@@ -0,0 +1,2 @@
1
+ import { Tooltip, TooltipProps } from "./Tooltip.js";
2
+ export { Tooltip, type TooltipProps };
@@ -0,0 +1,17 @@
1
+ import React from "react";
2
+ export interface ConfirmDialogProps {
3
+ open: boolean;
4
+ /** Cancel and Escape. */
5
+ onClose: () => void;
6
+ /** Never closes the dialog: the app closes it when its work finishes. */
7
+ onConfirm: () => void;
8
+ title: string;
9
+ description?: string;
10
+ confirmLabel: string;
11
+ cancelLabel?: string;
12
+ destructive?: boolean;
13
+ /** Confirm shows busy, Cancel is disabled and Escape is ignored. */
14
+ loading?: boolean;
15
+ 'data-testid'?: string;
16
+ }
17
+ export declare const ConfirmDialog: React.FC<ConfirmDialogProps>;
@@ -0,0 +1,28 @@
1
+ import { Button } from "../../atomic/Button/Button.js";
2
+ import { AlertDialog, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "../../ui/alert-dialog.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ // src/components/molecule/ConfirmDialog/ConfirmDialog.tsx
5
+ const ConfirmDialog = (props) => {
6
+ const { open, onClose, onConfirm, title, description, confirmLabel, cancelLabel = "Cancel", destructive = false, loading = false, "data-testid": testId } = props;
7
+ return /* @__PURE__ */ jsx(AlertDialog, {
8
+ open,
9
+ onOpenChange: (next) => {
10
+ if (!next && !loading) onClose();
11
+ },
12
+ children: /* @__PURE__ */ jsxs(AlertDialogContent, {
13
+ "data-testid": testId,
14
+ children: [/* @__PURE__ */ jsxs(AlertDialogHeader, { children: [/* @__PURE__ */ jsx(AlertDialogTitle, { children: title }), description && /* @__PURE__ */ jsx(AlertDialogDescription, { children: description })] }), /* @__PURE__ */ jsxs(AlertDialogFooter, { children: [/* @__PURE__ */ jsx(Button, {
15
+ variant: "outline",
16
+ disabled: loading,
17
+ onClick: onClose,
18
+ children: cancelLabel
19
+ }), /* @__PURE__ */ jsx(Button, {
20
+ variant: destructive ? "destructive" : "primary",
21
+ loading,
22
+ onClick: onConfirm,
23
+ children: confirmLabel
24
+ })] })]
25
+ })
26
+ });
27
+ };
28
+ export { ConfirmDialog };
@@ -0,0 +1,2 @@
1
+ import { ConfirmDialog, ConfirmDialogProps } from "./ConfirmDialog.js";
2
+ export { ConfirmDialog, type ConfirmDialogProps };
@@ -0,0 +1,28 @@
1
+ import "../../atomic/Button/index.js";
2
+ import { ButtonVariant } from "../../atomic/Button/buttonVariant.js";
3
+ import { IconComponent, OverlayAlign, Size } from "../../../lib/types.js";
4
+ import React from "react";
5
+ export interface MenuItem {
6
+ label: string;
7
+ onSelect: () => void;
8
+ icon?: IconComponent;
9
+ /** Second line under the label. */
10
+ description?: string;
11
+ disabled?: boolean;
12
+ destructive?: boolean;
13
+ }
14
+ export interface DropdownMenuProps {
15
+ /** Trigger text (the trigger's aria-label when iconOnly). */
16
+ label: string;
17
+ items: MenuItem[];
18
+ icon?: IconComponent;
19
+ iconOnly?: boolean;
20
+ /** Trigger look. */
21
+ variant?: ButtonVariant;
22
+ size?: Size;
23
+ align?: OverlayAlign;
24
+ disabled?: boolean;
25
+ onOpenChange?: (open: boolean) => void;
26
+ 'data-testid'?: string;
27
+ }
28
+ export declare const DropdownMenu: React.FC<DropdownMenuProps>;
@@ -0,0 +1,53 @@
1
+ import { buttonLook } from "../../atomic/Button/buttonLook.js";
2
+ import { Button } from "../../ui/button.js";
3
+ import { DropdownMenu as DropdownMenu$1, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../../ui/dropdown-menu.js";
4
+ import { cn } from "../../../lib/utils.js";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { ChevronDownIcon } from "lucide-react";
7
+ // src/components/molecule/DropdownMenu/DropdownMenu.tsx
8
+ const DropdownMenu = (props) => {
9
+ const { label, items, icon: Icon, iconOnly = false, variant = "outline", size = "md", align = "end", disabled = false, onOpenChange, "data-testid": testId } = props;
10
+ const look = buttonLook(variant, size, iconOnly);
11
+ return /* @__PURE__ */ jsxs(DropdownMenu$1, {
12
+ onOpenChange: (open) => onOpenChange?.(open),
13
+ children: [/* @__PURE__ */ jsxs(DropdownMenuTrigger, {
14
+ disabled,
15
+ "aria-label": iconOnly ? label : void 0,
16
+ "data-testid": testId,
17
+ render: /* @__PURE__ */ jsx(Button, {
18
+ variant: look.variant,
19
+ size: look.size
20
+ }),
21
+ children: [
22
+ Icon && /* @__PURE__ */ jsx(Icon, {
23
+ "data-icon": iconOnly ? void 0 : "inline-start",
24
+ "aria-hidden": true
25
+ }),
26
+ !iconOnly && label,
27
+ !iconOnly && /* @__PURE__ */ jsx(ChevronDownIcon, {
28
+ "data-icon": "inline-end",
29
+ "aria-hidden": true
30
+ })
31
+ ]
32
+ }), /* @__PURE__ */ jsx(DropdownMenuContent, {
33
+ align,
34
+ className: "w-auto max-w-80 min-w-48",
35
+ children: items.map((item) => {
36
+ const ItemIcon = item.icon;
37
+ return /* @__PURE__ */ jsxs(DropdownMenuItem, {
38
+ onClick: item.onSelect,
39
+ disabled: item.disabled,
40
+ variant: item.destructive ? "destructive" : "default",
41
+ children: [ItemIcon && /* @__PURE__ */ jsx(ItemIcon, { "aria-hidden": true }), /* @__PURE__ */ jsxs("span", {
42
+ className: "flex flex-col",
43
+ children: [/* @__PURE__ */ jsx("span", { children: item.label }), item.description && /* @__PURE__ */ jsx("span", {
44
+ className: cn("text-xs", !item.destructive && "text-muted-foreground"),
45
+ children: item.description
46
+ })]
47
+ })]
48
+ }, item.label);
49
+ })
50
+ })]
51
+ });
52
+ };
53
+ export { DropdownMenu };
@@ -0,0 +1,2 @@
1
+ import { DropdownMenu, DropdownMenuProps, MenuItem } from "./DropdownMenu.js";
2
+ export { DropdownMenu, type DropdownMenuProps, type MenuItem };
@@ -0,0 +1,14 @@
1
+ import { FacetOption } from "./FacetList.js";
2
+ import { FacetSize } from "./FacetTrigger.js";
3
+ import React from "react";
4
+ export interface FacetFilterProps {
5
+ title: string;
6
+ options: FacetOption[];
7
+ /** Selected option values. */
8
+ value: string[];
9
+ onChange: (value: string[]) => void;
10
+ size?: FacetSize;
11
+ disabled?: boolean;
12
+ 'data-testid'?: string;
13
+ }
14
+ export declare const FacetFilter: React.FC<FacetFilterProps>;
@@ -0,0 +1,30 @@
1
+ import { Popover, PopoverContent } from "../../ui/popover.js";
2
+ import { FacetList } from "./FacetList.js";
3
+ import { FacetTrigger } from "./FacetTrigger.js";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ // src/components/molecule/FacetFilter/FacetFilter.tsx
6
+ const FacetFilter = (props) => {
7
+ const { title, options, value, onChange, size, disabled, "data-testid": testId } = props;
8
+ const selected = options.filter((option) => value.includes(option.value)).length;
9
+ const active = selected > 0 && selected < options.length;
10
+ return /* @__PURE__ */ jsxs(Popover, { children: [/* @__PURE__ */ jsx(FacetTrigger, {
11
+ title,
12
+ summary: active ? String(selected) : "All",
13
+ active,
14
+ size,
15
+ disabled,
16
+ "data-testid": testId
17
+ }), /* @__PURE__ */ jsx(PopoverContent, {
18
+ align: "start",
19
+ "aria-label": title,
20
+ className: "w-60 gap-0 p-0",
21
+ children: /* @__PURE__ */ jsx(FacetList, {
22
+ label: title,
23
+ options,
24
+ value,
25
+ onChange,
26
+ showSelectAll: true
27
+ })
28
+ })] });
29
+ };
30
+ export { FacetFilter };
@@ -0,0 +1,10 @@
1
+ import { IconComponent } from "../../../lib/types.js";
2
+ import "react";
3
+ export interface FacetOption {
4
+ value: string;
5
+ label: string;
6
+ icon?: IconComponent;
7
+ /** Result count shown at the end of the row. */
8
+ count?: number;
9
+ disabled?: boolean;
10
+ }
@@ -0,0 +1,103 @@
1
+ import { Button } from "../../atomic/Button/Button.js";
2
+ import { Combobox, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList } from "../../ui/combobox.js";
3
+ import { cn } from "../../../lib/utils.js";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { CheckIcon } from "lucide-react";
6
+ import { useState } from "react";
7
+ // src/components/molecule/FacetFilter/FacetList.tsx
8
+ const CheckBox = (props) => /* @__PURE__ */ jsx("span", {
9
+ "aria-hidden": true,
10
+ className: cn("border-input flex size-4 shrink-0 items-center justify-center rounded-[4px] border", props.checked && "border-primary bg-primary"),
11
+ children: props.checked && /* @__PURE__ */ jsx(CheckIcon, { className: "text-primary-foreground! size-3.5" })
12
+ });
13
+ /** Internal: search box + arrow-key list, shared by the facet filters and FilterMenu. */
14
+ const FacetList = (props) => {
15
+ const { label, options, value, onChange, multiple = true, showSelectAll = false, clearable = true } = props;
16
+ const [query, setQuery] = useState("");
17
+ const byValue = new Map(options.map((option) => [option.value, option]));
18
+ const enabled = options.filter((option) => !option.disabled).map((option) => option.value);
19
+ const allSelected = enabled.length > 0 && enabled.every((item) => value.includes(item));
20
+ const locked = value.filter((item) => byValue.get(item)?.disabled);
21
+ const handleChange = (next) => {
22
+ if (multiple) {
23
+ onChange(next);
24
+ return;
25
+ }
26
+ onChange(next.filter((item) => !value.includes(item)).slice(-1));
27
+ };
28
+ return /* @__PURE__ */ jsxs(Combobox, {
29
+ inline: true,
30
+ open: true,
31
+ multiple: true,
32
+ items: options.map((option) => option.value),
33
+ itemToStringLabel: (item) => byValue.get(item)?.label ?? item,
34
+ value,
35
+ onValueChange: handleChange,
36
+ inputValue: query,
37
+ onInputValueChange: (next, details) => {
38
+ if (!details.isItemPress) setQuery(next);
39
+ },
40
+ children: [
41
+ /* @__PURE__ */ jsx("div", {
42
+ className: "border-b p-1",
43
+ children: /* @__PURE__ */ jsx(ComboboxInput, {
44
+ showTrigger: false,
45
+ placeholder: "Search",
46
+ "aria-label": label
47
+ })
48
+ }),
49
+ showSelectAll && query === "" && /* @__PURE__ */ jsx("div", {
50
+ className: "px-1 pt-1",
51
+ children: /* @__PURE__ */ jsxs("button", {
52
+ type: "button",
53
+ "aria-pressed": allSelected,
54
+ onClick: () => onChange(allSelected ? locked : [...locked, ...enabled]),
55
+ className: "hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground flex w-full cursor-default items-center gap-2 rounded-md py-1 pl-1.5 text-sm font-medium outline-none",
56
+ children: [/* @__PURE__ */ jsx(CheckBox, { checked: allSelected }), "Select all"]
57
+ })
58
+ }),
59
+ /* @__PURE__ */ jsx(ComboboxEmpty, {
60
+ className: "flex empty:hidden",
61
+ children: "No results"
62
+ }),
63
+ /* @__PURE__ */ jsx(ComboboxList, {
64
+ className: "max-h-60",
65
+ "aria-multiselectable": multiple,
66
+ children: (item) => {
67
+ const option = byValue.get(item);
68
+ if (!option) return null;
69
+ const Icon = option.icon;
70
+ const selected = value.includes(item);
71
+ return /* @__PURE__ */ jsxs(ComboboxItem, {
72
+ value: item,
73
+ disabled: option.disabled,
74
+ className: cn(selected && "bg-brand-tint text-brand-tint-foreground", multiple && "pr-1.5 [&>.absolute]:hidden"),
75
+ children: [
76
+ multiple && /* @__PURE__ */ jsx(CheckBox, { checked: selected }),
77
+ Icon && /* @__PURE__ */ jsx(Icon, { "aria-hidden": true }),
78
+ /* @__PURE__ */ jsx("span", {
79
+ className: "min-w-0 flex-1 truncate",
80
+ children: option.label
81
+ }),
82
+ option.count !== void 0 && /* @__PURE__ */ jsx("span", {
83
+ className: cn("text-xs tabular-nums", !selected && "text-muted-foreground"),
84
+ children: option.count
85
+ })
86
+ ]
87
+ }, item);
88
+ }
89
+ }),
90
+ clearable && value.length > 0 && /* @__PURE__ */ jsx("div", {
91
+ className: "border-t p-1",
92
+ children: /* @__PURE__ */ jsx(Button, {
93
+ variant: "ghost",
94
+ size: "sm",
95
+ fullWidth: true,
96
+ onClick: () => onChange([]),
97
+ children: "Clear filters"
98
+ })
99
+ })
100
+ ]
101
+ });
102
+ };
103
+ export { FacetList };
@@ -0,0 +1,2 @@
1
+ import "react";
2
+ export type FacetSize = 'sm' | 'md';
@@ -0,0 +1,35 @@
1
+ import { Button } from "../../ui/button.js";
2
+ import { PopoverTrigger } from "../../ui/popover.js";
3
+ import { cn } from "../../../lib/utils.js";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { ChevronDownIcon } from "lucide-react";
6
+ // src/components/molecule/FacetFilter/FacetTrigger.tsx
7
+ /** Internal: the dashed filter button; must be rendered inside a popover root. */
8
+ const FacetTrigger = (props) => {
9
+ const { title, summary, active, size = "sm", disabled = false, "data-testid": testId } = props;
10
+ return /* @__PURE__ */ jsxs(PopoverTrigger, {
11
+ disabled,
12
+ "data-testid": testId,
13
+ render: /* @__PURE__ */ jsx(Button, {
14
+ variant: "outline",
15
+ size: size === "sm" ? "sm" : "default",
16
+ className: cn("font-normal", !active && "border-dashed")
17
+ }),
18
+ children: [
19
+ /* @__PURE__ */ jsx("span", { children: title }),
20
+ /* @__PURE__ */ jsx("span", {
21
+ "aria-hidden": true,
22
+ className: "bg-border h-4 w-px"
23
+ }),
24
+ /* @__PURE__ */ jsx("span", {
25
+ className: cn("max-w-40 truncate font-medium", active && "bg-brand-tint text-brand-tint-foreground rounded-sm px-1"),
26
+ children: summary
27
+ }),
28
+ /* @__PURE__ */ jsx(ChevronDownIcon, {
29
+ "data-icon": "inline-end",
30
+ "aria-hidden": true
31
+ })
32
+ ]
33
+ });
34
+ };
35
+ export { FacetTrigger };
@@ -0,0 +1,3 @@
1
+ import { FacetFilter, FacetFilterProps } from "./FacetFilter.js";
2
+ import { FacetOption } from "./FacetList.js";
3
+ export { FacetFilter, type FacetFilterProps, type FacetOption };
@@ -0,0 +1,29 @@
1
+ import { FacetSize } from "../FacetFilter/FacetTrigger.js";
2
+ import React from "react";
3
+ export interface NumberCondition {
4
+ /** e.g. 'gt', 'between'. */
5
+ value: string;
6
+ /** e.g. 'Greater than'. */
7
+ label: string;
8
+ /** Takes two values: from and to. */
9
+ range?: boolean;
10
+ }
11
+ export interface NumberFilterValue {
12
+ condition: string;
13
+ /** As typed; '' when empty. */
14
+ from: string;
15
+ /** Only used by range conditions. */
16
+ to: string;
17
+ }
18
+ export interface FacetFilterNumberProps {
19
+ title: string;
20
+ conditions: NumberCondition[];
21
+ value: NumberFilterValue | null;
22
+ onChange: (value: NumberFilterValue | null) => void;
23
+ /** Formats values in the trigger, e.g. value => `${value} km/h`. */
24
+ format?: (value: string) => string;
25
+ size?: FacetSize;
26
+ disabled?: boolean;
27
+ 'data-testid'?: string;
28
+ }
29
+ export declare const FacetFilterNumber: React.FC<FacetFilterNumberProps>;
@@ -0,0 +1,93 @@
1
+ import { Button } from "../../atomic/Button/Button.js";
2
+ import { Select } from "../../atomic/Select/Select.js";
3
+ import { FacetTrigger } from "../FacetFilter/FacetTrigger.js";
4
+ import { FormField } from "../FormField/FormField.js";
5
+ import { Input } from "../../ui/input.js";
6
+ import { Popover, PopoverContent } from "../../ui/popover.js";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ // src/components/molecule/FacetFilterNumber/FacetFilterNumber.tsx
9
+ const NumberField = (props) => /* @__PURE__ */ jsx(FormField, {
10
+ label: props.label,
11
+ children: (control) => /* @__PURE__ */ jsx(Input, {
12
+ id: control.id,
13
+ type: "number",
14
+ value: props.value,
15
+ onChange: (event) => props.onChange(event.target.value)
16
+ })
17
+ });
18
+ const summarize = (value, condition, format) => {
19
+ if (value.from === "") return condition.label;
20
+ const from = `${condition.label} ${format(value.from)}`;
21
+ return condition.range && value.to !== "" ? `${from} and ${format(value.to)}` : from;
22
+ };
23
+ const FacetFilterNumber = (props) => {
24
+ const { title, conditions, value, onChange, format = (text) => text, size, disabled, "data-testid": testId } = props;
25
+ const condition = conditions.find((item) => item.value === value?.condition);
26
+ const from = value?.from ?? "";
27
+ const to = value?.to ?? "";
28
+ return /* @__PURE__ */ jsxs(Popover, { children: [/* @__PURE__ */ jsx(FacetTrigger, {
29
+ title,
30
+ summary: value && condition ? summarize(value, condition, format) : "All",
31
+ active: condition !== void 0,
32
+ size,
33
+ disabled,
34
+ "data-testid": testId
35
+ }), /* @__PURE__ */ jsxs(PopoverContent, {
36
+ align: "start",
37
+ "aria-label": title,
38
+ className: "w-64",
39
+ children: [
40
+ /* @__PURE__ */ jsx(Select, {
41
+ label: "Condition",
42
+ options: conditions.map((item) => ({
43
+ value: item.value,
44
+ label: item.label
45
+ })),
46
+ value: condition?.value ?? null,
47
+ onChange: (next) => onChange({
48
+ condition: next,
49
+ from,
50
+ to: ""
51
+ }),
52
+ placeholder: "Select condition"
53
+ }),
54
+ value && condition && !condition.range && /* @__PURE__ */ jsx(NumberField, {
55
+ label: "Value",
56
+ value: from,
57
+ onChange: (next) => onChange({
58
+ condition: value.condition,
59
+ from: next,
60
+ to: ""
61
+ })
62
+ }),
63
+ value && condition?.range && /* @__PURE__ */ jsxs("div", {
64
+ className: "grid grid-cols-2 gap-2",
65
+ children: [/* @__PURE__ */ jsx(NumberField, {
66
+ label: "From",
67
+ value: from,
68
+ onChange: (next) => onChange({
69
+ condition: value.condition,
70
+ from: next,
71
+ to
72
+ })
73
+ }), /* @__PURE__ */ jsx(NumberField, {
74
+ label: "To",
75
+ value: to,
76
+ onChange: (next) => onChange({
77
+ condition: value.condition,
78
+ from,
79
+ to: next
80
+ })
81
+ })]
82
+ }),
83
+ value && /* @__PURE__ */ jsx(Button, {
84
+ variant: "ghost",
85
+ size: "sm",
86
+ fullWidth: true,
87
+ onClick: () => onChange(null),
88
+ children: "Clear filter"
89
+ })
90
+ ]
91
+ })] });
92
+ };
93
+ export { FacetFilterNumber };
@@ -0,0 +1,2 @@
1
+ import { FacetFilterNumber, FacetFilterNumberProps, NumberCondition, NumberFilterValue } from "./FacetFilterNumber.js";
2
+ export { FacetFilterNumber, type FacetFilterNumberProps, type NumberCondition, type NumberFilterValue };
@@ -0,0 +1,15 @@
1
+ import { FacetOption } from "../FacetFilter/FacetList.js";
2
+ import { FacetSize } from "../FacetFilter/FacetTrigger.js";
3
+ import React from "react";
4
+ export interface FacetFilterSelectProps {
5
+ title: string;
6
+ options: FacetOption[];
7
+ value: string | null;
8
+ onChange: (value: string | null) => void;
9
+ /** Always holds a value: no clear option, and onChange never gets null. */
10
+ required?: boolean;
11
+ size?: FacetSize;
12
+ disabled?: boolean;
13
+ 'data-testid'?: string;
14
+ }
15
+ export declare const FacetFilterSelect: React.FC<FacetFilterSelectProps>;
@@ -0,0 +1,41 @@
1
+ import { FacetList } from "../FacetFilter/FacetList.js";
2
+ import { FacetTrigger } from "../FacetFilter/FacetTrigger.js";
3
+ import { Popover, PopoverContent } from "../../ui/popover.js";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { useState } from "react";
6
+ // src/components/molecule/FacetFilterSelect/FacetFilterSelect.tsx
7
+ const FacetFilterSelect = (props) => {
8
+ const { title, options, value, onChange, required = false, size, disabled, "data-testid": testId } = props;
9
+ const [open, setOpen] = useState(false);
10
+ const selected = options.find((option) => option.value === value);
11
+ const handleChange = (next) => {
12
+ setOpen(false);
13
+ if (next.length > 0) onChange(next[0]);
14
+ else if (!required) onChange(null);
15
+ };
16
+ return /* @__PURE__ */ jsxs(Popover, {
17
+ open,
18
+ onOpenChange: setOpen,
19
+ children: [/* @__PURE__ */ jsx(FacetTrigger, {
20
+ title,
21
+ summary: selected?.label ?? "All",
22
+ active: selected !== void 0,
23
+ size,
24
+ disabled,
25
+ "data-testid": testId
26
+ }), /* @__PURE__ */ jsx(PopoverContent, {
27
+ align: "start",
28
+ "aria-label": title,
29
+ className: "w-60 gap-0 p-0",
30
+ children: /* @__PURE__ */ jsx(FacetList, {
31
+ label: title,
32
+ options,
33
+ value: value === null ? [] : [value],
34
+ onChange: handleChange,
35
+ multiple: false,
36
+ clearable: !required
37
+ })
38
+ })]
39
+ });
40
+ };
41
+ export { FacetFilterSelect };
@@ -0,0 +1,2 @@
1
+ import { FacetFilterSelect, FacetFilterSelectProps } from "./FacetFilterSelect.js";
2
+ export { FacetFilterSelect, type FacetFilterSelectProps };
@@ -0,0 +1,23 @@
1
+ import { FacetOption } from "../FacetFilter/FacetList.js";
2
+ import { Size } from "../../../lib/types.js";
3
+ import React from "react";
4
+ export interface FilterCategory {
5
+ id: string;
6
+ label: string;
7
+ options: FacetOption[];
8
+ /** false: one choice in this category. */
9
+ multiple?: boolean;
10
+ }
11
+ /** Selected option values by category id. */
12
+ export type FilterMenuValue = Record<string, string[]>;
13
+ export interface FilterMenuProps {
14
+ categories: FilterCategory[];
15
+ value: FilterMenuValue;
16
+ /** Always the whole object; keys that aren't categories are kept. */
17
+ onChange: (value: FilterMenuValue) => void;
18
+ label?: string;
19
+ size?: Size;
20
+ disabled?: boolean;
21
+ 'data-testid'?: string;
22
+ }
23
+ export declare const FilterMenu: React.FC<FilterMenuProps>;