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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +3 -1
- data/app/assets/javascripts/stimulus-plumbers/controllers.manifest.json +63 -1
- data/app/assets/javascripts/stimulus-plumbers/index.es.js +345 -198
- data/app/assets/javascripts/stimulus-plumbers/index.es.js.map +1 -1
- data/app/assets/javascripts/stimulus-plumbers/index.umd.js +1 -1
- data/app/assets/javascripts/stimulus-plumbers/index.umd.js.map +1 -1
- data/config/locales/en.yml +12 -0
- data/docs/component/combobox.md +3 -2
- data/docs/component/form.md +85 -18
- data/docs/component/indicator.md +1 -1
- data/docs/component/plumber.md +40 -1
- data/docs/component/progress.md +51 -16
- data/docs/guide.md +63 -4
- data/lib/stimulus_plumbers/components/card/slots.rb +0 -2
- data/lib/stimulus_plumbers/components/combobox/{builder.rb → config.rb} +7 -11
- data/lib/stimulus_plumbers/components/combobox.rb +12 -12
- data/lib/stimulus_plumbers/components/password_strength.rb +103 -0
- data/lib/stimulus_plumbers/components/progress/shared.rb +74 -0
- data/lib/stimulus_plumbers/components/progress_bar.rb +127 -25
- data/lib/stimulus_plumbers/components/progress_meter.rb +16 -14
- data/lib/stimulus_plumbers/components/progress_ring.rb +9 -23
- data/lib/stimulus_plumbers/components/timeline/event/slots.rb +0 -1
- data/lib/stimulus_plumbers/form/builder.rb +35 -8
- data/lib/stimulus_plumbers/form/field.rb +34 -6
- data/lib/stimulus_plumbers/form/fields/inputs/password/revealable.rb +72 -0
- data/lib/stimulus_plumbers/form/fields/inputs/password/strength.rb +31 -0
- data/lib/stimulus_plumbers/form/fields/inputs/password.rb +34 -65
- data/lib/stimulus_plumbers/form/fields/inputs/progress.rb +30 -0
- data/lib/stimulus_plumbers/form/fields/inputs/range.rb +81 -0
- data/lib/stimulus_plumbers/form/fields/inputs/text.rb +1 -1
- data/lib/stimulus_plumbers/form/fields/renderer.rb +41 -0
- data/lib/stimulus_plumbers/helpers/progress_helper.rb +14 -4
- data/lib/stimulus_plumbers/password/requirements.rb +182 -0
- data/lib/stimulus_plumbers/password_strength_validator.rb +29 -0
- data/lib/stimulus_plumbers/plumber/config.rb +37 -0
- data/lib/stimulus_plumbers/plumber/dispatcher/callable_inspector.rb +4 -0
- data/lib/stimulus_plumbers/plumber/slots.rb +2 -2
- data/lib/stimulus_plumbers/themes/schema/progress/ranges.rb +13 -0
- data/lib/stimulus_plumbers/themes/schema.rb +23 -4
- data/lib/stimulus_plumbers/version.rb +1 -1
- data/lib/stimulus_plumbers.rb +7 -1
- data/vendor/ARIA.md +10 -0
- data/vendor/component/manifest.json +16 -2
- data/vendor/controller/docs/password-strength.md +38 -0
- data/vendor/controller/docs/progress.md +100 -23
- data/vendor/controller/guide.md +61 -7
- data/vendor/controller/manifest.json +63 -1
- metadata +13 -2
data/config/locales/en.yml
CHANGED
|
@@ -30,3 +30,15 @@ en:
|
|
|
30
30
|
hide: "Hide password"
|
|
31
31
|
search:
|
|
32
32
|
clear: "Clear search"
|
|
33
|
+
password:
|
|
34
|
+
rules_heading: "It's better to have:"
|
|
35
|
+
rules:
|
|
36
|
+
length: "At least %{count} characters"
|
|
37
|
+
uppercase: "One uppercase letter"
|
|
38
|
+
lowercase: "One lowercase letter"
|
|
39
|
+
digit: "One number"
|
|
40
|
+
symbol: "One symbol"
|
|
41
|
+
levels:
|
|
42
|
+
weak: "Weak password"
|
|
43
|
+
fine: "Fine password"
|
|
44
|
+
strong: "Strong password"
|
data/docs/component/combobox.md
CHANGED
|
@@ -8,7 +8,8 @@ See [docs/component/combobox.md](../../../stimulus-plumbers/docs/component/combo
|
|
|
8
8
|
|
|
9
9
|
### `sp_combobox`
|
|
10
10
|
|
|
11
|
-
Single entry point. The panel type is chosen by a method call on the yielded
|
|
11
|
+
Single entry point. The panel type is chosen by a method call on the yielded
|
|
12
|
+
`Combobox::Config` ([`Plumber::Config`](plumber.md#plumberconfig))
|
|
12
13
|
(`c.dropdown`, `c.typeahead`, `c.date`, `c.time`) — the panel owns its `aria-haspopup`,
|
|
13
14
|
popup id, trigger icon, and wrapper data. The `sp_combobox_*` helpers below are thin
|
|
14
15
|
wrappers over this.
|
|
@@ -31,7 +32,7 @@ end %>
|
|
|
31
32
|
| `close_on_select` | `false` keeps the panel open after a selection |
|
|
32
33
|
| `**html_options` | Forwarded to the wrapper `div` |
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
Config methods: `c.dropdown(options:, value:, label:)`, `c.typeahead(options:, value:, label:, url:)`,
|
|
35
36
|
`c.date(value:)`, `c.time(format:, step:, value:)`.
|
|
36
37
|
|
|
37
38
|
---
|
data/docs/component/form.md
CHANGED
|
@@ -44,6 +44,7 @@ Standard Rails helpers are overridden to apply theme CSS classes. All native HTM
|
|
|
44
44
|
<%= f.time_zone_select :timezone %>
|
|
45
45
|
<%= f.weekday_select :weekday %>
|
|
46
46
|
<%= f.search_field :query %>
|
|
47
|
+
<%= f.range_field :volume %>
|
|
47
48
|
<%= f.check_box :agree %>
|
|
48
49
|
<%= f.radio_button :plan, "basic" %>
|
|
49
50
|
```
|
|
@@ -54,6 +55,7 @@ Special options on native helpers:
|
|
|
54
55
|
| ---------------- | ------------------ | ------------------------------------------------------------------ |
|
|
55
56
|
| `password_field` | `revealable: true` | Wraps input in an `input-revealable` controller |
|
|
56
57
|
| `search_field` | `clearable: true` | Wraps input in an `input-clearable` controller with a clear button |
|
|
58
|
+
| `range_field` | — | Themed track and thumb; no controller wiring, readout, or fill |
|
|
57
59
|
|
|
58
60
|
### f.submit
|
|
59
61
|
|
|
@@ -80,23 +82,25 @@ Submit buttons need an accessible name: pass non-blank text, or an `aria` label
|
|
|
80
82
|
|
|
81
83
|
Three methods render a complete, accessible field:
|
|
82
84
|
|
|
83
|
-
| Method | `as:` values
|
|
84
|
-
| --------------------------------------------------------------------------------- |
|
|
85
|
-
| `f.field(attr, as:, **opts)` | `:text` `:email` `:number` `:url` `:tel` `:color` `:month` `:week` `:range` `:datetime_local` `:text_area` `:file` `:password` `:date` `:time` `:select` `:search` `:code` `:credit_card` |
|
|
86
|
-
| `f.collection_field(attr, as:, collection:, value_method:, text_method:, **opts)` | `:collection_select` `:grouped_collection_select`
|
|
87
|
-
| `f.choice(attr, as:, **opts)` | `:radio` `:check_box`
|
|
85
|
+
| Method | `as:` values |
|
|
86
|
+
| --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
87
|
+
| `f.field(attr, as:, **opts)` | `:text` `:email` `:number` `:url` `:tel` `:color` `:month` `:week` `:range` `:datetime_local` `:text_area` `:file` `:password` `:date` `:time` `:select` `:search` `:code` `:credit_card` `:progress` |
|
|
88
|
+
| `f.collection_field(attr, as:, collection:, value_method:, text_method:, **opts)` | `:collection_select` `:grouped_collection_select` |
|
|
89
|
+
| `f.choice(attr, as:, **opts)` | `:radio` `:check_box` |
|
|
88
90
|
|
|
89
91
|
### Shared field options
|
|
90
92
|
|
|
91
|
-
| Option | Type | Default
|
|
92
|
-
| ------------ | ------------------------------------- |
|
|
93
|
-
| `label` | String |
|
|
94
|
-
| `hint` | String | `nil`
|
|
95
|
-
| `error` | String / Array | `nil`
|
|
96
|
-
| `required` | Boolean | `false`
|
|
97
|
-
| `hide_label` | Boolean | `false`
|
|
98
|
-
| `layout` | `:stacked` / `:inline` | `:stacked`
|
|
99
|
-
| `floating` | `:filled` / `:outlined` / `:standard` | `nil`
|
|
93
|
+
| Option | Type | Default | Description |
|
|
94
|
+
| ------------ | ------------------------------------- | ------------------------- | -------------------------------------------------------------------------- |
|
|
95
|
+
| `label` | String | translated attribute name | Override label / legend text |
|
|
96
|
+
| `hint` | String | `nil` | Hint text rendered below the field |
|
|
97
|
+
| `error` | String / Array | `nil` | Override error message(s); suppresses `model.errors[attribute]` |
|
|
98
|
+
| `required` | Boolean | `false` | Adds `required` + `aria-required="true"` |
|
|
99
|
+
| `hide_label` | Boolean | `false` | Renders label visually hidden (screen-reader accessible) |
|
|
100
|
+
| `layout` | `:stacked` / `:inline` | `:stacked` | Label above input vs beside it |
|
|
101
|
+
| `floating` | `:filled` / `:outlined` / `:standard` | `nil` | Floating-label style (text-like inputs only; `nil` = standard label above) |
|
|
102
|
+
|
|
103
|
+
Labels and legends default to the text `f.label` renders: `helpers.label.<object_name>.<attribute>`, then the model's `human_attribute_name` (`activerecord.attributes.*` / `activemodel.attributes.*`), then the humanized attribute name when the form has no model. In `fields_for`, the nested object supplies the name.
|
|
100
104
|
|
|
101
105
|
---
|
|
102
106
|
|
|
@@ -108,6 +112,9 @@ Three methods render a complete, accessible field:
|
|
|
108
112
|
<%= f.field :bio, as: :text_area, hint: "Tell us about yourself." %>
|
|
109
113
|
<%= f.field :avatar, as: :file %>
|
|
110
114
|
<%= f.field :password, as: :password, revealable: true %>
|
|
115
|
+
<%= f.field :password, as: :password do |p| %>
|
|
116
|
+
<% p.enforce min_length: 12, max_length: 64 %>
|
|
117
|
+
<% end %>
|
|
111
118
|
<%= f.field :verification_code, as: :code, length: 6 %>
|
|
112
119
|
<%= f.field :card_number, as: :credit_card %>
|
|
113
120
|
<%= f.field :email, as: :email, floating: :filled %>
|
|
@@ -118,6 +125,8 @@ Three methods render a complete, accessible field:
|
|
|
118
125
|
choices: ["ruby", "rails", "hotwire"], clearable: true %>
|
|
119
126
|
```
|
|
120
127
|
|
|
128
|
+
`f.field` can take a block when its renderer declares `&block`; that declaration opts the renderer into the block DSL. For example, the password renderer yields a `Password::Requirements` that accepts `enforce(**options)` and `rule(...)` configuration — see **Password** below. Strength renders a `password-strength` wrapper containing the input, a native `<meter>`, a polite live level, a rules heading, and a rules list. The input references the rules list with `aria-describedby`.
|
|
129
|
+
|
|
121
130
|
`choices:` takes the standard Rails shape — an array of `[label, value]` pairs (or a flat array of strings).
|
|
122
131
|
|
|
123
132
|
**Floating label variants** — label starts inside the input, animates above on focus/fill. Compatible with text-like inputs only.
|
|
@@ -162,13 +171,40 @@ Use `f.search_field` for a native `<input type="search">`.
|
|
|
162
171
|
|
|
163
172
|
**Password** (`as: :password`) — reveal-toggle wrapper backed by `input-revealable`.
|
|
164
173
|
|
|
165
|
-
| Option | Values
|
|
166
|
-
| -------------- |
|
|
167
|
-
| `revealable` | Boolean
|
|
168
|
-
| `autocomplete` | String
|
|
174
|
+
| Option | Values | Default | Description |
|
|
175
|
+
| -------------- | ------------------------ | -------------------- | ------------------------------------------------ |
|
|
176
|
+
| `revealable` | Boolean | `false` | Adds a show/hide toggle button on the input |
|
|
177
|
+
| `autocomplete` | String | `"current-password"` | Native autocomplete value |
|
|
178
|
+
| `requirements` | `Password::Requirements` | `nil` | Prebuilt rule set that drives the strength meter |
|
|
169
179
|
|
|
170
180
|
Use `f.password_field` for a plain `<input type="password">` (also accepts `revealable:`).
|
|
171
181
|
|
|
182
|
+
**Strength rules.** Declare rules inline with a block, or pass a shared `requirements:` object. `enforce(min_length:, max_length:, uppercase:/lowercase:/digit:/symbol:)` enables built-ins — a character-class option takes `true` (≥1), an Integer (≥N), or a Range (`N..M` occurrences); the length rule requires **both** `min_length` and `max_length`. `rule(key, label, pattern:, min:, max:, negate:)` adds a custom rule (`negate: true` forbids matches).
|
|
183
|
+
|
|
184
|
+
```erb
|
|
185
|
+
<%= f.field :password, as: :password do |p| %>
|
|
186
|
+
<% p.enforce min_length: 12, max_length: 64, digit: true %>
|
|
187
|
+
<% p.rule :no_spaces, "No spaces", pattern: /\s/, negate: true %>
|
|
188
|
+
<% end %>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Server enforcement.** Build one `Password::Requirements` and share it between the form meter and the model validator so they cannot drift:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
PASSWORD_RULES = StimulusPlumbers::Password::Requirements.build do |r|
|
|
195
|
+
r.enforce(min_length: 12, max_length: 64, digit: true)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# model — PasswordStrengthValidator; valid? iff every enabled rule passes
|
|
199
|
+
validates :password, password_strength: { with: PASSWORD_RULES }
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```erb
|
|
203
|
+
<%= f.field :password, as: :password, requirements: PASSWORD_RULES %>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The validator also accepts inline options (`password_strength: { min_length: 12, max_length: 64, digit: true }`) and a custom `message:`. For the rule-descriptor wire contract and meter behaviour, see the [JS controller doc](../../../stimulus-plumbers/docs/component/password-strength.md).
|
|
207
|
+
|
|
172
208
|
**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.
|
|
173
209
|
|
|
174
210
|
| Option | Values | Default | Description |
|
|
@@ -191,6 +227,37 @@ Use `f.password_field` for a plain `<input type="password">` (also accepts `reve
|
|
|
191
227
|
|
|
192
228
|
Character-cell fields do not support `floating:` labels.
|
|
193
229
|
|
|
230
|
+
**Range** (`as: :range`) — native `<input type="range">` styled as a progress track with a thumb. Unlike the progress field it is a real control, so it keeps an ordinary `<label for>`.
|
|
231
|
+
|
|
232
|
+
| Option | Values | Default | Description |
|
|
233
|
+
| --------------- | ------------------------------------ | ----------- | ------------------------------------ |
|
|
234
|
+
| `format:` | `:percent` / `:value` / `:value_max` | `nil` | Adds a live readout beside the track |
|
|
235
|
+
| `min:` / `max:` | Number | `0` / `100` | Range bounds |
|
|
236
|
+
|
|
237
|
+
```erb
|
|
238
|
+
<%= f.field :volume, as: :range, format: :percent %>
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The controller writes no `aria-*` — a native range already exposes its own slider semantics, and the readout is `aria-hidden` so the value isn't announced twice. The fill percentage is server-rendered onto the input, so it is correct before the controller connects.
|
|
242
|
+
|
|
243
|
+
Cross-browser caveat: WebKit has no filled-track pseudo-element, so the fill is a gradient driven by `--sp-progress-percent`; Firefox uses its native `::-moz-range-progress`. Use `f.range_field` for the same themed slider without the controller wiring, readout, or server-rendered fill.
|
|
244
|
+
|
|
245
|
+
**Progress** (`as: :progress`) — read-only indicator for a model attribute, rendered by [`sp_progress_bar`](progress.md#sp_progress_bar). It submits nothing and is never invalid.
|
|
246
|
+
|
|
247
|
+
| Option | Values | Default | Description |
|
|
248
|
+
| --------------- | ------------------------------------ | ----------- | --------------------------------------------------------------------------------------------------- |
|
|
249
|
+
| `format:` | `:percent` / `:value` / `:value_max` | `nil` | On-screen readout of the value |
|
|
250
|
+
| `readout:` | `:inside` / `:outside` | `:inside` | Readout placement — see [`sp_progress_bar`](progress.md#sp_progress_bar) |
|
|
251
|
+
| `segments:` | positive Integer | `nil` | Renders segmented instead of a single bar; excludes `format:`. Anything else raises `ArgumentError` |
|
|
252
|
+
| `min:` / `max:` | Number | `0` / `100` | Range bounds |
|
|
253
|
+
|
|
254
|
+
```erb
|
|
255
|
+
<%= f.field :completion, as: :progress, format: :percent %>
|
|
256
|
+
<%= f.field :profile_strength, as: :progress, segments: 5, max: 5 %>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Its label renders as a `<span>`, not a `<label>`, and names the bar via `aria-labelledby`. `<label for>` is only valid against a labelable element (`button`, `input`, `meter`, `output`, `progress`, `select`, `textarea`), and a `div[role="progressbar"]` is none of those — a `for=` pointing at it would silently leave the bar unnamed. For the same reason it takes no `required` or `aria-invalid`; `hint:` and `error:` still describe it via `aria-describedby`.
|
|
260
|
+
|
|
194
261
|
---
|
|
195
262
|
|
|
196
263
|
## f.collection_field
|
data/docs/component/indicator.md
CHANGED
|
@@ -35,7 +35,7 @@ Presentational status marker — a colored dot, an animated "pulse" ring, or a n
|
|
|
35
35
|
|
|
36
36
|
When `pulse: true`, the dot is wrapped in an extra `<span>` (`indicator_wrapper` theme key) that positions the ring behind the dot — no margin offsets involved.
|
|
37
37
|
|
|
38
|
-
**Every indicator must be paired with an accessible name** — a visible label or `aria-label`/adjacent `sr-only` text. The component
|
|
38
|
+
**Every indicator must be paired with an accessible name** — a visible label or `aria-label`/adjacent `sr-only` text. The component renders no text and cannot infer the right label, so the caller must supply one.
|
|
39
39
|
|
|
40
40
|
## Legend pattern
|
|
41
41
|
|
data/docs/component/plumber.md
CHANGED
|
@@ -105,7 +105,7 @@ end
|
|
|
105
105
|
|
|
106
106
|
## Plumber::Slots
|
|
107
107
|
|
|
108
|
-
Base class for a component's slot DSL (e.g. `renderer.with_title(value)`, `renderer.with_content { ... }`). Used by `Card`, `Button`, `Link`, `List::Item`, `Timeline::Event
|
|
108
|
+
Base class for a component's slot DSL (e.g. `renderer.with_title(value)`, `renderer.with_content { ... }`). Used by `Card`, `Button`, `Link`, `List::Item`, and `Timeline::Event`.
|
|
109
109
|
|
|
110
110
|
```ruby
|
|
111
111
|
class MySlots < StimulusPlumbers::Plumber::Slots
|
|
@@ -123,6 +123,45 @@ yield slots if block_given?
|
|
|
123
123
|
|
|
124
124
|
Slots needing custom validation (e.g. a required keyword) define `with_*` manually instead of using the `slot` DSL — see `Card::Slots#with_action`.
|
|
125
125
|
|
|
126
|
+
`set_slot` is private and returns `nil`, so a `with_*` method reads as a command — hand-written setters need no trailing `nil`. Reads go through the public `resolve` / `options_for`.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Plumber::Config
|
|
131
|
+
|
|
132
|
+
Sibling of `Slots` for block DSLs whose payload is configuration rather than content. Used by `Combobox::Config`. (Password rules are **not** a `Plumber::Config` — `Password::Requirements` is a standalone model-layer object; see the password field in [form.md](form.md).)
|
|
133
|
+
|
|
134
|
+
**Which one to subclass — does the block body produce markup?** Yes → `Slots`. No → `Config`.
|
|
135
|
+
|
|
136
|
+
| | `Plumber::Slots` | `Plumber::Config` |
|
|
137
|
+
| ----------------- | ------------------------------------------------ | ------------------------------------------------ |
|
|
138
|
+
| Holds | content — blocks rendered through the view | configuration — values, hashes, class references |
|
|
139
|
+
| Template used for | `capture` (essential) | passing through to renderers only |
|
|
140
|
+
| Declares | `slot :a, :b` macro, generating `with_*` setters | plain named methods |
|
|
141
|
+
| Public read API | `resolve` / `options_for` | none — subclasses expose named readers |
|
|
142
|
+
|
|
143
|
+
`Config` never captures, so it inherits no `slot` macro. Its whole store is **private**: `Slots` publishes `resolve` because its payload is uniform content a renderer reads generically, whereas a `Config`'s settings are typed, so each subclass names its own readers.
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
class MyConfig < StimulusPlumbers::Plumber::Config
|
|
147
|
+
def panel(**options)
|
|
148
|
+
configure(:panel, options) # stores, returns nil
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def panel_options
|
|
152
|
+
config(:panel) # reads, nil when unset
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def panel?
|
|
156
|
+
configured?(:panel) # distinguishes unset from set-to-nil
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Setters return `nil` for the same reason `set_slot` does — the DSL method reads as a command.
|
|
162
|
+
|
|
163
|
+
Construct with the view `template`; `Config` exposes it via `attr_reader` so subclasses can hand it to renderers (see `Combobox::Config#render_panel`).
|
|
164
|
+
|
|
126
165
|
---
|
|
127
166
|
|
|
128
167
|
## Plumber::Options::Html
|
data/docs/component/progress.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Progress
|
|
2
2
|
|
|
3
|
-
Rails helpers for rendering the `progress`
|
|
3
|
+
Rails helpers for rendering the four standalone `progress` variants. The controller also has a `range` variant, which has no `sp_*` helper — it is a form control, reached through [`f.field(as: :range)`](form.md). See [stimulus-plumbers's docs/component/progress.md](../../../stimulus-plumbers/docs/component/progress.md) for the controller's Values/Targets/Methods/Dispatches.
|
|
4
4
|
|
|
5
5
|
## Helpers
|
|
6
6
|
|
|
@@ -10,34 +10,69 @@ Rails helpers for rendering the `progress` Stimulus controller's three variants.
|
|
|
10
10
|
<%= sp_progress_bar(value: 65, max: 100, aria: { label: "Upload progress" }) %>
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
| Option | Default
|
|
14
|
-
| ---------------- |
|
|
15
|
-
| `value:` | —
|
|
16
|
-
| `min:` | `0`
|
|
17
|
-
| `max:` | `100`
|
|
18
|
-
| `indeterminate:` | `false`
|
|
19
|
-
|
|
|
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
|
+
| `format:` | `nil` | Renders an on-screen readout: `:percent` \| `:value` \| `:value_max`. Any other value raises `ArgumentError` |
|
|
20
|
+
| `readout:` | `:inside` | Where that readout sits: `:inside` (over the track) \| `:outside` (beside it). Always validated; no effect without `format:` |
|
|
21
|
+
| `**html_options` | — | Forwarded to the outer `<div role="progressbar">` |
|
|
22
|
+
|
|
23
|
+
`format:` renders the readout server-side, so it is correct before the controller connects. The text and its `aria-valuetext` behaviour are the same as the controller's — see [Readout formats](../../../stimulus-plumbers/docs/component/progress.md#readout-formats). The theme is told whether a readout is present, so it can give the track room for the text.
|
|
24
|
+
|
|
25
|
+
```erb
|
|
26
|
+
<%= sp_progress_bar(value: 45, format: :percent, aria: { label: "Upload progress" }) %>
|
|
27
|
+
<%= sp_progress_bar(value: 45, format: :percent, readout: :outside, aria: { label: "Upload progress" }) %>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`:outside` wraps the root around a track element plus the readout, since the track clips its overflow. Role, ARIA, and `data-controller` stay on the root in both.
|
|
31
|
+
|
|
32
|
+
Theme keys: `progress_bar_value` (inside), `progress_bar_value_outside` + `progress_bar_group` (outside). A theme without the latter two renders `:outside` unstyled, not broken.
|
|
20
33
|
|
|
21
34
|
When `indeterminate:` is true, the JS controller sets a fixed 25% fill
|
|
22
35
|
width directly — this is the same with or without a theme. A theme may
|
|
23
36
|
layer motion on top (Tailwind slides it); without one, the bar renders
|
|
24
37
|
as a static partial fill.
|
|
25
38
|
|
|
39
|
+
### `sp_progress_segmented`
|
|
40
|
+
|
|
41
|
+
```erb
|
|
42
|
+
<%= sp_progress_segmented(value: 6, segments: 5, max: 10, aria: { label: "Password strength" }) %>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`format:` is not supported here (it raises `ArgumentError`) — there is no single track to center a readout over.
|
|
46
|
+
|
|
47
|
+
Splits the track into `segments:` equal slots and distributes the value across them (max 10 with `segments: 5` → each slot spans 2 units). Renders one `fill` target per slot; the JS controller fills them.
|
|
48
|
+
|
|
49
|
+
| Option | Default | Description |
|
|
50
|
+
| ---------------- | ----------- | ------------------------------------------------------------------------------------------------------ |
|
|
51
|
+
| `value:` | — | Required. Current value |
|
|
52
|
+
| `segments:` | — | Required. Positive Integer; anything else raises `ArgumentError` |
|
|
53
|
+
| `min:` | `0` | Range minimum |
|
|
54
|
+
| `max:` | `100` | Range maximum |
|
|
55
|
+
| `mode:` | `:discrete` | `:discrete` lights a whole slot once reached; `:continuous` partially fills the boundary slot |
|
|
56
|
+
| `ramp:` | `nil` | `:strength` colors slots danger → warning → success by position (strength meter); `nil` = single color |
|
|
57
|
+
| `indeterminate:` | `false` | Omits `aria-valuenow`; a single chunk relays across the slots, one at a time |
|
|
58
|
+
| `**html_options` | — | Forwarded to the outer `<div role="progressbar">` |
|
|
59
|
+
|
|
26
60
|
### `sp_progress_ring`
|
|
27
61
|
|
|
28
62
|
```erb
|
|
29
63
|
<%= sp_progress_ring(value: 60, max: 100, aria: { label: "Storage used" }) %>
|
|
30
64
|
```
|
|
31
65
|
|
|
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
|
-
|
|
|
66
|
+
| Option | Default | Description |
|
|
67
|
+
| ---------------- | ------- | --------------------------------------------------------------------------------------------- |
|
|
68
|
+
| `value:` | — | Required. Current value |
|
|
69
|
+
| `min:` | `0` | Range minimum |
|
|
70
|
+
| `max:` | `100` | Range maximum |
|
|
71
|
+
| `indeterminate:` | `false` | Omits `aria-valuenow`; adds the indeterminate hook class |
|
|
72
|
+
| `size:` | `nil` | `:sm` \| `:md` \| `:lg` size token; `nil` uses the icon's own size (override with `classes:`) |
|
|
73
|
+
| `**html_options` | — | Forwarded to the rendered icon (role, aria, classes, etc.) |
|
|
39
74
|
|
|
40
|
-
Renders via the theme's icon registry (icon name `"progress-ring"`), the same mechanism as `sp_icon`.
|
|
75
|
+
Renders via the theme's icon registry (icon name `"progress-ring"`), the same mechanism as `sp_icon`. Resize with the `size:` token (`:sm`/`:md`/`:lg`) or, for an exact size, a `classes:`/`class:` override (e.g. `classes: "size-16"`) — there is no 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
76
|
|
|
42
77
|
When `indeterminate:` is true (and a theme provides the ring icon), the JS
|
|
43
78
|
controller sets the fill circle's `stroke-dasharray` to a fixed 25% arc
|
data/docs/guide.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# Guide
|
|
2
2
|
|
|
3
|
+
## Quickstart
|
|
4
|
+
|
|
5
|
+
Requires Ruby >= 3.0 and Rails >= 6.1.
|
|
6
|
+
|
|
7
|
+
1. Add the gem and install:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# Gemfile
|
|
11
|
+
gem "stimulus_plumbers"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bundle install
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
2. Run the install generator — it copies `tokens.css` into your app and injects the import into your
|
|
19
|
+
CSS entry file (see [CSS entry file detection](#css-entry-file-detection)):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bin/rails generate stimulus_plumbers:install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
3. Register the Stimulus controllers that back the interactive components. The gem renders the
|
|
26
|
+
`data-controller` attributes; the JS package supplies the controllers themselves:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install @stimulus-plumbers/controllers
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Register them as `guide://controller` describes — identifiers must match what the helpers emit.
|
|
33
|
+
|
|
34
|
+
4. Make the form builder the default (or pass `builder:` per `form_with`):
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
# config/application.rb
|
|
38
|
+
config.action_view.default_form_builder = StimulusPlumbers::Form::Builder
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
5. Render a first form:
|
|
42
|
+
|
|
43
|
+
```erb
|
|
44
|
+
<%= form_with model: @user do |f| %>
|
|
45
|
+
<%= f.field :email, as: :email, hint: "We never share it" %>
|
|
46
|
+
<%= f.field :password, as: :password, revealable: true %>
|
|
47
|
+
<%= f.field :country, as: :select, choices: ["Australia", "Canada"] %>
|
|
48
|
+
<%= f.submit "Create account" %>
|
|
49
|
+
<% end %>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Each `f.field` renders the label, input, hint, and error with their ARIA wiring already
|
|
53
|
+
associated — no `label`/`aria-describedby` by hand.
|
|
54
|
+
|
|
55
|
+
For view components outside a form, include the helper modules you use in `ApplicationHelper` (see
|
|
56
|
+
the gem's
|
|
57
|
+
[README](https://github.com/ryancyq/stimulus-plumbers/blob/main/stimulus-plumbers-rails/README.md#installation))
|
|
58
|
+
and call the `sp_*` helpers described under [Building views](#building-views).
|
|
59
|
+
|
|
3
60
|
## Building forms
|
|
4
61
|
|
|
5
62
|
Use `StimulusPlumbers::Form::Builder` (set `config.action_view.default_form_builder`, or pass
|
|
@@ -7,20 +64,22 @@ Use `StimulusPlumbers::Form::Builder` (set `config.action_view.default_form_buil
|
|
|
7
64
|
|
|
8
65
|
- **Level 2 — recommended.** Full accessible field (label + input + hint + error):
|
|
9
66
|
`f.field(attr, as:)`, `f.collection_field(attr, as:, collection:, ...)`, `f.choice(attr, as:)`.
|
|
10
|
-
See
|
|
67
|
+
See `component://form/docs` ([form.md](https://github.com/ryancyq/stimulus-plumbers/blob/main/stimulus-plumbers-rails/docs/component/form.md))
|
|
68
|
+
for valid `as:` values per builder method and
|
|
11
69
|
which ones are backed by a Stimulus controller (date/time/select/search pickers).
|
|
12
70
|
- **Level 1.** Native helper overrides (`f.text_field`, `f.select`, `f.check_box`, ...) render
|
|
13
71
|
only the themed input element — use when you control the surrounding markup.
|
|
14
72
|
|
|
15
73
|
Submit with `f.submit` (themed button; supports `icon_leading:`/`icon_trailing:` and
|
|
16
|
-
`hide_label:` for an icon-only button — see
|
|
74
|
+
`hide_label:` for an icon-only button — see `component://form/docs`).
|
|
17
75
|
|
|
18
76
|
## Building views
|
|
19
77
|
|
|
20
78
|
Render components with `sp_*` helpers (`sp_button`, `sp_button_group`, `sp_card`, `sp_list`,
|
|
21
79
|
`sp_link`, `sp_avatar`, `sp_divider`, `sp_icon`, `sp_popover`, ...) — see the
|
|
22
|
-
[Components table](
|
|
23
|
-
`
|
|
80
|
+
[Components table](https://github.com/ryancyq/stimulus-plumbers/blob/main/stimulus-plumbers-rails/README.md#components)
|
|
81
|
+
for the full helper list (`list_components` / `list_component_docs` over MCP), and each component's
|
|
82
|
+
`component://{name}/docs` for its keyword options, slots, and themed params.
|
|
24
83
|
|
|
25
84
|
## Stimulus integration
|
|
26
85
|
|
|
@@ -10,7 +10,6 @@ module StimulusPlumbers
|
|
|
10
10
|
raise ArgumentError, "card.body requires a block" unless block
|
|
11
11
|
|
|
12
12
|
set_slot(:body, block)
|
|
13
|
-
nil
|
|
14
13
|
end
|
|
15
14
|
|
|
16
15
|
# Defined manually (not via `slot` DSL) because it requires a named `url:` keyword with validation.
|
|
@@ -18,7 +17,6 @@ module StimulusPlumbers
|
|
|
18
17
|
raise ArgumentError, "card.action requires content (string or block) when url: is given" if url && value.nil? && !block
|
|
19
18
|
|
|
20
19
|
set_slot(:action, block || value, url ? { url: url } : {})
|
|
21
|
-
nil
|
|
22
20
|
end
|
|
23
21
|
end
|
|
24
22
|
end
|
|
@@ -5,7 +5,7 @@ module StimulusPlumbers
|
|
|
5
5
|
class Combobox
|
|
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
|
-
class
|
|
8
|
+
class Config < Plumber::Config
|
|
9
9
|
def dropdown(**options)
|
|
10
10
|
select(Dropdown, options)
|
|
11
11
|
end
|
|
@@ -23,22 +23,22 @@ module StimulusPlumbers
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def selected?
|
|
26
|
-
|
|
26
|
+
configured?(:renderer)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def renderer
|
|
30
|
-
|
|
30
|
+
config(:renderer)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def options
|
|
34
|
-
|
|
34
|
+
config(:options) || {}
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def metadata
|
|
38
38
|
renderer ? renderer::Metadata : DefaultMetadata
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
def render_panel(
|
|
41
|
+
def render_panel(panel_attrs:)
|
|
42
42
|
renderer&.new(template)&.render(panel_attrs: panel_attrs, **options)
|
|
43
43
|
end
|
|
44
44
|
|
|
@@ -70,12 +70,8 @@ module StimulusPlumbers
|
|
|
70
70
|
private
|
|
71
71
|
|
|
72
72
|
def select(renderer, options)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def selection
|
|
78
|
-
resolve(:variant)
|
|
73
|
+
configure(:renderer, renderer)
|
|
74
|
+
configure(:options, options)
|
|
79
75
|
end
|
|
80
76
|
end
|
|
81
77
|
end
|
|
@@ -9,42 +9,42 @@ module StimulusPlumbers
|
|
|
9
9
|
|
|
10
10
|
def render(trigger: {}, input: {}, id: nil, label: nil, close_on_select: nil, **kwargs, &block)
|
|
11
11
|
trigger_opts = trigger.dup
|
|
12
|
-
|
|
12
|
+
config = resolve_config(&block)
|
|
13
13
|
trigger_id = id || trigger_opts.delete(:id) || template.sp_dom_id
|
|
14
14
|
panel_id = Popover.panel_id_for(trigger_id)
|
|
15
15
|
|
|
16
|
-
template.content_tag(:div, **combobox_attrs(input, close_on_select,
|
|
17
|
-
build_popover(trigger_opts, input,
|
|
16
|
+
template.content_tag(:div, **combobox_attrs(input, close_on_select, config, panel_id, kwargs)) do
|
|
17
|
+
build_popover(trigger_opts, input, config, trigger_id, panel_id, label)
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
|
-
def
|
|
24
|
-
|
|
25
|
-
yield
|
|
26
|
-
|
|
23
|
+
def resolve_config
|
|
24
|
+
config = Combobox::Config.new(template)
|
|
25
|
+
yield config if block_given?
|
|
26
|
+
config
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def build_popover(trigger, input,
|
|
30
|
-
metadata =
|
|
29
|
+
def build_popover(trigger, input, config, trigger_id, panel_id, label)
|
|
30
|
+
metadata = config.metadata
|
|
31
31
|
|
|
32
32
|
Components::Popover.new(template).build(panel_id: panel_id) do |p|
|
|
33
33
|
p.trigger(haspopup: metadata.haspopup, controls: metadata.popup_id_for(panel_id)) do |attrs|
|
|
34
34
|
build_combobox_trigger(attrs, trigger, input, metadata, trigger_id, label)
|
|
35
35
|
end
|
|
36
36
|
p.build_panel(classes: theme.resolve(:combobox_popover).fetch(:classes, "")) do |panel_attrs|
|
|
37
|
-
|
|
37
|
+
config.render_panel(panel_attrs: panel_attrs)
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def combobox_attrs(input, close_on_select,
|
|
42
|
+
def combobox_attrs(input, close_on_select, config, panel_id, kwargs)
|
|
43
43
|
merge_html_options(
|
|
44
44
|
theme.resolve(:combobox),
|
|
45
45
|
kwargs,
|
|
46
46
|
{ data: stimulus_data(input[:value], close_on_select) },
|
|
47
|
-
{ data:
|
|
47
|
+
{ data: config.metadata.stimulus_data(panel_id, config.options) }
|
|
48
48
|
)
|
|
49
49
|
end
|
|
50
50
|
|