@domglyph/primitives 2.0.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 llcortex
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,224 @@
1
+ # @domglyph/primitives (formerly [@cortexui/primitives](https://www.npmjs.com/package/@cortexui/primitives))
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@domglyph/primitives?color=0ea5e9)](https://www.npmjs.com/package/@domglyph/primitives)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](../../LICENSE)
5
+
6
+ Low-level accessible UI primitives. The foundation of DOMglyph components.
7
+
8
+ ---
9
+
10
+ ## Overview
11
+
12
+ `@domglyph/primitives` provides the base layer of the DOMglyph architecture. Understanding the three-layer model helps clarify where each package fits:
13
+
14
+ ```
15
+ ┌─────────────────────────────────────┐
16
+ │ @domglyph/components │ ← AI contract + visual design
17
+ │ ActionButton, FormField, DataTable │
18
+ ├─────────────────────────────────────┤
19
+ │ @domglyph/primitives │ ← Behavior + accessibility
20
+ │ Box, Stack, Text, ButtonBase, … │
21
+ ├─────────────────────────────────────┤
22
+ │ HTML elements + ARIA │ ← DOM
23
+ └─────────────────────────────────────┘
24
+ ```
25
+
26
+ **Primitives handle behavior and accessibility.** They manage focus management, keyboard interaction, ARIA roles, and layout — the hard, invisible work that makes a UI actually accessible.
27
+
28
+ **Primitives do NOT add `data-ai-*` attributes.** That is the responsibility of the component layer. This separation means primitives are reusable for any purpose, while components carry the full AI contract.
29
+
30
+ ---
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ npm install @domglyph/primitives
36
+ ```
37
+
38
+ Peer dependencies:
39
+
40
+ ```bash
41
+ npm install react@^18 react-dom@^18
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Primitives
47
+
48
+ ### Box
49
+
50
+ A polymorphic container element. Renders as any HTML element via the `as` prop, defaulting to `div`. The base building block for layout.
51
+
52
+ ```tsx
53
+ import { Box } from '@domglyph/primitives';
54
+
55
+ <Box as="section" style={{ padding: '24px' }}>
56
+ Page content here
57
+ </Box>
58
+
59
+ <Box as="article" style={{ maxWidth: '640px', margin: '0 auto' }}>
60
+ Article content
61
+ </Box>
62
+
63
+ <Box as="header">
64
+ Site header
65
+ </Box>
66
+ ```
67
+
68
+ `Box` forwards all standard HTML attributes and refs. It has no opinion about styling.
69
+
70
+ ---
71
+
72
+ ### Stack
73
+
74
+ A flexbox layout primitive for stacking children vertically or horizontally with consistent spacing.
75
+
76
+ ```tsx
77
+ import { Stack } from '@domglyph/primitives';
78
+
79
+ // Vertical stack with gap
80
+ <Stack direction="column" gap="16px">
81
+ <FormField id="name" fieldType="text" label="Full name" />
82
+ <FormField id="email" fieldType="email" label="Email" />
83
+ <ActionButton action="submit-form" state="idle" label="Submit" />
84
+ </Stack>
85
+
86
+ // Horizontal stack
87
+ <Stack direction="row" gap="8px" align="center">
88
+ <StatusIcon />
89
+ <Text size="sm">3 items selected</Text>
90
+ </Stack>
91
+ ```
92
+
93
+ ---
94
+
95
+ ### Text
96
+
97
+ A semantic typography primitive. Renders as any text-level HTML element via the `as` prop. Use it for labels, headings, captions, and body copy.
98
+
99
+ ```tsx
100
+ import { Text } from '@domglyph/primitives';
101
+
102
+ // Form label
103
+ <Text as="label" size="sm" weight="medium">
104
+ Full name
105
+ </Text>
106
+
107
+ // Heading
108
+ <Text as="h2" size="xl" weight="bold">
109
+ Account settings
110
+ </Text>
111
+
112
+ // Caption
113
+ <Text as="span" size="xs" color="muted">
114
+ Last updated 2 minutes ago
115
+ </Text>
116
+ ```
117
+
118
+ ---
119
+
120
+ ### ButtonBase
121
+
122
+ An accessible button primitive with no visual styling. Handles keyboard events (`Enter`, `Space`), ARIA attributes, and disabled state management. The foundation of `ActionButton`.
123
+
124
+ When building custom components on `ButtonBase`, you are responsible for adding `data-ai-*` attributes:
125
+
126
+ ```tsx
127
+ import { ButtonBase } from '@domglyph/primitives';
128
+
129
+ <ButtonBase
130
+ onClick={handleClick}
131
+ disabled={isDisabled}
132
+ aria-label="Close dialog"
133
+ data-ai-role="action"
134
+ data-ai-id="close-dialog"
135
+ data-ai-action="close-dialog"
136
+ data-ai-state="idle"
137
+ >
138
+ <CloseIcon />
139
+ </ButtonBase>
140
+ ```
141
+
142
+ `ButtonBase` always renders as a native `<button>` element. It does not use `role="button"` on a `<div>`. This ensures correct keyboard behaviour, screen reader support, and form interaction without extra work.
143
+
144
+ ---
145
+
146
+ ### InputBase
147
+
148
+ An accessible input primitive with no visual styling. Handles value state, change events, and ARIA attributes for error and required states. The foundation of `FormField`.
149
+
150
+ ```tsx
151
+ import { InputBase } from '@domglyph/primitives';
152
+
153
+ <InputBase
154
+ type="email"
155
+ value={value}
156
+ onChange={(e) => setValue(e.target.value)}
157
+ aria-label="Email address"
158
+ aria-required="true"
159
+ aria-invalid={hasError}
160
+ data-ai-role="field"
161
+ data-ai-id="user-email"
162
+ data-ai-field-type="email"
163
+ data-ai-required="true"
164
+ data-ai-state={hasError ? 'error' : 'idle'}
165
+ />
166
+ ```
167
+
168
+ ---
169
+
170
+ ### DialogBase
171
+
172
+ An accessible dialog primitive with focus trapping, scroll locking, and `Escape` key dismissal. Renders as a native `<dialog>` element. The foundation of `ConfirmDialog`.
173
+
174
+ ```tsx
175
+ import { DialogBase } from '@domglyph/primitives';
176
+
177
+ <DialogBase
178
+ open={isOpen}
179
+ onClose={handleClose}
180
+ aria-labelledby="dialog-title"
181
+ aria-describedby="dialog-description"
182
+ data-ai-role="modal"
183
+ data-ai-id="confirm-delete"
184
+ data-ai-state={isOpen ? 'expanded' : 'idle'}
185
+ >
186
+ <h2 id="dialog-title">Confirm deletion</h2>
187
+ <p id="dialog-description">This cannot be undone.</p>
188
+ <ButtonBase onClick={handleClose}>Cancel</ButtonBase>
189
+ <ButtonBase onClick={handleConfirm}>Confirm</ButtonBase>
190
+ </DialogBase>
191
+ ```
192
+
193
+ `DialogBase` traps focus within the dialog while it is open and restores focus to the trigger element when it closes.
194
+
195
+ ---
196
+
197
+ ## When to use primitives directly
198
+
199
+ Use `@domglyph/primitives` directly when:
200
+
201
+ - You are building a **custom component** that needs DOMglyph's accessibility guarantees but has unique visual requirements not served by the components package
202
+ - You need a **polymorphic container** (`Box`) or **layout primitive** (`Stack`) without pulling in a full component
203
+
204
+ When you build on primitives directly, **you are responsible for adding `data-ai-*` attributes** to make the component inspectable by the AI runtime. See [`@domglyph/ai-contract`](../ai-contract/README.md) for the full attribute specification and [`@domglyph/testing`](../testing/README.md) for validation utilities to use in your tests.
205
+
206
+ ---
207
+
208
+ ## Part of DOMglyph
209
+
210
+ `@domglyph/primitives` is part of the [DOMglyph](../../README.md) design system.
211
+
212
+ - [Main repository](../../README.md)
213
+ - [Documentation](http://localhost:3001/docs/primitives)
214
+ - [Contributing](../../CONTRIBUTING.md)
215
+
216
+ ---
217
+
218
+ ## ☕ Support
219
+
220
+ If you find DOMglyph useful, you can support the project:
221
+
222
+ 👉 https://buymeacoffee.com/nishchya
223
+
224
+ It helps keep the project alive and growing.
@@ -0,0 +1,108 @@
1
+ import * as react from 'react';
2
+ import { ElementType, ComponentPropsWithoutRef, ReactNode, CSSProperties, ComponentPropsWithRef, RefObject, AriaAttributes, createElement, ReactPortal, ReactElement } from 'react';
3
+
4
+ type PrimitiveElement = ElementType;
5
+ type PolymorphicRef<C extends PrimitiveElement> = ComponentPropsWithRef<C>["ref"];
6
+ type PolymorphicProps<C extends PrimitiveElement, Props = {}> = Props & Omit<ComponentPropsWithoutRef<C>, keyof Props | "as"> & {
7
+ as?: C;
8
+ };
9
+ interface PrimitiveStyleProps {
10
+ readonly className?: string;
11
+ readonly children?: ReactNode;
12
+ }
13
+ interface PrimitiveBoxProps extends PrimitiveStyleProps {
14
+ readonly style?: CSSProperties;
15
+ }
16
+ interface SurfaceDescriptor {
17
+ readonly id: string;
18
+ readonly tokenNamespace: string;
19
+ }
20
+ interface DialogBaseOwnProps extends PrimitiveStyleProps {
21
+ readonly open: boolean;
22
+ readonly onOpenChange?: (open: boolean) => void;
23
+ readonly ariaLabel?: string;
24
+ readonly ariaLabelledBy?: string;
25
+ readonly ariaDescribedBy?: string;
26
+ readonly closeOnEscape?: boolean;
27
+ readonly closeOnInteractOutside?: boolean;
28
+ readonly initialFocusRef?: RefObject<HTMLElement>;
29
+ }
30
+ type DialogBaseElementProps = Omit<ComponentPropsWithoutRef<"div">, keyof DialogBaseOwnProps | "children">;
31
+ type DialogBaseProps = DialogBaseOwnProps & DialogBaseElementProps;
32
+ interface PortalProps {
33
+ readonly children?: ReactNode;
34
+ readonly container?: Element | DocumentFragment | null;
35
+ }
36
+ type InputBaseProps = Omit<ComponentPropsWithoutRef<"input">, "size"> & PrimitiveStyleProps & {
37
+ readonly invalid?: boolean;
38
+ };
39
+ type ButtonBaseProps = ComponentPropsWithoutRef<"button"> & PrimitiveStyleProps & {
40
+ readonly loading?: boolean;
41
+ readonly loadingLabel?: string;
42
+ };
43
+ interface StackOwnProps extends PrimitiveBoxProps {
44
+ readonly direction?: "row" | "column";
45
+ readonly gap?: number | string;
46
+ readonly align?: CSSProperties["alignItems"];
47
+ readonly justify?: CSSProperties["justifyContent"];
48
+ }
49
+ interface TextOwnProps extends PrimitiveBoxProps {
50
+ readonly tone?: "default" | "muted" | "danger" | "success";
51
+ readonly visuallyHidden?: boolean;
52
+ }
53
+ type PrimitiveAriaProps = AriaAttributes;
54
+
55
+ type BoxComponent = <C extends ElementType = "div">(props: PolymorphicProps<C, PrimitiveBoxProps> & {
56
+ ref?: PolymorphicRef<C>;
57
+ }) => ReturnType<typeof createElement>;
58
+ declare const Box: BoxComponent & {
59
+ displayName?: string;
60
+ };
61
+
62
+ declare const ButtonBase: react.ForwardRefExoticComponent<Omit<react.DetailedHTMLProps<react.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "ref"> & PrimitiveStyleProps & {
63
+ readonly loading?: boolean;
64
+ readonly loadingLabel?: string;
65
+ } & react.RefAttributes<HTMLButtonElement>>;
66
+
67
+ declare const DialogBase: react.ForwardRefExoticComponent<DialogBaseOwnProps & DialogBaseElementProps & react.RefAttributes<HTMLDivElement>>;
68
+
69
+ declare const InputBase: react.ForwardRefExoticComponent<Omit<Omit<react.DetailedHTMLProps<react.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "ref">, "size"> & PrimitiveStyleProps & {
70
+ readonly invalid?: boolean;
71
+ } & react.RefAttributes<HTMLInputElement>>;
72
+
73
+ declare function Portal({ children, container }: PortalProps): ReactPortal | null;
74
+
75
+ type StackComponent = <C extends ElementType = "div">(props: PolymorphicProps<C, StackOwnProps> & {
76
+ ref?: PolymorphicRef<C>;
77
+ }) => ReactElement | null;
78
+ declare const Stack: StackComponent & {
79
+ displayName?: string;
80
+ };
81
+
82
+ type TextComponent = <C extends ElementType = "span">(props: PolymorphicProps<C, TextOwnProps> & {
83
+ ref?: PolymorphicRef<C>;
84
+ }) => ReactElement | null;
85
+ declare const Text: TextComponent & {
86
+ displayName?: string;
87
+ };
88
+
89
+ declare const primitiveVars: {
90
+ readonly borderColor: "--domglyph-border-color";
91
+ readonly dangerColor: "--domglyph-danger-color";
92
+ readonly focusRing: "--domglyph-focus-ring";
93
+ readonly foreground: "--domglyph-foreground";
94
+ readonly mutedForeground: "--domglyph-muted-foreground";
95
+ readonly radius: "--domglyph-radius";
96
+ readonly spacing: "--domglyph-spacing";
97
+ readonly successColor: "--domglyph-success-color";
98
+ readonly surface: "--domglyph-surface";
99
+ };
100
+ declare const primitiveTheme: CSSProperties;
101
+
102
+ /**
103
+ * Backwards-compatible descriptor retained while higher-level packages migrate
104
+ * to the React primitive exports introduced in this package.
105
+ */
106
+ declare const primitiveSurface: SurfaceDescriptor;
107
+
108
+ export { Box, ButtonBase, type ButtonBaseProps, DialogBase, type DialogBaseProps, InputBase, type InputBaseProps, type PolymorphicProps, Portal, type PortalProps, type PrimitiveAriaProps, type PrimitiveBoxProps, type PrimitiveElement, type PrimitiveStyleProps, Stack, type StackOwnProps, type SurfaceDescriptor, Text, type TextOwnProps, primitiveSurface, primitiveTheme, primitiveVars };
package/dist/index.js ADDED
@@ -0,0 +1,416 @@
1
+ // src/Box.tsx
2
+ import {
3
+ createElement,
4
+ forwardRef
5
+ } from "react";
6
+
7
+ // src/styles.ts
8
+ import { colorTokens } from "@domglyph/tokens";
9
+ var primitiveVars = {
10
+ borderColor: "--domglyph-border-color",
11
+ dangerColor: "--domglyph-danger-color",
12
+ focusRing: "--domglyph-focus-ring",
13
+ foreground: "--domglyph-foreground",
14
+ mutedForeground: "--domglyph-muted-foreground",
15
+ radius: "--domglyph-radius",
16
+ spacing: "--domglyph-spacing",
17
+ successColor: "--domglyph-success-color",
18
+ surface: "--domglyph-surface"
19
+ };
20
+ var primitiveTheme = {
21
+ [primitiveVars.surface]: colorTokens.values.surface,
22
+ [primitiveVars.foreground]: colorTokens.values.accent,
23
+ [primitiveVars.borderColor]: "rgba(17, 24, 39, 0.16)",
24
+ [primitiveVars.focusRing]: "rgba(17, 24, 39, 0.24)",
25
+ [primitiveVars.mutedForeground]: "rgba(17, 24, 39, 0.7)",
26
+ [primitiveVars.dangerColor]: "#b91c1c",
27
+ [primitiveVars.successColor]: "#15803d",
28
+ [primitiveVars.radius]: "12px",
29
+ [primitiveVars.spacing]: "0.75rem"
30
+ };
31
+ var visuallyHiddenStyle = {
32
+ border: 0,
33
+ clip: "rect(0 0 0 0)",
34
+ height: "1px",
35
+ margin: "-1px",
36
+ overflow: "hidden",
37
+ padding: 0,
38
+ position: "absolute",
39
+ whiteSpace: "nowrap",
40
+ width: "1px"
41
+ };
42
+
43
+ // src/Box.tsx
44
+ var defaultBoxStyle = {
45
+ boxSizing: "border-box"
46
+ };
47
+ var BoxImpl = ({ as, children, className, style, ...rest }, ref) => {
48
+ const Component = as ?? "div";
49
+ return createElement(
50
+ Component,
51
+ {
52
+ ...rest,
53
+ className,
54
+ ref,
55
+ style: {
56
+ ...primitiveTheme,
57
+ ...defaultBoxStyle,
58
+ ...style
59
+ }
60
+ },
61
+ children
62
+ );
63
+ };
64
+ var BoxWithRef = forwardRef(BoxImpl);
65
+ BoxWithRef.displayName = "Box";
66
+ var Box = BoxWithRef;
67
+
68
+ // src/ButtonBase.tsx
69
+ import { forwardRef as forwardRef2 } from "react";
70
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
71
+ var baseButtonStyle = {
72
+ alignItems: "center",
73
+ appearance: "none",
74
+ background: `var(${primitiveVars.foreground})`,
75
+ border: `1px solid var(${primitiveVars.foreground})`,
76
+ borderRadius: `var(${primitiveVars.radius})`,
77
+ color: `var(${primitiveVars.surface})`,
78
+ cursor: "pointer",
79
+ display: "inline-flex",
80
+ font: "inherit",
81
+ fontWeight: 600,
82
+ gap: "0.5rem",
83
+ justifyContent: "center",
84
+ minHeight: "2.5rem",
85
+ padding: "0.625rem 1rem",
86
+ transition: "opacity 120ms ease, transform 120ms ease"
87
+ };
88
+ var spinnerStyle = {
89
+ animation: "domglyph-spin 0.8s linear infinite",
90
+ border: "2px solid currentColor",
91
+ borderBottomColor: "transparent",
92
+ borderRadius: "999px",
93
+ display: "inline-block",
94
+ height: "0.9rem",
95
+ width: "0.9rem"
96
+ };
97
+ var ButtonBase = forwardRef2(
98
+ ({
99
+ children,
100
+ className,
101
+ disabled = false,
102
+ loading = false,
103
+ loadingLabel = "Loading",
104
+ style,
105
+ type = "button",
106
+ ...rest
107
+ }, ref) => {
108
+ const isDisabled = disabled || loading;
109
+ return /* @__PURE__ */ jsx(
110
+ "button",
111
+ {
112
+ ...rest,
113
+ "aria-busy": loading || void 0,
114
+ className,
115
+ disabled: isDisabled,
116
+ ref,
117
+ style: {
118
+ ...baseButtonStyle,
119
+ opacity: isDisabled ? 0.64 : 1,
120
+ ...style
121
+ },
122
+ type,
123
+ children: loading ? /* @__PURE__ */ jsxs(Fragment, { children: [
124
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: spinnerStyle }),
125
+ /* @__PURE__ */ jsx("span", { children: loadingLabel })
126
+ ] }) : children
127
+ }
128
+ );
129
+ }
130
+ );
131
+ ButtonBase.displayName = "ButtonBase";
132
+
133
+ // src/DialogBase.tsx
134
+ import {
135
+ forwardRef as forwardRef3,
136
+ useEffect,
137
+ useId,
138
+ useRef
139
+ } from "react";
140
+
141
+ // src/Portal.tsx
142
+ import { createPortal } from "react-dom";
143
+ function Portal({ children, container }) {
144
+ if (typeof document === "undefined") {
145
+ return null;
146
+ }
147
+ return createPortal(children, container ?? document.body);
148
+ }
149
+
150
+ // src/DialogBase.tsx
151
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
152
+ var overlayStyle = {
153
+ alignItems: "center",
154
+ background: "rgba(17, 24, 39, 0.48)",
155
+ display: "flex",
156
+ inset: 0,
157
+ justifyContent: "center",
158
+ padding: "1.5rem",
159
+ position: "fixed",
160
+ zIndex: 1e3
161
+ };
162
+ var dialogStyle = {
163
+ background: `var(${primitiveVars.surface})`,
164
+ border: `1px solid var(${primitiveVars.borderColor})`,
165
+ borderRadius: `var(${primitiveVars.radius})`,
166
+ boxShadow: "0 24px 80px rgba(15, 23, 42, 0.24)",
167
+ color: `var(${primitiveVars.foreground})`,
168
+ maxWidth: "40rem",
169
+ outline: "none",
170
+ padding: "1.25rem",
171
+ width: "100%"
172
+ };
173
+ function assignRef(ref, value) {
174
+ if (typeof ref === "function") {
175
+ ref(value);
176
+ return;
177
+ }
178
+ if (ref && "current" in ref) {
179
+ ref.current = value;
180
+ }
181
+ }
182
+ var DialogBase = forwardRef3(
183
+ ({
184
+ ariaDescribedBy,
185
+ ariaLabel,
186
+ ariaLabelledBy,
187
+ children,
188
+ className,
189
+ closeOnEscape = true,
190
+ closeOnInteractOutside = true,
191
+ initialFocusRef,
192
+ onOpenChange,
193
+ open,
194
+ ...rest
195
+ }, ref) => {
196
+ const fallbackFocusRef = useRef(null);
197
+ const generatedLabelId = useId();
198
+ const lastActiveElementRef = useRef(null);
199
+ useEffect(() => {
200
+ if (!open || typeof document === "undefined") {
201
+ return;
202
+ }
203
+ lastActiveElementRef.current = document.activeElement;
204
+ const target = initialFocusRef?.current ?? fallbackFocusRef.current;
205
+ target?.focus();
206
+ return () => {
207
+ if (lastActiveElementRef.current instanceof HTMLElement) {
208
+ lastActiveElementRef.current.focus();
209
+ }
210
+ };
211
+ }, [initialFocusRef, open]);
212
+ useEffect(() => {
213
+ if (!open || !closeOnEscape || typeof document === "undefined") {
214
+ return;
215
+ }
216
+ const handleKeyDown = (event) => {
217
+ if (event.key === "Escape") {
218
+ onOpenChange?.(false);
219
+ }
220
+ };
221
+ document.addEventListener("keydown", handleKeyDown);
222
+ return () => {
223
+ document.removeEventListener("keydown", handleKeyDown);
224
+ };
225
+ }, [closeOnEscape, onOpenChange, open]);
226
+ if (!open) {
227
+ return null;
228
+ }
229
+ const labelId = ariaLabel ? void 0 : ariaLabelledBy ?? generatedLabelId;
230
+ const handleOverlayKeyDown = (event) => {
231
+ if (event.key === "Escape" && closeOnEscape) {
232
+ event.stopPropagation();
233
+ onOpenChange?.(false);
234
+ }
235
+ };
236
+ return /* @__PURE__ */ jsx2(Portal, { children: /* @__PURE__ */ jsx2(
237
+ Box,
238
+ {
239
+ "aria-hidden": false,
240
+ onClick: closeOnInteractOutside ? () => {
241
+ onOpenChange?.(false);
242
+ } : void 0,
243
+ onKeyDown: handleOverlayKeyDown,
244
+ style: overlayStyle,
245
+ children: /* @__PURE__ */ jsxs2(
246
+ Box,
247
+ {
248
+ ...rest,
249
+ "aria-describedby": ariaDescribedBy,
250
+ "aria-label": ariaLabel,
251
+ "aria-labelledby": labelId,
252
+ "aria-modal": "true",
253
+ className,
254
+ onClick: (event) => {
255
+ event.stopPropagation();
256
+ },
257
+ ref: (node) => {
258
+ fallbackFocusRef.current = node;
259
+ assignRef(ref, node);
260
+ },
261
+ role: "dialog",
262
+ style: dialogStyle,
263
+ tabIndex: -1,
264
+ children: [
265
+ !ariaLabel && !ariaLabelledBy ? /* @__PURE__ */ jsx2("div", { id: generatedLabelId, style: { display: "none" }, children: "Dialog" }) : null,
266
+ children
267
+ ]
268
+ }
269
+ )
270
+ }
271
+ ) });
272
+ }
273
+ );
274
+ DialogBase.displayName = "DialogBase";
275
+
276
+ // src/InputBase.tsx
277
+ import { forwardRef as forwardRef4 } from "react";
278
+ import { jsx as jsx3 } from "react/jsx-runtime";
279
+ var inputBaseStyle = {
280
+ appearance: "none",
281
+ background: `var(${primitiveVars.surface})`,
282
+ border: `1px solid var(${primitiveVars.borderColor})`,
283
+ borderRadius: `var(${primitiveVars.radius})`,
284
+ color: `var(${primitiveVars.foreground})`,
285
+ font: "inherit",
286
+ minHeight: "2.5rem",
287
+ outline: "none",
288
+ padding: "0.625rem 0.875rem",
289
+ width: "100%"
290
+ };
291
+ var InputBase = forwardRef4(
292
+ ({ className, invalid = false, style, ...rest }, ref) => /* @__PURE__ */ jsx3(
293
+ "input",
294
+ {
295
+ ...rest,
296
+ "aria-invalid": invalid || rest["aria-invalid"] ? true : void 0,
297
+ className,
298
+ ref,
299
+ style: {
300
+ ...inputBaseStyle,
301
+ borderColor: invalid ? `var(${primitiveVars.dangerColor})` : `var(${primitiveVars.borderColor})`,
302
+ boxShadow: invalid ? `0 0 0 3px color-mix(in srgb, var(${primitiveVars.dangerColor}) 16%, transparent)` : void 0,
303
+ ...style
304
+ }
305
+ }
306
+ )
307
+ );
308
+ InputBase.displayName = "InputBase";
309
+
310
+ // src/Stack.tsx
311
+ import {
312
+ forwardRef as forwardRef5
313
+ } from "react";
314
+ import { jsx as jsx4 } from "react/jsx-runtime";
315
+ var StackImpl = ({
316
+ align,
317
+ as,
318
+ children,
319
+ className,
320
+ direction = "column",
321
+ gap = "var(--cortexui-spacing)",
322
+ justify,
323
+ style,
324
+ ...rest
325
+ }, ref) => {
326
+ const stackStyle = {
327
+ alignItems: align,
328
+ display: "flex",
329
+ flexDirection: direction,
330
+ gap,
331
+ justifyContent: justify,
332
+ ...style
333
+ };
334
+ return /* @__PURE__ */ jsx4(
335
+ Box,
336
+ {
337
+ ...rest,
338
+ as,
339
+ className,
340
+ ref,
341
+ style: stackStyle,
342
+ children
343
+ }
344
+ );
345
+ };
346
+ var StackWithRef = forwardRef5(StackImpl);
347
+ StackWithRef.displayName = "Stack";
348
+ var Stack = StackWithRef;
349
+
350
+ // src/Text.tsx
351
+ import {
352
+ forwardRef as forwardRef6
353
+ } from "react";
354
+ import { jsx as jsx5 } from "react/jsx-runtime";
355
+ var textToneStyles = {
356
+ default: {
357
+ color: `var(${primitiveVars.foreground})`
358
+ },
359
+ muted: {
360
+ color: `var(${primitiveVars.mutedForeground})`
361
+ },
362
+ danger: {
363
+ color: `var(${primitiveVars.dangerColor})`
364
+ },
365
+ success: {
366
+ color: `var(${primitiveVars.successColor})`
367
+ }
368
+ };
369
+ var TextImpl = ({
370
+ as,
371
+ children,
372
+ className,
373
+ style,
374
+ tone = "default",
375
+ visuallyHidden = false,
376
+ ...rest
377
+ }, ref) => /* @__PURE__ */ jsx5(
378
+ Box,
379
+ {
380
+ ...rest,
381
+ as: as ?? "span",
382
+ className,
383
+ ref,
384
+ style: {
385
+ fontFamily: "inherit",
386
+ lineHeight: 1.5,
387
+ margin: 0,
388
+ ...textToneStyles[tone],
389
+ ...visuallyHidden ? visuallyHiddenStyle : void 0,
390
+ ...style
391
+ },
392
+ children
393
+ }
394
+ );
395
+ var TextWithRef = forwardRef6(TextImpl);
396
+ TextWithRef.displayName = "Text";
397
+ var Text = TextWithRef;
398
+
399
+ // src/index.ts
400
+ import { colorTokens as colorTokens2 } from "@domglyph/tokens";
401
+ var primitiveSurface = {
402
+ id: "surface",
403
+ tokenNamespace: colorTokens2.name
404
+ };
405
+ export {
406
+ Box,
407
+ ButtonBase,
408
+ DialogBase,
409
+ InputBase,
410
+ Portal,
411
+ Stack,
412
+ Text,
413
+ primitiveSurface,
414
+ primitiveTheme,
415
+ primitiveVars
416
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@domglyph/primitives",
3
+ "version": "2.0.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "dependencies": {
18
+ "react": "^18.3.1",
19
+ "react-dom": "^18.3.1",
20
+ "@domglyph/tokens": "2.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/react": "^18.3.3",
24
+ "@types/react-dom": "^18.3.0",
25
+ "tsup": "^8.5.1"
26
+ },
27
+ "scripts": {
28
+ "build": "tsup src/index.ts --format esm --dts --clean",
29
+ "dev": "tsup src/index.ts --format esm --dts --watch",
30
+ "lint": "eslint src --ext .ts,.tsx",
31
+ "test": "vitest run --passWithNoTests",
32
+ "typecheck": "tsc -b"
33
+ }
34
+ }