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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium-kanban/SKILL.md +89 -24
  3. data/CHANGELOG.md +27 -0
  4. data/app/assets/plutonium.css +1 -1
  5. data/app/assets/plutonium.js +315 -38
  6. data/app/assets/plutonium.js.map +4 -4
  7. data/app/assets/plutonium.min.js +31 -31
  8. data/app/assets/plutonium.min.js.map +4 -4
  9. data/app/views/resource/_kanban_move_action_form.html.erb +1 -0
  10. data/app/views/resource/kanban_move_form.html.erb +1 -0
  11. data/config/brakeman.ignore +2 -2
  12. data/docs/.vitepress/config.ts +21 -1
  13. data/docs/.vitepress/sync-skills.mjs +45 -0
  14. data/docs/ai.md +99 -0
  15. data/docs/guides/kanban.md +128 -18
  16. data/docs/reference/kanban/authorization.md +25 -5
  17. data/docs/reference/kanban/dsl.md +49 -8
  18. data/docs/reference/kanban/index.md +3 -3
  19. data/docs/reference/kanban/positioning.md +1 -1
  20. data/docs/reference/resource/definition.md +10 -1
  21. data/docs/reference/resource/model.md +26 -0
  22. data/docs/reference/ui/forms.md +41 -0
  23. data/docs/reference/wizard/dsl.md +5 -0
  24. data/docs/superpowers/plans/2026-07-02-kanban-drop-interactions.md +714 -0
  25. data/docs/superpowers/plans/2026-07-02-kanban-drop-interactions.md.tasks.json +68 -0
  26. data/docs/superpowers/specs/2026-07-03-kanban-auth-simplification.md +159 -0
  27. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  28. data/lib/generators/pu/gem/active_shrine/active_shrine_generator.rb +5 -0
  29. data/lib/plutonium/action/base.rb +8 -0
  30. data/lib/plutonium/configuration.rb +12 -0
  31. data/lib/plutonium/definition/index_views.rb +16 -0
  32. data/lib/plutonium/kanban/column.rb +80 -27
  33. data/lib/plutonium/models/has_cents.rb +30 -2
  34. data/lib/plutonium/resource/controller.rb +22 -1
  35. data/lib/plutonium/resource/controllers/crud_actions.rb +8 -0
  36. data/lib/plutonium/resource/controllers/kanban_actions.rb +489 -93
  37. data/lib/plutonium/resource/policy.rb +6 -0
  38. data/lib/plutonium/routing/mapper_extensions.rb +1 -0
  39. data/lib/plutonium/ui/display/components/currency.rb +41 -9
  40. data/lib/plutonium/ui/display/options/inferred_types.rb +2 -5
  41. data/lib/plutonium/ui/form/base.rb +6 -0
  42. data/lib/plutonium/ui/form/components/currency.rb +64 -0
  43. data/lib/plutonium/ui/form/components/intl_tel_input.rb +27 -1
  44. data/lib/plutonium/ui/form/components/uppy.rb +20 -2
  45. data/lib/plutonium/ui/form/kanban_move.rb +46 -0
  46. data/lib/plutonium/ui/form/options/inferred_types.rb +6 -0
  47. data/lib/plutonium/ui/form/resource.rb +12 -0
  48. data/lib/plutonium/ui/form/theme.rb +7 -0
  49. data/lib/plutonium/ui/grid/card.rb +40 -13
  50. data/lib/plutonium/ui/kanban/column.rb +111 -24
  51. data/lib/plutonium/ui/kanban/resource.rb +118 -11
  52. data/lib/plutonium/ui/layout/base.rb +1 -1
  53. data/lib/plutonium/ui/options/has_cents_field.rb +21 -0
  54. data/lib/plutonium/ui/page/index.rb +1 -1
  55. data/lib/plutonium/ui/page/interactive_action.rb +12 -2
  56. data/lib/plutonium/ui/page/kanban_move.rb +20 -0
  57. data/lib/plutonium/ui/page/show.rb +7 -2
  58. data/lib/plutonium/ui/table/resource.rb +1 -1
  59. data/lib/plutonium/ui/wizard/summary_display.rb +33 -0
  60. data/lib/plutonium/version.rb +1 -1
  61. data/package.json +5 -3
  62. data/src/css/components.css +5 -0
  63. data/src/js/controllers/currency_input_controller.js +39 -0
  64. data/src/js/controllers/intl_tel_input_controller.js +4 -0
  65. data/src/js/controllers/kanban_controller.js +442 -55
  66. data/src/js/controllers/register_controllers.js +2 -0
  67. data/yarn.lock +674 -4
  68. metadata +14 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f8adf3a1153ed95a357bf90ab145279c7f7b72540c6f160c8f52f859ed14e4e
