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
@@ -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).
@@ -103,6 +103,28 @@ end
103
103
 
104
104
  ---
105
105
 
106
+ ## Plumber::Slots
107
+
108
+ Base class for a component's slot DSL (e.g. `renderer.with_title(value)`, `renderer.with_content { ... }`). Used by `Card`, `Button`, `Link`, `List::Item`, `Timeline::Event`, and `Combobox::Builder`.
109
+
110
+ ```ruby
111
+ class MySlots < StimulusPlumbers::Plumber::Slots
112
+ slot :icon, :title # flat slots: with_icon(value) / with_title(value)
113
+ slot :header, by: HeaderSlots # nested slots: with_header { |h| h.with_x(...) }
114
+ end
115
+ ```
116
+
117
+ Always construct with the view `template` — required for a slot's multi-line block form (`with_content do ... end`) to render correctly:
118
+
119
+ ```ruby
120
+ slots = MySlots.new(template)
121
+ yield slots if block_given?
122
+ ```
123
+
124
+ Slots needing custom validation (e.g. a required keyword) define `with_*` manually instead of using the `slot` DSL — see `Card::Slots#with_action`.
125
+
126
+ ---
127
+
106
128
  ## Plumber::Options::Html
107
129
 
108
130
  Mixin for safely deep-merging HTML attribute hashes. Handles three concerns that a plain `Hash#merge` gets wrong:
@@ -181,3 +203,24 @@ def merge_html_options(*hashes)
181
203
  end
182
204
  end
