plutonium 0.62.2 → 0.64.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/SKILL.md +44 -0
- data/.claude/skills/plutonium-app/SKILL.md +3 -3
- data/.claude/skills/plutonium-async-interactions/SKILL.md +191 -0
- data/.claude/skills/plutonium-auth/SKILL.md +36 -0
- data/.claude/skills/plutonium-behavior/SKILL.md +121 -24
- data/.claude/skills/plutonium-kanban/SKILL.md +17 -3
- data/.claude/skills/plutonium-resource/SKILL.md +259 -12
- data/.claude/skills/plutonium-tenancy/SKILL.md +32 -3
- data/.claude/skills/plutonium-ui/SKILL.md +115 -14
- data/.claude/skills/plutonium-wizard/SKILL.md +73 -4
- data/.standard.yml +1 -1
- data/CHANGELOG.md +73 -0
- data/CLAUDE.md +87 -0
- data/Rakefile +34 -0
- data/SECURITY.md +1 -1
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +685 -102
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +53 -53
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/rodauth/_login_form.html.erb +13 -0
- data/db/migrate/async_interactions/20260817000001_create_plutonium_async_runs.rb +170 -0
- data/docs/.vitepress/config.ts +81 -3
- data/docs/.vitepress/theme/blog.data.ts +44 -0
- data/docs/.vitepress/theme/components/BlogIndex.vue +87 -0
- data/docs/.vitepress/theme/components/BlogMeta.vue +47 -0
- data/docs/.vitepress/theme/components/HomeFeatureTour.vue +293 -0
- data/docs/.vitepress/theme/components/HomeHero.vue +3 -3
- data/docs/.vitepress/theme/components/HomeInTheBox.vue +8 -0
- data/docs/.vitepress/theme/components/HomeStopWriting.vue +1 -0
- data/docs/.vitepress/theme/components/HomeWhyPlutonium.vue +84 -0
- data/docs/.vitepress/theme/index.ts +8 -4
- data/docs/blog/association-inputs-post-signed-ids.md +70 -0
- data/docs/blog/fix-the-model-not-the-policy.md +122 -0
- data/docs/blog/fractional-ordering-runs-out-of-room.md +67 -0
- data/docs/blog/half-finished-forms-are-pii.md +76 -0
- data/docs/blog/index.md +13 -0
- data/docs/blog/interactions-are-presentation-objects.md +152 -0
- data/docs/blog/introducing-plutonium.md +253 -0
- data/docs/blog/jobs-are-not-permission-snapshots.md +100 -0
- data/docs/blog/plutonium-and-ai-agents.md +48 -0
- data/docs/blog/realtime-is-one-line-and-four-dependencies.md +70 -0
- data/docs/blog/two-forms-one-dom-id.md +69 -0
- data/docs/blog/whats-new-async-kanban-wizards.md +130 -0
- data/docs/getting-started/tutorial/04-authorization.md +12 -3
- data/docs/getting-started/tutorial/06-nested-resources.md +3 -1
- data/docs/getting-started/tutorial/07-author-portal.md +2 -2
- data/docs/guides/authentication.md +73 -0
- data/docs/guides/authorization.md +2 -0
- data/docs/guides/creating-packages.md +5 -3
- data/docs/guides/custom-actions.md +74 -12
- data/docs/guides/customizing-ui.md +9 -2
- data/docs/guides/index.md +1 -0
- data/docs/guides/kanban.md +7 -5
- data/docs/guides/nested-resources.md +11 -1
- data/docs/guides/performance.md +104 -0
- data/docs/guides/user-invites.md +1 -1
- data/docs/guides/wizards.md +9 -1
- data/docs/index.md +3 -3
- data/docs/public/images/home/tour-actions.png +0 -0
- data/docs/public/images/home/tour-async.png +0 -0
- data/docs/public/images/home/tour-kanban.png +0 -0
- data/docs/public/images/home/tour-tenancy.png +0 -0
- data/docs/public/images/home/tour-wizard.png +0 -0
- data/docs/public/images/reference/async-progress-page.png +0 -0
- data/docs/public/images/reference/async-running-banner.png +0 -0
- data/docs/public/templates/experimental.rb +34 -0
- data/docs/public/templates/pluton8.rb +14 -0
- data/docs/reference/app/portals.md +15 -3
- data/docs/reference/auth/accounts.md +19 -0
- data/docs/reference/behavior/async-interactions.md +295 -0
- data/docs/reference/behavior/controllers.md +17 -4
- data/docs/reference/behavior/index.md +7 -1
- data/docs/reference/behavior/interactions.md +152 -22
- data/docs/reference/configuration.md +5 -0
- data/docs/reference/index.md +1 -0
- data/docs/reference/kanban/dsl.md +7 -4
- data/docs/reference/kanban/index.md +1 -1
- data/docs/reference/kanban/positioning.md +26 -4
- data/docs/reference/positioning.md +568 -0
- data/docs/reference/resource/actions.md +97 -4
- data/docs/reference/resource/definition.md +181 -9
- data/docs/reference/tenancy/invites.md +1 -1
- data/docs/reference/tenancy/nested-resources.md +60 -2
- data/docs/reference/ui/assets.md +4 -0
- data/docs/reference/ui/components.md +57 -4
- data/docs/reference/ui/displays.md +20 -10
- data/docs/reference/ui/index.md +1 -1
- data/docs/reference/wizard/dsl.md +33 -0
- data/docs/reference/wizard/storage-config.md +1 -0
- data/docs/superpowers/plans/2026-07-16-homepage-depth-upgrade.md +624 -0
- data/docs/superpowers/plans/2026-07-16-homepage-depth-upgrade.md.tasks.json +32 -0
- data/docs/superpowers/plans/2026-07-31-positioned-drag-and-drop.md +1787 -0
- data/docs/superpowers/plans/2026-07-31-positioned-drag-and-drop.md.tasks.json +91 -0
- data/docs/superpowers/plans/2026-08-17-async-interactions.md +1414 -0
- data/docs/superpowers/plans/2026-08-17-async-interactions.md.tasks.json +66 -0
- data/docs/superpowers/specs/2026-07-16-homepage-depth-upgrade-design.md +111 -0
- data/docs/superpowers/specs/2026-07-17-action-html-attributes-design.md +124 -0
- data/docs/superpowers/specs/2026-07-31-positioned-drag-and-drop-design.md +506 -0
- data/docs/superpowers/specs/2026-08-17-async-interactions-design.md +185 -0
- data/gemfiles/postgres.gemfile.lock +85 -85
- data/gemfiles/rails_7.gemfile.lock +323 -141
- data/gemfiles/rails_8.0.gemfile.lock +126 -116
- data/gemfiles/rails_8.1.gemfile.lock +127 -117
- data/lib/generators/pu/async_interactions/install_generator.rb +111 -0
- data/lib/generators/pu/async_interactions/templates/app/controllers/async_runs_controller.rb.tt +15 -0
- data/lib/generators/pu/core/typespec/typespec_generator.rb +7 -4
- data/lib/generators/pu/invites/install_generator.rb +3 -3
- data/lib/generators/pu/invites/templates/packages/invites/app/views/layouts/invites/invitation.html.erb.tt +2 -2
- data/lib/generators/pu/lib/plutonium_generators/concerns/mounts_engines.rb +47 -2
- data/lib/generators/pu/lib/plutonium_generators/concerns/resource_registration.rb +41 -0
- data/lib/generators/pu/lite/litestream/litestream_generator.rb +1 -1
- data/lib/generators/pu/lite/solid_queue/solid_queue_generator.rb +1 -1
- data/lib/generators/pu/res/conn/conn_generator.rb +19 -39
- data/lib/generators/pu/res/conn/templates/app/controllers/resource_controller.rb.tt +4 -0
- data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +15 -6
- data/lib/generators/pu/rodauth/templates/app/rodauth/rodauth_plugin.rb.tt +7 -0
- data/lib/generators/pu/saas/welcome/templates/app/views/layouts/welcome.html.erb.tt +2 -2
- data/lib/generators/pu/wizards/install_generator.rb +78 -0
- data/lib/plutonium/action/base.rb +71 -9
- data/lib/plutonium/action/interactive.rb +9 -0
- data/lib/plutonium/attachments.rb +254 -0
- data/lib/plutonium/configuration.rb +82 -1
- data/lib/plutonium/core/controller.rb +50 -7
- data/lib/plutonium/core/controllers/authorizable.rb +16 -0
- data/lib/plutonium/core/controllers/entity_scoping.rb +12 -2
- data/lib/plutonium/definition/base.rb +51 -0
- data/lib/plutonium/definition/display_layout.rb +112 -0
- data/lib/plutonium/definition/index_views.rb +8 -7
- data/lib/plutonium/definition/input_aliases.rb +38 -0
- data/lib/plutonium/definition/page_widths.rb +65 -0
- data/lib/plutonium/definition/positioning.rb +126 -0
- data/lib/plutonium/definition/sorting.rb +17 -2
- data/lib/plutonium/helpers/turbo_helper.rb +7 -0
- data/lib/plutonium/interaction/README.md +61 -24
- data/lib/plutonium/interaction/async/configuration.rb +38 -0
- data/lib/plutonium/interaction/async/context.rb +419 -0
- data/lib/plutonium/interaction/async/executor.rb +422 -0
- data/lib/plutonium/interaction/async/job.rb +80 -0
- data/lib/plutonium/interaction/async/reap_job.rb +81 -0
- data/lib/plutonium/interaction/async/run.rb +394 -0
- data/lib/plutonium/interaction/async/run_definition.rb +155 -0
- data/lib/plutonium/interaction/async/run_policy.rb +86 -0
- data/lib/plutonium/interaction/base.rb +34 -7
- data/lib/plutonium/interaction/concerns/dispatchable.rb +518 -0
- data/lib/plutonium/interaction/concerns/scoping.rb +70 -9
- data/lib/plutonium/interaction/response/redirect.rb +11 -3
- data/lib/plutonium/kanban/board.rb +14 -0
- data/lib/plutonium/kanban/column.rb +4 -2
- data/lib/plutonium/kanban/dsl.rb +4 -1
- data/lib/plutonium/kanban/grouping.rb +9 -22
- data/lib/plutonium/kanban/positioning.rb +5 -65
- data/lib/plutonium/positioning/config.rb +94 -0
- data/lib/plutonium/positioning/model.rb +128 -0
- data/lib/plutonium/positioning.rb +25 -86
- data/lib/plutonium/railtie.rb +1 -0
- data/lib/plutonium/resource/controller.rb +118 -38
- data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +32 -2
- data/lib/plutonium/resource/controllers/crud_actions.rb +30 -2
- data/lib/plutonium/resource/controllers/eager_loading.rb +87 -0
- data/lib/plutonium/resource/controllers/export_csv.rb +10 -1
- data/lib/plutonium/resource/controllers/kanban_actions.rb +53 -14
- data/lib/plutonium/resource/controllers/position_actions.rb +390 -0
- data/lib/plutonium/resource/controllers/presentable.rb +19 -13
- data/lib/plutonium/resource/controllers/queryable.rb +5 -1
- data/lib/plutonium/resource/controllers/wizard_actions.rb +21 -0
- data/lib/plutonium/resource/policy.rb +33 -0
- data/lib/plutonium/resource/query_object.rb +36 -0
- data/lib/plutonium/routing/mapper_extensions.rb +100 -8
- data/lib/plutonium/routing/route_set_extensions.rb +15 -1
- data/lib/plutonium/routing/wizard_registration.rb +4 -0
- data/lib/plutonium/testing/resource_policy.rb +6 -2
- data/lib/plutonium/ui/action_button.rb +12 -7
- data/lib/plutonium/ui/actions_dropdown.rb +1 -1
- data/lib/plutonium/ui/block.rb +21 -1
- data/lib/plutonium/ui/breadcrumbs.rb +187 -55
- data/lib/plutonium/ui/component/methods.rb +5 -0
- data/lib/plutonium/ui/component/positionable.rb +112 -0
- data/lib/plutonium/ui/component/resolves_tags.rb +57 -0
- data/lib/plutonium/ui/component/section.rb +185 -0
- data/lib/plutonium/ui/display/base.rb +13 -1
- data/lib/plutonium/ui/display/components/formatted_value.rb +26 -0
- data/lib/plutonium/ui/display/components/section.rb +18 -0
- data/lib/plutonium/ui/display/resource.rb +141 -22
- data/lib/plutonium/ui/display/theme.rb +20 -1
- data/lib/plutonium/ui/export_button.rb +1 -1
- data/lib/plutonium/ui/form/base.rb +8 -7
- data/lib/plutonium/ui/form/components/intl_tel_input.rb +1 -1
- data/lib/plutonium/ui/form/components/section.rb +7 -62
- data/lib/plutonium/ui/form/components/uppy.rb +12 -1
- data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +16 -3
- data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +5 -1
- data/lib/plutonium/ui/form/query.rb +2 -4
- data/lib/plutonium/ui/form/resource.rb +92 -15
- data/lib/plutonium/ui/form/theme.rb +17 -0
- data/lib/plutonium/ui/form/wizard.rb +25 -1
- data/lib/plutonium/ui/grid/card.rb +79 -11
- data/lib/plutonium/ui/grid/resource.rb +47 -5
- data/lib/plutonium/ui/interaction/async/run_progress.rb +227 -0
- data/lib/plutonium/ui/interaction/async/running_banner.rb +65 -0
- data/lib/plutonium/ui/kanban/card.rb +2 -1
- data/lib/plutonium/ui/kanban/column.rb +12 -6
- data/lib/plutonium/ui/kanban/resource.rb +6 -7
- data/lib/plutonium/ui/layout/base.rb +10 -3
- data/lib/plutonium/ui/nav_grid_menu.rb +1 -0
- data/lib/plutonium/ui/page/base.rb +19 -0
- data/lib/plutonium/ui/page/edit.rb +4 -1
- data/lib/plutonium/ui/page/index.rb +69 -18
- data/lib/plutonium/ui/page/interactive_action.rb +5 -1
- data/lib/plutonium/ui/page/new.rb +4 -1
- data/lib/plutonium/ui/page/show.rb +27 -10
- data/lib/plutonium/ui/page/wizard.rb +10 -1
- data/lib/plutonium/ui/page/wizard_chooser.rb +36 -11
- data/lib/plutonium/ui/page_width.rb +58 -0
- data/lib/plutonium/ui/table/base.rb +34 -1
- data/lib/plutonium/ui/table/components/attachment.rb +1 -1
- data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +32 -8
- data/lib/plutonium/ui/table/components/drag_handle.rb +120 -0
- data/lib/plutonium/ui/table/components/filter_form.rb +1 -4
- data/lib/plutonium/ui/table/components/filter_pills.rb +1 -1
- data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +1 -1
- data/lib/plutonium/ui/table/resource.rb +50 -5
- data/lib/plutonium/ui/table/theme.rb +59 -2
- data/lib/plutonium/ui/wizard/review.rb +4 -2
- data/lib/plutonium/ui/wizard/summary_display.rb +42 -14
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium/wizard/attachments.rb +32 -197
- data/lib/plutonium/wizard/base.rb +6 -1
- data/lib/plutonium/wizard/configuration.rb +12 -0
- data/lib/plutonium/wizard/controller.rb +14 -0
- data/lib/plutonium/wizard/driving.rb +99 -15
- data/lib/plutonium/wizard/dsl.rb +23 -0
- data/lib/plutonium/wizard/resume.rb +127 -49
- data/lib/plutonium/wizard/runner.rb +46 -2
- data/lib/plutonium/wizard/step_adapter.rb +1 -1
- data/lib/plutonium/wizard/sweep_job.rb +16 -0
- data/lib/plutonium.rb +21 -0
- data/lib/rodauth/features/session_isolation.rb +92 -0
- data/lib/rodauth/plugins.rb +1 -0
- data/package.json +2 -1
- data/plutonium.gemspec +20 -20
- data/src/css/components.css +89 -1
- data/src/css/slim_select.css +20 -0
- data/src/js/controllers/breadcrumbs_controller.js +112 -0
- data/src/js/controllers/bulk_actions_controller.js +10 -2
- data/src/js/controllers/kanban_controller.js +30 -21
- data/src/js/controllers/positioned_controller.js +452 -0
- data/src/js/controllers/register_controllers.js +6 -0
- data/src/js/controllers/resource_drop_down_controller.js +5 -0
- data/src/js/controllers/run_progress_controller.js +73 -0
- data/src/js/drag/sortable.js +186 -0
- data/yarn.lock +108 -63
- metadata +101 -22
- data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +0 -53
- data/docs/.vitepress/theme/components/HomePillars.vue +0 -42
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"planPath": "docs/superpowers/plans/2026-08-17-async-interactions.md",
|
|
3
|
+
"tasks": [
|
|
4
|
+
{
|
|
5
|
+
"id": 0,
|
|
6
|
+
"subject": "Task 1: Config, migration and registration",
|
|
7
|
+
"status": "completed",
|
|
8
|
+
"description": "**Goal:** The plutonium_interaction_runs table exists in a host app when the feature is enabled, following the wizard subsystem's pattern.\n\n**Verify:** bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/runs/configuration_test.rb\n\n```json:metadata\n{\"files\": [\"lib/plutonium/interaction/runs/configuration.rb\", \"db/migrate/interaction_runs/20260817000001_create_plutonium_interaction_runs.rb\", \"lib/plutonium/railtie.rb\", \"lib/plutonium/configuration.rb\"], \"verifyCommand\": \"bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/runs/configuration_test.rb\", \"acceptanceCriteria\": [\"enabled defaults to false\", \"migration path surfaced only when enabled\", \"polymorphic *_id columns are string\"], \"requiresUserVerification\": false}\n```"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": 1,
|
|
12
|
+
"subject": "Task 2: The Run model",
|
|
13
|
+
"status": "completed",
|
|
14
|
+
"blockedBy": [
|
|
15
|
+
0
|
|
16
|
+
],
|
|
17
|
+
"description": "**Goal:** STI base with state transitions, progress accounting and typed options.\n\n**Verify:** bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/run_test.rb\n\n```json:metadata\n{\"files\": [\"lib/plutonium/interaction/run.rb\"], \"verifyCommand\": \"bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/run_test.rb\", \"acceptanceCriteria\": [\"start!/finish!/fail! move state\", \"progress_fraction nil for opaque work\", \"record_target_failure! accumulates\", \"targeted? reflects perform_on\"], \"requiresUserVerification\": false}\n```"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": 2,
|
|
21
|
+
"subject": "Task 3: Authorization context and target resolution",
|
|
22
|
+
"status": "completed",
|
|
23
|
+
"blockedBy": [
|
|
24
|
+
1
|
|
25
|
+
],
|
|
26
|
+
"description": "**Goal:** Rebuild (initiator, scoped_entity) in the job and resolve targets through the policy scope. SECURITY CORE \u2014 both failure modes fail open.\n\n**Verify:** bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/runs/context_test.rb\n\n```json:metadata\n{\"files\": [\"lib/plutonium/interaction/runs/context.rb\", \"test/dummy/app/runs/test_post_run.rb\"], \"verifyCommand\": \"bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/runs/context_test.rb\", \"acceptanceCriteria\": [\"targets resolve through the policy scope\", \"cross-tenant target not returned\", \"vanished target reported not dropped\", \"single query\"], \"requiresUserVerification\": false}\n```"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": 3,
|
|
30
|
+
"subject": "Task 4: Executor and failure policies",
|
|
31
|
+
"status": "completed",
|
|
32
|
+
"blockedBy": [
|
|
33
|
+
2
|
|
34
|
+
],
|
|
35
|
+
"description": "**Goal:** Run the work, honour on_failure (continue/halt/transactional), keep progress current.\n\n**Verify:** bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/runs/executor_test.rb\n\n```json:metadata\n{\"files\": [\"lib/plutonium/interaction/runs/executor.rb\", \"lib/plutonium/interaction/runs/job.rb\"], \"verifyCommand\": \"bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/runs/executor_test.rb\", \"acceptanceCriteria\": [\"continue records and proceeds\", \"halt stops at first error\", \"transactional rolls back\", \"missing targets recorded\", \"opaque run performs once\"], \"requiresUserVerification\": false}\n```"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": 4,
|
|
39
|
+
"subject": "Task 5: Interactions dispatch",
|
|
40
|
+
"status": "completed",
|
|
41
|
+
"blockedBy": [
|
|
42
|
+
3
|
|
43
|
+
],
|
|
44
|
+
"description": "**Goal:** dispatches_to creates and enqueues a run, then redirects. Outcome stays synchronous.\n\n**Verify:** bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/dispatchable_test.rb\n\n```json:metadata\n{\"files\": [\"lib/plutonium/interaction/concerns/dispatchable.rb\", \"lib/plutonium/interaction/base.rb\"], \"verifyCommand\": \"bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/dispatchable_test.rb\", \"acceptanceCriteria\": [\"run created with validated options\", \"initiator and scoped_entity persisted\", \"success outcome redirects to run\", \"invalid interaction enqueues nothing\"], \"requiresUserVerification\": false}\n```"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": 5,
|
|
48
|
+
"subject": "Task 6: The run as a resource, and its progress page",
|
|
49
|
+
"status": "completed",
|
|
50
|
+
"blockedBy": [
|
|
51
|
+
4
|
|
52
|
+
],
|
|
53
|
+
"description": "**Goal:** Portal-registerable run resource whose show page is the progress page and refreshes itself while in progress.\n\n**Verify:** bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/runs/policy_test.rb test/integration/admin_portal/interaction_run_progress_test.rb\n\n```json:metadata\n{\"files\": [\"lib/plutonium/interaction/runs/definition.rb\", \"lib/plutonium/interaction/runs/policy.rb\", \"lib/plutonium/ui/interaction/run_progress.rb\", \"test/dummy/packages/admin_portal/config/routes.rb\"], \"verifyCommand\": \"bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/runs/policy_test.rb test/integration/admin_portal/interaction_run_progress_test.rb\", \"acceptanceCriteria\": [\"relation_scope limits to entity scope\", \"cross-tenant run not visible\", \"in-progress page polls, finished does not\", \"indeterminate progress renders a spinner\"], \"requiresUserVerification\": false}\n```"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": 6,
|
|
57
|
+
"subject": "Task 7: In-progress runs on the target index",
|
|
58
|
+
"status": "completed",
|
|
59
|
+
"blockedBy": [
|
|
60
|
+
5
|
|
61
|
+
],
|
|
62
|
+
"description": "**Goal:** A resource's index lists runs currently working on it, above the collection.\n\n**Verify:** bundle exec appraisal rails-8.1 ruby -Itest test/integration/admin_portal/interaction_run_banner_test.rb\n\n```json:metadata\n{\"files\": [\"lib/plutonium/ui/interaction/running_banner.rb\", \"lib/plutonium/ui/page/index.rb\"], \"verifyCommand\": \"bundle exec appraisal rails-8.1 ruby -Itest test/integration/admin_portal/interaction_run_banner_test.rb\", \"acceptanceCriteria\": [\"lists in-progress runs for this resource\", \"excludes completed and other-resource runs\", \"excludes other tenants\", \"no banner when none running\"], \"requiresUserVerification\": false}\n```"
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"lastUpdated": "2026-08-17T13:42:04.578647"
|
|
66
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Homepage Depth & Proof Upgrade — Design
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-16
|
|
4
|
+
**Status:** Approved
|
|
5
|
+
**Scope:** VitePress docs homepage (`docs/index.md` + `docs/.vitepress/theme/components/Home*.vue`)
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
The homepage looks good but undersells the product. Two confirmed weaknesses
|
|
10
|
+
(vs Avo/ActiveAdmin/Administrate, Filament/Refine/AdminJS, and Jumpstart/Bullet Train):
|
|
11
|
+
|
|
12
|
+
1. **Feature depth is hidden.** Wizards, kanban boards, custom/bulk actions, and
|
|
13
|
+
multi-tenancy/nesting barely appear. The surface area reads smaller than it is.
|
|
14
|
+
2. **Not enough proof.** Claims outnumber evidence — three CRUD screenshots and one
|
|
15
|
+
asciinema cast carry the whole page.
|
|
16
|
+
|
|
17
|
+
Constraint: the overall look (design system, section rhythm, dark-terminal aesthetic)
|
|
18
|
+
stays. No hosted demo. No new screenshot production — reuse existing guide assets.
|
|
19
|
+
|
|
20
|
+
## Approved changes
|
|
21
|
+
|
|
22
|
+
New page order:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
Hero → Stop writing → Walkthrough → Feature tour (NEW) → Why Plutonium (NEW, merged) → In the box → CTA
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 1. `HomeFeatureTour.vue` (new section)
|
|
29
|
+
|
|
30
|
+
Slots between `HomeWalkthrough` and the merged band.
|
|
31
|
+
|
|
32
|
+
- **Heading:** "More than CRUD." Sub: "Four features other frameworks make you
|
|
33
|
+
build yourself. All declarative. All policy-aware."
|
|
34
|
+
- **Desktop (>768px):** left vertical rail listing the four features, each with a
|
|
35
|
+
one-line hook; right panel shows the selected feature. Selected rail item gets the
|
|
36
|
+
accent left-border treatment (matches `--pu-accent`).
|
|
37
|
+
- **Mobile (≤768px):** the rail reflows into an accordion — all four heads always
|
|
38
|
+
visible, one open at a time, same panel content inside.
|
|
39
|
+
- **Panel anatomy (each feature):**
|
|
40
|
+
1. File-path caption bar (e.g. `app/definitions/task_definition.rb`)
|
|
41
|
+
2. DSL code snippet, dark terminal styling (`--pu-bg-dark`), syntax-highlighted
|
|
42
|
+
3. Real screenshot below the code, reused from `docs/public/images/guides/`
|
|
43
|
+
4. A one-line "policy-aware" callout: the button/column/step disappears when the
|
|
44
|
+
policy says no
|
|
45
|
+
5. "Read the guide →" link to the relevant guide page
|
|
46
|
+
|
|
47
|
+
| Rail item | Hook | Snippet | Screenshot | Guide link |
|
|
48
|
+
|---|---|---|---|---|
|
|
49
|
+
| Kanban boards | drag-drop from one block | `kanban do … end` column DSL (todo/doing/done, `wip:`, `role:`) | `guides/kanban-board.png` | `/guides/kanban` |
|
|
50
|
+
| Wizards | multi-step flows, branching | trimmed `CompanyOnboardingWizard` (2 steps + `review` + `execute`) | `guides/wizards-step.png` | `/guides/wizards` |
|
|
51
|
+
| Actions & interactions | business logic + auto UI | `action :publish, interaction: PublishPostInteraction` + `def publish? = update? && record.draft?` | `guides/custom-actions-bulk.png` | `/guides/custom-actions` |
|
|
52
|
+
| Multi-tenancy & nesting | scoping, invites, nesting | entity-scoping snippet (`scoped_to_entity` / portal strategy) | `guides/multi-tenancy-dashboard.png` | `/guides/multi-tenancy` |
|
|
53
|
+
|
|
54
|
+
- Interactivity via a small Vue `ref` for the selected feature (desktop rail and
|
|
55
|
+
mobile accordion share state). No new dependencies.
|
|
56
|
+
- Note: the wizards guide labels the feature experimental; the tour panel copy should
|
|
57
|
+
not contradict that (avoid "stable", fine to omit the caveat).
|
|
58
|
+
|
|
59
|
+
### 2. `HomeWhyPlutonium.vue` (new, replaces `HomePillars` + `HomeAudienceSplit`)
|
|
60
|
+
|
|
61
|
+
One `pu-section--band` section that merges both messages, cutting a full section of
|
|
62
|
+
page height:
|
|
63
|
+
|
|
64
|
+
- **Top:** the four pillar cards in compact form — Convention over configuration /
|
|
65
|
+
It's just Rails / Multi-tenant ready / AI-readable — same icons and card styling
|
|
66
|
+
as today, tighter padding.
|
|
67
|
+
- **Bottom:** a two-column footer row inside the same section:
|
|
68
|
+
- "For Rails developers" — existing lede + 2 bullets: "Convention extended to
|
|
69
|
+
CRUD, policies, and portals" and "Generated code lives in your repo — edit anything"
|
|
70
|
+
- "For founders & teams" — existing lede + 2 bullets: "Admin panel, signup, and
|
|
71
|
+
invites on day one" and "Multi-tenant scoping when you need it"
|
|
72
|
+
- `HomePillars.vue` and `HomeAudienceSplit.vue` are deleted; `docs/index.md` updated.
|
|
73
|
+
|
|
74
|
+
### 3. Copy-only weaves (existing components)
|
|
75
|
+
|
|
76
|
+
- **`HomeHero.vue`:** pillars line gains **Wizards.** and **Kanban.** in the bold list.
|
|
77
|
+
- **`HomeStopWriting.vue`:** Plutonium win column gains `+ Actions` (accurate — the
|
|
78
|
+
scaffold ships action wiring).
|
|
79
|
+
- **`HomeInTheBox.vue`:** new fourth category row **Workflows** with three links:
|
|
80
|
+
Wizards (`/guides/wizards`), Kanban (`/guides/kanban`), Interactions
|
|
81
|
+
(`/guides/custom-actions` — there is no separate interactions guide).
|
|
82
|
+
|
|
83
|
+
### 4. Out of scope
|
|
84
|
+
|
|
85
|
+
- Hosted live demo
|
|
86
|
+
- New screenshot/asset production
|
|
87
|
+
- Full-spine restructure (each feature as a full-width alternating panel) —
|
|
88
|
+
considered and superseded by the rail-tour + tightened-middle structure
|
|
89
|
+
- Credibility signals (testimonials, star counts) and quantified-value claims —
|
|
90
|
+
explicitly not the weak spots being addressed
|
|
91
|
+
|
|
92
|
+
## Error handling / edge cases
|
|
93
|
+
|
|
94
|
+
- Screenshots must be referenced with `withBase(...)` like existing components.
|
|
95
|
+
- Rail/accordion is progressive: SSR renders the first feature expanded so the
|
|
96
|
+
section is meaningful without JS.
|
|
97
|
+
- Accordion heads are `<button>`s with `aria-expanded`; rail follows the existing
|
|
98
|
+
`role="tablist"` pattern used by `HomeCta.vue` pills.
|
|
99
|
+
|
|
100
|
+
## Testing / verification
|
|
101
|
+
|
|
102
|
+
- `yarn docs:dev` visual pass at desktop and ≤768px widths (rail ↔ accordion reflow).
|
|
103
|
+
- `yarn docs:build` must pass (dead-link check catches wrong guide paths).
|
|
104
|
+
- Verify all four screenshots resolve under the site base path in the built output.
|
|
105
|
+
|
|
106
|
+
## Decisions log
|
|
107
|
+
|
|
108
|
+
- Tour layout: feature rail (chosen over tabbed showcase) — breadth visible at a glance
|
|
109
|
+
- Mobile: accordion (chosen over horizontal pill strip) — same breadth rationale
|
|
110
|
+
- Phase 2 pulled forward, resolved as structure A: rail tour + merged middle band
|
|
111
|
+
(chosen over full-spine panels)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Native `link:` / `button:` HTML attributes on actions
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-17
|
|
4
|
+
**Status:** Approved (design)
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
On released Plutonium (≤0.62.2) there is no native way to put `target`, `rel`, or
|
|
9
|
+
arbitrary HTML attributes on an action's rendered control. `Action::Base` reads a
|
|
10
|
+
fixed key set in `initialize`, so any extra option passed to an action is silently
|
|
11
|
+
inert — not stored, never rendered. `ActionButton`'s three render paths
|
|
12
|
+
(`render_link`, `render_button`, `render_dropdown_item`) hardcode their attributes.
|
|
13
|
+
|
|
14
|
+
Concrete need: an action that opens an external link in a new tab
|
|
15
|
+
(`target: "_blank" rel: "noopener noreferrer"`), or that carries custom
|
|
16
|
+
`data-*` / `aria-*` attributes.
|
|
17
|
+
|
|
18
|
+
## API
|
|
19
|
+
|
|
20
|
+
Two target-named attribute bags on the action DSL, each naming the **element**
|
|
21
|
+
it applies to. No merged/"always" bag — `link:` covers every anchor rendering
|
|
22
|
+
and `button:` covers the button_to form, so per-element bags cover everything
|
|
23
|
+
with no "which element?" ambiguity.
|
|
24
|
+
|
|
25
|
+
The element rule, not HTTP method, decides which bag applies: dropdown items
|
|
26
|
+
are anchors even for non-GET actions (they submit via `data-turbo-method`), so
|
|
27
|
+
`link:` applies to them regardless of method, and `button:` applies only to the
|
|
28
|
+
toolbar's button_to rendering. A non-GET action that needs attributes in both
|
|
29
|
+
placements sets both bags.
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
action :docs,
|
|
33
|
+
link: { target: "_blank", rel: "noopener noreferrer", data: { analytics: "docs" } },
|
|
34
|
+
button: { data: { turbo_confirm: "Sure?" } }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- **`link:`** → every `<a>` rendered for the action: the GET toolbar link,
|
|
38
|
+
dropdown items (any method), bulk-actions toolbar links, kanban column
|
|
39
|
+
action links, and the grid/kanban card's hidden show anchor (for `:show`).
|
|
40
|
+
- **`button:`** → the `button_to` **`<form>`** element, in `render_button` (non-GET),
|
|
41
|
+
via `button_to`'s `form:` option. The form wrapper, not the inner `<button>`.
|
|
42
|
+
- Both default to `{}`. `target`/`rel` are just keys inside `link:` — no first-class
|
|
43
|
+
sugar, nothing special-cased.
|
|
44
|
+
|
|
45
|
+
## Storage — `Action::Base`
|
|
46
|
+
|
|
47
|
+
- New ivars `@link` / `@button` in `initialize`, each normalized on the way in:
|
|
48
|
+
the action stores its **own deep-symbolized copy** of the bag (never freezing
|
|
49
|
+
or sharing the caller's hash), structurally deep-frozen. Symbolizing is
|
|
50
|
+
load-bearing: the render-time `deep_merge` matches keys exactly, so a
|
|
51
|
+
string-keyed bag would sit *alongside* the framework's symbol keys and emit
|
|
52
|
+
duplicate attributes (browser keeps the first — the framework's) instead of
|
|
53
|
+
overriding. Any non-Hash value — including `false` — raises `ArgumentError`.
|
|
54
|
+
- Matching `attr_reader :link, :button`, plus the merge points
|
|
55
|
+
`link_attributes(base)` / `button_attributes(base)`: rendering surfaces hand
|
|
56
|
+
the framework-built attribute hash to the action, which deep-merges its bag
|
|
57
|
+
over it (author wins). Surfaces never merge the raw bags themselves.
|
|
58
|
+
- Both added to `to_options` so `with(**overrides)` round-trips them (miss this and
|
|
59
|
+
`.with` silently drops them — the one non-negotiable).
|
|
60
|
+
- `Action::Interactive` already forwards `**options` to `super`, so the bags flow
|
|
61
|
+
through interactive actions automatically. No change needed there.
|
|
62
|
+
- Verify during implementation that the definition-level `action` /
|
|
63
|
+
`interactive_action` DSL forwards unknown kwargs into the action constructor
|
|
64
|
+
(it splats `**options` into `Action::Simple.new` / `Interactive::Factory.create`,
|
|
65
|
+
so it should — confirm, don't assume).
|
|
66
|
+
|
|
67
|
+
## Rendering
|
|
68
|
+
|
|
69
|
+
Merge rule everywhere: build the framework's attribute hash as the base, then
|
|
70
|
+
hand it to the action's merge point (`action.link_attributes(base)` /
|
|
71
|
+
`action.button_attributes(base)`) — **the author wins on every key**,
|
|
72
|
+
recursively through nested `data`. This gives authors full control (they can
|
|
73
|
+
override `turbo_frame`, `class`, `data-*`, anything) at their own risk.
|
|
74
|
+
|
|
75
|
+
Surfaces funnelling through the merge points:
|
|
76
|
+
|
|
77
|
+
- **`ActionButton#render_link`** — base
|
|
78
|
+
`{ class: button_classes, data: { turbo_frame: … }.merge(@extra_data) }`
|
|
79
|
+
through `link_attributes`, passed to `link_to`.
|
|
80
|
+
- **`ActionButton#render_dropdown_item`** — the existing `link_attrs` hash
|
|
81
|
+
through `link_attributes` before `a(**…)`. Applies to any method — dropdown
|
|
82
|
+
items are anchors; `button:` never applies here.
|
|
83
|
+
- **`ActionButton#render_button`** — the `form:` option hash passed to
|
|
84
|
+
`button_to` through `button_attributes`. So `button: { target: "_top" }` sets
|
|
85
|
+
the form's target; `button: { data: { … } }` deep-merges into the form's data.
|
|
86
|
+
- **`BulkActionsToolbar#render_action_button`** — the bulk-action anchor's
|
|
87
|
+
attrs through `link_attributes`.
|
|
88
|
+
- **`Kanban::Column#column_action_link_attributes`** — the column-action
|
|
89
|
+
anchor's attrs through `link_attributes` (of the registered action).
|
|
90
|
+
- **`Grid::Card#render_show_link`** — the hidden row-click show anchor's attrs
|
|
91
|
+
through the `:show` action's `link_attributes`.
|
|
92
|
+
|
|
93
|
+
## Tests
|
|
94
|
+
|
|
95
|
+
- `test/plutonium/action/base_test.rb`:
|
|
96
|
+
- `link` / `button` readers default to `{}` and return what was passed.
|
|
97
|
+
- `to_options` includes both bags; `with(link: …)` round-trips and overrides.
|
|
98
|
+
- `ActionButton` rendering tests:
|
|
99
|
+
- `link:` attributes (`target`, `rel`, custom `data-*`) land on the `<a>` in the
|
|
100
|
+
GET-link and dropdown renderings.
|
|
101
|
+
- `button:` attributes land on the `<form>` in the non-GET rendering.
|
|
102
|
+
- Author wins on collision: an author `data.turbo_frame` overrides the
|
|
103
|
+
framework's.
|
|
104
|
+
|
|
105
|
+
## Scope guard (out)
|
|
106
|
+
|
|
107
|
+
- No third "always" / `html:` bag.
|
|
108
|
+
- No attribute-name validation — authors own their HTML; a bad attribute is their
|
|
109
|
+
problem. (Container shape IS validated: a non-Hash bag raises; keys are
|
|
110
|
+
symbolized so string-keyed bags can't silently lose the merge.)
|
|
111
|
+
- No per-element targeting *within* a rendering (e.g. the inner `<button>` vs the
|
|
112
|
+
`<form>`) — `button:` targets the form only. Can be extended later if needed.
|
|
113
|
+
|
|
114
|
+
## Known footguns (documented, accepted)
|
|
115
|
+
|
|
116
|
+
- `deep_merge` only recurses when **both** values are hashes: an author scalar
|
|
117
|
+
`data:` (`button: { data: "x" }`) wholesale-replaces the framework's data hash,
|
|
118
|
+
deleting `turbo_confirm`/`turbo_frame`. Pass `data:` as a hash. Same mechanics
|
|
119
|
+
mean `class:` **replaces** the framework's classes (no token append) — that's
|
|
120
|
+
the author-wins contract working as designed.
|
|
121
|
+
- `button:` lands on the `<form>` wrapper, not the visible `<button>`: click-
|
|
122
|
+
element attributes (`aria-label`, `class` for the control) don't reach the
|
|
123
|
+
button element. Documented in the guide; per-element targeting is the
|
|
124
|
+
deferred extension above.
|