stimulus_plumbers 0.4.8 → 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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/README.md +7 -9
  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 +1 -0
  13. data/docs/component/calendar.md +7 -16
  14. data/docs/component/checklist.md +153 -0
  15. data/docs/component/icon.md +7 -5
  16. data/docs/component/indicator.md +55 -0
  17. data/docs/component/list.md +2 -3
  18. data/docs/component/ordered_list.md +118 -0
  19. data/docs/component/plumber.md +21 -0
  20. data/docs/component/popover.md +1 -2
  21. data/docs/component/progress.md +65 -0
  22. data/docs/component/timeline.md +47 -12
  23. data/docs/guide.md +12 -0
  24. data/lib/generators/stimulus_plumbers/install/install_generator.rb +16 -12
  25. data/lib/stimulus_plumbers/components/button.rb +1 -0
  26. data/lib/stimulus_plumbers/components/checklist/item/slots.rb +13 -0
  27. data/lib/stimulus_plumbers/components/checklist/item.rb +64 -0
  28. data/lib/stimulus_plumbers/components/checklist.rb +50 -0
  29. data/lib/stimulus_plumbers/components/combobox/typeahead.rb +1 -0
  30. data/lib/stimulus_plumbers/components/icon.rb +2 -2
  31. data/lib/stimulus_plumbers/components/indicator.rb +30 -0
  32. data/lib/stimulus_plumbers/components/link.rb +1 -0
  33. data/lib/stimulus_plumbers/components/ordered_list/item/slots.rb +13 -0
  34. data/lib/stimulus_plumbers/components/ordered_list/item.rb +129 -0
  35. data/lib/stimulus_plumbers/components/ordered_list.rb +24 -0
  36. data/lib/stimulus_plumbers/components/progress_bar.rb +48 -0
  37. data/lib/stimulus_plumbers/components/progress_meter.rb +37 -0
  38. data/lib/stimulus_plumbers/components/progress_ring.rb +40 -0
  39. data/lib/stimulus_plumbers/components/timeline/event.rb +2 -1
  40. data/lib/stimulus_plumbers/components/timeline/group.rb +5 -3
  41. data/lib/stimulus_plumbers/form/fields/inputs/password.rb +1 -0
  42. data/lib/stimulus_plumbers/form/fields/inputs/search.rb +1 -0
  43. data/lib/stimulus_plumbers/generators/css_entrypoint.rb +15 -9
  44. data/lib/stimulus_plumbers/generators/tokens_directive.rb +6 -2
  45. data/lib/stimulus_plumbers/helpers/avatar_helper.rb +1 -7
  46. data/lib/stimulus_plumbers/helpers/button_helper.rb +1 -7
  47. data/lib/stimulus_plumbers/helpers/calendar_helper.rb +2 -9
  48. data/lib/stimulus_plumbers/helpers/calendar_turbo_helper.rb +3 -6
  49. data/lib/stimulus_plumbers/helpers/checklist_helper.rb +11 -0
  50. data/lib/stimulus_plumbers/helpers/indicator_helper.rb +11 -0
  51. data/lib/stimulus_plumbers/helpers/ordered_list_helper.rb +11 -0
  52. data/lib/stimulus_plumbers/helpers/popover_helper.rb +1 -7
  53. data/lib/stimulus_plumbers/helpers/progress_helper.rb +21 -0
  54. data/lib/stimulus_plumbers/helpers.rb +8 -0
  55. data/lib/stimulus_plumbers/themes/base.rb +4 -0
  56. data/lib/stimulus_plumbers/themes/schema/icon.rb +21 -4
  57. data/lib/stimulus_plumbers/themes/schema/indicator/ranges.rb +14 -0
  58. data/lib/stimulus_plumbers/themes/schema.rb +39 -3
  59. data/lib/stimulus_plumbers/version.rb +1 -1
  60. data/lib/stimulus_plumbers.rb +10 -0
  61. data/vendor/ARIA.md +22 -2
  62. data/vendor/component/manifest.json +212 -0
  63. data/vendor/controller/docs/calendar.md +4 -7
  64. data/vendor/controller/docs/checklist.md +45 -0
  65. data/vendor/controller/docs/combobox.md +1 -31
  66. data/vendor/controller/docs/input-clearable.md +1 -4
  67. data/vendor/controller/docs/input-formatter.md +1 -2
  68. data/vendor/controller/docs/modal.md +1 -4
  69. data/vendor/controller/docs/popover.md +1 -3
  70. data/vendor/controller/docs/progress.md +82 -0
  71. data/vendor/controller/docs/reorderable.md +82 -0
  72. data/vendor/controller/docs/visibility.md +31 -0
  73. data/vendor/controller/manifest.json +331 -20
  74. metadata +25 -1
@@ -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.
@@ -133,3 +133,4 @@ When no text is provided, no `<span>` is rendered, and the button becomes square
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) -->
@@ -206,25 +205,17 @@ Renders all three frames. Used inside a `combobox-date` controller for drill-dow
206
205
 
