stimulus_plumbers 0.4.5 → 0.4.9

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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/README.md +15 -1
  4. data/app/assets/javascripts/stimulus-plumbers/controllers.manifest.json +331 -20
  5. data/app/assets/javascripts/stimulus-plumbers/index.es.js +493 -226
  6. data/app/assets/javascripts/stimulus-plumbers/index.es.js.map +1 -1
  7. data/app/assets/javascripts/stimulus-plumbers/index.umd.js +1 -1
  8. data/app/assets/javascripts/stimulus-plumbers/index.umd.js.map +1 -1
  9. data/app/assets/stylesheets/stimulus_plumbers/tokens.css +58 -53
  10. data/docs/architecture.md +0 -2
  11. data/docs/component/avatar.md +1 -0
  12. data/docs/component/button.md +2 -1
  13. data/docs/component/calendar.md +18 -20
  14. data/docs/component/card.md +14 -12
  15. data/docs/component/checklist.md +153 -0
  16. data/docs/component/combobox.md +2 -2
  17. data/docs/component/form.md +32 -6
  18. data/docs/component/icon.md +7 -5
  19. data/docs/component/indicator.md +55 -0
  20. data/docs/component/list.md +4 -5
  21. data/docs/component/ordered_list.md +118 -0
  22. data/docs/component/plumber.md +43 -0
  23. data/docs/component/popover.md +1 -2
  24. data/docs/component/progress.md +65 -0
  25. data/docs/component/theme.md +15 -2
  26. data/docs/component/timeline.md +49 -14
  27. data/docs/guide.md +39 -0
  28. data/lib/generators/stimulus_plumbers/install/install_generator.rb +33 -0
  29. data/lib/stimulus_plumbers/components/button.rb +2 -1
  30. data/lib/stimulus_plumbers/components/card.rb +1 -1
  31. data/lib/stimulus_plumbers/components/checklist/item/slots.rb +13 -0
  32. data/lib/stimulus_plumbers/components/checklist/item.rb +64 -0
  33. data/lib/stimulus_plumbers/components/checklist.rb +50 -0
  34. data/lib/stimulus_plumbers/components/combobox/typeahead.rb +1 -0
  35. data/lib/stimulus_plumbers/components/combobox.rb +1 -1
  36. data/lib/stimulus_plumbers/components/icon.rb +2 -2
  37. data/lib/stimulus_plumbers/components/indicator.rb +30 -0
  38. data/lib/stimulus_plumbers/components/link.rb +2 -1
  39. data/lib/stimulus_plumbers/components/list/item.rb +1 -1
  40. data/lib/stimulus_plumbers/components/ordered_list/item/slots.rb +13 -0
  41. data/lib/stimulus_plumbers/components/ordered_list/item.rb +129 -0
  42. data/lib/stimulus_plumbers/components/ordered_list.rb +24 -0
  43. data/lib/stimulus_plumbers/components/progress_bar.rb +48 -0
  44. data/lib/stimulus_plumbers/components/progress_meter.rb +37 -0
  45. data/lib/stimulus_plumbers/components/progress_ring.rb +40 -0
  46. data/lib/stimulus_plumbers/components/timeline/event.rb +3 -2
  47. data/lib/stimulus_plumbers/components/timeline/group.rb +5 -3
  48. data/lib/stimulus_plumbers/engine.rb +4 -0
  49. data/lib/stimulus_plumbers/form/fields/inputs/password.rb +1 -0
  50. data/lib/stimulus_plumbers/form/fields/inputs/search.rb +1 -0
  51. data/lib/stimulus_plumbers/generators/css_entrypoint.rb +80 -0
  52. data/lib/stimulus_plumbers/generators/tokens_directive.rb +24 -0
  53. data/lib/stimulus_plumbers/helpers/avatar_helper.rb +1 -7
  54. data/lib/stimulus_plumbers/helpers/button_helper.rb +1 -7
  55. data/lib/stimulus_plumbers/helpers/calendar_helper.rb +2 -9
  56. data/lib/stimulus_plumbers/helpers/calendar_turbo_helper.rb +3 -6
  57. data/lib/stimulus_plumbers/helpers/checklist_helper.rb +11 -0
  58. data/lib/stimulus_plumbers/helpers/indicator_helper.rb +11 -0
  59. data/lib/stimulus_plumbers/helpers/ordered_list_helper.rb +11 -0
  60. data/lib/stimulus_plumbers/helpers/popover_helper.rb +1 -7
  61. data/lib/stimulus_plumbers/helpers/progress_helper.rb +21 -0
  62. data/lib/stimulus_plumbers/helpers.rb +8 -0
  63. data/lib/stimulus_plumbers/plumber/slots.rb +10 -3
  64. data/lib/stimulus_plumbers/themes/base.rb +4 -0
  65. data/lib/stimulus_plumbers/themes/schema/icon.rb +21 -4
  66. data/lib/stimulus_plumbers/themes/schema/indicator/ranges.rb +14 -0
  67. data/lib/stimulus_plumbers/themes/schema.rb +39 -3
  68. data/lib/stimulus_plumbers/version.rb +1 -1
  69. data/lib/stimulus_plumbers.rb +10 -0
  70. data/lib/tasks/stimulus_plumbers.rake +18 -0
  71. data/vendor/ARIA.md +108 -0
  72. data/vendor/component/manifest.json +212 -0
  73. data/vendor/controller/docs/calendar.md +212 -0
  74. data/vendor/controller/docs/checklist.md +45 -0
  75. data/vendor/controller/docs/clipboard.md +61 -0
  76. data/vendor/controller/docs/combobox.md +212 -0
  77. data/vendor/controller/docs/dismisser.md +39 -0
  78. data/vendor/controller/docs/flipper.md +46 -0
  79. data/vendor/controller/docs/input-clearable.md +43 -0
  80. data/vendor/controller/docs/input-formatter.md +89 -0
  81. data/vendor/controller/docs/modal.md +57 -0
  82. data/vendor/controller/docs/panner.md +24 -0
  83. data/vendor/controller/docs/popover.md +111 -0
  84. data/vendor/controller/docs/progress.md +82 -0
  85. data/vendor/controller/docs/reorderable.md +82 -0
  86. data/vendor/controller/docs/timeline.md +84 -0
  87. data/vendor/controller/docs/visibility.md +31 -0
  88. data/vendor/controller/guide.md +16 -0
  89. data/vendor/{controllers.manifest.json → controller/manifest.json} +331 -20
  90. metadata +44 -2
