@phcdevworks/spectre-components 1.2.0 → 1.4.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 (94) hide show
  1. package/README.md +359 -199
  2. package/dist/badge.cjs +450 -0
  3. package/dist/badge.cjs.map +1 -0
  4. package/dist/badge.d.cts +61 -0
  5. package/dist/badge.d.ts +61 -0
  6. package/dist/badge.js +445 -0
  7. package/dist/badge.js.map +1 -0
  8. package/dist/{base-B_9DJkh2.d.cts → base-BRQ7QAAr.d.cts} +3 -0
  9. package/dist/{base-B_9DJkh2.d.ts → base-BRQ7QAAr.d.ts} +3 -0
  10. package/dist/button.cjs +53 -4
  11. package/dist/button.cjs.map +1 -1
  12. package/dist/button.d.cts +12 -15
  13. package/dist/button.d.ts +12 -15
  14. package/dist/button.js +54 -5
  15. package/dist/button.js.map +1 -1
  16. package/dist/card.cjs +439 -0
  17. package/dist/card.cjs.map +1 -0
  18. package/dist/card.d.cts +67 -0
  19. package/dist/card.d.ts +67 -0
  20. package/dist/card.js +435 -0
  21. package/dist/card.js.map +1 -0
  22. package/dist/checkbox.cjs +48 -2
  23. package/dist/checkbox.cjs.map +1 -1
  24. package/dist/checkbox.d.cts +11 -14
  25. package/dist/checkbox.d.ts +11 -14
  26. package/dist/checkbox.js +48 -2
  27. package/dist/checkbox.js.map +1 -1
  28. package/dist/fieldset.cjs +45 -0
  29. package/dist/fieldset.cjs.map +1 -1
  30. package/dist/fieldset.d.cts +11 -2
  31. package/dist/fieldset.d.ts +11 -2
  32. package/dist/fieldset.js +45 -0
  33. package/dist/fieldset.js.map +1 -1
  34. package/dist/form-Dg6kuOS7.d.cts +18 -0
  35. package/dist/form-Dg6kuOS7.d.ts +18 -0
  36. package/dist/icon-box.cjs +462 -0
  37. package/dist/icon-box.cjs.map +1 -0
  38. package/dist/icon-box.d.cts +73 -0
  39. package/dist/icon-box.d.ts +73 -0
  40. package/dist/icon-box.js +457 -0
  41. package/dist/icon-box.js.map +1 -0
  42. package/dist/index.cjs +754 -26
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.d.cts +8 -3
  45. package/dist/index.d.ts +8 -3
  46. package/dist/index.js +739 -28
  47. package/dist/index.js.map +1 -1
  48. package/dist/input.cjs +55 -7
  49. package/dist/input.cjs.map +1 -1
  50. package/dist/input.d.cts +16 -27
  51. package/dist/input.d.ts +16 -27
  52. package/dist/input.js +55 -7
  53. package/dist/input.js.map +1 -1
  54. package/dist/label.cjs +29 -0
  55. package/dist/label.cjs.map +1 -1
  56. package/dist/label.d.cts +4 -3
  57. package/dist/label.d.ts +4 -3
  58. package/dist/label.js +29 -0
  59. package/dist/label.js.map +1 -1
  60. package/dist/{projectable-CIfxi7dv.d.cts → projectable-Bx-IVk0w.d.cts} +1 -1
  61. package/dist/{projectable-Ksv9eoIr.d.ts → projectable-D6IDeKfr.d.ts} +1 -1
  62. package/dist/radio.cjs +50 -4
  63. package/dist/radio.cjs.map +1 -1
  64. package/dist/radio.d.cts +11 -14
  65. package/dist/radio.d.ts +11 -14
  66. package/dist/radio.js +50 -4
  67. package/dist/radio.js.map +1 -1
  68. package/dist/rating.cjs +373 -0
  69. package/dist/rating.cjs.map +1 -0
  70. package/dist/rating.d.cts +67 -0
  71. package/dist/rating.d.ts +67 -0
  72. package/dist/rating.js +369 -0
  73. package/dist/rating.js.map +1 -0
  74. package/dist/select.cjs +65 -2
  75. package/dist/select.cjs.map +1 -1
  76. package/dist/select.d.cts +19 -16
  77. package/dist/select.d.ts +19 -16
  78. package/dist/select.js +65 -2
  79. package/dist/select.js.map +1 -1
  80. package/dist/testimonial.cjs +433 -0
  81. package/dist/testimonial.cjs.map +1 -0
  82. package/dist/testimonial.d.cts +61 -0
  83. package/dist/testimonial.d.ts +61 -0
  84. package/dist/testimonial.js +429 -0
  85. package/dist/testimonial.js.map +1 -0
  86. package/dist/textarea.cjs +55 -7
  87. package/dist/textarea.cjs.map +1 -1
  88. package/dist/textarea.d.cts +15 -26
  89. package/dist/textarea.d.ts +15 -26
  90. package/dist/textarea.js +55 -7
  91. package/dist/textarea.js.map +1 -1
  92. package/package.json +45 -15
  93. package/dist/form-9cZG1E9P.d.cts +0 -10
  94. package/dist/form-9cZG1E9P.d.ts +0 -10
