@dillingerstaffing/strand-ui 0.2.0 → 0.2.1

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.
@@ -0,0 +1,753 @@
1
+ # Strand HTML Reference
2
+
3
+ CSS class API for using Strand UI components as raw HTML. Use this when building without the Preact/React library, for static sites, documentation pages, or server-rendered HTML.
4
+
5
+ ## Required CSS
6
+
7
+ ```html
8
+ <link rel="stylesheet" href="path/to/@dillingerstaffing/strand/css/tokens.css">
9
+ <link rel="stylesheet" href="path/to/@dillingerstaffing/strand-ui/dist/css/strand-ui.css">
10
+ ```
11
+
12
+ ## Presentation Mode
13
+
14
+ Wrap static previews in `.strand-static` to render at full visual fidelity without interaction:
15
+
16
+ ```html
17
+ <div class="strand-static">
18
+ <button class="strand-btn strand-btn--primary strand-btn--md" disabled>
19
+ <span class="strand-btn__content">Submit</span>
20
+ </button>
21
+ </div>
22
+ ```
23
+
24
+ The `disabled` attribute prevents interaction. `.strand-static` overrides the disabled visual styling so components appear at full opacity. All transitions and animations are also suppressed inside `.strand-static`.
25
+
26
+ ## Recessed Viewport
27
+
28
+ Use `.strand-viewport` for component previews and showcase containers. It creates a recessed instrument panel effect (sits below the card surface):
29
+
30
+ ```html
31
+ <div class="strand-viewport strand-static">
32
+ <!-- component previews here -->
33
+ </div>
34
+ ```
35
+
36
+ Provides: `--strand-surface-recessed` background, inset shadow, `--strand-radius-lg` corners, `--strand-space-6` padding.
37
+
38
+ ## Padding Tiers
39
+
40
+ Card padding tiers (used via `strand-card--pad-{sm|md|lg}`):
41
+ - `sm`: 16px (compact widgets, dense UIs)
42
+ - `md`: 24px (standard cards, default for most contexts)
43
+ - `lg`: 40px (showcase, hero, documentation)
44
+
45
+ ## Focus States
46
+
47
+ All interactive elements include `:focus-visible` styling per Part XII of the design language:
48
+
49
+ ```css
50
+ :focus-visible {
51
+ outline: 2px solid var(--strand-blue-primary);
52
+ outline-offset: 2px;
53
+ }
54
+ ```
55
+
56
+ - Appears on keyboard navigation only (not on mouse click)
57
+ - 2px solid blue outline with 2px offset from the element edge
58
+ - Applied to: buttons, links, interactive cards, tab buttons, sort buttons, nav links, hamburger menu
59
+
60
+ No additional classes needed. Focus rings are built into each component's CSS.
61
+
62
+ ## Boundary Integrity
63
+
64
+ All container components (Grid, Stack, Card, Container) enforce boundary integrity. Children cannot visually breach the parent's padding zone. This is enforced at the CSS level via `overflow: hidden`, `max-width: 100%`, and `min-width: 0` on children. You do not need to add these yourself.
65
+
66
+ ---
67
+
68
+ ## Input Components
69
+
70
+ ### Button
71
+
72
+ ```html
73
+ <button class="strand-btn strand-btn--primary strand-btn--md" type="button">
74
+ <span class="strand-btn__content">Label</span>
75
+ </button>
76
+ ```
77
+
78
+ **Variants:** `strand-btn--primary` | `strand-btn--secondary` | `strand-btn--ghost` | `strand-btn--danger`
79
+ **Sizes:** `strand-btn--sm` (32px) | `strand-btn--md` (40px) | `strand-btn--lg` (48px)
80
+ **Modifiers:** `strand-btn--full-width` | `strand-btn--icon-only`
81
+ **Loading:** Add class `strand-btn--loading` + `<span class="strand-btn__spinner" aria-hidden="true"></span>` before content. Set content span `style="visibility:hidden"`.
82
+ **Disabled:** Add `disabled` attribute. Use `.strand-static` parent to show full opacity.
83
+
84
+ ---
85
+
86
+ ### Input
87
+
88
+ ```html
89
+ <div class="strand-input">
90
+ <input type="text" class="strand-input__field" placeholder="Enter text">
91
+ </div>
92
+ ```
93
+
94
+ **States:** `strand-input--error` | `strand-input--disabled`
95
+ **Addons:** Add `strand-input--has-leading` to wrapper + `<span class="strand-input__leading" aria-hidden="true">$</span>` before the input. Add `strand-input--has-trailing` to wrapper + `<span class="strand-input__trailing" aria-hidden="true">kg</span>` after the input.
96
+ **Error:** Add `strand-input--error` to wrapper and `aria-invalid="true"` to the input element.
97
+ **Disabled:** Add `strand-input--disabled` to wrapper and `disabled` to the input element.
98
+
99
+ ---
100
+
101
+ ### Textarea
102
+
103
+ ```html
104
+ <div class="strand-textarea">
105
+ <textarea class="strand-textarea__field" placeholder="Enter text"></textarea>
106
+ </div>
107
+ ```
108
+
109
+ **States:** `strand-textarea--error` | `strand-textarea--disabled` | `strand-textarea--auto-resize`
110
+ **Character count:** Add `<span class="strand-textarea__count" aria-live="polite">0/500</span>` after the textarea. Set `maxlength` on the textarea.
111
+ **Error:** Add `strand-textarea--error` to wrapper and `aria-invalid="true"` to the textarea.
112
+ **Disabled:** Add `strand-textarea--disabled` to wrapper and `disabled` to the textarea.
113
+
114
+ ---
115
+
116
+ ### Select
117
+
118
+ ```html
119
+ <div class="strand-select">
120
+ <select class="strand-select__field">
121
+ <option value="" disabled>Choose one</option>
122
+ <option value="a">Option A</option>
123
+ <option value="b">Option B</option>
124
+ </select>
125
+ <span class="strand-select__arrow" aria-hidden="true"></span>
126
+ </div>
127
+ ```
128
+
129
+ **States:** `strand-select--error` | `strand-select--disabled`
130
+ **Error:** Add `strand-select--error` to wrapper and `aria-invalid="true"` to the select.
131
+ **Disabled:** Add `strand-select--disabled` to wrapper and `disabled` to the select.
132
+ **Note:** The `strand-select__arrow` span renders the dropdown caret via CSS borders. Always include it.
133
+
134
+ ---
135
+
136
+ ### Checkbox
137
+
138
+ ```html
139
+ <label class="strand-checkbox strand-checkbox--checked">
140
+ <input type="checkbox" class="strand-checkbox__native" checked role="checkbox" aria-checked="true">
141
+ <span class="strand-checkbox__control" aria-hidden="true">
142
+ <svg class="strand-checkbox__icon" viewBox="0 0 16 16" fill="none">
143
+ <path d="M3.5 8L6.5 11L12.5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
144
+ </svg>
145
+ </span>
146
+ <span class="strand-checkbox__label">Accept terms</span>
147
+ </label>
148
+ ```
149
+
150
+ **States:** `strand-checkbox--checked` | `strand-checkbox--indeterminate` | `strand-checkbox--disabled`
151
+ **Unchecked:** Omit `strand-checkbox--checked` and the SVG inside `strand-checkbox__control`. Set `aria-checked="false"`.
152
+ **Indeterminate:** Use `strand-checkbox--indeterminate` + `aria-checked="mixed"` + indeterminate SVG: `<svg class="strand-checkbox__icon" viewBox="0 0 16 16" fill="none"><line x1="4" y1="8" x2="12" y2="8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>`
153
+ **Note:** The native input is visually hidden via CSS. The `strand-checkbox__control` span provides the custom visual. SVG is required for the check/dash icon.
154
+
155
+ ---
156
+
157
+ ### Radio
158
+
159
+ ```html
160
+ <label class="strand-radio strand-radio--checked">
161
+ <input type="radio" class="strand-radio__native" name="group" value="a" checked>
162
+ <span class="strand-radio__control" aria-hidden="true">
163
+ <span class="strand-radio__dot"></span>
164
+ </span>
165
+ <span class="strand-radio__label">Option A</span>
166
+ </label>
167
+ ```
168
+
169
+ **States:** `strand-radio--checked` | `strand-radio--disabled`
170
+ **Unchecked:** Omit `strand-radio--checked`. The dot is hidden via `transform: scale(0)` in CSS.
171
+ **Note:** The native input is visually hidden. The dot scales up when `strand-radio--checked` is present. Group radios with the same `name` attribute.
172
+
173
+ ---
174
+
175
+ ### Switch
176
+
177
+ ```html
178
+ <label class="strand-switch strand-switch--checked">
179
+ <button type="button" role="switch" class="strand-switch__track" aria-checked="true">
180
+ <span class="strand-switch__thumb" aria-hidden="true"></span>
181
+ </button>
182
+ <span class="strand-switch__label">Dark mode</span>
183
+ </label>
184
+ ```
185
+
186
+ **States:** `strand-switch--checked` | `strand-switch--disabled`
187
+ **Off:** Omit `strand-switch--checked`. Set `aria-checked="false"`.
188
+ **Disabled:** Add `strand-switch--disabled` to the label and `disabled` to the button.
189
+ **Note:** The track is a `<button>` with `role="switch"`. The thumb translates right when checked via CSS.
190
+
191
+ ---
192
+
193
+ ### Slider
194
+
195
+ ```html
196
+ <div class="strand-slider">
197
+ <input type="range" class="strand-slider__field" min="0" max="100" step="1" value="50"
198
+ aria-valuemin="0" aria-valuemax="100" aria-valuenow="50">
199
+ </div>
200
+ ```
201
+
202
+ **States:** `strand-slider--disabled`
203
+ **Disabled:** Add `strand-slider--disabled` to wrapper and `disabled` to the input.
204
+ **Note:** Thumb styling uses both `-webkit-slider-thumb` and `-moz-range-thumb` pseudo-elements in the CSS.
205
+
206
+ ---
207
+
208
+ ### FormField
209
+
210
+ ```html
211
+ <div class="strand-form-field">
212
+ <label class="strand-form-field__label" for="email">
213
+ Email
214
+ <span class="strand-form-field__required" aria-hidden="true">*</span>
215
+ </label>
216
+ <div class="strand-form-field__control">
217
+ <!-- Place any input component here -->
218
+ <div class="strand-input">
219
+ <input type="email" id="email" class="strand-input__field" placeholder="you@example.com">
220
+ </div>
221
+ </div>
222
+ <p class="strand-form-field__hint" id="email-hint">We will never share your email.</p>
223
+ </div>
224
+ ```
225
+
226
+ **States:** `strand-form-field--error`
227
+ **Error:** Replace `strand-form-field__hint` with `<p class="strand-form-field__error" id="email-error" role="alert">Invalid email address.</p>` and add `strand-form-field--error` to the wrapper.
228
+ **Required indicator:** Include `<span class="strand-form-field__required" aria-hidden="true">*</span>` inside the label.
229
+ **Note:** The `for` attribute on the label must match the `id` on the input control.
230
+
231
+ ---
232
+
233
+ ## Display Components
234
+
235
+ ### Card
236
+
237
+ ```html
238
+ <div class="strand-card strand-card--elevated strand-card--pad-md">
239
+ Card content here.
240
+ </div>
241
+ ```
242
+
243
+ **Variants:** `strand-card--elevated` | `strand-card--outlined` | `strand-card--interactive`
244
+ **Padding:** `strand-card--pad-none` | `strand-card--pad-sm` | `strand-card--pad-md` | `strand-card--pad-lg`
245
+ **Note:** `strand-card--interactive` adds hover lift and cursor pointer. Use for clickable cards.
246
+
247
+ ---
248
+
249
+ ### Badge
250
+
251
+ **Inline (standalone):**
252
+
253
+ ```html
254
+ <span class="strand-badge strand-badge--inline">
255
+ <span class="strand-badge__indicator strand-badge--count strand-badge--blue" role="status" aria-label="5 notifications">5</span>
256
+ </span>
257
+ ```
258
+
259
+ **Wrapping a child (positioned at top-right):**
260
+
261
+ ```html
262
+ <span class="strand-badge">
263
+ <!-- Wrapped element (e.g., icon, avatar) -->
264
+ <span style="width:24px;height:24px;display:inline-block;background:#ccc;border-radius:4px;"></span>
265
+ <span class="strand-badge__indicator strand-badge--dot strand-badge--red" role="status" aria-label="Status indicator"></span>
266
+ </span>
267
+ ```
268
+
269
+ **Display modes:** `strand-badge--dot` (8px circle, no text) | `strand-badge--count` (pill with number)
270
+ **Colors:** `strand-badge--default` | `strand-badge--teal` | `strand-badge--blue` | `strand-badge--amber` | `strand-badge--red`
271
+ **Note:** When wrapping children, omit `strand-badge--inline`. The indicator auto-positions to top-right via `position: absolute`.
272
+
273
+ ---
274
+
275
+ ### Avatar
276
+
277
+ **With image:**
278
+
279
+ ```html
280
+ <div class="strand-avatar strand-avatar--md" role="img" aria-label="Jane Smith">
281
+ <img class="strand-avatar__img" src="photo.jpg" alt="Jane Smith">
282
+ </div>
283
+ ```
284
+
285
+ **With initials (fallback):**
286
+
287
+ ```html
288
+ <div class="strand-avatar strand-avatar--md" role="img" aria-label="JS">
289
+ <span class="strand-avatar__initials" aria-hidden="true">JS</span>
290
+ </div>
291
+ ```
292
+
293
+ **Sizes:** `strand-avatar--sm` (32px) | `strand-avatar--md` (40px) | `strand-avatar--lg` (48px) | `strand-avatar--xl` (64px)
294
+ **Note:** Initials are auto-uppercased via CSS. Use 1 or 2 characters.
295
+
296
+ ---
297
+
298
+ ### Tag
299
+
300
+ ```html
301
+ <span class="strand-tag strand-tag--solid strand-tag--blue">
302
+ <span class="strand-tag__text">Design</span>
303
+ </span>
304
+ ```
305
+
306
+ **Variants:** `strand-tag--solid` | `strand-tag--outlined`
307
+ **Colors:** `strand-tag--default` | `strand-tag--teal` | `strand-tag--blue` | `strand-tag--amber` | `strand-tag--red`
308
+ **Removable:** Add a remove button after the text span:
309
+
310
+ ```html
311
+ <span class="strand-tag strand-tag--solid strand-tag--blue">
312
+ <span class="strand-tag__text">Design</span>
313
+ <button type="button" class="strand-tag__remove" aria-label="Remove">
314
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
315
+ <path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
316
+ </svg>
317
+ </button>
318
+ </span>
319
+ ```
320
+
321
+ **Note:** Color classes combine with variant classes (e.g., `strand-tag--solid strand-tag--blue`). The remove icon SVG is required.
322
+
323
+ ---
324
+
325
+ ### Table
326
+
327
+ ```html
328
+ <div class="strand-table-wrapper">
329
+ <table class="strand-table">
330
+ <thead class="strand-table__head">
331
+ <tr>
332
+ <th class="strand-table__th">Name</th>
333
+ <th class="strand-table__th">Role</th>
334
+ <th class="strand-table__th">Status</th>
335
+ </tr>
336
+ </thead>
337
+ <tbody class="strand-table__body">
338
+ <tr class="strand-table__row">
339
+ <td class="strand-table__td">Alice</td>
340
+ <td class="strand-table__td">Engineer</td>
341
+ <td class="strand-table__td">Active</td>
342
+ </tr>
343
+ <tr class="strand-table__row">
344
+ <td class="strand-table__td">Bob</td>
345
+ <td class="strand-table__td">Designer</td>
346
+ <td class="strand-table__td">Away</td>
347
+ </tr>
348
+ </tbody>
349
+ </table>
350
+ </div>
351
+ ```
352
+
353
+ **Sortable column header:**
354
+
355
+ ```html
356
+ <th class="strand-table__th">
357
+ <button type="button" class="strand-table__sort-btn" aria-label="Sort by Name">
358
+ Name <span class="strand-table__sort-indicator" aria-hidden="true">&#8597;</span>
359
+ </button>
360
+ </th>
361
+ ```
362
+
363
+ **Sort indicators:** `&#8593;` (ascending) | `&#8595;` (descending) | `&#8597;` (unsorted)
364
+ **Note:** `strand-table-wrapper` provides horizontal scroll on overflow. Rows highlight on hover.
365
+
366
+ ---
367
+
368
+ ### DataReadout
369
+
370
+ ```html
371
+ <!-- Default (md) -->
372
+ <div class="strand-data-readout">
373
+ <span class="strand-data-readout__label">Revenue</span>
374
+ <span class="strand-data-readout__value">$142,800</span>
375
+ </div>
376
+
377
+ <!-- Small (compact dashboards, sidebar metrics) -->
378
+ <div class="strand-data-readout strand-data-readout--sm">
379
+ <span class="strand-data-readout__label">Users</span>
380
+ <span class="strand-data-readout__value">12.8K</span>
381
+ </div>
382
+
383
+ <!-- Large (hero metrics, feature highlights) -->
384
+ <div class="strand-data-readout strand-data-readout--lg">
385
+ <span class="strand-data-readout__label">Total Revenue</span>
386
+ <span class="strand-data-readout__value">$1.2M</span>
387
+ </div>
388
+ ```
389
+
390
+ **Sizes:** `--sm` (text-xl, 25px) | default (text-3xl, 39px) | `--lg` (text-4xl, 49px). Label stays xs across all sizes. Use `--sm` in compact cards and dense data views. Use `--lg` for hero and landing page metrics.
391
+
392
+ ---
393
+
394
+ ## Layout Components
395
+
396
+ ### Stack
397
+
398
+ ```html
399
+ <div class="strand-stack strand-stack--vertical" style="gap: var(--strand-space-4);">
400
+ <div>Item 1</div>
401
+ <div>Item 2</div>
402
+ <div>Item 3</div>
403
+ </div>
404
+ ```
405
+
406
+ **Direction:** `strand-stack--vertical` | `strand-stack--horizontal`
407
+ **Gap (utility classes):** `strand-stack--gap-1` | `strand-stack--gap-2` | `strand-stack--gap-3` | `strand-stack--gap-4` | `strand-stack--gap-5` | `strand-stack--gap-6` | `strand-stack--gap-8`
408
+ **Alignment:** `strand-stack--align-start` | `strand-stack--align-center` | `strand-stack--align-end` (default is stretch)
409
+ **Justification:** `strand-stack--justify-start` | `strand-stack--justify-center` | `strand-stack--justify-end` | `strand-stack--justify-between` | `strand-stack--justify-around`
410
+ **Wrap:** `strand-stack--wrap`
411
+ **Note:** Use either the gap utility class (`strand-stack--gap-4`) or inline `style="gap: var(--strand-space-4)"`. The React component uses inline style; utility classes are available for pure HTML.
412
+
413
+ ---
414
+
415
+ ### Grid
416
+
417
+ ```html
418
+ <div class="strand-grid strand-grid--cols-3 strand-grid--gap-4">
419
+ <div>Cell 1</div>
420
+ <div>Cell 2</div>
421
+ <div>Cell 3</div>
422
+ </div>
423
+ ```
424
+
425
+ **Columns (utility classes):** `strand-grid--cols-2` | `strand-grid--cols-3` | `strand-grid--cols-4`
426
+ **Gap (utility classes):** `strand-grid--gap-1` | `strand-grid--gap-2` | `strand-grid--gap-3` | `strand-grid--gap-4` | `strand-grid--gap-5` | `strand-grid--gap-6` | `strand-grid--gap-8`
427
+ **Note:** For column counts beyond 4, use inline style: `style="grid-template-columns: repeat(6, 1fr); gap: var(--strand-space-4);"`. The React component always uses inline style.
428
+
429
+ ---
430
+
431
+ ### Container
432
+
433
+ ```html
434
+ <div class="strand-container strand-container--default">
435
+ Centered content with max-width constraint.
436
+ </div>
437
+ ```
438
+
439
+ **Sizes:** `strand-container--narrow` | `strand-container--default` | `strand-container--wide` | `strand-container--full`
440
+ **Note:** Centers content with `margin-inline: auto` and responsive horizontal padding via `clamp(1.5rem, 5vw, 4rem)`.
441
+
442
+ ---
443
+
444
+ ### Divider
445
+
446
+ **Horizontal (default):**
447
+
448
+ ```html
449
+ <hr class="strand-divider strand-divider--horizontal" role="separator" aria-orientation="horizontal">
450
+ ```
451
+
452
+ **Horizontal with label:**
453
+
454
+ ```html
455
+ <div class="strand-divider strand-divider--horizontal strand-divider--labeled" role="separator" aria-orientation="horizontal">
456
+ <span class="strand-divider__line"></span>
457
+ <span class="strand-divider__label">Or</span>
458
+ <span class="strand-divider__line"></span>
459
+ </div>
460
+ ```
461
+
462
+ **Vertical:**
463
+
464
+ ```html
465
+ <div class="strand-divider strand-divider--vertical" role="separator" aria-orientation="vertical"></div>
466
+ ```
467
+
468
+ **Note:** Plain horizontal uses `<hr>`. Labeled horizontal uses `<div>` with two `__line` spans flanking the label. Vertical dividers use `align-self: stretch` to fill the parent height.
469
+
470
+ ---
471
+
472
+ ### Section
473
+
474
+ ```html
475
+ <section class="strand-section strand-section--standard strand-section--bg-primary">
476
+ <div class="strand-container strand-container--default">
477
+ Section content here.
478
+ </div>
479
+ </section>
480
+ ```
481
+
482
+ **Variants:** `strand-section--standard` | `strand-section--hero`
483
+ **Backgrounds:** `strand-section--bg-primary` | `strand-section--bg-elevated` | `strand-section--bg-recessed`
484
+ **Note:** `standard` uses responsive vertical padding `clamp(4rem, 8vw, 8rem)`. `hero` uses `clamp(6rem, 12vw, 12rem)`. Pair with `strand-container` inside for horizontal constraints.
485
+
486
+ ---
487
+
488
+ ## Navigation Components
489
+
490
+ ### Link
491
+
492
+ ```html
493
+ <a href="/about" class="strand-link">About us</a>
494
+ ```
495
+
496
+ **External:** Add `target="_blank"` and `rel="noopener noreferrer"`.
497
+ **Note:** The underline animates on hover from 0% to 100% width via `background-size`. No variant or modifier classes.
498
+
499
+ ---
500
+
501
+ ### Tabs
502
+
503
+ ```html
504
+ <div class="strand-tabs">
505
+ <div role="tablist">
506
+ <button id="tab-overview" role="tab" type="button" class="strand-tabs__tab strand-tabs__tab--active"
507
+ aria-selected="true" aria-controls="panel-overview" tabindex="0">Overview</button>
508
+ <button id="tab-specs" role="tab" type="button" class="strand-tabs__tab"
509
+ aria-selected="false" aria-controls="panel-specs" tabindex="-1">Specs</button>
510
+ <button id="tab-reviews" role="tab" type="button" class="strand-tabs__tab"
511
+ aria-selected="false" aria-controls="panel-reviews" tabindex="-1">Reviews</button>
512
+ </div>
513
+
514
+ <div id="panel-overview" role="tabpanel" aria-labelledby="tab-overview" tabindex="0">
515
+ Overview content here.
516
+ </div>
517
+ <div id="panel-specs" role="tabpanel" aria-labelledby="tab-specs" tabindex="0" hidden>
518
+ Specs content here.
519
+ </div>
520
+ <div id="panel-reviews" role="tabpanel" aria-labelledby="tab-reviews" tabindex="0" hidden>
521
+ Reviews content here.
522
+ </div>
523
+ </div>
524
+ ```
525
+
526
+ **Active tab:** Add `strand-tabs__tab--active` + `aria-selected="true"` + `tabindex="0"`.
527
+ **Inactive tabs:** Set `aria-selected="false"` + `tabindex="-1"`. Add `hidden` to their panels.
528
+ **Note:** The tablist has a bottom border. Active tab shows a blue bottom border. Wire `aria-controls` / `aria-labelledby` IDs correctly. Arrow key navigation requires JavaScript.
529
+
530
+ ---
531
+
532
+ ### Breadcrumb
533
+
534
+ ```html
535
+ <nav aria-label="Breadcrumb" class="strand-breadcrumb">
536
+ <ol class="strand-breadcrumb__list">
537
+ <li class="strand-breadcrumb__item">
538
+ <a href="/" class="strand-breadcrumb__link">Home</a>
539
+ </li>
540
+ <li class="strand-breadcrumb__item">
541
+ <span class="strand-breadcrumb__separator" aria-hidden="true">/</span>
542
+ <a href="/products" class="strand-breadcrumb__link">Products</a>
543
+ </li>
544
+ <li class="strand-breadcrumb__item">
545
+ <span class="strand-breadcrumb__separator" aria-hidden="true">/</span>
546
+ <span class="strand-breadcrumb__current" aria-current="page">Widget</span>
547
+ </li>
548
+ </ol>
549
+ </nav>
550
+ ```
551
+
552
+ **Note:** The last item uses `strand-breadcrumb__current` with `aria-current="page"` instead of a link. Separators use `aria-hidden="true"`. The first item has no separator.
553
+
554
+ ---
555
+
556
+ ### Nav
557
+
558
+ ```html
559
+ <nav class="strand-nav" aria-label="Main navigation">
560
+ <div class="strand-nav__inner">
561
+ <div class="strand-nav__logo">
562
+ <!-- Logo element (image, SVG, or text) -->
563
+ <strong>Brand</strong>
564
+ </div>
565
+ <div class="strand-nav__items">
566
+ <a href="/" class="strand-nav__link strand-nav__link--active" aria-current="page">Home</a>
567
+ <a href="/about" class="strand-nav__link">About</a>
568
+ <a href="/contact" class="strand-nav__link">Contact</a>
569
+ </div>
570
+ <div class="strand-nav__actions">
571
+ <!-- Action buttons or elements -->
572
+ <button class="strand-btn strand-btn--primary strand-btn--sm" type="button">
573
+ <span class="strand-btn__content">Sign in</span>
574
+ </button>
575
+ </div>
576
+ <button type="button" class="strand-nav__hamburger" aria-expanded="false" aria-label="Menu">
577
+ <span class="strand-nav__hamburger-icon" aria-hidden="true"></span>
578
+ </button>
579
+ </div>
580
+ <!-- Mobile menu (hidden by default, shown via JS toggling display) -->
581
+ <div class="strand-nav__mobile-menu" style="display:none;">
582
+ <a href="/" class="strand-nav__mobile-link strand-nav__mobile-link--active" aria-current="page">Home</a>
583
+ <a href="/about" class="strand-nav__mobile-link">About</a>
584
+ <a href="/contact" class="strand-nav__mobile-link">Contact</a>
585
+ </div>
586
+ </nav>
587
+ ```
588
+
589
+ **Active link:** `strand-nav__link--active` (desktop) | `strand-nav__mobile-link--active` (mobile)
590
+ **Note:** The nav bar is 64px tall. Desktop items and actions hide below 768px; the hamburger and mobile menu show instead. The hamburger icon is a CSS-only three-line icon (middle line + `::before`/`::after` pseudo-elements). Toggle mobile menu visibility with JavaScript.
591
+
592
+ ---
593
+
594
+ ## Feedback Components
595
+
596
+ ### Toast
597
+
598
+ ```html
599
+ <div class="strand-toast strand-toast--info" role="status" aria-live="polite">
600
+ <span class="strand-toast__message">Changes saved successfully.</span>
601
+ <button type="button" class="strand-toast__dismiss" aria-label="Dismiss">&times;</button>
602
+ </div>
603
+ ```
604
+
605
+ **Statuses:** `strand-toast--info` | `strand-toast--success` | `strand-toast--warning` | `strand-toast--error`
606
+ **Toast container (for stacking multiple toasts):**
607
+
608
+ ```html
609
+ <div class="strand-toast__container">
610
+ <!-- Toasts stack here, newest at bottom -->
611
+ </div>
612
+ ```
613
+
614
+ **Note:** Container is fixed to bottom-right (`position: fixed`). Use `aria-live="assertive"` for `warning` and `error` statuses. Each status sets a colored left border accent. Dismiss button renders `&times;`.
615
+
616
+ ---
617
+
618
+ ### Alert
619
+
620
+ ```html
621
+ <div class="strand-alert strand-alert--info" role="status">
622
+ <div class="strand-alert__content">This is an informational message.</div>
623
+ </div>
624
+ ```
625
+
626
+ **Statuses:** `strand-alert--info` | `strand-alert--success` | `strand-alert--warning` | `strand-alert--error`
627
+ **Dismissible:** Add a dismiss button after content:
628
+
629
+ ```html
630
+ <div class="strand-alert strand-alert--warning" role="alert">
631
+ <div class="strand-alert__content">Disk space running low.</div>
632
+ <button type="button" class="strand-alert__dismiss" aria-label="Dismiss">&times;</button>
633
+ </div>
634
+ ```
635
+
636
+ **Note:** Use `role="alert"` for `warning` and `error`, `role="status"` for `info` and `success`. Each status has a colored left border and tinted background.
637
+
638
+ ---
639
+
640
+ ### Dialog
641
+
642
+ ```html
643
+ <div class="strand-dialog__backdrop">
644
+ <div class="strand-dialog__panel" role="dialog" aria-modal="true" aria-labelledby="dialog-title" tabindex="-1">
645
+ <div class="strand-dialog__header">
646
+ <h2 id="dialog-title" class="strand-dialog__title">Confirm action</h2>
647
+ </div>
648
+ <button type="button" class="strand-dialog__close" aria-label="Close">&times;</button>
649
+ <div class="strand-dialog__body">
650
+ Are you sure you want to proceed?
651
+ </div>
652
+ </div>
653
+ </div>
654
+ ```
655
+
656
+ **Note:** The backdrop covers the viewport with a semi-transparent overlay (`position: fixed; inset: 0`). The panel is centered, max-width 560px, with elevation shadow. Focus trap and scroll lock require JavaScript. Omit the `strand-dialog__header` div if no title is needed.
657
+
658
+ ---
659
+
660
+ ### Tooltip
661
+
662
+ ```html
663
+ <span class="strand-tooltip__wrapper" aria-describedby="tip-1">
664
+ Hover me
665
+ <span id="tip-1" class="strand-tooltip strand-tooltip--top strand-tooltip--visible" role="tooltip">
666
+ Helpful tip
667
+ </span>
668
+ </span>
669
+ ```
670
+
671
+ **Positions:** `strand-tooltip--top` | `strand-tooltip--right` | `strand-tooltip--bottom` | `strand-tooltip--left`
672
+ **Visibility:** Add `strand-tooltip--visible` to show. Without it, tooltip has `opacity: 0`.
673
+ **Note:** The wrapper needs `position: relative` (provided by `strand-tooltip__wrapper`). For static display, add `strand-tooltip--visible` directly. For interactive use, toggle that class via JavaScript on hover/focus. Wire `aria-describedby` on the wrapper to the tooltip `id`.
674
+
675
+ ---
676
+
677
+ ### Progress
678
+
679
+ **Determinate bar:**
680
+
681
+ ```html
682
+ <div class="strand-progress strand-progress--bar strand-progress--md" role="progressbar"
683
+ aria-valuemin="0" aria-valuemax="100" aria-valuenow="65">
684
+ <div class="strand-progress__fill" style="width: 65%;"></div>
685
+ </div>
686
+ ```
687
+
688
+ **Indeterminate bar:**
689
+
690
+ ```html
691
+ <div class="strand-progress strand-progress--bar strand-progress--md strand-progress--indeterminate"
692
+ role="progressbar" aria-valuemin="0" aria-valuemax="100">
693
+ <div class="strand-progress__fill"></div>
694
+ </div>
695
+ ```
696
+
697
+ **Determinate ring:**
698
+
699
+ ```html
700
+ <div class="strand-progress strand-progress--ring strand-progress--md" role="progressbar"
701
+ aria-valuemin="0" aria-valuemax="100" aria-valuenow="65">
702
+ <svg width="40" height="40" viewBox="0 0 40 40" class="strand-progress__ring">
703
+ <circle cx="20" cy="20" r="18.5" fill="none" stroke-width="3" class="strand-progress__track"/>
704
+ <circle cx="20" cy="20" r="18.5" fill="none" stroke-width="3"
705
+ stroke-dasharray="116.24" stroke-dashoffset="40.68"
706
+ stroke-linecap="round" class="strand-progress__fill"
707
+ transform="rotate(-90 20 20)"/>
708
+ </svg>
709
+ </div>
710
+ ```
711
+
712
+ **Sizes (bar):** `strand-progress--sm` (4px) | `strand-progress--md` (8px) | `strand-progress--lg` (12px)
713
+ **Sizes (ring):** `strand-progress--sm` (24px) | `strand-progress--md` (40px) | `strand-progress--lg` (56px)
714
+ **Note:** For indeterminate bars, omit `aria-valuenow`; the fill animates via CSS. For rings, compute `stroke-dasharray` as `2 * PI * radius` and `stroke-dashoffset` as `dasharray * (1 - value/100)`. Ring dimensions by size: sm=24 (r=10.5), md=40 (r=18.5), lg=56 (r=26.5). Stroke width is always 3.
715
+
716
+ ---
717
+
718
+ ### Spinner
719
+
720
+ ```html
721
+ <span class="strand-spinner strand-spinner--md" role="status">
722
+ <span class="strand-spinner__ring" aria-hidden="true"></span>
723
+ <span class="strand-spinner__sr-only">Loading</span>
724
+ </span>
725
+ ```
726
+
727
+ **Sizes:** `strand-spinner--sm` (16px) | `strand-spinner--md` (20px) | `strand-spinner--lg` (32px)
728
+ **Note:** The ring animates a spinning border. `strand-spinner__sr-only` provides accessible text (visually hidden, read by screen readers). Always include it.
729
+
730
+ ---
731
+
732
+ ### Skeleton
733
+
734
+ ```html
735
+ <div class="strand-skeleton strand-skeleton--text strand-skeleton--shimmer" aria-hidden="true"
736
+ style="width: 100%; height: 1em;"></div>
737
+ ```
738
+
739
+ **Variants:** `strand-skeleton--text` (4px border-radius, 1em default height) | `strand-skeleton--rectangle` (md border-radius) | `strand-skeleton--circle` (full border-radius)
740
+ **Shimmer:** Always add `strand-skeleton--shimmer` for the animated gradient effect.
741
+ **Sizing:** Set `width` and `height` via inline `style`. For circles, set equal width and height.
742
+
743
+ ```html
744
+ <!-- Rectangle -->
745
+ <div class="strand-skeleton strand-skeleton--rectangle strand-skeleton--shimmer" aria-hidden="true"
746
+ style="width: 200px; height: 120px;"></div>
747
+
748
+ <!-- Circle -->
749
+ <div class="strand-skeleton strand-skeleton--circle strand-skeleton--shimmer" aria-hidden="true"
750
+ style="width: 48px; height: 48px;"></div>
751
+ ```
752
+
753
+ **Note:** Always include `aria-hidden="true"`. Skeletons are placeholder visuals, not interactive.
@@ -5,6 +5,8 @@ export interface DataReadoutProps extends Omit<JSX.HTMLAttributes<HTMLDivElement
5
5
  label: string;
