eyecomfy-ui 0.0.0-stage → 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/AGENTS.md ADDED
@@ -0,0 +1,280 @@
1
+ # eyecomfy-ui: guide for AI agents
2
+
3
+ Read this file first. It covers everything needed to install `eyecomfy-ui` and build UI from its ten components. Per-component detail is in `docs/<Component>.md` inside this package.
4
+
5
+ ## What it is
6
+
7
+ Ten basic React components styled with plain CSS custom properties. There is no layout system, no icon set and no runtime dependency beyond React. You compose the components yourself with your own layout (flexbox, grid, plain CSS).
8
+
9
+ ## Install and setup
10
+
11
+ ```bash
12
+ npm install eyecomfy-ui
13
+ ```
14
+
15
+ Peer dependencies: `react` and `react-dom`, version 18.2 or later (19 supported).
16
+
17
+ 1. Import the stylesheet once, near the top of the app (for example in the root layout or entry file):
18
+
19
+ ```js
20
+ import 'eyecomfy-ui/ui.css';
21
+ ```
22
+
23
+ 2. Import components by name from the package root:
24
+
25
+ ```js
26
+ import { Button, TextField } from 'eyecomfy-ui';
27
+ ```
28
+
29
+ The project's bundler must compile JSX. Vite, Next.js, Remix and Create React App do this by default.
30
+
31
+ TypeScript: the stylesheet import has no types of its own. If `tsc` reports "Cannot find module or type declarations for side-effect import of 'eyecomfy-ui/ui.css'", add a declaration once, unless your framework already provides one (Next.js and Vite do):
32
+
33
+ ```ts
34
+ // css.d.ts
35
+ declare module '*.css';
36
+ ```
37
+
38
+ ### Next.js (App Router)
39
+
40
+ - Import `eyecomfy-ui/ui.css` in `app/layout.jsx` (or `.tsx`), not in a page.
41
+ - Components that hold state or use hooks are client components. The package already marks them with `'use client'`, so you can import them from a server component. Put a `'use client'` directive in your own file only when your file uses state, effects or event handlers.
42
+ - Server-safe components: `Button`, `Badge`, `Alert`, `Card`.
43
+ - Client components: `TextField`, `Textarea`, `Checkbox`, `Dialog`, `Tabs`, `Accordion`.
44
+
45
+ ### Vite / React SPA
46
+
47
+ Import the stylesheet in `main.jsx` (or `main.tsx`) and render your tree as usual.
48
+
49
+ ## Component reference
50
+
51
+ | Component | Client | Props (beyond native attributes) | Detail |
52
+ | --- | --- | --- | --- |
53
+ | `Button` | no | `variant` `'primary' \| 'secondary' \| 'ghost' \| 'danger'` (default `'primary'`); `size` `'sm' \| 'md'` (default `'md'`); `loading` boolean; `type` defaults to `'button'` | docs/Button.md |
54
+ | `Badge` | no | `tone` `'neutral' \| 'info' \| 'success' \| 'warning' \| 'error'` (default `'neutral'`) | docs/Badge.md |
55
+ | `Alert` | no | `tone` `'info' \| 'success' \| 'warning' \| 'error'` (default `'info'`); `title` ReactNode; children | docs/Alert.md |
56
+ | `Card` | no | `as` `'div' \| 'article' \| 'section'` (default `'div'`); `title` ReactNode; `headingLevel` 2–6 (default 3); `footer` ReactNode; children | docs/Card.md |
57
+ | `TextField` | yes | `label` string (required); `hint` string; `error` string; plus native `<input>` props | docs/TextField.md |
58
+ | `Textarea` | yes | `label` string (required); `hint`; `error`; `rows` (default 4); plus native `<textarea>` props | docs/Textarea.md |
59
+ | `Checkbox` | yes | `label` string (required); `hint`; `error`; plus native `<input>` props except `type` | docs/Checkbox.md |
60
+ | `Dialog` | yes | `open` boolean (required); `onClose` function (required); `title` string (required); `footer` ReactNode; children | docs/Dialog.md |
61
+ | `Tabs` | yes | `items` `{ id, label, content, disabled? }[]` (required); `label` string for the tab list (required); `defaultValue` id; `value` id and `onValueChange(id)` for controlled use | docs/Tabs.md |
62
+ | `Accordion` | yes | `items` `{ id, title, content, defaultOpen? }[]` (required); `multiple` boolean (default `false`); `headingLevel` 2–6 (default 3) | docs/Accordion.md |
63
+
64
+ Exported types: `ButtonProps`, `ButtonVariant`, `ButtonSize`, `BadgeProps`, `BadgeTone`, `AlertProps`, `AlertTone`, `CardProps`, `TextFieldProps`, `TextareaProps`, `CheckboxProps`, `DialogProps`, `TabsProps`, `TabItem`, `AccordionProps`, `AccordionItem`, `HeadingLevel`.
65
+
66
+ ## Behaviour rules
67
+
68
+ - **Native props pass through.** Any attribute not listed above (`name`, `aria-*`, `onClick`, `disabled`, `className`, `id`) goes to the underlying element. `className` is appended after the component's own `ui-*` classes.
69
+ - **Form fields** (`TextField`, `Textarea`, `Checkbox`): `label` is required. `hint` renders under the field and is linked with `aria-describedby`. `error` sets `aria-invalid="true"` and renders a `role="alert"` message. Pass `id` when you need a stable id; otherwise one is generated.
70
+ - **Alert** uses `role="alert"` for `tone="error"` and `role="status"` for the other tones.
71
+ - **Button** `loading` disables the button, sets `aria-busy`, and shows a spinner that is `aria-hidden`. Keep the text label changing too (for example "Saving").
72
+ - **Dialog** is controlled. Set `open` to true to show it. `onClose` runs when the user presses the close button or Escape, so set your state to false inside it. The dialog title is always an `<h2>`.
73
+ - **Tabs** is uncontrolled by default (`defaultValue`, else the first enabled tab). Use `value` with `onValueChange` to control it. Arrow keys, Home and End move between tabs.
74
+ - **Accordion** keeps its own open state. `defaultOpen` on an item sets it open on first render. Without `multiple`, opening one panel closes the others.
75
+ - **Heading levels** (`Card`, `Accordion`) are props. Choose the level that fits the page outline; do not default to a level just because it looks right.
76
+ - Do not add `ui-*` classes by hand unless a doc lists one. The stylesheet is written against the component classes.
77
+
78
+ ## Layout is your job
79
+
80
+ The package has no grid, stack or container components. Write layout with your own CSS. Typical patterns:
81
+
82
+ ```jsx
83
+ <div style={{ display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(16rem, 1fr))' }}>
84
+ <Card title="Starter">...</Card>
85
+ <Card title="Team">...</Card>
86
+ </div>
87
+ ```
88
+
89
+ Keep the page responsive. Form fields and buttons size to their container. Set `max-width` on forms yourself.
90
+
91
+ ## Design tokens
92
+
93
+ Set these on `:root` (or on a container) to restyle everything. Defaults are shown.
94
+
95
+ | Token | Default (light) | Used for |
96
+ | --- | --- | --- |
97
+ | `--ui-font` | `system-ui, -apple-system, "Segoe UI", Roboto, sans-serif` | All text |
98
+ | `--ui-text` | `#1d2530` | Body text |
99
+ | `--ui-muted` | `#5a6573` | Hints, secondary text |
100
+ | `--ui-surface` | `#ffffff` | Card, dialog, input backgrounds |
101
+ | `--ui-surface-2` | `#f3f6f9` | Footers, tab list, subtle fills |
102
+ | `--ui-border` | `#c4cdd7` | Borders |
103
+ | `--ui-accent` | `#1f4e8c` | Primary button, selected tab, links |
104
+ | `--ui-accent-text` | `#ffffff` | Text on accent |
105
+ | `--ui-focus` | `#2563eb` | Focus ring |
106
+ | `--ui-success` / `--ui-success-bg` | `#1d6b3a` / `#e5f4ea` | Success tone |
107
+ | `--ui-warning` / `--ui-warning-bg` | `#7a4b00` / `#fff3d1` | Warning tone |
108
+ | `--ui-error` / `--ui-error-bg` | `#9b1c1c` / `#fbe9e9` | Error tone, danger button |
109
+ | `--ui-info` / `--ui-info-bg` | `#1f4e8c` / `#e6eef8` | Info tone |
110
+ | `--ui-radius` | `0` | Corner radius everywhere |
111
+ | `--ui-shadow` | `0 1px 2px rgb(0 0 0 / 0.08)` | Elevation |
112
+
113
+ Example theme override:
114
+
115
+ ```css
116
+ :root {
117
+ --ui-accent: #0f766e;
118
+ --ui-accent-text: #ffffff;
119
+ --ui-radius: 0.5rem;
120
+ }
121
+ ```
122
+
123
+ Dark mode: colours switch automatically under `prefers-color-scheme: dark`. To force a mode, set `data-theme="dark"` or `data-theme="light"` on `<html>`. Setting `data-theme="light"` opts out of the automatic dark colours.
124
+
125
+ ## Recipes
126
+
127
+ Each recipe uses only documented props.
128
+
129
+ ### Sign-up form
130
+
131
+ ```jsx
132
+ 'use client';
133
+ import { useState } from 'react';
134
+ import { Alert, Button, Checkbox, TextField } from 'eyecomfy-ui';
135
+
136
+ export function SignUpForm() {
137
+ const [email, setEmail] = useState('');
138
+ const [emailError, setEmailError] = useState('');
139
+ const [agreed, setAgreed] = useState(false);
140
+
141
+ function submit(event) {
142
+ event.preventDefault();
143
+ setEmailError(email.includes('@') ? '' : 'Enter an email address.');
144
+ }
145
+
146
+ return (
147
+ <form noValidate onSubmit={submit} style={{ display: 'grid', gap: '1rem', maxWidth: '24rem' }}>
148
+ <TextField label="Email" type="email" required value={email}
149
+ onChange={(e) => setEmail(e.target.value)} error={emailError} />
150
+ <Checkbox label="I accept the terms" checked={agreed}
151
+ onChange={(e) => setAgreed(e.target.checked)} />
152
+ <Button type="submit">Sign up</Button>
153
+ </form>
154
+ );
155
+ }
156
+ ```
157
+
158
+ ### Settings panel with tabs
159
+
160
+ ```jsx
161
+ 'use client';
162
+ import { useState } from 'react';
163
+ import { Checkbox, Tabs, TextField } from 'eyecomfy-ui';
164
+
165
+ export function SettingsPanel() {
166
+ const [tab, setTab] = useState('profile');
167
+ return (
168
+ <Tabs
169
+ label="Settings sections"
170
+ value={tab}
171
+ onValueChange={setTab}
172
+ items={[
173
+ { id: 'profile', label: 'Profile', content: <TextField label="Display name" /> },
174
+ { id: 'alerts', label: 'Alerts', content: <Checkbox label="Email me about billing" /> },
175
+ { id: 'billing', label: 'Billing', content: <p>Coming soon.</p>, disabled: true },
176
+ ]}
177
+ />
178
+ );
179
+ }
180
+ ```
181
+
182
+ ### Confirm delete
183
+
184
+ ```jsx
185
+ 'use client';
186
+ import { useState } from 'react';
187
+ import { Button, Dialog } from 'eyecomfy-ui';
188
+
189
+ export function DeleteProject({ onDelete }) {
190
+ const [open, setOpen] = useState(false);
191
+ return (
192
+ <>
193
+ <Button variant="danger" onClick={() => setOpen(true)}>Delete project</Button>
194
+ <Dialog
195
+ open={open}
196
+ onClose={() => setOpen(false)}
197
+ title="Delete this project?"
198
+ footer={
199
+ <>
200
+ <Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
201
+ <Button variant="danger" onClick={() => { onDelete(); setOpen(false); }}>Delete</Button>
202
+ </>
203
+ }
204
+ >
205
+ <p>This cannot be undone.</p>
206
+ </Dialog>
207
+ </>
208
+ );
209
+ }
210
+ ```
211
+
212
+ ### Plan comparison cards
213
+
214
+ ```jsx
215
+ import { Badge, Button, Card } from 'eyecomfy-ui';
216
+
217
+ export function Plans() {
218
+ return (
219
+ <div style={{ display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(16rem, 1fr))' }}>
220
+ <Card as="article" title="Starter" headingLevel={2}
221
+ footer={<Button variant="secondary">Choose</Button>}>
222
+ <p>Free for small projects.</p>
223
+ </Card>
224
+ <Card as="article" title={<>Team <Badge tone="success">Popular</Badge></>} headingLevel={2}
225
+ footer={<Button>Choose</Button>}>
226
+ <p>Shared workspace for teams.</p>
227
+ </Card>
228
+ </div>
229
+ );
230
+ }
231
+ ```
232
+
233
+ ### Status list
234
+
235
+ ```jsx
236
+ import { Alert, Badge } from 'eyecomfy-ui';
237
+
238
+ export function Status({ deploys }) {
239
+ return (
240
+ <>
241
+ <Alert tone="warning" title="Maintenance tonight">Writes pause from 23:00 to 23:30.</Alert>
242
+ <ul>
243
+ {deploys.map((d) => (
244
+ <li key={d.id}>{d.name} <Badge tone={d.ok ? 'success' : 'error'}>{d.ok ? 'Live' : 'Failed'}</Badge></li>
245
+ ))}
246
+ </ul>
247
+ </>
248
+ );
249
+ }
250
+ ```
251
+
252
+ ### FAQ
253
+
254
+ ```jsx
255
+ 'use client';
256
+ import { Accordion } from 'eyecomfy-ui';
257
+
258
+ export function Faq() {
259
+ return (
260
+ <Accordion
261
+ headingLevel={2}
262
+ items={[
263
+ { id: 'refunds', title: 'Can I get a refund?', content: <p>Yes, within 14 days.</p>, defaultOpen: true },
264
+ { id: 'export', title: 'Can I export my data?', content: <p>Yes, from Settings.</p> },
265
+ ]}
266
+ />
267
+ );
268
+ }
269
+ ```
270
+
271
+ ## Do and don't
272
+
273
+ - Do import the stylesheet once and only once.
274
+ - Do pass a `label` to every `TextField`, `Textarea` and `Checkbox`.
275
+ - Do keep the `onClose` of `Dialog` in sync with your `open` state.
276
+ - Do use a `Tabs` `label` that names the group.
277
+ - Don't invent props. If a prop is not in the table above or in the component's doc, it is not supported; a native attribute is the only other option.
278
+ - Don't use `Alert` tone `error` for non-errors: it interrupts screen readers.
279
+ - Don't use `Button` as a link. Use an anchor and style it yourself, or a plain `<a>`.
280
+ - Don't rely on `Card` or `Accordion` heading levels to match a visual size. Set `headingLevel` for the outline and style the heading with CSS if needed.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 eyecomfy-ui contributors
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 CHANGED
@@ -1,3 +1,44 @@
1
- # Temporary Holding Version
1
+ # eyecomfy-ui
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Ten basic React UI components, styled with plain CSS custom properties. No runtime dependencies beyond React.
4
+
5
+ - Button, Badge, Alert, Card (server-safe)
6
+ - TextField, Textarea, Checkbox, Dialog, Tabs, Accordion (client components)
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install eyecomfy-ui
12
+ ```
13
+
14
+ Import the stylesheet once, then the components you need:
15
+
16
+ ```jsx
17
+ import 'eyecomfy-ui/ui.css';
18
+ import { Button, TextField } from 'eyecomfy-ui';
19
+
20
+ export function Greeting() {
21
+ return (
22
+ <>
23
+ <TextField label="Name" />
24
+ <Button>Say hello</Button>
25
+ </>
26
+ );
27
+ }
28
+ ```
29
+
30
+ Requires React 18.2 or later. Your bundler must compile JSX.
31
+
32
+ ## Theming
33
+
34
+ Override the `--ui-*` custom properties on `:root`. Dark colours apply under `prefers-color-scheme: dark`, or when `data-theme="dark"` is set on `<html>`.
35
+
36
+ ## Documentation
37
+
38
+ - Component pages: `node_modules/eyecomfy-ui/docs/`
39
+ - AI agents: read `node_modules/eyecomfy-ui/AGENTS.md`. It has the full prop reference, token list, composition rules and recipes.
40
+ - Index for LLM tools: `node_modules/eyecomfy-ui/llms.txt`
41
+
42
+ ## License
43
+
44
+ MIT
@@ -0,0 +1,52 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useId, useState } from "react";
4
+ import { cx } from "./utils.js";
5
+ function Accordion({ items, multiple = false, headingLevel = 3, className }) {
6
+ const baseId = useId();
7
+ const Heading = `h${headingLevel}`;
8
+ const [open, setOpen] = useState(
9
+ () => new Set(items.filter((item) => item.defaultOpen).map((item) => item.id))
10
+ );
11
+ function toggle(id) {
12
+ setOpen((previous) => {
13
+ const next = new Set(multiple ? previous : []);
14
+ if (previous.has(id)) next.delete(id);
15
+ else next.add(id);
16
+ return next;
17
+ });
18
+ }
19
+ return /* @__PURE__ */ jsx("div", { className: cx("ui-accordion", className), children: items.map((item) => {
20
+ const isOpen = open.has(item.id);
21
+ const triggerId = `${baseId}-trigger-${item.id}`;
22
+ const panelId = `${baseId}-panel-${item.id}`;
23
+ return /* @__PURE__ */ jsxs("div", { className: cx("ui-accordion__item", isOpen && "is-open"), children: [
24
+ /* @__PURE__ */ jsx(Heading, { className: "ui-accordion__heading", children: /* @__PURE__ */ jsx(
25
+ "button",
26
+ {
27
+ type: "button",
28
+ id: triggerId,
29
+ className: "ui-accordion__trigger",
30
+ "aria-expanded": isOpen,
31
+ "aria-controls": panelId,
32
+ onClick: () => toggle(item.id),
33
+ children: item.title
34
+ }
35
+ ) }),
36
+ /* @__PURE__ */ jsx(
37
+ "div",
38
+ {
39
+ id: panelId,
40
+ role: "region",
41
+ "aria-labelledby": triggerId,
42
+ hidden: !isOpen,
43
+ className: "ui-accordion__panel",
44
+ children: item.content
45
+ }
46
+ )
47
+ ] }, item.id);
48
+ }) });
49
+ }
50
+ export {
51
+ Accordion
52
+ };
@@ -0,0 +1,19 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { cx } from "./utils.js";
3
+ function Alert({ tone = "info", title, className, children, ...rest }) {
4
+ return /* @__PURE__ */ jsxs(
5
+ "div",
6
+ {
7
+ role: tone === "error" ? "alert" : "status",
8
+ ...rest,
9
+ className: cx("ui-alert", `ui-alert--${tone}`, className),
10
+ children: [
11
+ title && /* @__PURE__ */ jsx("p", { className: "ui-alert__title", children: title }),
12
+ children
13
+ ]
14
+ }
15
+ );
16
+ }
17
+ export {
18
+ Alert
19
+ };
@@ -0,0 +1,8 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { cx } from "./utils.js";
3
+ function Badge({ tone = "neutral", className, ...rest }) {
4
+ return /* @__PURE__ */ jsx("span", { ...rest, className: cx("ui-badge", `ui-badge--${tone}`, className) });
5
+ }
6
+ export {
7
+ Badge
8
+ };
@@ -0,0 +1,30 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { cx } from "./utils.js";
3
+ function Button({
4
+ variant = "primary",
5
+ size = "md",
6
+ loading = false,
7
+ disabled,
8
+ type = "button",
9
+ className,
10
+ children,
11
+ ...rest
12
+ }) {
13
+ return /* @__PURE__ */ jsxs(
14
+ "button",
15
+ {
16
+ ...rest,
17
+ type,
18
+ className: cx("ui-button", `ui-button--${variant}`, `ui-button--${size}`, className),
19
+ disabled: disabled || loading,
20
+ "aria-busy": loading || void 0,
21
+ children: [
22
+ loading && /* @__PURE__ */ jsx("span", { className: "ui-spinner", "aria-hidden": "true" }),
23
+ children
24
+ ]
25
+ }
26
+ );
27
+ }
28
+ export {
29
+ Button
30
+ };
@@ -0,0 +1,21 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { cx } from "./utils.js";
3
+ function Card({
4
+ as: Wrapper = "div",
5
+ title,
6
+ headingLevel = 3,
7
+ footer,
8
+ className,
9
+ children,
10
+ ...rest
11
+ }) {
12
+ const Heading = `h${headingLevel}`;
13
+ return /* @__PURE__ */ jsxs(Wrapper, { ...rest, className: cx("ui-card", className), children: [
14
+ title && /* @__PURE__ */ jsx(Heading, { className: "ui-card__title", children: title }),
15
+ /* @__PURE__ */ jsx("div", { className: "ui-card__body", children }),
16
+ footer && /* @__PURE__ */ jsx("div", { className: "ui-card__footer", children: footer })
17
+ ] });
18
+ }
19
+ export {
20
+ Card
21
+ };
@@ -0,0 +1,27 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { cx, useFieldIds } from "./utils.js";
4
+ function Checkbox({ label, hint, error, id, className, ...rest }) {
5
+ const { inputId, hintId, errorId, describedBy } = useFieldIds(id, Boolean(hint), Boolean(error));
6
+ return /* @__PURE__ */ jsxs("div", { className: cx("ui-checkbox", className), children: [
7
+ /* @__PURE__ */ jsxs("div", { className: "ui-checkbox__row", children: [
8
+ /* @__PURE__ */ jsx(
9
+ "input",
10
+ {
11
+ ...rest,
12
+ id: inputId,
13
+ type: "checkbox",
14
+ className: "ui-checkbox__input",
15
+ "aria-invalid": error ? true : void 0,
16
+ "aria-describedby": describedBy
17
+ }
18
+ ),
19
+ /* @__PURE__ */ jsx("label", { className: "ui-checkbox__label", htmlFor: inputId, children: label })
20
+ ] }),
21
+ hint && /* @__PURE__ */ jsx("p", { id: hintId, className: "ui-hint", children: hint }),
22
+ error && /* @__PURE__ */ jsx("p", { id: errorId, className: "ui-error", role: "alert", children: error })
23
+ ] });
24
+ }
25
+ export {
26
+ Checkbox
27
+ };
@@ -0,0 +1,33 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useRef } from "react";
4
+ function Dialog({ open, onClose, title, children, footer }) {
5
+ const ref = useRef(null);
6
+ const titleId = useId();
7
+ useEffect(() => {
8
+ const dialog = ref.current;
9
+ if (!dialog) return;
10
+ if (open && !dialog.open) dialog.showModal();
11
+ if (!open && dialog.open) dialog.close();
12
+ }, [open]);
13
+ return /* @__PURE__ */ jsxs("dialog", { ref, className: "ui-dialog", "aria-labelledby": titleId, onClose, children: [
14
+ /* @__PURE__ */ jsxs("div", { className: "ui-dialog__header", children: [
15
+ /* @__PURE__ */ jsx("h2", { id: titleId, className: "ui-dialog__title", children: title }),
16
+ /* @__PURE__ */ jsx(
17
+ "button",
18
+ {
19
+ type: "button",
20
+ className: "ui-dialog__close",
21
+ "aria-label": "Close",
22
+ onClick: () => ref.current?.close(),
23
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xD7" })
24
+ }
25
+ )
26
+ ] }),
27
+ /* @__PURE__ */ jsx("div", { className: "ui-dialog__body", children }),
28
+ footer && /* @__PURE__ */ jsx("div", { className: "ui-dialog__footer", children: footer })
29
+ ] });
30
+ }
31
+ export {
32
+ Dialog
33
+ };
@@ -0,0 +1,83 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useId, useRef, useState } from "react";
4
+ import { cx } from "./utils.js";
5
+ function Tabs({ items, label, defaultValue, value, onValueChange, className }) {
6
+ const baseId = useId();
7
+ const firstEnabled = items.find((item) => !item.disabled)?.id;
8
+ const [uncontrolled, setUncontrolled] = useState(defaultValue ?? firstEnabled);
9
+ const selected = value ?? uncontrolled;
10
+ const tabRefs = useRef(/* @__PURE__ */ new Map());
11
+ const enabledIds = items.filter((item) => !item.disabled).map((item) => item.id);
12
+ function select(id) {
13
+ if (value === void 0) setUncontrolled(id);
14
+ onValueChange?.(id);
15
+ }
16
+ function handleKeyDown(event, id) {
17
+ const position = enabledIds.indexOf(id);
18
+ const last = enabledIds.length - 1;
19
+ let next;
20
+ switch (event.key) {
21
+ case "ArrowRight":
22
+ next = enabledIds[position === last ? 0 : position + 1];
23
+ break;
24
+ case "ArrowLeft":
25
+ next = enabledIds[position <= 0 ? last : position - 1];
26
+ break;
27
+ case "Home":
28
+ next = enabledIds[0];
29
+ break;
30
+ case "End":
31
+ next = enabledIds[last];
32
+ break;
33
+ default:
34
+ return;
35
+ }
36
+ event.preventDefault();
37
+ if (next === void 0) return;
38
+ select(next);
39
+ tabRefs.current.get(next)?.focus();
40
+ }
41
+ return /* @__PURE__ */ jsxs("div", { className: cx("ui-tabs", className), children: [
42
+ /* @__PURE__ */ jsx("div", { role: "tablist", "aria-label": label, className: "ui-tabs__list", children: items.map((item) => {
43
+ const isSelected = item.id === selected;
44
+ return /* @__PURE__ */ jsx(
45
+ "button",
46
+ {
47
+ type: "button",
48
+ role: "tab",
49
+ id: `${baseId}-tab-${item.id}`,
50
+ ref: (element) => {
51
+ if (element) tabRefs.current.set(item.id, element);
52
+ else tabRefs.current.delete(item.id);
53
+ },
54
+ "aria-selected": isSelected,
55
+ "aria-controls": `${baseId}-panel-${item.id}`,
56
+ tabIndex: isSelected ? 0 : -1,
57
+ disabled: item.disabled,
58
+ className: cx("ui-tabs__tab", isSelected && "is-selected"),
59
+ onClick: () => select(item.id),
60
+ onKeyDown: (event) => handleKeyDown(event, item.id),
61
+ children: item.label
62
+ },
63
+ item.id
64
+ );
65
+ }) }),
66
+ items.map((item) => /* @__PURE__ */ jsx(
67
+ "div",
68
+ {
69
+ role: "tabpanel",
70
+ id: `${baseId}-panel-${item.id}`,
71
+ "aria-labelledby": `${baseId}-tab-${item.id}`,
72
+ tabIndex: 0,
73
+ hidden: item.id !== selected,
74
+ className: "ui-tabs__panel",
75
+ children: item.content
76
+ },
77
+ item.id
78
+ ))
79
+ ] });
80
+ }
81
+ export {
82
+ Tabs
83
+ };
@@ -0,0 +1,28 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { cx, useFieldIds } from "./utils.js";
4
+ function TextField({ label, hint, error, id, required, className, ...rest }) {
5
+ const { inputId, hintId, errorId, describedBy } = useFieldIds(id, Boolean(hint), Boolean(error));
6
+ return /* @__PURE__ */ jsxs("div", { className: cx("ui-field", className), children: [
7
+ /* @__PURE__ */ jsxs("label", { className: "ui-label", htmlFor: inputId, children: [
8
+ label,
9
+ required && /* @__PURE__ */ jsx("span", { className: "ui-required", "aria-hidden": "true", children: " *" })
10
+ ] }),
11
+ /* @__PURE__ */ jsx(
12
+ "input",
13
+ {
14
+ ...rest,
15
+ id: inputId,
16
+ className: "ui-input",
17
+ required,
18
+ "aria-invalid": error ? true : void 0,
19
+ "aria-describedby": describedBy
20
+ }
21
+ ),
22
+ hint && /* @__PURE__ */ jsx("p", { id: hintId, className: "ui-hint", children: hint }),
23
+ error && /* @__PURE__ */ jsx("p", { id: errorId, className: "ui-error", role: "alert", children: error })
24
+ ] });
25
+ }
26
+ export {
27
+ TextField
28
+ };