plutonium 0.51.0 → 0.53.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 (160) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium-app/SKILL.md +2 -0
  3. data/.claude/skills/plutonium-auth/SKILL.md +6 -4
  4. data/.claude/skills/plutonium-behavior/SKILL.md +1 -1
  5. data/.claude/skills/plutonium-resource/SKILL.md +6 -4
  6. data/.claude/skills/plutonium-tenancy/SKILL.md +31 -7
  7. data/.claude/skills/plutonium-testing/SKILL.md +3 -1
  8. data/.claude/skills/plutonium-ui/SKILL.md +32 -8
  9. data/CHANGELOG.md +33 -0
  10. data/app/assets/plutonium.css +1 -1
  11. data/app/assets/plutonium.js +258 -11
  12. data/app/assets/plutonium.js.map +4 -4
  13. data/app/assets/plutonium.min.js +39 -39
  14. data/app/assets/plutonium.min.js.map +4 -4
  15. data/app/views/plutonium/_resource_header.html.erb +2 -1
  16. data/docs/.vitepress/config.ts +2 -2
  17. data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +53 -0
  18. data/docs/.vitepress/theme/components/HomeCta.vue +108 -0
  19. data/docs/.vitepress/theme/components/HomeHero.vue +70 -0
  20. data/docs/.vitepress/theme/components/HomeInTheBox.vue +74 -0
  21. data/docs/.vitepress/theme/components/HomePillars.vue +42 -0
  22. data/docs/.vitepress/theme/components/HomeStopWriting.vue +49 -0
  23. data/docs/.vitepress/theme/components/HomeWalkthrough.vue +111 -0
  24. data/docs/.vitepress/theme/components/SectionLanding.vue +115 -0
  25. data/docs/.vitepress/theme/custom.css +144 -0
  26. data/docs/.vitepress/theme/index.ts +58 -1
  27. data/docs/getting-started/index.md +33 -50
  28. data/docs/getting-started/tutorial/02-first-resource.md +17 -8
  29. data/docs/getting-started/tutorial/03-authentication.md +31 -23
  30. data/docs/getting-started/tutorial/05-custom-actions.md +9 -4
  31. data/docs/getting-started/tutorial/06-nested-resources.md +7 -1
  32. data/docs/getting-started/tutorial/07-author-portal.md +8 -0
  33. data/docs/getting-started/tutorial/08-customizing-ui.md +4 -0
  34. data/docs/guides/authentication.md +11 -6
  35. data/docs/guides/authorization.md +3 -3
  36. data/docs/guides/creating-packages.md +8 -11
  37. data/docs/guides/custom-actions.md +8 -2
  38. data/docs/guides/customizing-ui.md +259 -0
  39. data/docs/guides/index.md +49 -32
  40. data/docs/guides/multi-tenancy.md +14 -6
  41. data/docs/guides/nested-resources.md +69 -0
  42. data/docs/guides/search-filtering.md +6 -0
  43. data/docs/guides/testing.md +5 -1
  44. data/docs/guides/theming.md +14 -1
  45. data/docs/guides/user-invites.md +10 -4
  46. data/docs/guides/user-profile.md +8 -0
  47. data/docs/index.md +10 -219
  48. data/docs/public/asciinema/home-scaffold.cast +305 -0
  49. data/docs/public/images/components/avatar.png +0 -0
  50. data/docs/public/images/guides/custom-actions-bulk.png +0 -0
  51. data/docs/public/images/guides/multi-tenancy-dashboard.png +0 -0
  52. data/docs/public/images/guides/multi-tenancy-welcome.png +0 -0
  53. data/docs/public/images/guides/nested-inputs.png +0 -0
  54. data/docs/public/images/guides/nested-resources-tab.png +0 -0
  55. data/docs/public/images/guides/search-filtering-index.png +0 -0
  56. data/docs/public/images/guides/search-filtering-panel.png +0 -0
  57. data/docs/public/images/guides/theming-after.png +0 -0
  58. data/docs/public/images/guides/theming-before.png +0 -0
  59. data/docs/public/images/guides/user-invites-landing.png +0 -0
  60. data/docs/public/images/guides/user-profile-edit.png +0 -0
  61. data/docs/public/images/guides/user-profile-show.png +0 -0
  62. data/docs/public/images/home-index.png +0 -0
  63. data/docs/public/images/home-new.png +0 -0
  64. data/docs/public/images/home-show.png +0 -0
  65. data/docs/public/images/tutorial/02-empty-index.png +0 -0
  66. data/docs/public/images/tutorial/02-index-with-posts.png +0 -0
  67. data/docs/public/images/tutorial/02-new-form-modal.png +0 -0
  68. data/docs/public/images/tutorial/02-new-form.png +0 -0
  69. data/docs/public/images/tutorial/03-create-account.png +0 -0
  70. data/docs/public/images/tutorial/03-login.png +0 -0
  71. data/docs/public/images/tutorial/04-admin-index.png +0 -0
  72. data/docs/public/images/tutorial/05-actions-menu.png +0 -0
  73. data/docs/public/images/tutorial/05-row-actions.png +0 -0
  74. data/docs/public/images/tutorial/06-comments-tab.png +0 -0
  75. data/docs/public/images/tutorial/06-post-with-comments.png +0 -0
  76. data/docs/public/images/tutorial/07-author-dashboard.png +0 -0
  77. data/docs/public/images/tutorial/07-author-portal.png +0 -0
  78. data/docs/public/images/tutorial/08-customized-index.png +0 -0
  79. data/docs/reference/app/generators.md +4 -4
  80. data/docs/reference/auth/accounts.md +7 -8
  81. data/docs/reference/auth/index.md +1 -1
  82. data/docs/reference/behavior/policies.md +2 -2
  83. data/docs/reference/configuration.md +61 -0
  84. data/docs/reference/index.md +67 -55
  85. data/docs/reference/resource/actions.md +2 -1
  86. data/docs/reference/resource/definition.md +5 -4
  87. data/docs/reference/tenancy/entity-scoping.md +14 -8
  88. data/docs/reference/tenancy/index.md +1 -1
  89. data/docs/reference/tenancy/invites.md +12 -5
  90. data/docs/reference/ui/components.md +53 -0
  91. data/docs/reference/ui/forms.md +1 -1
  92. data/docs/reference/ui/pages.md +6 -5
  93. data/docs/reference/ui/tables.md +8 -4
  94. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md +1648 -0
  95. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md.tasks.json +109 -0
  96. data/docs/superpowers/specs/2026-05-15-public-pages-overhaul-design.md +263 -0
  97. data/docs/superpowers/specs/2026-05-29-avatar-component-design.md +153 -0
  98. data/gemfiles/rails_7.gemfile.lock +1 -1
  99. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  100. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  101. data/lib/generators/pu/core/assets/assets_generator.rb +10 -0
  102. data/lib/generators/pu/invites/install_generator.rb +44 -0
  103. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +1 -0
  104. data/lib/generators/pu/lite/solid_errors/solid_errors_generator.rb +7 -3
  105. data/lib/generators/pu/profile/conn_generator.rb +2 -2
  106. data/lib/generators/pu/res/conn/conn_generator.rb +33 -6
  107. data/lib/generators/pu/res/model/templates/model.rb.tt +4 -0
  108. data/lib/generators/pu/rodauth/account_generator.rb +2 -1
  109. data/lib/generators/pu/rodauth/admin_generator.rb +0 -2
  110. data/lib/generators/pu/rodauth/migration_generator.rb +0 -2
  111. data/lib/generators/pu/rodauth/views_generator.rb +0 -2
  112. data/lib/generators/pu/saas/membership/USAGE +4 -1
  113. data/lib/generators/pu/saas/setup_generator.rb +16 -4
  114. data/lib/generators/pu/saas/welcome/templates/app/controllers/welcome_controller.rb.tt +1 -1
  115. data/lib/plutonium/action/base.rb +43 -63
  116. data/lib/plutonium/configuration.rb +7 -0
  117. data/lib/plutonium/definition/actions.rb +10 -11
  118. data/lib/plutonium/definition/base.rb +29 -0
  119. data/lib/plutonium/helpers/assets_helper.rb +0 -30
  120. data/lib/plutonium/helpers/content_helper.rb +0 -44
  121. data/lib/plutonium/helpers/display_helper.rb +0 -62
  122. data/lib/plutonium/helpers/turbo_helper.rb +17 -2
  123. data/lib/plutonium/helpers.rb +0 -2
  124. data/lib/plutonium/resource/controllers/crud_actions.rb +4 -4
  125. data/lib/plutonium/resource/controllers/interactive_actions.rb +3 -3
  126. data/lib/plutonium/resource/definition.rb +0 -42
  127. data/lib/plutonium/ui/action_button.rb +4 -3
  128. data/lib/plutonium/ui/avatar.rb +182 -0
  129. data/lib/plutonium/ui/component/kit.rb +2 -0
  130. data/lib/plutonium/ui/component/methods.rb +1 -0
  131. data/lib/plutonium/ui/form/base.rb +32 -2
  132. data/lib/plutonium/ui/form/components/secure_association.rb +14 -8
  133. data/lib/plutonium/ui/form/interaction.rb +1 -1
  134. data/lib/plutonium/ui/form/resource.rb +58 -0
  135. data/lib/plutonium/ui/form/theme.rb +8 -4
  136. data/lib/plutonium/ui/grid/card.rb +10 -26
  137. data/lib/plutonium/ui/modal/base.rb +36 -1
  138. data/lib/plutonium/ui/modal/centered.rb +24 -6
  139. data/lib/plutonium/ui/modal/slideover.rb +26 -11
  140. data/lib/plutonium/ui/nav_user.rb +3 -23
  141. data/lib/plutonium/ui/page/edit.rb +7 -4
  142. data/lib/plutonium/ui/page/interactive_action.rb +5 -3
  143. data/lib/plutonium/ui/page/new.rb +7 -4
  144. data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +1 -1
  145. data/lib/plutonium/ui/table/components/filter_form.rb +12 -4
  146. data/lib/plutonium/version.rb +1 -1
  147. data/package.json +4 -1
  148. data/src/css/components.css +38 -1
  149. data/src/css/slim_select.css +3 -2
  150. data/src/js/controllers/dirty_form_guard_controller.js +165 -0
  151. data/src/js/controllers/form_controller.js +5 -4
  152. data/src/js/controllers/register_controllers.js +2 -0
  153. data/src/js/controllers/remote_modal_controller.js +53 -19
  154. data/src/js/turbo/index.js +1 -0
  155. data/src/js/turbo/turbo_confirm.js +128 -0
  156. data/yarn.lock +108 -1
  157. metadata +52 -6
  158. data/lib/plutonium/helpers/attachment_helper.rb +0 -73
  159. data/lib/plutonium/helpers/table_helper.rb +0 -35
  160. /data/lib/generators/pu/rodauth/templates/app/views/rodauth_mailer/{password_changed.text.erb → change_password_notify.text.erb} +0 -0
