@ashwa-io/ui 0.0.5 → 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.
@@ -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>;
@@ -0,0 +1,131 @@
1
+ import { Badge } from "../../atomic/Badge/Badge.js";
2
+ import { Button } from "../../atomic/Button/Button.js";
3
+ import { buttonLook } from "../../atomic/Button/buttonLook.js";
4
+ import { FacetList } from "../FacetFilter/FacetList.js";
5
+ import { Button as Button$1 } from "../../ui/button.js";
6
+ import { Input } from "../../ui/input.js";
7
+ import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover.js";
8
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
+ import { ChevronRightIcon, FilterIcon } from "lucide-react";
10
+ import { useState } from "react";
11
+ // src/components/molecule/FilterMenu/FilterMenu.tsx
12
+ const FilterMenu = (props) => {
13
+ const { categories, value, onChange, label = "Filter", size = "sm", disabled = false, "data-testid": testId } = props;
14
+ const [query, setQuery] = useState("");
15
+ const [openId, setOpenId] = useState(null);
16
+ const selectedCount = (category) => category.options.filter((option) => value[category.id]?.includes(option.value)).length;
17
+ const activeCount = categories.filter((category) => selectedCount(category) > 0).length;
18
+ const look = buttonLook("outline", size, false);
19
+ const needle = query.trim().toLowerCase();
20
+ const visible = categories.filter((category) => category.label.toLowerCase().includes(needle));
21
+ const clearAll = () => onChange({
22
+ ...value,
23
+ ...Object.fromEntries(categories.map(({ id }) => [id, []]))
24
+ });
25
+ return /* @__PURE__ */ jsxs(Popover, {
26
+ onOpenChange: (open) => {
27
+ if (open) return;
28
+ setQuery("");
29
+ setOpenId(null);
30
+ },
31
+ children: [/* @__PURE__ */ jsxs(PopoverTrigger, {
32
+ disabled,
33
+ "data-testid": testId,
34
+ render: /* @__PURE__ */ jsx(Button$1, {
35
+ variant: look.variant,
36
+ size: look.size
37
+ }),
38
+ children: [
39
+ /* @__PURE__ */ jsx(FilterIcon, {
40
+ "data-icon": "inline-start",
41
+ "aria-hidden": true
42
+ }),
43
+ label,
44
+ " ",
45
+ activeCount > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Badge, {
46
+ tone: "brand",
47
+ children: activeCount
48
+ }), /* @__PURE__ */ jsx("span", {
49
+ className: "sr-only",
50
+ children: " active"
51
+ })] })
52
+ ]
53
+ }), /* @__PURE__ */ jsxs(PopoverContent, {
54
+ align: "start",
55
+ "aria-label": label,
56
+ className: "w-60 gap-0 p-0",
57
+ children: [
58
+ /* @__PURE__ */ jsx("div", {
59
+ className: "border-b p-1",
60
+ children: /* @__PURE__ */ jsx(Input, {
61
+ "aria-label": "Search filters",
62
+ placeholder: "Search",
63
+ value: query,
64
+ onChange: (event) => setQuery(event.target.value)
65
+ })
66
+ }),
67
+ /* @__PURE__ */ jsxs("div", {
68
+ className: "max-h-60 overflow-y-auto p-1",
69
+ children: [visible.length === 0 && /* @__PURE__ */ jsx("p", {
70
+ className: "text-muted-foreground py-2 text-center text-sm",
71
+ children: "No results"
72
+ }), visible.map((category) => {
73
+ const count = selectedCount(category);
74
+ const multiple = category.multiple !== false;
75
+ return /* @__PURE__ */ jsxs(Popover, {
76
+ open: openId === category.id,
77
+ onOpenChange: (open) => setOpenId(open ? category.id : null),
78
+ children: [/* @__PURE__ */ jsxs(PopoverTrigger, {
79
+ render: /* @__PURE__ */ jsx("button", {
80
+ type: "button",
81
+ className: "hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent data-popup-open:bg-accent data-popup-open:text-accent-foreground flex w-full cursor-default items-center gap-2 rounded-md px-1.5 py-1 text-sm outline-none"
82
+ }),
83
+ children: [
84
+ /* @__PURE__ */ jsx("span", {
85
+ className: "min-w-0 flex-1 truncate text-left",
86
+ children: category.label
87
+ }),
88
+ count > 0 && /* @__PURE__ */ jsx("span", {
89
+ className: "text-xs tabular-nums",
90
+ children: count
91
+ }),
92
+ /* @__PURE__ */ jsx(ChevronRightIcon, {
93
+ "aria-hidden": true,
94
+ className: "size-4 shrink-0"
95
+ })
96
+ ]
97
+ }), /* @__PURE__ */ jsx(PopoverContent, {
98
+ side: "right",
99
+ align: "start",
100
+ "aria-label": category.label,
101
+ className: "w-60 gap-0 p-0",
102
+ children: /* @__PURE__ */ jsx(FacetList, {
103
+ label: category.label,
104
+ options: category.options,
105
+ value: value[category.id] ?? [],
106
+ onChange: (next) => onChange({
107
+ ...value,
108
+ [category.id]: next
109
+ }),
110
+ multiple,
111
+ showSelectAll: multiple
112
+ })
113
+ })]
114
+ }, category.id);
115
+ })]
116
+ }),
117
+ activeCount > 0 && /* @__PURE__ */ jsx("div", {
118
+ className: "border-t p-1",
119
+ children: /* @__PURE__ */ jsx(Button, {
120
+ variant: "ghost",
121
+ size: "sm",
122
+ fullWidth: true,
123
+ onClick: clearAll,
124
+ children: "Clear all"
125
+ })
126
+ })
127
+ ]
128
+ })]
129
+ });
130
+ };
131
+ export { FilterMenu };
@@ -0,0 +1,2 @@
1
+ import { FilterCategory, FilterMenu, FilterMenuProps, FilterMenuValue } from "./FilterMenu.js";
2
+ export { type FilterCategory, FilterMenu, type FilterMenuProps, type FilterMenuValue };
@@ -0,0 +1,78 @@
1
+ "use client";
2
+ import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "./input-group.js";
3
+ import { cn } from "cn";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react";
6
+ import "react";
7
+ import { Combobox } from "@base-ui/react";
8
+ // src/components/ui/combobox.tsx
9
+ const Combobox$1 = Combobox.Root;
10
+ const ComboboxTrigger = ({ className, children, ...props }) => {
11
+ return /* @__PURE__ */ jsxs(Combobox.Trigger, {
12
+ "data-slot": "combobox-trigger",
13
+ className: cn("[&_svg:not([class*='size-'])]:size-4", className),
14
+ ...props,
15
+ children: [children, /* @__PURE__ */ jsx(ChevronDownIcon, { className: "text-muted-foreground pointer-events-none size-4" })]
16
+ });
17
+ };
18
+ const ComboboxClear = ({ className, ...props }) => {
19
+ return /* @__PURE__ */ jsx(Combobox.Clear, {
20
+ "data-slot": "combobox-clear",
21
+ render: /* @__PURE__ */ jsx(InputGroupButton, {
22
+ variant: "ghost",
23
+ size: "icon-xs"
24
+ }),
25
+ className: cn(className),
26
+ ...props,
27
+ children: /* @__PURE__ */ jsx(XIcon, { className: "pointer-events-none" })
28
+ });
29
+ };
30
+ const ComboboxInput = ({ className, children, disabled = false, showTrigger = true, showClear = false, ...props }) => {
31
+ return /* @__PURE__ */ jsxs(InputGroup, {
32
+ className: cn("w-auto", className),
33
+ children: [
34
+ /* @__PURE__ */ jsx(Combobox.Input, {
35
+ render: /* @__PURE__ */ jsx(InputGroupInput, { disabled }),
36
+ ...props
37
+ }),
38
+ /* @__PURE__ */ jsxs(InputGroupAddon, {
39
+ align: "inline-end",
40
+ children: [showTrigger && /* @__PURE__ */ jsx(InputGroupButton, {
41
+ size: "icon-xs",
42
+ variant: "ghost",
43
+ render: /* @__PURE__ */ jsx(ComboboxTrigger, {}),
44
+ "data-slot": "input-group-button",
45
+ className: "group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent",
46
+ disabled
47
+ }), showClear && /* @__PURE__ */ jsx(ComboboxClear, { disabled })]
48
+ }),
49
+ children
50
+ ]
51
+ });
52
+ };
53
+ const ComboboxList = ({ className, ...props }) => {
54
+ return /* @__PURE__ */ jsx(Combobox.List, {
55
+ "data-slot": "combobox-list",
56
+ className: cn("no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto overscroll-contain p-1 data-empty:p-0", className),
57
+ ...props
58
+ });
59
+ };
60
+ const ComboboxItem = ({ className, children, ...props }) => {
61
+ return /* @__PURE__ */ jsxs(Combobox.Item, {
62
+ "data-slot": "combobox-item",
63
+ className: cn("data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground relative flex w-full cursor-default items-center gap-2 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className),
64
+ ...props,
65
+ children: [children, /* @__PURE__ */ jsx(Combobox.ItemIndicator, {
66
+ render: /* @__PURE__ */ jsx("span", { className: "pointer-events-none absolute right-2 flex size-4 items-center justify-center" }),
67
+ children: /* @__PURE__ */ jsx(CheckIcon, { className: "pointer-events-none" })
68
+ })]
69
+ });
70
+ };
71
+ const ComboboxEmpty = ({ className, ...props }) => {
72
+ return /* @__PURE__ */ jsx(Combobox.Empty, {
73
+ "data-slot": "combobox-empty",
74
+ className: cn("text-muted-foreground hidden w-full justify-center py-2 text-center text-sm group-data-empty/combobox-content:flex", className),
75
+ ...props
76
+ });
77
+ };
78
+ export { Combobox$1 as Combobox, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, ComboboxTrigger };
@@ -0,0 +1,63 @@
1
+ import { Button } from "./button.js";
2
+ import { Input } from "./input.js";
3
+ import { cva } from "class-variance-authority";
4
+ import { cn } from "cn";
5
+ import { jsx } from "react/jsx-runtime";
6
+ import "react";
7
+ // src/components/ui/input-group.tsx
8
+ const InputGroup = ({ className, ...props }) => {
9
+ return /* @__PURE__ */ jsx("div", {
10
+ "data-slot": "input-group",
11
+ role: "group",
12
+ className: cn("group/input-group border-input has-disabled:bg-input/50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 relative flex h-8 w-full min-w-0 items-center rounded-lg border transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-3 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5", className),
13
+ ...props
14
+ });
15
+ };
16
+ const inputGroupAddonVariants = cva("flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4", {
17
+ variants: { align: {
18
+ "inline-start": "order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
19
+ "inline-end": "order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
20
+ "block-start": "order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
21
+ "block-end": "order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2"
22
+ } },
23
+ defaultVariants: { align: "inline-start" }
24
+ });
25
+ const InputGroupAddon = ({ className, align = "inline-start", ...props }) => {
26
+ return /* @__PURE__ */ jsx("div", {
27
+ role: "group",
28
+ "data-slot": "input-group-addon",
29
+ "data-align": align,
30
+ className: cn(inputGroupAddonVariants({ align }), className),
31
+ onClick: (e) => {
32
+ if (e.target.closest("button")) return;
33
+ e.currentTarget.parentElement?.querySelector("input")?.focus();
34
+ },
35
+ ...props
36
+ });
37
+ };
38
+ const inputGroupButtonVariants = cva("flex items-center gap-2 text-sm shadow-none", {
39
+ variants: { size: {
40
+ xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
41
+ sm: "",
42
+ "icon-xs": "size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
43
+ "icon-sm": "size-8 p-0 has-[>svg]:p-0"
44
+ } },
45
+ defaultVariants: { size: "xs" }
46
+ });
47
+ const InputGroupButton = ({ className, type = "button", variant = "ghost", size = "xs", ...props }) => {
48
+ return /* @__PURE__ */ jsx(Button, {
49
+ type,
50
+ "data-size": size,
51
+ variant,
52
+ className: cn(inputGroupButtonVariants({ size }), className),
53
+ ...props
54
+ });
55
+ };
56
+ const InputGroupInput = ({ className, ...props }) => {
57
+ return /* @__PURE__ */ jsx(Input, {
58
+ "data-slot": "input-group-control",
59
+ className: cn("flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent", className),
60
+ ...props
61
+ });
62
+ };
63
+ export { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput };
package/dist/index.d.ts CHANGED
@@ -35,6 +35,15 @@ import "./components/molecule/ConfirmDialog/index.js";
35
35
  import { ConfirmDialog, ConfirmDialogProps } from "./components/molecule/ConfirmDialog/ConfirmDialog.js";
36
36
  import "./components/molecule/DropdownMenu/index.js";
37
37
  import { DropdownMenu, DropdownMenuProps, MenuItem } from "./components/molecule/DropdownMenu/DropdownMenu.js";
38
+ import "./components/molecule/FacetFilter/index.js";
39
+ import { FacetOption } from "./components/molecule/FacetFilter/FacetList.js";
40
+ import { FacetFilter, FacetFilterProps } from "./components/molecule/FacetFilter/FacetFilter.js";
41
+ import "./components/molecule/FacetFilterSelect/index.js";
42
+ import { FacetFilterSelect, FacetFilterSelectProps } from "./components/molecule/FacetFilterSelect/FacetFilterSelect.js";
43
+ import "./components/molecule/FacetFilterNumber/index.js";
44
+ import { FacetFilterNumber, FacetFilterNumberProps, NumberCondition, NumberFilterValue } from "./components/molecule/FacetFilterNumber/FacetFilterNumber.js";
45
+ import "./components/molecule/FilterMenu/index.js";
46
+ import { FilterCategory, FilterMenu, FilterMenuProps, FilterMenuValue } from "./components/molecule/FilterMenu/FilterMenu.js";
38
47
  import "./components/molecule/FormField/index.js";
39
48
  import { FormField, FormFieldControlProps, FormFieldProps } from "./components/molecule/FormField/FormField.js";
40
49
  import "./components/molecule/Modal/index.js";
@@ -47,4 +56,4 @@ import { Toaster } from "./components/molecule/Toaster/Toaster.js";
47
56
  import "./components/layout/Stack/index.js";
48
57
  import { HStack, HStackProps, Stack, StackAlign, StackGap, StackJustify, StackProps, VStack, VStackProps } from "./components/layout/Stack/Stack.js";
49
58
  import { FieldBaseProps, IconComponent, OverlayAlign, OverlaySide, Size, Tone } from "./lib/types.js";
50
- export { Badge, type BadgeProps, Button, type ButtonProps, type ButtonVariant, Checkbox, type CheckboxProps, ColorInput, type ColorInputProps, ConfirmDialog, type ConfirmDialogProps, Divider, type DividerProps, DropdownMenu, type DropdownMenuProps, type FieldBaseProps, FormField, type FormFieldControlProps, type FormFieldProps, FormLayout, type FormLayoutDirection, type FormLayoutProps, HStack, type HStackProps, Heading, type HeadingLevel, type HeadingProps, IconButton, type IconButtonProps, type IconButtonVariant, type IconComponent, type MenuItem, Modal, type ModalProps, type ModalSize, type OverlayAlign, type OverlaySide, Popover, type PopoverProps, type PopoverWidth, Select, type SelectOption, type SelectProps, type Size, Skeleton, type SkeletonProps, type SkeletonShape, Spinner, type SpinnerProps, Stack, type StackAlign, type StackGap, type StackJustify, type StackProps, Switch, type SwitchProps, Text, TextArea, type TextAreaProps, TextInput, type TextInputProps, type TextInputType, type TextProps, type TextSize, type TextTone, type TextWeight, type ToastOptions, Toaster, type Tone, Tooltip, type TooltipProps, VStack, type VStackProps, toast };
59
+ export { Badge, type BadgeProps, Button, type ButtonProps, type ButtonVariant, Checkbox, type CheckboxProps, ColorInput, type ColorInputProps, ConfirmDialog, type ConfirmDialogProps, Divider, type DividerProps, DropdownMenu, type DropdownMenuProps, FacetFilter, FacetFilterNumber, type FacetFilterNumberProps, type FacetFilterProps, FacetFilterSelect, type FacetFilterSelectProps, type FacetOption, type FieldBaseProps, type FilterCategory, FilterMenu, type FilterMenuProps, type FilterMenuValue, FormField, type FormFieldControlProps, type FormFieldProps, FormLayout, type FormLayoutDirection, type FormLayoutProps, HStack, type HStackProps, Heading, type HeadingLevel, type HeadingProps, IconButton, type IconButtonProps, type IconButtonVariant, type IconComponent, type MenuItem, Modal, type ModalProps, type ModalSize, type NumberCondition, type NumberFilterValue, type OverlayAlign, type OverlaySide, Popover, type PopoverProps, type PopoverWidth, Select, type SelectOption, type SelectProps, type Size, Skeleton, type SkeletonProps, type SkeletonShape, Spinner, type SpinnerProps, Stack, type StackAlign, type StackGap, type StackJustify, type StackProps, Switch, type SwitchProps, Text, TextArea, type TextAreaProps, TextInput, type TextInputProps, type TextInputType, type TextProps, type TextSize, type TextTone, type TextWeight, type ToastOptions, Toaster, type Tone, Tooltip, type TooltipProps, VStack, type VStackProps, toast };
package/dist/index.js CHANGED
@@ -16,10 +16,14 @@ import { Spinner } from "./components/atomic/Spinner/Spinner.js";
16
16
  import { FormLayout } from "./components/layout/FormLayout/FormLayout.js";
17
17
  import { ConfirmDialog } from "./components/molecule/ConfirmDialog/ConfirmDialog.js";
18
18
  import { DropdownMenu } from "./components/molecule/DropdownMenu/DropdownMenu.js";
19
+ import { FacetFilter } from "./components/molecule/FacetFilter/FacetFilter.js";
20
+ import { FacetFilterSelect } from "./components/molecule/FacetFilterSelect/FacetFilterSelect.js";
21
+ import { FacetFilterNumber } from "./components/molecule/FacetFilterNumber/FacetFilterNumber.js";
22
+ import { FilterMenu } from "./components/molecule/FilterMenu/FilterMenu.js";
19
23
  import { FormField } from "./components/molecule/FormField/FormField.js";
20
24
  import { Modal } from "./components/molecule/Modal/Modal.js";
21
25
  import { Popover } from "./components/molecule/Popover/Popover.js";
22
26
  import { toast } from "./components/molecule/Toaster/toast.js";
23
27
  import { Toaster } from "./components/molecule/Toaster/Toaster.js";
24
28
  import { HStack, Stack, VStack } from "./components/layout/Stack/Stack.js";
25
- export { Badge, Button, Checkbox, ColorInput, ConfirmDialog, Divider, DropdownMenu, FormField, FormLayout, HStack, Heading, IconButton, Modal, Popover, Select, Skeleton, Spinner, Stack, Switch, Text, TextArea, TextInput, Toaster, Tooltip, VStack, toast };
29
+ export { Badge, Button, Checkbox, ColorInput, ConfirmDialog, Divider, DropdownMenu, FacetFilter, FacetFilterNumber, FacetFilterSelect, FilterMenu, FormField, FormLayout, HStack, Heading, IconButton, Modal, Popover, Select, Skeleton, Spinner, Stack, Switch, Text, TextArea, TextInput, Toaster, Tooltip, VStack, toast };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ashwa-io/ui",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Ashwa design system: locked React components on shadcn/ui (Base UI) and Tailwind v4, with per-org brand theming.",
5
5
  "license": "MIT",
6
6
  "type": "module",