package/README.md CHANGED
@@ -8,9 +8,9 @@
8
8
  `@phcdevworks/spectre-components` is the **Layer 3** Lit-based web component
9
9
  package of the Spectre design system. It turns Spectre tokens
10
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.
11
+ (`@phcdevworks/spectre-ui`) into reusable, accessible, framework-agnostic custom
12
+ elements — the canonical component implementation layer for Spectre, designed to
13
+ be consumed directly or wrapped by downstream adapter packages.
14
14
 
15
15
  [Contributing](CONTRIBUTING.md) | [Code of Conduct](CODE_OF_CONDUCT.md) |
16
16
  [Changelog](CHANGELOG.md) | [Roadmap](ROADMAP.md) |
@@ -22,23 +22,23 @@ designed to be consumed directly or wrapped by downstream adapter packages.
22
22
  styling contract that maps Spectre tokens to visual output. It ships CSS rules
23
23
  and JavaScript class-name helpers — nothing more.
24
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.
25
+ This package sits above that. It owns **behavior**: the Lit element classes that
26
+ apply those CSS recipes, forward ARIA attributes to native elements, manage
27
+ focus delegation, handle content projection, validate properties, and expose a
28
+ stable TypeScript API surface for downstream adapters.
29
29
 
30
30
  The separation keeps each layer focused:
31
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 |
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
36
  | **L3** | **`@phcdevworks/spectre-components`** | **Lit web component behavior and API** |
37
- | L4 | Downstream adapters | Framework-specific delivery |
37
+ | L4 | Downstream adapters | Framework-specific delivery |
38
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.
39
+ If you only need CSS class names, use `@phcdevworks/spectre-ui` directly. If you
40
+ need ready-to-use HTML elements with behavior, accessibility, and a typed API,
41
+ use this package.
42
42
 
43
43
  ## Key capabilities
44
44
 
@@ -57,16 +57,17 @@ API, use this package.
57
57
 
58
58
  - You are building UI with the Spectre design system and want standards-based
59
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.
60
+ - You want typed form controls (`sp-button`, `sp-input`, `sp-select`, etc.) and
61
+ display primitives (`sp-badge`, `sp-card`, `sp-rating`, etc.) that work in any
62
+ framework or in plain HTML.
62
63
  - You are writing a framework adapter (React, Vue, Astro) and need a reliable,
63
64
  stable element layer to wrap.
64
65
 
65
66
  ## When not to use this package
66
67
 
67
68
  - 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.
69
+ - You are adding routing, shell logic, or app-startup orchestration — those are
70
+ out of scope here.
70
71
  - You need framework-specific component files (JSX, SFCs, Astro components) —
71
72
  those belong in a downstream adapter package.
72
73
 
@@ -89,19 +90,30 @@ consumption.
89
90
  <html lang="en">
90
91
  <head>
91
92
  <!-- 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" />
93
+ <link
94
+ rel="stylesheet"
95
+ href="/node_modules/@phcdevworks/spectre-tokens/index.css"
96
+ />
97
+ <link
98
+ rel="stylesheet"
99
+ href="/node_modules/@phcdevworks/spectre-ui/index.css"
100
+ />
94
101
  </head>
95
102
  <body>
96
103
  <sp-label for="email">Email address</sp-label>
97
- <sp-input id="email" name="email" type="email" placeholder="you@example.com"></sp-input>
104
+ <sp-input
105
+ id="email"
106
+ name="email"
107
+ type="email"
108
+ placeholder="you@example.com"
109
+ ></sp-input>
98
110
 
99
111
  <sp-button variant="primary" type="submit">Send</sp-button>
100
112
  <sp-button variant="ghost" type="button">Cancel</sp-button>
101
113
 
102
114
  <script type="module">
103
- import { defineSpectreComponents } from '/node_modules/@phcdevworks/spectre-components/dist/index.js';
104
- defineSpectreComponents();
115
+ import { defineSpectreComponents } from '/node_modules/@phcdevworks/spectre-components/dist/index.js'
116
+ defineSpectreComponents()
105
117
  </script>
106
118
  </body>
107
119
  </html>
@@ -110,18 +122,18 @@ consumption.
110
122
  ### JavaScript / TypeScript module
111
123
 
112
124
  ```ts
113
- import '@phcdevworks/spectre-tokens/index.css';
114
- import '@phcdevworks/spectre-ui/index.css';
125
+ import '@phcdevworks/spectre-tokens/index.css'
126
+ import '@phcdevworks/spectre-ui/index.css'
115
127
 
116
128
  // Register everything at once
117
- import { defineSpectreComponents } from '@phcdevworks/spectre-components';
118
- defineSpectreComponents();
129
+ import { defineSpectreComponents } from '@phcdevworks/spectre-components'
130
+ defineSpectreComponents()
119
131
 
120
132
  // 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();
133
+ import { defineSpectreButton } from '@phcdevworks/spectre-components/button'
134
+ import { defineSpectreInput } from '@phcdevworks/spectre-components/input'
135
+ defineSpectreButton()
136
+ defineSpectreInput()
125
137
  ```
126
138
 
127
139
  ### Full form example