6
6
  /** The large displayed value */
7
7
  value: string | number;
8
+ /** Size variant: sm (compact), md (default), lg (hero) */
9
+ size?: "sm" | "md" | "lg";
8
10
  }
9
11
  export declare const DataReadout: import("preact").FunctionalComponent<import("preact/compat").PropsWithoutRef<DataReadoutProps> & {
10
12
  ref?: import("preact").Ref<HTMLDivElement> | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"DataReadout.d.ts","sourceRoot":"","sources":["../../../src/components/DataReadout/DataReadout.tsx"],"names":[],"mappings":"AAAA,sDAAsD;AAEtD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAGlC,MAAM,WAAW,gBACf,SAAQ,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzD,0BAA0B;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,WAAW;;EAavB,CAAC"}
1
+ {"version":3,"file":"DataReadout.d.ts","sourceRoot":"","sources":["../../../src/components/DataReadout/DataReadout.tsx"],"names":[],"mappings":"AAAA,sDAAsD;AAEtD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAGlC,MAAM,WAAW,gBACf,SAAQ,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzD,0BAA0B;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,0DAA0D;IAC1D,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;CAC3B;AAED,eAAO,MAAM,WAAW;;EAiBvB,CAAC"}
@@ -693,6 +693,15 @@
693
693
  font-variant-numeric: tabular-nums;
