stimulus_plumbers 0.4.11 → 0.4.12

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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/docs/architecture.md +1 -1
  4. data/docs/component/form.md +36 -15
  5. data/docs/guide.md +2 -1
  6. data/lib/stimulus_plumbers/components/button/icon_layout.rb +34 -0
  7. data/lib/stimulus_plumbers/components/button.rb +2 -23
  8. data/lib/stimulus_plumbers/components/combobox/builder.rb +52 -14
  9. data/lib/stimulus_plumbers/components/combobox/date.rb +35 -9
  10. data/lib/stimulus_plumbers/components/combobox/dropdown.rb +22 -6
  11. data/lib/stimulus_plumbers/components/combobox/time.rb +18 -5
  12. data/lib/stimulus_plumbers/components/combobox/typeahead.rb +22 -7
  13. data/lib/stimulus_plumbers/components/icon.rb +4 -2
  14. data/lib/stimulus_plumbers/components/indicator.rb +3 -1
  15. data/lib/stimulus_plumbers/components/popover.rb +7 -3
  16. data/lib/stimulus_plumbers/components/progress_bar.rb +3 -1
  17. data/lib/stimulus_plumbers/components/progress_meter.rb +3 -1
  18. data/lib/stimulus_plumbers/components/progress_ring.rb +3 -1
  19. data/lib/stimulus_plumbers/components/timeline/event.rb +4 -2
  20. data/lib/stimulus_plumbers/form/field.rb +4 -2
  21. data/lib/stimulus_plumbers/form/fields/inputs/code.rb +93 -32
  22. data/lib/stimulus_plumbers/form/fields/inputs/credit_card.rb +61 -33
  23. data/lib/stimulus_plumbers/form/fields/inputs/password.rb +5 -2
  24. data/lib/stimulus_plumbers/form/fields/inputs/submit.rb +52 -7
  25. data/lib/stimulus_plumbers/generators/css_entrypoint.rb +10 -0
  26. data/lib/stimulus_plumbers/plumber/dispatcher.rb +14 -12
  27. data/lib/stimulus_plumbers/plumber/slots.rb +8 -8
  28. data/lib/stimulus_plumbers/themes/schema.rb +4 -0
  29. data/lib/stimulus_plumbers/version.rb +1 -1
  30. data/lib/stimulus_plumbers.rb +1 -0
  31. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5e0d3bcf8e99d854f9ec64f1831c969a83f0e658f8b0bb7704e51129e24743a
4
- data.tar.gz: '01789f5f50ccfa6cf83acde5ff4d4f61fb15552f49212742c39fe3540600b487'
3
+ metadata.gz: daf604780fb07315e9c562c6a3c9d4ea0cb054c03236c0947e1a58ed0d100ba0
4
+ data.tar.gz: abbb7e4dbe89695cd04a6c1951d1c693ac3c24bcc45d3ccace964c62b1fec6ac
5
5
  SHA512:
6
- metadata.gz: 5f16ccf7913cc4122ce9eb12c50d8e39470007ac60bfafc211cd7fb3bba2b22d20ffaaf50ecc1ac31889c0cb85df35dc17b7f649cbc4c72d49b826c62d6797a3
7
- data.tar.gz: af073d3c389b348b2b312d2616e55f4d921fd09a949684e5517b37483d33f9d707f893a3f1e5019e5a2265f21b346d7fbdd2c06d67978391bd54a904929bba77
6
+ metadata.gz: a99f6ebfbbb8e7aa0bfb273926f6828a82fb463b19407ef84f9d145d5e7f59aa14486f0388fc1aad96c6a76cd1508edae469f42bf3e2274915037e5faf2875d3
7
+ data.tar.gz: 33a5f7f61e581f30604a9f6a49c55d2b244f52aa0d52283c0c114e57e45adca7dee6013bdc7072a9c511d42edd96cbd1ec01d18ac476bbfd1a5dd4d675e3748e
data/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.
4
4
 