@@ -154,10 +166,11 @@ defineSpectreInput();
154
166
 
155
167
  ### Framework integration note
156
168
 
157
- These are standard HTML custom elements. They work in every major framework
158
- that supports the Custom Elements standard:
169
+ These are standard HTML custom elements. They work in every major framework that
170
+ supports the Custom Elements standard:
159
171
 
160
172
  **React 19+** — supports custom element properties and events natively:
173
+
161
174
  ```tsx
162
175
  // React 19: properties and events work directly
163
176
  <sp-input name="email" type="email" onInput={(e) => setValue(e.target.value)} />
@@ -165,27 +178,38 @@ that supports the Custom Elements standard:
165
178
 
166
179
  **React 18 and below** — set attributes via `ref` for properties, listen for
167
180
  native events on the element:
181
+
168
182
  ```tsx
169
- const inputRef = useRef(null);
183
+ const inputRef = useRef(null)
170
184
  useEffect(() => {
171
- if (inputRef.current) inputRef.current.invalid = true;
172
- }, []);
173
- <sp-input ref={inputRef} name="email" />
185
+ if (inputRef.current) inputRef.current.invalid = true
186
+ }, [])
187
+
188
+ return <sp-input ref={inputRef} name="email" />
174
189
  ```
175
190
 
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:
191
+ **Vue 3** — supports custom elements out of the box with `v-bind` and `v-on`
192
+ directive compatibility. Mark the `sp-*` prefix in `compilerOptions` as a custom
193
+ element to suppress unknown-element warnings:
194
+
179
195
  ```ts
180
196
  // vite.config.ts
181
- plugins: [vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('sp-') } } })]
197
+ plugins: [
198
+ vue({
199
+ template: {
200
+ compilerOptions: { isCustomElement: (tag) => tag.startsWith('sp-') }
201
+ }
202
+ })
203
+ ]
182
204
  ```
205
+
183
206
  ```html
184
207
  <sp-input name="email" :invalid="hasError" @change="handleChange" />
185
208
  ```
186
209
 
187
- **Astro** — use components as static custom elements or with `client:load`
188
- when JavaScript interactivity is needed:
210
+ **Astro** — use components as static custom elements or with `client:load` when
211
+ JavaScript interactivity is needed:
212
+
189
213
  ```astro
190
214
  ---
191
215
  import '@phcdevworks/spectre-tokens/index.css';
@@ -210,25 +234,25 @@ All components follow WCAG 2.1 AA baseline expectations by default.
210
234
  inner native element so screen readers receive them on the correct target.
211
235
 
212
236
  **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.
237
+ (`<button>`, `<input>`, `<textarea>`, `<select>`, `<label>`, `<fieldset>`) or a
238
+ semantic light-DOM container with forwarded ARIA attributes, so browser
239
+ accessibility APIs work without customization.
215
240
 
216
241
  **State communication**
217
242
 
218
- | State | ARIA effect |
219
- |-------|-------------|
220
- | `loading` | `aria-busy="true"` on the native element |
221
- | `invalid` | `aria-invalid="true"` on the native element |
243
+ | State | ARIA effect |
244
+ | ---------- | ---------------------------------------------------- |
245
+ | `loading` | `aria-busy="true"` on the native element |
246
+ | `invalid` | `aria-invalid="true"` on the native element |
222
247
  | `disabled` | native `disabled` attribute (removes from tab order) |
223
- | `required` | native `required` attribute |
248
+ | `required` | native `required` attribute |
224
249
 
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.
250
+ **Focus delegation** — `.focus()` and `.blur()` called on the host are delegated
251
+ to the inner native element so external `focus()` calls work as expected.
228
252
 
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.
253
+ **Label association** — use `<sp-label for="id">` paired with `id` on the target
254
+ control, or wrap controls inside a `<sp-fieldset>`. The `for` attribute forwards
255
+ to the native `<label>` element.
232
256
 
233
257
  **Keyboard behavior** — provided entirely by the native element inside each
234
258
  component. No custom keyboard handling is layered on top.
@@ -236,19 +260,23 @@ component. No custom keyboard handling is layered on top.
236
260
  ## Light DOM rendering
237
261
 
238
262
  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.
263
+ This is intentional: it allows `@phcdevworks/spectre-ui` global CSS to reach the
264
+ native element directly without Shadow DOM piercing.
241
265
 
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:
266
+ As a result, these components have no `::part()` exports — the native element is
267
+ directly selectable using standard CSS combinators or the stable internal data
268
+ attributes:
245
269
 
246
270
  ```css
247
271
  /* Target the native input inside sp-input */
248
- sp-input input { font-size: 0.875rem; }
272
+ sp-input input {
273
+ font-size: 0.875rem;
274
+ }
249
275
 
250
276
  /* Stable internal hook — won't break if markup restructures */
251
- sp-input [data-sp-input-native] { font-size: 0.875rem; }
277
+ sp-input [data-sp-input-native] {
278
+ font-size: 0.875rem;
279
+ }
252
280
  ```
253
281
 
254
282
  Do not switch any component from light DOM to Shadow DOM without a
@@ -262,26 +290,26 @@ Renders a `<button>` with Spectre variant, size, loading, and pill support.
262
290
 