694
694
  }
695
695
 
696
+ /* ── Size variants ── */
697
+ .strand-data-readout--sm .strand-data-readout__value {
698
+ font-size: var(--strand-text-xl);
699
+ }
700
+
701
+ .strand-data-readout--lg .strand-data-readout__value {
702
+ font-size: var(--strand-text-4xl);
703
+ }
704
+
696
705
 
697
706
  /* Dialog */
698
707
  /*! Strand UI | MIT License | dillingerstaffing.com */
package/dist/index.js CHANGED
@@ -499,7 +499,7 @@ const V = p(
499
499
  }
500
500
  );
501
501
  V.displayName = "Avatar";
502
- const z = p(
502
+ const O = p(
503
503
  ({
504
504
  variant: a = "solid",
505
505
  status: t = "default",
@@ -548,8 +548,8 @@ const z = p(
548
548
  ] });
549
549
  }
550
550
  );
551
- z.displayName = "Tag";
552
- const O = p(
551
+ O.displayName = "Tag";
552
+ const z = p(
553
553
  ({ columns: a, data: t, onSort: e, className: n = "", ...i }, o) => {
554
554
  const [r, l] = B(null), [c, u] = B("asc"), d = b(
555
555
  (m) => {
@@ -591,11 +591,15 @@ const O = p(
591
591
  ] }) });
592
592
  }
