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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -0
  3. data/README.md +7 -9
  4. data/app/assets/javascripts/stimulus-plumbers/controllers.manifest.json +367 -23
  5. data/app/assets/javascripts/stimulus-plumbers/index.es.js +678 -233
  6. data/app/assets/javascripts/stimulus-plumbers/index.es.js.map +1 -1
  7. data/app/assets/javascripts/stimulus-plumbers/index.umd.js +1 -1
  8. data/app/assets/javascripts/stimulus-plumbers/index.umd.js.map +1 -1
  9. data/app/assets/stylesheets/stimulus_plumbers/tokens.css +58 -53
  10. data/docs/architecture.md +0 -2
  11. data/docs/component/avatar.md +1 -0
  12. data/docs/component/button.md +1 -0
  13. data/docs/component/calendar.md +7 -16
  14. data/docs/component/checklist.md +153 -0
  15. data/docs/component/form.md +27 -5
  16. data/docs/component/icon.md +7 -5
  17. data/docs/component/indicator.md +55 -0
  18. data/docs/component/list.md +2 -3
  19. data/docs/component/ordered_list.md +118 -0
  20. data/docs/component/plumber.md +21 -0
  21. data/docs/component/popover.md +1 -2
  22. data/docs/component/progress.md +65 -0
  23. data/docs/component/timeline.md +47 -12
  24. data/docs/guide.md +21 -0
  25. data/lib/generators/stimulus_plumbers/install/install_generator.rb +21 -11
  26. data/lib/stimulus_plumbers/components/button.rb +1 -0
  27. data/lib/stimulus_plumbers/components/checklist/item/slots.rb +13 -0
  28. data/lib/stimulus_plumbers/components/checklist/item.rb +64 -0
  29. data/lib/stimulus_plumbers/components/checklist.rb +50 -0
  30. data/lib/stimulus_plumbers/components/combobox/typeahead.rb +1 -0
  31. data/lib/stimulus_plumbers/components/icon.rb +2 -2
  32. data/lib/stimulus_plumbers/components/indicator.rb +30 -0
  33. data/lib/stimulus_plumbers/components/link.rb +1 -0
  34. data/lib/stimulus_plumbers/components/ordered_list/item/slots.rb +13 -0
  35. data/lib/stimulus_plumbers/components/ordered_list/item.rb +129 -0
  36. data/lib/stimulus_plumbers/components/ordered_list.rb +24 -0
  37. data/lib/stimulus_plumbers/components/progress_bar.rb +48 -0
  38. data/lib/stimulus_plumbers/components/progress_meter.rb +37 -0
  39. data/lib/stimulus_plumbers/components/progress_ring.rb +40 -0
  40. data/lib/stimulus_plumbers/components/timeline/event.rb +2 -1
  41. data/lib/stimulus_plumbers/components/timeline/group.rb +5 -3
  42. data/lib/stimulus_plumbers/engine.rb +0 -4
  43. data/lib/stimulus_plumbers/form/builder.rb +4 -0
  44. data/lib/stimulus_plumbers/form/field.rb +1 -1
  45. data/lib/stimulus_plumbers/form/fields/inputs/code.rb +86 -0
  46. data/lib/stimulus_plumbers/form/fields/inputs/credit_card.rb +98 -0
  47. data/lib/stimulus_plumbers/form/fields/inputs/password.rb +1 -0
  48. data/lib/stimulus_plumbers/form/fields/inputs/search.rb +1 -0
  49. data/lib/stimulus_plumbers/form/fields/renderer.rb +2 -0
  50. data/lib/stimulus_plumbers/generators/css_entrypoint.rb +37 -9
  51. data/lib/stimulus_plumbers/generators/tokens_directive.rb +9 -5
  52. data/lib/stimulus_plumbers/helpers/avatar_helper.rb +1 -7
  53. data/lib/stimulus_plumbers/helpers/button_helper.rb +1 -7
  54. data/lib/stimulus_plumbers/helpers/calendar_helper.rb +2 -9
  55. data/lib/stimulus_plumbers/helpers/calendar_turbo_helper.rb +3 -6
  56. data/lib/stimulus_plumbers/helpers/checklist_helper.rb +11 -0
  57. data/lib/stimulus_plumbers/helpers/indicator_helper.rb +11 -0
  58. data/lib/stimulus_plumbers/helpers/ordered_list_helper.rb +11 -0
  59. data/lib/stimulus_plumbers/helpers/popover_helper.rb +1 -7
  60. data/lib/stimulus_plumbers/helpers/progress_helper.rb +21 -0
  61. data/lib/stimulus_plumbers/helpers.rb +8 -0
  62. data/lib/stimulus_plumbers/themes/base.rb +4 -0
  63. data/lib/stimulus_plumbers/themes/schema/icon.rb +21 -4
  64. data/lib/stimulus_plumbers/themes/schema/indicator/ranges.rb +14 -0
  65. data/lib/stimulus_plumbers/themes/schema.rb +82 -25
  66. data/lib/stimulus_plumbers/version.rb +1 -1
  67. data/lib/stimulus_plumbers.rb +10 -0
  68. data/vendor/ARIA.md +29 -2
  69. data/vendor/component/manifest.json +215 -0
  70. data/vendor/controller/docs/calendar.md +4 -7
  71. data/vendor/controller/docs/checklist.md +45 -0
  72. data/vendor/controller/docs/combobox.md +1 -31
  73. data/vendor/controller/docs/input-clearable.md +1 -4
  74. data/vendor/controller/docs/input-formatter.md +88 -23
  75. data/vendor/controller/docs/modal.md +1 -4
  76. data/vendor/controller/docs/popover.md +1 -3
  77. data/vendor/controller/docs/progress.md +82 -0
  78. data/vendor/controller/docs/reorderable.md +82 -0
  79. data/vendor/controller/docs/visibility.md +31 -0
  80. data/vendor/controller/manifest.json +367 -23
  81. metadata +27 -1
