stimulus_plumbers 0.4.14 → 0.4.16

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/README.md +3 -1
  4. data/app/assets/javascripts/stimulus-plumbers/controllers.manifest.json +63 -1
  5. data/app/assets/javascripts/stimulus-plumbers/index.es.js +345 -198
  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/config/locales/en.yml +12 -0
  10. data/docs/component/combobox.md +3 -2
  11. data/docs/component/form.md +85 -18
  12. data/docs/component/indicator.md +1 -1
  13. data/docs/component/plumber.md +40 -1
  14. data/docs/component/progress.md +51 -16
  15. data/docs/guide.md +63 -4
  16. data/lib/stimulus_plumbers/components/card/slots.rb +0 -2
  17. data/lib/stimulus_plumbers/components/combobox/{builder.rb → config.rb} +7 -11
  18. data/lib/stimulus_plumbers/components/combobox.rb +12 -12
  19. data/lib/stimulus_plumbers/components/password_strength.rb +103 -0
  20. data/lib/stimulus_plumbers/components/progress/shared.rb +74 -0
  21. data/lib/stimulus_plumbers/components/progress_bar.rb +127 -25
  22. data/lib/stimulus_plumbers/components/progress_meter.rb +16 -14
  23. data/lib/stimulus_plumbers/components/progress_ring.rb +9 -23
  24. data/lib/stimulus_plumbers/components/timeline/event/slots.rb +0 -1
  25. data/lib/stimulus_plumbers/form/builder.rb +35 -8
  26. data/lib/stimulus_plumbers/form/field.rb +34 -6
  27. data/lib/stimulus_plumbers/form/fields/inputs/password/revealable.rb +72 -0
  28. data/lib/stimulus_plumbers/form/fields/inputs/password/strength.rb +31 -0
  29. data/lib/stimulus_plumbers/form/fields/inputs/password.rb +34 -65
  30. data/lib/stimulus_plumbers/form/fields/inputs/progress.rb +30 -0
  31. data/lib/stimulus_plumbers/form/fields/inputs/range.rb +81 -0
  32. data/lib/stimulus_plumbers/form/fields/inputs/text.rb +1 -1
  33. data/lib/stimulus_plumbers/form/fields/renderer.rb +41 -0
  34. data/lib/stimulus_plumbers/helpers/progress_helper.rb +14 -4
  35. data/lib/stimulus_plumbers/password/requirements.rb +182 -0
  36. data/lib/stimulus_plumbers/password_strength_validator.rb +29 -0
  37. data/lib/stimulus_plumbers/plumber/config.rb +37 -0
  38. data/lib/stimulus_plumbers/plumber/dispatcher/callable_inspector.rb +4 -0
  39. data/lib/stimulus_plumbers/plumber/slots.rb +2 -2
  40. data/lib/stimulus_plumbers/themes/schema/progress/ranges.rb +13 -0
  41. data/lib/stimulus_plumbers/themes/schema.rb +23 -4
  42. data/lib/stimulus_plumbers/version.rb +1 -1
  43. data/lib/stimulus_plumbers.rb +7 -1
  44. data/vendor/ARIA.md +10 -0
  45. data/vendor/component/manifest.json +16 -2
  46. data/vendor/controller/docs/password-strength.md +38 -0
  47. data/vendor/controller/docs/progress.md +100 -23
  48. data/vendor/controller/guide.md +61 -7
  49. data/vendor/controller/manifest.json +63 -1
  50. metadata +13 -2
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Plumber
5
+ # Sibling of Slots for block DSLs whose payload is configuration, not content.
6
+ # Slots captures blocks through the view; Config only stores values, and holds
7
+ # the template to pass on to renderers.
8
+ #
9
+ # The store is private. Slots publishes `resolve` because its payload is uniform
10
+ # content a renderer reads generically; a Config's settings are typed, so each
11
+ # subclass exposes its own named readers instead.
12
+ class Config
13
+ attr_reader :template
14
+
15
+ def initialize(template = nil)
16
+ @template = template
17
+ @config = {}
18
+ end
19
+
20
+ private
21
+
22
+ # Returns nil so a subclass's DSL method reads as a command, not a value.
23
+ def configure(name, value)
24
+ @config[name] = value
25
+ nil
26
+ end
27
+
28
+ def config(name)
29
+ @config[name]
30
+ end
31
+
32
+ def configured?(name)
33
+ @config.key?(name)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -13,6 +13,10 @@ module StimulusPlumbers
13
13
  def accepts_kwargs?(callable)