5
+ ---
6
+ ## [0.4.11](https://github.com/ryancyq/stimulus-plumbers/compare/stimulus-plumbers-rails/v0.4.10..stimulus-plumbers-rails/v0.4.11) - 2026-07-19
7
+
8
+ ### Bug Fixes
9
+
10
+ - tailwind source being committed with machine path. migrate to generated source file which should be gitignored + import the generated file in css entry point file instead. ([#180](https://github.com/ryancyq/stimulus-plumbers/issues/180)) - ([347e6ed](https://github.com/ryancyq/stimulus-plumbers/commit/347e6edeee92571f4f2fe7e509b5eed8fd0f2893)) - Ryan Chang
11
+
5
12
  ---
6
13
  ## [0.4.10](https://github.com/ryancyq/stimulus-plumbers/compare/stimulus-plumbers-rails/v0.4.9..stimulus-plumbers-rails/v0.4.10) - 2026-07-19
7
14
 
data/docs/architecture.md CHANGED
@@ -26,6 +26,6 @@ Validation ranges for theme schema params live under `lib/stimulus_plumbers/them
26
26
 
27
27
  `Button#build_button` and `Link#build_content` always wrap non-nil text/block content in `<span>`. When content is nil and no block is given (icon-only), no `<span>` is rendered.
28
28
 
29
- The active theme uses `:has(> span)` / `:not(:has(> span))` CSS to distinguish icon-only from text buttons without any Ruby flag.
29
+ The active theme uses `:has(> span)` / `:not(:has(> span))` CSS to distinguish icon-only from text buttons without any Ruby flag. Submit buttons with visually hidden text additionally use `:has(> span[data-sp-label-hidden])` so they retain the icon-only square layout while keeping an accessible name.
30
30
 
31
31
  **Do not change this contract** without updating `stimulus-plumbers-tailwind`'s `Button::LAYOUT` and `Link::BUTTON` constants accordingly. See [tailwind architecture doc](../../stimulus-plumbers-tailwind/docs/architecture.md#icon-only-square-pattern).
@@ -55,6 +55,25 @@ Special options on native helpers:
55
55
  | `password_field` | `revealable: true` | Wraps input in an `input-formatter` reveal controller |
56
56
  | `search_field` | `clearable: true` | Wraps input in an `input-clearable` controller with a clear button |
57
57
 
58
+ ### f.submit
59
+
60
+ `f.submit` renders a themed `<button type="submit">` without a field wrapper.
61
+
62
+ ```erb
63
+ <%= f.submit "Save" %>
64
+ <%= f.submit "Save", icon_leading: "save" %>
65
+ <%= f.submit "Continue", icon_trailing: "arrow-right" %>
66
+ <%= f.submit "Save", icon_leading: "save", hide_label: true %>
67
+ ```
68
+
69
+ | Option | Description |
70
+ | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
71
+ | `type:` / `variant:` | Button theme type and variant. |
72
+ | `icon_leading:` / `icon_trailing:` | Decorative icon name placed before or after the submit text. |
73
+ | `hide_label: true` | Visually hides the button's own text span while keeping it available to screen readers. This differs from `Form::Field#hide_label`, which hides an associated `<label>` element. |
74
+
75
+ Submit buttons need an accessible name: pass non-blank text, or an `aria` label such as `aria: { label: "Save changes" }`.
76
+
58
77
  ---
59
78
 
60
79
  ## Level 2 — Full-field helpers
@@ -151,21 +170,23 @@ Use `f.password_field` for a plain `<input type="password">` (also accepts `reve
151
170
 
152
171
  **Code** (`as: :code`) — character-cell entry backed by `input-formatter` and the `character-cells` plumber. The native input remains the accessible control; cells are decorative.
153
172
 
154
- | Option | Values | Default | Description |
155
- | -------------- | ---------------------------------------- | ---------------------- | ------------------------------------------- |
156
- | `length` | positive Integer | required | Number of cells and input maximum length |
157
- | `charset` | `:digits` / `:letters` / `:alphanumeric` | `:digits` | Allowed code characters |
158
- | `groups` | Array of positive Integers | `[]` | Visual cell groups; must add up to `length` |
159
- | `autocomplete` | String | `"one-time-code"` | Native autocomplete value |
160
- | `inputmode` | String | `"numeric"` for digits | Native input mode |
161
-
162
- **Credit card** (`as: :credit_card`) — grouped card-number entry backed by `input-formatter` and the `character-cells` plumber (grouped mode). Renders one cell per group with a literal dash separator between cells; their sum sets `maxlength`.
163
-
164
- | Option | Values | Default | Description |
165
- | -------------- | -------------------------- | -------------- | ---------------------------------------------------------------------------- |
166
- | `groups` | Array of positive Integers | `[4, 4, 4, 4]` | Cell groups — one cell per group, dash-separated; their sum sets `maxlength` |
167
- | `autocomplete` | String | `"cc-number"` | Native autocomplete value |
168
- | `inputmode` | String | `"numeric"` | Native input mode |
173
+ | Option | Values | Default | Description |
174
+ | -------------- | ---------------------------------------- | ---------------------- | -------------------------------------------------------- |
175
+ | `length` | positive Integer | required | Number of cells and input maximum length |
176
+ | `charset` | `:digits` / `:letters` / `:alphanumeric` | `:digits` | Allowed code characters |
177
+ | `groups` | Array of positive Integers | `[]` | Visual cell groups; must add up to `length` |
178
+ | `separator` | String / `nil` | `nil` | Character between groups; `nil` shows the break as a gap |
179
+ | `autocomplete` | String | `"one-time-code"` | Native autocomplete value |
180
+ | `inputmode` | String | `"numeric"` for digits | Native input mode |
181
+
182
+ **Credit card** (`as: :credit_card`) — grouped card-number entry backed by `input-formatter` and the `character-cells` plumber (grouped mode). Renders one cell per group; their sum sets `maxlength`.
183
+
184
+ | Option | Values | Default | Description |
185
+ | -------------- | -------------------------- | -------------- | ------------------------------------------------------------ |
186
+ | `groups` | Array of positive Integers | `[4, 4, 4, 4]` | Cell groups — one cell per group; their sum sets `maxlength` |
187
+ | `separator` | String / `nil` | `nil` | Character between cells; `nil` renders none |
188
+ | `autocomplete` | String | `"cc-number"` | Native autocomplete value |
189
+ | `inputmode` | String | `"numeric"` | Native input mode |
169
190
 
170
191
  Character-cell fields do not support `floating:` labels.
171
192
 
data/docs/guide.md CHANGED
@@ -12,7 +12,8 @@ Use `StimulusPlumbers::Form::Builder` (set `config.action_view.default_form_buil
12
12
  - **Level 1.** Native helper overrides (`f.text_field`, `f.select`, `f.check_box`, ...) render
13
13
  only the themed input element — use when you control the surrounding markup.
14
14
 
15
- Submit with `f.submit` (themed button).
15
+ Submit with `f.submit` (themed button; supports `icon_leading:`/`icon_trailing:` and
16
+ `hide_label:` for an icon-only button — see [docs/component/form.md](component/form.md)).
16
17
 
17
18
  ## Building views
18
19
 
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Button < Plumber::Base
6
+ module IconLayout
7
+ private
8
+
9
+ def build_layout(slots, &block)
10
+ @template.safe_join(
11
+ [
12
+ render_icon_slot(slots, :icon_leading),
13
+ @template.capture(&block),
14
+ render_icon_slot(slots, :icon_trailing)
15
+ ]
16
+ )
17
+ end
18
+
19
+ def render_icon_slot(slots, name)
20
+ slots.resolve(name) do |value|
21
+ next value unless Components::Icon.icon_name?(value)
22
+
23
+ Components::Icon.new(@template).render(
24
+ name: value,
25
+ size: :sm,
26
+ classes: theme.resolve(:button_icon).fetch(:classes, ""),
27
+ aria: { hidden: "true" }
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -3,6 +3,8 @@
3
3
  module StimulusPlumbers
4
4
  module Components
5
5
  class Button < Plumber::Base
6
+ include IconLayout
7
+
6
8
  def render(content = nil, icon_leading: nil, icon_trailing: nil, **kwargs, &block)
7
9
  slots = Button::Slots.new(template)
8
10
  slots.with_icon_leading(icon_leading) if icon_leading
@@ -34,29 +36,6 @@ module StimulusPlumbers
34
36
  template.content_tag(:span, content)
35
37
  end
36
38
  end
37
-
38
- def build_layout(slots, &block)
39
- template.safe_join(
40
- [
41
- render_icon_slot(slots, :icon_leading),
42
- template.capture(&block),
43
- render_icon_slot(slots, :icon_trailing)
44
- ]
45
- )
46
- end
47
-
48
- def render_icon_slot(slots, name)
49
- slots.resolve(name) do |value|
50
- next value unless Components::Icon.icon_name?(value)
51
-
52
- Components::Icon.new(template).render(
53
- name: value,
54
- size: :sm,
55
- classes: theme.resolve(:button_icon).fetch(:classes, ""),
56
- aria: { hidden: "true" }
57
- )
58
- end
59
- end
60
39
  end
61
40
  end
62
41
  end
@@ -6,15 +6,37 @@ module StimulusPlumbers
6
6
  # Yielded to `Combobox#render`: selects a variant renderer, then exposes its
7
7
  # `metadata` (trigger/wrapper wiring) and renders its panel body.
8
8
  class Builder < Plumber::Slots
9
- def dropdown(**options) = select(Dropdown, options)
10
- def typeahead(**options) = select(Typeahead, options)
11
- def date(**options) = select(Date, options)
12
- def time(**options) = select(Time, options)
9
+ def dropdown(**options)
10
+ select(Dropdown, options)
11
+ end
12
+
13
+ def typeahead(**options)
14
+ select(Typeahead, options)
15
+ end
16
+
17
+ def date(**options)
18
+ select(Date, options)
19
+ end
13
20
 
14
- def selected? = @slots.key?(:variant)
15
- def renderer = selection&.fetch(:renderer)
16
- def options = selection ? selection[:options] : {}
17
- def metadata = renderer ? renderer::Metadata : DefaultMetadata
21
+ def time(**options)
22
+ select(Time, options)
23
+ end
24
+
25
+ def selected?
26
+ @slots.key?(:variant)
27
+ end
28
+
29
+ def renderer
30
+ selection&.fetch(:renderer)
31
+ end
32
+
33
+ def options
34
+ selection ? selection[:options] : {}
35
+ end
36
+
37
+ def metadata
38
+ renderer ? renderer::Metadata : DefaultMetadata
39
+ end
18
40
 
19
41
  def render_panel(template, panel_attrs:)
20
42
  renderer&.new(template)&.render(panel_attrs: panel_attrs, **options)
@@ -24,11 +46,25 @@ module StimulusPlumbers
24
46
  module DefaultMetadata
25
47
  module_function
26
48
 
27
- def haspopup = "dialog"
28
- def popup_id_for(panel_id) = panel_id
29
- def trigger_icon = nil
30
- def trigger_options = {}
31
- def stimulus_data(_panel_id, _options) = {}
49
+ def haspopup
50
+ "dialog"
51
+ end
52
+
53
+ def popup_id_for(panel_id)
54
+ panel_id
55
+ end
56
+
57
+ def trigger_icon
58
+ nil
59
+ end
60
+
61
+ def trigger_options
62
+ {}
63
+ end
64
+
65
+ def stimulus_data(_panel_id, _options)
66
+ {}
67
+ end
32
68
  end
33
69
 
34
70
  private
@@ -38,7 +74,9 @@ module StimulusPlumbers
38
74
  nil
39
75
  end
40
76
 
41
- def selection = resolve(:variant)
77
+ def selection
78
+ resolve(:variant)
79
+ end
42
80
  end
43
81
  end
44
82
  end
@@ -16,21 +16,47 @@ module StimulusPlumbers
16
16
  "#{STIMULUS_CONTROLLER}:selected->#{Components::Popover::STIMULUS_CONTROLLER}#closeOnSelect"
17
17
  ].join(" ").freeze
18
18
 
19
- def self.month_id_for(panel_id) = [panel_id, "calendar_month"].compact.join("_")
20
- def self.year_id_for(panel_id) = [panel_id, "calendar_year"].compact.join("_")
21
- def self.decade_id_for(panel_id) = [panel_id, "calendar_decade"].compact.join("_")
19
+ class << self
20
+ def month_id_for(panel_id)
21
+ [panel_id, "calendar_month"].compact.join("_")
22
+ end
23
+
24
+ def year_id_for(panel_id)
25
+ [panel_id, "calendar_year"].compact.join("_")
26
+ end
27
+
28
+ def decade_id_for(panel_id)
29
+ [panel_id, "calendar_decade"].compact.join("_")
30
+ end
31
+ end
22
32
 
23
33
  module Metadata
24
34
  module_function
25
35
 
26
- def haspopup = "dialog"
27
- def popup_id_for(panel_id) = panel_id
28
- def trigger_icon = "calendar"
29
- def trigger_options = {}
30
- def stimulus_data(_panel_id, _options) = { input_formatter_format_value: "date" }
36
+ def haspopup
37
+ "dialog"
38
+ end
39
+
40
+ def popup_id_for(panel_id)
41
+ panel_id
42
+ end
43
+
44
+ def trigger_icon
45
+ "calendar"
46
+ end
47
+
48
+ def trigger_options
49
+ {}
50
+ end
51
+
52
+ def stimulus_data(_panel_id, _options)
53
+ { input_formatter_format_value: "date" }
54
+ end
31
55
  end
32
56
 
33
- def render(...) = render_date(...)
57
+ def render(...)
58
+ render_date(...)
59
+ end
34
60
 
35
61
  private
36
62
 
@@ -15,14 +15,30 @@ module StimulusPlumbers
15
15
  module Metadata
16
16
  module_function
17
17
 
18
- def haspopup = "listbox"
19
- def popup_id_for(panel_id) = panel_id
20
- def trigger_icon = "chevron-down"
21
- def trigger_options = {}
22
- def stimulus_data(_panel_id, _options) = {}
18
+ def haspopup
19
+ "listbox"
20
+ end
21
+
22
+ def popup_id_for(panel_id)
23
+ panel_id
24
+ end
25
+
26
+ def trigger_icon
27
+ "chevron-down"
28
+ end
29
+
30
+ def trigger_options
31
+ {}
32
+ end
33
+
34
+ def stimulus_data(_panel_id, _options)
35
+ {}
36
+ end
23
37
  end
24
38
 
25
- def render(...) = render_dropdown(...)
39
+ def render(...)
40
+ render_dropdown(...)
41
+ end
26
42
 
27
43
  private
28
44
 
@@ -13,10 +13,21 @@ module StimulusPlumbers
13
13
  module Metadata
14
14
  module_function
15
15
 
16
- def haspopup = "dialog"
17
- def popup_id_for(panel_id) = panel_id
18
- def trigger_icon = "clock"
19
- def trigger_options = {}
16
+ def haspopup
17
+ "dialog"
18
+ end
19
+
20
+ def popup_id_for(panel_id)
21
+ panel_id
22
+ end
23
+
24
+ def trigger_icon
25
+ "clock"
26
+ end
27
+
28
+ def trigger_options
29
+ {}
30
+ end
20
31
 
21
32
  def stimulus_data(_panel_id, options)
22
33
  {
@@ -26,7 +37,9 @@ module StimulusPlumbers
26
37
  end
27
38
  end
28
39
 
29
- def render(...) = render_time(...)
40
+ def render(...)
41
+ render_time(...)
42
+ end
30
43
 
31
44
  private
32
45
 
@@ -4,17 +4,30 @@ module StimulusPlumbers
4
4
  module Components
5
5
  class Combobox
6
6
  class Typeahead < Plumber::Base
7
- def self.listbox_id_for(panel_id)
8
- [panel_id, "listbox"].compact.join("_")
7
+ class << self
8
+ def listbox_id_for(panel_id)
9
+ [panel_id, "listbox"].compact.join("_")
10
+ end
9
11
  end
10
12
 
11
13
  module Metadata
12
14
  module_function
13
15
 
14
- def haspopup = "listbox"
15
- def popup_id_for(panel_id) = Typeahead.listbox_id_for(panel_id)
16
- def trigger_icon = nil
17
- def trigger_options = { readonly: false, aria: { autocomplete: "list" } }
16
+ def haspopup
17
+ "listbox"
18
+ end
19
+
20
+ def popup_id_for(panel_id)
21
+ Typeahead.listbox_id_for(panel_id)
22
+ end
23
+
24
+ def trigger_icon
25
+ nil
26
+ end
27
+
28
+ def trigger_options
29
+ { readonly: false, aria: { autocomplete: "list" } }
30
+ end
18
31
 
19
32
  def stimulus_data(panel_id, _options)
20
33
  {
@@ -24,7 +37,9 @@ module StimulusPlumbers
24
37
  end
25
38
  end
26
39
 
27
- def render(...) = render_typeahead(...)
40
+ def render(...)
41
+ render_typeahead(...)
42
+ end
28
43
 
29
44
  private
30
45
 
@@ -3,8 +3,10 @@
3
3
  module StimulusPlumbers
4
4
  module Components
5
5
  class Icon < Plumber::Base
6
- def self.icon_name?(value)
7
- value.is_a?(Symbol) || (value.is_a?(String) && !value.html_safe?)
6
+ class << self
7
+ def icon_name?(value)
8
+ value.is_a?(Symbol) || (value.is_a?(String) && !value.html_safe?)
9
+ end
8
10
  end
9
11
 
10
12
  def render(name:, size: :lg, **kwargs)
@@ -3,7 +3,9 @@
3
3
  module StimulusPlumbers
4
4
  module Components
5
5
  class Indicator < Plumber::Base
6
- def render(...) = render_indicator(...)
6
+ def render(...)
7
+ render_indicator(...)
8
+ end
7
9
 
8
10
  private
9
11
 
@@ -5,11 +5,15 @@ module StimulusPlumbers
5
5
  class Popover < Plumber::Base
6
6
  STIMULUS_CONTROLLER = "popover"
7
7
 
8
- def self.panel_id_for(trigger_id)
9
- [trigger_id, "popover"].compact.join("_")
8
+ class << self
9
+ def panel_id_for(trigger_id)
10
+ [trigger_id, "popover"].compact.join("_")
11
+ end
10
12
  end
11
13
 
12
- def render(...) = render_popover(...)
14
+ def render(...)
15
+ render_popover(...)
16
+ end
13
17
 
14
18
  def build(panel_id: nil, &block)
15
19
  @panel_id = panel_id || self.class.panel_id_for(template.sp_dom_id)
@@ -3,7 +3,9 @@
3
3
  module StimulusPlumbers
4
4
  module Components
5
5
  class ProgressBar < Plumber::Base
6
- def render(...) = render_bar(...)
6
+ def render(...)
7
+ render_bar(...)
8
+ end
7
9
 
8
10
  private
9
11
 
@@ -3,7 +3,9 @@
3
3
  module StimulusPlumbers
4
4
  module Components
5
5
  class ProgressMeter < Plumber::Base
6
- def render(...) = render_meter(...)
6
+ def render(...)
7
+ render_meter(...)
8
+ end
7
9
 
8
10
  private
9
11
 
@@ -3,7 +3,9 @@
3
3
  module StimulusPlumbers
4
4
  module Components
5
5
  class ProgressRing < Plumber::Base
6
- def render(...) = render_ring(...)
6
+ def render(...)
7
+ render_ring(...)
8
+ end
7
9
 
8
10
  private
9
11
 
@@ -6,8 +6,10 @@ module StimulusPlumbers
6
6
  class Event < Plumber::Base
7
7
  attr_reader :interactive
8
8
 
9
- def self.detail_id_for(event_id)
10
- [event_id, "detail"].compact.join("_")
9
+ class << self
10
+ def detail_id_for(event_id)
11
+ [event_id, "detail"].compact.join("_")
12
+ end
11
13
  end
12
14
 
13
15
  def render(datetime: nil, id: nil, interactive: false, orientation: :vertical, **kwargs, &block)
@@ -15,8 +15,10 @@ module StimulusPlumbers
15
15
 
16
16
  attr_reader :hide_label
17
17
 
18
- def self.label_id(input_id)
19
- [input_id, "label"].compact.join("_")
18
+ class << self
19
+ def label_id(input_id)
20
+ [input_id, "label"].compact.join("_")
21
+ end
20
22
  end
21
23
 
22
24
  def initialize(template, hide_label: false, **kwargs)
@@ -5,64 +5,124 @@ module StimulusPlumbers
5
5
  module Fields
6
6
  module Inputs
7
7
  module Code
8
+ STIMULUS_CONTROLLER = "input-formatter"
9
+ STIMULUS_ACTION = [
10
+ "input->#{STIMULUS_CONTROLLER}#onInput",
11
+ "focus->#{STIMULUS_CONTROLLER}#onFocus",
12
+ "blur->#{STIMULUS_CONTROLLER}#onBlur"
13
+ ].join(" ").freeze
14
+ DEFAULT_CHARSET = :digits
15
+ DEFAULT_AUTOCOMPLETE = "one-time-code"
16
+ DEFAULT_SEPARATOR = nil
17
+
8
18
  private
9
19
 
10
- def render_code_input(attribute, html_opts, opts, error, length:, charset: :digits, groups: [], **kwargs)
11
- floating = kwargs.delete(:floating)
20
+ def render_code_input(
21
+ attribute,
22
+ html_opts,
23
+ opts,
24
+ error,
25
+ length: nil,
26
+ charset: DEFAULT_CHARSET,
27
+ groups: [],
28
+ separator: DEFAULT_SEPARATOR,
29
+ floating: nil,
30
+ **kwargs
31
+ )
12
32
  raise ArgumentError, "floating labels are not supported for code fields" if floating.present?
13
33
 
14
34
  validate_code_options!(length, groups)
15
- inputmode = kwargs.delete(:inputmode) || (charset.to_sym == :digits ? "numeric" : nil)
16
- autocomplete = kwargs.delete(:autocomplete) || "one-time-code"
17
- config = { error: error, length: length, charset: charset, groups: groups,
18
- autocomplete: autocomplete, inputmode: inputmode, kwargs: kwargs
19
- }
35
+ config = {
36
+ error: error,
37
+ length: length,
38
+ charset: charset,
39
+ groups: groups,
40
+ separator: separator,
41
+ autocomplete: kwargs.delete(:autocomplete) || DEFAULT_AUTOCOMPLETE,
42
+ inputmode: kwargs.delete(:inputmode) || (charset.to_sym == DEFAULT_CHARSET ? "numeric" : nil),
43
+ kwargs: kwargs
44
+ }
20
45
  render_code_field(attribute, html_opts, opts, config)
21
46
  end
22
47
 
23
48
  def render_code_field(attribute, html_opts, opts, config)
24
- @template.content_tag(:div, **code_wrapper_options(config)) do
49
+ overlay_options = merge_html_options(
50
+ theme.resolve(:form_field_input_code_overlay, error: config[:error]),
51
+ opts,
52
+ html_opts
53
+ )
54
+ wrapper_options = merge_html_options(
55
+ theme.resolve(:form_field_input_code, error: config[:error]),
56
+ code_options(config)
57
+ )
58
+ @template.content_tag(:div, **wrapper_options) do
25
59
  @template.safe_join(
26
- [render_code_cells(config), code_input(attribute, html_opts, opts, config)]
60
+ [render_code_cells(config), code_input(attribute, overlay_options, config)]
27
61
  )
28
62
  end
29
63
  end
30
64
 
31
- def code_wrapper_options(config)
32
- merge_html_options(
33
- theme.resolve(:form_field_input_code, error: config[:error]),
34
- { data: {
35
- controller: "input-formatter",
65
+ def code_options(config)
66
+ {
67
+ data: {
68
+ controller: STIMULUS_CONTROLLER,
36
69
  input_formatter_format_value: "code",
37
- input_formatter_options_value: { charset: config[:charset].to_s, length: config[:length] }.to_json,
38
- input_formatter_groups_value: config[:groups].presence
70
+ input_formatter_options_value: { charset: config[:charset].to_s, length: config[:length] },
71
+ input_formatter_groups_value: code_groups_value(config)
39
72
  }
40
- }
41
- )
73
+ }
74
+ end
75
+
76
+ # Server-rendered separators already mark the group breaks. Passing groups as well
77
+ # would stamp data-group-end and lay the themed gap on top of the separator.
78
+ def code_groups_value(config)
79
+ return if config[:separator].present?
80
+
81
+ config[:groups].presence
42
82
  end
43
83
 
44
- def code_input(attribute, html_opts, opts, config)
84
+ def code_input(attribute, html_opts, config)
45
85
  input_options = merge_html_options(
46
- theme.resolve(:form_field_input_code_overlay, error: config[:error]),
47
- opts,
48
86
  html_opts,
49
87
  config[:kwargs],
50
- { maxlength: config[:length], autocomplete: config[:autocomplete], inputmode: config[:inputmode],
51
- data: { input_formatter_target: "input", action: formatter_actions }
52
- }
88
+ {
89
+ maxlength: config[:length],
90
+ autocomplete: config[:autocomplete],
91
+ inputmode: config[:inputmode],
92
+ data: { input_formatter_target: "input", action: STIMULUS_ACTION }
93
+ }
53
94
  )
54
95
  @template.text_field(@object_name, attribute, objectify_options(input_options))
55
96
  end
56
97
 
57
98
  def render_code_cells(config)
58
- count = config[:groups].present? ? config[:groups].sum : config[:length]
99
+ @template.content_tag(:div, **merge_html_options(theme.resolve(:form_field_input_code_cells))) do
100
+ @template.safe_join(code_cells(config))
101
+ end
102
+ end
103
+
104
+ def code_cells(config)
105
+ groups = config[:groups].presence || [config[:length]]
106
+ groups.flat_map.with_index do |width, index|
107
+ cells = Array.new(width) { render_code_cell(config) }
108
+ index.zero? || config[:separator].blank? ? cells : [render_code_separator(config), *cells]
109
+ end
110
+ end
111
+
112
+ def render_code_cell(config)
59
113
  cell_options = merge_html_options(
60
114
  theme.resolve(:form_field_input_code_cell, error: config[:error]),
61
115
  { data: { input_formatter_target: "cell" } }
62
116
  )
63
- @template.content_tag(:div, **merge_html_options(theme.resolve(:form_field_input_code_cells))) do
64
- @template.safe_join(Array.new(count) { @template.content_tag(:span, nil, **cell_options) })
65
- end
117
+ @template.content_tag(:span, nil, **cell_options)
118
+ end
119
+
120
+ def render_code_separator(config)
121
+ separator_options = merge_html_options(
122
+ theme.resolve(:form_field_input_code_separator),
123
+ { aria: { hidden: true } }
124
+ )
125
+ @template.content_tag(:span, config[:separator], **separator_options)
66
126
  end
67
127
 
68
128
  def validate_code_options!(length, groups)
@@ -73,11 +133,12 @@ module StimulusPlumbers
73
133
  raise ArgumentError, "groups must add up to length" unless groups.sum == length
74
134
  end
75
135
 
76
- def valid_groups?(groups) = groups.is_a?(Array) && groups.all? { |group| positive_integer?(group) }
77
- def positive_integer?(value) = value.is_a?(Integer) && value.positive?
136
+ def valid_groups?(groups)
137
+ groups.is_a?(Array) && groups.all? { |group| positive_integer?(group) }
138
+ end
78
139
 
79
- def formatter_actions
80
- "input->input-formatter#onInput focus->input-formatter#onFocus blur->input-formatter#onBlur"
140
+ def positive_integer?(value)
141
+ value.is_a?(Integer) && value.positive?
81
142
  end
82
143
  end
83
144
  end
@@ -5,64 +5,96 @@ module StimulusPlumbers
5
5
  module Fields
6
6
  module Inputs
7
7
  module CreditCard
8
- DEFAULT_GROUPS = [4, 4, 4, 4].freeze
8
+ STIMULUS_CONTROLLER = "input-formatter"
9
+ STIMULUS_ACTION = [
10
+ "input->#{STIMULUS_CONTROLLER}#onInput",
11
+ "focus->#{STIMULUS_CONTROLLER}#onFocus",
12
+ "blur->#{STIMULUS_CONTROLLER}#onBlur"
13
+ ].join(" ").freeze
14
+ DEFAULT_GROUPS = [4, 4, 4, 4].freeze
15
+ DEFAULT_AUTOCOMPLETE = "cc-number"
16
+ DEFAULT_INPUTMODE = "numeric"
17
+ DEFAULT_SEPARATOR = nil
9
18
 
10
19
  private
11
20
 
12
- def render_credit_card_input(attribute, html_opts, opts, error, groups: DEFAULT_GROUPS, floating: nil, **kwargs)
21
+ def render_credit_card_input(
22
+ attribute,
23
+ html_opts,
24
+ opts,
25
+ error,
26
+ groups: DEFAULT_GROUPS,
27
+ separator: DEFAULT_SEPARATOR,
28
+ floating: nil,
29
+ **kwargs
30
+ )
13
31
  raise ArgumentError, "floating labels are not supported for credit card fields" if floating.present?
14
32
 
15
33
  normalized_groups = groups.presence || DEFAULT_GROUPS
16
- inputmode = kwargs.delete(:inputmode) || "numeric"
17
- autocomplete = kwargs.delete(:autocomplete) || "cc-number"
18
34
  validate_credit_card_groups!(normalized_groups)
19
- length = normalized_groups.sum
20
- config = { error: error, groups: normalized_groups, length: length,
21
- autocomplete: autocomplete, inputmode: inputmode, kwargs: kwargs
22
- }
35
+ config = {
36
+ error: error,
37
+ groups: normalized_groups,
38
+ length: normalized_groups.sum,
39
+ separator: separator,
40
+ autocomplete: kwargs.delete(:autocomplete) || DEFAULT_AUTOCOMPLETE,
41
+ inputmode: kwargs.delete(:inputmode) || DEFAULT_INPUTMODE,
42
+ kwargs: kwargs
43
+ }
23
44
  render_credit_card_field(attribute, html_opts, opts, config)
24
45
  end
25
46
 
26
47
  def render_credit_card_field(attribute, html_opts, opts, config)
27
- @template.content_tag(:div, **credit_card_wrapper_options(config)) do
48
+ overlay_options = merge_html_options(
49
+ theme.resolve(:form_field_input_credit_card_overlay, error: config[:error]),
50
+ opts,
51
+ html_opts
52
+ )
53
+ wrapper_options = merge_html_options(
54
+ theme.resolve(:form_field_input_credit_card, error: config[:error]),
55
+ credit_card_options(config)
56
+ )
57
+ @template.content_tag(:div, **wrapper_options) do
28
58
  @template.safe_join(
29
- [render_credit_card_cells(config), credit_card_input(attribute, html_opts, opts, config)]
59
+ [render_credit_card_cells(config), credit_card_input(attribute, overlay_options, config)]
30
60
  )
31
61
  end
32
62
  end
33
63
 
34
- def credit_card_wrapper_options(config)
35
- merge_html_options(
36
- theme.resolve(:form_field_input_credit_card, error: config[:error]),
37
- { data: {
38
- controller: "input-formatter",
64
+ def credit_card_options(config)
65
+ {
66
+ data: {
67
+ controller: STIMULUS_CONTROLLER,
39
68
  input_formatter_format_value: "creditCard",
40
69
  input_formatter_groups_value: config[:groups]
41
70
  }
42
- }
43
- )
71
+ }
44
72
  end
45
73
 
46
- def credit_card_input(attribute, html_opts, opts, config)
74
+ def credit_card_input(attribute, html_opts, config)
47
75
  input_options = merge_html_options(
48
- theme.resolve(:form_field_input_credit_card_overlay, error: config[:error]),
49
- opts,
50
76
  html_opts,
51
77
  config[:kwargs],
52
- { maxlength: config[:length], autocomplete: config[:autocomplete], inputmode: config[:inputmode],
53
- data: { input_formatter_target: "input", action: formatter_actions }
54
- }
78
+ {
79
+ maxlength: config[:length],
80
+ autocomplete: config[:autocomplete],
81
+ inputmode: config[:inputmode],
82
+ data: { input_formatter_target: "input", action: STIMULUS_ACTION }
83
+ }
55
84
  )
56
85
  @template.text_field(@object_name, attribute, objectify_options(input_options))
57
86
  end
58
87
 
59
88
  def render_credit_card_cells(config)
60
- cells = config[:groups].each_index.flat_map do |index|
61
- index.zero? ? [render_credit_card_cell(config)] : [render_credit_card_separator, render_credit_card_cell(config)]
89
+ @template.content_tag(:div, **merge_html_options(theme.resolve(:form_field_input_credit_card_cells))) do
90
+ @template.safe_join(credit_card_cells(config))
62
91
  end
92
+ end
63
93
 
64
- @template.content_tag(:div, **merge_html_options(theme.resolve(:form_field_input_credit_card_cells))) do
65
- @template.safe_join(cells)
94
+ def credit_card_cells(config)
95
+ config[:groups].each_index.flat_map do |index|
96
+ cell = render_credit_card_cell(config)
97
+ index.zero? || config[:separator].blank? ? [cell] : [render_credit_card_separator(config), cell]
66
98
  end
67
99
  end
68
100
 
@@ -74,12 +106,12 @@ module StimulusPlumbers
74
106
  @template.content_tag(:span, nil, **cell_options)
75
107
  end
76
108
 
77
- def render_credit_card_separator
109
+ def render_credit_card_separator(config)
78
110
  separator_options = merge_html_options(
79
111
  theme.resolve(:form_field_input_credit_card_separator),
80
112
  { aria: { hidden: true } }
81
113
  )
82
- @template.content_tag(:span, "-", **separator_options)
114
+ @template.content_tag(:span, config[:separator], **separator_options)
83
115
  end
84
116
 
85
117
  def validate_credit_card_groups!(groups)
@@ -87,10 +119,6 @@ module StimulusPlumbers
87
119
 
88
120
  raise ArgumentError, "groups must be an array of positive integers"
89
121
  end
90
-
91
- def formatter_actions
92
- "input->input-formatter#onInput focus->input-formatter#onFocus blur->input-formatter#onBlur"
93
- end
94
122
  end
95
123
  end
96
124
  end
@@ -5,6 +5,9 @@ module StimulusPlumbers
5
5
  module Fields
6
6
  module Inputs
7
7
  module Password
8
+ STIMULUS_CONTROLLER = "input-formatter"
9
+ STIMULUS_ACTION = "click->#{STIMULUS_CONTROLLER}#toggle".freeze
10
+
8
11
  def password_field(attribute, floating: nil, revealable: false, **options)
9
12
  html_options = merge_html_options(theme.resolve(:form_field_input, floating: floating), options)
10
13
  if revealable
@@ -42,7 +45,7 @@ module StimulusPlumbers
42
45
  trailing: method(:reveal_button),
43
46
  **merge_html_options(
44
47
  theme.resolve(:form_field_input_reveal, error: error),
45
- { data: { controller: "input-formatter", input_formatter_format_value: "password" } }
48
+ { data: { controller: STIMULUS_CONTROLLER, input_formatter_format_value: "password" } }
46
49
  )
47
50
  ) { @template.capture(&block) }
48
51
  end
@@ -66,7 +69,7 @@ module StimulusPlumbers
66
69
  {
67
70
  type: "button",
68
71
  aria: { label: I18n.t("stimulus_plumbers.form.password.show", default: "Show password"), pressed: "false" },
69
- data: { input_formatter_target: "toggle", action: "click->input-formatter#toggle" }
72
+ data: { input_formatter_target: "toggle", action: STIMULUS_ACTION }
70
73
  }
71
74
  )
72
75
  ) { @template.capture(&block) }
@@ -5,22 +5,67 @@ module StimulusPlumbers
5
5
  module Fields
6
6
  module Inputs
7
7
  module Submit
8
+ include Components::Button::IconLayout
9
+
8
10
  def submit(value = nil, options = {})
9
- if value.is_a?(Hash)
10
- options = value
11
- value = nil
12
- end
11
+ value, options = normalize_submit_arguments(value, options)
13
12
  value ||= submit_default_value
14
- type = options.delete(:type) { :default }
15
- variant = options.delete(:variant) { :primary }
13
+ submit_options = extract_submit_options(options)
14
+
15
+ validate_submit_accessible_name!(value, options)
16
+
17
+ render_submit_button(value, options, **submit_options)
18
+ end
19
+
20
+ private
21
+
22
+ def normalize_submit_arguments(value, options)
23
+ value.is_a?(Hash) ? [nil, value] : [value, options]
24
+ end
25
+
26
+ def extract_submit_options(options)
27
+ {
28
+ type: options.delete(:type) { :default },
29
+ variant: options.delete(:variant) { :primary },
30
+ icon_leading: options.delete(:icon_leading),
31
+ icon_trailing: options.delete(:icon_trailing),
32
+ hide_label: options.delete(:hide_label) { false }
33
+ }
34
+ end
16
35
 
36
+ def render_submit_button(value, options, type:, variant:, icon_leading:, icon_trailing:, hide_label:)
17
37
  Components::Button.new(@template).build(type: type, variant: variant) do |attrs|
18
38
  @template.tag.button(
19
39
  type: "submit",
20
40
  **merge_html_options(attrs, theme.resolve(:form_submit, type: type, variant: variant), options)
21
- ) { @template.content_tag(:span, value) }
41
+ ) { build_layout(submit_slots(icon_leading, icon_trailing)) { submit_label(value, hide_label) } }
22
42
  end
23
43
  end
44
+
45
+ def submit_slots(icon_leading, icon_trailing)
46
+ Components::Button::Slots.new(@template).tap do |slots|
47
+ slots.with_icon_leading(icon_leading) if icon_leading
48
+ slots.with_icon_trailing(icon_trailing) if icon_trailing
49
+ end
50
+ end
51
+
52
+ def submit_label(value, hide_label)
53
+ @template.content_tag(
54
+ :span,
55
+ value,
56
+ **merge_html_options(
57
+ theme.resolve(:form_submit_label, hidden: hide_label),
58
+ (hide_label ? { data: { sp_label_hidden: true } } : {})
59
+ )
60
+ )
61
+ end
62
+
63
+ def validate_submit_accessible_name!(value, options)
64
+ return if value.present? || options.dig(:aria, :label).present? ||
65
+ options.dig(:aria, :labelledby).present? || options[:"aria-label"].present?
66
+
67
+ raise ArgumentError, "submit button has no accessible name: pass a value or aria: { label: ... }"
68
+ end
24
69
  end
25
70
  end
26
71
  end
@@ -74,6 +74,7 @@ module StimulusPlumbers
74
74
 
75
75
  entry = "/#{relative_to_destination(path)}"
76
76
  return if File.readlines(gitignore, chomp: true).include?(entry)
77
+ return if git_ignores?(path)
77
78
 
78
79
  File.open(gitignore, "a") { |file| file.puts(entry) }
79
80
  say_status :append, ".gitignore", :green
@@ -81,6 +82,15 @@ module StimulusPlumbers
81
82
  say "Could not update .gitignore: #{e.message}. Skipping.", :yellow
82
83
  end
83
84
 
85
+ def git_ignores?(path)
86
+ Dir.chdir(destination_root) do
87
+ system("git", "check-ignore", "-q", relative_to_destination(path), out: File::NULL, err: File::NULL)
88
+ end
89
+ rescue StandardError => e
90
+ say "Could not check .gitignore coverage with git: #{e.message}. Proceeding to append.", :yellow
91
+ false
92
+ end
93
+
84
94
  # CSS copied by an installer belongs to the application from this point on.
85
95
  # Keep an existing file intact so a generator rerun never overwrites local
86
96
  # customizations (the same policy used by tailwindcss-rails' installer).
@@ -8,19 +8,21 @@ require_relative "dispatcher/klass_proxy"
8
8
  module StimulusPlumbers
9
9
  module Plumber
10
10
  module Dispatcher
11
- def self.build(callable, *args, method_name: nil, init_args: [], init_kwargs: {}, **kwargs, &block)
12
- case callable
13
- when Symbol
14
- MethodCall.new(callable, *args, **kwargs, &block)
15
- when Proc
16
- InstanceExec.new(callable, *args, **kwargs)
17
- when Module
18
- KlassProxy.new(callable, method_name, *args, init_args: init_args, init_kwargs: init_kwargs, **kwargs, &block)
19
- when String
20
- klass = callable.safe_constantize
21
- raise ArgumentError, "could not resolve class from: #{callable.inspect}" unless klass
11
+ class << self
12
+ def build(callable, *args, method_name: nil, init_args: [], init_kwargs: {}, **kwargs, &block)
13
+ case callable
14
+ when Symbol
15
+ MethodCall.new(callable, *args, **kwargs, &block)
16
+ when Proc
17
+ InstanceExec.new(callable, *args, **kwargs)
18
+ when Module
19
+ KlassProxy.new(callable, method_name, *args, init_args: init_args, init_kwargs: init_kwargs, **kwargs, &block)
20
+ when String
21
+ klass = callable.safe_constantize
22
+ raise ArgumentError, "could not resolve class from: #{callable.inspect}" unless klass
22
23
 
23
- KlassProxy.new(klass, method_name, *args, init_args: init_args, init_kwargs: init_kwargs, **kwargs, &block)
24
+ KlassProxy.new(klass, method_name, *args, init_args: init_args, init_kwargs: init_kwargs, **kwargs, &block)
25
+ end
24
26
  end
25
27
  end
26
28
  end
@@ -3,14 +3,6 @@
3
3
  module StimulusPlumbers
4
4
  module Plumber
5
5
  class Slots
6
- def self.slot(*names, by: nil)
7
- names.each do |name|
8
- by ? define_by_slot(name, by) : define_flat_slot(name)
9
- define_reader(name)
10
- define_predicate(name)
11
- end
12
- end
13
-
14
6
  def initialize(template = nil)
15
7
  @slots = {}
16
8
  @template = template
@@ -50,6 +42,14 @@ module StimulusPlumbers
50
42
  end
51
43
 
52
44
  class << self
45
+ def slot(*names, by: nil)
46
+ names.each do |name|
47
+ by ? define_by_slot(name, by) : define_flat_slot(name)
48
+ define_reader(name)
49
+ define_predicate(name)
50
+ end
51
+ end
52
+
53
53
  private
54
54
 
55
55
  def define_flat_slot(name)
@@ -186,6 +186,7 @@ module StimulusPlumbers
186
186
  form_field_input_code_cell: {
187
187
  error: { default: false, validate: Ranges::BOOL }
188
188
  }.freeze,
189
+ form_field_input_code_separator: {}.freeze,
189
190
  form_field_input_code_overlay: {
190
191
  error: { default: false, validate: Ranges::BOOL }
191
192
  }.freeze,
@@ -205,6 +206,9 @@ module StimulusPlumbers
205
206
  form_submit: {
206
207
  type: { default: :default, validate: Button::Ranges::TYPE },
207
208
  variant: { default: :primary, validate: Button::Ranges::VARIANT }
209
+ }.freeze,
210
+ form_submit_label: {
211
+ hidden: { default: false, validate: Ranges::BOOL }
208
212
  }.freeze
209
213
  }.freeze
210
214
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StimulusPlumbers
4
- VERSION = "0.4.11"
4
+ VERSION = "0.4.12"
5
5
  end
@@ -19,6 +19,7 @@ require_relative "stimulus_plumbers/plumber/base"
19
19
  require_relative "stimulus_plumbers/components/icon"
20
20
  require_relative "stimulus_plumbers/components/indicator"
21
21
  require_relative "stimulus_plumbers/components/avatar"
22
+ require_relative "stimulus_plumbers/components/button/icon_layout"
22
23
  require_relative "stimulus_plumbers/components/button"
23
24
  require_relative "stimulus_plumbers/components/button/slots"
24
25
  require_relative "stimulus_plumbers/components/button/group"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stimulus_plumbers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.11
4
+ version: 0.4.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Chang
@@ -74,6 +74,7 @@ files:
74
74
  - lib/stimulus_plumbers/components/avatar.rb
75
75
  - lib/stimulus_plumbers/components/button.rb
76
76
  - lib/stimulus_plumbers/components/button/group.rb
77
+ - lib/stimulus_plumbers/components/button/icon_layout.rb
77
78
  - lib/stimulus_plumbers/components/button/slots.rb
78
79
  - lib/stimulus_plumbers/components/calendar.rb
79
80
  - lib/stimulus_plumbers/components/calendar/turbo.rb