263
291
  **Attributes**
264
292
 
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>` |
293
+ | Attribute | Type | Default | Description |
294
+ | ------------------ | --------------------------------------------------------------------- | --------- | -------------------------------------------------------- |
295
+ | `variant` | `primary \| secondary \| ghost \| danger \| success \| cta \| accent` | `primary` | Visual style |
296
+ | `size` | `sm \| md \| lg` | `md` | Control size |
297
+ | `type` | `button \| submit \| reset` | `button` | Native button type |
298
+ | `label` | string | — | Text label (overridden by content projection) |
299
+ | `loading` | boolean | `false` | Busy state — disables the button and shows loading label |
300
+ | `loading-label` | string | `Loading` | Accessible text shown during loading |
301
+ | `disabled` | boolean | `false` | Disables the button |
302
+ | `full-width` | boolean | `false` | Spans full container width |
303
+ | `pill` | boolean | `false` | Pill / fully-rounded corners |
304
+ | `name` | string | — | Form field name |
305
+ | `value` | string | `''` | Submitted value |
306
+ | `form` | string | — | Associates with a form by ID |
307
+ | `autofocus` | boolean | `false` | Autofocus on page load |
308
+ | `id` | string | — | Forwarded to the native `<button>` |
309
+ | `title` | string | — | Forwarded to the native `<button>` |
310
+ | `aria-label` | string | — | Forwarded to the native `<button>` |
311
+ | `aria-labelledby` | string | — | Forwarded to the native `<button>` |
312
+ | `aria-describedby` | string | — | Forwarded to the native `<button>` |
285
313
 
286
314
  **Events** — native button events bubble normally (`click`, `focus`, `blur`).
287
315
 
@@ -305,28 +333,28 @@ Renders an `<input>` with state, size, and type support.
305
333
 
306
334
  **Attributes**
307
335
 
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>` |
336
+ | Attribute | Type | Default | Description |
337
+ | ------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------- | ----------------------------- |
338
+ | `type` | `text \| email \| password \| search \| tel \| url \| number \| date \| datetime-local \| month \| time \| week` | `text` | Native input type |
339
+ | `size` | `sm \| md \| lg` | `md` | Control size |
340
+ | `name` | string | — | Form field name |
341
+ | `value` | string | `''` | Current value |
342
+ | `placeholder` | string | — | Placeholder text |
343
+ | `disabled` | boolean | `false` | Disables the input |
344
+ | `loading` | boolean | `false` | Busy state |
345
+ | `readonly` | boolean | `false` | Read-only mode |
346
+ | `required` | boolean | `false` | Marks field as required |
347
+ | `invalid` | boolean | `false` | Error state (`aria-invalid`) |
348
+ | `success` | boolean | `false` | Success state |
349
+ | `full-width` | boolean | `false` | Spans full container width |
350
+ | `pill` | boolean | `false` | Pill / fully-rounded corners |
351
+ | `autocomplete` | string | — | Native autocomplete hint |
352
+ | `inputmode` | string | — | Virtual keyboard hint |
353
+ | `min` / `max` / `step` | string | — | Numeric/date range |
354
+ | `minlength` / `maxlength` | number | — | Character length constraints |
355
+ | `form` | string | — | Associates with a form by ID |
356
+ | `autofocus` | boolean | `false` | Autofocus on page load |
357
+ | `id` / `title` / `aria-*` | string | — | Forwarded to native `<input>` |
330
358
 
331
359
  **Events** — `input` and `change` fire from the native `<input>` and bubble.
332
360
 
@@ -338,30 +366,32 @@ Renders an `<input>` with state, size, and type support.
338
366
 
339
367
  Renders a `<textarea>` with row control and resize support.
340
368
 
341
- **Attributes** — same as `sp-input` except no `type`, `min`, `max`, `step`, and adds:
369
+ **Attributes** — same as `sp-input` except no `type`, `min`, `max`, `step`, and
370
+ adds:
342
371
 
343
- | Attribute | Type | Default | Description |
344
- |-----------|------|---------|-------------|
345
- | `rows` | number | `2` | Visible row height |
372
+ | Attribute | Type | Default | Description |
373
+ | --------- | ------ | ------- | ------------------ |
374
+ | `rows` | number | `2` | Visible row height |
346
375
 
347
376
  **Events** — `input` and `change` fire from the native `<textarea>`.
348
377
 
349
- **Internal target** — `[data-sp-textarea-native]` selects the native `<textarea>`.
378
+ **Internal target** — `[data-sp-textarea-native]` selects the native
379
+ `<textarea>`.
350
380
 
351
381
  ---
352
382
 
353
383
  ### sp-select
354
384
 
355
- Renders a `<select>`. Pass `<option>` elements as children — they are
356
- projected into the native select element.
385
+ Renders a `<select>`. Pass `<option>` elements as children — they are projected
386
+ into the native select element.
357
387
 
358
388
  **Attributes** — same as `sp-input` minus `type`, `placeholder`, `readonly`,
359
389
  `inputmode`, `min`, `max`, `step`, `minlength`, `maxlength`.
360
390
 
361
391
  **Events** — `input` and `change` fire from the native `<select>`.
