@phcdevworks/spectre-components 1.0.0 → 1.2.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.
Files changed (66) hide show
  1. package/README.md +586 -107
  2. package/dist/base-B_9DJkh2.d.cts +38 -0
  3. package/dist/base-B_9DJkh2.d.ts +38 -0
  4. package/dist/button.cjs +252 -90
  5. package/dist/button.cjs.map +1 -1
  6. package/dist/button.d.cts +39 -37
  7. package/dist/button.d.ts +39 -37
  8. package/dist/button.js +252 -90
  9. package/dist/button.js.map +1 -1
  10. package/dist/checkbox.cjs +228 -67
  11. package/dist/checkbox.cjs.map +1 -1
  12. package/dist/checkbox.d.cts +21 -21
  13. package/dist/checkbox.d.ts +21 -21
  14. package/dist/checkbox.js +228 -67
  15. package/dist/checkbox.js.map +1 -1
  16. package/dist/fieldset.cjs +245 -56
  17. package/dist/fieldset.cjs.map +1 -1
  18. package/dist/fieldset.d.cts +35 -14
  19. package/dist/fieldset.d.ts +35 -14
  20. package/dist/fieldset.js +245 -56
  21. package/dist/fieldset.js.map +1 -1
  22. package/dist/form-9cZG1E9P.d.cts +10 -0
  23. package/dist/form-9cZG1E9P.d.ts +10 -0
  24. package/dist/index.cjs +647 -328
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +4 -4
  27. package/dist/index.d.ts +4 -4
  28. package/dist/index.js +647 -328
  29. package/dist/index.js.map +1 -1
  30. package/dist/input.cjs +262 -77
  31. package/dist/input.cjs.map +1 -1
  32. package/dist/input.d.cts +72 -40
  33. package/dist/input.d.ts +72 -40
  34. package/dist/input.js +262 -77
  35. package/dist/input.js.map +1 -1
  36. package/dist/label.cjs +217 -55
  37. package/dist/label.cjs.map +1 -1
  38. package/dist/label.d.cts +17 -10
  39. package/dist/label.d.ts +17 -10
  40. package/dist/label.js +217 -55
  41. package/dist/label.js.map +1 -1
  42. package/dist/{projectable-DcnBRiko.d.cts → projectable-CIfxi7dv.d.cts} +1 -1
  43. package/dist/{projectable-SzbPvXfM.d.ts → projectable-Ksv9eoIr.d.ts} +1 -1
  44. package/dist/radio.cjs +248 -67
  45. package/dist/radio.cjs.map +1 -1
  46. package/dist/radio.d.cts +23 -21
  47. package/dist/radio.d.ts +23 -21
  48. package/dist/radio.js +248 -67
  49. package/dist/radio.js.map +1 -1
  50. package/dist/select.cjs +230 -67
  51. package/dist/select.cjs.map +1 -1
  52. package/dist/select.d.cts +27 -27
  53. package/dist/select.d.ts +27 -27
  54. package/dist/select.js +230 -67
  55. package/dist/select.js.map +1 -1
  56. package/dist/textarea.cjs +256 -77
  57. package/dist/textarea.cjs.map +1 -1
  58. package/dist/textarea.d.cts +53 -32
  59. package/dist/textarea.d.ts +53 -32
  60. package/dist/textarea.js +256 -77
  61. package/dist/textarea.js.map +1 -1
  62. package/package.json +22 -12
  63. package/dist/base-DQtCQ-dP.d.cts +0 -22
  64. package/dist/base-DQtCQ-dP.d.ts +0 -22
  65. package/dist/form-iI7vV-3W.d.cts +0 -6
  66. package/dist/form-iI7vV-3W.d.ts +0 -6
package/README.md CHANGED
@@ -1,26 +1,74 @@
1
1
  # @phcdevworks/spectre-components
2
2
 