@@ -0,0 +1,109 @@
1
+ {
2
+ "planPath": "docs/superpowers/plans/2026-05-15-public-pages-overhaul.md",
3
+ "tasks": [
4
+ {
5
+ "id": 1,
6
+ "subject": "Task 0: Shared CSS tokens and primitives",
7
+ "status": "pending",
8
+ "description": "**Goal:** Add design-system tokens to docs/.vitepress/theme/custom.css.\n\n```json:metadata\n{\"files\":[\"docs/.vitepress/theme/custom.css\"],\"verifyCommand\":\"yarn docs:dev\",\"acceptanceCriteria\":[\"tokens defined\",\"primitives render\",\"no regressions\"],\"requiresUserVerification\":false}\n```"
9
+ },
10
+ {
11
+ "id": 2,
12
+ "subject": "Task 1: HomeHero component",
13
+ "status": "pending",
14
+ "blockedBy": [1],
15
+ "description": "**Goal:** Build the locked hero.\n\n```json:metadata\n{\"files\":[\"docs/.vitepress/theme/components/HomeHero.vue\",\"docs/.vitepress/theme/index.ts\",\"docs/index.md\"],\"verifyCommand\":\"yarn docs:dev\",\"acceptanceCriteria\":[\"hero renders\",\"cursor blinks\",\"responsive\",\"CTAs work\"],\"requiresUserVerification\":false}\n```"
16
+ },
17
+ {
18
+ "id": 3,
19
+ "subject": "Task 2: HomeStopWriting (Section 1)",
20
+ "status": "pending",
21
+ "blockedBy": [1, 2],
22
+ "description": "**Goal:** Surface-area before/after.\n\n```json:metadata\n{\"files\":[\"docs/.vitepress/theme/components/HomeStopWriting.vue\",\"docs/.vitepress/theme/index.ts\",\"docs/index.md\"],\"verifyCommand\":\"yarn docs:dev\",\"acceptanceCriteria\":[\"section renders\",\"stats correct\",\"responsive\"],\"requiresUserVerification\":false}\n```"
23
+ },
24
+ {
25
+ "id": 4,
26
+ "subject": "Task 3: HomePillars (Section 2)",
27
+ "status": "pending",
28
+ "blockedBy": [1, 3],
29
+ "description": "**Goal:** Four-pillar grid.\n\n```json:metadata\n{\"files\":[\"docs/.vitepress/theme/components/HomePillars.vue\",\"docs/.vitepress/theme/index.ts\",\"docs/index.md\"],\"verifyCommand\":\"yarn docs:dev\",\"acceptanceCriteria\":[\"4 pillars render\",\"copy verbatim\",\"responsive\"],\"requiresUserVerification\":false}\n```"
30
+ },
31
+ {
32
+ "id": 5,
33
+ "subject": "Task 4: HomeWalkthrough layout (Section 3)",
34
+ "status": "pending",
35
+ "blockedBy": [1, 4],
36
+ "description": "**Goal:** Walkthrough section layout with placeholders.\n\n```json:metadata\n{\"files\":[\"docs/.vitepress/theme/components/HomeWalkthrough.vue\",\"docs/.vitepress/theme/index.ts\",\"docs/index.md\"],\"verifyCommand\":\"yarn docs:dev\",\"acceptanceCriteria\":[\"layout reserves space\",\"labels visible\"],\"requiresUserVerification\":false}\n```"
37
+ },
38
+ {
39
+ "id": 6,
40
+ "subject": "Task 5: HomeAudienceSplit (Section 4)",
41
+ "status": "pending",
42
+ "blockedBy": [1, 5],
43
+ "description": "**Goal:** Side-by-side audience split.\n\n```json:metadata\n{\"files\":[\"docs/.vitepress/theme/components/HomeAudienceSplit.vue\",\"docs/.vitepress/theme/index.ts\",\"docs/index.md\"],\"verifyCommand\":\"yarn docs:dev\",\"acceptanceCriteria\":[\"two columns\",\"locked copy\",\"responsive\"],\"requiresUserVerification\":false}\n```"
44
+ },
45
+ {
46
+ "id": 7,
47
+ "subject": "Task 6: HomeInTheBox (Section 5)",
48
+ "status": "pending",
49
+ "blockedBy": [1, 6],
50
+ "description": "**Goal:** Categorized in-the-box section.\n\n```json:metadata\n{\"files\":[\"docs/.vitepress/theme/components/HomeInTheBox.vue\",\"docs/.vitepress/theme/index.ts\",\"docs/index.md\"],\"verifyCommand\":\"yarn docs:dev\",\"acceptanceCriteria\":[\"3 rows render\",\"copy matches spec\"],\"requiresUserVerification\":false}\n```"
51
+ },
52
+ {
53
+ "id": 8,
54
+ "subject": "Task 7: HomeCta with template-toggle pills (Section 6)",
55
+ "status": "pending",
56
+ "blockedBy": [1, 7],
57
+ "description": "**Goal:** Manifesto CTA with reactive template-toggle pills.\n\n```json:metadata\n{\"files\":[\"docs/.vitepress/theme/components/HomeCta.vue\",\"docs/.vitepress/theme/index.ts\",\"docs/index.md\"],\"verifyCommand\":\"yarn docs:dev\",\"acceptanceCriteria\":[\"pills toggle\",\"URLs correct\",\"CTAs link\"],\"requiresUserVerification\":false}\n```"
58
+ },
59
+ {
60
+ "id": 9,
61
+ "subject": "Task 8: SectionLanding shared component",
62
+ "status": "pending",
63
+ "blockedBy": [1],
64
+ "description": "**Goal:** Reusable rail+sidebar layout for section landings.\n\n```json:metadata\n{\"files\":[\"docs/.vitepress/theme/components/SectionLanding.vue\",\"docs/.vitepress/theme/index.ts\"],\"verifyCommand\":\"yarn docs:dev\",\"acceptanceCriteria\":[\"numbered renders\",\"categorized renders\",\"responsive\"],\"requiresUserVerification\":false}\n```"
65
+ },
66
+ {
67
+ "id": 10,
68
+ "subject": "Task 9: Getting Started landing",
69
+ "status": "pending",
70
+ "blockedBy": [9],
71
+ "description": "**Goal:** Rewrite getting-started/index.md.\n\n```json:metadata\n{\"files\":[\"docs/getting-started/index.md\"],\"verifyCommand\":\"yarn docs:dev\",\"acceptanceCriteria\":[\"8 steps render\",\"links resolve\",\"prereqs preserved\"],\"requiresUserVerification\":false}\n```"
72
+ },
73
+ {
74
+ "id": 11,
75
+ "subject": "Task 10: Guides landing",
76
+ "status": "pending",
77
+ "blockedBy": [9],
78
+ "description": "**Goal:** Rewrite guides/index.md.\n\n```json:metadata\n{\"files\":[\"docs/guides/index.md\"],\"verifyCommand\":\"yarn docs:dev\",\"acceptanceCriteria\":[\"categories render\",\"links resolve\"],\"requiresUserVerification\":false}\n```"
79
+ },
80
+ {
81
+ "id": 12,
82
+ "subject": "Task 11: Reference landing",
83
+ "status": "pending",
84
+ "blockedBy": [9],
85
+ "description": "**Goal:** Rewrite reference/index.md.\n\n```json:metadata\n{\"files\":[\"docs/reference/index.md\"],\"verifyCommand\":\"yarn docs:dev\",\"acceptanceCriteria\":[\"categories match real sidebar\",\"links resolve\"],\"requiresUserVerification\":false}\n```"
86
+ },
87
+ {
88
+ "id": 13,
89
+ "subject": "Task 12: Demo app + asciinema + screenshots",
90
+ "status": "pending",
91
+ "description": "**Goal:** Produce walkthrough assets from a fresh demo app.\n\n```json:metadata\n{\"files\":[\"docs/public/images/home-portal.png\",\"docs/public/images/home-index.png\",\"docs/public/images/home-form.png\",\"docs/public/asciinema/home-scaffold.cast\"],\"verifyCommand\":\"ls -la docs/public/images/home-*.png docs/public/asciinema/home-scaffold.cast\",\"acceptanceCriteria\":[\"4 assets exist\",\"non-empty\",\"correct format\"],\"requiresUserVerification\":false}\n```"
92
+ },
93
+ {
94
+ "id": 14,
95
+ "subject": "Task 13: Wire assets into HomeWalkthrough",
96
+ "status": "pending",
97
+ "blockedBy": [5, 13],
98
+ "description": "**Goal:** Replace placeholders with real screenshots + asciinema embed.\n\n```json:metadata\n{\"files\":[\"docs/.vitepress/theme/components/HomeWalkthrough.vue\",\"docs/.vitepress/theme/index.ts\"],\"verifyCommand\":\"yarn docs:dev\",\"acceptanceCriteria\":[\"screenshots render\",\"asciinema plays\",\"no console errors\"],\"requiresUserVerification\":false}\n```"
99
+ },
100
+ {
101
+ "id": 15,
102
+ "subject": "Task 14: Visual sweep + user verification",
103
+ "status": "pending",
104
+ "blockedBy": [8, 10, 11, 12, 14],
105
+ "description": "**Goal:** Cross-cutting sweep and user sign-off.\n\n```json:metadata\n{\"files\":[],\"verifyCommand\":\"yarn docs:build\",\"acceptanceCriteria\":[\"all 4 pages render in both modes\",\"no console errors\",\"build succeeds\",\"user approved\"],\"requiresUserVerification\":true,\"userVerificationPrompt\":\"All four public pages are live in `yarn docs:dev`. Have you reviewed Home, Getting Started, Guides, and Reference and are they ready to ship?\"}\n```"
106
+ }
107
+ ],
108
+ "lastUpdated": "2026-05-15T00:00:00Z"
109
+ }
@@ -0,0 +1,263 @@
1
+ # Public Pages Overhaul — Design
2
+
3
+ **Date:** 2026-05-15
4
+ **Scope:** `docs/index.md`, `docs/getting-started/index.md`, `docs/guides/index.md`, `docs/reference/index.md`
5
+ **Goal:** Overhaul the four public landing pages of the Plutonium docs site. Blend Filament's polish with Rails' editorial voice. Address two audiences (Rails developers, founders/teams) without choosing one. Make AI-readiness an equal pillar, not the headline.
6
+
7
+ ---
8
+
9
+ ## Direction at a glance
10
+
11
+ - **Voice:** plain & honest, peer-to-peer to Rails devs, accessible to founders. No hyped numerals, no "0% AI-comprehensible" framing.
12
+ - **Polish:** Filament-style two-column hero, animated terminal, dark sections punctuated by light section bands.
13
+ - **Proof:** real screenshots captured from a fresh scaffolded demo app; asciinema recording captured during the same scaffolding session.
14
+ - **Sequence (home):** proof-first arc — show what it does, then why it's built that way, then who it's for.
15
+
16
+ ---
17
+
18
+ ## Home page (`docs/index.md`)
19
+
20
+ Seven blocks. Hero first, then six numbered sections. Section ordering is fixed.
21
+
22
+ ### 0. Hero — locked
23
+
24
+ ```
25
+ ┌────────────────────────────────────────────────────────────────┐
26
+ │ PLUTONIUM · THE RAILS RAD FRAMEWORK │
27
+ │ │
28
+ │ The Rails framework ┌────────────────────┐ │
29
+ │ for things you should never │ $ rails g pu:res: │ │
30
+ │ write again. │ scaffold Post … │ │
31
+ │ │ $ rails g pu:res: │ │
32
+ │ Convention over configuration, │ conn Post --dest │ │
33
+ │ extended to everything you │ =admin_portal │ │
34
+ │ keep rebuilding. │ $ _ │ │
35
+ │ └────────────────────┘ │
36
+ │ CRUD. Auth. Authorization. Multi- │
37
+ │ tenancy. Admin portals. Search, │
38
+ │ filters, bulk actions. All generated. │
39
+ │ All customizable. All Rails. │
40
+ │ │
41
+ │ [ Get started → ] [ 15-min tutorial ] │
42
+ └────────────────────────────────────────────────────────────────┘
43
+ ```
44
+
45
+ - **Layout:** dark background (`#0d1117`), two columns. Left: eyebrow → headline → lede → pillar list → CTAs. Right: animated terminal (asciinema embed once recorded; CSS-animated placeholder until then).
46
+ - **Eyebrow:** `PLUTONIUM · THE RAILS RAD FRAMEWORK`
47
+ - **Headline:** "The Rails framework for things you should never write again."
48
+ - **Lede:** "Convention over configuration, extended to everything you keep rebuilding."
49
+ - **Pillar line:** "**CRUD. Auth. Authorization. Multi-tenancy. Admin portals. Search, filters, bulk actions.** All generated. All customizable. All Rails."
50
+ - **CTAs:** primary "Get started →" → `/getting-started/`; ghost "15-min tutorial" → `/getting-started/tutorial/`.
51
+ - **Terminal content:**
52
+ ```
53
+ $ rails g pu:res:scaffold Post title:string body:text
54
+ create app/models/post.rb
55
+ create app/resource_registries/post_definition.rb
56
+ create db/migrate/...create_posts.rb
57
+ $ rails g pu:res:conn Post --dest=admin_portal
58
+ route resource :posts
59
+ ✓ Connected Post to AdminPortal
60
+ $ _
61
+ ```
62
+
63
+ ### 1. What you stop writing — capability comparison
64
+
65
+ Surface-area before/after. Compares **what's included** rather than lines or time. Two columns side-by-side: hand-rolled file tree (with "before search, filters, bulk actions, auth…" trailing) vs Plutonium two-command terminal. Stat row below each side compares capabilities (Just CRUD / No auth / No search vs Full CRUD / + Auth / + Search / + Filters / + Bulk actions).
66
+
67
+ Section title: "What you stop writing."
68
+ Subtitle: "A blog with posts, comments, an admin panel, and authorization. Same feature, two paths."
69
+
70
+ ### 2. Four pillars
71
+
72
+ Plain & honest naming. Four equal cards in a 4-column grid. Light section band.
73
+
74
+ - **Convention over configuration** — Extended to resources, policies, portals, and tenancy — not just routes and views.
75
+ - **It's just Rails** — Generated code lives in your repo. Edit it, override it, delete it. The "magic" is regular Ruby mixins you can read.
76
+ - **Multi-tenant ready** — Path or domain tenancy. Scoped relations. Invites and memberships out of the box.
77
+ - **AI-readable** — Predictable file layout and naming. Built-in skills teach AI assistants the patterns.
78
+
79
+ Section title: "Built on principles, not magic."
80
+
81
+ ### 3. A real example, walked through
82
+
83
+ Hero portal shot up top (wide), then a 3-column strip below: asciinema terminal · index page screenshot · form page screenshot.
84
+
85
+ - **Wide shot:** the portal layout with sidebar/nav and the posts index visible.
86
+ - **Asciinema:** the scaffold commands captured during the demo-app creation (see Assets section).
87
+ - **Index screenshot:** `/admin/posts` table view.
88
+ - **Form screenshot:** `/admin/posts/new` auto-generated form.
89
+
90
+ Section title: "Two commands. A whole portal."
91
+
92
+ ### 4. For Rails devs / For founders
93
+
94
+ Side-by-side audience split. Two columns of equal weight separated by a thin divider.
95
+
96
+ **Left — For Rails developers**
97
+ > The missing layer between Rails and the apps you keep building.
98
+ - Convention extended to CRUD, policies, and portals
99
+ - Generated code lives in your repo — edit anything
100
+ - Mountable Rails engines for packages and portals
101
+ - ActionPolicy authorization, baked in
102
+
103
+ **Right — For founders & teams**
104
+ > Skip the SaaS template debate. Plutonium turns Rails into a SaaS toolkit.
105
+ - Admin panel, signup, and invites on day one
106
+ - Multi-tenant scoping when you need it
107
+ - No template lock-in — it's just your Rails app
108
+ - Ship faster with AI tools that understand your code
109
+
110
+ Section title: "Plutonium fits two kinds of teams."
111
+
112
+ ### 5. What's in the box — categorized
113
+
114
+ Three category rows. Each row has a small uppercase header (red `#d33`) and a 3-column grid of capabilities. Each capability has a bold name and a one-line description.
115
+
116
+ - **Resources** — Scaffolds · Search & filters · Custom & bulk actions
117
+ - **App structure** — Portals · Packages · Multi-tenancy
118
+ - **People & access** — Auth (Rodauth) · Authorization · Invites & memberships
119
+
120
+ Section title: "Organized the way you'll use it."
121
+
122
+ ### 6. CTA — manifesto close
123
+
124
+ Centered block with subtle gradient background.
125
+
126
+ - **Line:** *"Stop writing the parts of every Rails app you've already written. Plutonium is what should have been there all along."*
127
+ - **Template toggle pills (centered):**
128
+ - `plutonium` — *core + portals*
129
+ - `pluton8` — *+ SaaS lite stack*
130
+ - **Install command (changes with selected pill):**
131
+ - `plutonium`: `rails new my_app -m https://radioactive-labs.github.io/plutonium-core/templates/plutonium.rb`
132
+ - `pluton8`: `rails new my_app -m https://radioactive-labs.github.io/plutonium-core/templates/pluton8.rb`
133
+ - **CTAs:** primary "Get started →" → `/getting-started/`; ghost "GitHub" → repo.
134
+
135
+ The pill toggle is a small Vue component (the docs site is VitePress, not a Plutonium app — so no Stimulus). Swaps the install command line on click.
136
+
137
+ ---
138
+
139
+ ## Section landings (`getting-started/`, `guides/`, `reference/`)
140
+
141
+ All three use **Pattern B — numbered rail with sidebar**. Eyebrow + h1 + lede above; below, a two-column layout: left rail = numbered/categorized list of the section's content (vertical red bar), right sidebar = shortcuts and help.
142
+
143
+ ### Getting Started (`docs/getting-started/index.md`)
144
+
145
+ - **Eyebrow:** GETTING STARTED
146
+ - **H1:** "Get a working Plutonium app in 15 minutes."
147
+ - **Lede:** "Walk the path top to bottom, or skip to the part you need."
148
+ - **Left rail (numbered steps from the tutorial):**
149
+ 1. Project setup
150
+ 2. First resource
151
+ 3. Authentication
152
+ 4. Authorization
153
+ 5. Custom actions
154
+ 6. Nested resources
155
+ 7. Author portal
156
+ 8. Customizing UI
157
+ - **Right sidebar:**
158
+ - *Already know your way around?* — Installation · Concepts overview · Generators reference
159
+ - *Need help?* — GitHub Discussions · Open an issue
160
+
161
+ ### Guides (`docs/guides/index.md`)
162
+
163
+ - **Eyebrow:** GUIDES
164
+ - **H1:** "How to do the things Plutonium apps do."
165
+ - **Lede:** "Task-oriented walkthroughs for the parts of the framework you reach for most."
166
+ - **Left rail (categorized, not numbered — uses category headers between groups):**
167
+ - Setup & Resources — Adding Resources · Creating Packages
168
+ - Auth — Authentication · Authorization
169
+ - Features — Custom Actions · Nested Resources · Multi-tenancy · Search & Filtering · User Invites
170
+ - Customization — Theming
171
+ - Quality — Testing
172
+ - **Right sidebar:**
173
+ - *New to Plutonium?* — Start with the tutorial
174
+ - *Looking for APIs?* — Browse the reference
175
+ - *Need help?* — GitHub Discussions
176
+
177
+ ### Reference (`docs/reference/index.md`)
178
+
179
+ - **Eyebrow:** REFERENCE
180
+ - **H1:** "Every API, in one place."
181
+ - **Lede:** "The full surface area of Plutonium — controllers, policies, definitions, fields, interactions, generators."
182
+ - **Left rail (categorized, not numbered):**
183
+ - App — Overview · Packages · Portals
184
+ - Resource — Definitions · Fields · Policies · Controllers · Interactions
185
+ - UI — Pages · Forms · Tables · Displays
186
+ - Tooling — Generators · Testing helpers
187
+ - **Right sidebar:**
188
+ - *Learning?* — Tutorial · Concepts
189
+ - *Solving a problem?* — Guides
190
+ - *Need help?* — GitHub Discussions
191
+
192
+ ---
193
+
194
+ ## Visual system
195
+
196
+ Reused across all four pages so they feel like one product.
197
+
198
+ - **Type:** system sans, headlines with `-0.02em` to `-0.025em` letter-spacing for tightness.
199
+ - **Color tokens:**
200
+ - Dark surface: `#0d1117`
201
+ - Light surface: `#fff`
202
+ - Subtle band: `#fafafa`
203
+ - Border: `#e0e0e0`
204
+ - Muted text: `#666`
205
+ - Accent (red): `#d33` (matches existing Plutonium brand)
206
+ - Success pill: `#ecf9f1` / `#0a7c3f`
207
+ - **Terminal block:** rounded 6–8px, `#161b22` body on dark sections, `#0d1117` on light sections, `#7ee787` for prompts, `#58a6ff` cursor.
208
+ - **Animated cursor:** blink every 1s.
209
+ - **Buttons:** primary red (`#d33` bg, white text); ghost (transparent, border, current color).
210
+ - **Eyebrow style:** 11–12px uppercase, `0.1em` letter-spacing.
211
+
212
+ These tokens should be added as CSS variables in `docs/.vitepress/theme/custom.css` (or wherever existing landing styles live) so they're shareable across pages.
213
+
214
+ ---
215
+
216
+ ## Assets to produce
217
+
218
+ 1. **Demo Rails app for screenshots + asciinema** — scaffold a fresh Rails app via the `plutonium` template, generate `Post(title:string, body:text, published:boolean)` and `Comment(post:references, body:text)`, connect both to an `AdminPortal`. Capture asciinema during this scaffold + boot session.
219
+
220
+ 2. **Asciinema recording** — saved alongside the page assets (likely `docs/public/asciinema/` or `docs/public/images/`). Trim to ~30s. Loop. Embedded via asciinema-player or a CSS-animated SVG/gif fallback.
221
+
222
+ 3. **Screenshots (3)** —
223
+ - `home-portal.png` — wide shot of the portal layout with sidebar + posts index visible
224
+ - `home-index.png` — close shot of `/admin/posts` table
225
+ - `home-form.png` — close shot of `/admin/posts/new` form
226
+
227
+ All captured at consistent viewport size (e.g., 1280×800), light mode, with seeded data (3–5 posts including a draft to show the boolean pill).
228
+
229
+ ---
230
+
231
+ ## Interaction
232
+
233
+ Only one interactive element across the four pages:
234
+
235
+ - **Template toggle pills** in the home-page CTA. Swaps between `plutonium.rb` and `pluton8.rb` install commands. Implementation: a Vue component registered in `docs/.vitepress/theme/index.ts` (the standard VitePress theme extension point), used in `docs/index.md` via `<TemplateToggle />`.
236
+
237
+ Everything else is static.
238
+
239
+ ---
240
+
241
+ ## Structure of work
242
+
243
+ Each piece below is independently buildable.
244
+
245
+ 1. **CSS theme tokens** — add the variables, button styles, terminal block, eyebrow class to the VitePress custom CSS. Foundation for everything else.
246
+ 2. **Home hero + section 1** — through the first proof block.
247
+ 3. **Home sections 2–4** — pillars, walkthrough placeholders (asciinema/screenshots come last), audience split.
248
+ 4. **Home sections 5–6** — in-the-box grid, manifesto CTA with template-toggle pills + Stimulus controller.
249
+ 5. **Getting Started landing** — Pattern B page.
250
+ 6. **Guides landing** — Pattern B page.
251
+ 7. **Reference landing** — Pattern B page.
252
+ 8. **Demo app + asciinema + screenshots** — produce the assets. Can run in parallel to 1–7.
253
+ 9. **Wire assets into home section 3** — replace placeholders.
254
+
255
+ ---
256
+
257
+ ## Out of scope
258
+
259
+ - Inner pages (tutorial chapters, individual guides, reference subpages) — only the four landing pages are in scope.
260
+ - Vitepress theme switch (light/dark toggle) — keep current behavior.
261
+ - Logo, favicon, brand mark changes.
262
+ - Pricing, blog, changelog — no such pages exist; not adding them here.
263
+ - Search UX, sidebar/nav changes — those live in `.vitepress/config.ts` and aren't part of this overhaul.
@@ -0,0 +1,153 @@
1
+ # Avatar Component (Navii fallback) — Design
2
+
3
+ **Date:** 2026-05-29
4
+ **Status:** Approved, ready for implementation plan
5
+
6
+ ## Summary
7
+
8
+ Add a reusable `Plutonium::UI::Avatar` Phlex component that renders a profile
9
+ image from an optional attachment/uploader/URL, and falls back to a
10
+ deterministic avatar generated by the hosted [Navii](https://navii.dev) service
11
+ when no image is supplied. Adopt it in `NavUser` and `Card`.
12
+
13
+ ## Motivation
14
+
15
+ `NavUser` currently renders a supplied `avatar_url` or a generic User icon, and
16
+ `Card`'s `:image` slot resolves attachments/URLs but has no fallback. There is no
17
+ shared, reusable way to render a profile image with a sensible default. Navii
18
+ provides deterministic, zero-dependency avatars via a simple image URL, which
19
+ fits a server-rendered Phlex component cleanly.
20
+
21
+ ## Integration approach
22
+
23
+ **Hosted URL, server-rendered.** Render a plain `<img>` whose `src` points at the
24
+ Navii hosted API. No npm dependency, no Stimulus controller, no build step, fully
25
+ SSR-friendly.
26
+
27
+ Navii URL shape: `https://api.navii.dev/avatar/{seed}?size={px}` (SVG by default,
28
+ which scales cleanly inside an `<img>`).
29
+
30
+ ## Component API
31
+
32
+ New component: `lib/plutonium/ui/avatar.rb` → `Plutonium::UI::Avatar`, extending
33
+ `Plutonium::UI::Component::Base`, sitting alongside `NavUser` as a general UI
34
+ primitive. Registered in `Component::Kit` as `Avatar(...)`.
35
+
36
+ ```ruby
37
+ Avatar(user) # → Navii fallback seeded from the record
38
+ Avatar(user, src: :photo) # → user.photo if present, else Navii fallback
39
+ Avatar(user, src: user.photo) # → pass the attachment/uploader/URL directly
40
+ Avatar("acme-team") # → String subject = literal seed
41
+ Avatar(src: "https://.../p.png") # → bare image, no subject/fallback
42
+ ```
43
+
44
+ ### Parameters
45
+
46
+ | Param | Default | Notes |
47
+ |------------|---------|-------|
48
+ | `subject` | `nil` | **Positional.** The identity the fallback is seeded from: a record (hashed to a PII-free seed) or a String (used verbatim). Also the default `alt`. |
49
+ | `src:` | `nil` | The image. A Symbol names a method on the subject (`:avatar` → `subject.avatar`); otherwise an ActiveStorage attachment, active_shrine/Shrine uploader, or URL string. |
50
+ | `size:` | `:md` | Semantic `:xs 24 / :sm 32 / :md 40 / :lg 48 / :xl 64`, or a raw Integer (px). Drives the Navii `?size=`, the `<img>` width/height, and the `w-/h-` classes. |
51
+ | `alt:` | derived | Defaults to the String subject, or `display_name_of(record)`. |
52
+ | `class:` | — | Merged over the default `rounded-full` classes (passthrough). |
53
+
54
+ The earlier `record:`/`seed:` split collapsed into the single positional
55
+ `subject` (record **or** String), and `size:` moved from raw pixels to a
56
+ semantic scale (Integer still accepted as an escape hatch).
57
+
58
+ ## Resolution + seed logic
59
+
60
+ 1. **Resolve `src`** using the logic currently in `card.rb`'s `image_src_for`:
61
+ - ActiveStorage (`responds_to? :attached?`) → `helpers.url_for(value)` when attached, else `nil`
62
+ - Uploader (`responds_to? :url`) → `value.url`
63
+ - String starting with `http` or `/` → passthrough
64
+ - rescue `ArgumentError`/`URI::InvalidURIError` → `nil`
65
+
66
+ This method is **extracted into the Avatar component** (or a shared resolver
67
+ module) and `Card` delegates to it, removing the current duplication.
68
+
69
+ 2. If `src` resolves to a URL → render it.
70
+
71
+ 3. Otherwise build a Navii URL from the seed.
72
+
73
+ ### Seed derivation (no PII)
74
+
75
+ The value sent to Navii is **always** a short SHA256 hash, regardless of subject
76
+ type — no plaintext ever leaves the app:
77
+
78
+ - Record `subject` → identity `"#{record.class.name}:#{record.id}"`, then hashed.
79
+ - String `subject` → the string is hashed (not sent verbatim).
80
+ - Final seed = `Digest::SHA256.hexdigest(identity)[0, 16]`.
81
+
82
+ This keeps model names, raw IDs, emails, and any caller-provided seed string out
83
+ of the URL that reaches the external service, while preserving determinism (same
84
+ identity → same hash → same avatar).
85
+
86
+ ### Navii host configuration
87
+
88
+ Add `config.navii_host_url` to `lib/plutonium/configuration.rb`, defaulting to
89
+ `https://api.navii.dev` (host only — the component appends the `/avatar/:seed`
90
+ route, which is Navii's API shape). Lets apps self-host or repoint the service
91
+ without code changes.
92
+
93
+ ## Rendering
94
+
95
+ A single `<img>`:
96
+ - `src` = resolved image URL or Navii URL
97
+ - default classes `rounded-full object-cover`, plus the semantic `w-/h-` size
98
+ class (so Tailwind preflight's `img { height: auto }` doesn't collapse the
99
+ dimensions), with caller `class:` merged over
100
+ - `width` / `height` = pixel size; raw-Integer sizes also emit an inline
101
+ `width/height` style
102
+ - `loading: "lazy"`
103
+ - `alt:` from `alt:` or the subject
104
+
105
+ No JavaScript; fully server-rendered.
106
+
107
+ ### Last-resort guard
108
+
109
+ If there is no resolvable `src` **and** no `subject` to derive a seed from, fall
110
+ back to a generic User icon (sized to match) rather than emitting a broken Navii
111
+ URL. In all other cases a Navii avatar is rendered (the generic-icon branch in
112
+ `NavUser` is otherwise removed).
113
+
114
+ ## Integration points
115
+
116
+ ### NavUser (`lib/plutonium/ui/nav_user.rb`)
117
+
118
+ - Add a `record:` parameter, passed `current_user` from
119
+ `app/views/plutonium/_resource_header.html.erb`.
120
+ - Replace the inline `<img>` / icon-fallback branch in `render_trigger_button`
121
+ with `Avatar(record, src: avatar_url, size: :sm, ...)`.
122
+ - `avatar_url:` continues to work as an explicit override.
123
+ - The generic-User-icon fallback is removed (now handled by the component's
124
+ last-resort guard only).
125
+
126
+ ### Card (`lib/plutonium/ui/grid/card.rb`)
127
+
128
+ - The small `:image` slot renders `Avatar(src: value, size: :lg)` — **no
129
+ subject is passed**, so an image-less card falls back to Avatar's generic
130
+ icon rather than firing a per-card request to the external Navii service
131
+ (which would mean one third-party request per record in a grid). The
132
+ `:cover` banner stays a plain `<img>` (no avatar fallback).
133
+ - The `:cover` branch calls `Avatar.resolve_image_src` directly; the old
134
+ `image_src_for` helper (duplicated resolution logic) is removed.
135
+
136
+ ## Testing
137
+
138
+ Unit tests for `Avatar` (`test/plutonium/ui/avatar_test.rb`):
139
+ - `resolve_image_src` paths: nil, URL strings, uploader `#url`, ActiveStorage via
140
+ `url_for`, and active_shrine-style wrappers (attached?+url) via `#url`
141
+ - `src` precedence; Symbol `src` sent to the subject; Symbol `src` with no subject
142
+ - record seed determinism + absence of PII; String subject as literal seed (+escaping)
143
+ - semantic size → `?size=`, `w-/h-` classes, `width`/`height`; Integer size → inline style
144
+ - `alt` default vs explicit
145
+ - last-resort icon when there is neither `src` nor subject (and record without id)
146
+ - configured Navii base URL
147
+
148
+ ## Out of scope (YAGNI)
149
+
150
+ - Client-side `@usenavii` SDK / Stimulus rendering
151
+ - Self-hosted/vendored generation
152
+ - Non-circular shapes, status badges, image cropping/upload UI
153
+ - Caching / proxying Navii responses
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.50.0)
4
+ plutonium (0.52.0)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 43.0)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.50.0)
4
+ plutonium (0.52.0)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 43.0)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.50.0)
4
+ plutonium (0.52.0)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 43.0)
@@ -30,6 +30,8 @@ module Pu
30
30
  end