362
392
 
363
- **Content projection** — `<option>` and `<optgroup>` children are moved into
364
- the native `<select>`:
393
+ **Content projection** — `<option>` and `<optgroup>` children are moved into the
394
+ native `<select>`:
365
395
 
366
396
  ```html
367
397
  <sp-select name="country" required>
@@ -383,18 +413,18 @@ Renders a `<label>` wrapping an `<input type="checkbox">` with indicator.
383
413
 
384
414
  **Attributes**
385
415
 
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>` |
416
+ | Attribute | Type | Default | Description |
417
+ | ------------------------------------------------ | ------- | ------- | --------------------------------------------- |
418
+ | `name` | string | — | Form field name |
419
+ | `value` | string | `on` | Submitted value when checked |
420
+ | `checked` | boolean | `false` | Checked state |
421
+ | `label` | string | — | Text label (overridden by content projection) |
422
+ | `disabled` | boolean | `false` | Disables the checkbox |
423
+ | `loading` | boolean | `false` | Busy state |
424
+ | `required` | boolean | `false` | Marks field as required |
425
+ | `invalid` | boolean | `false` | Error state |
426
+ | `success` | boolean | `false` | Success state |
427
+ | `form` / `autofocus` / `id` / `title` / `aria-*` | — | — | Forwarded to native `<input>` |
398
428
 
399
429
  **Events** — `input` and `change` fire from the native checkbox input.
400
430
 
@@ -413,8 +443,8 @@ markup):
413
443
 
414
444
  ### sp-radio
415
445
 
416
- Renders a `<label>` wrapping an `<input type="radio">` with indicator.
417
- Group multiple `sp-radio` elements by giving them the same `name`.
446
+ Renders a `<label>` wrapping an `<input type="radio">` with indicator. Group
447
+ multiple `sp-radio` elements by giving them the same `name`.
418
448
 
419
449
  **Attributes** — same as `sp-checkbox`. `value` defaults to `on`.
420
450
 
@@ -433,15 +463,15 @@ Group multiple `sp-radio` elements by giving them the same `name`.
433
463
 
434
464
  ### sp-label
435
465
 
436
- Renders a `<label>` with `for` forwarding. Use to associate a visible label
437
- with any form control.
466
+ Renders a `<label>` with `for` forwarding. Use to associate a visible label with
467
+ any form control.
438
468
 
439
469
  **Attributes**
440
470
 
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>` |
471
+ | Attribute | Type | Default | Description |
472
+ | ------------------------- | ------ | ------- | ------------------------------------------------------------ |
473
+ | `for` | string | — | ID of the associated control (forwarded to native `<label>`) |
474
+ | `id` / `title` / `aria-*` | string | — | Forwarded to native `<label>` |
445
475
 
446
476
  **Content projection** — children become the label text (supports rich markup):
447
477
 
@@ -461,14 +491,14 @@ Renders a `<fieldset>` with optional legend and group-level state.
461
491
 
462
492
  **Attributes**
463
493
 
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>` |
494
+ | Attribute | Type | Default | Description |
495
+ | ------------------------------------------- | ------- | ------- | ---------------------------------- |
496
+ | `legend` | string | — | Text for the `<legend>` element |
497
+ | `disabled` | boolean | `false` | Disables all controls in the group |
498
+ | `loading` | boolean | `false` | Busy state |
499
+ | `invalid` | boolean | `false` | Group-level error state |
500
+ | `success` | boolean | `false` | Group-level success state |
501
+ | `form` / `name` / `id` / `title` / `aria-*` | string | — | Forwarded to native `<fieldset>` |
472
502
 
473
503
  **Content projection** — children are placed inside the native `<fieldset>`
474
504
  alongside the legend:
@@ -483,7 +513,119 @@ alongside the legend:
483
513
  </sp-fieldset>