@@ -203,3 +203,24 @@ def merge_html_options(*hashes)
203
203
  end
204
204
  end
205
205
  ```
206
+
207
+ ---
208
+
209
+ ## PlumberHelper
210
+
211
+ ### `sp_dom_id(record = nil, prefix: nil, suffix: nil)`
212
+
213
+ Generates an `"sp_"`-prefixed DOM id for wiring `aria-controls`/`id` pairs between a trigger and its target. Not a component renderer — an internal id-generation utility used by `Combobox`, `Popover`, and `Timeline::Event` when the caller doesn't supply an explicit `id:`.
214
+
215
+ | Param | Description |
216
+ | --------- | ---------------------------------------------------------------------------------------------------------------------- |
217
+ | `record` | ActiveRecord-like object — delegates to Rails' `dom_id(record, prefix)`. Omit for a random id (`SecureRandom.hex(8)`). |
218
+ | `prefix:` | Forwarded to `dom_id` when `record` is given. |
219
+ | `suffix:` | Appended after an underscore, regardless of whether `record` is given. |
220
+
221
+ ```ruby
222
+ sp_dom_id # => "sp_3f9a1b2c4d5e6f70"
223
+ sp_dom_id(suffix: "panel") # => "sp_3f9a1b2c4d5e6f70_panel"
224
+ sp_dom_id(post) # => "sp_post_1"
225
+ sp_dom_id(post, prefix: :edit, suffix: :form) # => "sp_edit_post_1_form"
226
+ ```
@@ -130,9 +130,8 @@ end
130
130
 
131
131
  ## ARIA
132
132
 
133
- - Default trigger button has `aria-haspopup="dialog"`, `aria-expanded="false"`, and `aria-controls` pointing to the panel.
134
- - `aria-expanded` is toggled to `"true"` / `"false"` by the Stimulus controller.
135
133
  - Pass `role: "dialog"` or `role: "tooltip"` on `p.panel` based on whether the content is interactive.
136
134
  - `aria: { label: }` or `aria: { labelledby: }` on `p.panel` provides the accessible name.
135
+ - See [ARIA.md's Popover pattern](../../../ARIA.md) for the trigger's `aria-haspopup`/`aria-expanded`/`aria-controls` wiring.
137
136
 
138
137
  For the JS controller API (targets, values, keyboard behaviour), see the [JS package docs](../../../stimulus-plumbers/docs/component/popover.md).
@@ -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.
@@ -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 | 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
 
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
- STIMULUS_PLUMBERS_CSS_FILE = CssEntrypoint::STIMULUS_PLUMBERS_CSS_FILE
13
- CSS_CANDIDATES = %w[
14
- app/assets/stylesheets/application.tailwind.css
15
- app/assets/stylesheets/application.css
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(candidates: CSS_CANDIDATES, env_var: STIMULUS_PLUMBERS_CSS_FILE)
21
- unless css_file
22
- return warn_entry_css_not_found(candidates: CSS_CANDIDATES, env_var: STIMULUS_PLUMBERS_CSS_FILE, label: "CSS")
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(css_file, TokensDirective.directive, stale_pattern: TokensDirective.stale_pattern)
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
@@ -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
  )
@@ -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
  )
@@ -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
@@ -45,6 +45,7 @@ module StimulusPlumbers
45
45
 
46
46
  Components::Icon.new(template).render(
47
47
  name: value,
48
+ size: :sm,
48
49
  classes: theme.resolve(:link_icon).fetch(:classes, ""),
49
50
  aria: { hidden: "true" }
50
51
  )
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class OrderedList
6
+ class Item
7
+ class Slots < Plumber::Slots
8
+ slot :icon_leading, :title, :description, :icon_trailing
9
+ end
10
+ end
11
+ end
12
+ end
13
+ 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