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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +15 -1
- data/app/assets/javascripts/stimulus-plumbers/controllers.manifest.json +331 -20
- data/app/assets/javascripts/stimulus-plumbers/index.es.js +493 -226
- data/app/assets/javascripts/stimulus-plumbers/index.es.js.map +1 -1
- data/app/assets/javascripts/stimulus-plumbers/index.umd.js +1 -1
- data/app/assets/javascripts/stimulus-plumbers/index.umd.js.map +1 -1
- data/app/assets/stylesheets/stimulus_plumbers/tokens.css +58 -53
- data/docs/architecture.md +0 -2
- data/docs/component/avatar.md +1 -0
- data/docs/component/button.md +2 -1
- data/docs/component/calendar.md +18 -20
- data/docs/component/card.md +14 -12
- data/docs/component/checklist.md +153 -0
- data/docs/component/combobox.md +2 -2
- data/docs/component/form.md +32 -6
- data/docs/component/icon.md +7 -5
- data/docs/component/indicator.md +55 -0
- data/docs/component/list.md +4 -5
- data/docs/component/ordered_list.md +118 -0
- data/docs/component/plumber.md +43 -0
- data/docs/component/popover.md +1 -2
- data/docs/component/progress.md +65 -0
- data/docs/component/theme.md +15 -2
- data/docs/component/timeline.md +49 -14
- data/docs/guide.md +39 -0
- data/lib/generators/stimulus_plumbers/install/install_generator.rb +33 -0
- data/lib/stimulus_plumbers/components/button.rb +2 -1
- data/lib/stimulus_plumbers/components/card.rb +1 -1
- data/lib/stimulus_plumbers/components/checklist/item/slots.rb +13 -0
- data/lib/stimulus_plumbers/components/checklist/item.rb +64 -0
- data/lib/stimulus_plumbers/components/checklist.rb +50 -0
- data/lib/stimulus_plumbers/components/combobox/typeahead.rb +1 -0
- data/lib/stimulus_plumbers/components/combobox.rb +1 -1
- data/lib/stimulus_plumbers/components/icon.rb +2 -2
- data/lib/stimulus_plumbers/components/indicator.rb +30 -0
- data/lib/stimulus_plumbers/components/link.rb +2 -1
- data/lib/stimulus_plumbers/components/list/item.rb +1 -1
- data/lib/stimulus_plumbers/components/ordered_list/item/slots.rb +13 -0
- data/lib/stimulus_plumbers/components/ordered_list/item.rb +129 -0
- data/lib/stimulus_plumbers/components/ordered_list.rb +24 -0
- data/lib/stimulus_plumbers/components/progress_bar.rb +48 -0
- data/lib/stimulus_plumbers/components/progress_meter.rb +37 -0
- data/lib/stimulus_plumbers/components/progress_ring.rb +40 -0
- data/lib/stimulus_plumbers/components/timeline/event.rb +3 -2
- data/lib/stimulus_plumbers/components/timeline/group.rb +5 -3
- data/lib/stimulus_plumbers/engine.rb +4 -0
- data/lib/stimulus_plumbers/form/fields/inputs/password.rb +1 -0
- data/lib/stimulus_plumbers/form/fields/inputs/search.rb +1 -0
- data/lib/stimulus_plumbers/generators/css_entrypoint.rb +80 -0
- data/lib/stimulus_plumbers/generators/tokens_directive.rb +24 -0
- data/lib/stimulus_plumbers/helpers/avatar_helper.rb +1 -7
- data/lib/stimulus_plumbers/helpers/button_helper.rb +1 -7
- data/lib/stimulus_plumbers/helpers/calendar_helper.rb +2 -9
- data/lib/stimulus_plumbers/helpers/calendar_turbo_helper.rb +3 -6
- data/lib/stimulus_plumbers/helpers/checklist_helper.rb +11 -0
- data/lib/stimulus_plumbers/helpers/indicator_helper.rb +11 -0
- data/lib/stimulus_plumbers/helpers/ordered_list_helper.rb +11 -0
- data/lib/stimulus_plumbers/helpers/popover_helper.rb +1 -7
- data/lib/stimulus_plumbers/helpers/progress_helper.rb +21 -0
- data/lib/stimulus_plumbers/helpers.rb +8 -0
- data/lib/stimulus_plumbers/plumber/slots.rb +10 -3
- data/lib/stimulus_plumbers/themes/base.rb +4 -0
- data/lib/stimulus_plumbers/themes/schema/icon.rb +21 -4
- data/lib/stimulus_plumbers/themes/schema/indicator/ranges.rb +14 -0
- data/lib/stimulus_plumbers/themes/schema.rb +39 -3
- data/lib/stimulus_plumbers/version.rb +1 -1
- data/lib/stimulus_plumbers.rb +10 -0
- data/lib/tasks/stimulus_plumbers.rake +18 -0
- data/vendor/ARIA.md +108 -0
- data/vendor/component/manifest.json +212 -0
- data/vendor/controller/docs/calendar.md +212 -0
- data/vendor/controller/docs/checklist.md +45 -0
- data/vendor/controller/docs/clipboard.md +61 -0
- data/vendor/controller/docs/combobox.md +212 -0
- data/vendor/controller/docs/dismisser.md +39 -0
- data/vendor/controller/docs/flipper.md +46 -0
- data/vendor/controller/docs/input-clearable.md +43 -0
- data/vendor/controller/docs/input-formatter.md +89 -0
- data/vendor/controller/docs/modal.md +57 -0
- data/vendor/controller/docs/panner.md +24 -0
- data/vendor/controller/docs/popover.md +111 -0
- data/vendor/controller/docs/progress.md +82 -0
- data/vendor/controller/docs/reorderable.md +82 -0
- data/vendor/controller/docs/timeline.md +84 -0
- data/vendor/controller/docs/visibility.md +31 -0
- data/vendor/controller/guide.md +16 -0
- data/vendor/{controllers.manifest.json → controller/manifest.json} +331 -20
- 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).
|
data/docs/component/plumber.md
CHANGED
|
@@ -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
|
+
```
|
data/docs/component/popover.md
CHANGED
|
@@ -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.
|
data/docs/component/theme.md
CHANGED
|
@@ -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(
|
|
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.
|
data/docs/component/timeline.md
CHANGED
|
@@ -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
|
|
119
|
-
|
|
|
120
|
-
| `timeline`
|
|
121
|
-
| `timeline_item`
|
|
122
|
-
| `timeline_item_indicator`
|
|
123
|
-
| `timeline_item_time`
|
|
124
|
-
| `timeline_item_title`
|
|
125
|
-
| `timeline_item_heading`
|
|
126
|
-
| `timeline_item_trigger`
|
|
127
|
-
| `timeline_item_description`
|
|
128
|
-
| `timeline_item_detail`
|
|
129
|
-
| `timeline_item_actions`
|
|
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](
|
|
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](
|
|
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,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
|
|
@@ -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
|