484
514
  ```
485
515
 
486
- **Internal target** — `[data-sp-fieldset-native]` selects the native `<fieldset>`.
516
+ **Internal target** — `[data-sp-fieldset-native]` selects the native
517
+ `<fieldset>`.
518
+
519
+ ---
520
+
521
+ ### sp-badge
522
+
523
+ Renders a `<span>` display primitive backed by the Spectre badge recipe.
524
+
525
+ **Attributes**
526
+
527
+ | Attribute | Type | Default | Description |
528
+ | ------------------------- | -------------------------------------------------------------------------------------------------------------- | --------- | -------------------------------- |
529
+ | `variant` | `primary \| secondary \| ghost \| danger \| success \| warning \| info \| accent \| cta \| neutral \| outline` | `primary` | Visual style |
530
+ | `size` | `sm \| md \| lg` | `md` | Badge size |
531
+ | `disabled` | boolean | `false` | Disabled visual state |
532
+ | `loading` | boolean | `false` | Busy visual state |
533
+ | `full-width` | boolean | `false` | Spans full container width |
534
+ | `id` / `title` / `aria-*` | string | — | Forwarded to the native `<span>` |
535
+
536
+ **Content projection** — children become the badge content.
537
+
538
+ **Internal target** — `[data-sp-badge-native]` selects the native `<span>`.
539
+
540
+ ---
541
+
542
+ ### sp-card
543
+
544
+ Renders a `<div>` container backed by the Spectre card recipe.
545
+
546
+ **Attributes**
547
+
548
+ | Attribute | Type | Default | Description |
549
+ | ------------------------- | -------------------------------------- | ---------- | ------------------------------- |
550
+ | `variant` | `elevated \| flat \| outline \| ghost` | `elevated` | Visual style |
551
+ | `padded` | boolean | `true` | Applies card padding |
552
+ | `full-height` | boolean | `false` | Spans full container height |
553
+ | `interactive` | boolean | `false` | Applies interactive styling |
554
+ | `disabled` | boolean | `false` | Disabled visual state |
555
+ | `loading` | boolean | `false` | Busy visual state |
556
+ | `id` / `title` / `aria-*` | string | — | Forwarded to the native `<div>` |
557
+
558
+ **Content projection** — children become the card content.
559
+
560
+ **Internal target** — `[data-sp-card-native]` selects the native `<div>`.
561
+
562
+ ---
563
+
564
+ ### sp-icon-box
565
+
566
+ Renders a `<div>` icon container backed by the Spectre icon-box recipe.
567
+
568
+ **Attributes**
569
+
570
+ | Attribute | Type | Default | Description |
571
+ | ------------------------- | -------------------------------------------------------------------------------------------------------------- | --------- | ------------------------------- |
572
+ | `variant` | `primary \| secondary \| ghost \| danger \| success \| warning \| info \| accent \| cta \| neutral \| outline` | `primary` | Visual style |
573
+ | `size` | `sm \| md \| lg` | `md` | Icon-box size |
574
+ | `disabled` | boolean | `false` | Disabled visual state |
575
+ | `loading` | boolean | `false` | Busy visual state |
576
+ | `interactive` | boolean | `false` | Applies interactive styling |
577
+ | `pill` | boolean | `false` | Pill / fully-rounded corners |
578
+ | `full-width` | boolean | `false` | Spans full container width |
579
+ | `id` / `title` / `aria-*` | string | — | Forwarded to the native `<div>` |
580
+
581
+ **Content projection** — children become the icon-box content.
582
+
583
+ **Internal target** — `[data-sp-icon-box-native]` selects the native `<div>`.
584
+
585
+ ---
586
+
587
+ ### sp-rating
588
+
589
+ Renders a read-only rating visualization with generated star spans.
590
+
591
+ **Attributes**
592
+
593
+ | Attribute | Type | Default | Description |
594
+ | ------------------------- | ---------------- | ------- | -------------------------------------- |
595
+ | `value` | number | `0` | Filled star count |
596
+ | `max` | number | `5` | Total star count |
597
+ | `size` | `sm \| md \| lg` | `md` | Rating size |
598
+ | `label` | string | — | Optional visible text beside the stars |
599
+ | `disabled` | boolean | `false` | Disabled visual state |
600
+ | `loading` | boolean | `false` | Busy visual state |
601
+ | `id` / `title` / `aria-*` | string | — | Forwarded to the rating container |
602
+
603
+ **Accessibility** — renders `role="img"` and computes an accessible label like
604
+ `Rating: 4 out of 5` unless `aria-label` is provided.
605
+
606
+ **Internal target** — `[data-sp-rating-native]` selects the rating container.
607
+
608
+ ---
609
+
610
+ ### sp-testimonial
611
+
612
+ Renders a `<div>` testimonial container backed by the Spectre testimonial
613
+ recipe.
614
+
615
+ **Attributes**
616
+
617
+ | Attribute | Type | Default | Description |
618
+ | ------------------------- | -------------------------------------- | ---------- | ------------------------------- |
619
+ | `variant` | `elevated \| flat \| outline \| ghost` | `elevated` | Visual style |
620
+ | `full-height` | boolean | `false` | Spans full container height |
621
+ | `interactive` | boolean | `false` | Applies interactive styling |
622
+ | `disabled` | boolean | `false` | Disabled visual state |
623
+ | `loading` | boolean | `false` | Busy visual state |
624
+ | `id` / `title` / `aria-*` | string | — | Forwarded to the native `<div>` |
625
+
626
+ **Content projection** — children become the testimonial content.
627
+
628
+ **Internal target** — `[data-sp-testimonial-native]` selects the native `<div>`.
487
629
 
488
630
  ## Package exports / API surface
489
631
 
@@ -495,52 +637,66 @@ helper.
495
637
  **Bulk registration**
496
638
 
497
639
  ```ts
