stimulus_plumbers 0.4.8 → 0.4.10
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 +39 -0
- data/README.md +7 -9
- data/app/assets/javascripts/stimulus-plumbers/controllers.manifest.json +367 -23
- data/app/assets/javascripts/stimulus-plumbers/index.es.js +678 -233
- 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 +1 -0
- data/docs/component/calendar.md +7 -16
- data/docs/component/checklist.md +153 -0
- data/docs/component/form.md +27 -5
- data/docs/component/icon.md +7 -5
- data/docs/component/indicator.md +55 -0
- data/docs/component/list.md +2 -3
- data/docs/component/ordered_list.md +118 -0
- data/docs/component/plumber.md +21 -0
- data/docs/component/popover.md +1 -2
- data/docs/component/progress.md +65 -0
- data/docs/component/timeline.md +47 -12
- data/docs/guide.md +21 -0
- data/lib/generators/stimulus_plumbers/install/install_generator.rb +21 -11
- data/lib/stimulus_plumbers/components/button.rb +1 -0
- 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/icon.rb +2 -2
- data/lib/stimulus_plumbers/components/indicator.rb +30 -0
- data/lib/stimulus_plumbers/components/link.rb +1 -0
- 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 +2 -1
- data/lib/stimulus_plumbers/components/timeline/group.rb +5 -3
- data/lib/stimulus_plumbers/engine.rb +0 -4
- data/lib/stimulus_plumbers/form/builder.rb +4 -0
- data/lib/stimulus_plumbers/form/field.rb +1 -1
- data/lib/stimulus_plumbers/form/fields/inputs/code.rb +86 -0
- data/lib/stimulus_plumbers/form/fields/inputs/credit_card.rb +98 -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/form/fields/renderer.rb +2 -0
- data/lib/stimulus_plumbers/generators/css_entrypoint.rb +37 -9
- data/lib/stimulus_plumbers/generators/tokens_directive.rb +9 -5
- 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/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 +82 -25
- data/lib/stimulus_plumbers/version.rb +1 -1
- data/lib/stimulus_plumbers.rb +10 -0
- data/vendor/ARIA.md +29 -2
- data/vendor/component/manifest.json +215 -0
- data/vendor/controller/docs/calendar.md +4 -7
- data/vendor/controller/docs/checklist.md +45 -0
- data/vendor/controller/docs/combobox.md +1 -31
- data/vendor/controller/docs/input-clearable.md +1 -4
- data/vendor/controller/docs/input-formatter.md +88 -23
- data/vendor/controller/docs/modal.md +1 -4
- data/vendor/controller/docs/popover.md +1 -3
- data/vendor/controller/docs/progress.md +82 -0
- data/vendor/controller/docs/reorderable.md +82 -0
- data/vendor/controller/docs/visibility.md +31 -0
- data/vendor/controller/manifest.json +367 -23
- metadata +27 -1
data/docs/component/plumber.md
CHANGED
|
@@ -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
|
+
```
|
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/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,18 +146,22 @@ 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
|
|
data/docs/guide.md
CHANGED
|
@@ -25,3 +25,24 @@ Render components with `sp_*` helpers (`sp_button`, `sp_button_group`, `sp_card`
|
|
|
25
25
|
|
|
26
26
|
Most display components are pure markup; interactive ones (combobox, popover, calendar) emit their
|
|
27
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.
|
|
40
|
+
|
|
41
|
+
## Installed CSS files
|
|
42
|
+
|
|
43
|
+
The core installer copies its token defaults to
|
|
44
|
+
`app/assets/stylesheets/stimulus_plumbers/tokens.css` and imports that app-local file.
|
|
45
|
+
|
|
46
|
+
This file is application-owned: later generator runs create a missing file but never overwrite an
|
|
47
|
+
existing one. They also replace legacy imports that pointed into an installed gem with the new
|
|
48
|
+
relative app-local import.
|
|
@@ -9,20 +9,30 @@ module StimulusPlumbers
|
|
|
9
9
|
class InstallGenerator < Rails::Generators::Base
|
|
10
10
|
include StimulusPlumbers::Generators::CssEntrypoint
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
app/javascript/entrypoints/application.css
|
|
17
|
-
].freeze
|
|
12
|
+
TOKENS_CSS_SOURCE = File.join(
|
|
13
|
+
File.expand_path("../../../..", __dir__),
|
|
14
|
+
TokensDirective::TOKENS_CSS_PATH
|
|
15
|
+
)
|
|
18
16
|
|
|
19
17
|
def install
|
|
20
|
-
css_file = entry_css_file(
|
|
21
|
-
unless css_file
|
|
22
|
-
|
|
23
|
-
end
|
|
18
|
+
css_file = entry_css_file(**css_file_lookup_options)
|
|
19
|
+
return warn_entry_css_not_found(label: "CSS", **css_file_lookup_options) unless css_file
|
|
20
|
+
return unless copy_asset(TOKENS_CSS_SOURCE, TokensDirective::TOKENS_CSS_PATH)
|
|
24
21
|
|
|
25
|
-
apply_edit(
|
|
22
|
+
apply_edit(
|
|
23
|
+
css_file,
|
|
24
|
+
TokensDirective.directive(from: File.dirname(css_file), destination_root: destination_root),
|
|
25
|
+
stale_pattern: TokensDirective.stale_pattern
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def css_file_lookup_options
|
|
32
|
+
{
|
|
33
|
+
candidates: CssEntrypoint::ENTRY_CANDIDATES,
|
|
34
|
+
env_var: CssEntrypoint::STIMULUS_PLUMBERS_CSS_ENTRY
|
|
35
|
+
}
|
|
26
36
|
end
|
|
27
37
|
end
|
|
28
38
|
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
|
|
@@ -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
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
class OrderedList
|
|
6
|
+
class Item < Plumber::Base
|
|
7
|
+
HANDLE_ACTION = "pointerdown->reorderable#onPointerDown pointermove->reorderable#onPointerMove " \
|
|
8
|
+
"pointerup->reorderable#onPointerUp"
|
|
9
|
+
|
|
10
|
+
def render(content = nil, id:, handle: :item, url: nil, target: nil, active: false, **html_options, &block)
|
|
11
|
+
validate_options!(id, url, html_options)
|
|
12
|
+
|
|
13
|
+
slots = build_slots(content, &block)
|
|
14
|
+
|
|
15
|
+
build(id: id, handle: handle) do |attrs|
|
|
16
|
+
template.content_tag(:li, **attrs) do
|
|
17
|
+
template.safe_join(
|
|
18
|
+
[
|
|
19
|
+
render_icon_position(slots, :leading),
|
|
20
|
+
render_body(slots, url: url, target: target, active: active, **html_options),
|
|
21
|
+
render_icon_position(slots, :trailing)
|
|
22
|
+
]
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def build(id:, handle: :item, &block)
|
|
29
|
+
@handle = handle
|
|
30
|
+
html_options = merge_html_options(theme.resolve(:ordered_list_item), { id: id }, item_target_attrs)
|
|
31
|
+
template.capture(html_options, &block)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def validate_options!(id, url, html_options)
|
|
37
|
+
raise ArgumentError, "id: is required so the item appears in reorderable:reordered's itemIds" if id.blank?
|
|
38
|
+
return unless url.blank? && html_options.present?
|
|
39
|
+
|
|
40
|
+
raise ArgumentError, "html_options are only applied to the <a> rendered when url: is set"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def build_slots(content, &block)
|
|
44
|
+
slots = OrderedList::Item::Slots.new(template)
|
|
45
|
+
slots.with_title(content) if content
|
|
46
|
+
yield slots if block_given?
|
|
47
|
+
slots
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def item_target_attrs
|
|
51
|
+
if @handle == :item
|
|
52
|
+
{ data: { "reorderable-target": "item handle", action: HANDLE_ACTION } }
|
|
53
|
+
else
|
|
54
|
+
{ data: { "reorderable-target": "item" } }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def render_body(slots, url:, target:, active:, **html_options)
|
|
59
|
+
content = render_content_slot(slots)
|
|
60
|
+
return content unless url.present?
|
|
61
|
+
|
|
62
|
+
aria = active ? { aria: { current: "page" } } : {}
|
|
63
|
+
attrs = merge_html_options(html_options, { data: { "reorderable-target": "trigger" } }, aria)
|
|
64
|
+
template.content_tag(:a, content, href: url, target: target, **attrs)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def render_icon_position(slots, position)
|
|
68
|
+
slot_name = position == :leading ? :icon_leading : :icon_trailing
|
|
69
|
+
is_handle = @handle == position
|
|
70
|
+
custom_icon = render_icon_slot(slots, slot_name)
|
|
71
|
+
return unless custom_icon || is_handle
|
|
72
|
+
|
|
73
|
+
html_options = merge_html_options(theme.resolve(:ordered_list_item_handle), handle_target_attrs(is_handle))
|
|
74
|
+
template.content_tag(:span, custom_icon || default_handle_icon, **html_options)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def handle_target_attrs(is_handle)
|
|
78
|
+
return {} unless is_handle
|
|
79
|
+
|
|
80
|
+
{ data: { "reorderable-target": "handle", action: HANDLE_ACTION } }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def render_icon_slot(slots, name)
|
|
84
|
+
slots.resolve(name) do |value|
|
|
85
|
+
next value unless Components::Icon.icon_name?(value)
|
|
86
|
+
|
|
87
|
+
Components::Icon.new(template).render(
|
|
88
|
+
name: value,
|
|
89
|
+
size: :sm,
|
|
90
|
+
classes: theme.resolve(:ordered_list_item_handle).fetch(:classes, ""),
|
|
91
|
+
aria: { hidden: "true" }
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def default_handle_icon
|
|
97
|
+
Components::Icon.new(template).render(
|
|
98
|
+
name: "grip-vertical",
|
|
99
|
+
size: :sm,
|
|
100
|
+
classes: theme.resolve(:ordered_list_item_handle).fetch(:classes, ""),
|
|
101
|
+
aria: { hidden: "true" }
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def render_content_slot(slots)
|
|
106
|
+
title = render_title_slot(slots)
|
|
107
|
+
description = render_description_slot(slots)
|
|
108
|
+
return unless title || description
|
|
109
|
+
|
|
110
|
+
template.content_tag(:span, **merge_html_options(theme.resolve(:ordered_list_item_content))) do
|
|
111
|
+
template.safe_join([title, description])
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def render_title_slot(slots)
|
|
116
|
+
slots.resolve(:title) do |v|
|
|
117
|
+
template.content_tag(:span, v, **merge_html_options(theme.resolve(:ordered_list_item_title)))
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def render_description_slot(slots)
|
|
122
|
+
slots.resolve(:description) do |v|
|
|
123
|
+
template.content_tag(:span, v, **merge_html_options(theme.resolve(:ordered_list_item_description)))
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
class OrderedList < Plumber::Base
|
|
6
|
+
def render(move_key: "Alt", editing: false, orientation: nil, role: "list", **kwargs, &block)
|
|
7
|
+
stimulus = {
|
|
8
|
+
data: {
|
|
9
|
+
controller: "reorderable",
|
|
10
|
+
"reorderable-move-key-value": move_key,
|
|
11
|
+
"reorderable-editing-value": editing,
|
|
12
|
+
"reorderable-orientation-value": orientation
|
|
13
|
+
}.compact
|
|
14
|
+
}
|
|
15
|
+
html_options = merge_html_options(theme.resolve(:ordered_list), kwargs, stimulus)
|
|
16
|
+
template.content_tag(:ol, template.capture(self, &block), **html_options, role: role)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def item(content = nil, **kwargs, &block)
|
|
20
|
+
OrderedList::Item.new(template).render(content, **kwargs, &block)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Components
|
|
5
|
+
class ProgressBar < Plumber::Base
|
|
6
|
+
def render(...) = render_bar(...)
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def render_bar(value:, min: 0, max: 100, indeterminate: false, **kwargs)
|
|
11
|
+
html_options = merge_html_options(
|
|
12
|
+
theme.resolve(:progress_bar),
|
|
13
|
+
kwargs,
|
|
14
|
+
stimulus_data(value: value, min: min, max: max, indeterminate: indeterminate),
|
|
15
|
+
{ role: "progressbar", aria: progress_aria(value: value, min: min, max: max, indeterminate: indeterminate) }
|
|
16
|
+
)
|
|
17
|
+
template.content_tag(:div, render_fill, **html_options)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def render_fill
|
|
21
|
+
template.content_tag(
|
|
22
|
+
:div,
|
|
23
|
+
nil,
|
|
24
|
+
**merge_html_options(theme.resolve(:progress_bar_fill), { data: { "progress-target": "fill" } })
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def progress_aria(value:, min:, max:, indeterminate:)
|
|
29
|
+
aria = { valuemin: min, valuemax: max }
|
|
30
|
+
aria[:valuenow] = value unless indeterminate
|
|
31
|
+
aria
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def stimulus_data(value:, min:, max:, indeterminate:)
|
|
35
|
+
{
|
|
36
|
+
data: {
|
|
37
|
+
controller: "progress",
|
|
38
|
+
"progress-variant-value": "bar",
|
|
39
|
+
"progress-current-value": value,
|
|
40
|
+
"progress-min-value": min,
|
|
41
|
+
"progress-max-value": max,
|
|
42
|
+
"progress-indeterminate-value": indeterminate
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|