183
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).
@@ -0,0 +1,65 @@
1
+ # Progress
2
+
3
+ Rails helpers for rendering the `progress` Stimulus controller's three variants. See [stimulus-plumbers's docs/component/progress.md](../../../stimulus-plumbers/docs/component/progress.md) for the controller's Values/Targets/Methods/Dispatches.
4
+
5
+ ## Helpers
6
+
7
+ ### `sp_progress_bar`
8
+
9
+ ```erb
10
+ <%= sp_progress_bar(value: 65, max: 100, aria: { label: "Upload progress" }) %>
11
+ ```
12
+
13
+ | Option | Default | Description |
14
+ | ---------------- | ------- | -------------------------------------------------------- |
15
+ | `value:` | — | Required. Current value |
16
+ | `min:` | `0` | Range minimum |
17
+ | `max:` | `100` | Range maximum |
18
+ | `indeterminate:` | `false` | Omits `aria-valuenow`; adds the indeterminate hook class |
19
+ | `**html_options` | — | Forwarded to the outer `<div role="progressbar">` |
20
+
21
+ When `indeterminate:` is true, the JS controller sets a fixed 25% fill
22
+ width directly — this is the same with or without a theme. A theme may
23
+ layer motion on top (Tailwind slides it); without one, the bar renders
24
+ as a static partial fill.
25
+
26
+ ### `sp_progress_ring`
27
+
28
+ ```erb
29
+ <%= sp_progress_ring(value: 60, max: 100, aria: { label: "Storage used" }) %>
30
+ ```
31
+
32
+ | Option | Default | Description |
33
+ | ---------------- | ------- | ---------------------------------------------------------- |
34
+ | `value:` | — | Required. Current value |
35
+ | `min:` | `0` | Range minimum |
36
+ | `max:` | `100` | Range maximum |
37
+ | `indeterminate:` | `false` | Omits `aria-valuenow`; adds the indeterminate hook class |
38
+ | `**html_options` | — | Forwarded to the rendered icon (role, aria, classes, etc.) |
39
+
40
+ Renders via the theme's icon registry (icon name `"progress-ring"`), the same mechanism as `sp_icon`. Track/fill color and ring size are fixed by the icon's own SVG — resize with a `classes:`/`class:` override (e.g. `classes: "size-16"`) rather than a radius option. **Themes must register a `"progress-ring"` icon to render the ring's visual structure** — `stimulus-plumbers-tailwind` ships one; a theme without it (including the unstyled `Themes::Base`) falls back to an empty `<span role="progressbar">` with no visible ring, same as `sp_icon` for an unknown icon name.
41
+
42
+ When `indeterminate:` is true (and a theme provides the ring icon), the JS
43
+ controller sets the fill circle's `stroke-dasharray` to a fixed 25% arc
44
+ directly, same as the bar. A theme may spin it (Tailwind's `animate-spin`);
45
+ without one, the arc renders static.
46
+
47
+ ### `sp_progress_meter`
48
+
49
+ ```erb
50
+ <%= sp_progress_meter(value: 6, min: 0, max: 10, low: 3, high: 8, optimum: 9, aria: { label: "Disk usage" }) %>
51
+ ```
52
+
53
+ | Option | Default | Description |
54
+ | ---------------- | ------- | ------------------------------------------------- |
55
+ | `value:` | — | Required. Current value |
56
+ | `min:` | `0` | Range minimum |
57
+ | `max:` | `100` | Range maximum |
58
+ | `low:` | `nil` | Native `<meter low>` — omitted when not given |
59
+ | `high:` | `nil` | Native `<meter high>` — omitted when not given |
60
+ | `optimum:` | `nil` | Native `<meter optimum>` — omitted when not given |
61
+ | `**html_options` | — | Forwarded to the `<meter>` |
62
+
63
+ ## Known limitation
64
+
65
+ `<meter>` styling relies on `::-webkit-meter-*`/`::-moz-meter-*` pseudo-elements — cross-browser visual consistency is limited; this is a native-element tradeoff, not a bug.
@@ -4,14 +4,27 @@ Stimulus Plumbers uses a theme system to apply presentational CSS classes to for
4
4
 
5
5
  ## Configuration
6
6
 
7
+ Adding the `stimulus_plumbers_tailwind` gem registers and activates the Tailwind theme automatically — no config needed. To switch to a custom theme instead:
8
+
7
9
  ```ruby
8
10
  # config/initializers/stimulus_plumbers.rb
9
11
  StimulusPlumbers.configure do |config|
10
- config.theme.use(:tailwind)
11
- # config.theme.use(MyCustomTheme.new)
12
+ config.theme.use(MyCustomTheme.new)
12
13
  end
13
14
  ```
14
15
 
16
+ ## Design tokens
17
+
18
+ - Component classes reference CSS custom properties (e.g. `--sp-color-primary`) defined in `stimulus_plumbers/tokens.css`
19
+ - Run `bin/rails generate stimulus_plumbers:install` once to import it — see the [README](../../README.md#installation)
20
+ - Override tokens with your own `:root` block after the generated import — the cascade applies your values without forking the file
21
+
22
+ ```css
23
+ :root {
24
+ --sp-color-primary: oklch(60% 0.18 260);
25
+ }
26
+ ```
27
+
15
28
  ## Theme keys
16
29
 
17
30
  Each row lists the theme key, the component that uses it, and the variants it accepts.
@@ -64,6 +64,37 @@ Rails helper for rendering a themed timeline as an ordered list with optional in
64
64
  - `e.with_detail` requires `e.with_trigger` — raises `ArgumentError` otherwise.
65
65
  - `e.with_time` requires `datetime:` on `t.event` — raises `ArgumentError` otherwise.
66
66
 
67
+ ### `sp_timeline_group`
68
+
69
+ Groups events under dated sections, each with its own `<ol>` — for timelines broken up by day/month rather than one flat list.
70
+
71
+ ```erb
72
+ <%= sp_timeline_group do |g| %>
73
+ <% g.section(date: "January 2025", datetime: "2025-01-15") do |t| %>
74
+ <% t.event do |e| %>
75
+ <% e.with_indicator %>
76
+ <% e.with_title { "Event title" } %>
77
+ <% e.with_description { "Brief text" } %>
78
+ <% end %>
79
+ <% end %>
80
+ <% end %>
81
+ ```
82
+
83
+ | Option | Default | Description |
84
+ | ---------------- | ----------- | ------------------------------------------------------------------------- |
85
+ | `orientation:` | `:vertical` | Layout direction for each section's events — `:vertical` \| `:horizontal` |
86
+ | `**html_options` | — | Forwarded to the outer `<div>` |
87
+
88
+ `g.section` options:
89
+
90
+ | Option | Default | Description |
91
+ | ---------------- | ------- | -------------------------------------------------- |
92
+ | `date:` | — | Required — text content for the section's `<time>` |
93
+ | `datetime:` | `nil` | Renders `<time datetime="...">` when set |
94
+ | `**html_options` | — | Forwarded to the section's outer `<div>` |
95
+
96
+ Each `g.section` block yields a `t` scoped to that section (same `t.event` API as `sp_timeline`, non-interactive, orientation inherited from `sp_timeline_group`'s `orientation:`).
97
+
67
98
  ---
68
99
 
69
100
  ## Rendered HTML Structure
@@ -115,24 +146,28 @@ Rails helper for rendering a themed timeline as an ordered list with optional in
115
146
 
116
147
  ## Theme keys
117
148
 
118
- | Key | Element | Variants |
119
- | --------------------------- | ---------------------------------- | --------------------- |
120
- | `timeline` | Outer `<ol>` | `orientation:` |
121
- | `timeline_item` | `<li>` | `orientation:` |
122
- | `timeline_item_indicator` | Indicator `<div>` | `type: :dot \| :icon` |
123
- | `timeline_item_time` | `<time>` | — |
124
- | `timeline_item_title` | Static `<h3>` | — |
125
- | `timeline_item_heading` | `<h3>` wrapping the trigger button | — |
126
- | `timeline_item_trigger` | `<button>` inside the heading | — |
127
- | `timeline_item_description` | `<p>` | — |
128
- | `timeline_item_detail` | Collapsible `<div>` | — |
129
- | `timeline_item_actions` | Actions `<div>` | — |
149
+ | Key | Element | Variants |
150
+ | ----------------------------- | ---------------------------------- | --------------------- |
151
+ | `timeline` | Outer `<ol>` | `orientation:` |
152
+ | `timeline_item` | `<li>` | `orientation:` |
153
+ | `timeline_item_indicator` | Indicator `<div>` | `type: :dot \| :icon` |
154
+ | `timeline_item_time` | `<time>` | — |
155
+ | `timeline_item_title` | Static `<h3>` | — |
156
+ | `timeline_item_heading` | `<h3>` wrapping the trigger button | — |
157
+ | `timeline_item_trigger` | `<button>` inside the heading | — |
158
+ | `timeline_item_description` | `<p>` | — |
159
+ | `timeline_item_detail` | Collapsible `<div>` | — |
160
+ | `timeline_item_actions` | Actions `<div>` | — |
161
+ | `timeline_group` | `sp_timeline_group` outer `<div>` | — |
162
+ | `timeline_group_section` | `g.section` outer `<div>` | — |
163
+ | `timeline_group_section_date` | Section `<time>` | — |
164
+ | `timeline_group_section_list` | Section `<ol>` | `orientation:` |
130
165
 
131
166
  ---
132
167
 
133
168
  ## ARIA
134
169
 
135
- - See [ARIA.md](../../ARIA.md) for WCAG 2.1 AA criteria and disclosure widget patterns.
170
+ - See [ARIA.md](../../../ARIA.md) for WCAG 2.1 AA criteria and disclosure widget patterns.
136
171
  - The trigger button uses `aria-expanded` (managed by the JS controller) and `aria-controls` pointing to the detail `id`.
137
172
  - Indicator elements carry `aria-hidden="true"` to hide decorative markup from screen readers.
138
- - For JS controller targets, values, and actions, see the [stimulus-plumbers JS controller doc](../../stimulus-plumbers/docs/component/timeline.md).
173
+ - For JS controller targets, values, and actions, see the [stimulus-plumbers JS controller doc](../../../stimulus-plumbers/docs/component/timeline.md).
data/docs/guide.md ADDED
@@ -0,0 +1,39 @@
1
+ # Guide
2
+
3
+ ## Building forms
4
+
5
+ Use `StimulusPlumbers::Form::Builder` (set `config.action_view.default_form_builder`, or pass
6
+ `builder:` to `form_with`). Two levels:
7
+
8
+ - **Level 2 — recommended.** Full accessible field (label + input + hint + error):
9
+ `f.field(attr, as:)`, `f.collection_field(attr, as:, collection:, ...)`, `f.choice(attr, as:)`.
10
+ See [docs/component/form.md](component/form.md) for valid `as:` values per builder method and
11
+ which ones are backed by a Stimulus controller (date/time/select/search pickers).
12
+ - **Level 1.** Native helper overrides (`f.text_field`, `f.select`, `f.check_box`, ...) render
13
+ only the themed input element — use when you control the surrounding markup.
14
+
15
+ Submit with `f.submit` (themed button).
16
+
17
+ ## Building views
18
+
19
+ Render components with `sp_*` helpers (`sp_button`, `sp_button_group`, `sp_card`, `sp_list`,
20
+ `sp_link`, `sp_avatar`, `sp_divider`, `sp_icon`, `sp_popover`, ...) — see the
21
+ [Components table](../README.md#components) for the full helper list, and each component's
22
+ `docs/component/<name>.md` for its keyword options, slots, and themed params.
23
+
24
+ ## Stimulus integration
25
+
26
+ Most display components are pure markup; interactive ones (combobox, popover, calendar) emit their
27
+ `data-controller` attributes automatically — no manual wiring needed in Rails views.
28
+
29
+ ## CSS entry file detection
30
+
31
+ `bin/rails generate stimulus_plumbers:install` (and `stimulus_plumbers_tailwind`'s own install
32
+ generator) inject their CSS directives into the first entry file found, in order:
33
+
34
+ 1. `app/assets/stylesheets/application.tailwind.css` (`tailwindcss-rails` 2.x default)
35
+ 2. `app/assets/tailwind/application.css` (`tailwindcss-rails` 3.x+ default)
36
+ 3. `app/assets/stylesheets/application.css` (Rails/Propshaft default manifest)
37
+ 4. `app/javascript/entrypoints/application.css` (`jsbundling-rails` esbuild/webpack default)
38
+
39
+ Override with `STIMULUS_PLUMBERS_CSS_ENTRY=/path/to/entry.css` — used by both gems' generators.
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "stimulus_plumbers/generators/css_entrypoint"
5
+ require "stimulus_plumbers/generators/tokens_directive"
6
+
7
+ module StimulusPlumbers
8
+ module Generators
9
+ class InstallGenerator < Rails::Generators::Base
10
+ include StimulusPlumbers::Generators::CssEntrypoint
11
+
12
+ def install
13
+ css_file = entry_css_file(**css_file_lookup_options)
14
+ return warn_entry_css_not_found(label: "CSS", **css_file_lookup_options) unless css_file
15
+
16
+ apply_edit(
17
+ css_file,
18
+ TokensDirective.directive(from: File.dirname(css_file)),
19
+ stale_pattern: TokensDirective.stale_pattern
20
+ )
21
+ end
22
+
23
+ private
24
+
25
+ def css_file_lookup_options
26
+ {
27
+ candidates: CssEntrypoint::ENTRY_CANDIDATES,
28
+ env_var: CssEntrypoint::STIMULUS_PLUMBERS_CSS_ENTRY
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -4,7 +4,7 @@ module StimulusPlumbers
4
4
  module Components
5
5
  class Button < Plumber::Base
6
6
  def render(content = nil, icon_leading: nil, icon_trailing: nil, **kwargs, &block)
7
- slots = Button::Slots.new
7
+ slots = Button::Slots.new(template)
8
8
  slots.with_icon_leading(icon_leading) if icon_leading
9
9
  slots.with_icon_trailing(icon_trailing) if icon_trailing
10
10
 
@@ -51,6 +51,7 @@ module StimulusPlumbers
51
51
 
52
52
  Components::Icon.new(template).render(
53
53
  name: value,
54
+ size: :sm,
54
55
  classes: theme.resolve(:button_icon).fetch(:classes, ""),
55
56
  aria: { hidden: "true" }
56
57
  )
@@ -4,7 +4,7 @@ module StimulusPlumbers
4
4
  module Components
5
5
  class Card < Plumber::Base
6
6
  def render(variant: :tertiary, title_tag: :h2, **kwargs, &block)
7
- slots = Card::Slots.new
7
+ slots = Card::Slots.new(template)
8
8
  yield slots if block_given?
9
9
 
10
10
  html_options = merge_html_options(theme.resolve(:card, variant: variant), kwargs)
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Checklist
6
+ class Item
7
+ class Slots < Plumber::Slots
8
+ slot :title, :description
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Checklist
6
+ class Item < Plumber::Base
7
+ def render(content = nil, checked: nil, readonly: false, **html_options, &block)
8
+ raise ArgumentError, "checked: is required" if checked.nil?
9
+
10
+ slots = build_slots(content, &block)
11
+
12
+ template.content_tag(:label, **merge_html_options(theme.resolve(:checklist_item), html_options)) do
13
+ template.safe_join([render_input(checked, readonly), render_content_slot(slots)].compact)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def build_slots(content)
20
+ slots = Checklist::Item::Slots.new(template)
21
+ slots.with_title(content) if content
22
+ yield slots if block_given?
23
+ slots
24
+ end
25
+
26
+ def render_input(checked, readonly)
27
+ template.tag.input(
28
+ **merge_html_options(
29
+ theme.resolve(:checklist_item_input),
30
+ {
31
+ type: "checkbox",
32
+ checked: (checked ? true : nil),
33
+ disabled: (readonly ? true : nil),
34
+ data: { checklist_target: "item" }
35
+ }
36
+ )
37
+ )
38
+ end
39
+
40
+ def render_content_slot(slots)
41
+ title = render_title_slot(slots)
42
+ description = render_description_slot(slots)
43
+ return unless title || description
44
+
45
+ template.content_tag(:span, **merge_html_options(theme.resolve(:checklist_item_content))) do
46
+ template.safe_join([title, description])
47
+ end
48
+ end
49
+
50
+ def render_title_slot(slots)
51
+ slots.resolve(:title) do |v|
52
+ template.content_tag(:span, v, **merge_html_options(theme.resolve(:checklist_item_title)))
53
+ end
54
+ end
55
+
56
+ def render_description_slot(slots)
57
+ slots.resolve(:description) do |v|
58
+ template.content_tag(:span, v, **merge_html_options(theme.resolve(:checklist_item_description)))
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Checklist < Plumber::Base
6
+ def render(label: nil, labelledby: nil, select_all: false, select_all_label: "Select all", **kwargs, &block)
7
+ @item_states = [] if select_all
8
+ captured = template.capture(self, &block)
9
+ master = select_all ? render_select_all(select_all_label) : nil
10
+ html_options = merge_html_options(
11
+ theme.resolve(:checklist),
12
+ kwargs,
13
+ { role: "group", aria: labelled_aria(label, labelledby: labelledby) }
14
+ )
15
+ html_options = merge_html_options(html_options, select_all_wrapper_attrs) if select_all
16
+ template.content_tag(:div, template.safe_join([master, captured].compact), **html_options)
17
+ end
18
+
19
+ def item(content = nil, **kwargs, &block)
20
+ @item_states << kwargs[:checked] if @item_states && !kwargs[:readonly]
21
+ Checklist::Item.new(template).render(content, **kwargs, &block)
22
+ end
23
+
24
+ private
25
+
26
+ def render_select_all(select_all_label)
27
+ template.content_tag(:label, **merge_html_options(theme.resolve(:checklist_item))) do
28
+ template.safe_join([render_master_input, template.content_tag(:span, select_all_label)])
29
+ end
30
+ end
31
+
32
+ def render_master_input
33
+ template.tag.input(
34
+ **merge_html_options(
35
+ theme.resolve(:checklist_item_input),
36
+ { type: "checkbox", checked: all_items_checked? || nil, data: { checklist_target: "master" } }
37
+ )
38
+ )
39
+ end
40
+
41
+ def select_all_wrapper_attrs
42
+ { data: { controller: "checklist", action: "change->checklist#onChange" } }
43
+ end
44
+
45
+ def all_items_checked?
46
+ @item_states.present? && @item_states.all? { |state| state == true }
47
+ end
48
+ end
49
+ end
50
+ end
@@ -75,6 +75,7 @@ module StimulusPlumbers
75
75
  ) do
76
76
  Components::Icon.new(template).render(
77
77
  name: "spinner",
78
+ size: :md,
78
79
  classes: theme.resolve(:combobox_typeahead_loading_icon).fetch(:classes, ""),
79
80
  aria: { hidden: "true" }
80
81
  )
@@ -21,7 +21,7 @@ module StimulusPlumbers
21
21
  private
22
22
 
23
23
  def resolve_builder
24
- builder = Combobox::Builder.new
24
+ builder = Combobox::Builder.new(template)
25
25
  yield builder if block_given?
26
26
  builder
27
27
  end
@@ -7,9 +7,9 @@ module StimulusPlumbers
7
7
  value.is_a?(Symbol) || (value.is_a?(String) && !value.html_safe?)
8
8
  end
9
9
 
10
- def render(name:, **kwargs)
10
+ def render(name:, size: :lg, **kwargs)
11
11
  html_options = merge_html_options(
12
- theme.resolve(:icon),
12
+ theme.resolve(:icon, size: size),
13
13
  kwargs
14
14
  )
15
15
 
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Indicator < Plumber::Base
6
+ def render(...) = render_indicator(...)
7
+
8
+ private
9
+
10
+ def render_indicator(variant: :primary, type: :dot, pulse: false, **kwargs, &block)
11
+ dot = render_dot(variant: variant, type: type, kwargs: kwargs, block: block)
12
+ return dot unless pulse
13
+
14
+ template.content_tag(
15
+ :span, template.safe_join([render_pulse_ring, dot]), **merge_html_options(theme.resolve(:indicator_wrapper))
16
+ )
17
+ end
18
+
19
+ def render_dot(variant:, type:, kwargs:, block:)
20
+ content = block ? template.capture(&block) : nil
21
+ html_options = merge_html_options(theme.resolve(:indicator, type: type, variant: variant), kwargs)
22
+ template.content_tag(:span, content, **html_options)
23
+ end
24
+
25
+ def render_pulse_ring
26
+ template.content_tag(:span, nil, **merge_html_options(theme.resolve(:indicator_pulse), { aria: { hidden: "true" } }))
27
+ end
28
+ end
29
+ end
30
+ end