plutonium 0.61.0 → 0.62.0
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/.claude/skills/plutonium-kanban/SKILL.md +89 -24
- data/CHANGELOG.md +27 -0
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +315 -38
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +31 -31
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/resource/_kanban_move_action_form.html.erb +1 -0
- data/app/views/resource/kanban_move_form.html.erb +1 -0
- data/config/brakeman.ignore +2 -2
- data/docs/.vitepress/config.ts +21 -1
- data/docs/.vitepress/sync-skills.mjs +45 -0
- data/docs/ai.md +99 -0
- data/docs/guides/kanban.md +128 -18
- data/docs/reference/kanban/authorization.md +25 -5
- data/docs/reference/kanban/dsl.md +49 -8
- data/docs/reference/kanban/index.md +3 -3
- data/docs/reference/kanban/positioning.md +1 -1
- data/docs/reference/resource/definition.md +10 -1
- data/docs/reference/resource/model.md +26 -0
- data/docs/reference/ui/forms.md +41 -0
- data/docs/reference/wizard/dsl.md +5 -0
- data/docs/superpowers/plans/2026-07-02-kanban-drop-interactions.md +714 -0
- data/docs/superpowers/plans/2026-07-02-kanban-drop-interactions.md.tasks.json +68 -0
- data/docs/superpowers/specs/2026-07-03-kanban-auth-simplification.md +159 -0
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/gem/active_shrine/active_shrine_generator.rb +5 -0
- data/lib/plutonium/action/base.rb +8 -0
- data/lib/plutonium/configuration.rb +12 -0
- data/lib/plutonium/definition/index_views.rb +16 -0
- data/lib/plutonium/kanban/column.rb +80 -27
- data/lib/plutonium/models/has_cents.rb +30 -2
- data/lib/plutonium/resource/controller.rb +22 -1
- data/lib/plutonium/resource/controllers/crud_actions.rb +8 -0
- data/lib/plutonium/resource/controllers/kanban_actions.rb +489 -93
- data/lib/plutonium/resource/policy.rb +6 -0
- data/lib/plutonium/routing/mapper_extensions.rb +1 -0
- data/lib/plutonium/ui/display/components/currency.rb +41 -9
- data/lib/plutonium/ui/display/options/inferred_types.rb +2 -5
- data/lib/plutonium/ui/form/base.rb +6 -0
- data/lib/plutonium/ui/form/components/currency.rb +64 -0
- data/lib/plutonium/ui/form/components/intl_tel_input.rb +27 -1
- data/lib/plutonium/ui/form/components/uppy.rb +20 -2
- data/lib/plutonium/ui/form/kanban_move.rb +46 -0
- data/lib/plutonium/ui/form/options/inferred_types.rb +6 -0
- data/lib/plutonium/ui/form/resource.rb +12 -0
- data/lib/plutonium/ui/form/theme.rb +7 -0
- data/lib/plutonium/ui/grid/card.rb +40 -13
- data/lib/plutonium/ui/kanban/column.rb +111 -24
- data/lib/plutonium/ui/kanban/resource.rb +118 -11
- data/lib/plutonium/ui/layout/base.rb +1 -1
- data/lib/plutonium/ui/options/has_cents_field.rb +21 -0
- data/lib/plutonium/ui/page/index.rb +1 -1
- data/lib/plutonium/ui/page/interactive_action.rb +12 -2
- data/lib/plutonium/ui/page/kanban_move.rb +20 -0
- data/lib/plutonium/ui/page/show.rb +7 -2
- data/lib/plutonium/ui/table/resource.rb +1 -1
- data/lib/plutonium/ui/wizard/summary_display.rb +33 -0
- data/lib/plutonium/version.rb +1 -1
- data/package.json +5 -3
- data/src/css/components.css +5 -0
- data/src/js/controllers/currency_input_controller.js +39 -0
- data/src/js/controllers/intl_tel_input_controller.js +4 -0
- data/src/js/controllers/kanban_controller.js +442 -55
- data/src/js/controllers/register_controllers.js +2 -0
- data/yarn.lock +674 -4
- metadata +14 -2
|
@@ -90,7 +90,7 @@ Plutonium still orders column cards by `sort_order` (the first argument); your b
|
|
|
90
90
|
position_on false
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
No ordering is applied. Cards render in the relation's natural order. On drop, only `
|
|
93
|
+
No ordering is applied. Cards render in the relation's natural order. On drop, only `on_enter` fires (if set); the position attribute is never touched.
|
|
94
94
|
|
|
95
95
|
---
|
|
96
96
|
|
|
@@ -165,6 +165,8 @@ Overrides the slot layout for every kanban card on this board, using the same sl
|
|
|
165
165
|
|
|
166
166
|
When `card_fields` is not set, cards fall back to the resource definition's `grid_fields`. If neither is declared, the card renders the default header-only layout.
|
|
167
167
|
|
|
168
|
+
The `meta` slot renders each field as a colored badge, and formats values by type before badging: a `has_cents` field renders as currency, a `belongs_to` association renders as its label (not an object inspect), and everything else is humanized — with status-like enums (`active`, `pending`, `published`…) resolving to a semantic color. The badge color is deterministic per value, so a given status is the same color on every card.
|
|
169
|
+
|
|
168
170
|
---
|
|
169
171
|
|
|
170
172
|
## Static columns
|
|
@@ -177,12 +179,12 @@ kanban do
|
|
|
177
179
|
label: "To Do",
|
|
178
180
|
color: :blue,
|
|
179
181
|
scope: -> { where(status: "todo") },
|
|
180
|
-
|
|
182
|
+
on_enter: ->(r) { r.update!(status: "todo") },
|
|
181
183
|
role: :backlog
|
|
182
184
|
|
|
183
185
|
column :done,
|
|
184
186
|
scope: -> { where(status: "done") },
|
|
185
|
-
|
|
187
|
+
on_enter: :mark_done!,
|
|
186
188
|
accepts: [:doing],
|
|
187
189
|
role: :done do
|
|
188
190
|
action :archive_all, interaction: ArchiveTasksInteraction, on: :all
|
|
@@ -197,23 +199,60 @@ end
|
|
|
197
199
|
| `label:` | String | `key.to_s.titleize` | Column header label |
|
|
198
200
|
| `color:` | Symbol or String | `nil` | Header color dot. Named colors: `:red`, `:orange`, `:amber`, `:yellow`, `:green`, `:blue`, `:purple`, `:pink`, `:gray`. Raw CSS string also accepted |
|
|
199
201
|
| `scope:` | Symbol or Proc | `nil` | Relation filter for this column. **Symbol** → `relation.public_send(sym)` (named AR scope). **Proc** → 0-arg lambda called via `instance_exec` on the relation, e.g. `-> { where(status: "todo") }` |
|
|
200
|
-
| `
|
|
201
|
-
| `
|
|
202
|
+
| `on_enter:` | Symbol or Proc | `nil` | Fired when a card is dropped into this column. **Symbol** → `record.public_send(sym)`. **Proc** → 1-arg lambda `->(record) { … }` where `self` inside the block is the view context (giving access to `current_user`, helpers, etc.). The callback may assign attributes in memory (`r.status = :done`) or call `update!` directly; if the record has unsaved changes after `on_enter` returns the controller saves it automatically. |
|
|
203
|
+
| `on_exit:` | Symbol or Proc | `nil` | The source-side counterpart to `on_enter:`, fired when a card **leaves** this column on a cross-column move. Same dispatch (**Symbol** → `record.public_send`; **Proc** → 1-arg lambda, `self` = view context). Runs **before** the destination's `on_enter`, inside the same move transaction, so it sees the pre-move state and rolls back if the move fails. Use it for source-tied side effects the destination can't own (stop a timer, release a slot). Fires only on a drag-move through `kanban_move` — not on destroy, a programmatic status change, or quick-add. Skipped on same-column reorders. |
|
|
204
|
+
| `enter_interaction:` | Class | `nil` | A **record-scoped** interaction class (declares `attribute :resource`) run when a card is dropped **into** this column from another column. Opens the interaction's form as a modal to collect input, then commits `on_enter` + the interaction + repositioning atomically. Auto-registered as a hidden record action under a column-scoped key (`:<column>_enter_interaction`), authorized by `kanban_move?` (no policy method of its own). See [enter_interaction](#drop-interaction) below |
|
|
205
|
+
| `role:` | `:backlog`, `:done`, `:lost` | `nil` | Applies a preset (see below) |
|
|
202
206
|
| `collapsed:` | Boolean | `false` | Column starts collapsed (a thin strip with the label rotated). The Stimulus controller persists the toggled state to `localStorage` (key: `pu-kanban:<path>:<column-key>:collapsed`) so the user preference survives page reloads; this DSL value sets the server-rendered initial state only. |
|
|
203
207
|
| `add:` | Boolean | `false` | Show a `+ Add` quick-add button |
|
|
204
|
-
| `accepts:` | `true`, `false`,
|
|
208
|
+
| `accepts:` | `true`, `false`, or Array | `true` | Drop policy — **structural topology only**. `true` accepts any source column. `false` rejects all drops (display-only column). An Array of column key symbols accepts only those sources. This is client-hintable: the value is emitted as `data-kanban-accepts` so the drag UI greys out disallowed source columns before the drop. **Record- or user-conditional rules do not belong here** — put them in `kanban_move?`, which sees the record and the `from`/`to` columns (see [Authorization](./authorization)). Passing a `Proc` raises `ArgumentError`. |
|
|
205
209
|
| `locked:` | Boolean | `false` | Prevent dragging cards **out of** this column |
|
|
206
210
|
| `wip:` | Integer | `nil` | WIP limit. Reject cross-column drops when `dest_count + 1 > wip`. Has no effect on same-column reordering |
|
|
207
211
|
|
|
212
|
+
::: warning Renamed: `on_drop:` → `on_enter:`, `drop_interaction:` → `enter_interaction:`
|
|
213
|
+
The older `on_drop:` and `drop_interaction:` option names have been **renamed** to `on_enter:` and `enter_interaction:` (a card *entering* a column is the event, independent of how it got there).
|
|
214
|
+
|
|
215
|
+
The old names are **deprecated aliases**:
|
|
216
|
+
- In **development and test** they raise an `ArgumentError` so the rename is caught before release.
|
|
217
|
+
- In **deployed environments** (production, staging) they log a deprecation warning and map onto the new option, so an in-flight deployment keeps working across the upgrade.
|
|
218
|
+
|
|
219
|
+
If both the old and new name are given, the new one wins. Update your definitions to `on_enter:` / `enter_interaction:` at your earliest convenience — a future release will drop the aliases entirely.
|
|
220
|
+
:::
|
|
221
|
+
|
|
208
222
|
### Role presets
|
|
209
223
|
|
|
210
224
|
| Role | Equivalent options |
|
|
211
225
|
|------|--------------------|
|
|
212
226
|
| `:backlog` | `add: true` |
|
|
213
227
|
| `:done` | `color: :green, collapsed: true` |
|
|
228
|
+
| `:lost` | `color: :red, collapsed: true` |
|
|
229
|
+
|
|
230
|
+
`:done` and `:lost` are the two terminal roles — both collapsed by default, the
|
|
231
|
+
colour signalling the outcome (`:done` = positive close, `:lost` = negative
|
|
232
|
+
close). Use them as the won/lost pair in pipelines (leads, deals, tickets).
|
|
214
233
|
|
|
215
234
|
Explicitly passed options override the preset. Unknown role values raise `ArgumentError`.
|
|
216
235
|
|
|
236
|
+
### `enter_interaction:` {#drop-interaction}
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
column :lost,
|
|
240
|
+
scope: -> { where(status: "lost") },
|
|
241
|
+
enter_interaction: MarkLostInteraction
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Runs an authorization-aware, input-collecting interaction when a card is dropped **into** this column from another column.
|
|
245
|
+
|
|
246
|
+
- **Must be a record-scoped interaction** — the class declares `attribute :resource` (singular) and acts on the one dropped card. A `resources`-plural (bulk) interaction is not valid here; that shape is for [column actions](#column-actions).
|
|
247
|
+
- **Auto-registered as a hidden record action** under a column-scoped key (`:lost` → `:lost_enter_interaction`) — unique by construction, so two columns can reuse the same interaction class without colliding. Hidden = it does not render as an action button on the show page, table rows, or grid cards; it is reachable only by a drop.
|
|
248
|
+
- **Authorized by `kanban_move?` alone** — the interaction has **no policy method of its own**. The move (and therefore the interaction) is gated by the single `kanban_move?` predicate, which can gate this specific transition via its `to` column context (e.g. `def kanban_move? = kanban_to&.key == :lost ? user.manager? : super`). See [Authorization](./authorization).
|
|
249
|
+
- **Move flow only.** Dropping cross-column opens the interaction's form as a modal; on submit `on_enter` + the interaction + repositioning commit in **one atomic transaction**. Validation failure rolls the whole transaction back (membership write included) and re-renders the modal with errors — nothing persists. Same-column reorders run positioning only (neither `on_enter` nor the interaction fires).
|
|
250
|
+
- **Quick-add (`+ Add`)** applies `on_enter` + positioning **post-create** — the record is created first (needs a grouping-column default), then `on_enter` places it in the column; the `enter_interaction` is not involved.
|
|
251
|
+
- **Author contract** — when a column declares both, `on_enter` owns the membership attribute (e.g. `status`) and the interaction owns the extras (reason, mail, audit). If the interaction also writes the membership attribute it must set the same value `on_enter` does (idempotent). With no `on_enter`, the interaction owns everything.
|
|
252
|
+
- **Success response limitation** — the interaction's success **message** (`.with_message`) surfaces as a toast, but a custom success *response* (`with_redirect_response`, `with_file_response`, …) is **not** honored on the drop path; the board just re-renders and closes the modal.
|
|
253
|
+
|
|
254
|
+
See the [guide's Interaction on drop section](/guides/kanban#interaction-on-drop) for a full worked example.
|
|
255
|
+
|
|
217
256
|
---
|
|
218
257
|
|
|
219
258
|
## Dynamic columns
|
|
@@ -230,7 +269,7 @@ kanban do
|
|
|
230
269
|
label: status.name,
|
|
231
270
|
color: status.color_symbol,
|
|
232
271
|
scope: -> { where(status_id: status.id) },
|
|
233
|
-
|
|
272
|
+
on_enter: ->(r) { r.update!(status_id: status.id) }
|
|
234
273
|
)
|
|
235
274
|
end
|
|
236
275
|
end
|
|
@@ -256,6 +295,8 @@ class TaskDefinition < ResourceDefinition
|
|
|
256
295
|
end
|
|
257
296
|
end
|
|
258
297
|
```
|
|
298
|
+
|
|
299
|
+
**`enter_interaction:` is not supported on dynamic boards.** Unlike a column action, its hidden action can't be registered manually — the key is column-scoped and internal — and the static registration pass has no columns to see. A drop into a dynamic column that declares `enter_interaction:` is rejected with a snap-back (it does not crash). Use a static board when a column needs an `enter_interaction:`.
|
|
259
300
|
:::
|
|
260
301
|
|
|
261
302
|
---
|
|
@@ -267,7 +308,7 @@ Declare actions inside a column `do…end` block:
|
|
|
267
308
|
```ruby
|
|
268
309
|
column :done,
|
|
269
310
|
scope: -> { where(status: "done") },
|
|
270
|
-
|
|
311
|
+
on_enter: :mark_done! do
|
|
271
312
|
|
|
272
313
|
action :archive_all,
|
|
273
314
|
interaction: ArchiveTasksInteraction,
|
|
@@ -20,17 +20,17 @@ class TaskDefinition < ResourceDefinition
|
|
|
20
20
|
|
|
21
21
|
column :todo,
|
|
22
22
|
scope: -> { where(status: "todo") },
|
|
23
|
-
|
|
23
|
+
on_enter: ->(r) { r.update!(status: "todo") },
|
|
24
24
|
role: :backlog
|
|
25
25
|
|
|
26
26
|
column :doing,
|
|
27
27
|
scope: -> { where(status: "doing") },
|
|
28
|
-
|
|
28
|
+
on_enter: ->(r) { r.update!(status: "doing") },
|
|
29
29
|
wip: 3
|
|
30
30
|
|
|
31
31
|
column :done,
|
|
32
32
|
scope: -> { where(status: "done") },
|
|
33
|
-
|
|
33
|
+
on_enter: :mark_done!,
|
|
34
34
|
accepts: [:doing],
|
|
35
35
|
role: :done
|
|
36
36
|
end
|
|
@@ -140,7 +140,7 @@ kanban do
|
|
|
140
140
|
end
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
No ordering is applied (relation is returned unchanged). On drop, `
|
|
143
|
+
No ordering is applied (relation is returned unchanged). On drop, `on_exit`/`on_enter` still fire; the position attribute is never touched. Cards render in the relation's default order.
|
|
144
144
|
|
|
145
145
|
---
|
|
146
146
|
|
|
@@ -121,7 +121,7 @@ These render automatically — declare an `as:` only to override or pass options
|
|
|
121
121
|
|---|---|---|
|
|
122
122
|
| `boolean` | Yes/No pill (`:boolean`) | green "Yes" / neutral "No"; override with `true_label:` / `false_label:` |
|
|
123
123
|
| `enum` | status badge (`:badge`) | known statuses auto-colored; unknown values get a stable decorative color; override per-value with `colors:` |
|
|
124
|
-
| `has_cents` decimal | currency (`:currency`) | delimited, 2 decimals
|
|
124
|
+
| `has_cents` decimal | currency (`:currency`) | delimited, 2 decimals; symbol from `unit:` on `has_cents` (model-wide) or per-display, else `config.default_currency_unit` / the i18n default (see below) |
|
|
125
125
|
|
|
126
126
|
```ruby
|
|
127
127
|
display :status, as: :badge, colors: {archived: :neutral, vip: :accent}
|
|
@@ -129,6 +129,15 @@ display :price, as: :currency, unit: "£"
|
|
|
129
129
|
display :active, as: :boolean, true_label: "Live", false_label: "Off"
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
+
**Currency symbol.** The `unit:` can be set on the model's `has_cents` declaration
|
|
133
|
+
(`has_cents :price_cents, unit: "£"`, or `unit: :currency_symbol` to read a method
|
|
134
|
+
off the record for per-row currencies). That model-level unit is used everywhere the
|
|
135
|
+
value renders as currency — the show page, tables, **and grid/kanban cards**. A
|
|
136
|
+
per-display `unit:` overrides it for that one display; `unit: false` explicitly
|
|
137
|
+
renders no symbol. When neither is set, currency falls back to
|
|
138
|
+
`Plutonium.configuration.default_currency_unit` (default: the i18n
|
|
139
|
+
`number.currency.format.unit` if the locale defines it — `$` in `en` — else no symbol).
|
|
140
|
+
|
|
132
141
|
## Field options
|
|
133
142
|
|
|
134
143
|
```ruby
|
|
@@ -97,6 +97,9 @@ class Product < ResourceRecord
|
|
|
97
97
|
has_cents :cost_cents, name: :wholesale # custom accessor name
|
|
98
98
|
has_cents :tax_cents, rate: 1000 # 3 decimal places (e.g. for fractional currencies)
|
|
99
99
|
has_cents :amount_yen, rate: 1 # currencies with no subunit (JPY)
|
|
100
|
+
has_cents :gbp_cents, unit: "£" # currency symbol used wherever this renders as currency
|
|
101
|
+
has_cents :multi_cents, unit: :currency_symbol # per-row: reads record.currency_symbol
|
|
102
|
+
has_cents :points_cents, unit: false # explicitly no symbol (skips the default)
|
|
100
103
|
end
|
|
101
104
|
|
|
102
105
|
product = Product.new
|
|
@@ -109,6 +112,29 @@ product.price = 10.999
|
|
|
109
112
|
product.price_cents # => 1099
|
|
110
113
|
```
|
|
111
114
|
|
|
115
|
+
**Currency symbol (`unit:`)** — a `String` is used verbatim (`unit: "£"`); a `Symbol`
|
|
116
|
+
names a method read off the record for per-row currencies (`unit: :currency_symbol`
|
|
117
|
+
→ `record.currency_symbol`); `false` explicitly renders no symbol. This unit is picked
|
|
118
|
+
up automatically anywhere the value renders as currency — show pages, tables, and
|
|
119
|
+
grid/kanban cards — so you configure it once on the model. A per-display
|
|
120
|
+
`display :price, as: :currency, unit: …` overrides it for that display.
|
|
121
|
+
|
|
122
|
+
A `has_cents` field also **infers the currency input** on forms: a bare `input :price`
|
|
123
|
+
renders the [currency input](../ui/forms#currency-fields) (number field + unit prefix),
|
|
124
|
+
no `as: :currency` needed — matching the display side.
|
|
125
|
+
|
|
126
|
+
Resolution is `display unit → has_cents unit → config default`, where `nil` means
|
|
127
|
+
"not set, keep looking" and `false` means "stop, no symbol". When nothing is set, it
|
|
128
|
+
falls back to `Plutonium.configuration.default_currency_unit`, which itself defaults
|
|
129
|
+
to the i18n `number.currency.format.unit` *if the locale defines it* (`$` in `en`),
|
|
130
|
+
else no symbol:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
Plutonium.configure do |config|
|
|
134
|
+
config.default_currency_unit = "£" # app-wide default; false for no symbol; nil → i18n-if-set
|
|
135
|
+
end
|
|
136
|
+
```
|
|
137
|
+
|
|
112
138
|
::: danger Use the virtual accessor in policies and definitions
|
|
113
139
|
Reference `:price`, NOT `:price_cents`:
|
|
114
140
|
|
data/docs/reference/ui/forms.md
CHANGED
|
@@ -162,6 +162,7 @@ render field(:title).wrapped(class: "col-span-full") { |f| f.input_tag }
|
|
|
162
162
|
| `slim_select_tag` | Slim Select (enhanced dropdown) |
|
|
163
163
|
| `flatpickr_tag` | Flatpickr date/time picker |
|
|
164
164
|
| `phone_tag` / `int_tel_input_tag` | intl-tel-input phone field |
|
|
165
|
+
| `currency_tag` | Money input (number field + optional unit prefix) |
|
|
165
166
|
| `uppy_tag` / `file_tag` | Uppy file upload |
|
|
166
167
|
| `secure_association_tag` | Association with policy-checked options (inline `+` add, typeahead) |
|
|
167
168
|
| `belongs_to_tag` / `has_many_tag` / `has_one_tag` | Association selects |
|
|
@@ -175,6 +176,46 @@ render field(:avatar).wrapped do |f|
|
|
|
175
176
|
end
|
|
176
177
|
```
|
|
177
178
|
|
|
179
|
+
### Phone fields (intl-tel-input) {#phone-fields}
|
|
180
|
+
|
|
181
|
+
`as: :phone` renders an [intl-tel-input](https://github.com/jackocnr/intl-tel-input) field. Forward library options two ways:
|
|
182
|
+
|
|
183
|
+
- `initial_country:` — a convenient shortcut for the library's `initialCountry` (ISO2, e.g. `"gh"`). This preselects a country so the widget doesn't show *"No country selected"* and a bare local number validates.
|
|
184
|
+
- `intl_options:` — any other library option, using the library's own camelCase names. Merged over the shortcut, so it wins on conflict.
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
input :phone, as: :phone, initial_country: "gh"
|
|
188
|
+
input :phone, as: :phone, intl_options: {separateDialCode: true, strictMode: false}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
When a field sets no country, it falls back to `Plutonium.configuration.default_phone_country` (an ISO2 code, or `nil` to leave it to the library):
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
Plutonium.configure do |config|
|
|
195
|
+
config.default_phone_country = "gh"
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The field defaults to `strictMode: true`; override it via `intl_options: {strictMode: false}`.
|
|
200
|
+
|
|
201
|
+
### Currency fields {#currency-fields}
|
|
202
|
+
|
|
203
|
+
`as: :currency` renders a number input (`inputmode="decimal"`, `step="0.01"`) with an **optional** currency-unit prefix overlaid at its left edge. The unit resolves by the **same** chain as the currency *display* ([`Currency.resolve_unit`](../resource/model#has-cents)), so the form, show page, index, and wizard summary all show the same symbol:
|
|
204
|
+
|
|
205
|
+
**explicit `unit:` → the record's `has_cents` unit → `config.default_currency_unit` → the i18n `number.currency.format.unit`.**
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
input :price, as: :currency # unit from has_cents / config / i18n
|
|
209
|
+
input :price, as: :currency, unit: "£" # a literal symbol
|
|
210
|
+
input :price, as: :currency, unit: false # no prefix — a plain number input
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
When nothing resolves (or `unit: false`), the prefix is omitted and it's an ordinary number input.
|
|
214
|
+
|
|
215
|
+
**`has_cents` fields infer it automatically** — just like the display. A bare `input :price` on a `has_cents` attribute renders the currency input with the unit read off `has_cents`; no `as: :currency` needed. Use the explicit `as: :currency` for a non-`has_cents` decimal, or to override the unit.
|
|
216
|
+
|
|
217
|
+
In a **wizard** step the data snapshot has no `has_cents` reflection, so there's nothing to infer from — declare `as: :currency` and pass `unit:` explicitly (`input :price, as: :currency, unit: "$"`); the review summary reads it back and formats the value as currency.
|
|
218
|
+
|
|
178
219
|
### Password & secret fields {#password-fields}
|
|
179
220
|
|
|
180
221
|
`password_tag` renders a masking input that **never emits the stored value** into the DOM. A stored secret renders a fixed sentinel (masking both the value and its length); on submit:
|
|
@@ -204,6 +204,11 @@ What it renders depends on completion state and the `summary:` / block options:
|
|
|
204
204
|
| `summary:` | Show the auto-summary of completed steps (default `true`). When `false`, the complete-state body is your block — or the built-in "ready to complete" panel if there's no block. The summary always renders in the incomplete state. |
|
|
205
205
|
| `header:` | Show the step-header section (the label plus the "check everything over" prompt, which only appears when the summary is shown) above the body (default `true`). `false` drops it for a chromeless finish. |
|
|
206
206
|
|
|
207
|
+
The auto-summary renders each field through the display pipeline, honoring the input's declared `as:`/`label:`:
|
|
208
|
+
|
|
209
|
+
- a **choice input** (`select`/`radio_buttons` with `choices:`) resolves the stored value back to its label (`"pro"` → `"Pro"`) using the same choice mapper the form uses, so the recap matches what the user picked;
|
|
210
|
+
- an **`as: :currency`** input formats the value as currency (`"1500.5"` → `"$1,500.50"`) rather than echoing a bare decimal — pass `unit:` on the input (the data snapshot has no `has_cents` reflection to infer it from).
|
|
211
|
+
|
|
207
212
|
```ruby
|
|
208
213
|
review label: "Review & submit" # auto-summary + gated finish
|
|
209
214
|
|