593
593
  );
594
- O.displayName = "Table";
594
+ z.displayName = "Table";
595
595
  const W = p(
596
- ({ label: a, value: t, className: e = "", ...n }, i) => {
597
- const o = ["strand-data-readout", e].filter(Boolean).join(" ");
598
- return /* @__PURE__ */ _("div", { ref: i, className: o, ...n, children: [
596
+ ({ label: a, value: t, size: e, className: n = "", ...i }, o) => {
597
+ const r = [
598
+ "strand-data-readout",
599
+ e && e !== "md" ? `strand-data-readout--${e}` : "",
600
+ n
601
+ ].filter(Boolean).join(" ");
602
+ return /* @__PURE__ */ _("div", { ref: o, className: r, ...i, children: [
599
603
  /* @__PURE__ */ s("span", { className: "strand-data-readout__label", children: a }),
600
604
  /* @__PURE__ */ s("span", { className: "strand-data-readout__value", children: t })
601
605
  ] });
@@ -1353,9 +1357,9 @@ export {
1353
1357
  pa as Spinner,
1354
1358
  Z as Stack,
1355
1359
  F as Switch,
1356
- O as Table,
1360
+ z as Table,
1357
1361
  ea as Tabs,
1358
- z as Tag,
1362
+ O as Tag,
1359
1363
  A as Textarea,
1360
1364
  ia as Toast,
1361
1365
  ra as ToastProvider,
package/package.json CHANGED
@@ -1,21 +1,10 @@
1
1
  {
2
2
  "name": "@dillingerstaffing/strand-ui",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Strand UI - Preact/React component library built on the Strand Design Language",
5
5
  "author": "Dillinger Staffing <engineering@dillingerstaffing.com> (https://dillingerstaffing.com)",
6
6
  "license": "MIT",
7
- "keywords": [
8
- "design-system",
9
- "ui-components",
10
- "preact",
11
- "react",
12
- "css-custom-properties",
13
- "design-tokens",
14
- "accessibility",
15
- "wcag",
16
- "aria",
17
- "component-library"
18
- ],
7
+ "keywords": ["design-system", "ui-components", "preact", "react", "css-custom-properties", "design-tokens", "accessibility", "wcag", "aria", "component-library"],
19
8
  "homepage": "https://dillingerstaffing.com/labs/strand",
20
9
  "repository": {
21
10
  "type": "git",
@@ -39,11 +28,18 @@
39
28
  "style": "./dist/css/strand-ui.css",
40
29
  "files": [
41
30
  "dist/",
42
- "src/"
31
+ "src/",
32
+ "HTML_REFERENCE.md"
43
33
  ],
44
34
  "sideEffects": [
45
35
  "dist/css/*.css"
46
36
  ],
37
+ "scripts": {
38
+ "build": "vite build && tsc --emitDeclarationOnly && cp ../../HTML_REFERENCE.md ./HTML_REFERENCE.md",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest",
41
+ "test:coverage": "vitest run --coverage"
42
+ },
47
43
  "peerDependencies": {
48
44
  "preact": "^10.0.0"
49
45
  },
@@ -53,7 +49,7 @@
53
49
  }
54
50
  },
55
51
  "dependencies": {
56
- "@dillingerstaffing/strand": "0.2.0"
52
+ "@dillingerstaffing/strand": "workspace:*"
57
53
  },
58
54
  "devDependencies": {
59
55
  "@testing-library/preact": "^3.2.0",
@@ -62,11 +58,5 @@
62
58
  "preact": "^10.25.0",
63
59
  "vite": "^6.0.0",
64
60
  "vitest": "^3.0.0"
65
- },
66
- "scripts": {
67
- "build": "vite build && tsc --emitDeclarationOnly",
68
- "test": "vitest run",
69
- "test:watch": "vitest",
70
- "test:coverage": "vitest run --coverage"
71
61
  }
72
- }
62
+ }
@@ -28,3 +28,12 @@
28
28
  line-height: var(--strand-leading-tight);
29
29
  font-variant-numeric: tabular-nums;
30
30
  }
31
+
32
+ /* ── Size variants ── */
33
+ .strand-data-readout--sm .strand-data-readout__value {
34
+ font-size: var(--strand-text-xl);
35
+ }
36
+
37
+ .strand-data-readout--lg .strand-data-readout__value {
38
+ font-size: var(--strand-text-4xl);
39
+ }
@@ -93,6 +93,42 @@ describe("DataReadout", () => {
93
93
  expect(readout?.className).toContain("custom");
94
94
  });
95
95
 
96
+ // ── Size variants ──
97
+
98
+ it("applies sm size modifier class", () => {
99
+ const { container } = render(
100
+ <DataReadout label="Users" value="12.8K" size="sm" />,
101
+ );
102
+ const readout = container.querySelector(".strand-data-readout");
103
+ expect(readout?.className).toContain("strand-data-readout--sm");
104
+ });
105
+
106
+ it("applies lg size modifier class", () => {
107
+ const { container } = render(
108
+ <DataReadout label="Revenue" value="$1.2M" size="lg" />,
109
+ );
110
+ const readout = container.querySelector(".strand-data-readout");
111
+ expect(readout?.className).toContain("strand-data-readout--lg");
112
+ });
113
+
114
+ it("does not apply size modifier for md (default)", () => {
115
+ const { container } = render(
116
+ <DataReadout label="Metric" value="100" size="md" />,
117
+ );
118
+ const readout = container.querySelector(".strand-data-readout");
119
+ expect(readout?.className).not.toContain("strand-data-readout--md");
120
+ expect(readout?.className).not.toContain("strand-data-readout--sm");
121
+ expect(readout?.className).not.toContain("strand-data-readout--lg");
122
+ });
123
+
124
+ it("does not apply size modifier when size is omitted", () => {
125
+ const { container } = render(
126
+ <DataReadout label="Metric" value="100" />,
127
+ );
128
+ const readout = container.querySelector(".strand-data-readout");
129
+ expect(readout?.className).toBe("strand-data-readout");
130
+ });
131
+
96
132
  // ── Forwarded props ──
97
133
 
98
134
  it("forwards additional props", () => {
@@ -9,11 +9,17 @@ export interface DataReadoutProps
9
9
  label: string;
10
10
  /** The large displayed value */
11
11
  value: string | number;
12
+ /** Size variant: sm (compact), md (default), lg (hero) */
13
+ size?: "sm" | "md" | "lg";
12
14
  }
13
15
 
14
16
  export const DataReadout = forwardRef<HTMLDivElement, DataReadoutProps>(
15
- ({ label, value, className = "", ...rest }, ref) => {
16
- const classes = ["strand-data-readout", className]
17
+ ({ label, value, size, className = "", ...rest }, ref) => {
18
+ const classes = [
19
+ "strand-data-readout",
20
+ size && size !== "md" ? `strand-data-readout--${size}` : "",
21
+ className,
22
+ ]
17
23
  .filter(Boolean)
18
24
  .join(" ");
19
25
 
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Dillinger Staffing (https://dillingerstaffing.com)
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.