plutonium 0.51.0 → 0.52.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 (120) 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-tenancy/SKILL.md +25 -6
  6. data/.claude/skills/plutonium-testing/SKILL.md +3 -1
  7. data/.claude/skills/plutonium-ui/SKILL.md +3 -3
  8. data/CHANGELOG.md +17 -0
  9. data/app/assets/plutonium.css +1 -1
  10. data/app/assets/plutonium.js +1 -0
  11. data/app/assets/plutonium.js.map +3 -3
  12. data/app/assets/plutonium.min.js +1 -1
  13. data/app/assets/plutonium.min.js.map +3 -3
  14. data/docs/.vitepress/config.ts +1 -2
  15. data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +53 -0
  16. data/docs/.vitepress/theme/components/HomeCta.vue +108 -0
  17. data/docs/.vitepress/theme/components/HomeHero.vue +70 -0
  18. data/docs/.vitepress/theme/components/HomeInTheBox.vue +74 -0
  19. data/docs/.vitepress/theme/components/HomePillars.vue +42 -0
  20. data/docs/.vitepress/theme/components/HomeStopWriting.vue +49 -0
  21. data/docs/.vitepress/theme/components/HomeWalkthrough.vue +111 -0
  22. data/docs/.vitepress/theme/components/SectionLanding.vue +115 -0
  23. data/docs/.vitepress/theme/custom.css +144 -0
  24. data/docs/.vitepress/theme/index.ts +58 -1
  25. data/docs/getting-started/index.md +33 -50
  26. data/docs/getting-started/tutorial/02-first-resource.md +17 -8
  27. data/docs/getting-started/tutorial/03-authentication.md +31 -23
  28. data/docs/getting-started/tutorial/05-custom-actions.md +9 -4
  29. data/docs/getting-started/tutorial/06-nested-resources.md +7 -1
  30. data/docs/getting-started/tutorial/07-author-portal.md +8 -0
  31. data/docs/getting-started/tutorial/08-customizing-ui.md +4 -0
  32. data/docs/guides/authentication.md +10 -5
  33. data/docs/guides/authorization.md +3 -3
  34. data/docs/guides/creating-packages.md +8 -11
  35. data/docs/guides/custom-actions.md +6 -1
  36. data/docs/guides/customizing-ui.md +258 -0
  37. data/docs/guides/index.md +49 -32
  38. data/docs/guides/multi-tenancy.md +10 -2
  39. data/docs/guides/nested-resources.md +69 -0
  40. data/docs/guides/search-filtering.md +6 -0
  41. data/docs/guides/testing.md +5 -1
  42. data/docs/guides/theming.md +13 -0
  43. data/docs/guides/user-invites.md +10 -4
  44. data/docs/guides/user-profile.md +8 -0
  45. data/docs/index.md +10 -219
  46. data/docs/public/asciinema/home-scaffold.cast +305 -0
  47. data/docs/public/images/guides/custom-actions-bulk.png +0 -0
  48. data/docs/public/images/guides/multi-tenancy-dashboard.png +0 -0
  49. data/docs/public/images/guides/multi-tenancy-welcome.png +0 -0
  50. data/docs/public/images/guides/nested-inputs.png +0 -0
  51. data/docs/public/images/guides/nested-resources-tab.png +0 -0
  52. data/docs/public/images/guides/search-filtering-index.png +0 -0
  53. data/docs/public/images/guides/search-filtering-panel.png +0 -0
  54. data/docs/public/images/guides/theming-after.png +0 -0
  55. data/docs/public/images/guides/theming-before.png +0 -0
  56. data/docs/public/images/guides/user-invites-landing.png +0 -0
  57. data/docs/public/images/guides/user-profile-edit.png +0 -0
  58. data/docs/public/images/guides/user-profile-show.png +0 -0
  59. data/docs/public/images/home-index.png +0 -0
  60. data/docs/public/images/home-new.png +0 -0
  61. data/docs/public/images/home-show.png +0 -0
  62. data/docs/public/images/tutorial/02-empty-index.png +0 -0
  63. data/docs/public/images/tutorial/02-index-with-posts.png +0 -0
  64. data/docs/public/images/tutorial/02-new-form-modal.png +0 -0
  65. data/docs/public/images/tutorial/02-new-form.png +0 -0
  66. data/docs/public/images/tutorial/03-create-account.png +0 -0
  67. data/docs/public/images/tutorial/03-login.png +0 -0
  68. data/docs/public/images/tutorial/04-admin-index.png +0 -0
  69. data/docs/public/images/tutorial/05-actions-menu.png +0 -0
  70. data/docs/public/images/tutorial/05-row-actions.png +0 -0
  71. data/docs/public/images/tutorial/06-comments-tab.png +0 -0
  72. data/docs/public/images/tutorial/06-post-with-comments.png +0 -0
  73. data/docs/public/images/tutorial/07-author-dashboard.png +0 -0
  74. data/docs/public/images/tutorial/07-author-portal.png +0 -0
  75. data/docs/public/images/tutorial/08-customized-index.png +0 -0
  76. data/docs/reference/app/generators.md +4 -4
  77. data/docs/reference/auth/accounts.md +6 -7
  78. data/docs/reference/auth/index.md +1 -1
  79. data/docs/reference/behavior/policies.md +1 -1
  80. data/docs/reference/index.md +67 -55
  81. data/docs/reference/resource/definition.md +1 -1
  82. data/docs/reference/tenancy/entity-scoping.md +8 -1
  83. data/docs/reference/tenancy/index.md +1 -1
  84. data/docs/reference/tenancy/invites.md +12 -5
  85. data/docs/reference/ui/tables.md +8 -4
  86. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md +1648 -0
  87. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md.tasks.json +109 -0
  88. data/docs/superpowers/specs/2026-05-15-public-pages-overhaul-design.md +263 -0
  89. data/gemfiles/rails_7.gemfile.lock +1 -1
  90. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  91. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  92. data/lib/generators/pu/core/assets/assets_generator.rb +10 -0
  93. data/lib/generators/pu/invites/install_generator.rb +44 -0
  94. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +1 -0
  95. data/lib/generators/pu/profile/conn_generator.rb +2 -2
  96. data/lib/generators/pu/res/conn/conn_generator.rb +33 -6
  97. data/lib/generators/pu/res/model/templates/model.rb.tt +4 -0
  98. data/lib/generators/pu/rodauth/account_generator.rb +2 -1
  99. data/lib/generators/pu/rodauth/admin_generator.rb +0 -2
  100. data/lib/generators/pu/rodauth/migration_generator.rb +0 -2
  101. data/lib/generators/pu/rodauth/views_generator.rb +0 -2
  102. data/lib/generators/pu/saas/membership/USAGE +4 -1
  103. data/lib/generators/pu/saas/setup_generator.rb +16 -4
  104. data/lib/generators/pu/saas/welcome/templates/app/controllers/welcome_controller.rb.tt +1 -1
  105. data/lib/plutonium/helpers/turbo_helper.rb +19 -0
  106. data/lib/plutonium/resource/controllers/crud_actions.rb +4 -4
  107. data/lib/plutonium/resource/controllers/interactive_actions.rb +3 -3
  108. data/lib/plutonium/ui/component/methods.rb +1 -0
  109. data/lib/plutonium/ui/form/base.rb +17 -1
  110. data/lib/plutonium/ui/form/components/secure_association.rb +11 -6
  111. data/lib/plutonium/ui/form/interaction.rb +1 -1
  112. data/lib/plutonium/ui/form/theme.rb +1 -1
  113. data/lib/plutonium/ui/page/edit.rb +1 -1
  114. data/lib/plutonium/ui/page/new.rb +1 -1
  115. data/lib/plutonium/ui/table/components/filter_form.rb +12 -4
  116. data/lib/plutonium/version.rb +1 -1
  117. data/package.json +4 -1
  118. data/src/js/controllers/form_controller.js +5 -4
  119. data/yarn.lock +108 -1
  120. metadata +45 -3
