@duestel/ui 0.1.0 → 0.1.2
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/README.md +12 -2
- package/dist/components/chip-input/ChipInput.d.ts +91 -0
- package/dist/components/chip-input/ChipInput.js +161 -0
- package/dist/components/chip-input/ChipInput.js.map +1 -0
- package/dist/components/chip-input/index.d.ts +2 -0
- package/dist/components/chip-input/index.js +2 -0
- package/dist/components/combobox/Combobox.js +1 -1
- package/dist/components/combobox/Combobox.js.map +1 -1
- package/dist/components/multi-select/MultiSelect.d.ts +77 -0
- package/dist/components/multi-select/MultiSelect.js +54 -0
- package/dist/components/multi-select/MultiSelect.js.map +1 -0
- package/dist/components/multi-select/index.d.ts +2 -0
- package/dist/components/multi-select/index.js +3 -0
- package/dist/components/number-field/NumberField.js +1 -1
- package/dist/components/number-field/NumberField.js.map +1 -1
- package/dist/components/otp-field/OtpField.js +1 -1
- package/dist/components/otp-field/OtpField.js.map +1 -1
- package/dist/components/select/Select.js +1 -1
- package/dist/components/select/Select.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +41 -39
- package/dist/styles.css +1 -1
- package/package.json +169 -42
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ A **Material 3 (M3)** React component library — the Duestel design guide, pack
|
|
|
10
10
|
|
|
11
11
|
> Published publicly under the `@duestel` scope — `npm install @duestel/ui`, no auth required.
|
|
12
12
|
|
|
13
|
-
📖 **Docs & live component explorer:** [duestel.com
|
|
13
|
+
📖 **Docs & live component explorer:** [ui.duestel.com](https://ui.duestel.com/)
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
@@ -98,7 +98,17 @@ This imports the M3 `@theme` token layer and tells Tailwind to scan the package
|
|
|
98
98
|
| separator | slider | switch | tabs |
|
|
99
99
|
| toast | toggle | toolbar | tooltip |
|
|
100
100
|
|
|
101
|
-
**Data tables** are powered by [TanStack Table](https://tanstack.com/table); **app forms** by [TanStack Form](https://tanstack.com/form) (`useAppForm` pre-binds M3 fields). Browse every component's stories, variants, and props at [duestel.com
|
|
101
|
+
**Data tables** are powered by [TanStack Table](https://tanstack.com/table); **app forms** by [TanStack Form](https://tanstack.com/form) (`useAppForm` pre-binds M3 fields). Browse every component's stories, variants, and props at [ui.duestel.com](https://ui.duestel.com/).
|
|
102
|
+
|
|
103
|
+
## MCP server (AI agents)
|
|
104
|
+
|
|
105
|
+
A hosted [Storybook MCP](https://storybook.js.org/) server exposes the component docs to AI coding agents — list components, fetch props/variants/usage, and preview stories, straight from the live Storybook at [ui.duestel.com](https://ui.duestel.com/). Add it to Claude Code:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
claude mcp add --transport http duestel-ui https://ui.duestel.com/mcp
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Any MCP client that speaks streamable HTTP can connect to the same URL.
|
|
102
112
|
|
|
103
113
|
## Requirements
|
|
104
114
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ComponentProps, ReactNode, RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* ChipInput — Material 3 input-chips field: free text typed into an outlined
|
|
4
|
+
* text field is committed as removable M3 input chips (tags). No headless
|
|
5
|
+
* primitive exists for this in Base UI, so behaviour is implemented here:
|
|
6
|
+
* Enter or comma commits the trimmed text as a chip (duplicates ignored),
|
|
7
|
+
* Backspace in an empty input removes the last chip, and pasted text is split
|
|
8
|
+
* on commas/newlines into multiple chips. `name` renders one hidden input per
|
|
9
|
+
* chip for native form submission.
|
|
10
|
+
* Visuals match the library's existing chip language (Combobox multi-select):
|
|
11
|
+
* outlined 56px field container (extra-small corner, outline → primary on
|
|
12
|
+
* focus-within) wrapping 32px input chips (small corner, outline-variant
|
|
13
|
+
* border, surface-container-low) with a trailing remove target.
|
|
14
|
+
* M3 ref: https://m3.material.io/components/chips (input chips).
|
|
15
|
+
*
|
|
16
|
+
* Compound API:
|
|
17
|
+
* <ChipInput.Root defaultValue={['Design']} name="tags">
|
|
18
|
+
* <ChipInput.Label>Tags</ChipInput.Label>
|
|
19
|
+
* <ChipInput.Group>
|
|
20
|
+
* <ChipInput.Chips />
|
|
21
|
+
* <ChipInput.Input placeholder="Add a tag…" />
|
|
22
|
+
* <ChipInput.Clear />
|
|
23
|
+
* </ChipInput.Group>
|
|
24
|
+
* </ChipInput.Root>
|
|
25
|
+
*/
|
|
26
|
+
type ChipInputContextValue = {
|
|
27
|
+
value: string[];
|
|
28
|
+
add: (chip: string) => void;
|
|
29
|
+
remove: (chip: string) => void;
|
|
30
|
+
clear: () => void;
|
|
31
|
+
disabled: boolean;
|
|
32
|
+
inputValue: string;
|
|
33
|
+
setInputValue: (text: string) => void;
|
|
34
|
+
inputId: string;
|
|
35
|
+
inputRef: RefObject<HTMLInputElement | null>;
|
|
36
|
+
};
|
|
37
|
+
declare function useChipInput(): ChipInputContextValue;
|
|
38
|
+
type RootProps = {
|
|
39
|
+
children: ReactNode;
|
|
40
|
+
/** The committed chips — controlled. */
|
|
41
|
+
value?: string[];
|
|
42
|
+
/** The committed chips — uncontrolled initial. */
|
|
43
|
+
defaultValue?: string[];
|
|
44
|
+
onValueChange?: (value: string[]) => void;
|
|
45
|
+
/** Renders one hidden input per chip for native form submission. */
|
|
46
|
+
name?: string;
|
|
47
|
+
disabled?: boolean;
|
|
48
|
+
};
|
|
49
|
+
declare function Root({ children, value, defaultValue, onValueChange, name, disabled }: RootProps): import("react").JSX.Element;
|
|
50
|
+
/** M3 label above the field (label-large, on-surface); focuses the input. */
|
|
51
|
+
declare function Label({ className, ...props }: ComponentProps<'label'>): import("react").JSX.Element;
|
|
52
|
+
/**
|
|
53
|
+
* The M3 outlined text-field container: draws the 56px outlined box around the
|
|
54
|
+
* chips and the input; turns primary on focus-within. Clicking the empty area
|
|
55
|
+
* focuses the input.
|
|
56
|
+
*/
|
|
57
|
+
declare function Group({ className, onClick, ...props }: ComponentProps<'div'>): import("react").JSX.Element;
|
|
58
|
+
/**
|
|
59
|
+
* The committed chips. With no children renders each chip as
|
|
60
|
+
* `<Chip>{chip}<ChipRemove /></Chip>`; pass a render prop to customise.
|
|
61
|
+
*/
|
|
62
|
+
declare function Chips({ className, children, ...props }: Omit<ComponentProps<'div'>, 'children'> & {
|
|
63
|
+
children?: ((chip: string) => ReactNode) | ReactNode;
|
|
64
|
+
}): import("react").JSX.Element;
|
|
65
|
+
/** M3 input chip: 32px, small corner, outlined, surface-container-low. */
|
|
66
|
+
declare function Chip({ className, value, ...props }: ComponentProps<'div'> & {
|
|
67
|
+
/** The committed value this chip represents (consumed by ChipRemove). */
|
|
68
|
+
value: string;
|
|
69
|
+
}): import("react").JSX.Element;
|
|
70
|
+
/** The chip's trailing remove target (M3 input-chip trailing icon). */
|
|
71
|
+
declare function ChipRemove({ className, children, ...props }: ComponentProps<'button'>): import("react").JSX.Element;
|
|
72
|
+
/**
|
|
73
|
+
* Borderless by design — the surrounding Group draws the M3 field box.
|
|
74
|
+
* Enter/comma commits the text as a chip; Backspace in an empty input removes
|
|
75
|
+
* the last chip; pasted text splits on commas/newlines.
|
|
76
|
+
*/
|
|
77
|
+
declare function Input({ className, onKeyDown, onPaste, ...props }: Omit<ComponentProps<'input'>, 'value' | 'onChange'>): import("react").JSX.Element;
|
|
78
|
+
/** Clears all chips and the pending text; only mounted while there is something to clear. */
|
|
79
|
+
declare function Clear({ className, children, ...props }: ComponentProps<'button'>): import("react").JSX.Element | null;
|
|
80
|
+
export declare const ChipInput: {
|
|
81
|
+
Root: typeof Root;
|
|
82
|
+
Label: typeof Label;
|
|
83
|
+
Group: typeof Group;
|
|
84
|
+
Chips: typeof Chips;
|
|
85
|
+
Chip: typeof Chip;
|
|
86
|
+
ChipRemove: typeof ChipRemove;
|
|
87
|
+
Input: typeof Input;
|
|
88
|
+
Clear: typeof Clear;
|
|
89
|
+
};
|
|
90
|
+
export { Root, Label, Group, Chips, Chip, ChipRemove, Input, Clear, useChipInput };
|
|
91
|
+
export type { RootProps as ChipInputRootProps };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { cn as e } from "../../lib/cn.js";
|
|
2
|
+
import { X as t } from "lucide-react";
|
|
3
|
+
import { jsx as n, jsxs as r } from "react/jsx-runtime";
|
|
4
|
+
import { createContext as i, useCallback as a, useContext as o, useId as s, useMemo as c, useRef as l, useState as u } from "react";
|
|
5
|
+
//#region src/components/chip-input/ChipInput.tsx
|
|
6
|
+
var d = i(null);
|
|
7
|
+
function f() {
|
|
8
|
+
let e = o(d);
|
|
9
|
+
if (!e) throw Error("ChipInput parts must be used within <ChipInput.Root>");
|
|
10
|
+
return e;
|
|
11
|
+
}
|
|
12
|
+
function p({ children: e, value: t, defaultValue: i, onValueChange: o, name: f, disabled: p = !1 }) {
|
|
13
|
+
let [m, h] = u(i ?? []), g = t !== void 0, _ = g ? t : m, [v, y] = u(""), b = s(), x = l(null), S = a((e) => {
|
|
14
|
+
g || h(e), o?.(e);
|
|
15
|
+
}, [g, o]), C = a((e) => {
|
|
16
|
+
let t = e.trim();
|
|
17
|
+
!t || _.includes(t) || S([..._, t]);
|
|
18
|
+
}, [_, S]), w = a((e) => {
|
|
19
|
+
S(_.filter((t) => t !== e));
|
|
20
|
+
}, [_, S]), T = a(() => {
|
|
21
|
+
S([]), y(""), x.current?.focus();
|
|
22
|
+
}, [S]), E = c(() => ({
|
|
23
|
+
value: _,
|
|
24
|
+
add: C,
|
|
25
|
+
remove: w,
|
|
26
|
+
clear: T,
|
|
27
|
+
disabled: p,
|
|
28
|
+
inputValue: v,
|
|
29
|
+
setInputValue: y,
|
|
30
|
+
inputId: b,
|
|
31
|
+
inputRef: x
|
|
32
|
+
}), [
|
|
33
|
+
_,
|
|
34
|
+
C,
|
|
35
|
+
w,
|
|
36
|
+
T,
|
|
37
|
+
p,
|
|
38
|
+
v,
|
|
39
|
+
b
|
|
40
|
+
]);
|
|
41
|
+
return /* @__PURE__ */ r(d.Provider, {
|
|
42
|
+
value: E,
|
|
43
|
+
children: [e, f && _.map((e) => /* @__PURE__ */ n("input", {
|
|
44
|
+
type: "hidden",
|
|
45
|
+
name: f,
|
|
46
|
+
value: e,
|
|
47
|
+
disabled: p
|
|
48
|
+
}, e))]
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function m({ className: t, ...r }) {
|
|
52
|
+
let { inputId: i } = f();
|
|
53
|
+
return /* @__PURE__ */ n("label", {
|
|
54
|
+
htmlFor: i,
|
|
55
|
+
className: e("mb-1.5 block text-label-large text-on-surface", t),
|
|
56
|
+
...r
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function h({ className: t, onClick: r, ...i }) {
|
|
60
|
+
let { disabled: a, inputRef: o } = f();
|
|
61
|
+
return /* @__PURE__ */ n("div", {
|
|
62
|
+
onClick: (e) => {
|
|
63
|
+
r?.(e), e.defaultPrevented || o.current?.focus();
|
|
64
|
+
},
|
|
65
|
+
className: e("flex min-h-14 w-full cursor-text flex-wrap items-center gap-1.5 py-1 pl-4 pr-2", "rounded-small border border-outline bg-surface text-on-surface", "transition-colors", "focus-within:border-primary focus-within:outline-2 focus-within:-outline-offset-2 focus-within:outline-primary", a && "pointer-events-none opacity-[0.38]", t),
|
|
66
|
+
...i
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function g({ className: t, children: i, ...a }) {
|
|
70
|
+
let { value: o } = f();
|
|
71
|
+
return /* @__PURE__ */ n("div", {
|
|
72
|
+
className: e("contents", t),
|
|
73
|
+
...a,
|
|
74
|
+
children: typeof i == "function" ? o.map((e) => i(e)) : i ?? o.map((e) => /* @__PURE__ */ r(v, {
|
|
75
|
+
value: e,
|
|
76
|
+
children: [e, /* @__PURE__ */ n(y, {})]
|
|
77
|
+
}, e))
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
var _ = i(null);
|
|
81
|
+
function v({ className: t, value: r, ...i }) {
|
|
82
|
+
return /* @__PURE__ */ n(_.Provider, {
|
|
83
|
+
value: r,
|
|
84
|
+
children: /* @__PURE__ */ n("div", {
|
|
85
|
+
className: e("flex h-8 cursor-default items-center gap-1 rounded-small border border-outline-variant", "bg-surface-container-low pl-3 pr-1 text-label-large text-on-surface", t),
|
|
86
|
+
...i
|
|
87
|
+
})
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function y({ className: r, children: i, ...a }) {
|
|
91
|
+
let { remove: s, disabled: c } = f(), l = o(_);
|
|
92
|
+
if (l === null) throw Error("<ChipInput.ChipRemove> must be used within <ChipInput.Chip>");
|
|
93
|
+
return /* @__PURE__ */ n("button", {
|
|
94
|
+
type: "button",
|
|
95
|
+
"aria-label": `Remove ${l}`,
|
|
96
|
+
disabled: c,
|
|
97
|
+
onClick: () => s(l),
|
|
98
|
+
className: e("flex size-6 cursor-pointer items-center justify-center rounded-full", "text-on-surface-variant transition-colors hover:bg-on-surface/[0.08]", "focus-visible:outline-2 focus-visible:outline-primary", "disabled:cursor-not-allowed", r),
|
|
99
|
+
...a,
|
|
100
|
+
children: i ?? /* @__PURE__ */ n(t, {
|
|
101
|
+
"aria-hidden": !0,
|
|
102
|
+
className: "size-4"
|
|
103
|
+
})
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function b({ className: t, onKeyDown: r, onPaste: i, ...a }) {
|
|
107
|
+
let { value: o, add: s, remove: c, disabled: l, inputValue: u, setInputValue: d, inputId: p, inputRef: m } = f(), h = () => {
|
|
108
|
+
s(u), d("");
|
|
109
|
+
};
|
|
110
|
+
return /* @__PURE__ */ n("input", {
|
|
111
|
+
id: p,
|
|
112
|
+
ref: m,
|
|
113
|
+
type: "text",
|
|
114
|
+
value: u,
|
|
115
|
+
onChange: (e) => d(e.target.value),
|
|
116
|
+
onKeyDown: (e) => {
|
|
117
|
+
r?.(e), !e.defaultPrevented && (e.key === "Enter" || e.key === "," ? (e.preventDefault(), h()) : e.key === "Backspace" && u === "" && o.length > 0 && c(o[o.length - 1]));
|
|
118
|
+
},
|
|
119
|
+
onPaste: (e) => {
|
|
120
|
+
if (i?.(e), e.defaultPrevented) return;
|
|
121
|
+
let t = e.clipboardData.getData("text");
|
|
122
|
+
if (/[,\n]/.test(t)) {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
for (let e of `${u}${t}`.split(/[,\n]/)) s(e);
|
|
125
|
+
d("");
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
disabled: l,
|
|
129
|
+
className: e("h-12 min-w-12 flex-1 bg-transparent text-body-large text-on-surface", "outline-none placeholder:text-on-surface-variant", "disabled:cursor-not-allowed", t),
|
|
130
|
+
...a
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function x({ className: r, children: i, ...a }) {
|
|
134
|
+
let { value: o, inputValue: s, clear: c, disabled: l } = f();
|
|
135
|
+
return o.length === 0 && s === "" ? null : /* @__PURE__ */ n("button", {
|
|
136
|
+
type: "button",
|
|
137
|
+
"aria-label": "Clear",
|
|
138
|
+
disabled: l,
|
|
139
|
+
onClick: c,
|
|
140
|
+
className: e("flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full", "text-on-surface-variant transition-colors hover:bg-on-surface/[0.08]", "focus-visible:outline-2 focus-visible:outline-primary", "disabled:cursor-not-allowed", r),
|
|
141
|
+
...a,
|
|
142
|
+
children: i ?? /* @__PURE__ */ n(t, {
|
|
143
|
+
"aria-hidden": !0,
|
|
144
|
+
className: "size-4"
|
|
145
|
+
})
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
var S = {
|
|
149
|
+
Root: p,
|
|
150
|
+
Label: m,
|
|
151
|
+
Group: h,
|
|
152
|
+
Chips: g,
|
|
153
|
+
Chip: v,
|
|
154
|
+
ChipRemove: y,
|
|
155
|
+
Input: b,
|
|
156
|
+
Clear: x
|
|
157
|
+
};
|
|
158
|
+
//#endregion
|
|
159
|
+
export { v as Chip, S as ChipInput, y as ChipRemove, g as Chips, x as Clear, h as Group, b as Input, m as Label, p as Root, f as useChipInput };
|
|
160
|
+
|
|
161
|
+
//# sourceMappingURL=ChipInput.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChipInput.js","names":[],"sources":["../../../src/components/chip-input/ChipInput.tsx"],"sourcesContent":["import {\n createContext,\n useCallback,\n useContext,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { X } from 'lucide-react'\nimport { cn } from '#/lib/cn'\n\nimport type {\n ClipboardEvent,\n ComponentProps,\n KeyboardEvent,\n ReactNode,\n RefObject,\n} from 'react'\n\n/**\n * ChipInput — Material 3 input-chips field: free text typed into an outlined\n * text field is committed as removable M3 input chips (tags). No headless\n * primitive exists for this in Base UI, so behaviour is implemented here:\n * Enter or comma commits the trimmed text as a chip (duplicates ignored),\n * Backspace in an empty input removes the last chip, and pasted text is split\n * on commas/newlines into multiple chips. `name` renders one hidden input per\n * chip for native form submission.\n * Visuals match the library's existing chip language (Combobox multi-select):\n * outlined 56px field container (extra-small corner, outline → primary on\n * focus-within) wrapping 32px input chips (small corner, outline-variant\n * border, surface-container-low) with a trailing remove target.\n * M3 ref: https://m3.material.io/components/chips (input chips).\n *\n * Compound API:\n * <ChipInput.Root defaultValue={['Design']} name=\"tags\">\n * <ChipInput.Label>Tags</ChipInput.Label>\n * <ChipInput.Group>\n * <ChipInput.Chips />\n * <ChipInput.Input placeholder=\"Add a tag…\" />\n * <ChipInput.Clear />\n * </ChipInput.Group>\n * </ChipInput.Root>\n */\n\ntype ChipInputContextValue = {\n value: string[]\n add: (chip: string) => void\n remove: (chip: string) => void\n clear: () => void\n disabled: boolean\n inputValue: string\n setInputValue: (text: string) => void\n inputId: string\n inputRef: RefObject<HTMLInputElement | null>\n}\n\nconst ChipInputContext = createContext<ChipInputContextValue | null>(null)\n\nfunction useChipInput() {\n const ctx = useContext(ChipInputContext)\n if (!ctx) throw new Error('ChipInput parts must be used within <ChipInput.Root>')\n return ctx\n}\n\ntype RootProps = {\n children: ReactNode\n /** The committed chips — controlled. */\n value?: string[]\n /** The committed chips — uncontrolled initial. */\n defaultValue?: string[]\n onValueChange?: (value: string[]) => void\n /** Renders one hidden input per chip for native form submission. */\n name?: string\n disabled?: boolean\n}\n\nfunction Root({ children, value, defaultValue, onValueChange, name, disabled = false }: RootProps) {\n const [internalValue, setInternalValue] = useState<string[]>(defaultValue ?? [])\n const controlled = value !== undefined\n const currentValue = controlled ? value : internalValue\n\n const [inputValue, setInputValue] = useState('')\n const inputId = useId()\n const inputRef = useRef<HTMLInputElement>(null)\n\n const commitValue = useCallback(\n (next: string[]) => {\n if (!controlled) setInternalValue(next)\n onValueChange?.(next)\n },\n [controlled, onValueChange],\n )\n\n const add = useCallback(\n (chip: string) => {\n const trimmed = chip.trim()\n if (!trimmed || currentValue.includes(trimmed)) return\n commitValue([...currentValue, trimmed])\n },\n [currentValue, commitValue],\n )\n\n const remove = useCallback(\n (chip: string) => {\n commitValue(currentValue.filter((v) => v !== chip))\n },\n [currentValue, commitValue],\n )\n\n const clear = useCallback(() => {\n commitValue([])\n setInputValue('')\n inputRef.current?.focus()\n }, [commitValue])\n\n const ctx = useMemo<ChipInputContextValue>(\n () => ({\n value: currentValue,\n add,\n remove,\n clear,\n disabled,\n inputValue,\n setInputValue,\n inputId,\n inputRef,\n }),\n [currentValue, add, remove, clear, disabled, inputValue, inputId],\n )\n\n return (\n <ChipInputContext.Provider value={ctx}>\n {children}\n {name &&\n currentValue.map((chip) => (\n <input key={chip} type=\"hidden\" name={name} value={chip} disabled={disabled} />\n ))}\n </ChipInputContext.Provider>\n )\n}\n\n/** M3 label above the field (label-large, on-surface); focuses the input. */\nfunction Label({ className, ...props }: ComponentProps<'label'>) {\n const { inputId } = useChipInput()\n return (\n <label\n htmlFor={inputId}\n className={cn('mb-1.5 block text-label-large text-on-surface', className)}\n {...props}\n />\n )\n}\n\n/**\n * The M3 outlined text-field container: draws the 56px outlined box around the\n * chips and the input; turns primary on focus-within. Clicking the empty area\n * focuses the input.\n */\nfunction Group({ className, onClick, ...props }: ComponentProps<'div'>) {\n const { disabled, inputRef } = useChipInput()\n return (\n <div\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) inputRef.current?.focus()\n }}\n className={cn(\n 'flex min-h-14 w-full cursor-text flex-wrap items-center gap-1.5 py-1 pl-4 pr-2',\n 'rounded-small border border-outline bg-surface text-on-surface',\n 'transition-colors',\n 'focus-within:border-primary focus-within:outline-2 focus-within:-outline-offset-2 focus-within:outline-primary',\n disabled && 'pointer-events-none opacity-[0.38]',\n className,\n )}\n {...props}\n />\n )\n}\n\n/**\n * The committed chips. With no children renders each chip as\n * `<Chip>{chip}<ChipRemove /></Chip>`; pass a render prop to customise.\n */\nfunction Chips({\n className,\n children,\n ...props\n}: Omit<ComponentProps<'div'>, 'children'> & {\n children?: ((chip: string) => ReactNode) | ReactNode\n}) {\n const { value } = useChipInput()\n return (\n <div className={cn('contents', className)} {...props}>\n {typeof children === 'function'\n ? value.map((chip) => children(chip))\n : (children ??\n value.map((chip) => (\n <Chip key={chip} value={chip}>\n {chip}\n <ChipRemove />\n </Chip>\n )))}\n </div>\n )\n}\n\nconst ChipContext = createContext<string | null>(null)\n\n/** M3 input chip: 32px, small corner, outlined, surface-container-low. */\nfunction Chip({\n className,\n value,\n ...props\n}: ComponentProps<'div'> & {\n /** The committed value this chip represents (consumed by ChipRemove). */\n value: string\n}) {\n return (\n <ChipContext.Provider value={value}>\n <div\n className={cn(\n 'flex h-8 cursor-default items-center gap-1 rounded-small border border-outline-variant',\n 'bg-surface-container-low pl-3 pr-1 text-label-large text-on-surface',\n className,\n )}\n {...props}\n />\n </ChipContext.Provider>\n )\n}\n\n/** The chip's trailing remove target (M3 input-chip trailing icon). */\nfunction ChipRemove({ className, children, ...props }: ComponentProps<'button'>) {\n const { remove, disabled } = useChipInput()\n const chip = useContext(ChipContext)\n if (chip === null) throw new Error('<ChipInput.ChipRemove> must be used within <ChipInput.Chip>')\n return (\n <button\n type=\"button\"\n aria-label={`Remove ${chip}`}\n disabled={disabled}\n onClick={() => remove(chip)}\n className={cn(\n 'flex size-6 cursor-pointer items-center justify-center rounded-full',\n 'text-on-surface-variant transition-colors hover:bg-on-surface/[0.08]',\n 'focus-visible:outline-2 focus-visible:outline-primary',\n 'disabled:cursor-not-allowed',\n className,\n )}\n {...props}\n >\n {children ?? <X aria-hidden className=\"size-4\" />}\n </button>\n )\n}\n\n/**\n * Borderless by design — the surrounding Group draws the M3 field box.\n * Enter/comma commits the text as a chip; Backspace in an empty input removes\n * the last chip; pasted text splits on commas/newlines.\n */\nfunction Input({\n className,\n onKeyDown,\n onPaste,\n ...props\n}: Omit<ComponentProps<'input'>, 'value' | 'onChange'>) {\n const { value, add, remove, disabled, inputValue, setInputValue, inputId, inputRef } =\n useChipInput()\n\n const commit = () => {\n add(inputValue)\n setInputValue('')\n }\n\n const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n onKeyDown?.(event)\n if (event.defaultPrevented) return\n if (event.key === 'Enter' || event.key === ',') {\n event.preventDefault()\n commit()\n } else if (event.key === 'Backspace' && inputValue === '' && value.length > 0) {\n remove(value[value.length - 1])\n }\n }\n\n const handlePaste = (event: ClipboardEvent<HTMLInputElement>) => {\n onPaste?.(event)\n if (event.defaultPrevented) return\n const text = event.clipboardData.getData('text')\n if (!/[,\\n]/.test(text)) return\n event.preventDefault()\n for (const part of `${inputValue}${text}`.split(/[,\\n]/)) add(part)\n setInputValue('')\n }\n\n return (\n <input\n id={inputId}\n ref={inputRef}\n type=\"text\"\n value={inputValue}\n onChange={(event) => setInputValue(event.target.value)}\n onKeyDown={handleKeyDown}\n onPaste={handlePaste}\n disabled={disabled}\n className={cn(\n 'h-12 min-w-12 flex-1 bg-transparent text-body-large text-on-surface',\n 'outline-none placeholder:text-on-surface-variant',\n 'disabled:cursor-not-allowed',\n className,\n )}\n {...props}\n />\n )\n}\n\n/** Clears all chips and the pending text; only mounted while there is something to clear. */\nfunction Clear({ className, children, ...props }: ComponentProps<'button'>) {\n const { value, inputValue, clear, disabled } = useChipInput()\n if (value.length === 0 && inputValue === '') return null\n return (\n <button\n type=\"button\"\n aria-label=\"Clear\"\n disabled={disabled}\n onClick={clear}\n className={cn(\n 'flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full',\n 'text-on-surface-variant transition-colors hover:bg-on-surface/[0.08]',\n 'focus-visible:outline-2 focus-visible:outline-primary',\n 'disabled:cursor-not-allowed',\n className,\n )}\n {...props}\n >\n {children ?? <X aria-hidden className=\"size-4\" />}\n </button>\n )\n}\n\nexport const ChipInput = {\n Root,\n Label,\n Group,\n Chips,\n Chip,\n ChipRemove,\n Input,\n Clear,\n}\nexport { Root, Label, Group, Chips, Chip, ChipRemove, Input, Clear, useChipInput }\nexport type { RootProps as ChipInputRootProps }\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { Chip as e, ChipInput as t, ChipRemove as n, Chips as r, Clear as i, Group as a, Input as o, Label as s, Root as c, useChipInput as l } from "./ChipInput.js";
|
|
2
|
+
export { e as Chip, t as ChipInput, n as ChipRemove, r as Chips, i as Clear, a as Group, o as Input, s as Label, c as Root, l as useChipInput };
|
|
@@ -12,7 +12,7 @@ function f({ className: t, ...n }) {
|
|
|
12
12
|
}
|
|
13
13
|
function p({ className: t, ...n }) {
|
|
14
14
|
return /* @__PURE__ */ i(a.InputGroup, {
|
|
15
|
-
className: e("flex min-h-14 w-full flex-wrap items-center gap-1 py-1 pl-4 pr-2", "rounded-
|
|
15
|
+
className: e("flex min-h-14 w-full flex-wrap items-center gap-1 py-1 pl-4 pr-2", "rounded-small border border-outline bg-surface text-on-surface", "transition-colors", "focus-within:border-primary focus-within:outline-2 focus-within:-outline-offset-2 focus-within:outline-primary", "data-[disabled]:pointer-events-none data-[disabled]:opacity-[0.38]", t),
|
|
16
16
|
...n
|
|
17
17
|
});
|
|
18
18
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Combobox.js","names":[],"sources":["../../../src/components/combobox/Combobox.tsx"],"sourcesContent":["import { Combobox as BaseCombobox } from '@base-ui/react/combobox'\nimport { Check, ChevronDown, X } from 'lucide-react'\nimport { cn } from '#/lib/cn'\n\nimport type { ComponentProps } from 'react'\n\n/**\n * Combobox — Material 3 styled wrapper over Base UI's headless Combobox.\n * Behaviour/accessibility (filtering, listbox semantics, typeahead, chips for\n * multiple selection): Base UI (https://base-ui.com/react/components/combobox).\n * Visuals: M3 \"menu anchored to a text field\" anatomy — outlined text-field\n * container (56px, extra-small corner, outline → primary on focus), menu popup\n * on surface-container with small corner + level-2 elevation, 48px list items\n * with state layers, input chips for multi-select.\n * Design refs: port/core/ui/components/dropdown, port/core/ui/components/text-field\n * (see CLAUDE.md).\n *\n * Compound API mirrors Base UI:\n * <Combobox.Root items={items}>\n * <Combobox.Label>Fruit</Combobox.Label>\n * <Combobox.InputGroup>\n * <Combobox.Input placeholder=\"Search…\" />\n * <Combobox.Clear />\n * <Combobox.Trigger />\n * </Combobox.InputGroup>\n * <Combobox.Portal>\n * <Combobox.Positioner>\n * <Combobox.Popup>\n * <Combobox.Empty>No results.</Combobox.Empty>\n * <Combobox.List>\n * {(item) => (\n * <Combobox.Item key={item} value={item}>\n * {item}\n * <Combobox.ItemIndicator />\n * </Combobox.Item>\n * )}\n * </Combobox.List>\n * </Combobox.Popup>\n * </Combobox.Positioner>\n * </Combobox.Portal>\n * </Combobox.Root>\n */\n\n// These parts render no HTML element of their own (Root/Portal/Value/Collection),\n// so there is nothing to style — re-export them directly (preserves Root's generics).\nconst Root = BaseCombobox.Root\nconst Portal = BaseCombobox.Portal\nconst Value = BaseCombobox.Value\nconst Collection = BaseCombobox.Collection\n\n// Filtering hooks, re-exported for convenience.\nconst useFilter = BaseCombobox.useFilter\nconst useFilteredItems = BaseCombobox.useFilteredItems\n\nfunction Label({ className, ...props }: ComponentProps<typeof BaseCombobox.Label>) {\n return (\n <BaseCombobox.Label\n className={cn('mb-1.5 block text-label-large text-on-surface', className)}\n {...props}\n />\n )\n}\n\n/**\n * The M3 outlined text-field container: draws the 56px outlined box around the\n * input and its trailing controls; turns primary on focus-within.\n */\nfunction InputGroup({ className, ...props }: ComponentProps<typeof BaseCombobox.InputGroup>) {\n return (\n <BaseCombobox.InputGroup\n className={cn(\n 'flex min-h-14 w-full flex-wrap items-center gap-1 py-1 pl-4 pr-2',\n 'rounded-extra-small border border-outline bg-surface text-on-surface',\n 'transition-colors',\n 'focus-within:border-primary focus-within:outline-2 focus-within:-outline-offset-2 focus-within:outline-primary',\n 'data-[disabled]:pointer-events-none data-[disabled]:opacity-[0.38]',\n className,\n )}\n {...props}\n />\n )\n}\n\n/** Borderless by design — the surrounding InputGroup draws the M3 field box. */\nfunction Input({ className, ...props }: ComponentProps<typeof BaseCombobox.Input>) {\n return (\n <BaseCombobox.Input\n className={cn(\n 'h-12 min-w-12 flex-1 bg-transparent text-body-large text-on-surface',\n 'outline-none placeholder:text-on-surface-variant',\n 'disabled:cursor-not-allowed',\n className,\n )}\n {...props}\n />\n )\n}\n\nfunction Trigger({ className, children, ...props }: ComponentProps<typeof BaseCombobox.Trigger>) {\n return (\n <BaseCombobox.Trigger\n className={cn(\n 'group flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full',\n 'text-on-surface-variant transition-colors hover:bg-on-surface/[0.08]',\n 'focus-visible:outline-2 focus-visible:outline-primary',\n 'disabled:cursor-not-allowed disabled:opacity-[0.38]',\n className,\n )}\n {...props}\n >\n {children ?? (\n <Icon>\n <ChevronDown aria-hidden className=\"size-5\" />\n </Icon>\n )}\n </BaseCombobox.Trigger>\n )\n}\n\n/** Chevron wrapper; flips while the popup is open (parent Trigger is `group`). */\nfunction Icon({ className, ...props }: ComponentProps<typeof BaseCombobox.Icon>) {\n return (\n <BaseCombobox.Icon\n className={cn(\n 'flex transition-transform duration-200 ease-out group-data-[popup-open]:rotate-180',\n className,\n )}\n {...props}\n />\n )\n}\n\n/** Clears the value; only mounted while there is something to clear. */\nfunction Clear({ className, children, ...props }: ComponentProps<typeof BaseCombobox.Clear>) {\n return (\n <BaseCombobox.Clear\n className={cn(\n 'flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full',\n 'text-on-surface-variant transition-opacity hover:bg-on-surface/[0.08]',\n 'focus-visible:outline-2 focus-visible:outline-primary',\n 'data-[ending-style]:opacity-0 data-[starting-style]:opacity-0',\n 'disabled:cursor-not-allowed disabled:opacity-[0.38]',\n className,\n )}\n {...props}\n >\n {children ?? <X aria-hidden className=\"size-4\" />}\n </BaseCombobox.Clear>\n )\n}\n\nfunction Backdrop({ className, ...props }: ComponentProps<typeof BaseCombobox.Backdrop>) {\n return <BaseCombobox.Backdrop className={cn('fixed inset-0 z-40', className)} {...props} />\n}\n\nfunction Positioner({ className, ...props }: ComponentProps<typeof BaseCombobox.Positioner>) {\n return (\n <BaseCombobox.Positioner\n sideOffset={4}\n className={cn('z-50 outline-none', className)}\n {...props}\n />\n )\n}\n\n/** M3 menu container: surface-container, small corner, level-2 elevation. */\nfunction Popup({ className, ...props }: ComponentProps<typeof BaseCombobox.Popup>) {\n return (\n <BaseCombobox.Popup\n className={cn(\n 'max-h-[min(24rem,var(--available-height))] w-(--anchor-width) overflow-y-auto overscroll-contain',\n 'rounded-small bg-surface-container py-2 text-on-surface shadow-mm-2',\n 'origin-(--transform-origin) transition-[transform,opacity] duration-150 ease-out',\n 'outline-none data-[ending-style]:scale-95 data-[ending-style]:opacity-0',\n 'data-[starting-style]:scale-95 data-[starting-style]:opacity-0',\n className,\n )}\n {...props}\n />\n )\n}\n\nfunction Arrow({ className, ...props }: ComponentProps<typeof BaseCombobox.Arrow>) {\n return <BaseCombobox.Arrow className={cn('flex', className)} {...props} />\n}\n\nfunction List({ className, ...props }: ComponentProps<typeof BaseCombobox.List>) {\n return <BaseCombobox.List className={cn('outline-none', className)} {...props} />\n}\n\n/** Polite screen-reader status region (e.g. async loading); stays mounted. */\nfunction Status({ className, ...props }: ComponentProps<typeof BaseCombobox.Status>) {\n return (\n <BaseCombobox.Status\n className={cn('px-4 py-2 text-body-small text-on-surface-variant empty:p-0', className)}\n {...props}\n />\n )\n}\n\n/** Shown only when the filtered list is empty; collapses otherwise. */\nfunction Empty({ className, ...props }: ComponentProps<typeof BaseCombobox.Empty>) {\n return (\n <BaseCombobox.Empty\n className={cn('px-4 py-3 text-body-medium text-on-surface-variant empty:p-0', className)}\n {...props}\n />\n )\n}\n\nfunction Group({ className, ...props }: ComponentProps<typeof BaseCombobox.Group>) {\n return <BaseCombobox.Group className={cn(className)} {...props} />\n}\n\n/** M3 list subheader. */\nfunction GroupLabel({ className, ...props }: ComponentProps<typeof BaseCombobox.GroupLabel>) {\n return (\n <BaseCombobox.GroupLabel\n className={cn('px-4 pb-1 pt-3 text-label-medium text-on-surface-variant', className)}\n {...props}\n />\n )\n}\n\n/** M3 menu item: 48px row, label-large, hover/highlight state layers. */\nfunction Item({ className, ...props }: ComponentProps<typeof BaseCombobox.Item>) {\n return (\n <BaseCombobox.Item\n className={cn(\n 'flex min-h-12 cursor-pointer select-none items-center gap-3 px-4',\n 'text-label-large text-on-surface outline-none transition-colors',\n 'data-[highlighted]:bg-on-surface/[0.08]',\n 'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-[0.38]',\n className,\n )}\n {...props}\n />\n )\n}\n\n/** Trailing selected check (primary role); only mounted while selected. */\nfunction ItemIndicator({\n className,\n children,\n ...props\n}: ComponentProps<typeof BaseCombobox.ItemIndicator>) {\n return (\n <BaseCombobox.ItemIndicator\n className={cn('ml-auto flex shrink-0 items-center text-primary', className)}\n {...props}\n >\n {children ?? <Check aria-hidden className=\"size-5\" />}\n </BaseCombobox.ItemIndicator>\n )\n}\n\n/** Wraps selected-value chips and the inline input in multiple mode. */\nfunction Chips({ className, ...props }: ComponentProps<typeof BaseCombobox.Chips>) {\n return (\n <BaseCombobox.Chips\n className={cn('flex flex-1 flex-wrap items-center gap-1.5', className)}\n {...props}\n />\n )\n}\n\n/** M3 input chip: 32px, small corner, outlined. */\nfunction Chip({ className, ...props }: ComponentProps<typeof BaseCombobox.Chip>) {\n return (\n <BaseCombobox.Chip\n className={cn(\n 'flex h-8 cursor-default items-center gap-1 rounded-small border border-outline-variant',\n 'bg-surface-container-low pl-3 pr-1 text-label-large text-on-surface',\n 'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-[0.38]',\n className,\n )}\n {...props}\n />\n )\n}\n\nfunction ChipRemove({\n className,\n children,\n ...props\n}: ComponentProps<typeof BaseCombobox.ChipRemove>) {\n return (\n <BaseCombobox.ChipRemove\n className={cn(\n 'flex size-6 cursor-pointer items-center justify-center rounded-full',\n 'text-on-surface-variant transition-colors hover:bg-on-surface/[0.08]',\n 'focus-visible:outline-2 focus-visible:outline-primary',\n className,\n )}\n {...props}\n >\n {children ?? <X aria-hidden className=\"size-4\" />}\n </BaseCombobox.ChipRemove>\n )\n}\n\n/** Single row of items when the listbox is in grid mode. */\nfunction Row({ className, ...props }: ComponentProps<typeof BaseCombobox.Row>) {\n return <BaseCombobox.Row className={cn('flex', className)} {...props} />\n}\n\nfunction Separator({ className, ...props }: ComponentProps<typeof BaseCombobox.Separator>) {\n return (\n <BaseCombobox.Separator\n className={cn('mx-4 my-2 h-px bg-outline-variant', className)}\n {...props}\n />\n )\n}\n\nexport const Combobox = {\n Root,\n Label,\n Value,\n Input,\n InputGroup,\n Trigger,\n List,\n Status,\n Portal,\n Backdrop,\n Positioner,\n Popup,\n Arrow,\n Icon,\n Group,\n GroupLabel,\n Item,\n ItemIndicator,\n Chips,\n Chip,\n ChipRemove,\n Row,\n Collection,\n Empty,\n Clear,\n Separator,\n useFilter,\n useFilteredItems,\n}\nexport {\n Root,\n Label,\n Value,\n Input,\n InputGroup,\n Trigger,\n List,\n Status,\n Portal,\n Backdrop,\n Positioner,\n Popup,\n Arrow,\n Icon,\n Group,\n GroupLabel,\n Item,\n ItemIndicator,\n Chips,\n Chip,\n ChipRemove,\n Row,\n Collection,\n Empty,\n Clear,\n Separator,\n useFilter,\n useFilteredItems,\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Combobox.js","names":[],"sources":["../../../src/components/combobox/Combobox.tsx"],"sourcesContent":["import { Combobox as BaseCombobox } from '@base-ui/react/combobox'\nimport { Check, ChevronDown, X } from 'lucide-react'\nimport { cn } from '#/lib/cn'\n\nimport type { ComponentProps } from 'react'\n\n/**\n * Combobox — Material 3 styled wrapper over Base UI's headless Combobox.\n * Behaviour/accessibility (filtering, listbox semantics, typeahead, chips for\n * multiple selection): Base UI (https://base-ui.com/react/components/combobox).\n * Visuals: M3 \"menu anchored to a text field\" anatomy — outlined text-field\n * container (56px, extra-small corner, outline → primary on focus), menu popup\n * on surface-container with small corner + level-2 elevation, 48px list items\n * with state layers, input chips for multi-select.\n * Design refs: port/core/ui/components/dropdown, port/core/ui/components/text-field\n * (see CLAUDE.md).\n *\n * Compound API mirrors Base UI:\n * <Combobox.Root items={items}>\n * <Combobox.Label>Fruit</Combobox.Label>\n * <Combobox.InputGroup>\n * <Combobox.Input placeholder=\"Search…\" />\n * <Combobox.Clear />\n * <Combobox.Trigger />\n * </Combobox.InputGroup>\n * <Combobox.Portal>\n * <Combobox.Positioner>\n * <Combobox.Popup>\n * <Combobox.Empty>No results.</Combobox.Empty>\n * <Combobox.List>\n * {(item) => (\n * <Combobox.Item key={item} value={item}>\n * {item}\n * <Combobox.ItemIndicator />\n * </Combobox.Item>\n * )}\n * </Combobox.List>\n * </Combobox.Popup>\n * </Combobox.Positioner>\n * </Combobox.Portal>\n * </Combobox.Root>\n */\n\n// These parts render no HTML element of their own (Root/Portal/Value/Collection),\n// so there is nothing to style — re-export them directly (preserves Root's generics).\nconst Root = BaseCombobox.Root\nconst Portal = BaseCombobox.Portal\nconst Value = BaseCombobox.Value\nconst Collection = BaseCombobox.Collection\n\n// Filtering hooks, re-exported for convenience.\nconst useFilter = BaseCombobox.useFilter\nconst useFilteredItems = BaseCombobox.useFilteredItems\n\nfunction Label({ className, ...props }: ComponentProps<typeof BaseCombobox.Label>) {\n return (\n <BaseCombobox.Label\n className={cn('mb-1.5 block text-label-large text-on-surface', className)}\n {...props}\n />\n )\n}\n\n/**\n * The M3 outlined text-field container: draws the 56px outlined box around the\n * input and its trailing controls; turns primary on focus-within.\n */\nfunction InputGroup({ className, ...props }: ComponentProps<typeof BaseCombobox.InputGroup>) {\n return (\n <BaseCombobox.InputGroup\n className={cn(\n 'flex min-h-14 w-full flex-wrap items-center gap-1 py-1 pl-4 pr-2',\n 'rounded-small border border-outline bg-surface text-on-surface',\n 'transition-colors',\n 'focus-within:border-primary focus-within:outline-2 focus-within:-outline-offset-2 focus-within:outline-primary',\n 'data-[disabled]:pointer-events-none data-[disabled]:opacity-[0.38]',\n className,\n )}\n {...props}\n />\n )\n}\n\n/** Borderless by design — the surrounding InputGroup draws the M3 field box. */\nfunction Input({ className, ...props }: ComponentProps<typeof BaseCombobox.Input>) {\n return (\n <BaseCombobox.Input\n className={cn(\n 'h-12 min-w-12 flex-1 bg-transparent text-body-large text-on-surface',\n 'outline-none placeholder:text-on-surface-variant',\n 'disabled:cursor-not-allowed',\n className,\n )}\n {...props}\n />\n )\n}\n\nfunction Trigger({ className, children, ...props }: ComponentProps<typeof BaseCombobox.Trigger>) {\n return (\n <BaseCombobox.Trigger\n className={cn(\n 'group flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full',\n 'text-on-surface-variant transition-colors hover:bg-on-surface/[0.08]',\n 'focus-visible:outline-2 focus-visible:outline-primary',\n 'disabled:cursor-not-allowed disabled:opacity-[0.38]',\n className,\n )}\n {...props}\n >\n {children ?? (\n <Icon>\n <ChevronDown aria-hidden className=\"size-5\" />\n </Icon>\n )}\n </BaseCombobox.Trigger>\n )\n}\n\n/** Chevron wrapper; flips while the popup is open (parent Trigger is `group`). */\nfunction Icon({ className, ...props }: ComponentProps<typeof BaseCombobox.Icon>) {\n return (\n <BaseCombobox.Icon\n className={cn(\n 'flex transition-transform duration-200 ease-out group-data-[popup-open]:rotate-180',\n className,\n )}\n {...props}\n />\n )\n}\n\n/** Clears the value; only mounted while there is something to clear. */\nfunction Clear({ className, children, ...props }: ComponentProps<typeof BaseCombobox.Clear>) {\n return (\n <BaseCombobox.Clear\n className={cn(\n 'flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full',\n 'text-on-surface-variant transition-opacity hover:bg-on-surface/[0.08]',\n 'focus-visible:outline-2 focus-visible:outline-primary',\n 'data-[ending-style]:opacity-0 data-[starting-style]:opacity-0',\n 'disabled:cursor-not-allowed disabled:opacity-[0.38]',\n className,\n )}\n {...props}\n >\n {children ?? <X aria-hidden className=\"size-4\" />}\n </BaseCombobox.Clear>\n )\n}\n\nfunction Backdrop({ className, ...props }: ComponentProps<typeof BaseCombobox.Backdrop>) {\n return <BaseCombobox.Backdrop className={cn('fixed inset-0 z-40', className)} {...props} />\n}\n\nfunction Positioner({ className, ...props }: ComponentProps<typeof BaseCombobox.Positioner>) {\n return (\n <BaseCombobox.Positioner\n sideOffset={4}\n className={cn('z-50 outline-none', className)}\n {...props}\n />\n )\n}\n\n/** M3 menu container: surface-container, small corner, level-2 elevation. */\nfunction Popup({ className, ...props }: ComponentProps<typeof BaseCombobox.Popup>) {\n return (\n <BaseCombobox.Popup\n className={cn(\n 'max-h-[min(24rem,var(--available-height))] w-(--anchor-width) overflow-y-auto overscroll-contain',\n 'rounded-small bg-surface-container py-2 text-on-surface shadow-mm-2',\n 'origin-(--transform-origin) transition-[transform,opacity] duration-150 ease-out',\n 'outline-none data-[ending-style]:scale-95 data-[ending-style]:opacity-0',\n 'data-[starting-style]:scale-95 data-[starting-style]:opacity-0',\n className,\n )}\n {...props}\n />\n )\n}\n\nfunction Arrow({ className, ...props }: ComponentProps<typeof BaseCombobox.Arrow>) {\n return <BaseCombobox.Arrow className={cn('flex', className)} {...props} />\n}\n\nfunction List({ className, ...props }: ComponentProps<typeof BaseCombobox.List>) {\n return <BaseCombobox.List className={cn('outline-none', className)} {...props} />\n}\n\n/** Polite screen-reader status region (e.g. async loading); stays mounted. */\nfunction Status({ className, ...props }: ComponentProps<typeof BaseCombobox.Status>) {\n return (\n <BaseCombobox.Status\n className={cn('px-4 py-2 text-body-small text-on-surface-variant empty:p-0', className)}\n {...props}\n />\n )\n}\n\n/** Shown only when the filtered list is empty; collapses otherwise. */\nfunction Empty({ className, ...props }: ComponentProps<typeof BaseCombobox.Empty>) {\n return (\n <BaseCombobox.Empty\n className={cn('px-4 py-3 text-body-medium text-on-surface-variant empty:p-0', className)}\n {...props}\n />\n )\n}\n\nfunction Group({ className, ...props }: ComponentProps<typeof BaseCombobox.Group>) {\n return <BaseCombobox.Group className={cn(className)} {...props} />\n}\n\n/** M3 list subheader. */\nfunction GroupLabel({ className, ...props }: ComponentProps<typeof BaseCombobox.GroupLabel>) {\n return (\n <BaseCombobox.GroupLabel\n className={cn('px-4 pb-1 pt-3 text-label-medium text-on-surface-variant', className)}\n {...props}\n />\n )\n}\n\n/** M3 menu item: 48px row, label-large, hover/highlight state layers. */\nfunction Item({ className, ...props }: ComponentProps<typeof BaseCombobox.Item>) {\n return (\n <BaseCombobox.Item\n className={cn(\n 'flex min-h-12 cursor-pointer select-none items-center gap-3 px-4',\n 'text-label-large text-on-surface outline-none transition-colors',\n 'data-[highlighted]:bg-on-surface/[0.08]',\n 'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-[0.38]',\n className,\n )}\n {...props}\n />\n )\n}\n\n/** Trailing selected check (primary role); only mounted while selected. */\nfunction ItemIndicator({\n className,\n children,\n ...props\n}: ComponentProps<typeof BaseCombobox.ItemIndicator>) {\n return (\n <BaseCombobox.ItemIndicator\n className={cn('ml-auto flex shrink-0 items-center text-primary', className)}\n {...props}\n >\n {children ?? <Check aria-hidden className=\"size-5\" />}\n </BaseCombobox.ItemIndicator>\n )\n}\n\n/** Wraps selected-value chips and the inline input in multiple mode. */\nfunction Chips({ className, ...props }: ComponentProps<typeof BaseCombobox.Chips>) {\n return (\n <BaseCombobox.Chips\n className={cn('flex flex-1 flex-wrap items-center gap-1.5', className)}\n {...props}\n />\n )\n}\n\n/** M3 input chip: 32px, small corner, outlined. */\nfunction Chip({ className, ...props }: ComponentProps<typeof BaseCombobox.Chip>) {\n return (\n <BaseCombobox.Chip\n className={cn(\n 'flex h-8 cursor-default items-center gap-1 rounded-small border border-outline-variant',\n 'bg-surface-container-low pl-3 pr-1 text-label-large text-on-surface',\n 'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-[0.38]',\n className,\n )}\n {...props}\n />\n )\n}\n\nfunction ChipRemove({\n className,\n children,\n ...props\n}: ComponentProps<typeof BaseCombobox.ChipRemove>) {\n return (\n <BaseCombobox.ChipRemove\n className={cn(\n 'flex size-6 cursor-pointer items-center justify-center rounded-full',\n 'text-on-surface-variant transition-colors hover:bg-on-surface/[0.08]',\n 'focus-visible:outline-2 focus-visible:outline-primary',\n className,\n )}\n {...props}\n >\n {children ?? <X aria-hidden className=\"size-4\" />}\n </BaseCombobox.ChipRemove>\n )\n}\n\n/** Single row of items when the listbox is in grid mode. */\nfunction Row({ className, ...props }: ComponentProps<typeof BaseCombobox.Row>) {\n return <BaseCombobox.Row className={cn('flex', className)} {...props} />\n}\n\nfunction Separator({ className, ...props }: ComponentProps<typeof BaseCombobox.Separator>) {\n return (\n <BaseCombobox.Separator\n className={cn('mx-4 my-2 h-px bg-outline-variant', className)}\n {...props}\n />\n )\n}\n\nexport const Combobox = {\n Root,\n Label,\n Value,\n Input,\n InputGroup,\n Trigger,\n List,\n Status,\n Portal,\n Backdrop,\n Positioner,\n Popup,\n Arrow,\n Icon,\n Group,\n GroupLabel,\n Item,\n ItemIndicator,\n Chips,\n Chip,\n ChipRemove,\n Row,\n Collection,\n Empty,\n Clear,\n Separator,\n useFilter,\n useFilteredItems,\n}\nexport {\n Root,\n Label,\n Value,\n Input,\n InputGroup,\n Trigger,\n List,\n Status,\n Portal,\n Backdrop,\n Positioner,\n Popup,\n Arrow,\n Icon,\n Group,\n GroupLabel,\n Item,\n ItemIndicator,\n Chips,\n Chip,\n ChipRemove,\n Row,\n Collection,\n Empty,\n Clear,\n Separator,\n useFilter,\n useFilteredItems,\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Select as BaseSelect, SelectRootProps } from '@base-ui/react/select';
|
|
2
|
+
import { Label, Trigger, Value, Icon, Portal, Backdrop, Positioner, Popup, Arrow, List, ItemText, ScrollUpArrow, ScrollDownArrow, Group, GroupLabel, Separator } from '../../../components/select';
|
|
3
|
+
import { ComponentProps } from 'react';
|
|
4
|
+
/**
|
|
5
|
+
* MultiSelect — Material 3 multi-select exposed dropdown, built on Base UI's
|
|
6
|
+
* headless Select in `multiple` mode.
|
|
7
|
+
* Behaviour/accessibility (multiselectable listbox semantics, typeahead,
|
|
8
|
+
* keyboard navigation, form integration): Base UI
|
|
9
|
+
* (https://base-ui.com/react/components/select).
|
|
10
|
+
* Visuals: the same M3 "exposed dropdown menu" anatomy as `Select` — the field
|
|
11
|
+
* trigger, popup, and list parts are reused from `#/components/select` so the
|
|
12
|
+
* two stay in sync. The deltas are the M3 multi-select menu conventions:
|
|
13
|
+
* items carry a LEADING checkbox (mirroring `#/components/checkbox` visuals)
|
|
14
|
+
* instead of a trailing check + selected fill, the popup stays open across
|
|
15
|
+
* selections (Base UI `multiple` behaviour), and the trigger's Value shows the
|
|
16
|
+
* selected labels as a comma-separated list (Base UI's `multiple` default).
|
|
17
|
+
*
|
|
18
|
+
* Compound API mirrors Select (Root is always `multiple`):
|
|
19
|
+
* <MultiSelect.Root defaultValue={['Apple']}>
|
|
20
|
+
* <MultiSelect.Label>Fruits</MultiSelect.Label>
|
|
21
|
+
* <MultiSelect.Trigger>
|
|
22
|
+
* <MultiSelect.Value placeholder="Pick fruits" />
|
|
23
|
+
* <MultiSelect.Icon />
|
|
24
|
+
* </MultiSelect.Trigger>
|
|
25
|
+
* <MultiSelect.Portal>
|
|
26
|
+
* <MultiSelect.Positioner>
|
|
27
|
+
* <MultiSelect.Popup>
|
|
28
|
+
* <MultiSelect.List>
|
|
29
|
+
* <MultiSelect.Item value="Apple">
|
|
30
|
+
* <MultiSelect.ItemCheckbox />
|
|
31
|
+
* <MultiSelect.ItemText>Apple</MultiSelect.ItemText>
|
|
32
|
+
* </MultiSelect.Item>
|
|
33
|
+
* </MultiSelect.List>
|
|
34
|
+
* </MultiSelect.Popup>
|
|
35
|
+
* </MultiSelect.Positioner>
|
|
36
|
+
* </MultiSelect.Portal>
|
|
37
|
+
* </MultiSelect.Root>
|
|
38
|
+
*/
|
|
39
|
+
type RootProps<TValue> = Omit<SelectRootProps<TValue, true>, 'multiple'>;
|
|
40
|
+
/** Base UI Select.Root locked to `multiple` (value/onValueChange take arrays). */
|
|
41
|
+
declare function Root<TValue>(props: RootProps<TValue>): import("react").JSX.Element;
|
|
42
|
+
/**
|
|
43
|
+
* M3 multi-select menu item: 48px row with a leading checkbox. Unlike the
|
|
44
|
+
* single Select's item there is no selected container fill — the checkbox
|
|
45
|
+
* carries the selected state.
|
|
46
|
+
*/
|
|
47
|
+
declare function Item({ className, ...props }: ComponentProps<typeof BaseSelect.Item>): import("react").JSX.Element;
|
|
48
|
+
/**
|
|
49
|
+
* Decorative leading checkbox reflecting the item's selected state (mirrors
|
|
50
|
+
* `#/components/checkbox` visuals: 20px box, 6px corner,
|
|
51
|
+
* surface-container-highest → inverse-primary with a white check). The item
|
|
52
|
+
* itself is the option; this is purely visual, hence aria-hidden.
|
|
53
|
+
*/
|
|
54
|
+
declare function ItemCheckbox({ className, children, ...props }: ComponentProps<'span'>): import("react").JSX.Element;
|
|
55
|
+
export declare const MultiSelect: {
|
|
56
|
+
Root: typeof Root;
|
|
57
|
+
Label: typeof Label;
|
|
58
|
+
Trigger: typeof Trigger;
|
|
59
|
+
Value: typeof Value;
|
|
60
|
+
Icon: typeof Icon;
|
|
61
|
+
Portal: import('react').ForwardRefExoticComponent<Omit<import('@base-ui/react').SelectPortalProps, "ref"> & import('react').RefAttributes<HTMLDivElement>>;
|
|
62
|
+
Backdrop: typeof Backdrop;
|
|
63
|
+
Positioner: typeof Positioner;
|
|
64
|
+
Popup: typeof Popup;
|
|
65
|
+
Arrow: typeof Arrow;
|
|
66
|
+
List: typeof List;
|
|
67
|
+
Item: typeof Item;
|
|
68
|
+
ItemCheckbox: typeof ItemCheckbox;
|
|
69
|
+
ItemText: typeof ItemText;
|
|
70
|
+
ScrollUpArrow: typeof ScrollUpArrow;
|
|
71
|
+
ScrollDownArrow: typeof ScrollDownArrow;
|
|
72
|
+
Group: typeof Group;
|
|
73
|
+
GroupLabel: typeof GroupLabel;
|
|
74
|
+
Separator: typeof Separator;
|
|
75
|
+
};
|
|
76
|
+
export { Root, Label, Trigger, Value, Icon, Portal, Backdrop, Positioner, Popup, Arrow, List, Item, ItemCheckbox, ItemText, ScrollUpArrow, ScrollDownArrow, Group, GroupLabel, Separator, };
|
|
77
|
+
export type { RootProps as MultiSelectRootProps };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { cn as e } from "../../lib/cn.js";
|
|
2
|
+
import { Arrow as t, Backdrop as n, Group as r, GroupLabel as i, Icon as a, ItemText as o, Label as s, List as c, Popup as l, Portal as u, Positioner as d, ScrollDownArrow as f, ScrollUpArrow as p, Separator as m, Trigger as h, Value as g } from "../select/Select.js";
|
|
3
|
+
import { Check as _ } from "lucide-react";
|
|
4
|
+
import { jsx as v } from "react/jsx-runtime";
|
|
5
|
+
import { Select as y } from "@base-ui/react/select";
|
|
6
|
+
//#region src/components/multi-select/MultiSelect.tsx
|
|
7
|
+
function b(e) {
|
|
8
|
+
return /* @__PURE__ */ v(y.Root, {
|
|
9
|
+
multiple: !0,
|
|
10
|
+
...e
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
function x({ className: t, ...n }) {
|
|
14
|
+
return /* @__PURE__ */ v(y.Item, {
|
|
15
|
+
className: e("group/item flex min-h-12 cursor-pointer select-none items-center gap-3 px-4", "text-label-large text-on-surface outline-none transition-colors", "data-[highlighted]:bg-on-surface/[0.08]", "data-[disabled]:cursor-not-allowed data-[disabled]:opacity-[0.38]", t),
|
|
16
|
+
...n
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function S({ className: t, children: n, ...r }) {
|
|
20
|
+
return /* @__PURE__ */ v("span", {
|
|
21
|
+
"aria-hidden": !0,
|
|
22
|
+
className: e("flex size-5 shrink-0 items-center justify-center rounded-[6px]", "bg-surface-container-highest transition-colors duration-150", "group-data-[selected]/item:bg-inverse-primary", t),
|
|
23
|
+
...r,
|
|
24
|
+
children: n ?? /* @__PURE__ */ v(_, {
|
|
25
|
+
"aria-hidden": !0,
|
|
26
|
+
className: e("size-3.5 scale-50 text-white opacity-0 transition-all duration-150 ease-out", "group-data-[selected]/item:scale-100 group-data-[selected]/item:opacity-100")
|
|
27
|
+
})
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
var C = {
|
|
31
|
+
Root: b,
|
|
32
|
+
Label: s,
|
|
33
|
+
Trigger: h,
|
|
34
|
+
Value: g,
|
|
35
|
+
Icon: a,
|
|
36
|
+
Portal: u,
|
|
37
|
+
Backdrop: n,
|
|
38
|
+
Positioner: d,
|
|
39
|
+
Popup: l,
|
|
40
|
+
Arrow: t,
|
|
41
|
+
List: c,
|
|
42
|
+
Item: x,
|
|
43
|
+
ItemCheckbox: S,
|
|
44
|
+
ItemText: o,
|
|
45
|
+
ScrollUpArrow: p,
|
|
46
|
+
ScrollDownArrow: f,
|
|
47
|
+
Group: r,
|
|
48
|
+
GroupLabel: i,
|
|
49
|
+
Separator: m
|
|
50
|
+
};
|
|
51
|
+
//#endregion
|
|
52
|
+
export { x as Item, S as ItemCheckbox, C as MultiSelect, b as Root };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=MultiSelect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MultiSelect.js","names":[],"sources":["../../../src/components/multi-select/MultiSelect.tsx"],"sourcesContent":["import { Select as BaseSelect } from '@base-ui/react/select'\nimport { Check } from 'lucide-react'\nimport { cn } from '#/lib/cn'\n// Everything but Root/Item is identical to the single Select — reuse its\n// styled parts so the two stay in sync.\nimport {\n Label,\n Trigger,\n Value,\n Icon,\n Portal,\n Backdrop,\n Positioner,\n Popup,\n Arrow,\n List,\n ItemText,\n ScrollUpArrow,\n ScrollDownArrow,\n Group,\n GroupLabel,\n Separator,\n} from '#/components/select'\n\nimport type { ComponentProps } from 'react'\nimport type { SelectRootProps } from '@base-ui/react/select'\n\n/**\n * MultiSelect — Material 3 multi-select exposed dropdown, built on Base UI's\n * headless Select in `multiple` mode.\n * Behaviour/accessibility (multiselectable listbox semantics, typeahead,\n * keyboard navigation, form integration): Base UI\n * (https://base-ui.com/react/components/select).\n * Visuals: the same M3 \"exposed dropdown menu\" anatomy as `Select` — the field\n * trigger, popup, and list parts are reused from `#/components/select` so the\n * two stay in sync. The deltas are the M3 multi-select menu conventions:\n * items carry a LEADING checkbox (mirroring `#/components/checkbox` visuals)\n * instead of a trailing check + selected fill, the popup stays open across\n * selections (Base UI `multiple` behaviour), and the trigger's Value shows the\n * selected labels as a comma-separated list (Base UI's `multiple` default).\n *\n * Compound API mirrors Select (Root is always `multiple`):\n * <MultiSelect.Root defaultValue={['Apple']}>\n * <MultiSelect.Label>Fruits</MultiSelect.Label>\n * <MultiSelect.Trigger>\n * <MultiSelect.Value placeholder=\"Pick fruits\" />\n * <MultiSelect.Icon />\n * </MultiSelect.Trigger>\n * <MultiSelect.Portal>\n * <MultiSelect.Positioner>\n * <MultiSelect.Popup>\n * <MultiSelect.List>\n * <MultiSelect.Item value=\"Apple\">\n * <MultiSelect.ItemCheckbox />\n * <MultiSelect.ItemText>Apple</MultiSelect.ItemText>\n * </MultiSelect.Item>\n * </MultiSelect.List>\n * </MultiSelect.Popup>\n * </MultiSelect.Positioner>\n * </MultiSelect.Portal>\n * </MultiSelect.Root>\n */\n\ntype RootProps<TValue> = Omit<SelectRootProps<TValue, true>, 'multiple'>\n\n/** Base UI Select.Root locked to `multiple` (value/onValueChange take arrays). */\nfunction Root<TValue>(props: RootProps<TValue>) {\n return <BaseSelect.Root multiple {...props} />\n}\n\n/**\n * M3 multi-select menu item: 48px row with a leading checkbox. Unlike the\n * single Select's item there is no selected container fill — the checkbox\n * carries the selected state.\n */\nfunction Item({ className, ...props }: ComponentProps<typeof BaseSelect.Item>) {\n return (\n <BaseSelect.Item\n className={cn(\n 'group/item flex min-h-12 cursor-pointer select-none items-center gap-3 px-4',\n 'text-label-large text-on-surface outline-none transition-colors',\n 'data-[highlighted]:bg-on-surface/[0.08]',\n 'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-[0.38]',\n className,\n )}\n {...props}\n />\n )\n}\n\n/**\n * Decorative leading checkbox reflecting the item's selected state (mirrors\n * `#/components/checkbox` visuals: 20px box, 6px corner,\n * surface-container-highest → inverse-primary with a white check). The item\n * itself is the option; this is purely visual, hence aria-hidden.\n */\nfunction ItemCheckbox({ className, children, ...props }: ComponentProps<'span'>) {\n return (\n <span\n aria-hidden\n className={cn(\n 'flex size-5 shrink-0 items-center justify-center rounded-[6px]',\n 'bg-surface-container-highest transition-colors duration-150',\n 'group-data-[selected]/item:bg-inverse-primary',\n className,\n )}\n {...props}\n >\n {children ?? (\n <Check\n aria-hidden\n className={cn(\n 'size-3.5 scale-50 text-white opacity-0 transition-all duration-150 ease-out',\n 'group-data-[selected]/item:scale-100 group-data-[selected]/item:opacity-100',\n )}\n />\n )}\n </span>\n )\n}\n\nexport const MultiSelect = {\n Root,\n Label,\n Trigger,\n Value,\n Icon,\n Portal,\n Backdrop,\n Positioner,\n Popup,\n Arrow,\n List,\n Item,\n ItemCheckbox,\n ItemText,\n ScrollUpArrow,\n ScrollDownArrow,\n Group,\n GroupLabel,\n Separator,\n}\nexport {\n Root,\n Label,\n Trigger,\n Value,\n Icon,\n Portal,\n Backdrop,\n Positioner,\n Popup,\n Arrow,\n List,\n Item,\n ItemCheckbox,\n ItemText,\n ScrollUpArrow,\n ScrollDownArrow,\n Group,\n GroupLabel,\n Separator,\n}\nexport type { RootProps as MultiSelectRootProps }\n"],"mappings":";;;;;;AAkEA,SAAS,EAAa,GAA0B;CAC9C,OAAO,kBAAC,EAAW,MAAZ;EAAiB,UAAA;EAAS,GAAI;CAAQ,CAAA;AAC/C;AAOA,SAAS,EAAK,EAAE,cAAW,GAAG,KAAiD;CAC7E,OACE,kBAAC,EAAW,MAAZ;EACE,WAAW,EACT,+EACA,mEACA,2CACA,qEACA,CACF;EACA,GAAI;CACL,CAAA;AAEL;AAQA,SAAS,EAAa,EAAE,cAAW,aAAU,GAAG,KAAiC;CAC/E,OACE,kBAAC,QAAD;EACE,eAAA;EACA,WAAW,EACT,kEACA,+DACA,iDACA,CACF;EACA,GAAI;YAEH,KACC,kBAAC,GAAD;GACE,eAAA;GACA,WAAW,EACT,+EACA,6EACF;EACD,CAAA;CAEC,CAAA;AAEV;AAEA,IAAa,IAAc;CACzB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { MultiSelect, Root, Label, Trigger, Value, Icon, Portal, Backdrop, Positioner, Popup, Arrow, List, Item, ItemCheckbox, ItemText, ScrollUpArrow, ScrollDownArrow, Group, GroupLabel, Separator, } from './MultiSelect';
|
|
2
|
+
export type { MultiSelectRootProps } from './MultiSelect';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { Arrow as e, Backdrop as t, Group as n, GroupLabel as r, Icon as i, ItemText as a, Label as o, List as s, Popup as c, Portal as l, Positioner as u, ScrollDownArrow as d, ScrollUpArrow as f, Separator as p, Trigger as m, Value as h } from "../select/Select.js";
|
|
2
|
+
import { Item as g, ItemCheckbox as _, MultiSelect as v, Root as y } from "./MultiSelect.js";
|
|
3
|
+
export { e as Arrow, t as Backdrop, n as Group, r as GroupLabel, i as Icon, g as Item, _ as ItemCheckbox, a as ItemText, o as Label, s as List, v as MultiSelect, c as Popup, l as Portal, u as Positioner, y as Root, d as ScrollDownArrow, f as ScrollUpArrow, p as Separator, m as Trigger, h as Value };
|
|
@@ -11,7 +11,7 @@ function o({ className: t, ...n }) {
|
|
|
11
11
|
}
|
|
12
12
|
function s({ className: t, ...n }) {
|
|
13
13
|
return /* @__PURE__ */ i(a.Group, {
|
|
14
|
-
className: e("flex h-14 items-stretch overflow-hidden rounded-
|
|
14
|
+
className: e("flex h-14 items-stretch overflow-hidden rounded-small border border-outline bg-surface", "transition-colors hover:border-on-surface", "focus-within:border-primary focus-within:ring-1 focus-within:ring-primary focus-within:hover:border-primary", "data-[disabled]:pointer-events-none data-[disabled]:opacity-[0.38]", t),
|
|
15
15
|
...n
|
|
16
16
|
});
|
|
17
17
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NumberField.js","names":[],"sources":["../../../src/components/number-field/NumberField.tsx"],"sourcesContent":["import { NumberField as BaseNumberField } from '@base-ui/react/number-field'\nimport { Minus, MoveHorizontal, Plus } from 'lucide-react'\nimport { cn } from '#/lib/cn'\n\nimport type { ComponentProps } from 'react'\n\n/**\n * NumberField — Material 3 styled wrapper over Base UI's headless Number Field.\n * Behaviour/accessibility (spinbutton semantics, locale-aware parsing/formatting,\n * pointer scrubbing, wheel + keyboard stepping): Base UI\n * (https://base-ui.com/react/components/number-field).\n * Visuals: M3 outlined text field anatomy — 56px field box, extra-small corner,\n * outline border (primary on focus), body-large input text, on-surface-variant\n * stepper icons with state layers.\n * Design ref: port/core/ui/components/text-field (see CLAUDE.md).\n *\n * Compound API mirrors Base UI:\n * <NumberField.Root defaultValue={100}>\n * <NumberField.ScrubArea>\n * <label>Amount</label>\n * <NumberField.ScrubAreaCursor />\n * </NumberField.ScrubArea>\n * <NumberField.Group>\n * <NumberField.Decrement />\n * <NumberField.Input />\n * <NumberField.Increment />\n * </NumberField.Group>\n * </NumberField.Root>\n */\n\nfunction Root({ className, ...props }: ComponentProps<typeof BaseNumberField.Root>) {\n return (\n <BaseNumberField.Root\n className={cn('flex flex-col items-start gap-1 text-on-surface', className)}\n {...props}\n />\n )\n}\n\n/**\n * The visual field box (M3 outlined text field container): outline border,\n * extra-small corner, primary border + ring while the input inside has focus.\n */\nfunction Group({ className, ...props }: ComponentProps<typeof BaseNumberField.Group>) {\n return (\n <BaseNumberField.Group\n className={cn(\n 'flex h-14 items-stretch overflow-hidden rounded-
|
|
1
|
+
{"version":3,"file":"NumberField.js","names":[],"sources":["../../../src/components/number-field/NumberField.tsx"],"sourcesContent":["import { NumberField as BaseNumberField } from '@base-ui/react/number-field'\nimport { Minus, MoveHorizontal, Plus } from 'lucide-react'\nimport { cn } from '#/lib/cn'\n\nimport type { ComponentProps } from 'react'\n\n/**\n * NumberField — Material 3 styled wrapper over Base UI's headless Number Field.\n * Behaviour/accessibility (spinbutton semantics, locale-aware parsing/formatting,\n * pointer scrubbing, wheel + keyboard stepping): Base UI\n * (https://base-ui.com/react/components/number-field).\n * Visuals: M3 outlined text field anatomy — 56px field box, extra-small corner,\n * outline border (primary on focus), body-large input text, on-surface-variant\n * stepper icons with state layers.\n * Design ref: port/core/ui/components/text-field (see CLAUDE.md).\n *\n * Compound API mirrors Base UI:\n * <NumberField.Root defaultValue={100}>\n * <NumberField.ScrubArea>\n * <label>Amount</label>\n * <NumberField.ScrubAreaCursor />\n * </NumberField.ScrubArea>\n * <NumberField.Group>\n * <NumberField.Decrement />\n * <NumberField.Input />\n * <NumberField.Increment />\n * </NumberField.Group>\n * </NumberField.Root>\n */\n\nfunction Root({ className, ...props }: ComponentProps<typeof BaseNumberField.Root>) {\n return (\n <BaseNumberField.Root\n className={cn('flex flex-col items-start gap-1 text-on-surface', className)}\n {...props}\n />\n )\n}\n\n/**\n * The visual field box (M3 outlined text field container): outline border,\n * extra-small corner, primary border + ring while the input inside has focus.\n */\nfunction Group({ className, ...props }: ComponentProps<typeof BaseNumberField.Group>) {\n return (\n <BaseNumberField.Group\n className={cn(\n 'flex h-14 items-stretch overflow-hidden rounded-small border border-outline bg-surface',\n 'transition-colors hover:border-on-surface',\n 'focus-within:border-primary focus-within:ring-1 focus-within:ring-primary focus-within:hover:border-primary',\n 'data-[disabled]:pointer-events-none data-[disabled]:opacity-[0.38]',\n className,\n )}\n {...props}\n />\n )\n}\n\n/** Stepper button that decreases the value; renders a minus icon by default. */\nfunction Decrement({\n className,\n children,\n ...props\n}: ComponentProps<typeof BaseNumberField.Decrement>) {\n return (\n <BaseNumberField.Decrement\n className={cn(\n 'flex w-12 shrink-0 cursor-pointer items-center justify-center',\n 'border-r border-outline-variant text-on-surface-variant transition-colors',\n 'hover:bg-on-surface/[0.08] active:bg-on-surface/[0.12]',\n 'focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary',\n 'disabled:cursor-not-allowed disabled:opacity-[0.38] disabled:hover:bg-transparent',\n className,\n )}\n {...props}\n >\n {children ?? <Minus aria-hidden className=\"size-5\" />}\n </BaseNumberField.Decrement>\n )\n}\n\n/** Stepper button that increases the value; renders a plus icon by default. */\nfunction Increment({\n className,\n children,\n ...props\n}: ComponentProps<typeof BaseNumberField.Increment>) {\n return (\n <BaseNumberField.Increment\n className={cn(\n 'flex w-12 shrink-0 cursor-pointer items-center justify-center',\n 'border-l border-outline-variant text-on-surface-variant transition-colors',\n 'hover:bg-on-surface/[0.08] active:bg-on-surface/[0.12]',\n 'focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary',\n 'disabled:cursor-not-allowed disabled:opacity-[0.38] disabled:hover:bg-transparent',\n className,\n )}\n {...props}\n >\n {children ?? <Plus aria-hidden className=\"size-5\" />}\n </BaseNumberField.Increment>\n )\n}\n\nfunction Input({ className, ...props }: ComponentProps<typeof BaseNumberField.Input>) {\n return (\n <BaseNumberField.Input\n className={cn(\n 'h-full w-full min-w-0 bg-transparent px-3 text-center text-body-large tabular-nums',\n 'text-on-surface placeholder:text-on-surface-variant focus:outline-none',\n className,\n )}\n {...props}\n />\n )\n}\n\n/**\n * Click-and-drag area (usually wrapping the label) that scrubs the value\n * horizontally, like a video-editing slider.\n */\nfunction ScrubArea({ className, ...props }: ComponentProps<typeof BaseNumberField.ScrubArea>) {\n return (\n <BaseNumberField.ScrubArea\n className={cn(\n 'cursor-ew-resize select-none text-label-medium text-on-surface-variant',\n className,\n )}\n {...props}\n />\n )\n}\n\n/** Virtual cursor shown while scrubbing; renders a horizontal-move icon by default. */\nfunction ScrubAreaCursor({\n className,\n children,\n ...props\n}: ComponentProps<typeof BaseNumberField.ScrubAreaCursor>) {\n return (\n <BaseNumberField.ScrubAreaCursor\n className={cn('text-on-surface drop-shadow-sm', className)}\n {...props}\n >\n {children ?? <MoveHorizontal aria-hidden className=\"size-5\" />}\n </BaseNumberField.ScrubAreaCursor>\n )\n}\n\nexport const NumberField = {\n Root,\n Group,\n Decrement,\n Input,\n Increment,\n ScrubArea,\n ScrubAreaCursor,\n}\nexport { Root, Group, Decrement, Input, Increment, ScrubArea, ScrubAreaCursor }\n"],"mappings":";;;;;AA8BA,SAAS,EAAK,EAAE,cAAW,GAAG,KAAsD;CAClF,OACE,kBAAC,EAAgB,MAAjB;EACE,WAAW,EAAG,mDAAmD,CAAS;EAC1E,GAAI;CACL,CAAA;AAEL;AAMA,SAAS,EAAM,EAAE,cAAW,GAAG,KAAuD;CACpF,OACE,kBAAC,EAAgB,OAAjB;EACE,WAAW,EACT,0FACA,6CACA,+GACA,sEACA,CACF;EACA,GAAI;CACL,CAAA;AAEL;AAGA,SAAS,EAAU,EACjB,cACA,aACA,GAAG,KACgD;CACnD,OACE,kBAAC,EAAgB,WAAjB;EACE,WAAW,EACT,iEACA,6EACA,0DACA,yFACA,qFACA,CACF;EACA,GAAI;YAEH,KAAY,kBAAC,GAAD;GAAO,eAAA;GAAY,WAAU;EAAU,CAAA;CAC3B,CAAA;AAE/B;AAGA,SAAS,EAAU,EACjB,cACA,aACA,GAAG,KACgD;CACnD,OACE,kBAAC,EAAgB,WAAjB;EACE,WAAW,EACT,iEACA,6EACA,0DACA,yFACA,qFACA,CACF;EACA,GAAI;YAEH,KAAY,kBAAC,GAAD;GAAM,eAAA;GAAY,WAAU;EAAU,CAAA;CAC1B,CAAA;AAE/B;AAEA,SAAS,EAAM,EAAE,cAAW,GAAG,KAAuD;CACpF,OACE,kBAAC,EAAgB,OAAjB;EACE,WAAW,EACT,sFACA,0EACA,CACF;EACA,GAAI;CACL,CAAA;AAEL;AAMA,SAAS,EAAU,EAAE,cAAW,GAAG,KAA2D;CAC5F,OACE,kBAAC,EAAgB,WAAjB;EACE,WAAW,EACT,0EACA,CACF;EACA,GAAI;CACL,CAAA;AAEL;AAGA,SAAS,EAAgB,EACvB,cACA,aACA,GAAG,KACsD;CACzD,OACE,kBAAC,EAAgB,iBAAjB;EACE,WAAW,EAAG,kCAAkC,CAAS;EACzD,GAAI;YAEH,KAAY,kBAAC,GAAD;GAAgB,eAAA;GAAY,WAAU;EAAU,CAAA;CAC9B,CAAA;AAErC;AAEA,IAAa,IAAc;CACzB;CACA;CACA;CACA;CACA;CACA;CACA;AACF"}
|
|
@@ -10,7 +10,7 @@ function r({ className: r, ...i }) {
|
|
|
10
10
|
}
|
|
11
11
|
function i({ className: r, ...i }) {
|
|
12
12
|
return /* @__PURE__ */ t(n.Input, {
|
|
13
|
-
className: e("h-14 w-11 rounded-
|
|
13
|
+
className: e("h-14 w-11 rounded-small border border-outline bg-transparent", "text-center text-title-large text-on-surface caret-primary", "transition-colors selection:bg-primary/[0.12]", "not-disabled:hover:border-on-surface", "focus:border-primary focus:outline-2 focus:-outline-offset-1 focus:outline-primary", "data-[invalid]:border-error data-[invalid]:caret-error", "data-[invalid]:not-disabled:hover:border-error", "data-[invalid]:focus:border-error data-[invalid]:focus:outline-error", "disabled:cursor-not-allowed disabled:opacity-[0.38]", r),
|
|
14
14
|
...i
|
|
15
15
|
});
|
|
16
16
|
}
|