31
31
 
32
32
  def install_dependencies
33
+ failed = []
34
+
33
35
  [
34
36
  "@radioactive-labs/plutonium",
35
37
  "postcss", "postcss-cli", "postcss-import",
@@ -38,9 +40,17 @@ module Pu
38
40
  "flowbite-typography"
39
41
  ].each do |package|
40
42
  run "yarn add #{package}"
43
+ failed << "yarn add #{package}" unless $?.success?
41
44
  end
42
45
 
43
46
  run "yarn upgrade tailwindcss --latest"
47
+ failed << "yarn upgrade tailwindcss --latest" unless $?.success?
48
+
49
+ return if failed.empty?
50
+
51
+ say_status :warn,
52
+ "the following commands failed — your app may not boot until you resolve them:\n #{failed.join("\n ")}",
53
+ :yellow
44
54
  end
45
55
 
46
56
  def configure_application
@@ -429,12 +429,56 @@ module Pu
429
429
  say_status :info, "Integrated invite check into WelcomeController", :green
430
430
  end
431
431
 
432
+ def verify_active_record_encryption
433
+ return if behavior == :revoke
434
+ return if active_record_encryption_configured?
435
+
436
+ say_status :warning, "ActiveRecord encryption keys are not configured", :yellow
437
+ say <<~MSG.indent(2)
438
+
439
+ Invite tokens use `encrypts :token, deterministic: true`. Without keys,
440
+ creating or accepting an invite raises
441
+ ActiveRecord::Encryption::Errors::Configuration.
442
+
443
+ Generate keys and add them to credentials with:
444
+
445
+ bin/rails db:encryption:init
446
+
447
+ Then paste the printed `active_record_encryption:` block into
448
+ config/credentials.yml.enc (or set the equivalent ENV vars in
449
+ production).
450
+
451
+ MSG
452
+ end
453
+
432
454
  def show_instructions