498
- import { defineSpectreComponents } from '@phcdevworks/spectre-components';
499
- defineSpectreComponents(); // registers all sp-* elements
640
+ import { defineSpectreComponents } from '@phcdevworks/spectre-components'
641
+ defineSpectreComponents() // registers all sp-* elements
500
642
  ```
501
643
 
502
644
  **Per-component helpers** (same as individual entry points):
503
645
  `defineSpectreButton`, `defineSpectreInput`, `defineSpectreTextarea`,
504
646
  `defineSpectreSelect`, `defineSpectreCheckbox`, `defineSpectreRadio`,
505
- `defineSpectreLabel`, `defineSpectreFieldset`
647
+ `defineSpectreLabel`, `defineSpectreFieldset`, `defineSpectreBadge`,
648
+ `defineSpectreCard`, `defineSpectreIconBox`, `defineSpectreRating`,
649
+ `defineSpectreTestimonial`
506
650
 
507
- **Element classes**:
508
- `SpectreButtonElement`, `SpectreInputElement`, `SpectreTextareaElement`,
509
- `SpectreSelectElement`, `SpectreCheckboxElement`, `SpectreRadioElement`,
510
- `SpectreLabelElement`, `SpectreFieldsetElement`
651
+ **Element classes**: `SpectreButtonElement`, `SpectreInputElement`,
652
+ `SpectreTextareaElement`, `SpectreSelectElement`, `SpectreCheckboxElement`,
653
+ `SpectreRadioElement`, `SpectreLabelElement`, `SpectreFieldsetElement`,
654
+ `SpectreBadgeElement`, `SpectreCardElement`, `SpectreIconBoxElement`,
655
+ `SpectreRatingElement`, `SpectreTestimonialElement`
511
656
 
512
- **Button constants and types**:
513
- `spectreButtonVariants`, `spectreButtonSizes`, `spectreButtonTypes`,
514
- `SpectreButtonVariant`, `SpectreButtonSize`, `SpectreButtonType`,
515
- `SpectreButtonProps`
657
+ **Button constants and types**: `spectreButtonVariants`, `spectreButtonSizes`,
658
+ `spectreButtonTypes`, `SpectreButtonVariant`, `SpectreButtonSize`,
659
+ `SpectreButtonType`, `SpectreButtonProps`
516
660
 
517
- **Input / textarea / select constants and types**:
518
- `spectreInputSizes`, `spectreInputTypes`, `SpectreInputSize`,
519
- `SpectreInputType`, `SpectreInputProps`, `SpectreTextareaProps`,
520
- `SpectreSelectProps`
661
+ **Input / textarea / select constants and types**: `spectreInputSizes`,
662
+ `spectreInputTypes`, `SpectreInputSize`, `SpectreInputType`,
663
+ `SpectreInputProps`, `SpectreTextareaProps`, `SpectreSelectProps`
521
664
 
522
665
  **Props interfaces** (checkbox / radio / label / fieldset):
523
666
  `SpectreCheckboxProps`, `SpectreRadioProps`, `SpectreLabelProps`,
524
667
  `SpectreFieldsetProps`
525
668
 
669
+ **Display constants and types**: `spectreBadgeVariants`, `spectreBadgeSizes`,
670
+ `spectreCardVariants`, `spectreIconBoxVariants`, `spectreIconBoxSizes`,
671
+ `spectreRatingSizes`, `spectreTestimonialVariants`, `SpectreBadgeVariant`,
672
+ `SpectreBadgeSize`, `SpectreCardVariant`, `SpectreIconBoxVariant`,
673
+ `SpectreIconBoxSize`, `SpectreRatingSize`, `SpectreTestimonialVariant`,
674
+ `SpectreBadgeProps`, `SpectreCardProps`, `SpectreIconBoxProps`,
675
+ `SpectreRatingProps`, `SpectreTestimonialProps`
676
+
526
677
  ### Subpath entry points
527
678
 
528
679
  Each entry point registers only that component and exports only its surface:
529
680
 
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` |
681
+ | Entry point | Registers | Key exports |
682
+ | ----------------- | ---------------- | ---------------------------------------------------------------------------------------- |
683
+ | `.../button` | `sp-button` | `defineSpectreButton`, `SpectreButtonElement`, button constants and types |
684
+ | `.../input` | `sp-input` | `defineSpectreInput`, `SpectreInputElement`, input constants and types |
685
+ | `.../textarea` | `sp-textarea` | `defineSpectreTextarea`, `SpectreTextareaElement`, `SpectreTextareaProps` |
686
+ | `.../select` | `sp-select` | `defineSpectreSelect`, `SpectreSelectElement`, `SpectreSelectProps` |
687
+ | `.../checkbox` | `sp-checkbox` | `defineSpectreCheckbox`, `SpectreCheckboxElement`, `SpectreCheckboxProps` |
688
+ | `.../radio` | `sp-radio` | `defineSpectreRadio`, `SpectreRadioElement`, `SpectreRadioProps` |
689
+ | `.../label` | `sp-label` | `defineSpectreLabel`, `SpectreLabelElement`, `SpectreLabelProps` |
690
+ | `.../fieldset` | `sp-fieldset` | `defineSpectreFieldset`, `SpectreFieldsetElement`, `SpectreFieldsetProps` |
691
+ | `.../badge` | `sp-badge` | `defineSpectreBadge`, `SpectreBadgeElement`, badge constants and types |
692
+ | `.../card` | `sp-card` | `defineSpectreCard`, `SpectreCardElement`, card constants and types |
693
+ | `.../icon-box` | `sp-icon-box` | `defineSpectreIconBox`, `SpectreIconBoxElement`, icon-box constants and types |
694
+ | `.../rating` | `sp-rating` | `defineSpectreRating`, `SpectreRatingElement`, rating constants and types |
695
+ | `.../testimonial` | `sp-testimonial` | `defineSpectreTestimonial`, `SpectreTestimonialElement`, testimonial constants and types |
540
696
 