14
14
  callable.parameters.any? { |type, _| %i[key keyreq keyrest].include?(type) }
15
15
  end
16
+
17
+ def accepts_block?(callable)
18
+ callable.parameters.any? { |type, _| type == :block }
19
+ end
16
20
  end
17
21
  end
18
22
  end
@@ -37,8 +37,10 @@ module StimulusPlumbers
37
37
  @template ? @template.capture(&block) : block.call
38
38
  end
39
39
 
40
+ # Returns nil so a subclass's `with_*` method reads as a command, not a value.
40
41
  def set_slot(name, value, options = {})
41
42
  @slots[name] = { value: value, options: options }
43
+ nil
42
44
  end
43
45
 
44
46
  class << self
@@ -55,7 +57,6 @@ module StimulusPlumbers
55
57
  def define_flat_slot(name)
56
58
  define_method(:"with_#{name}") do |value = nil, **opts, &block|
57
59
  set_slot(name, block || value, opts)
58
- nil
59
60
  end
60
61
  end
61
62
 
@@ -64,7 +65,6 @@ module StimulusPlumbers
64
65
  sub = by.new(@template)
65
66
  block&.call(sub)
66
67
  set_slot(name, sub)
67
- nil
68
68
  end
69
69
  end
70
70
 
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Themes
5
+ module Schema
6
+ module Progress
7
+ module Ranges
8
+ PLACEMENT = %i[inside outside].freeze
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -11,6 +11,7 @@ require_relative "schema/form/ranges"
11
11
  require_relative "schema/form/checkbox/ranges"
12
12
  require_relative "schema/form/floating/ranges"
13
13
  require_relative "schema/form/radio/ranges"
14
+ require_relative "schema/progress/ranges"
14
15
  require_relative "schema/icon"
15
16
 
16
17
  module StimulusPlumbers
@@ -159,6 +160,12 @@ module StimulusPlumbers
159
160
  error: { default: false, validate: Ranges::BOOL },
160
161
  floating: { default: nil, validate: [nil, *Form::Floating::Ranges::TYPE] }
161
162
  }.freeze,
163
+ password_strength_wrapper: {}.freeze,
164
+ password_strength_rules_heading: {}.freeze,
165
+ password_strength_rules: {}.freeze,
166
+ password_strength_rule: {}.freeze,
167
+ password_strength_rule_icon: {}.freeze,
168
+ password_strength_level: {}.freeze,
162
169
  form_field_input_select: {
163
170
  error: { default: false, validate: Ranges::BOOL },
164
171
  floating: { default: nil, validate: [nil, *Form::Floating::Ranges::TYPE] }
@@ -173,6 +180,10 @@ module StimulusPlumbers
173
180
  type: { default: :default, validate: Form::Radio::Ranges::TYPE },
174
181
  variant: { default: :tertiary, validate: Form::Radio::Ranges::VARIANT }
175
182
  }.freeze,
183
+ form_field_input_progress: {}.freeze,
184
+ form_field_input_range: {}.freeze,
185
+ form_field_input_range_group: {}.freeze,
186
+ form_field_input_range_value: {}.freeze,
176
187
  form_field_input_combobox: {
177
188
  error: { default: false, validate: Ranges::BOOL },
178
189
  floating: { default: nil, validate: [nil, *Form::Floating::Ranges::TYPE] }
@@ -249,11 +260,19 @@ module StimulusPlumbers
249
260
  popover: {}.freeze
250
261
  }.freeze
251
262
 