@@ -8,52 +8,52 @@
8
8
 
9
9
  :root {
10
10
  /* ── Semantic colors ─────────────────────────────────────────────────── */
11
- --sp-color-primary: oklch(55% 0.2 250);
12
- --sp-color-primary-fg: oklch(98% 0.01 250);
13
- --sp-color-primary-border: oklch(65% 0.2 250);
14
- --sp-color-primary-ring: oklch(75% 0.15 250);
15
-
16
- --sp-color-secondary: oklch(55% 0.05 250);
17
- --sp-color-secondary-fg: oklch(98% 0.01 250);
18
- --sp-color-secondary-border: oklch(65% 0.05 250);
19
- --sp-color-secondary-ring: oklch(75% 0.05 250);
20
-
21
- --sp-color-destructive: oklch(55% 0.22 25);
22
- --sp-color-destructive-fg: oklch(98% 0.01 25);
23
- --sp-color-destructive-border: oklch(65% 0.22 25);
24
- --sp-color-destructive-ring: oklch(75% 0.18 25);
25
-
26
- --sp-color-success: oklch(52% 0.18 145);
27
- --sp-color-success-fg: oklch(98% 0.01 145);
28
- --sp-color-success-border: oklch(62% 0.18 145);
29
- --sp-color-success-ring: oklch(72% 0.12 145);
30
-
31
- --sp-color-warning: oklch(68% 0.17 65);
32
- --sp-color-warning-fg: oklch(98% 0.01 65);
33
- --sp-color-warning-border: oklch(78% 0.17 65);
34
- --sp-color-warning-ring: oklch(88% 0.10 65);
35
-
36
- --sp-color-info: oklch(55% 0.18 220);
37
- --sp-color-info-fg: oklch(98% 0.01 220);
38
- --sp-color-info-border: oklch(65% 0.18 220);
39
- --sp-color-info-ring: oklch(75% 0.12 220);
40
-
41
- --sp-color-error: oklch(40% 0.20 25);
42
-
43
- --sp-color-muted: oklch(96% 0.02 250);
44
- --sp-color-muted-fg: oklch(45% 0.05 250);
45
- --sp-color-muted-border: oklch(88% 0.02 250);
46
- --sp-color-muted-ring: oklch(82% 0.02 250);
47
-
48
- --sp-color-border: oklch(90% 0.02 250);
49
- --sp-color-border-strong: oklch(75% 0.03 250);
50
- --sp-color-bg: oklch(100% 0 0);
51
- --sp-color-bg-muted: oklch(96% 0.02 250);
52
- --sp-color-fg: oklch(10% 0.02 250);
11
+ --sp-color-primary: oklch(50% 0.11 140);
12
+ --sp-color-primary-fg: oklch(98% 0.01 140);
13
+ --sp-color-primary-border: oklch(60% 0.11 140);
14
+ --sp-color-primary-ring: oklch(58% 0.15 140);
15
+
16
+ --sp-color-secondary: oklch(50% 0.03 140);
17
+ --sp-color-secondary-fg: oklch(98% 0.01 140);
18
+ --sp-color-secondary-border: oklch(60% 0.03 140);
19
+ --sp-color-secondary-ring: oklch(70% 0.02 140);
20
+
21
+ --sp-color-destructive: oklch(55% 0.13 30);
22
+ --sp-color-destructive-fg: oklch(98% 0.01 30);
23
+ --sp-color-destructive-border: oklch(65% 0.13 30);
24
+ --sp-color-destructive-ring: oklch(76% 0.09 30);
25
+
26
+ --sp-color-success: oklch(46% 0.13 165);
27
+ --sp-color-success-fg: oklch(98% 0.01 165);
28
+ --sp-color-success-border: oklch(56% 0.13 165);
29
+ --sp-color-success-ring: oklch(66% 0.09 165);
30
+
31
+ --sp-color-warning: oklch(68% 0.11 85);
32
+ --sp-color-warning-fg: oklch(20% 0.02 85);
33
+ --sp-color-warning-border: oklch(78% 0.11 85);
34
+ --sp-color-warning-ring: oklch(88% 0.07 85);
35
+
36
+ --sp-color-info: oklch(55% 0.10 210);
37
+ --sp-color-info-fg: oklch(98% 0.01 210);
38
+ --sp-color-info-border: oklch(65% 0.10 210);
39
+ --sp-color-info-ring: oklch(75% 0.07 210);
40
+
41
+ --sp-color-error: oklch(38% 0.16 25);
42
+
43
+ --sp-color-muted: oklch(95% 0.018 68);
44
+ --sp-color-muted-fg: oklch(45% 0.05 68);
45
+ --sp-color-muted-border: oklch(88% 0.015 68);
46
+ --sp-color-muted-ring: oklch(82% 0.015 68);
47
+
48
+ --sp-color-border: oklch(88% 0.012 68);
49
+ --sp-color-border-strong: oklch(75% 0.02 68);
50
+ --sp-color-bg: oklch(99% 0.005 68);
51
+ --sp-color-bg-muted: oklch(95% 0.015 68);
52
+ --sp-color-fg: oklch(22% 0.02 68);
53
53
 
54
54
 
55
55
  /* ── Component tokens ───────────────────────────────────────────────── */
56
- --sp-color-indicator: oklch(75% 0.02 250); /* timeline dot fill */
56
+ --sp-color-indicator: oklch(78% 0.015 68); /* timeline dot fill */
57
57
 
58
58
 
59
59
  /* ── Focus ring ──────────────────────────────────────────────────────── */
@@ -63,16 +63,16 @@
63
63
 
64
64
 
65
65
  /* ── Avatar palette (10 slots, index chosen by name hash) ────────────── */
66
- --sp-avatar-color-1: oklch(55% 0.15 58);
67
- --sp-avatar-color-2: oklch(54% 0.16 129);
68
- --sp-avatar-color-3: oklch(51% 0.13 233);
69
- --sp-avatar-color-4: oklch(53% 0.10 209);
70
- --sp-avatar-color-5: oklch(53% 0.09 186);
71
- --sp-avatar-color-6: oklch(51% 0.13 161);
72
- --sp-avatar-color-7: oklch(46% 0.22 277);
73
- --sp-avatar-color-8: oklch(49% 0.23 316);
74
- --sp-avatar-color-9: oklch(44% 0.21 16);
75
- --sp-avatar-color-10: oklch(48% 0.22 348);
66
+ --sp-avatar-color-1: oklch(55% 0.13 40);
67
+ --sp-avatar-color-2: oklch(58% 0.12 85);
68
+ --sp-avatar-color-3: oklch(52% 0.10 125);
69
+ --sp-avatar-color-4: oklch(55% 0.08 150);
70
+ --sp-avatar-color-5: oklch(52% 0.09 195);
71
+ --sp-avatar-color-6: oklch(50% 0.08 230);
72
+ --sp-avatar-color-7: oklch(48% 0.09 320);
73
+ --sp-avatar-color-8: oklch(38% 0.15 8);
74
+ --sp-avatar-color-9: oklch(45% 0.09 55);
75
+ --sp-avatar-color-10: oklch(58% 0.10 10);
76
76
  --sp-avatar-fg: oklch(100% 0 0);
77
77
 
78
78
 
@@ -128,3 +128,8 @@
128
128
  /* ── Z-index ─────────────────────────────────────────────────────────── */
129
129
  --sp-z-popover: 50;
130
130
  }