207
206
  ```html
208
207
  <!-- month view frame — visible by default -->
209
- <turbo-frame id="calendar-month-frame" data-combobox-date-target="monthView">
208
+ <turbo-frame id="calendar-month-frame">
210
209
  <!-- sp_calendar_turbo_month output -->
211
210
  </turbo-frame>
212
211
 
213
212
  <!-- year view frame — hidden until drill-up -->
214
- <turbo-frame
215
- id="calendar-year-frame"
216
- hidden
217
- data-combobox-date-target="yearView"
218
- >
213
+ <turbo-frame id="calendar-year-frame" hidden>
219
214
  <!-- sp_calendar_turbo_year output -->
220
215
  </turbo-frame>
221
216
 
222
217
  <!-- decade view frame — hidden until drill-up twice -->
223
- <turbo-frame
224
- id="calendar-decade-frame"
225
- hidden
226
- data-combobox-date-target="decadeView"
227
- >
218
+ <turbo-frame id="calendar-decade-frame" hidden>
228
219
  <!-- sp_calendar_turbo_decade output -->
229
220
  </turbo-frame>
230
221
  ```
@@ -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.
@@ -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
 
@@ -154,8 +154,7 @@ When `url:` is present and `target: "_blank"` is set, `icon_trailing: "external-
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).
@@ -0,0 +1,118 @@
1
+ # OrderedList
2
+
3
+ Rails helper for a flat, reorderable list — pointer-drag and keyboard reorder via the [`reorderable`](../../../stimulus-plumbers/docs/component/reorderable.md) JS controller. Unlike [`List`](list.md), item order is semantic content (`<ol>`, not `<ul>`), there are no sections, and every item has a pointer drag surface.
4
+
5
+ ## Helper
6
+
7
+ ### `sp_ordered_list`
8
+
9
+ ```erb
10
+ <%# Whole-item handle (default), content-only rows %>
11
+ <%= sp_ordered_list do |list| %>
12
+ <%= list.item("First", id: "item-1") %>
13
+ <%= list.item("Second", id: "item-2") %>
14
+ <% end %>
15
+
16
+ <%# Links + a dedicated leading-icon handle, editing enabled %>
17
+ <%= sp_ordered_list(editing: true) do |list| %>
18
+ <%= list.item(id: "link-1", url: "/", handle: :leading) do |item| %>
19
+ <% item.with_title("Dashboard") %>
20
+ <% end %>
21
+ <% end %>
22
+ ```
23
+
24
+ | Option | Default | Description |
25
+ | ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
26
+ | `move_key:` | `"Alt"` | Maps to `data-reorderable-move-key-value`. One of `Alt`, `Control`, `Shift`, `Meta`. |
27
+ | `editing:` | `false` | Initial render-time state, maps to `data-reorderable-editing-value`. |
28
+ | `orientation:` | `nil` | Maps to `data-reorderable-orientation-value` when set; omitted entirely otherwise (JS controller defaults to `"vertical"`). See the [reorderable JS controller doc](../../../stimulus-plumbers/docs/component/reorderable.md) for `"horizontal"` semantics. |
29
+ | `role:` | `"list"` | ARIA role on the `<ol>` — preserves list semantics when consumer CSS resets `list-style`. |
30
+ | `**html_options` | — | Forwarded to the outer `<ol>`. |
31
+
32
+ ### `list.item(content, id:, handle:, url:, target:, active:, **html_options, &block)`
33
+
34
+ | Option | Default | Description |
35
+ | ---------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
36
+ | `content` | `nil` | Item label — positional arg or via `item.with_title`. |
37
+ | `id:` | — | **Required.** Raises `ArgumentError` if missing. |
38
+ | `handle:` | `:item` | `:item` (whole `<li>` is the drag surface) \| `:leading` \| `:trailing` (that icon position is). |
39
+ | `url:` | `nil` | Renders `<a href>` around the title/description. Without it, content renders with no wrapper (no click target at all — no `<button>` fallback, unlike `List::Item`). |
40
+ | `target:` | `nil` | Forwarded to the `<a>` (e.g. `"_blank"`), only used when `url:` is set. |
41
+ | `active:` | `false` | Adds `aria-current="page"`, only used when `url:` is set. |
42
+ | `**html_options` | — | Forwarded to the `<a>` when `url:` is set. Raises `ArgumentError` if given without `url:` — there is no element for them to land on. |
43
+
44
+ ### Item slot methods (yielded as `item`)
45
+
46
+ | Slot method | Description |
47
+ | ------------------------------- | ----------------------------------------------------------------------------------------------------------- |
48
+ | `item.with_icon_leading(name)` | Icon at the leading position. Becomes the drag handle if `handle: :leading`; otherwise purely decorative. |
49
+ | `item.with_title(text)` | Title text (pre-populated when positional `content` is given). |
50
+ | `item.with_description(text)` | Secondary text below the title. |
51
+ | `item.with_icon_trailing(name)` | Icon at the trailing position. Becomes the drag handle if `handle: :trailing`; otherwise purely decorative. |
52
+
53
+ If `handle: :leading`/`:trailing` is set and the corresponding icon slot isn't, a default `grip-vertical` glyph renders there instead.
54
+
55
+ ---
56
+
57
+ ## Rendered HTML Structure
58
+
59
+ ### `handle: :item` (default), no link
60
+
61
+ ```html
62
+ <ol
63
+ role="list"
64
+ data-controller="reorderable"
65
+ data-reorderable-move-key-value="Alt"
66
+ data-reorderable-editing-value="false"
67
+ >
68
+ <li
69
+ id="item-1"
70
+ data-reorderable-target="item handle"
71
+ data-action="pointerdown->reorderable#onPointerDown pointermove->reorderable#onPointerMove pointerup->reorderable#onPointerUp"
72
+ >
73
+ First
74
+ </li>
75
+ </ol>
76
+ ```
77
+
78
+ ### `handle: :leading`, with a link
79
+
80
+ ```html
81
+ <li id="link-1" data-reorderable-target="item">
82
+ <span
83
+ data-reorderable-target="handle"
84
+ data-action="pointerdown->reorderable#onPointerDown ..."
85
+ >
86
+ <svg aria-hidden="true">...</svg>
87
+ <!-- grip-vertical, or a custom icon if item.with_icon_leading was set -->
88
+ </span>
89
+ <a href="/" data-reorderable-target="trigger">
90
+ <span>
91
+ <span>Dashboard</span>
92
+ <span>Overview</span>
93
+ </span>
94
+ </a>
95
+ </li>
96
+ ```
97
+
98
+ Icon positions (leading/trailing) are always siblings of the `<a>`, never nested inside it — this is what lets a real link coexist with drag reordering without one accidentally triggering the other. See [reorderable's editing mode](../../../stimulus-plumbers/docs/component/reorderable.md) for how the link itself is neutralized while editing.
99
+
100
+ ---
101
+
102
+ ## Theme keys
103
+
104
+ | Key | Element | Variants |
105
+ | ------------------------------- | ------------------------------------------------ | -------- |
106
+ | `ordered_list` | Outer `<ol>` | — |
107
+ | `ordered_list_item` | `<li>` | — |
108
+ | `ordered_list_item_handle` | Leading/trailing `<span>` (decorative-or-handle) | — |
109
+ | `ordered_list_item_content` | Content wrapper `<span>` inside `<a>` (or bare) | — |
110
+ | `ordered_list_item_title` | Title `<span>` | — |
111
+ | `ordered_list_item_description` | Description `<span>` | — |
112
+
113
+ ---
114
+
115
+ ## ARIA
116
+
117
+ - See [ARIA.md](../../../ARIA.md) for WCAG 2.1 AA criteria.
118
+ - For the `reorderable` JS controller's targets, values, and actions (including `editingValue`/`trigger`/`toggleEditing`), see the [stimulus-plumbers JS controller doc](../../../stimulus-plumbers/docs/component/reorderable.md).
@@ -203,3 +203,24 @@ def merge_html_options(*hashes)
203
203
  end
204
204
  end
205
205
  ```