541
697
  Size constants are shared between input, textarea, and select. Import
542
- `spectreInputSizes` / `SpectreInputSize` from `.../input` when needed
543
- alongside textarea or select.
698
+ `spectreInputSizes` / `SpectreInputSize` from `.../input` when needed alongside
699
+ textarea or select.
544
700
 
545
701
  ## Relationship to the rest of Spectre
546
702
 
@@ -552,8 +708,8 @@ spectre-components → Lit web component behavior ← you are here
552
708
  ```
553
709
 
554
710
  The Golden Rule: **tokens define meaning, UI defines structure, components
555
- define behavior, adapters define delivery.** This package only owns the
556
- behavior layer.
711
+ define behavior, adapters define delivery.** This package only owns the behavior
712
+ layer.
557
713
 
558
714
  ## Development
559
715
 
@@ -561,20 +717,23 @@ behavior layer.
561
717
  git clone https://github.com/phcdevworks/spectre-components.git
562
718
  cd spectre-components
563
719
  npm install
564
- npm run check # lint + typecheck + test + build + export validation
720
+ npm run check # full release validation gate
565
721
  ```
566
722
 
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/` |
723
+ Requires Node.js `^22.13.0 || >=24.0.0` and npm `11.16.0`.
724
+
725
+ | Command | Purpose |
726
+ | -------------------------- | ----------------------------------------------------------------------------------------------------- |
727
+ | `npm run check` | Full validation (lint → typecheck → test → build → export, contract, invariant, and ecosystem checks) |
728
+ | `npm run build` | Compile ESM + CJS with declarations into `dist/` |
729
+ | `npm test` | Run Vitest suite under happy-dom |
730
+ | `npm run lint` | ESLint |
731
+ | `npm run check:exports` | Verify built subpath exports resolve correctly |
732
+ | `npm run check:contract` | Verify built exports match `components.contract.json` |
733
+ | `npm run check:invariants` | Verify light-DOM and no-hardcoded-visual invariants |
734
+ | `npm run check:ecosystem` | Validate `spectre.manifest.json` |
735
+ | `npm run dev` | tsup watch mode |
736
+ | `npm run clean` | Remove `dist/` and `coverage/` |
578
737
 
579
738
  Key source areas:
580
739
 
@@ -582,27 +741,27 @@ Key source areas:
582
741
  - `src/utils/` — `base.ts`, `projectable.ts`, `form.ts`, `dom.ts`
583
742
  - `src/index.ts` — root public API and bulk registration helper
584
743
  - `tests/` — component behavior coverage (Vitest + happy-dom)
585
- - `scripts/check-exports.js` — post-build export resolution check
744
+ - `scripts/check-exports.ts` — post-build export resolution check
586
745
 
587
746
  ## Troubleshooting
588
747
 
589
- **Build fails with type errors** — TypeScript 6 is required. Run
590
- `npm install`, then `npm run build`.
748
+ **Build fails with type errors** — TypeScript 6 is required. Run `npm install`,
749
+ then `npm run build`.
591
750
 
592
751
  **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.
752
+ are on Node `^22.13.0 || >=24.0.0`. CI tests both versions.
594
753
 
595
754
  **Custom element already defined** — Each `defineSpectre*()` helper is
596
755
  idempotent; calling it twice is safe. If you see conflicts, two different
597
756
  versions of this package may be loaded in the same page.
598
757
 
599
- **Styles are not applying** — The Spectre CSS layers must load before
600
- components are registered. Import `@phcdevworks/spectre-tokens/index.css` and
758
+ **Styles are not applying** — The Spectre CSS layers must load before components
759
+ are registered. Import `@phcdevworks/spectre-tokens/index.css` and
601
760
  `@phcdevworks/spectre-ui/index.css` at the top of your entry module.
602
761
 
603
762
  **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.
763
+ properties as attributes. Use a `ref` to set properties imperatively, or upgrade
764
+ to React 19 which supports custom elements fully.
606
765
 
607
766
  ## Validation
608
767
 
@@ -612,8 +771,9 @@ Run the full validation gate before any pull request:
612
771
  npm run check
613
772
  ```
614
773
 
615
- This runs: lint → typecheck → tests → build → export validation. All steps
616
- must pass.
774
+ This runs: lint → typecheck → tests → build → export validation → contract
775
+ validation → invariant checks → ecosystem manifest validation. All steps must
776
+ pass.
617
777
 
618
778
  ## AI and automation boundaries
619
779
 
@@ -640,10 +800,10 @@ Contribution boundaries:
640
800
 
641
801
  - Components must consume `@phcdevworks/spectre-ui` class helpers — do not
642
802
  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.
803
+ - Design values must come from `@phcdevworks/spectre-tokens` — do not hardcode
804
+ colors, spacing, or other visual primitives.
805
+ - Component tags, properties, events, slots, and ARIA behavior are stable API —
806
+ breaking changes require a semver major bump.
647
807
  - Render in light DOM only — Shadow DOM changes require design-system approval.
648
808
  - No framework-specific code — no JSX, SFCs, or Astro components in this
649
809
  package.