@spacing-ui/core 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nhan Nguyen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # @spacing-ui/core
2
+
3
+ Headless, accessible React UI primitives.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @spacing-ui/core
9
+ ```
10
+
11
+ `react` and `react-dom` 18 or 19 are peer dependencies.
12
+
13
+ ## Components
14
+
15
+ ### `Select`
16
+
17
+ A fully accessible, unstyled listbox. Bring your own styles.
18
+
19
+ ```tsx
20
+ import { Select } from "@spacing-ui/core";
21
+
22
+ function Example() {
23
+ const [value, setValue] = useState("apple");
24
+
25
+ return (
26
+ <Select value={value} onValueChange={setValue}>
27
+ <Select.Trigger>
28
+ {({ open }) => (
29
+ <>
30
+ <span>{value}</span>
31
+ <ChevronIcon className={open ? "rotate-180" : ""} />
32
+ </>
33
+ )}
34
+ </Select.Trigger>
35
+ <Select.Content>
36
+ <Select.Option value="apple" textValue="Apple">
37
+ {({ selected, active }) => (
38
+ <div data-active={active} data-selected={selected}>
39
+ Apple
40
+ </div>
41
+ )}
42
+ </Select.Option>
43
+ <Select.Option value="banana" textValue="Banana">
44
+ Banana
45
+ </Select.Option>
46
+ </Select.Content>
47
+ </Select>
48
+ );
49
+ }
50
+ ```
51
+
52
+ Keyboard:
53
+
54
+ - `Space` / `Enter` / `↓` / `↑` on trigger: open
55
+ - `↑` / `↓`: move highlight
56
+ - `Home` / `End`: jump to first / last
57
+ - typing letters: jump to matching option
58
+ - `Enter` / `Space`: select highlighted
59
+ - `Esc`: close, return focus to trigger
60
+ - `Tab`: close, move focus on
61
+
62
+ ## License
63
+
64
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,258 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ // src/select/select.tsx
7
+ function render(children, state) {
8
+ return typeof children === "function" ? children(state) : children;
9
+ }
10
+ var SelectContext = react.createContext(null);
11
+ function useSelectContext(component) {
12
+ const ctx = react.useContext(SelectContext);
13
+ if (!ctx) {
14
+ throw new Error(`<Select.${component}> must be rendered inside <Select>`);
15
+ }
16
+ return ctx;
17
+ }
18
+ var Root = ({ value, onValueChange, children }) => {
19
+ const [open, setOpenState] = react.useState(false);
20
+ const [activeValue, setActiveValue] = react.useState(null);
21
+ const triggerRef = react.useRef(null);
22
+ const listboxRef = react.useRef(null);
23
+ const reactId = react.useId();
24
+ const triggerId = `${reactId}trigger`;
25
+ const listboxId = `${reactId}listbox`;
26
+ const optionIdPrefix = `${reactId}option-`;
27
+ const getOptionId = react.useCallback(
28
+ (v) => optionIdPrefix + v.replace(/[^a-zA-Z0-9_-]/g, "_"),
29
+ [optionIdPrefix]
30
+ );
31
+ const getOrderedOptions = react.useCallback(() => {
32
+ const root = listboxRef.current;
33
+ if (!root) return [];
34
+ const nodes = root.querySelectorAll('[role="option"]');
35
+ return Array.from(nodes).map((node) => ({
36
+ value: node.dataset.value ?? "",
37
+ textValue: node.dataset.textValue ?? node.textContent ?? ""
38
+ }));
39
+ }, []);
40
+ const setOpen = react.useCallback((next) => {
41
+ setOpenState(next);
42
+ if (!next) setActiveValue(null);
43
+ }, []);
44
+ react.useEffect(() => {
45
+ if (!open) return;
46
+ const onPointerDown = (e) => {
47
+ const target = e.target;
48
+ if (!triggerRef.current?.contains(target) && !listboxRef.current?.contains(target)) {
49
+ setOpen(false);
50
+ }
51
+ };
52
+ document.addEventListener("pointerdown", onPointerDown);
53
+ return () => document.removeEventListener("pointerdown", onPointerDown);
54
+ }, [open, setOpen]);
55
+ const ctxValue = react.useMemo(
56
+ () => ({
57
+ open,
58
+ setOpen,
59
+ value,
60
+ onValueChange,
61
+ activeValue,
62
+ setActiveValue,
63
+ triggerId,
64
+ listboxId,
65
+ triggerRef,
66
+ listboxRef,
67
+ getOptionId,
68
+ getOrderedOptions
69
+ }),
70
+ [
71
+ open,
72
+ setOpen,
73
+ value,
74
+ onValueChange,
75
+ activeValue,
76
+ triggerId,
77
+ listboxId,
78
+ getOptionId,
79
+ getOrderedOptions
80
+ ]
81
+ );
82
+ return /* @__PURE__ */ jsxRuntime.jsx(SelectContext.Provider, { value: ctxValue, children });
83
+ };
84
+ var Trigger = react.forwardRef(function Trigger2({ children, onClick, onKeyDown, ...rest }, forwardedRef) {
85
+ const ctx = useSelectContext("Trigger");
86
+ const setRefs = (node) => {
87
+ ctx.triggerRef.current = node;
88
+ if (typeof forwardedRef === "function") forwardedRef(node);
89
+ else if (forwardedRef) forwardedRef.current = node;
90
+ };
91
+ const handleClick = (e) => {
92
+ onClick?.(e);
93
+ if (e.defaultPrevented) return;
94
+ ctx.setOpen(!ctx.open);
95
+ };
96
+ const handleKeyDown = (e) => {
97
+ onKeyDown?.(e);
98
+ if (e.defaultPrevented) return;
99
+ if (e.key === " " || e.key === "Enter" || e.key === "ArrowDown" || e.key === "ArrowUp") {
100
+ e.preventDefault();
101
+ ctx.setOpen(true);
102
+ }
103
+ };
104
+ return /* @__PURE__ */ jsxRuntime.jsx(
105
+ "button",
106
+ {
107
+ ref: setRefs,
108
+ type: "button",
109
+ id: ctx.triggerId,
110
+ role: "combobox",
111
+ "aria-haspopup": "listbox",
112
+ "aria-expanded": ctx.open,
113
+ "aria-controls": ctx.listboxId,
114
+ onClick: handleClick,
115
+ onKeyDown: handleKeyDown,
116
+ ...rest,
117
+ children: render(children, { open: ctx.open, value: ctx.value })
118
+ }
119
+ );
120
+ });
121
+ var TYPEAHEAD_TIMEOUT_MS = 500;
122
+ var Content = ({ children, onKeyDown, ...rest }) => {
123
+ const ctx = useSelectContext("Content");
124
+ const typeaheadRef = react.useRef({ buffer: "", lastTime: 0 });
125
+ react.useEffect(() => {
126
+ if (!ctx.open) return;
127
+ const options = ctx.getOrderedOptions();
128
+ if (options.length === 0) {
129
+ ctx.listboxRef.current?.focus();
130
+ return;
131
+ }
132
+ const initial = options.find((o) => o.value === ctx.value)?.value ?? options[0].value;
133
+ ctx.setActiveValue(initial);
134
+ ctx.listboxRef.current?.focus();
135
+ }, [ctx.open]);
136
+ if (!ctx.open) return null;
137
+ const moveActive = (delta) => {
138
+ const options = ctx.getOrderedOptions();
139
+ if (options.length === 0) return;
140
+ const currentIdx = options.findIndex((o) => o.value === ctx.activeValue);
141
+ let nextIdx;
142
+ if (delta === "first") nextIdx = 0;
143
+ else if (delta === "last") nextIdx = options.length - 1;
144
+ else if (currentIdx === -1) nextIdx = delta === 1 ? 0 : options.length - 1;
145
+ else nextIdx = Math.min(options.length - 1, Math.max(0, currentIdx + delta));
146
+ ctx.setActiveValue(options[nextIdx].value);
147
+ };
148
+ const typeahead = (char) => {
149
+ const now = Date.now();
150
+ const reset = now - typeaheadRef.current.lastTime > TYPEAHEAD_TIMEOUT_MS;
151
+ const buffer = (reset ? "" : typeaheadRef.current.buffer) + char.toLowerCase();
152
+ typeaheadRef.current = { buffer, lastTime: now };
153
+ const options = ctx.getOrderedOptions();
154
+ const match = options.find((o) => o.textValue.toLowerCase().startsWith(buffer));
155
+ if (match) ctx.setActiveValue(match.value);
156
+ };
157
+ const handleKeyDown = (e) => {
158
+ onKeyDown?.(e);
159
+ if (e.defaultPrevented) return;
160
+ switch (e.key) {
161
+ case "ArrowDown":
162
+ e.preventDefault();
163
+ moveActive(1);
164
+ return;
165
+ case "ArrowUp":
166
+ e.preventDefault();
167
+ moveActive(-1);
168
+ return;
169
+ case "Home":
170
+ e.preventDefault();
171
+ moveActive("first");
172
+ return;
173
+ case "End":
174
+ e.preventDefault();
175
+ moveActive("last");
176
+ return;
177
+ case "Enter":
178
+ case " ":
179
+ e.preventDefault();
180
+ if (ctx.activeValue !== null) {
181
+ ctx.onValueChange(ctx.activeValue);
182
+ ctx.setOpen(false);
183
+ ctx.triggerRef.current?.focus();
184
+ }
185
+ return;
186
+ case "Escape":
187
+ e.preventDefault();
188
+ ctx.setOpen(false);
189
+ ctx.triggerRef.current?.focus();
190
+ return;
191
+ case "Tab":
192
+ ctx.setOpen(false);
193
+ return;
194
+ default:
195
+ if (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) {
196
+ typeahead(e.key);
197
+ }
198
+ }
199
+ };
200
+ return /* @__PURE__ */ jsxRuntime.jsx(
201
+ "div",
202
+ {
203
+ ref: (node) => {
204
+ ctx.listboxRef.current = node;
205
+ },
206
+ id: ctx.listboxId,
207
+ role: "listbox",
208
+ tabIndex: -1,
209
+ "aria-labelledby": ctx.triggerId,
210
+ "aria-activedescendant": ctx.activeValue !== null ? ctx.getOptionId(ctx.activeValue) : void 0,
211
+ onKeyDown: handleKeyDown,
212
+ ...rest,
213
+ children
214
+ }
215
+ );
216
+ };
217
+ var Option = ({ value, textValue, children, onMouseEnter, onClick, ...rest }) => {
218
+ const ctx = useSelectContext("Option");
219
+ const selected = ctx.value === value;
220
+ const active = ctx.activeValue === value;
221
+ const handleClick = (e) => {
222
+ onClick?.(e);
223
+ if (e.defaultPrevented) return;
224
+ ctx.onValueChange(value);
225
+ ctx.setOpen(false);
226
+ ctx.triggerRef.current?.focus();
227
+ };
228
+ const handleMouseEnter = (e) => {
229
+ onMouseEnter?.(e);
230
+ if (e.defaultPrevented) return;
231
+ ctx.setActiveValue(value);
232
+ };
233
+ return /* @__PURE__ */ jsxRuntime.jsx(
234
+ "div",
235
+ {
236
+ id: ctx.getOptionId(value),
237
+ role: "option",
238
+ "aria-selected": selected,
239
+ "data-value": value,
240
+ "data-text-value": textValue,
241
+ "data-active": active || void 0,
242
+ "data-selected": selected || void 0,
243
+ onClick: handleClick,
244
+ onMouseEnter: handleMouseEnter,
245
+ ...rest,
246
+ children: render(children, { selected, active })
247
+ }
248
+ );
249
+ };
250
+ var Select = Object.assign(Root, {
251
+ Trigger,
252
+ Content,
253
+ Option
254
+ });
255
+
256
+ exports.Select = Select;
257
+ //# sourceMappingURL=index.cjs.map
258
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/select/select.tsx"],"names":["createContext","useContext","useState","useRef","useId","useCallback","useEffect","useMemo","forwardRef","Trigger","jsx"],"mappings":"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","file":"index.cjs","sourcesContent":["import {\n createContext,\n useContext,\n useState,\n useRef,\n useEffect,\n useId,\n useMemo,\n useCallback,\n forwardRef,\n type ReactNode,\n type ButtonHTMLAttributes,\n type HTMLAttributes,\n type KeyboardEvent as ReactKeyboardEvent,\n type MouseEvent as ReactMouseEvent,\n type RefObject,\n} from \"react\";\n\ntype Renderable<S> = ReactNode | ((state: S) => ReactNode);\n\nfunction render<S>(children: Renderable<S> | undefined, state: S): ReactNode {\n return typeof children === \"function\" ? children(state) : children;\n}\n\ninterface OptionData {\n value: string;\n textValue: string;\n}\n\ninterface SelectContextValue {\n open: boolean;\n setOpen: (open: boolean) => void;\n value: string;\n onValueChange: (value: string) => void;\n activeValue: string | null;\n setActiveValue: (value: string | null) => void;\n triggerId: string;\n listboxId: string;\n triggerRef: RefObject<HTMLButtonElement | null>;\n listboxRef: RefObject<HTMLDivElement | null>;\n getOptionId: (value: string) => string;\n getOrderedOptions: () => OptionData[];\n}\n\nconst SelectContext = createContext<SelectContextValue | null>(null);\n\nfunction useSelectContext(component: string): SelectContextValue {\n const ctx = useContext(SelectContext);\n if (!ctx) {\n throw new Error(`<Select.${component}> must be rendered inside <Select>`);\n }\n return ctx;\n}\n\ninterface SelectProps {\n value: string;\n onValueChange: (value: string) => void;\n children: ReactNode;\n}\n\nconst Root = ({ value, onValueChange, children }: SelectProps) => {\n const [open, setOpenState] = useState(false);\n const [activeValue, setActiveValue] = useState<string | null>(null);\n const triggerRef = useRef<HTMLButtonElement | null>(null);\n const listboxRef = useRef<HTMLDivElement | null>(null);\n\n const reactId = useId();\n const triggerId = `${reactId}trigger`;\n const listboxId = `${reactId}listbox`;\n const optionIdPrefix = `${reactId}option-`;\n\n const getOptionId = useCallback(\n (v: string) => optionIdPrefix + v.replace(/[^a-zA-Z0-9_-]/g, \"_\"),\n [optionIdPrefix]\n );\n\n const getOrderedOptions = useCallback((): OptionData[] => {\n const root = listboxRef.current;\n if (!root) return [];\n const nodes = root.querySelectorAll<HTMLElement>('[role=\"option\"]');\n return Array.from(nodes).map((node) => ({\n value: node.dataset.value ?? \"\",\n textValue: node.dataset.textValue ?? node.textContent ?? \"\",\n }));\n }, []);\n\n const setOpen = useCallback((next: boolean) => {\n setOpenState(next);\n if (!next) setActiveValue(null);\n }, []);\n\n useEffect(() => {\n if (!open) return;\n const onPointerDown = (e: PointerEvent) => {\n const target = e.target as Node;\n if (!triggerRef.current?.contains(target) && !listboxRef.current?.contains(target)) {\n setOpen(false);\n }\n };\n document.addEventListener(\"pointerdown\", onPointerDown);\n return () => document.removeEventListener(\"pointerdown\", onPointerDown);\n }, [open, setOpen]);\n\n const ctxValue = useMemo<SelectContextValue>(\n () => ({\n open,\n setOpen,\n value,\n onValueChange,\n activeValue,\n setActiveValue,\n triggerId,\n listboxId,\n triggerRef,\n listboxRef,\n getOptionId,\n getOrderedOptions,\n }),\n [\n open,\n setOpen,\n value,\n onValueChange,\n activeValue,\n triggerId,\n listboxId,\n getOptionId,\n getOrderedOptions,\n ]\n );\n\n return <SelectContext.Provider value={ctxValue}>{children}</SelectContext.Provider>;\n};\n\ninterface TriggerProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"children\"> {\n children?: Renderable<{ open: boolean; value: string }>;\n}\n\nconst Trigger = forwardRef<HTMLButtonElement, TriggerProps>(function Trigger(\n { children, onClick, onKeyDown, ...rest },\n forwardedRef\n) {\n const ctx = useSelectContext(\"Trigger\");\n\n const setRefs = (node: HTMLButtonElement | null) => {\n ctx.triggerRef.current = node;\n if (typeof forwardedRef === \"function\") forwardedRef(node);\n else if (forwardedRef) forwardedRef.current = node;\n };\n\n const handleClick = (e: ReactMouseEvent<HTMLButtonElement>) => {\n onClick?.(e);\n if (e.defaultPrevented) return;\n ctx.setOpen(!ctx.open);\n };\n\n const handleKeyDown = (e: ReactKeyboardEvent<HTMLButtonElement>) => {\n onKeyDown?.(e);\n if (e.defaultPrevented) return;\n if (e.key === \" \" || e.key === \"Enter\" || e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n ctx.setOpen(true);\n }\n };\n\n return (\n <button\n ref={setRefs}\n type=\"button\"\n id={ctx.triggerId}\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-expanded={ctx.open}\n aria-controls={ctx.listboxId}\n onClick={handleClick}\n onKeyDown={handleKeyDown}\n {...rest}\n >\n {render(children, { open: ctx.open, value: ctx.value })}\n </button>\n );\n});\n\ninterface ContentProps extends HTMLAttributes<HTMLDivElement> {\n children: ReactNode;\n}\n\nconst TYPEAHEAD_TIMEOUT_MS = 500;\n\nconst Content = ({ children, onKeyDown, ...rest }: ContentProps) => {\n const ctx = useSelectContext(\"Content\");\n const typeaheadRef = useRef({ buffer: \"\", lastTime: 0 });\n\n useEffect(() => {\n if (!ctx.open) return;\n const options = ctx.getOrderedOptions();\n if (options.length === 0) {\n ctx.listboxRef.current?.focus();\n return;\n }\n const initial = options.find((o) => o.value === ctx.value)?.value ?? options[0]!.value;\n ctx.setActiveValue(initial);\n ctx.listboxRef.current?.focus();\n // only re-run on open transition\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [ctx.open]);\n\n if (!ctx.open) return null;\n\n const moveActive = (delta: 1 | -1 | \"first\" | \"last\") => {\n const options = ctx.getOrderedOptions();\n if (options.length === 0) return;\n const currentIdx = options.findIndex((o) => o.value === ctx.activeValue);\n let nextIdx: number;\n if (delta === \"first\") nextIdx = 0;\n else if (delta === \"last\") nextIdx = options.length - 1;\n else if (currentIdx === -1) nextIdx = delta === 1 ? 0 : options.length - 1;\n else nextIdx = Math.min(options.length - 1, Math.max(0, currentIdx + delta));\n ctx.setActiveValue(options[nextIdx]!.value);\n };\n\n const typeahead = (char: string) => {\n const now = Date.now();\n const reset = now - typeaheadRef.current.lastTime > TYPEAHEAD_TIMEOUT_MS;\n const buffer = (reset ? \"\" : typeaheadRef.current.buffer) + char.toLowerCase();\n typeaheadRef.current = { buffer, lastTime: now };\n const options = ctx.getOrderedOptions();\n const match = options.find((o) => o.textValue.toLowerCase().startsWith(buffer));\n if (match) ctx.setActiveValue(match.value);\n };\n\n const handleKeyDown = (e: ReactKeyboardEvent<HTMLDivElement>) => {\n onKeyDown?.(e);\n if (e.defaultPrevented) return;\n\n switch (e.key) {\n case \"ArrowDown\":\n e.preventDefault();\n moveActive(1);\n return;\n case \"ArrowUp\":\n e.preventDefault();\n moveActive(-1);\n return;\n case \"Home\":\n e.preventDefault();\n moveActive(\"first\");\n return;\n case \"End\":\n e.preventDefault();\n moveActive(\"last\");\n return;\n case \"Enter\":\n case \" \":\n e.preventDefault();\n if (ctx.activeValue !== null) {\n ctx.onValueChange(ctx.activeValue);\n ctx.setOpen(false);\n ctx.triggerRef.current?.focus();\n }\n return;\n case \"Escape\":\n e.preventDefault();\n ctx.setOpen(false);\n ctx.triggerRef.current?.focus();\n return;\n case \"Tab\":\n ctx.setOpen(false);\n return;\n default:\n if (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) {\n typeahead(e.key);\n }\n }\n };\n\n return (\n <div\n ref={(node) => {\n ctx.listboxRef.current = node;\n }}\n id={ctx.listboxId}\n role=\"listbox\"\n tabIndex={-1}\n aria-labelledby={ctx.triggerId}\n aria-activedescendant={\n ctx.activeValue !== null ? ctx.getOptionId(ctx.activeValue) : undefined\n }\n onKeyDown={handleKeyDown}\n {...rest}\n >\n {children}\n </div>\n );\n};\n\ninterface OptionProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n value: string;\n textValue?: string;\n children?: Renderable<{ selected: boolean; active: boolean }>;\n}\n\nconst Option = ({ value, textValue, children, onMouseEnter, onClick, ...rest }: OptionProps) => {\n const ctx = useSelectContext(\"Option\");\n const selected = ctx.value === value;\n const active = ctx.activeValue === value;\n\n const handleClick = (e: ReactMouseEvent<HTMLDivElement>) => {\n onClick?.(e);\n if (e.defaultPrevented) return;\n ctx.onValueChange(value);\n ctx.setOpen(false);\n ctx.triggerRef.current?.focus();\n };\n\n const handleMouseEnter = (e: ReactMouseEvent<HTMLDivElement>) => {\n onMouseEnter?.(e);\n if (e.defaultPrevented) return;\n ctx.setActiveValue(value);\n };\n\n return (\n <div\n id={ctx.getOptionId(value)}\n role=\"option\"\n aria-selected={selected}\n data-value={value}\n data-text-value={textValue}\n data-active={active || undefined}\n data-selected={selected || undefined}\n onClick={handleClick}\n onMouseEnter={handleMouseEnter}\n {...rest}\n >\n {render(children, { selected, active })}\n </div>\n );\n};\n\nexport const Select = Object.assign(Root, {\n Trigger,\n Content,\n Option,\n});\n\nexport type { SelectProps, TriggerProps, ContentProps, OptionProps };\n"]}
@@ -0,0 +1,33 @@
1
+ import * as react from 'react';
2
+ import { HTMLAttributes, ReactNode, ButtonHTMLAttributes } from 'react';
3
+
4
+ type Renderable<S> = ReactNode | ((state: S) => ReactNode);
5
+ interface SelectProps {
6
+ value: string;
7
+ onValueChange: (value: string) => void;
8
+ children: ReactNode;
9
+ }
10
+ interface TriggerProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
11
+ children?: Renderable<{
12
+ open: boolean;
13
+ value: string;
14
+ }>;
15
+ }
16
+ interface ContentProps extends HTMLAttributes<HTMLDivElement> {
17
+ children: ReactNode;
18
+ }
19
+ interface OptionProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
20
+ value: string;
21
+ textValue?: string;
22
+ children?: Renderable<{
23
+ selected: boolean;
24
+ active: boolean;
25
+ }>;
26
+ }
27
+ declare const Select: (({ value, onValueChange, children }: SelectProps) => react.JSX.Element) & {
28
+ Trigger: react.ForwardRefExoticComponent<TriggerProps & react.RefAttributes<HTMLButtonElement>>;
29
+ Content: ({ children, onKeyDown, ...rest }: ContentProps) => react.JSX.Element | null;
30
+ Option: ({ value, textValue, children, onMouseEnter, onClick, ...rest }: OptionProps) => react.JSX.Element;
31
+ };
32
+
33
+ export { type ContentProps, type OptionProps, Select, type SelectProps, type TriggerProps };
@@ -0,0 +1,33 @@
1
+ import * as react from 'react';
2
+ import { HTMLAttributes, ReactNode, ButtonHTMLAttributes } from 'react';
3
+
4
+ type Renderable<S> = ReactNode | ((state: S) => ReactNode);
5
+ interface SelectProps {
6
+ value: string;
7
+ onValueChange: (value: string) => void;
8
+ children: ReactNode;
9
+ }
10
+ interface TriggerProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
11
+ children?: Renderable<{
12
+ open: boolean;
13
+ value: string;
14
+ }>;
15
+ }
16
+ interface ContentProps extends HTMLAttributes<HTMLDivElement> {
17
+ children: ReactNode;
18
+ }
19
+ interface OptionProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
20
+ value: string;
21
+ textValue?: string;
22
+ children?: Renderable<{
23
+ selected: boolean;
24
+ active: boolean;
25
+ }>;
26
+ }
27
+ declare const Select: (({ value, onValueChange, children }: SelectProps) => react.JSX.Element) & {
28
+ Trigger: react.ForwardRefExoticComponent<TriggerProps & react.RefAttributes<HTMLButtonElement>>;
29
+ Content: ({ children, onKeyDown, ...rest }: ContentProps) => react.JSX.Element | null;
30
+ Option: ({ value, textValue, children, onMouseEnter, onClick, ...rest }: OptionProps) => react.JSX.Element;
31
+ };
32
+
33
+ export { type ContentProps, type OptionProps, Select, type SelectProps, type TriggerProps };
package/dist/index.js ADDED
@@ -0,0 +1,256 @@
1
+ import { createContext, forwardRef, useContext, useState, useRef, useId, useCallback, useEffect, useMemo } from 'react';
2
+ import { jsx } from 'react/jsx-runtime';
3
+
4
+ // src/select/select.tsx
5
+ function render(children, state) {
6
+ return typeof children === "function" ? children(state) : children;
7
+ }
8
+ var SelectContext = createContext(null);
9
+ function useSelectContext(component) {
10
+ const ctx = useContext(SelectContext);
11
+ if (!ctx) {
12
+ throw new Error(`<Select.${component}> must be rendered inside <Select>`);
13
+ }
14
+ return ctx;
15
+ }
16
+ var Root = ({ value, onValueChange, children }) => {
17
+ const [open, setOpenState] = useState(false);
18
+ const [activeValue, setActiveValue] = useState(null);
19
+ const triggerRef = useRef(null);
20
+ const listboxRef = useRef(null);
21
+ const reactId = useId();
22
+ const triggerId = `${reactId}trigger`;
23
+ const listboxId = `${reactId}listbox`;
24
+ const optionIdPrefix = `${reactId}option-`;
25
+ const getOptionId = useCallback(
26
+ (v) => optionIdPrefix + v.replace(/[^a-zA-Z0-9_-]/g, "_"),
27
+ [optionIdPrefix]
28
+ );
29
+ const getOrderedOptions = useCallback(() => {
30
+ const root = listboxRef.current;
31
+ if (!root) return [];
32
+ const nodes = root.querySelectorAll('[role="option"]');
33
+ return Array.from(nodes).map((node) => ({
34
+ value: node.dataset.value ?? "",
35
+ textValue: node.dataset.textValue ?? node.textContent ?? ""
36
+ }));
37
+ }, []);
38
+ const setOpen = useCallback((next) => {
39
+ setOpenState(next);
40
+ if (!next) setActiveValue(null);
41
+ }, []);
42
+ useEffect(() => {
43
+ if (!open) return;
44
+ const onPointerDown = (e) => {
45
+ const target = e.target;
46
+ if (!triggerRef.current?.contains(target) && !listboxRef.current?.contains(target)) {
47
+ setOpen(false);
48
+ }
49
+ };
50
+ document.addEventListener("pointerdown", onPointerDown);
51
+ return () => document.removeEventListener("pointerdown", onPointerDown);
52
+ }, [open, setOpen]);
53
+ const ctxValue = useMemo(
54
+ () => ({
55
+ open,
56
+ setOpen,
57
+ value,
58
+ onValueChange,
59
+ activeValue,
60
+ setActiveValue,
61
+ triggerId,
62
+ listboxId,
63
+ triggerRef,
64
+ listboxRef,
65
+ getOptionId,
66
+ getOrderedOptions
67
+ }),
68
+ [
69
+ open,
70
+ setOpen,
71
+ value,
72
+ onValueChange,
73
+ activeValue,
74
+ triggerId,
75
+ listboxId,
76
+ getOptionId,
77
+ getOrderedOptions
78
+ ]
79
+ );
80
+ return /* @__PURE__ */ jsx(SelectContext.Provider, { value: ctxValue, children });
81
+ };
82
+ var Trigger = forwardRef(function Trigger2({ children, onClick, onKeyDown, ...rest }, forwardedRef) {
83
+ const ctx = useSelectContext("Trigger");
84
+ const setRefs = (node) => {
85
+ ctx.triggerRef.current = node;
86
+ if (typeof forwardedRef === "function") forwardedRef(node);
87
+ else if (forwardedRef) forwardedRef.current = node;
88
+ };
89
+ const handleClick = (e) => {
90
+ onClick?.(e);
91
+ if (e.defaultPrevented) return;
92
+ ctx.setOpen(!ctx.open);
93
+ };
94
+ const handleKeyDown = (e) => {
95
+ onKeyDown?.(e);
96
+ if (e.defaultPrevented) return;
97
+ if (e.key === " " || e.key === "Enter" || e.key === "ArrowDown" || e.key === "ArrowUp") {
98
+ e.preventDefault();
99
+ ctx.setOpen(true);
100
+ }
101
+ };
102
+ return /* @__PURE__ */ jsx(
103
+ "button",
104
+ {
105
+ ref: setRefs,
106
+ type: "button",
107
+ id: ctx.triggerId,
108
+ role: "combobox",
109
+ "aria-haspopup": "listbox",
110
+ "aria-expanded": ctx.open,
111
+ "aria-controls": ctx.listboxId,
112
+ onClick: handleClick,
113
+ onKeyDown: handleKeyDown,
114
+ ...rest,
115
+ children: render(children, { open: ctx.open, value: ctx.value })
116
+ }
117
+ );
118
+ });
119
+ var TYPEAHEAD_TIMEOUT_MS = 500;
120
+ var Content = ({ children, onKeyDown, ...rest }) => {
121
+ const ctx = useSelectContext("Content");
122
+ const typeaheadRef = useRef({ buffer: "", lastTime: 0 });
123
+ useEffect(() => {
124
+ if (!ctx.open) return;
125
+ const options = ctx.getOrderedOptions();
126
+ if (options.length === 0) {
127
+ ctx.listboxRef.current?.focus();
128
+ return;
129
+ }
130
+ const initial = options.find((o) => o.value === ctx.value)?.value ?? options[0].value;
131
+ ctx.setActiveValue(initial);
132
+ ctx.listboxRef.current?.focus();
133
+ }, [ctx.open]);
134
+ if (!ctx.open) return null;
135
+ const moveActive = (delta) => {
136
+ const options = ctx.getOrderedOptions();
137
+ if (options.length === 0) return;
138
+ const currentIdx = options.findIndex((o) => o.value === ctx.activeValue);
139
+ let nextIdx;
140
+ if (delta === "first") nextIdx = 0;
141
+ else if (delta === "last") nextIdx = options.length - 1;
142
+ else if (currentIdx === -1) nextIdx = delta === 1 ? 0 : options.length - 1;
143
+ else nextIdx = Math.min(options.length - 1, Math.max(0, currentIdx + delta));
144
+ ctx.setActiveValue(options[nextIdx].value);
145
+ };
146
+ const typeahead = (char) => {
147
+ const now = Date.now();
148
+ const reset = now - typeaheadRef.current.lastTime > TYPEAHEAD_TIMEOUT_MS;
149
+ const buffer = (reset ? "" : typeaheadRef.current.buffer) + char.toLowerCase();
150
+ typeaheadRef.current = { buffer, lastTime: now };
151
+ const options = ctx.getOrderedOptions();
152
+ const match = options.find((o) => o.textValue.toLowerCase().startsWith(buffer));
153
+ if (match) ctx.setActiveValue(match.value);
154
+ };
155
+ const handleKeyDown = (e) => {
156
+ onKeyDown?.(e);
157
+ if (e.defaultPrevented) return;
158
+ switch (e.key) {
159
+ case "ArrowDown":
160
+ e.preventDefault();
161
+ moveActive(1);
162
+ return;
163
+ case "ArrowUp":
164
+ e.preventDefault();
165
+ moveActive(-1);
166
+ return;
167
+ case "Home":
168
+ e.preventDefault();
169
+ moveActive("first");
170
+ return;
171
+ case "End":
172
+ e.preventDefault();
173
+ moveActive("last");
174
+ return;
175
+ case "Enter":
176
+ case " ":
177
+ e.preventDefault();
178
+ if (ctx.activeValue !== null) {
179
+ ctx.onValueChange(ctx.activeValue);
180
+ ctx.setOpen(false);
181
+ ctx.triggerRef.current?.focus();
182
+ }
183
+ return;
184
+ case "Escape":
185
+ e.preventDefault();
186
+ ctx.setOpen(false);
187
+ ctx.triggerRef.current?.focus();
188
+ return;
189
+ case "Tab":
190
+ ctx.setOpen(false);
191
+ return;
192
+ default:
193
+ if (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) {
194
+ typeahead(e.key);
195
+ }
196
+ }
197
+ };
198
+ return /* @__PURE__ */ jsx(
199
+ "div",
200
+ {
201
+ ref: (node) => {
202
+ ctx.listboxRef.current = node;
203
+ },
204
+ id: ctx.listboxId,
205
+ role: "listbox",
206
+ tabIndex: -1,
207
+ "aria-labelledby": ctx.triggerId,
208
+ "aria-activedescendant": ctx.activeValue !== null ? ctx.getOptionId(ctx.activeValue) : void 0,
209
+ onKeyDown: handleKeyDown,
210
+ ...rest,
211
+ children
212
+ }
213
+ );
214
+ };
215
+ var Option = ({ value, textValue, children, onMouseEnter, onClick, ...rest }) => {
216
+ const ctx = useSelectContext("Option");
217
+ const selected = ctx.value === value;
218
+ const active = ctx.activeValue === value;
219
+ const handleClick = (e) => {
220
+ onClick?.(e);
221
+ if (e.defaultPrevented) return;
222
+ ctx.onValueChange(value);
223
+ ctx.setOpen(false);
224
+ ctx.triggerRef.current?.focus();
225
+ };
226
+ const handleMouseEnter = (e) => {
227
+ onMouseEnter?.(e);
228
+ if (e.defaultPrevented) return;
229
+ ctx.setActiveValue(value);
230
+ };
231
+ return /* @__PURE__ */ jsx(
232
+ "div",
233
+ {
234
+ id: ctx.getOptionId(value),
235
+ role: "option",
236
+ "aria-selected": selected,
237
+ "data-value": value,
238
+ "data-text-value": textValue,
239
+ "data-active": active || void 0,
240
+ "data-selected": selected || void 0,
241
+ onClick: handleClick,
242
+ onMouseEnter: handleMouseEnter,
243
+ ...rest,
244
+ children: render(children, { selected, active })
245
+ }
246
+ );
247
+ };
248
+ var Select = Object.assign(Root, {
249
+ Trigger,
250
+ Content,
251
+ Option
252
+ });
253
+
254
+ export { Select };
255
+ //# sourceMappingURL=index.js.map
256
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/select/select.tsx"],"names":["Trigger"],"mappings":"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","file":"index.js","sourcesContent":["import {\n createContext,\n useContext,\n useState,\n useRef,\n useEffect,\n useId,\n useMemo,\n useCallback,\n forwardRef,\n type ReactNode,\n type ButtonHTMLAttributes,\n type HTMLAttributes,\n type KeyboardEvent as ReactKeyboardEvent,\n type MouseEvent as ReactMouseEvent,\n type RefObject,\n} from \"react\";\n\ntype Renderable<S> = ReactNode | ((state: S) => ReactNode);\n\nfunction render<S>(children: Renderable<S> | undefined, state: S): ReactNode {\n return typeof children === \"function\" ? children(state) : children;\n}\n\ninterface OptionData {\n value: string;\n textValue: string;\n}\n\ninterface SelectContextValue {\n open: boolean;\n setOpen: (open: boolean) => void;\n value: string;\n onValueChange: (value: string) => void;\n activeValue: string | null;\n setActiveValue: (value: string | null) => void;\n triggerId: string;\n listboxId: string;\n triggerRef: RefObject<HTMLButtonElement | null>;\n listboxRef: RefObject<HTMLDivElement | null>;\n getOptionId: (value: string) => string;\n getOrderedOptions: () => OptionData[];\n}\n\nconst SelectContext = createContext<SelectContextValue | null>(null);\n\nfunction useSelectContext(component: string): SelectContextValue {\n const ctx = useContext(SelectContext);\n if (!ctx) {\n throw new Error(`<Select.${component}> must be rendered inside <Select>`);\n }\n return ctx;\n}\n\ninterface SelectProps {\n value: string;\n onValueChange: (value: string) => void;\n children: ReactNode;\n}\n\nconst Root = ({ value, onValueChange, children }: SelectProps) => {\n const [open, setOpenState] = useState(false);\n const [activeValue, setActiveValue] = useState<string | null>(null);\n const triggerRef = useRef<HTMLButtonElement | null>(null);\n const listboxRef = useRef<HTMLDivElement | null>(null);\n\n const reactId = useId();\n const triggerId = `${reactId}trigger`;\n const listboxId = `${reactId}listbox`;\n const optionIdPrefix = `${reactId}option-`;\n\n const getOptionId = useCallback(\n (v: string) => optionIdPrefix + v.replace(/[^a-zA-Z0-9_-]/g, \"_\"),\n [optionIdPrefix]\n );\n\n const getOrderedOptions = useCallback((): OptionData[] => {\n const root = listboxRef.current;\n if (!root) return [];\n const nodes = root.querySelectorAll<HTMLElement>('[role=\"option\"]');\n return Array.from(nodes).map((node) => ({\n value: node.dataset.value ?? \"\",\n textValue: node.dataset.textValue ?? node.textContent ?? \"\",\n }));\n }, []);\n\n const setOpen = useCallback((next: boolean) => {\n setOpenState(next);\n if (!next) setActiveValue(null);\n }, []);\n\n useEffect(() => {\n if (!open) return;\n const onPointerDown = (e: PointerEvent) => {\n const target = e.target as Node;\n if (!triggerRef.current?.contains(target) && !listboxRef.current?.contains(target)) {\n setOpen(false);\n }\n };\n document.addEventListener(\"pointerdown\", onPointerDown);\n return () => document.removeEventListener(\"pointerdown\", onPointerDown);\n }, [open, setOpen]);\n\n const ctxValue = useMemo<SelectContextValue>(\n () => ({\n open,\n setOpen,\n value,\n onValueChange,\n activeValue,\n setActiveValue,\n triggerId,\n listboxId,\n triggerRef,\n listboxRef,\n getOptionId,\n getOrderedOptions,\n }),\n [\n open,\n setOpen,\n value,\n onValueChange,\n activeValue,\n triggerId,\n listboxId,\n getOptionId,\n getOrderedOptions,\n ]\n );\n\n return <SelectContext.Provider value={ctxValue}>{children}</SelectContext.Provider>;\n};\n\ninterface TriggerProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"children\"> {\n children?: Renderable<{ open: boolean; value: string }>;\n}\n\nconst Trigger = forwardRef<HTMLButtonElement, TriggerProps>(function Trigger(\n { children, onClick, onKeyDown, ...rest },\n forwardedRef\n) {\n const ctx = useSelectContext(\"Trigger\");\n\n const setRefs = (node: HTMLButtonElement | null) => {\n ctx.triggerRef.current = node;\n if (typeof forwardedRef === \"function\") forwardedRef(node);\n else if (forwardedRef) forwardedRef.current = node;\n };\n\n const handleClick = (e: ReactMouseEvent<HTMLButtonElement>) => {\n onClick?.(e);\n if (e.defaultPrevented) return;\n ctx.setOpen(!ctx.open);\n };\n\n const handleKeyDown = (e: ReactKeyboardEvent<HTMLButtonElement>) => {\n onKeyDown?.(e);\n if (e.defaultPrevented) return;\n if (e.key === \" \" || e.key === \"Enter\" || e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n ctx.setOpen(true);\n }\n };\n\n return (\n <button\n ref={setRefs}\n type=\"button\"\n id={ctx.triggerId}\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-expanded={ctx.open}\n aria-controls={ctx.listboxId}\n onClick={handleClick}\n onKeyDown={handleKeyDown}\n {...rest}\n >\n {render(children, { open: ctx.open, value: ctx.value })}\n </button>\n );\n});\n\ninterface ContentProps extends HTMLAttributes<HTMLDivElement> {\n children: ReactNode;\n}\n\nconst TYPEAHEAD_TIMEOUT_MS = 500;\n\nconst Content = ({ children, onKeyDown, ...rest }: ContentProps) => {\n const ctx = useSelectContext(\"Content\");\n const typeaheadRef = useRef({ buffer: \"\", lastTime: 0 });\n\n useEffect(() => {\n if (!ctx.open) return;\n const options = ctx.getOrderedOptions();\n if (options.length === 0) {\n ctx.listboxRef.current?.focus();\n return;\n }\n const initial = options.find((o) => o.value === ctx.value)?.value ?? options[0]!.value;\n ctx.setActiveValue(initial);\n ctx.listboxRef.current?.focus();\n // only re-run on open transition\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [ctx.open]);\n\n if (!ctx.open) return null;\n\n const moveActive = (delta: 1 | -1 | \"first\" | \"last\") => {\n const options = ctx.getOrderedOptions();\n if (options.length === 0) return;\n const currentIdx = options.findIndex((o) => o.value === ctx.activeValue);\n let nextIdx: number;\n if (delta === \"first\") nextIdx = 0;\n else if (delta === \"last\") nextIdx = options.length - 1;\n else if (currentIdx === -1) nextIdx = delta === 1 ? 0 : options.length - 1;\n else nextIdx = Math.min(options.length - 1, Math.max(0, currentIdx + delta));\n ctx.setActiveValue(options[nextIdx]!.value);\n };\n\n const typeahead = (char: string) => {\n const now = Date.now();\n const reset = now - typeaheadRef.current.lastTime > TYPEAHEAD_TIMEOUT_MS;\n const buffer = (reset ? \"\" : typeaheadRef.current.buffer) + char.toLowerCase();\n typeaheadRef.current = { buffer, lastTime: now };\n const options = ctx.getOrderedOptions();\n const match = options.find((o) => o.textValue.toLowerCase().startsWith(buffer));\n if (match) ctx.setActiveValue(match.value);\n };\n\n const handleKeyDown = (e: ReactKeyboardEvent<HTMLDivElement>) => {\n onKeyDown?.(e);\n if (e.defaultPrevented) return;\n\n switch (e.key) {\n case \"ArrowDown\":\n e.preventDefault();\n moveActive(1);\n return;\n case \"ArrowUp\":\n e.preventDefault();\n moveActive(-1);\n return;\n case \"Home\":\n e.preventDefault();\n moveActive(\"first\");\n return;\n case \"End\":\n e.preventDefault();\n moveActive(\"last\");\n return;\n case \"Enter\":\n case \" \":\n e.preventDefault();\n if (ctx.activeValue !== null) {\n ctx.onValueChange(ctx.activeValue);\n ctx.setOpen(false);\n ctx.triggerRef.current?.focus();\n }\n return;\n case \"Escape\":\n e.preventDefault();\n ctx.setOpen(false);\n ctx.triggerRef.current?.focus();\n return;\n case \"Tab\":\n ctx.setOpen(false);\n return;\n default:\n if (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) {\n typeahead(e.key);\n }\n }\n };\n\n return (\n <div\n ref={(node) => {\n ctx.listboxRef.current = node;\n }}\n id={ctx.listboxId}\n role=\"listbox\"\n tabIndex={-1}\n aria-labelledby={ctx.triggerId}\n aria-activedescendant={\n ctx.activeValue !== null ? ctx.getOptionId(ctx.activeValue) : undefined\n }\n onKeyDown={handleKeyDown}\n {...rest}\n >\n {children}\n </div>\n );\n};\n\ninterface OptionProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n value: string;\n textValue?: string;\n children?: Renderable<{ selected: boolean; active: boolean }>;\n}\n\nconst Option = ({ value, textValue, children, onMouseEnter, onClick, ...rest }: OptionProps) => {\n const ctx = useSelectContext(\"Option\");\n const selected = ctx.value === value;\n const active = ctx.activeValue === value;\n\n const handleClick = (e: ReactMouseEvent<HTMLDivElement>) => {\n onClick?.(e);\n if (e.defaultPrevented) return;\n ctx.onValueChange(value);\n ctx.setOpen(false);\n ctx.triggerRef.current?.focus();\n };\n\n const handleMouseEnter = (e: ReactMouseEvent<HTMLDivElement>) => {\n onMouseEnter?.(e);\n if (e.defaultPrevented) return;\n ctx.setActiveValue(value);\n };\n\n return (\n <div\n id={ctx.getOptionId(value)}\n role=\"option\"\n aria-selected={selected}\n data-value={value}\n data-text-value={textValue}\n data-active={active || undefined}\n data-selected={selected || undefined}\n onClick={handleClick}\n onMouseEnter={handleMouseEnter}\n {...rest}\n >\n {render(children, { selected, active })}\n </div>\n );\n};\n\nexport const Select = Object.assign(Root, {\n Trigger,\n Content,\n Option,\n});\n\nexport type { SelectProps, TriggerProps, ContentProps, OptionProps };\n"]}
package/package.json ADDED
@@ -0,0 +1,103 @@
1
+ {
2
+ "name": "@spacing-ui/core",
3
+ "version": "0.1.0",
4
+ "description": "Headless, accessible React UI primitives.",
5
+ "license": "MIT",
6
+ "author": "Nhan Nguyen <nhan13574@gmail.com>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/nathannewyen/space-ui.git"
10
+ },
11
+ "homepage": "https://github.com/nathannewyen/space-ui#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/nathannewyen/space-ui/issues"
14
+ },
15
+ "type": "module",
16
+ "sideEffects": false,
17
+ "main": "./dist/index.cjs",
18
+ "module": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js",
24
+ "require": "./dist/index.cjs"
25
+ }
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "scripts": {
33
+ "build": "tsup",
34
+ "dev": "tsup --watch",
35
+ "test": "vitest run",
36
+ "test:watch": "vitest",
37
+ "typecheck": "tsc --noEmit",
38
+ "lint": "eslint .",
39
+ "lint:fix": "eslint . --fix",
40
+ "format": "prettier --write \"**/*.{ts,tsx,js,mjs,json,md,yml,yaml}\"",
41
+ "format:check": "prettier --check \"**/*.{ts,tsx,js,mjs,json,md,yml,yaml}\"",
42
+ "clean": "rm -rf dist",
43
+ "prepare": "husky",
44
+ "prepublishOnly": "pnpm clean && pnpm lint && pnpm typecheck && pnpm test && pnpm build"
45
+ },
46
+ "lint-staged": {
47
+ "*.{ts,tsx}": [
48
+ "eslint --fix",
49
+ "prettier --write"
50
+ ],
51
+ "*.{js,mjs,json,md,yml,yaml}": [
52
+ "prettier --write"
53
+ ]
54
+ },
55
+ "peerDependencies": {
56
+ "react": "^18.0.0 || ^19.0.0",
57
+ "react-dom": "^18.0.0 || ^19.0.0"
58
+ },
59
+ "devDependencies": {
60
+ "@eslint/js": "^10.0.1",
61
+ "@testing-library/jest-dom": "^6.6.3",
62
+ "@testing-library/react": "^16.1.0",
63
+ "@testing-library/user-event": "^14.5.2",
64
+ "@types/node": "^22.10.5",
65
+ "@types/react": "^19.0.7",
66
+ "@types/react-dom": "^19.0.3",
67
+ "@typescript-eslint/eslint-plugin": "^8.20.0",
68
+ "@typescript-eslint/parser": "^8.20.0",
69
+ "eslint": "^9.18.0",
70
+ "eslint-config-prettier": "^10.1.5",
71
+ "eslint-plugin-react": "^7.37.4",
72
+ "eslint-plugin-react-hooks": "^5.1.0",
73
+ "husky": "^9.1.7",
74
+ "jsdom": "^26.0.0",
75
+ "lint-staged": "^16.2.0",
76
+ "prettier": "^3.4.2",
77
+ "react": "^19.0.0",
78
+ "react-dom": "^19.0.0",
79
+ "tsup": "^8.3.5",
80
+ "typescript": "^5.7.3",
81
+ "vitest": "^2.1.8"
82
+ },
83
+ "keywords": [
84
+ "react",
85
+ "ui",
86
+ "headless",
87
+ "accessible",
88
+ "a11y",
89
+ "components",
90
+ "primitives"
91
+ ],
92
+ "engines": {
93
+ "node": ">=18"
94
+ },
95
+ "publishConfig": {
96
+ "access": "public"
97
+ },
98
+ "pnpm": {
99
+ "onlyBuiltDependencies": [
100
+ "esbuild"
101
+ ]
102
+ }
103
+ }