@@ -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.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.50.0)
4
+ plutonium (0.51.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.51.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.51.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
 
@@ -51,7 +51,7 @@ module Pu
51
51
  end
52
52
 
53
53
  def customize_policy
54
- content = <<-RUBY.chomp
54
+ content = <<-RUBY
55
55
 
56
56
  # Profile is scoped to current user, not entity.
57
57
  # Note: `user` here is the policy's user method (current authenticated user),
@@ -94,7 +94,7 @@ module Pu
94
94
 
95
95
  def customize_controller
96
96
  # Set user automatically when creating profile
97
- content = <<-RUBY.chomp
97
+ content = <<-RUBY
98
98
 
99
99
  private
100
100
 
@@ -36,9 +36,7 @@ module Pu
36
36
  @resource_class = resource
37
37
 
38
38
  if app_namespace == "MainApp"
39
- insert_into_file "config/routes.rb",
40
- indent("register_resource ::#{resource}#{singular_option}\n", 2),
41
- after: /.*Rails\.application\.routes\.draw do.*\n/
39
+ register_resource_in_routes("config/routes.rb", resource)
42
40
  else
43
41
  if options[:policy] || !expected_parent_policy
44
42
  template "app/policies/resource_policy.rb",
@@ -53,9 +51,7 @@ module Pu
53
51
  template "app/controllers/resource_controller.rb",
54
52
  "packages/#{package_namespace}/app/controllers/#{package_namespace}/#{resource.pluralize.underscore}_controller.rb"
55
53
 
56
- insert_into_file "packages/#{package_namespace}/config/routes.rb",
57
- indent("register_resource ::#{resource}#{singular_option}\n", 2),
58
- before: /.*# register resources above.*/
54
+ register_resource_in_routes("packages/#{package_namespace}/config/routes.rb", resource)
59
55
  end
60
56
  end
61
57
  rescue => e
@@ -66,6 +62,37 @@ module Pu
66
62
 
67
63
  attr_reader :app_namespace, :resource_class
68
64
 
65
+ # Insert `register_resource ::<Klass>` into a routes file. Idempotent:
66
+ # skips if already present. Falls back when the conventional
67
+ # `# register resources above.` marker is missing.
68
+ def register_resource_in_routes(routes_path, resource)
69
+ line = "register_resource ::#{resource}#{singular_option}"
70
+ content = File.read(File.join(destination_root, routes_path))
71
+
72
+ if /^\s*#{Regexp.escape(line)}\b/.match?(content)
73
+ say_status :identical, "#{routes_path} already registers #{resource}", :blue
74
+ return
75
+ end
76
+
77
+ if /^\s*#\s*register resources above\b/.match?(content)
78
+ insert_into_file routes_path,
79
+ indent("#{line}\n", 2),
80
+ before: /^\s*#\s*register resources above\b.*/
81
+ elsif /^\s*Rails\.application\.routes\.draw do\b/.match?(content)
82
+ insert_into_file routes_path,
83
+ indent("#{line}\n", 2),
84
+ after: /^\s*Rails\.application\.routes\.draw do.*\n/
85
+ elsif (match = content.match(/^(\w+::Engine)\.routes\.draw do.*\n/))
86
+ insert_into_file routes_path,
87
+ indent("#{line}\n", 2),
88
+ after: /^\s*#{Regexp.escape(match[1])}\.routes\.draw do.*\n/
89
+ else
90
+ say_status :warn,
91
+ "Could not locate routes block in #{routes_path}; add manually: #{line}",
92
+ :yellow
93
+ end
94
+ end
95
+
69
96
  def package_namespace
70
97
  app_namespace.underscore
71
98
  end
@@ -36,7 +36,11 @@ class <%= class_name %> < <%= [feature_package_name, "ResourceRecord"].join "::"
36
36
 
37
37
  <% attributes.select(&:required?).each do |attribute| -%>
38
38
  <%- next if attribute.reference? || attribute.rich_text? || attribute.token? || attribute.password_digest? -%>
39
+ <%- if attribute.type == :boolean -%>
40
+ validates :<%= attribute.attribute_name %>, inclusion: {in: [true, false]}
41
+ <%- else -%>
39
42
  validates :<%= attribute.attribute_name %>, presence: true
43
+ <%- end -%>
40
44
  <% end -%>
41
45
  # add validations above.
42
46
 
@@ -4,6 +4,7 @@ require "securerandom"
4
4
  require "#{__dir__}/concerns/configuration"
5
5
  require "#{__dir__}/concerns/account_selector"
6
6
  require "#{__dir__}/concerns/feature_selector"
7
+ require "#{__dir__}/migration_generator"
7
8
  require "#{__dir__}/../lib/plutonium_generators/concerns/actions"
8
9
 
9
10
  module Pu
@@ -96,7 +97,7 @@ module Pu
96
97
  def generate_rodauth_migration
97
98
  return if selected_migration_features.empty?
98
99
 
99
- invoke "pu:rodauth:migration", [table], features: selected_migration_features,
100
+ invoke Pu::Rodauth::MigrationGenerator, [table], features: selected_migration_features,
100
101
  name: kitchen_sink? ? "rodauth_kitchen_sink" : nil,
101
102
  migration_name: options[:migration_name],
102
103
  login_column: login_column,
@@ -1,5 +1,3 @@
1
- return unless defined?(Rodauth::Rails)
2
-
3
1
  require "rails/generators/base"
4
2
 
5
3
  require_relative "../lib/plutonium_generators"
@@ -1,5 +1,3 @@
1
- return unless defined?(Rodauth::Rails)
2
-
3
1
  require "rails/generators/base"
4
2
  require "rails/generators/active_record/migration"
5
3
  require "erb"
@@ -1,5 +1,3 @@
1
- return unless defined?(Rodauth::Rails)
2
-
3
1
  require "rails/generators/base"
4
2
 
5
3
  require "#{__dir__}/concerns/configuration"
@@ -6,9 +6,12 @@ Description:
6
6
 
7
7
  Example:
8
8
  rails g pu:saas:membership --user Customer --entity Organization
9
- rails g pu:saas:membership --user Customer --entity Organization --roles=member,admin,owner
9
+ rails g pu:saas:membership --user Customer --entity Organization --roles=admin,member
10
10
  rails g pu:saas:membership --user Customer --entity Organization --extra-attributes=joined_at:datetime
11
11
 
12
+ Role ordering: `owner` is auto-prepended as index 0 (most privileged) — do not include it
13
+ in --roles. Subsequent roles run from most-privileged to least.
14
+
12
15
  This creates:
13
16
  app/models/organization_customer.rb (with role enum and uniqueness validation)
14
17
  db/migrate/XXX_create_organization_customers.rb (with unique index)