4
- data.tar.gz: 96f58112aff1ffd9a21311c169169c31efe61c16470c65f1805b9860e987baca
3
+ metadata.gz: 62c3179e6fc69beaae9e39a5892a478bd41c546fcf0fb580dea6a4aa9680e296
4
+ data.tar.gz: 94c2ce8570a323536961196170f8cb7187efe724834485edfd210a783147cbe8
5
5
  SHA512:
6
- metadata.gz: 3cec89b62845ecb9177e21bb7307c18f182056ce01164f657b07b2dd5e165c041aa0172b7f22b967061170b3a39398834b2966c8d62ba333e279ce0ff8ee0f8d
7
- data.tar.gz: d8369fec9bd3e3ce5951248e03ee065c6dd9e31fa05bccd9ab7bc49abb69a0586a1b94adbcf5c0bec11b0dc8269927daa8ca41ad62ef7d6f20d045cc02004ca8
6
+ metadata.gz: 0b9dc172a16b74c0f313606cefe964a661842aacd63c21df3ed3cb9ca20179439d2f302b53658305a375ee2b752aacf056244eb0a6875e2a31e20276b8055a2c
7
+ data.tar.gz: e1f47b1429392d82253f27e8268309e129d33e5d62716b04c669c13e24cc4e3b34d74728d154189c116124319c7155911e49000be008ea72a2bbdd64fecc3dbd
@@ -14,8 +14,11 @@ For field-level rendering on cards (card_fields slots), see [[plutonium-resource
14
14
  - **`kanban do…end` in the Definition auto-enables `:kanban`** in `defined_index_views` — exactly like `grid_fields` enables `:grid`. You do not need to call `index_views :kanban` separately unless you want to remove the table view.
15
15
  - **The model needs `include Plutonium::Positioning`** (and a decimal `position` column + `positioned_on` call) for drag ordering to work. Without it, cards render unordered and moves raise an error. Use `position_on false` to explicitly opt out.
16
16
  - **Static column actions are auto-registered** as interactive resource actions at class-load time. Dynamic boards (`columns do…end`) cannot introspect their columns at load time — declare any column-action interactions separately with top-level `action` calls.
17
- - **Moves bypass `permitted_attributes_for_update`** — the `on_drop` callback runs with full model access. Gate the move itself with `kanban_move?` in the policy.
17
+ - **Moves bypass `permitted_attributes_for_update`** — the `on_enter` callback runs with full model access. Gate the move itself with `kanban_move?` in the policy.
18
18
  - **Quick-add (`add: true`) only appears when `create?` is true** in the policy.
19
+ - **Same-column drops = positioning only** — a reorder within a column fires neither `on_exit`, `on_enter`, nor an `enter_interaction`; they represent *leaving*/*entering* a column, so only cross-column drops trigger them.
20
+ - **`on_exit:` is the source-side hook** — fired when a card LEAVES a column (before the destination's `on_enter`, in the same transaction). Use it for source-tied side effects (stop a timer, release a slot) the destination can't own. It fires only on drag-moves via `kanban_move`, NOT on destroy/programmatic changes/quick-add — for those, use an ActiveRecord callback.
21
+ - **Use `on_enter:` / `enter_interaction:`, NOT `on_drop:` / `drop_interaction:`.** The old names were renamed. They still exist as deprecated aliases but **raise in development/test** (and only warn-and-map in production), so a definition using them fails your test suite. Always write the new names.
19
22
 
20
23
  ---
21
24
 
@@ -56,15 +59,15 @@ class TaskDefinition < ResourceDefinition
56
59
  kanban do
57
60
  column :todo,
58
61
  scope: -> { where(status: "todo") },
59
- on_drop: ->(r) { r.update!(status: "todo") }
62
+ on_enter: ->(r) { r.update!(status: "todo") }
60
63
 
61
64
  column :doing,
62
65
  scope: -> { where(status: "doing") },
63
- on_drop: ->(r) { r.update!(status: "doing") }
66
+ on_enter: ->(r) { r.update!(status: "doing") }
64
67
 
65
68
  column :done,
66
69
  scope: -> { where(status: "done") },
67
- on_drop: :mark_done! # Symbol → record.mark_done!
70
+ on_enter: :mark_done! # Symbol → record.mark_done!
68
71
  end
69
72
  end
70
73
  ```
@@ -104,7 +107,7 @@ card_fields header: :title, meta: [:status, :priority], footer: :due_at
104
107
 
105
108
  - **Mode A (default)** — delegates to `record.reposition!(prev_record:, next_record:)` from `Plutonium::Positioning`. Requires the model concern and a decimal column.
106
109
  - **Mode B (block)** — you write the persistence. Plutonium still orders by the attribute; the block only persists the new value. Block receives a `Plutonium::Kanban::Positioning::Move` (fields: `record`, `column`, `prev`, `next`, `index`).
107
- - **Mode C (`false`)** — no ordering, no repositioning. `on_drop` still fires.
110
+ - **Mode C (`false`)** — no ordering, no repositioning. `on_enter` still fires.
108
111
 
109
112
  ### `realtime`
110
113
 
@@ -135,6 +138,8 @@ end
135
138
 
136
139
  Evaluates the block at request time with the view context as `self` (`current_user`, `params`, `current_scoped_entity`, helpers all available). The block must return an Array of `Plutonium::Kanban::Column` objects — `column` is a DSL method only available outside the `columns` block. Declare any column-action interactions as top-level definition `action` calls — the block is not introspectable at class-load time.
137
140
 
141
+ > **`enter_interaction:` is NOT supported on dynamic boards.** Its hidden action is registered from the static column list at class-load time, which a `columns do…end` board doesn't have, and the key is column-scoped/internal so there's no manual-registration escape hatch (unlike column actions). A drop into such a column is rejected with a snap-back — it does not crash. Use a static board if you need `enter_interaction:`.
142
+
138
143
  ```ruby
139
144
  kanban do
140
145
  columns do
@@ -144,7 +149,7 @@ kanban do
144
149
  :"team_#{team.id}",
145
150
  label: team.name,
146
151
  scope: -> { where(team_id: team.id) },
147
- on_drop: ->(r) { r.update!(team_id: team.id) }
152
+ on_enter: ->(r) { r.update!(team_id: team.id) }
148
153
  )
149
154
  end
150
155
  end
@@ -161,12 +166,14 @@ column :key,
161
166
  color: :green, # Tailwind-mapped color hint
162
167
  wip: 3, # max cross-column moves into this column
163
168
  scope: -> { where(…) }, # 0-arg lambda or Symbol (sent to relation)
164
- on_drop: ->(r) { … }, # 1-arg lambda or Symbol → record.method!
169
+ on_enter: ->(r) { … }, # 1-arg lambda or Symbol → record.method! (card ENTERS)
170
+ on_exit: ->(r) { … }, # 1-arg lambda or Symbol → runs when a card LEAVES this column
171
+ enter_interaction: MarkLostInteraction, # record-scoped interaction run on cross-column drop (see below)
165
172
  collapsed: true, # starts collapsed (Stimulus persists toggle to localStorage)
166
173
  add: true, # show "+ Add" button (requires create?)
167
- accepts: true, # true (default), false, Array of source keys, or 1-arg Proc
174
+ accepts: true, # true (default), false, or Array of source keys (Proc raises)
168
175
  locked: false, # reject all incoming drops (server-enforced)
169
- role: :backlog # :backlog or :done (see presets below)
176
+ role: :backlog # :backlog, :done or :lost (see presets below)
170
177
  ```
171
178
 
172
179
  ### Column role presets
@@ -175,31 +182,76 @@ column :key,
175
182
  |---|---|
176
183
  | `:backlog` | `add: true` |
177
184
  | `:done` | `color: :green`, `collapsed: true` |
185
+ | `:lost` | `color: :red`, `collapsed: true` |
186
+
187
+ `:done` and `:lost` are the two terminal roles — collapsed by default, colour
188
+ signalling the outcome (`:done` = positive close, `:lost` = negative close). The
189
+ natural pair for won/lost pipelines (leads, deals, tickets).
178
190
 
179
191
  Explicit options override the preset (e.g. `role: :done, collapsed: false`).
180
192
 
181
193
  ### `accepts:`
182
194
 
183
- Controls which source columns may drop cards here:
195
+ Structural drop topology — which **source columns** may drop cards here:
184
196
 
185
197
  - `true` (default) — any source allowed
186
198
  - `false` — column is a drop target but refuses everything (snap-back)
187
199
  - `Array` — list of source column keys allowed: `accepts: [:doing]`
188
- - `Proc` (1-arg) — per-card predicate: `accepts: ->(record) { record.state == "doing" }`
189
200
 
190
- Checked server-side. Client-side visual hints read `data-kanban-accepts`.
201
+ Checked server-side; client-side visual hints read `data-kanban-accepts` (so the drag UI can grey out disallowed sources). **No Proc form** — a `Proc` raises `ArgumentError`. Record- or user-conditional rules belong in `kanban_move?`, which sees the record and the `from`/`to` columns (see Authorization below).
191
202
 
192
- ### `on_drop:`
203
+ ### `on_enter:`
193
204
 
194
205
  Runs inside a transaction after authorization and before repositioning. Receives the record for lambda form:
195
206
 
196
207
  ```ruby
197
- on_drop: ->(r) { r.update!(status: "done") } # update! directly
198
- on_drop: ->(r) { r.status = "done" } # attribute assignment — saved automatically
199
- on_drop: :mark_done! # dispatched as record.mark_done!
208
+ on_enter: ->(r) { r.update!(status: "done") } # update! directly
209
+ on_enter: ->(r) { r.status = "done" } # attribute assignment — saved automatically
210
+ on_enter: :mark_done! # dispatched as record.mark_done!
200
211
  ```
201
212
 
202
- If `on_drop` only assigns attributes without calling `save!`/`update!`, the controller calls `record.save!` automatically when the record has unsaved changes after `on_drop` returns.
213
+ If `on_enter` only assigns attributes without calling `save!`/`update!`, the controller calls `record.save!` automatically when the record has unsaved changes after `on_enter` returns.
214
+
215
+ ### `on_exit:`
216
+
217
+ The source-side counterpart to `on_enter:`. Runs on the column a card **leaves** during a cross-column move, **before** the destination's `on_enter`, in the same transaction (so it sees the pre-move state and rolls back if the move fails). Same Symbol/Proc dispatch and auto-save behaviour as `on_enter`.
218
+
219
+ ```ruby
220
+ column :doing,
221
+ scope: -> { where(status: "doing") },
222
+ on_enter: ->(r) { r.start_timer! }, # entering Doing
223
+ on_exit: ->(r) { r.stop_timer! } # leaving Doing (wherever it goes)
224
+ ```
225
+
226
+ Use it for side effects tied to the column being **left** — the destination's `on_enter` doesn't know where a card came from, so source concerns (stop a timer, release a WIP/lock, un-assign) belong here.
227
+
228
+ ⚠️ It fires **only** on a drag-move through `kanban_move` — not on `destroy`, a programmatic `status` change elsewhere, or quick-add. For "whenever this leaves, no matter how", use an ActiveRecord callback. Skipped on same-column reorders.
229
+
230
+ ### `enter_interaction:`
231
+
232
+ Run an input-collecting interaction when a card is dropped **into** this column from another column — for entries that need more than a membership flip (a reason, a mail, an audit entry).
233
+
234
+ ```ruby
235
+ column :lost, scope: -> { where(status: "lost") }, enter_interaction: MarkLostInteraction
236
+
237
+ class MarkLostInteraction < ResourceInteraction
238
+ attribute :resource # MUST be record-scoped (singular), not :resources
239
+ attribute :reason, :string
240
+ input :reason
241
+ validates :reason, presence: true
242
+ def execute
243
+ resource.update!(status: "lost", lost_reason: reason)
244
+ succeed(resource).with_message("Marked as lost") # message → toast
245
+ end
246
+ end
247
+ ```
248
+
249
+ - **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. No button on show/table/grid; reachable only by dropping. **No policy method of its own** — authorized by `kanban_move?` (see Authorization).
250
+ - **Move flow:** cross-column drop opens the interaction's form as a modal; on submit `on_enter` + interaction + repositioning commit in **one atomic transaction**. Validation failure rolls it all back (membership write included) and re-renders the modal with errors — nothing persists. Put side-effects on `deliver_later` so a rollback sends no stray mail.
251
+ - **Same-column reorder = positioning only** — neither `on_enter` nor the interaction fires (both = *entering* a column).
252
+ - **Quick-add (`+ Add`)** applies `on_enter` + positioning post-create; the interaction is not involved.
253
+ - **Author contract:** with both present, `on_enter` owns the membership attribute (`status`) and the interaction owns extras. If the interaction also writes membership it must set the **same** value (idempotent). With no `on_enter`, the interaction owns everything (like `:lost`).
254
+ - **Limitation:** custom success *responses* (`with_redirect_response`, `with_file_response`, …) are NOT honored on the drop path — board re-renders + modal closes. Use `.with_message` for feedback.
203
255
 
204
256
  ### Column actions
205
257
 
@@ -242,24 +294,37 @@ end
242
294
 
243
295
  When `kanban_move?` returns `false`, the board renders read-only — no drag handles, no drop zones.
244
296
 
297
+ **`kanban_move?` is the ONLY move authorization** — plain moves and `enter_interaction:` columns alike (the interaction has no policy method of its own). To gate a *specific* transition, read the destination (and source) column from the authorization context via the optional `kanban_to` / `kanban_from` policy readers (the `Column` objects; `nil` for every non-move check):
298
+
299
+ ```ruby
300
+ def kanban_move?
301
+ return user.manager? if kanban_to&.key == :closed_won
302
+ super
303
+ end
304
+ ```
305
+
306
+ Rules take no positional args in ActionPolicy — the columns arrive as declared optional context (`authorize :kanban_from/:kanban_to, optional: true` on the base policy), supplied by the controller on the move check.
307
+
245
308
  ### Move authorization flow
246
309
 
247
310
  1. Record loaded via current `relation_scope` (same as index).
248
- 2. `kanban_move?` checked — HTTP 403 on failure.
311
+ 2. `kanban_move?` checked (with `kanban_from`/`kanban_to` in context) — HTTP 403 on failure. This is the sole authorization; an `enter_interaction` rides on it.
249
312
  3. Column `accepts:` / `locked:` checked — HTTP 422 + card snap-back on failure.
250
313
  4. `wip:` limit checked for cross-column moves — HTTP 422 on failure.
251
- 5. `on_drop` fires + record repositioned, all in a transaction.
314
+ 5. `on_enter` fires + record repositioned, all in a transaction.
252
315
 
253
316
  On a 422 rejection (steps 3–4) the response re-renders the source column (snap-back) **and** appends a dismissable warning toast naming the reason (e.g. `“Pending” is at its WIP limit (5).`) to the board's `#kanban-flash` region — so the snap-back is never silent. The toast renders the shared `plutonium/toast` partial directly (not via `flash`), so a stale undisplayed flash can't leak into the turbo-stream response.
254
317
 
255
318
  ### No permitted-attributes gate
256
319
 
257
- Moves do not pass through `permitted_attributes_for_update`. `on_drop` is trusted author code; it is responsible for assigning only the appropriate attributes.
320
+ Moves do not pass through `permitted_attributes_for_update`. `on_enter` is trusted author code; it is responsible for assigning only the appropriate attributes.
258
321
 
259
322
  ### Quick-add
260
323
 
261
324
  The `+ Add` button (column `add: true`) only renders when the policy's `create?` is true. The opened form is the standard new-resource form.
262
325
 
326
+ The record is created normally, **then** the column's `on_enter` + positioning are applied to the **saved** record (it lands in the clicked column, appended to the bottom) — `on_enter` runs against a real record, exactly as on a drag. **Your grouping column must have a default** (DB or model), because `on_enter` runs after save; a `NOT NULL` grouping column with no default fails quick-add create. A raising `on_enter` keeps the created record in its default column and toasts the error (the create is not rolled back).
327
+
263
328
  ---
264
329
 
265
330
  ## Worked example (full)
@@ -272,18 +337,18 @@ class TaskDefinition < ResourceDefinition
272
337
 
273
338
  column :todo,
274
339
  scope: -> { where(status: "todo") },
275
- on_drop: ->(r) { r.update!(status: "todo") },
340
+ on_enter: ->(r) { r.update!(status: "todo") },
276
341
  role: :backlog # add: true
277
342
 
278
343
  column :doing,
279
344
  scope: -> { where(status: "doing") },
280
- on_drop: ->(r) { r.update!(status: "doing") },
345
+ on_enter: ->(r) { r.update!(status: "doing") },
281
346
  wip: 3
282
347
 
283
348
  column :done,
284
349
  scope: -> { where(status: "done") },
285
- on_drop: :mark_done!,
286
- accepts: ->(task) { task.status == "doing" },
350
+ on_enter: :mark_done!,
351
+ accepts: [:doing], # only cards coming from :doing
287
352
  role: :done do # color: :green, collapsed: true
288
353
  action :archive_all,
289
354
  interaction: ArchiveTasksInteraction,
data/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.62.0] - 2026-07-04
6
+
7
+ ### Bug Fixes
8
+
9
+ - Run input-less column actions directly instead of an empty modal
10
+ - Don't call signed_id on an unsaved ActiveStorage blob in uppy
11
+ - Active_shrine downstream fixes — mime-types gem + resource param double-read
12
+ - Stop the board blanking on search/filter/scope
13
+ - Size currency input padding to its unit prefix
14
+ - Preserve collapse + horizontal scroll across moves and refresh
15
+ - Give the body a base text color so unstyled text stays visible
16
+ - Give modal dialogs a base text color so unstyled text stays visible
17
+
18
+ ### Features
19
+
20
+ - AI agent on-ramp — llms.txt, /ai quickstart, crawlable skills
21
+ - Type-aware kanban meta badges + has_cents currency unit
22
+ - Expose intl-tel-input options + default_phone_country config
23
+ - Currency input + currency/choice-aware wizard review summary
24
+ - Add :lost terminal column role
25
+ - Keep the board fresh + scrolled across writes and actions
26
+ - [**breaking**] Drop interactions, immediate drops, on_exit + on_drop→on_enter rename ([#67](https://github.com/radioactive-labs/plutonium-core/issues/67))
27
+
28
+ ### Refactoring
29
+
30
+ - Server-read collapse cookie + stable frame placeholders
31
+
5
32
  ## [0.61.0] - 2026-06-30
6
33
 
7
34
  ### Bug Fixes