@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 +21 -0
- package/README.md +224 -0
- package/dist/index.d.ts +108 -0
- package/dist/index.js +416 -0
- package/package.json +34 -0
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
|
+
[](https://www.npmjs.com/package/@domglyph/primitives)
|
|
4
|
+
[](../../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.
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|