3
- [![GitHub issues](https://img.shields.io/github/issues/phcdevworks/spectre-components)](https://github.com/phcdevworks/spectre-components/issues)
4
- [![GitHub pull requests](https://img.shields.io/github/issues-pr/phcdevworks/spectre-components)](https://github.com/phcdevworks/spectre-components/pulls)
5
- [![License](https://img.shields.io/github/license/phcdevworks/spectre-components)](LICENSE)
6
-
7
- `@phcdevworks/spectre-components` is the Lit-based component layer of the
8
- Spectre suite.
9
-
10
- Maintained by PHCDevworks, it turns Spectre tokens and Spectre UI contracts
11
- into reusable, accessible, framework-agnostic web components. It is the
12
- canonical component implementation layer for Spectre, built on platform
13
- standards and designed to be consumed directly or wrapped later by adapter
14
- packages.
3
+ [![npm version](https://img.shields.io/npm/v/@phcdevworks/spectre-components)](https://www.npmjs.com/package/@phcdevworks/spectre-components)
4
+ [![CI](https://github.com/phcdevworks/spectre-components/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/phcdevworks/spectre-components/actions/workflows/ci.yml)
5
+ [![License](https://img.shields.io/npm/l/@phcdevworks/spectre-components)](LICENSE)
6
+ [![Node](https://img.shields.io/node/v/@phcdevworks/spectre-components)](https://nodejs.org)
7
+
8
+ `@phcdevworks/spectre-components` is the **Layer 3** Lit-based web component
9
+ package of the Spectre design system. It turns Spectre tokens
10
+ (`@phcdevworks/spectre-tokens`) and Spectre UI styling contracts
11
+ (`@phcdevworks/spectre-ui`) into reusable, accessible, framework-agnostic
12
+ custom elements — the canonical component implementation layer for Spectre,
13
+ designed to be consumed directly or wrapped by downstream adapter packages.
14
+
15
+ [Contributing](CONTRIBUTING.md) | [Code of Conduct](CODE_OF_CONDUCT.md) |
16
+ [Changelog](CHANGELOG.md) | [Roadmap](ROADMAP.md) |
17
+ [Security Policy](SECURITY.md)
18
+
19
+ ## Why this package exists alongside spectre-ui
20
+
21
+ `@phcdevworks/spectre-ui` owns CSS: class recipes, Tailwind helpers, and the
22
+ styling contract that maps Spectre tokens to visual output. It ships CSS rules
23
+ and JavaScript class-name helpers — nothing more.
24
+
25
+ This package sits above that. It owns **behavior**: the Lit element classes
26
+ that apply those CSS recipes, forward ARIA attributes to native elements,
27
+ manage focus delegation, handle content projection, validate properties, and
28
+ expose a stable TypeScript API surface for downstream adapters.
29
+
30
+ The separation keeps each layer focused:
31
+
32
+ | Layer | Package | Owns |
33
+ |-------|---------|------|
34
+ | L1 | `@phcdevworks/spectre-tokens` | Design values and semantic meaning |
35
+ | L2 | `@phcdevworks/spectre-ui` | CSS recipes and styling contracts |
36
+ | **L3** | **`@phcdevworks/spectre-components`** | **Lit web component behavior and API** |
37
+ | L4 | Downstream adapters | Framework-specific delivery |
38
+
39
+ If you only need CSS class names, use `@phcdevworks/spectre-ui` directly. If
40
+ you need ready-to-use HTML elements with behavior, accessibility, and a typed
41
+ API, use this package.
15
42
 
16
43
  ## Key capabilities
17
44
 
18
- - Ships reusable web components implemented with Lit
19
- - Consumes `@phcdevworks/spectre-tokens` as the source of visual meaning
20
- - Consumes `@phcdevworks/spectre-ui` as the styling contract layer
21
- - Keeps component delivery framework-agnostic through custom elements
22
- - Builds accessibility into the initial component patterns
23
- - Exposes a small, explicit API surface suitable for long-term growth
45
+ - Lit-based custom elements on the Custom Elements standard
46
+ - Renders in **light DOM** so `@phcdevworks/spectre-ui` global styles apply
47
+ directly — no Shadow DOM piercing required
48
+ - ARIA attributes (`aria-label`, `aria-labelledby`, `aria-describedby`) are
49
+ forwarded to the native element, not left on the host
50
+ - Focus and blur delegate to the inner native element
51
+ - Property validation with safe fallbacks in `willUpdate()`
52
+ - Idempotent `defineSpectre*()` helpers — safe to call multiple times
53
+ - ESM + CJS dual build with TypeScript declaration files
54
+ - Tree-shakeable subpath exports per component
55
+
56
+ ## When to use this package
57
+
58
+ - You are building UI with the Spectre design system and want standards-based
59
+ custom elements with baked-in behavior and accessibility.
60
+ - You want typed form controls (`sp-button`, `sp-input`, `sp-select`, etc.)
61
+ that work in any framework or in plain HTML.
62
+ - You are writing a framework adapter (React, Vue, Astro) and need a reliable,
63
+ stable element layer to wrap.
64
+
65
+ ## When not to use this package
66
+
67
+ - You only need CSS class names — use `@phcdevworks/spectre-ui` directly.
68
+ - You are adding routing, shell logic, or app-startup orchestration — those
69
+ are out of scope here.
70
+ - You need framework-specific component files (JSX, SFCs, Astro components) —
71
+ those belong in a downstream adapter package.
24
72
 
25
73
  ## Installation
26
74
 
@@ -30,147 +78,578 @@ npm install @phcdevworks/spectre-components @phcdevworks/spectre-ui @phcdevworks
30
78
 
31
79
  ## Quick start
32
80
 
33
- Import the Spectre CSS layers first, then register the custom elements you want
34
- to use:
81
+ ### Plain HTML
35
82
 
36
- ```ts
37
- import "@phcdevworks/spectre-tokens/index.css";
38
- import "@phcdevworks/spectre-ui/index.css";
83
+ Import the CSS layers and register all components from a script tag or entry
84
+ module. These are standard custom elements — no build step required for
85
+ consumption.
39
86
 
40
- import { defineSpectreComponents } from "@phcdevworks/spectre-components";
87
+ ```html
88
+ <!doctype html>
89
+ <html lang="en">
90
+ <head>
91
+ <!-- Spectre CSS layers must load before any markup is rendered -->
92
+ <link rel="stylesheet" href="/node_modules/@phcdevworks/spectre-tokens/index.css" />
93
+ <link rel="stylesheet" href="/node_modules/@phcdevworks/spectre-ui/index.css" />
94
+ </head>
95
+ <body>
96
+ <sp-label for="email">Email address</sp-label>
97
+ <sp-input id="email" name="email" type="email" placeholder="you@example.com"></sp-input>
98
+
99
+ <sp-button variant="primary" type="submit">Send</sp-button>
100
+ <sp-button variant="ghost" type="button">Cancel</sp-button>
101
+
102
+ <script type="module">
103
+ import { defineSpectreComponents } from '/node_modules/@phcdevworks/spectre-components/dist/index.js';
104
+ defineSpectreComponents();
105
+ </script>
106
+ </body>
107
+ </html>
108
+ ```
41
109
 
110
+ ### JavaScript / TypeScript module
111
+
112
+ ```ts
113
+ import '@phcdevworks/spectre-tokens/index.css';
114
+ import '@phcdevworks/spectre-ui/index.css';
115
+
116
+ // Register everything at once
117
+ import { defineSpectreComponents } from '@phcdevworks/spectre-components';
42
118
  defineSpectreComponents();
119
+
120
+ // Or register only what you use
121
+ import { defineSpectreButton } from '@phcdevworks/spectre-components/button';
122
+ import { defineSpectreInput } from '@phcdevworks/spectre-components/input';
123
+ defineSpectreButton();
124
+ defineSpectreInput();
43
125
  ```
44
126
 
45
- Use the components in markup:
127
+ ### Full form example
46
128
 
47
129
  ```html
48
- <sp-button variant="primary" size="md">Save</sp-button>
49
- <sp-button variant="ghost" size="sm">Cancel</sp-button>
50
- <sp-input name="email" type="email" placeholder="Email address"></sp-input>
130
+ <sp-fieldset legend="Contact preferences">
131
+ <sp-label for="email">Email address</sp-label>
132
+ <sp-input id="email" name="email" type="email" required></sp-input>
133
+
134
+ <sp-label for="bio">Bio</sp-label>
135
+ <sp-textarea id="bio" name="bio" rows="4" maxlength="500"></sp-textarea>
136
+
137
+ <sp-label for="role">Role</sp-label>
138
+ <sp-select id="role" name="role">
139
+ <option value="admin">Admin</option>
140
+ <option value="user">User</option>
141
+ </sp-select>
142
+
143
+ <sp-checkbox name="terms" value="accepted" required>
144
+ I accept the <a href="/terms">terms of service</a>
145
+ </sp-checkbox>
146
+
147
+ <sp-radio name="plan" value="monthly">Monthly billing</sp-radio>
148
+ <sp-radio name="plan" value="annual">Annual billing</sp-radio>
149
+
150
+ <sp-button variant="primary" type="submit">Save</sp-button>
151
+ <sp-button variant="ghost" type="reset">Reset</sp-button>
152
+ </sp-fieldset>
51
153
  ```
52
154
 
53
- Register only the button when you want a narrower entry point:
155
+ ### Framework integration note
54
156
 
55
- ```ts
56
- import { defineSpectreButton } from "@phcdevworks/spectre-components/button";
157
+ These are standard HTML custom elements. They work in every major framework
158
+ that supports the Custom Elements standard:
57
159
 
58
- defineSpectreButton();
160
+ **React 19+** — supports custom element properties and events natively:
161
+ ```tsx
162
+ // React 19: properties and events work directly
163
+ <sp-input name="email" type="email" onInput={(e) => setValue(e.target.value)} />
59
164
  ```
60
165
 
61
- Register only the input when you want the input-only entry point:
166
+ **React 18 and below** — set attributes via `ref` for properties, listen for
167
+ native events on the element:
168
+ ```tsx
169
+ const inputRef = useRef(null);
170
+ useEffect(() => {
171
+ if (inputRef.current) inputRef.current.invalid = true;
172
+ }, []);
173
+ <sp-input ref={inputRef} name="email" />
174
+ ```
62
175
 
176
+ **Vue 3** — supports custom elements out of the box with `v-bind` and
177
+ `v-on` directive compatibility. Mark the `sp-*` prefix in `compilerOptions`
178
+ as a custom element to suppress unknown-element warnings:
63
179
  ```ts
64
- import { defineSpectreInput } from "@phcdevworks/spectre-components/input";
180
+ // vite.config.ts
181
+ plugins: [vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('sp-') } } })]
182
+ ```
183
+ ```html
184
+ <sp-input name="email" :invalid="hasError" @change="handleChange" />
185
+ ```
65
186
 
66
- defineSpectreInput();
187
+ **Astro** — use components as static custom elements or with `client:load`
188
+ when JavaScript interactivity is needed:
189
+ ```astro
190
+ ---
191
+ import '@phcdevworks/spectre-tokens/index.css';
192
+ import '@phcdevworks/spectre-ui/index.css';
193
+ ---
194
+ <script>
195
+ import { defineSpectreComponents } from '@phcdevworks/spectre-components';
196
+ defineSpectreComponents();
197
+ </script>
198
+ <sp-button variant="primary">Click me</sp-button>
67
199
  ```
68
200
 
69
- ## What this package owns
201
+ > Framework adapter packages that wrap these components into idiomatic JSX or
202
+ > SFC APIs belong in a downstream adapter — not in this package.
70
203
 
71
- - Lit-based web component implementation for Spectre
72
- - Accessible component behavior and DOM structure
73
- - Public custom element registration helpers
74
- - Component-level TypeScript APIs for future adapters to build on
204
+ ## Accessibility
75
205
 
76
- Golden rule: implement components from Spectre contracts, do not redefine those
77
- contracts locally.
206
+ All components follow WCAG 2.1 AA baseline expectations by default.
78
207
 
79
- ## What this package does not own
208
+ **ARIA attribute forwarding** — `aria-label`, `aria-labelledby`, and
209
+ `aria-describedby` set on the host element are automatically forwarded to the
210
+ inner native element so screen readers receive them on the correct target.
80
211
 
81
- - Design-token values or semantic visual meaning That belongs to
82
- [`@phcdevworks/spectre-tokens`](https://github.com/phcdevworks/spectre-tokens).
83
- - CSS utilities, class recipes, or styling contracts That belongs to
84
- [`@phcdevworks/spectre-ui`](https://github.com/phcdevworks/spectre-ui).
85
- - Framework adapters such as React, Vue, Astro, or app-specific wrappers Those
86
- belong in downstream adapter packages.
87
- - Routing, shell logic, manifest behavior, or startup orchestration Those are
88
- outside the scope of this package.
212
+ **Native element semantics** — every component renders a real native element
213
+ (`<button>`, `<input>`, `<textarea>`, `<select>`, `<label>`, `<fieldset>`)
214
+ so browser accessibility APIs work without customization.
89
215
 
90
- ## Package exports / API surface
216
+ **State communication**
91
217
 
92
- ### Root package
218
+ | State | ARIA effect |
219
+ |-------|-------------|
220
+ | `loading` | `aria-busy="true"` on the native element |
221
+ | `invalid` | `aria-invalid="true"` on the native element |
222
+ | `disabled` | native `disabled` attribute (removes from tab order) |
223
+ | `required` | native `required` attribute |
93
224
 
94
- `@phcdevworks/spectre-components` exports:
225
+ **Focus delegation** — `.focus()` and `.blur()` called on the host are
226
+ delegated to the inner native element so external `focus()` calls work as
227
+ expected.
95
228
 
96
- - `defineSpectreComponents()`
97
- - `defineSpectreButton()`
98
- - `defineSpectreInput()`
99
- - `SpectreButtonElement`
100
- - `SpectreInputElement`
101
- - `spectreButtonVariants`
102
- - `spectreButtonSizes`
103
- - `spectreButtonTypes`
104
- - `spectreInputTypes`
105
- - `spectreInputSizes`
106
- - `SpectreButtonProps` and related button types
107
- - `SpectreInputProps` and related input types
229
+ **Label association** — use `<sp-label for="id">` paired with `id` on the
230
+ target control, or wrap controls inside a `<sp-fieldset>`. The `for` attribute
231
+ forwards to the native `<label>` element.
108
232
 
109
- ### Button entry point
233
+ **Keyboard behavior** — provided entirely by the native element inside each
234
+ component. No custom keyboard handling is layered on top.
110
235
 
111
- `@phcdevworks/spectre-components/button` exports the button-only API so
112
- consumers can register a single component without importing the full package
113
- entry.
236
+ ## Light DOM rendering
114
237
 
115
- ### Input entry point
238
+ All components render in **light DOM** (`createRenderRoot() { return this; }`).
239
+ This is intentional: it allows `@phcdevworks/spectre-ui` global CSS to reach
240
+ the native element directly without Shadow DOM piercing.
116
241
 
117
- `@phcdevworks/spectre-components/input` exports the input-only API so
118
- consumers can register just `sp-input` when they do not need the broader
119
- component bundle.
242
+ As a result, these components have no `::part()` exports — the native element
243
+ is directly selectable using standard CSS combinators or the stable internal
244
+ data attributes:
120
245
 
121
- ## Relationship to the rest of Spectre
246
+ ```css
247
+ /* Target the native input inside sp-input */
248
+ sp-input input { font-size: 0.875rem; }
122
249
 
123
- Spectre keeps responsibilities separate:
250
+ /* Stable internal hook — won't break if markup restructures */
251
+ sp-input [data-sp-input-native] { font-size: 0.875rem; }
252
+ ```
124
253
 
125
- - [`@phcdevworks/spectre-tokens`](https://github.com/phcdevworks/spectre-tokens)
126
- defines design values and semantic meaning
127
- - [`@phcdevworks/spectre-ui`](https://github.com/phcdevworks/spectre-ui)
128
- provides CSS, Tailwind helpers, recipes, and styling contracts
129
- - `@phcdevworks/spectre-components` turns those layers into Lit-based reusable
130
- web components
254
+ Do not switch any component from light DOM to Shadow DOM without a
255
+ design-system-level decision.
131
256
 
132
- That separation keeps visual meaning centralized, styling contracts stable, and
133
- component behavior reusable across frameworks.
257
+ ## Components
134
258
 
135
- ## Current foundation
259
+ ### sp-button
136
260
 
137
- This v0 foundation includes:
261
+ Renders a `<button>` with Spectre variant, size, loading, and pill support.
138
262
 
139
- - a publishable TypeScript package build
140
- - root and subpath exports
141
- - a Lit-based `sp-button` starter component
142
- - a Lit-based `sp-input` foundation component
143
- - explicit registration helpers instead of implicit global side effects
144
- - Vitest coverage for baseline rendering and accessibility behavior
263
+ **Attributes**
145
264
 
146
- ## Development
265
+ | Attribute | Type | Default | Description |
266
+ |-----------|------|---------|-------------|
267
+ | `variant` | `primary \| secondary \| ghost \| danger \| success \| cta \| accent` | `primary` | Visual style |
268
+ | `size` | `sm \| md \| lg` | `md` | Control size |
269
+ | `type` | `button \| submit \| reset` | `button` | Native button type |
270
+ | `label` | string | — | Text label (overridden by content projection) |
271
+ | `loading` | boolean | `false` | Busy state — disables the button and shows loading label |
272
+ | `loading-label` | string | `Loading` | Accessible text shown during loading |
273
+ | `disabled` | boolean | `false` | Disables the button |
274
+ | `full-width` | boolean | `false` | Spans full container width |
275
+ | `pill` | boolean | `false` | Pill / fully-rounded corners |
276
+ | `name` | string | — | Form field name |
277
+ | `value` | string | `''` | Submitted value |
278
+ | `form` | string | — | Associates with a form by ID |
279
+ | `autofocus` | boolean | `false` | Autofocus on page load |
280
+ | `id` | string | — | Forwarded to the native `<button>` |
281
+ | `title` | string | — | Forwarded to the native `<button>` |
282
+ | `aria-label` | string | — | Forwarded to the native `<button>` |
283
+ | `aria-labelledby` | string | — | Forwarded to the native `<button>` |
284
+ | `aria-describedby` | string | — | Forwarded to the native `<button>` |
285
+
286
+ **Events** — native button events bubble normally (`click`, `focus`, `blur`).
287
+
288
+ **Content projection** — place children inside `<sp-button>` to use them as
289
+ button content instead of the `label` property:
290
+
291
+ ```html
292
+ <sp-button variant="primary">
293
+ <svg aria-hidden="true">...</svg>
294
+ Save changes
295
+ </sp-button>
296
+ ```
297
+
298
+ **Internal target** — `[data-sp-button-native]` selects the native `<button>`.
299
+
300
+ ---
301
+
302
+ ### sp-input
303
+
304
+ Renders an `<input>` with state, size, and type support.
305
+
306
+ **Attributes**
307
+
308
+ | Attribute | Type | Default | Description |
309
+ |-----------|------|---------|-------------|
310
+ | `type` | `text \| email \| password \| search \| tel \| url \| number \| date \| datetime-local \| month \| time \| week` | `text` | Native input type |
311
+ | `size` | `sm \| md \| lg` | `md` | Control size |
312
+ | `name` | string | — | Form field name |
313
+ | `value` | string | `''` | Current value |
314
+ | `placeholder` | string | — | Placeholder text |
315
+ | `disabled` | boolean | `false` | Disables the input |
316
+ | `loading` | boolean | `false` | Busy state |
317
+ | `readonly` | boolean | `false` | Read-only mode |
318
+ | `required` | boolean | `false` | Marks field as required |
319
+ | `invalid` | boolean | `false` | Error state (`aria-invalid`) |
320
+ | `success` | boolean | `false` | Success state |
321
+ | `full-width` | boolean | `false` | Spans full container width |
322
+ | `pill` | boolean | `false` | Pill / fully-rounded corners |
323
+ | `autocomplete` | string | — | Native autocomplete hint |
324
+ | `inputmode` | string | — | Virtual keyboard hint |
325
+ | `min` / `max` / `step` | string | — | Numeric/date range |
326
+ | `minlength` / `maxlength` | number | — | Character length constraints |
327
+ | `form` | string | — | Associates with a form by ID |
328
+ | `autofocus` | boolean | `false` | Autofocus on page load |
329
+ | `id` / `title` / `aria-*` | string | — | Forwarded to native `<input>` |
330
+
331
+ **Events** — `input` and `change` fire from the native `<input>` and bubble.
332
+
333
+ **Internal target** — `[data-sp-input-native]` selects the native `<input>`.
334
+
335
+ ---
336
+
337
+ ### sp-textarea
338
+
339
+ Renders a `<textarea>` with row control and resize support.
340
+
341
+ **Attributes** — same as `sp-input` except no `type`, `min`, `max`, `step`, and adds:
342
+
343
+ | Attribute | Type | Default | Description |
344
+ |-----------|------|---------|-------------|
345
+ | `rows` | number | `2` | Visible row height |
346
+
347
+ **Events** — `input` and `change` fire from the native `<textarea>`.
348
+
349
+ **Internal target** — `[data-sp-textarea-native]` selects the native `<textarea>`.
350
+
351
+ ---
352
+
353
+ ### sp-select
354
+
355
+ Renders a `<select>`. Pass `<option>` elements as children — they are
356
+ projected into the native select element.
357
+
358
+ **Attributes** — same as `sp-input` minus `type`, `placeholder`, `readonly`,
359
+ `inputmode`, `min`, `max`, `step`, `minlength`, `maxlength`.
360
+
361
+ **Events** — `input` and `change` fire from the native `<select>`.
362
+
363
+ **Content projection** — `<option>` and `<optgroup>` children are moved into
364
+ the native `<select>`:
365
+
366
+ ```html
367
+ <sp-select name="country" required>
368
+ <option value="">Select a country</option>
369
+ <optgroup label="Americas">
370
+ <option value="us">United States</option>
371
+ <option value="ca">Canada</option>
372
+ </optgroup>
373
+ </sp-select>
374
+ ```
375
+
376
+ **Internal target** — `[data-sp-select-native]` selects the native `<select>`.
377
+
378
+ ---
379
+
380
+ ### sp-checkbox
381
+
382
+ Renders a `<label>` wrapping an `<input type="checkbox">` with indicator.
383
+
384
+ **Attributes**
385
+
386
+ | Attribute | Type | Default | Description |
387
+ |-----------|------|---------|-------------|
388
+ | `name` | string | — | Form field name |
389
+ | `value` | string | `on` | Submitted value when checked |
390
+ | `checked` | boolean | `false` | Checked state |
391
+ | `label` | string | — | Text label (overridden by content projection) |
392
+ | `disabled` | boolean | `false` | Disables the checkbox |
393
+ | `loading` | boolean | `false` | Busy state |
394
+ | `required` | boolean | `false` | Marks field as required |
395
+ | `invalid` | boolean | `false` | Error state |
396
+ | `success` | boolean | `false` | Success state |
397
+ | `form` / `autofocus` / `id` / `title` / `aria-*` | — | — | Forwarded to native `<input>` |
398
+
399
+ **Events** — `input` and `change` fire from the native checkbox input.
400
+
401
+ **Content projection** — children become the label content (supports rich
402
+ markup):
403
+
404
+ ```html
405
+ <sp-checkbox name="terms" value="accepted" required>
406
+ I accept the <a href="/terms">terms of service</a>
407
+ </sp-checkbox>
408
+ ```
147
409
 
148
- Install dependencies, then run the package checks:
410
+ **Internal target** — `[data-sp-checkbox-native]` selects the native checkbox.
411
+
412
+ ---
413
+
414
+ ### sp-radio
415
+
416
+ Renders a `<label>` wrapping an `<input type="radio">` with indicator.
417
+ Group multiple `sp-radio` elements by giving them the same `name`.
418
+
419
+ **Attributes** — same as `sp-checkbox`. `value` defaults to `on`.
420
+
421
+ **Events** — `input` and `change` fire from the native radio input.
422
+
423
+ **Content projection** — same as `sp-checkbox`.
424
+
425
+ ```html
426
+ <sp-radio name="plan" value="monthly">Monthly — $9/mo</sp-radio>
427
+ <sp-radio name="plan" value="annual">Annual — $90/yr</sp-radio>
428
+ ```
429
+
430
+ **Internal target** — `[data-sp-radio-native]` selects the native radio input.
431
+
432
+ ---
433
+
434
+ ### sp-label
435
+
436
+ Renders a `<label>` with `for` forwarding. Use to associate a visible label
437
+ with any form control.
438
+
439
+ **Attributes**
440
+
441
+ | Attribute | Type | Default | Description |
442
+ |-----------|------|---------|-------------|
443
+ | `for` | string | — | ID of the associated control (forwarded to native `<label>`) |
444
+ | `id` / `title` / `aria-*` | string | — | Forwarded to native `<label>` |
445
+
446
+ **Content projection** — children become the label text (supports rich markup):
447
+
448
+ ```html
449
+ <sp-label for="email">
450
+ Email address <span aria-hidden="true">*</span>
451
+ </sp-label>
452
+ ```
453
+
454
+ **Internal target** — `[data-sp-label-native]` selects the native `<label>`.
455
+
456
+ ---
457
+
458
+ ### sp-fieldset
459
+
460
+ Renders a `<fieldset>` with optional legend and group-level state.
461
+
462
+ **Attributes**
463
+
464
+ | Attribute | Type | Default | Description |
465
+ |-----------|------|---------|-------------|
466
+ | `legend` | string | — | Text for the `<legend>` element |
467
+ | `disabled` | boolean | `false` | Disables all controls in the group |
468
+ | `loading` | boolean | `false` | Busy state |
469
+ | `invalid` | boolean | `false` | Group-level error state |
470
+ | `success` | boolean | `false` | Group-level success state |
471
+ | `form` / `name` / `id` / `title` / `aria-*` | string | — | Forwarded to native `<fieldset>` |
472
+
473
+ **Content projection** — children are placed inside the native `<fieldset>`
474
+ alongside the legend:
475
+
476
+ ```html
477
+ <sp-fieldset legend="Billing address" name="billing">
478
+ <sp-label for="city">City</sp-label>
479
+ <sp-input id="city" name="city" required></sp-input>
480
+
481
+ <sp-label for="zip">ZIP code</sp-label>
482
+ <sp-input id="zip" name="zip" type="text" maxlength="10"></sp-input>
483
+ </sp-fieldset>
484
+ ```
485
+
486
+ **Internal target** — `[data-sp-fieldset-native]` selects the native `<fieldset>`.
487
+
488
+ ## Package exports / API surface
489
+
490
+ ### Root — `@phcdevworks/spectre-components`
491
+
492
+ Exports everything from all component entry points plus the bulk registration
493
+ helper.
494
+
495
+ **Bulk registration**
496
+
497
+ ```ts
498
+ import { defineSpectreComponents } from '@phcdevworks/spectre-components';
499
+ defineSpectreComponents(); // registers all sp-* elements
500
+ ```
501
+
502
+ **Per-component helpers** (same as individual entry points):
503
+ `defineSpectreButton`, `defineSpectreInput`, `defineSpectreTextarea`,
504
+ `defineSpectreSelect`, `defineSpectreCheckbox`, `defineSpectreRadio`,
505
+ `defineSpectreLabel`, `defineSpectreFieldset`
506
+
507
+ **Element classes**:
508
+ `SpectreButtonElement`, `SpectreInputElement`, `SpectreTextareaElement`,
509
+ `SpectreSelectElement`, `SpectreCheckboxElement`, `SpectreRadioElement`,
510
+ `SpectreLabelElement`, `SpectreFieldsetElement`
511
+
512
+ **Button constants and types**:
513
+ `spectreButtonVariants`, `spectreButtonSizes`, `spectreButtonTypes`,
514
+ `SpectreButtonVariant`, `SpectreButtonSize`, `SpectreButtonType`,
515
+ `SpectreButtonProps`
516
+
517
+ **Input / textarea / select constants and types**:
518
+ `spectreInputSizes`, `spectreInputTypes`, `SpectreInputSize`,
519
+ `SpectreInputType`, `SpectreInputProps`, `SpectreTextareaProps`,
520
+ `SpectreSelectProps`
521
+
522
+ **Props interfaces** (checkbox / radio / label / fieldset):
523
+ `SpectreCheckboxProps`, `SpectreRadioProps`, `SpectreLabelProps`,
524
+ `SpectreFieldsetProps`
525
+
526
+ ### Subpath entry points
527
+
528
+ Each entry point registers only that component and exports only its surface:
529
+
530
+ | Entry point | Registers | Key exports |
531
+ |-------------|-----------|-------------|
532
+ | `.../button` | `sp-button` | `defineSpectreButton`, `SpectreButtonElement`, button constants and types |
533
+ | `.../input` | `sp-input` | `defineSpectreInput`, `SpectreInputElement`, input constants and types |
534
+ | `.../textarea` | `sp-textarea` | `defineSpectreTextarea`, `SpectreTextareaElement`, `SpectreTextareaProps` |
535
+ | `.../select` | `sp-select` | `defineSpectreSelect`, `SpectreSelectElement`, `SpectreSelectProps` |
536
+ | `.../checkbox` | `sp-checkbox` | `defineSpectreCheckbox`, `SpectreCheckboxElement`, `SpectreCheckboxProps` |
537
+ | `.../radio` | `sp-radio` | `defineSpectreRadio`, `SpectreRadioElement`, `SpectreRadioProps` |
538
+ | `.../label` | `sp-label` | `defineSpectreLabel`, `SpectreLabelElement`, `SpectreLabelProps` |
539
+ | `.../fieldset` | `sp-fieldset` | `defineSpectreFieldset`, `SpectreFieldsetElement`, `SpectreFieldsetProps` |
540
+
541
+ Size constants are shared between input, textarea, and select. Import
542
+ `spectreInputSizes` / `SpectreInputSize` from `.../input` when needed
543
+ alongside textarea or select.
544
+
545
+ ## Relationship to the rest of Spectre
546
+
547
+ ```
548
+ spectre-tokens → design values (colors, spacing, typography)
549
+ spectre-ui → CSS recipes and Tailwind helpers
550
+ spectre-components → Lit web component behavior ← you are here
551
+ [adapters] → React / Vue / Astro wrappers
552
+ ```
553
+
554
+ The Golden Rule: **tokens define meaning, UI defines structure, components
555
+ define behavior, adapters define delivery.** This package only owns the
556
+ behavior layer.
557
+
558
+ ## Development
149
559
 
150
560
  ```bash
151
- npm run build
152
- npm test
153
- npm run lint
561
+ git clone https://github.com/phcdevworks/spectre-components.git
562
+ cd spectre-components
563
+ npm install
564
+ npm run check # lint + typecheck + test + build + export validation
154
565
  ```
155
566
 
567
+ Requires Node.js `^22.12.0 || >=24.0.0` and npm `11.14.1`.
568
+
569
+ | Command | Purpose |
570
+ |---------|---------|
571
+ | `npm run check` | Full validation (lint → typecheck → test → build → export check) |
572
+ | `npm run build` | Compile ESM + CJS with declarations into `dist/` |
573
+ | `npm test` | Run Vitest suite under happy-dom |
574
+ | `npm run lint` | ESLint |
575
+ | `npm run check:exports` | Verify built subpath exports resolve correctly |
576
+ | `npm run dev` | tsup watch mode |
577
+ | `npm run clean` | Remove `dist/` and `coverage/` |
578
+
156
579
  Key source areas:
157
580
 
158
- - `src/components/` for custom elements
159
- - `src/index.ts` for the root public API
160
- - `tests/` for component behavior coverage
581
+ - `src/components/` — one directory per custom element
582
+ - `src/utils/` — `base.ts`, `projectable.ts`, `form.ts`, `dom.ts`
583
+ - `src/index.ts` — root public API and bulk registration helper
584
+ - `tests/` — component behavior coverage (Vitest + happy-dom)
585
+ - `scripts/check-exports.js` — post-build export resolution check
586
+
587
+ ## Troubleshooting
588
+
589
+ **Build fails with type errors** — TypeScript 6 is required. Run
590
+ `npm install`, then `npm run build`.
591
+
592
+ **Tests fail in CI but pass locally** — Tests run under happy-dom. Confirm you
593
+ are on Node `^22.12.0 || >=24.0.0`. CI tests both versions.
594
+
595
+ **Custom element already defined** — Each `defineSpectre*()` helper is
596
+ idempotent; calling it twice is safe. If you see conflicts, two different
597
+ versions of this package may be loaded in the same page.
598
+
599
+ **Styles are not applying** — The Spectre CSS layers must load before
600
+ components are registered. Import `@phcdevworks/spectre-tokens/index.css` and
601
+ `@phcdevworks/spectre-ui/index.css` at the top of your entry module.
602
+
603
+ **Properties not reflecting in React 18** — React 18 sets custom element
604
+ properties as attributes. Use a `ref` to set properties imperatively, or
605
+ upgrade to React 19 which supports custom elements fully.
606
+
607
+ ## Validation
608
+
609
+ Run the full validation gate before any pull request:
610
+
611
+ ```bash
612
+ npm run check
613
+ ```
614
+
615
+ This runs: lint → typecheck → tests → build → export validation. All steps
616
+ must pass.
617
+
618
+ ## AI and automation boundaries
619
+
620
+ Claude Code (`claude-sonnet-4-6`) is the primary development agent for this
621
+ repository. Codex handles releases and production stabilization. Jules handles
622
+ small automated fixes and dependency updates. GitHub Copilot provides
623
+ development support.
624
+
625
+ Claude Code, Codex, and Copilot do not create git commits by default. Jules may
626
+ commit only bounded automated maintenance when the `JULES.md` scope and
627
+ validation gates pass. Release decisions, tags, and publishing remain with
628
+ Bradley Potts.
629
+
630
+ **Protected from automated change:** component public API surface (tags,
631
+ properties, events, slots, ARIA), the light-DOM rendering model, and the
632
+ zero-hardcode-values rule. See [AGENTS.md](AGENTS.md) for full agent governance
633
+ and boundary rules.
161
634
 
162
635
  ## Contributing
163
636
 
164
637
  PHCDevworks maintains this package as part of the Spectre suite.
165
638
 
166
- When contributing:
639
+ Contribution boundaries:
640
+
641
+ - Components must consume `@phcdevworks/spectre-ui` class helpers — do not
642
+ recreate CSS locally.
643
+ - Design values must come from `@phcdevworks/spectre-tokens` — do not
644
+ hardcode colors, spacing, or other visual primitives.
645
+ - Component tags, properties, events, slots, and ARIA behavior are stable API
646
+ — breaking changes require a semver major bump.
647
+ - Render in light DOM only — Shadow DOM changes require design-system approval.
648
+ - No framework-specific code — no JSX, SFCs, or Astro components in this
649
+ package.
650
+ - Run `npm run check` before opening a pull request.
167
651
 
168
- - treat `@phcdevworks/spectre-tokens` as the source of visual meaning
169
- - treat `@phcdevworks/spectre-ui` as the styling contract layer
170
- - avoid redefining visual primitives locally
171
- - keep components accessible by default
172
- - run `npm run build`, `npm test`, and `npm run lint` before opening a pull
173
- request
652
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide.
174
653
 
175
654
  ## License
176
655