131
+
132
+ /* Smooth width changes on the progress bar fill — renders with or without a theme. */
133
+ [data-progress-target="fill"] {
134
+ transition: width 0.3s ease;
135
+ }
data/docs/architecture.md CHANGED
@@ -9,8 +9,6 @@ Validation ranges for theme schema params live under `lib/stimulus_plumbers/them
9
9
  - If a component branches on the values internally (e.g. `when *FLOATING_TYPES`), the component owns the constant (`Form::Field::FLOATING_TYPES`). The schema references it directly — no alias.
10
10
  - If the component only passes the value through to the theme, the schema owns the range (e.g. `Schema::Button::Ranges::TYPE`).
11
11
 
12
- **Namespace rules:**
13
-
14
12
  **Namespaces:**
15
13
 
16
14
  - `Schema::Ranges` — cross-cutting constants (e.g. `BOOL`)
@@ -82,3 +82,4 @@ Rails helper for rendering a user avatar — image, initials, or fallback silhou
82
82
  - Always renders `role="img"` with `aria-label` set to `name` (when provided).
83
83
  - Image `alt` is set to `"[name]'s avatar"`; for anonymous avatars `alt=""` is used.
84
84
  - `onerror` on `<img>` clears the `src` to prevent broken-image icons.
85
+ - See [ARIA.md's Avatar / Card / Icon pattern](../../../ARIA.md) for the decorative-vs-meaningful image rule this follows.
@@ -107,7 +107,7 @@ Icons are rendered **inside** the `<button>`, before or after the label `<span>`
107
107
 
108
108
  ### Icon only
109
109
 
110
- When no text is provided, no `<span>` is rendered. The theme uses `:has(> span)` to detect this and applies `aspect-square` + `px-0`, making the button square (or a circle for `type: :fab` / `:fab_outline`):
110
+ When no text is provided, no `<span>` is rendered, and the button becomes square (or a circle for `type: :fab` / `:fab_outline`):
111
111
 
112
112
  ```html