263
+ # The outside readout gets its own keys, not parameters on existing ones — a theme method
264
+ # written before them has no keyword to accept.
252
265
  PROGRESS = {
253
- progress_bar: {}.freeze,
254
- progress_bar_fill: {}.freeze,
255
- progress_ring: {}.freeze,
256
- progress_meter: {}.freeze
266
+ progress_bar: { labelled: { default: false, validate: Ranges::BOOL } }.freeze,
267
+ progress_bar_group: {}.freeze,
268
+ progress_bar_fill: {}.freeze,
269
+ progress_bar_value: {}.freeze,
270
+ progress_bar_value_outside: {}.freeze,
271
+ progress_segment_group: {}.freeze,
272
+ progress_segment: {}.freeze,
273
+ progress_segment_fill: {}.freeze,
274
+ progress_ring: { size: { default: nil, validate: %i[sm md lg] } }.freeze,
275
+ progress_meter: {}.freeze
257
276
  }.freeze
258
277
 
259
278
  TIMELINE = {
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StimulusPlumbers
4
- VERSION = "0.4.14"
4
+ VERSION = "0.4.16"
5
5
  end
@@ -13,8 +13,11 @@ require_relative "stimulus_plumbers/logger"
13
13
  # -- Plumber base --
14
14
  require_relative "stimulus_plumbers/plumber/dispatcher"
15
15
  require_relative "stimulus_plumbers/plumber/slots"
16
+ require_relative "stimulus_plumbers/plumber/config"
16
17
  require_relative "stimulus_plumbers/plumber/base"
17
18
 
19
+ require_relative "stimulus_plumbers/password/requirements"
20
+
18
21
  # -- UI components --
19
22
  require_relative "stimulus_plumbers/components/icon"
20
23
  require_relative "stimulus_plumbers/components/indicator"
@@ -46,9 +49,11 @@ require_relative "stimulus_plumbers/components/input_group"
46
49
  require_relative "stimulus_plumbers/components/popover"
47
50
  require_relative "stimulus_plumbers/components/popover/trigger"
48
51
  require_relative "stimulus_plumbers/components/popover/panel"
52
+ require_relative "stimulus_plumbers/components/progress/shared"
49
53
  require_relative "stimulus_plumbers/components/progress_bar"
50
54
  require_relative "stimulus_plumbers/components/progress_ring"
51
55
  require_relative "stimulus_plumbers/components/progress_meter"
56
+ require_relative "stimulus_plumbers/components/password_strength"
52
57
 
53
58
  # -- Calendar --
54
59
  require_relative "stimulus_plumbers/components/calendar"
@@ -70,7 +75,7 @@ require_relative "stimulus_plumbers/components/combobox/date/navigation"
70
75
  require_relative "stimulus_plumbers/components/combobox/dropdown"
71
76
  require_relative "stimulus_plumbers/components/combobox/typeahead"
72
77
  require_relative "stimulus_plumbers/components/combobox/time"
73
- require_relative "stimulus_plumbers/components/combobox/builder"
78
+ require_relative "stimulus_plumbers/components/combobox/config"
74
79
 
75
80
  # -- Form --
76
81
  require_relative "stimulus_plumbers/form/field"
@@ -94,3 +99,4 @@ module StimulusPlumbers
94
99
  end
95
100
 
96
101
  require_relative "stimulus_plumbers/engine" if defined?(Rails::Engine)
102
+ require_relative "stimulus_plumbers/password_strength_validator" if defined?(ActiveModel::EachValidator)
data/vendor/ARIA.md CHANGED
@@ -97,6 +97,16 @@ Two helper classes handle keyboard navigation in controllers — see [`stimulus-
97
97
  - Accepted tradeoff: because `indeterminate` has no HTML attribute, the server can only render the master's initial `checked` state for the all-true case; every other case (including mixed) renders unchecked and is corrected to `indeterminate` once the `checklist` controller connects — a brief, accepted flash for the mixed case only.
98
98
  - Disabled (readonly) items are excluded from the master's aggregate and from bulk toggling — the `checklist` controller filters them out via their own `.disabled` property, mirroring their exclusion from tab order and AT interaction.
99
99
 
100
+ #### Progress (`progress_controller`, `sp_progress_*`)
101
+ - `role="progressbar"` is read-only and never focusable — it reports a value, it does not accept one. An interactive equivalent is a native `<input type="range">` (or `role="slider"`), not a progressbar with `tabindex`
102
+ - Value: `aria-valuemin`/`aria-valuemax` always; `aria-valuenow` omitted while indeterminate (an omitted `valuenow` is what signals "unknown progress" to AT)
103
+ - `aria-valuetext` only when the readout text is not derivable from `aria-valuenow` — set for the `value`/`value_max` formats, deliberately **not** for `percent`, where AT already computes the percentage and a duplicate would be announced twice
104
+ - On-screen readout is `aria-hidden="true"` — the value reaches AT through `aria-valuenow`/`aria-valuetext`, so exposing the span too would double-announce it
105
+ - Name: `aria-label` standalone, or `aria-labelledby` → a visible caption. `<label for>` cannot name a progressbar — `for=` is only valid against a labelable element (`button`, `input`, `meter`, `output`, `progress`, `select`, `textarea`), so a `<div role="progressbar">` targeted by one is silently left unnamed (WCAG 4.1.2). Form fields rendering a progressbar therefore emit a `<span>` caption, not a `<label>`
106
+ - No `aria-invalid`/`aria-required` on a progressbar — neither is supported on the role, and it submits nothing that could be invalid. Errors still attach via `aria-describedby`
107
+ - Segment slots are decorative (`aria-hidden="true"`) — the value is announced once, by the container
108
+ - The `range` variant drives a native `<input type="range">` and writes **no** ARIA: the native control already exposes slider role, value, and keyboard operation, and duplicating them announces worse than leaving them alone. It keeps an ordinary `<label for>` because an `input` is labelable
109
+
100
110
  #### Avatar / Card / Icon
101
111
  - Decorative images/icons: `aria-hidden="true"` or `alt=""`
102
112
  - Meaningful images: descriptive `alt` text
@@ -155,6 +155,14 @@
155
155
  "targets": [],
156
156
  "values": []
157
157
  },
158
+ "password-strength": {
159
+ "actions": [
160
+ "score"
161
+ ],
162
+ "listens": [],
163
+ "targets": [],
164
+ "values": []
165
+ },
158
166
  "popover": {
159
167
  "actions": [
160
168
  "close",
@@ -166,20 +174,26 @@
166
174
  "values": []
167
175
  },
168
176
  "progress": {
169
- "actions": [],
177
+ "actions": [
178
+ "refresh"
179
+ ],
170
180
  "listens": [],
171
181
  "targets": [
172
182
  "fill",
173
- "meter"
183
+ "input",
184
+ "meter",
185
+ "value"
174
186
  ],
175
187
  "values": [
176
188
  "current",
189
+ "format",
177
190
  "high",
178
191
  "indeterminate",
179
192
  "low",
180
193
  "max",
181
194
  "min",
182
195
  "optimum",
196
+ "segmentMode",
183
197
  "variant"
184
198
  ]
185
199
  },
@@ -0,0 +1,38 @@
1
+ # password-strength
2
+
3
+ Live password meter and requirements checklist. Scoring is provided by the [password strength plumber](../plumber/password_strength.md).
4
+
5
+ ## Targets
6
+
7
+ | Target | Element | Description |
8
+ | ------------------------- | --------- | ------------------------------------------------------- |
9
+ | `input` | `<input>` | Password input to score |
10
+ | `rule` | `<li>` | Requirement row with `data-rule` and `data-satisfied` |
11
+ | `level` | Element | Visible, polite live strength label |
12
+ | `checkIcon` / `closeIcon` | `<svg>` | Paired rule-state icons; both are required for swapping |
13
+
14
+ ## Values and outlet
15
+
16
+ | Name | Type | Default |
17
+ | ----------------- | ---------- | --------- |
18
+ | `scorer` | String | `'rules'` |
19
+ | `rules` | Array | `[]` |
20
+ | `options` | Object | `{}` |
21
+ | `labels` | Object | `{}` |
22
+ | `announceDelay` | Number | `700` |
23
+ | `progress` outlet | Controller | — |
24
+
25
+ `score()` is wired to the input event. It updates the progress outlet immediately and debounces level-label changes.
26
+
27
+ ## Rule descriptors
28
+
29
+ The controller holds no built-in rule table — the server (or a standalone caller) supplies every rule as data via the `rules` value. Each descriptor is `{ key, label?, pattern?, min?, max? }` and passes when `min ≤ n ≤ max`, where `n` is:
30
+
31
+ - **length** (no `pattern`) — the password length.
32
+ - **count** (with `pattern`) — non-overlapping occurrences of `pattern`: `(pw.match(new RegExp(pattern, "g")) || []).length`.
33
+
34
+ Defaults: `min` `0`, `max` `Infinity`. So `{ pattern: "\\d", min: 2 }` requires ≥2 digits, and `{ pattern: "\\s", min: 0, max: 0 }` forbids whitespace.
35
+
36
+ **Portability constraint:** patterns must be single-char-consuming character classes — no anchors (`^`/`$`), lookbehind, or unicode-property escapes — so non-overlapping counts agree across the Ruby and JS regex engines.
37
+
38
+ The strength `level` is `strong` when every rule passes; otherwise the score (`satisfied / total × 100`) splits `weak` from `fine` at `options.low` (default `34`). `options` carries only these non-rule knobs.
@@ -1,6 +1,6 @@
1
1
  # Progress
2
2
 
3
- Value-driven progress indicator supporting three render variants: a linear bar, an SVG ring, and a native `<meter>`.
3
+ Value-driven progress indicator supporting five render variants: a linear bar, a segmented bar, an SVG ring, a native `<meter>`, and a native `<input type="range">`.
4
4
 
5
5
  ## Stimulus Identifier
6
6
 
@@ -8,37 +8,58 @@ Value-driven progress indicator supporting three render variants: a linear bar,
8
8
 
9
9
  ## Targets
10
10
 
11
- | Name | Element | Purpose |
12
- | ------- | --------------------------------- | ----------------------------------------------------------------------------------- |
13
- | `fill` | `<div>` (bar) / `<circle>` (ring) | Element whose `width` (bar) or `stroke-dasharray`/`stroke-dashoffset` (ring) is set |
14
- | `meter` | `<meter>` | Present only for `variant: "meter"` — native element, attributes synced directly |
11
+ | Name | Element | Purpose |
12
+ | ------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `fill` | `<div>` (bar) / `<circle>` (ring) | Element whose `width` (bar) or `stroke-dasharray`/`stroke-dashoffset` (ring) is set. Segmented renders **one `fill` per segment**; the controller distributes the value across them |
14
+ | `meter` | `<meter>` | Present only for `variant: "meter"` — native element, attributes synced directly |
15
+ | `value` | `<span>` | Optional on-screen readout; its `textContent` is set from `format`. Bar and range variants |
16
+ | `input` | `<input type="range">` | Range-only, and only when a readout is present — the controller then sits on a wrapper containing both |
15
17
 
16
18
  ## Values
17
19
 
18
- | Name | Type | Default | Purpose |
19
- | ----------------------- | ------- | ------- | ------------------------------------------------------------------------- |
20
- | `variant` | String | `"bar"` | `"bar"` \| `"ring"` \| `"meter"` |
21
- | `current` | Number | `0` | Current value |
22
- | `min` | Number | `0` | Range minimum |
23
- | `max` | Number | `100` | Range maximum |
24
- | `optimum` | Number | — | Meter-only; maps to native `<meter optimum>` |
25
- | `low` | Number | — | Meter-only; maps to native `<meter low>` |
26
- | `high` | Number | — | Meter-only; maps to native `<meter high>` |
27
- | `indeterminate` | Boolean | `false` | Suppresses `aria-valuenow`; toggles the `sp-progress-indeterminate` class |
28
- | `indeterminateFraction` | Number | `0.25` | Static bar width / ring arc fraction rendered while indeterminate |
20
+ | Name | Type | Default | Purpose |
21
+ | ----------------------- | ------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
22
+ | `variant` | String | `"bar"` | `"bar"` \| `"segmented"` \| `"ring"` \| `"meter"` \| `"range"` |
23
+ | `current` | Number | `0` | Current value |
24
+ | `min` | Number | `0` | Range minimum |
25
+ | `max` | Number | `100` | Range maximum |
26
+ | `optimum` | Number | — | Meter-only; maps to native `<meter optimum>` |
27
+ | `low` | Number | — | Meter-only; maps to native `<meter low>` |
28
+ | `high` | Number | — | Meter-only; maps to native `<meter high>` |
29
+ | `indeterminate` | Boolean | `false` | Suppresses `aria-valuenow`; toggles the `sp-progress-indeterminate` class |
30
+ | `indeterminateFraction` | Number | `0.25` | Bar width / ring arc / segment chunk-width fraction rendered while indeterminate |
31
+ | `segmentMode` | String | `"discrete"` | Segmented-only. `"discrete"` lights a whole segment once progress reaches into it; `"continuous"` partially fills the boundary segment |
32
+ | `format` | String | `""` | Readout text for the `value` target: `"percent"` \| `"value"` \| `"value_max"`. Empty or unrecognized renders nothing |
33
+
34
+ ### Readout formats
35
+
36
+ | `format` | Renders | `aria-valuetext` |
37
+ | ------------- | --------- | ---------------- |
38
+ | `"percent"` | `45%` | not set |
39
+ | `"value"` | `45` | `45` |
40
+ | `"value_max"` | `45 / 60` | `45 / 60` |
41
+
42
+ `percent` omits `aria-valuetext` — assistive technology derives the percentage from `aria-valuenow` itself, so setting it would only duplicate what AT already announces. The readout element is decorative (`aria-hidden`); the value reaches AT through `aria-valuenow`/`aria-valuetext`.
43
+
44
+ The `range` variant writes no `aria-value*` at all — a native `<input type="range">` already exposes its own slider semantics — and sets `--sp-progress-percent` on the input so a theme can paint the filled portion of the track.
45
+
46
+ The `bar` variant sets `--sp-progress-percent` too, on the controller element, alongside the fill target's width. A theme can use it to split the readout's color at the fill edge. It is unset until the controller connects, so read it as `var(--sp-progress-percent, 0)` to match the server-rendered (empty) fill.
47
+
48
+ The rendered number is the value clamped to `[min, max]`, so an out-of-range `current` reads as the nearest bound rather than an impossible percentage. An empty or inverted range (`max <= min`) renders `0%`. While `indeterminate`, the readout is blank and `aria-valuetext` is removed.
29
49
 
30
50
  ## Methods
31
51
 
32
- | Method | Wired via | Purpose |
33
- | ---------------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------- |
34
- | `setValue(value)` | — | Programmatic API — clamps to `[min, max]`, updates `currentValue`, dispatches `progress:changed` |
35
- | `currentValueChanged(value)` | Stimulus value callback | Recalculates fill/meter attrs whenever `current` changes (covers `setValue()` and direct attribute edits) |
52
+ | Method | Wired via | Purpose |
53
+ | ---------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------- |
54
+ | `setValue(value)` | — | Programmatic API — clamps to `[min, max]`, updates `currentValue`, dispatches `progress:changed` |
55
+ | `currentValueChanged(value)` | Stimulus value callback | Recalculates fill/meter attrs whenever `current` changes (covers `setValue()` and direct attribute edits) |
56
+ | `refresh()` | `input->progress#refresh` | Range-only. Reads the native input's value and passes it to `setValue()` |
36
57
 
37
58
  ## Dispatches
38
59
 
39
- | Event | Detail | When |
40
- | ------------------ | --------------------- | ------------------------------------ |
41
- | `progress:changed` | `{ value, min, max }` | After `setValue()` updates the value |
60
+ | Event | Detail | When |
61
+ | ------------------ | --------------------- | -------------------------------------------------------------------------------------- |
62
+ | `progress:changed` | `{ value, min, max }` | After `setValue()` updates the value — including a range drag, which routes through it |
42
63
 
43
64
  ## Example HTML
44
65
 
@@ -54,6 +75,34 @@ Value-driven progress indicator supporting three render variants: a linear bar,
54
75
  <div data-progress-target="fill"></div>
55
76
  </div>
56
77
 
78
+ <!-- Bar with an on-screen readout -->
79
+ <div
80
+ role="progressbar"
81
+ data-controller="progress"
82
+ data-progress-current-value="45"
83
+ data-progress-max-value="100"
84
+ data-progress-format-value="percent"
85
+ >
86
+ <div data-progress-target="fill"></div>
87
+ <span data-progress-target="value" aria-hidden="true">45%</span>
88
+ </div>
89
+
90
+ <!-- Segmented — one fill per segment; number of segments = number of fill targets -->
91
+ <div
92
+ role="progressbar"
93
+ data-controller="progress"
94
+ data-progress-variant-value="segmented"
95
+ data-progress-current-value="6"
96
+ data-progress-max-value="10"
97
+ >
98
+ <!-- ×5 slots → segment size 2 -->
99
+ <div aria-hidden="true"><div data-progress-target="fill"></div></div>
100
+ <div aria-hidden="true"><div data-progress-target="fill"></div></div>
101
+ <div aria-hidden="true"><div data-progress-target="fill"></div></div>
102
+ <div aria-hidden="true"><div data-progress-target="fill"></div></div>
103
+ <div aria-hidden="true"><div data-progress-target="fill"></div></div>
104
+ </div>
105
+
57
106
  <!-- Ring -->
58
107
  <svg
59
108
  role="progressbar"
@@ -75,6 +124,34 @@ Value-driven progress indicator supporting three render variants: a linear bar,
75
124
  data-progress-max-value="100"
76
125
  ></meter>
77
126
 
127
+ <!-- Range — no readout, so the controller sits on the input itself -->
128
+ <input
129
+ type="range"
130
+ min="0"
131
+ max="100"
132
+ value="45"
133
+ style="--sp-progress-percent: 45"
134
+ data-controller="progress"
135
+ data-progress-variant-value="range"
136
+ data-progress-current-value="45"
137
+ data-progress-min-value="0"
138
+ data-progress-max-value="100"
139
+ data-action="input->progress#refresh"
140
+ />
141
+
142
+ <!-- Range with a readout — a target must be a descendant, so a wrapper hosts the controller -->
143
+ <div
144
+ data-controller="progress"
145
+ data-progress-variant-value="range"
146
+ data-progress-current-value="45"
147
+ data-progress-max-value="100"
148
+ data-progress-format-value="percent"
149
+ data-action="input->progress#refresh"
150
+ >
151
+ <input type="range" min="0" max="100" value="45" style="--sp-progress-percent: 45" data-progress-target="input" />
152
+ <span data-progress-target="value" aria-hidden="true">45%</span>
153
+ </div>
154
+
78
155
  <!-- Indeterminate -->
79
156
  <div role="progressbar" data-controller="progress" data-progress-indeterminate-value="true">
80
157
  <div data-progress-target="fill"></div>
@@ -3,14 +3,68 @@
3
3
  For a non-Rails / plain JS consumer of `@stimulus-plumbers/controllers`. Rails apps get this wired
4
4
  automatically via the `stimulus_plumbers` gem's `sp_*` helpers — skip this guide for Rails.
5
5
 
6
+ ## Install
7
+
6
8
  ```bash
7
- npm install @stimulus-plumbers/controllers
9
+ npm install @hotwired/stimulus @stimulus-plumbers/controllers
10
+ ```
11
+
12
+ ## Register
13
+
14
+ Every controller is a named export. Import the ones you use and register each under its identifier:
15
+
16
+ ```javascript
17
+ import { Application } from '@hotwired/stimulus';
18
+ import { PopoverController, ProgressController, ComboboxDateController } from '@stimulus-plumbers/controllers';
19
+
20
+ const application = Application.start();
21
+
22
+ application.register('popover', PopoverController);
23
+ application.register('progress', ProgressController);
24
+ application.register('combobox-date', ComboboxDateController);
25
+ ```
26
+
27
+ The export name is the identifier in PascalCase plus `Controller` — `combobox-date` →
28
+ `ComboboxDateController`. For the full identifier list ask `list_controllers` / read
29
+ `controller://index`; each one's targets, values, classes, outlets, and events are in
30
+ `get_controller_schema(name: identifier)` — `name:` is the identifier (`combobox-date`), not the
31
+ export name. Narrative docs come from `get_controller_docs(name: family)`, which takes the family
32
+ (`combobox`) rather than the identifier; `list_controller_docs` lists the families. Outside MCP, the
33
+ [Controllers table](https://github.com/ryancyq/stimulus-plumbers/blob/main/stimulus-plumbers/README.md#controllers)
34
+ lists the same identifiers.
35
+
36
+ Registering a controller the page never uses is harmless — Stimulus only instantiates on a matching
37
+ `data-controller`.
38
+
39
+ ## Wire the markup
40
+
41
+ Interactive components (combobox, popover, calendar) expect their `data-controller`, target, and
42
+ value attributes to already be present in the rendered HTML. Rails apps get these from `sp_*`
43
+ helpers; a plain-JS setup writes them by hand, following the HTML structure in each controller's
44
+ doc:
45
+
46
+ ```html
47
+ <div
48
+ role="progressbar"
49
+ data-controller="progress"
50
+ data-progress-current-value="30"
51
+ data-progress-min-value="0"
52
+ data-progress-max-value="100"
53
+ >
54
+ <div data-progress-target="fill"></div>
55
+ </div>
8
56
  ```
9
57
 
10
- Import and register each controller you use with your Stimulus application — see
11
- [README.md](../README.md#setup) for the full import + `application.register(...)` list and the
12
- [Controllers table](../README.md#controllers) for identifiers and their docs.
58
+ Markup shape is per-controller take the authoritative structure from `get_controller_docs(name:)`
59
+ rather than adapting this example.
60
+
61
+ ## Styling
62
+
63
+ Controllers ship no CSS; they toggle classes and attributes only. Bring your own styles, or use the
64
+ `stimulus_plumbers_tailwind` gem's token set (`guide://tailwind`).
65
+
66
+ ## Accessibility
13
67
 
14
- Interactive components (combobox, popover, calendar) expect their `data-controller` attributes to
15
- already be present in the rendered HTML Rails apps get these from `sp_*` helpers; a plain-JS setup
16
- must add them manually per each controller's doc.
68
+ Keyboard, focus, and ARIA behaviour per component is in `aria://reference` read it before
69
+ hand-writing markup, since the controllers assume the documented roles and relationships are
70
+ present.
@@ -558,6 +558,53 @@
558
558
  "actions": [],
559
559
  "dispatches": []
560
560
  },
561
+ "password-strength": {
562
+ "identifier": "password-strength",
563
+ "targets": [
564
+ "input",
565
+ "rule",
566
+ "level",
567
+ "checkIcon",
568
+ "closeIcon"
569
+ ],
570
+ "values": {
571
+ "scorer": {
572
+ "type": "String",
573
+ "default": "rules"
574
+ },
575
+ "rules": {
576
+ "type": "Array",
577
+ "default": []
578
+ },
579
+ "options": {
580
+ "type": "Object",
581
+ "default": {}
582
+ },
583
+ "labels": {
584
+ "type": "Object",
585
+ "default": {}
586
+ },
587
+ "announceDelay": {
588
+ "type": "Number",
589
+ "default": 700
590
+ }
591
+ },
592
+ "outlets": [
593
+ "progress"
594
+ ],
595
+ "classes": [],
596
+ "actions": [
597
+ "announce",
598
+ "constructor",
599
+ "drawIcons",
600
+ "drawRules",
601
+ "enhance",
602
+ "if",
603
+ "readValue",
604
+ "score"
605
+ ],
606
+ "dispatches": []
607
+ },
561
608
  "popover": {
562
609
  "identifier": "popover",
563
610
  "targets": [
@@ -616,7 +663,9 @@
616
663
  "identifier": "progress",
617
664
  "targets": [
618
665
  "fill",
619
- "meter"
666
+ "input",
667
+ "meter",
668
+ "value"
620
669
  ],
621
670
  "values": {
622
671
  "variant": {
@@ -643,6 +692,14 @@
643
692
  "type": "Number",
644
693
  "default": 0.25
645
694
  },
695
+ "segmentMode": {
696
+ "type": "String",
697
+ "default": "discrete"
698
+ },
699
+ "format": {
700
+ "type": "String",
701
+ "default": ""
702
+ },
646
703
  "optimum": {
647
704
  "type": "Number"
648
705
  },
@@ -657,11 +714,16 @@
657
714
  "classes": [],
658
715
  "actions": [
659
716
  "clamp",
717
+ "formattedValue",
660
718
  "percent",
719
+ "refresh",
661
720
  "render",
662
721
  "renderBar",
663
722
  "renderMeter",
723
+ "renderRange",
664
724
  "renderRing",
725
+ "renderSegmented",
726
+ "renderValueText",
665
727
  "setCircumference",
666
728
  "setValue"
667
729
  ],