433
455
  readme "INSTRUCTIONS" if behavior == :invoke
434
456
  end
435
457
 
436
458
  private
437
459
 
460
+ # Detect AR encryption configuration without booting the host app.
461
+ # Returns true if credentials.yml(.enc) has the keys, or if all three
462
+ # ENV vars are set. False otherwise — we'd rather over-warn than miss.
463
+ def active_record_encryption_configured?
464
+ env_keys = %w[
465
+ ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY
466
+ ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY
467
+ ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT
468
+ ]
469
+ return true if env_keys.all? { |k| ENV[k].to_s.length > 0 }
470
+
471
+ creds_path = Rails.root.join("config/credentials.yml.enc")
472
+ return false unless creds_path.exist?
473
+
474
+ decrypted = begin
475
+ Rails.application.credentials.config
476
+ rescue
477
+ nil
478
+ end
479
+ decrypted&.dig(:active_record_encryption, :primary_key).to_s.length > 0
480
+ end
481
+
438
482
  def entity_model
439
483
  options[:entity_model].camelize
440
484
  end
@@ -7,6 +7,7 @@ module Invites
7
7
  <% end -%>
8
8
  include Plutonium::Invites::Controller
9
9
 
10
+ prepend_view_path Invites::Engine.root.join("app/views")
10
11
  layout "invites/invitation"
11
12
  helper_method :login_path
12
13