113
113
  <button type="button" aria-label="Add" class="[theme classes]">
@@ -133,3 +133,4 @@ When no text is provided, no `<span>` is rendered. The theme uses `:has(> span)`
133
133
  - `<button>` always has `type="button"` to prevent accidental form submission.
134
134
  - Icon-only buttons must supply an accessible label via `aria: { label: "..." }` in `html_options`.
135
135
  - For navigation actions (links), use `sp_link` instead — it renders an `<a>` element.
136
+ - See [ARIA.md's Button pattern](../../../ARIA.md) for the `disabled` attribute requirement.
@@ -33,10 +33,10 @@ end
33
33
  </div>
34
34
  ```
35
35
 
36
- | Option | Description |
37
- | ---------------- | ------------------------------------------------------------------------------------ |
38
- | `date` | `Date` — sets `year-value`, `month-value` (0-indexed), `day-value` on the controller |
39
- | `**html_options` | Forwarded to the `calendar-month` controller root element |
36
+ | Option | Description |
37
+ | ---------------- | ----------------------------------------------------------------------- |
38
+ | `date` | `Date` — sets `year-value`, `month-value` (0-indexed) on the controller |
39
+ | `**html_options` | Forwarded to the `calendar-month` controller root element |
40
40
 
41
41
  For the JS controller API (targets, values, keyboard behaviour), see the [JS package docs](../../../stimulus-plumbers/docs/component/calendar.md).
42
42
 
@@ -50,7 +50,6 @@ For the JS controller API (targets, values, keyboard behaviour), see the [JS pac
50
50
  data-controller="calendar-month"
51
51
  data-calendar-month-year-value="2024"
52
52
  data-calendar-month-month-value="1"
53
- data-calendar-month-day-value="15"
54
53
  role="grid"
55
54
  >
56
55
  <!-- days-of-week header row (JS-populated) -->
@@ -94,10 +93,17 @@ Individual view helpers render a single view (used for Turbo Frame responses). T
94
93
  <%= sp_calendar_turbo_decade(date: @date, today: @today) %>
95
94
  ```
96
95
 
97
- | Helper | Extra option | Description |
98
- | ------------------------- | ---------------- | ------------------------------------------------------------------ |
99
- | `sp_calendar_turbo_month` | `weekday_format` | `:short` (default) \| `:long` \| `:narrow` — weekday header format |
100
- | `sp_calendar_turbo_year` | `month_format` | `:short` (default) \| `:long` \| `:narrow` — month button format |
96
+ **`sp_calendar_turbo_month`** extra option:
97
+
98
+ | Option | Description |
99
+ | ---------------- | ------------------------------------------------------------------ |
100
+ | `weekday_format` | `:short` (default) \| `:long` \| `:narrow` — weekday header format |
101
+
102
+ **`sp_calendar_turbo_year`** — extra option:
103
+
104
+ | Option | Description |
105
+ | -------------- | ---------------------------------------------------------------- |
106
+ | `month_format` | `:short` (default) \| `:long` \| `:narrow` — month button format |
101
107
 
102
108
  **Common options** (all helpers and `sp_calendar_turbo`):
103
109
 
@@ -199,25 +205,17 @@ Renders all three frames. Used inside a `combobox-date` controller for drill-dow
199
205
 
200
206
  ```html
201
207
  <!-- month view frame — visible by default -->
202
- <turbo-frame id="calendar-month-frame" data-combobox-date-target="monthView">
208
+ <turbo-frame id="calendar-month-frame">
203
209
  <!-- sp_calendar_turbo_month output -->
204
210
  </turbo-frame>
205
211
 
206
212
  <!-- year view frame — hidden until drill-up -->
207
- <turbo-frame
208
- id="calendar-year-frame"
209
- hidden
210
- data-combobox-date-target="yearView"
211
- >
213
+ <turbo-frame id="calendar-year-frame" hidden>
212
214
  <!-- sp_calendar_turbo_year output -->
213
215
  </turbo-frame>
214
216
 
215
217
  <!-- decade view frame — hidden until drill-up twice -->
216
- <turbo-frame
217
- id="calendar-decade-frame"
218
- hidden
219
- data-combobox-date-target="decadeView"
220
- >
218
+ <turbo-frame id="calendar-decade-frame" hidden>
221
219
  <!-- sp_calendar_turbo_decade output -->
222
220
  </turbo-frame>
223
221
  ```
@@ -63,10 +63,12 @@ Rails helper for rendering a themed card with optional icon, title, body, and ac
63
63
  <!-- body -->
64
64
  <div class="[card_body theme classes]">Your account is active.</div>
65
65
 
66
- <!-- action -->
67
- <div class="[card_action theme classes]">
68
- <a href="/settings">Manage</a>
69
- </div>
66
+ <!-- action: a ghost/tertiary sp_button, with card_action classes merged directly onto it (no wrapping div) -->
67
+ <a
68
+ href="/settings"
69
+ class="[button ghost/tertiary + card_action theme classes]"
70
+ ><span>Manage</span></a
71
+ >
70
72
  </div>
71
73
  ```
72
74
 
@@ -74,14 +76,14 @@ Rails helper for rendering a themed card with optional icon, title, body, and ac
74
76
 
75
77
  ## Theme keys
76
78
 
77
- | Key | Element | Variants |
78
- | ------------- | ----------------------------------- | --------------------------------------------- |
79
- | `card` | Outer `<div>` | `variant: :primary\|:secondary\|:tertiary\|…` |
80
- | `card_header` | Header wrapper `<div>` (icon+title) | — |
81
- | `card_icon` | Icon inside the header | — |
82
- | `card_title` | Title element (`h2` etc.) | — |
83
- | `card_body` | Body `<div>` | — |
84
- | `card_action` | Action `<div>` | — |
79
+ | Key | Element | Variants |
80
+ | ------------- | ------------------------------------------------------------- | --------------------------------------------- |
81
+ | `card` | Outer `<div>` | `variant: :primary\|:secondary\|:tertiary\|…` |
82
+ | `card_header` | Header wrapper `<div>` (icon+title) | — |
83
+ | `card_icon` | Icon inside the header | — |
84
+ | `card_title` | Title element (`h2` etc.) | — |
85
+ | `card_body` | Body `<div>` | — |
86
+ | `card_action` | Merged onto the action `<a>`/`<button>` (no wrapping element) | — |
85
87
 
86
88
  ---
87
89
 
@@ -0,0 +1,153 @@
1
+ # Checklist
2
+
3
+ Rails helper for rendering an accessible group of checkbox-style items. Items render as native `<label><input type="checkbox"></label>` pairs, no `<li>` wrapper — see [ARIA.md's Checklist pattern](../../../ARIA.md) for why.
4
+
5
+ ## Helper
6
+
7
+ ### `sp_checklist`
8
+
9
+ ```erb
10
+ <%= sp_checklist(label: "Groceries") do |checklist| %>
11
+ <%= checklist.item("Buy milk", checked: true) %>
12
+ <%= checklist.item("Walk the dog", checked: false) %>
13
+ <% end %>
14
+
15
+ <%# With a description %>
16
+ <%= sp_checklist(label: "Onboarding") do |checklist| %>
17
+ <%= checklist.item("Verify email", checked: true) do |item| %>
18
+ <% item.with_description("Sent to you at signup") %>
19
+ <% end %>
20
+ <% end %>
21
+
22
+ <%# Read-only summary (e.g. an activity feed) %>
23
+ <%= sp_checklist(label: "Completed steps") do |checklist| %>
24
+ <%= checklist.item("Account created", checked: true, readonly: true) %>
25
+ <% end %>
26
+ ```
27
+
28
+ | Option | Default | Description |
29
+ | ------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------- |
30
+ | `label:` | `nil` | Sets `aria-label` on the group wrapper |
31
+ | `labelledby:` | `nil` | Sets `aria-labelledby`; takes precedence over `label:` when both given |
32
+ | `select_all:` | `false` | `true` renders a master "select all" toggle wired to the group's items |
33
+ | `select_all_label:` | `"Select all"` | Label text for the master toggle (used as both `aria-label` and visible text); ignored unless `select_all: true` |
34
+ | `**html_options` | — | Forwarded to the `<div role="group">` |
35
+
36
+ ### `select_all:`
37
+
38
+ ```erb
39
+ <%= sp_checklist(label: "Groceries", select_all: true, select_all_label: "Select all items") do |checklist| %>
40
+ <%= checklist.item("Buy milk", checked: true) %>
41
+ <%= checklist.item("Walk the dog", checked: false) %>
42
+ <% end %>
43
+ ```
44
+
45
+ Renders a master `<label><input type="checkbox"></label>` before the items, wired to the `checklist` Stimulus controller. See [stimulus-plumbers's docs/component/checklist.md](../../../stimulus-plumbers/docs/component/checklist.md) for the controller's targets and actions.
46
+
47
+ Read-only items (`readonly: true`) are excluded from the master's aggregate state — see [ARIA.md's Checklist pattern](../../../ARIA.md) for why.
48
+
49
+ ### `checklist.item(content, checked:, readonly:, **html_options, &block)`
50
+
51
+ Renders a `<label>` wrapping a native `<input type="checkbox">`.
52
+
53
+ | Option | Default | Description |
54
+ | ---------------- | ------- | ------------------------------------------------------------------------------------ |
55
+ | `content` | `nil` | Item label — positional arg or via `item.with_title` |
56
+ | `checked:` | — | **Required.** Sets `checked` |
57
+ | `readonly:` | `false` | `true` renders **disabled** on the input — removed from the tab order, no controller |
58
+ | `**html_options` | — | Forwarded to the `<label>` |
59
+
60
+ ### Item slot methods (yielded as `item`)
61
+
62
+ | Slot method | Description |
63
+ | ----------------------------- | ------------------------------------------------------------- |
64
+ | `item.with_title(text)` | Title text (pre-populated when positional `content` is given) |
65
+ | `item.with_description(text)` | Secondary text below the title |
66
+
67
+ ---
68
+
69
+ ## Rendered HTML Structure
70
+
71
+ ```html
72
+ <div role="group" aria-label="Groceries" class="[checklist theme classes]">
73
+ <label class="[checklist_item theme classes]">
74
+ <input
75
+ type="checkbox"
76
+ checked
77
+ data-checklist-target="item"
78
+ class="[checklist_item_input theme classes]"
79
+ />
80
+ <span class="[checklist_item_content theme classes]">
81
+ <span class="[checklist_item_title theme classes]">Buy milk</span>
82
+ </span>
83
+ </label>
84
+ </div>
85
+ ```
86
+
87
+ ### `select_all: true`
88
+
89
+ ```html
90
+ <div
91
+ role="group"
92
+ aria-label="Groceries"
93
+ data-controller="checklist"
94
+ data-action="change->checklist#onChange"
95
+ class="[checklist theme classes]"
96
+ >
97
+ <label class="[checklist_item theme classes]">
98
+ <input
99
+ type="checkbox"
100
+ data-checklist-target="master"
101
+ class="[checklist_item_input theme classes]"
102
+ />
103
+ Select all
104
+ </label>
105
+
106
+ <label class="[checklist_item theme classes]">
107
+ <input
108
+ type="checkbox"
109
+ checked
110
+ data-checklist-target="item"
111
+ class="[checklist_item_input theme classes]"
112
+ />
113
+ ...
114
+ </label>
115
+ </div>
116
+ ```
117
+
118
+ The master's `checked` state is computed from the items' `checked:` values at render time (all-true renders `checked`; every other case, including mixed, renders unchecked) — the `checklist` controller corrects it to `indeterminate` client-side once connected. See [ARIA.md's Checklist pattern](../../../ARIA.md) for the accepted flash this tradeoff causes in the mixed case.
119
+
120
+ ### Read-only item (`readonly: true`)
121
+
122
+ ```html
123
+ <label class="[checklist_item theme classes]">
124
+ <input
125
+ type="checkbox"
126
+ checked
127
+ disabled
128
+ class="[checklist_item_input theme classes]"
129
+ />
130
+ ...
131
+ </label>
132
+ ```
133
+
134
+ ---
135
+
136
+ ## Theme keys
137
+
138
+ | Key | Element | Variants |
139
+ | ---------------------------- | ---------------------------------------------- | -------- |
140
+ | `checklist` | Outer `<div role="group">` | — |
141
+ | `checklist_item` | Item `<label>` | — |
142
+ | `checklist_item_input` | Item/master checkbox `<input>` | — |
143
+ | `checklist_item_content` | Content wrapper `<span>` (title + description) | — |
144
+ | `checklist_item_title` | Title `<span>` | — |
145
+ | `checklist_item_description` | Description `<span>` | — |
146
+
147
+ Checked-state styling (strikethrough title) reads the sibling `<input>`'s `:checked` state via Tailwind's `:has()`-based `group-has-checked/checklist-item:` variant — there is no `aria-checked` or `checked:` theme-resolver kwarg to pass.
148
+
149
+ ---
150
+
151
+ ## ARIA
152
+
153
+ See [ARIA.md's Checklist pattern](../../../ARIA.md) for why items use native `<input type="checkbox">` with no ARIA attributes, the `disabled` read-only behavior, and the `indeterminate` tradeoff for the master toggle.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Rails helpers that render fully-wired combobox components. Each variant shares the same wrapper structure (`input-combobox` + `input-formatter`) and differs only in its popover body.
4
4
 
5
- See [docs/component/combobox.md](../../stimulus-plumbers/docs/component/combobox.md) in the JS package for the underlying controller API.
5
+ See [docs/component/combobox.md](../../../stimulus-plumbers/docs/component/combobox.md) in the JS package for the underlying controller API.
6
6
 
7
7
  ## Helpers
8
8
 
@@ -127,7 +127,7 @@ iOS-style drum/scroll-wheel time picker.
127
127
 
128
128
  ## Form Builder
129
129
 
130
- `StimulusPlumbers::Form::Builder` exposes combobox-backed fields through purpose-specific methods rather than a single generic helper. See [form_builder.md](form_builder.md) for the full API.
130
+ `StimulusPlumbers::Form::Builder` exposes combobox-backed fields through purpose-specific methods rather than a single generic helper. See [form.md](form.md) for the full API.
131
131
 
132
132
  ```erb
133
133
  <%= form_with model: @user, builder: StimulusPlumbers::Form::Builder do |f| %>
@@ -118,9 +118,34 @@ Three methods render a complete, accessible field:
118
118
 
119
119
  Use `f.time_field` for a plain `<input type="time">`.
120
120
 
121
- **Select** (`as: :select`) — read-only listbox backed by `combobox-dropdown`. Accepts `choices:`, `include_blank:`, `prompt:`, `selected:`. Use `f.select` for a native `<select>`.
121
+ **Select** (`as: :select`) — read-only listbox backed by `combobox-dropdown`.
122
122
 
123
- **Search** (`as: :search`) editable typeahead backed by `combobox-dropdown`. Accepts `choices:`, `url:`, `clearable:`. Use `f.search_field` for a native `<input type="search">`.
123
+ | Option | Values | Default | Description |
124
+ | --------------- | -------------- | ------- | ------------------------------------------------------------- |
125
+ | `choices` | Array | `[]` | `[label, value]` pairs, or a flat array of strings |
126
+ | `include_blank` | Boolean/String | `nil` | Prepends a blank option; String overrides its label |
127
+ | `prompt` | Boolean/String | `nil` | Prepends a disabled placeholder option |
128
+ | `selected` | Value | `nil` | Pre-selected value; defaults to the attribute's current value |
129
+
130
+ Use `f.select` for a native `<select>`.
131
+
132
+ **Search** (`as: :search`) — editable typeahead backed by `combobox-dropdown`.
133
+
134
+ | Option | Values | Default | Description |
135
+ | ----------- | ------- | ------- | ----------------------------------------------------------------------------------- |
136
+ | `choices` | Array | `[]` | Client-side options to filter; omit when using `url:` |
137
+ | `url` | String | `nil` | Server-side endpoint, receives `?q=<query>`, returns `<li role="option">` fragments |
138
+ | `clearable` | Boolean | `false` | Adds a clear button that resets the input |
139
+
140
+ Use `f.search_field` for a native `<input type="search">`.
141
+
142
+ **Password** (`as: :password`) — reveal-toggle wrapper backed by `input-formatter`.
143
+
144
+ | Option | Values | Default | Description |
145
+ | ------------ | ------- | ------- | ------------------------------------------- |
146
+ | `revealable` | Boolean | `false` | Adds a show/hide toggle button on the input |
147
+
148
+ Use `f.password_field` for a plain `<input type="password">` (also accepts `revealable:`).
124
149
 
125
150
  ---
126
151
 
@@ -169,9 +194,9 @@ Renders a `<fieldset>` / `<legend>` for accessible grouping, or a single checkbo
169
194
 
170
195
  **Card / button behaviour:**
171
196
 
172
- - **Checkbox card** — input visible on right; card border changes on check via `has-[:checked]:`
173
- - **Radio card** — input hidden (`hidden peer`); entire card is the clickable area via `peer-checked:`
174
- - **Radio button** — input hidden (`hidden peer`); inline pill style via `peer-checked:`
197
+ - **Checkbox card** — input visible on the right; the card border highlights when checked
198
+ - **Radio card** — input visually hidden; the whole card is the clickable target and highlights when selected
199
+ - **Radio button** — input visually hidden; renders as an inline pill that highlights when selected
175
200
 
176
201
  ---
177
202
 
@@ -185,7 +210,8 @@ Renders a `<fieldset>` / `<legend>` for accessible grouping, or a single checkbo
185
210
  <input
186
211
  id="user_email"
187
212
  type="email"
188
- aria-describedby="user_email_hint"
213
+ aria-describedby="user_email_hint user_email_error"
214
+ aria-invalid="true"
189
215
  required
190
216
  aria-required="true"
191
217
  />
@@ -10,12 +10,14 @@ Rails helper for rendering a named icon from the active theme's icon registry.
10
10
  <%= sp_icon name: "check" %>
11
11
  <%= sp_icon name: "spinner", aria: { label: "Loading" }, role: "img" %>
12
12
  <%= sp_icon name: "unknown-icon" %>
13
+ <%= sp_icon name: "check", size: :sm %>
13
14
  ```
14
15
 
15
- | Option | Default | Description |
16
- | ---------------- | ---------- | ------------------------------------------- |
17
- | `name` | (required) | Icon name — looked up in the theme registry |
18
- | `**html_options` | — | Forwarded to the root element |
16
+ | Option | Default | Description |
17
+ | ---------------- | ---------- | --------------------------------------------------- |
18
+ | `name` | (required) | Icon name — looked up in the theme registry |
19
+ | `size` | `:lg` | `:sm` \| `:md` \| `:lg` theme-resolved size class |
20
+ | `**html_options` | — | Forwarded to the root element |
19
21
 
20
22
  ---
21
23
 
@@ -39,6 +41,6 @@ Rails helper for rendering a named icon from the active theme's icon registry.
39
41
 
40
42
  ## ARIA
41
43
 
42
- - Icons are **decorative by default** — the theme applies `aria-hidden="true"` so they are invisible to screen readers.
44
+ - The theme applies `aria-hidden="true"` by default (see [ARIA.md's Avatar / Card / Icon pattern](../../../ARIA.md)).
43
45
  - For meaningful icons (standalone, no adjacent label), pass `aria: { label: "..." }` and `role: "img"` to override.
44
46
  - Button and link icons are rendered internally with `aria-hidden="true"`; callers do not need to set this manually.
@@ -0,0 +1,55 @@
1
+ # Indicator
2
+
3
+ Presentational status marker — a colored dot, an animated "pulse" ring, or a numeric badge. No Stimulus controller.
4
+
5
+ ## Helper
6
+
7
+ ### `sp_indicator`
8
+
9
+ ```erb
10
+ <%# Dot — must be paired with an accessible name %>
11
+ <span>
12
+ <%= sp_indicator(variant: :success) %>
13
+ <span class="sr-only">Online</span>
14
+ </span>
15
+
16
+ <%# Pulse %>
17
+ <span>
18
+ <%= sp_indicator(type: :pulse, variant: :destructive, pulse: true) %>
19
+ <span class="sr-only">Live</span>
20
+ </span>
21
+
22
+ <%# Badge %>
23
+ <span>
24
+ <%= sp_indicator(type: :badge, variant: :primary) { "5" } %>
25
+ <span class="sr-only">5 unread notifications</span>
26
+ </span>
27
+ ```
28
+
29
+ | Option | Default | Description |
30
+ | ---------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------- |
31
+ | `type:` | `:dot` | `:dot` \| `:pulse` \| `:badge` — `:badge` renders the given block as content |
32
+ | `variant:` | `:primary` | Semantic color token — `:primary` \| `:secondary` \| `:tertiary` \| `:success` \| `:destructive` \| `:warning` \| `:info` |
33
+ | `pulse:` | `false` | Adds an animated ring element (`prefers-reduced-motion` suppresses the animation) |
34
+ | `**html_options` | — | Forwarded to the dot `<span>` |
35
+
36
+ When `pulse: true`, the dot is wrapped in an extra `<span>` (`indicator_wrapper` theme key) that positions the ring behind the dot — no margin offsets involved.
37
+
38
+ **Every indicator must be paired with an accessible name** — a visible label or `aria-label`/adjacent `sr-only` text. The component itself renders no text and cannot know the right label; this is enforced by an accessibility test (see `test/accessibility/components/indicator_accessibility_test.rb`), not by the component.
39
+
40
+ ## Legend pattern
41
+
42
+ To explain what each indicator color means (e.g. a status legend), pair indicators with visible text inside a list — do not rely on color alone:
43
+
44
+ ```erb
45
+ <%= sp_list do |list| %>
46
+ <%= list.item do |item| %>
47
+ <% item.with_icon_leading { sp_indicator(variant: :success) } %>
48
+ <% item.with_title("Online") %>
49
+ <% end %>
50
+ <%= list.item do |item| %>
51
+ <% item.with_icon_leading { sp_indicator(variant: :warning) } %>
52
+ <% item.with_title("Away") %>
53
+ <% end %>
54
+ <% end %>
55
+ ```
@@ -1,6 +1,6 @@
1
1
  # List
2
2
 
3
- Rails helper for rendering an accessible list of links, buttons, or grouped sections.
3
+ Rails helper for rendering an accessible list of links, buttons, or grouped sections. For a flat, drag/keyboard-reorderable list, see [`OrderedList`](ordered_list.md) instead.
4
4
 
5
5
  ## Helper
6
6
 
@@ -39,7 +39,7 @@ Rails helper for rendering an accessible list of links, buttons, or grouped sect
39
39
  | `role:` | `"list"` | ARIA role on the `<ul>` — use `"menu"` for interactive menus |
40
40
  | `**html_options` | — | Forwarded to the `<ul>` |
41
41
 
42
- ### `list.section(title:, description:, **html_options)`
42
+ ### `list.section(title:, description:, **html_options, &block)`
43
43
 
44
44
  Renders a `<li>` with an optional heading/description and a nested `<ul>`.
45
45
 
@@ -60,7 +60,7 @@ Renders a `<li>` containing an `<a>` (when `url:` present) or `<button>`.
60
60
  | `target:` | `nil` | Forwarded to the `<a>` (e.g. `"_blank"`) |
61
61
  | `**html_options` | — | Forwarded to the inner `<a>` or `<button>` |
62
62
 
63
- When `target: "_blank"` is set, `icon_trailing: "external-link"` is added automatically.
63
+ When `url:` is present and `target: "_blank"` is set, `icon_trailing: "external-link"` is added automatically.
64
64
 
65
65
  ### Item slot methods (yielded as `item`)
66
66
 
@@ -154,8 +154,7 @@ When `target: "_blank"` is set, `icon_trailing: "external-link"` is added automa
154
154
 
155
155
  ## ARIA
156
156
 
157
- - Default `role="list"` is appropriate for navigation or command lists.
158
157
  - Use `role="menu"` + `role="menuitem"` (via `html_options`) only when items form a widget menu — arrow key navigation must then be wired via a Stimulus controller.
159
- - `active: true` adds `aria-current="page"` on links and `aria-current="true"` on buttons; pair this with a visual style change so the state is not communicated by color alone.
160
158
  - Section titles without `heading_level` use `aria-hidden="true"` on the `<span>` and set `aria-label` on the nested `<ul>` to avoid double-announcing.
161
159
  - Nested sections increment the heading level automatically (clamped at `h6`).
160
+ - See [ARIA.md's List pattern](../../../ARIA.md) for the default `role="list"`/`role="listitem"` and `active:` → `aria-current` mapping (pair it with a visual style change so the state isn't communicated by color alone).