206
+
207
+ ---
208
+
209
+ ## PlumberHelper
210
+
211
+ ### `sp_dom_id(record = nil, prefix: nil, suffix: nil)`
212
+
213
+ Generates an `"sp_"`-prefixed DOM id for wiring `aria-controls`/`id` pairs between a trigger and its target. Not a component renderer — an internal id-generation utility used by `Combobox`, `Popover`, and `Timeline::Event` when the caller doesn't supply an explicit `id:`.
214
+
215
+ | Param | Description |
216
+ | --------- | ---------------------------------------------------------------------------------------------------------------------- |
217
+ | `record` | ActiveRecord-like object — delegates to Rails' `dom_id(record, prefix)`. Omit for a random id (`SecureRandom.hex(8)`). |
218
+ | `prefix:` | Forwarded to `dom_id` when `record` is given. |
219
+ | `suffix:` | Appended after an underscore, regardless of whether `record` is given. |
220
+
221
+ ```ruby
222
+ sp_dom_id # => "sp_3f9a1b2c4d5e6f70"
223
+ sp_dom_id(suffix: "panel") # => "sp_3f9a1b2c4d5e6f70_panel"
224
+ sp_dom_id(post) # => "sp_post_1"
225
+ sp_dom_id(post, prefix: :edit, suffix: :form) # => "sp_edit_post_1_form"
226
+ ```
@@ -130,9 +130,8 @@ end
130
130
 
131
131
  ## ARIA
132
132
 
133
- - Default trigger button has `aria-haspopup="dialog"`, `aria-expanded="false"`, and `aria-controls` pointing to the panel.
134
- - `aria-expanded` is toggled to `"true"` / `"false"` by the Stimulus controller.
135
133
  - Pass `role: "dialog"` or `role: "tooltip"` on `p.panel` based on whether the content is interactive.
136
134
  - `aria: { label: }` or `aria: { labelledby: }` on `p.panel` provides the accessible name.
135
+ - See [ARIA.md's Popover pattern](../../../ARIA.md) for the trigger's `aria-haspopup`/`aria-expanded`/`aria-controls` wiring.
137
136
 
138
137
  For the JS controller API (targets, values, keyboard behaviour), see the [JS package docs](../../../stimulus-plumbers/docs/component/popover.md).