@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.
- package/dist/components/atomic/Button/buttonLook.js +18 -0
- package/dist/components/atomic/Tooltip/Tooltip.d.ts +14 -0
- package/dist/components/atomic/Tooltip/Tooltip.js +14 -0
- package/dist/components/atomic/Tooltip/index.d.ts +2 -0
- package/dist/components/molecule/ConfirmDialog/ConfirmDialog.d.ts +17 -0
- package/dist/components/molecule/ConfirmDialog/ConfirmDialog.js +28 -0
- package/dist/components/molecule/ConfirmDialog/index.d.ts +2 -0
- package/dist/components/molecule/DropdownMenu/DropdownMenu.d.ts +28 -0
- package/dist/components/molecule/DropdownMenu/DropdownMenu.js +53 -0
- package/dist/components/molecule/DropdownMenu/index.d.ts +2 -0
- package/dist/components/molecule/FacetFilter/FacetFilter.d.ts +14 -0
- package/dist/components/molecule/FacetFilter/FacetFilter.js +30 -0
- package/dist/components/molecule/FacetFilter/FacetList.d.ts +10 -0
- package/dist/components/molecule/FacetFilter/FacetList.js +103 -0
- package/dist/components/molecule/FacetFilter/FacetTrigger.d.ts +2 -0
- package/dist/components/molecule/FacetFilter/FacetTrigger.js +35 -0
- package/dist/components/molecule/FacetFilter/index.d.ts +3 -0
- package/dist/components/molecule/FacetFilterNumber/FacetFilterNumber.d.ts +29 -0
- package/dist/components/molecule/FacetFilterNumber/FacetFilterNumber.js +93 -0
- package/dist/components/molecule/FacetFilterNumber/index.d.ts +2 -0
- package/dist/components/molecule/FacetFilterSelect/FacetFilterSelect.d.ts +15 -0
- package/dist/components/molecule/FacetFilterSelect/FacetFilterSelect.js +41 -0
- package/dist/components/molecule/FacetFilterSelect/index.d.ts +2 -0
- package/dist/components/molecule/FilterMenu/FilterMenu.d.ts +23 -0
- package/dist/components/molecule/FilterMenu/FilterMenu.js +131 -0
- package/dist/components/molecule/FilterMenu/index.d.ts +2 -0
- package/dist/components/molecule/Modal/Modal.d.ts +22 -0
- package/dist/components/molecule/Modal/Modal.js +48 -0
- package/dist/components/molecule/Modal/index.d.ts +2 -0
- package/dist/components/molecule/Popover/Popover.d.ts +24 -0
- package/dist/components/molecule/Popover/Popover.js +38 -0
- package/dist/components/molecule/Popover/index.d.ts +2 -0
- package/dist/components/molecule/Toaster/Toaster.d.ts +3 -0
- package/dist/components/molecule/Toaster/Toaster.js +19 -0
- package/dist/components/molecule/Toaster/index.d.ts +3 -0
- package/dist/components/molecule/Toaster/toast.d.ts +13 -0
- package/dist/components/molecule/Toaster/toast.js +18 -0
- package/dist/components/ui/alert-dialog.js +61 -0
- package/dist/components/ui/combobox.js +78 -0
- package/dist/components/ui/dialog.js +78 -0
- package/dist/components/ui/dropdown-menu.js +42 -0
- package/dist/components/ui/input-group.js +63 -0
- package/dist/components/ui/popover.js +32 -0
- package/dist/components/ui/tooltip.js +39 -0
- package/dist/index.d.ts +24 -2
- package/dist/index.js +12 -1
- package/dist/lib/types.d.ts +4 -0
- package/dist/theme/brandTheme.d.ts +1 -1
- package/dist/theme/brandTheme.js +10 -8
- package/package.json +2 -1
- 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,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,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,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,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,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,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,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,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>;
|