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.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium-app/SKILL.md +2 -0
- data/.claude/skills/plutonium-auth/SKILL.md +6 -4
- data/.claude/skills/plutonium-behavior/SKILL.md +1 -1
- data/.claude/skills/plutonium-tenancy/SKILL.md +25 -6
- data/.claude/skills/plutonium-testing/SKILL.md +3 -1
- data/.claude/skills/plutonium-ui/SKILL.md +3 -3
- data/CHANGELOG.md +17 -0
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +1 -0
- data/app/assets/plutonium.js.map +3 -3
- data/app/assets/plutonium.min.js +1 -1
- data/app/assets/plutonium.min.js.map +3 -3
- data/docs/.vitepress/config.ts +1 -2
- data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +53 -0
- data/docs/.vitepress/theme/components/HomeCta.vue +108 -0
- data/docs/.vitepress/theme/components/HomeHero.vue +70 -0
- data/docs/.vitepress/theme/components/HomeInTheBox.vue +74 -0
- data/docs/.vitepress/theme/components/HomePillars.vue +42 -0
- data/docs/.vitepress/theme/components/HomeStopWriting.vue +49 -0
- data/docs/.vitepress/theme/components/HomeWalkthrough.vue +111 -0
- data/docs/.vitepress/theme/components/SectionLanding.vue +115 -0
- data/docs/.vitepress/theme/custom.css +144 -0
- data/docs/.vitepress/theme/index.ts +58 -1
- data/docs/getting-started/index.md +33 -50
- data/docs/getting-started/tutorial/02-first-resource.md +17 -8
- data/docs/getting-started/tutorial/03-authentication.md +31 -23
- data/docs/getting-started/tutorial/05-custom-actions.md +9 -4
- data/docs/getting-started/tutorial/06-nested-resources.md +7 -1
- data/docs/getting-started/tutorial/07-author-portal.md +8 -0
- data/docs/getting-started/tutorial/08-customizing-ui.md +4 -0
- data/docs/guides/authentication.md +10 -5
- data/docs/guides/authorization.md +3 -3
- data/docs/guides/creating-packages.md +8 -11
- data/docs/guides/custom-actions.md +6 -1
- data/docs/guides/customizing-ui.md +258 -0
- data/docs/guides/index.md +49 -32
- data/docs/guides/multi-tenancy.md +10 -2
- data/docs/guides/nested-resources.md +69 -0
- data/docs/guides/search-filtering.md +6 -0
- data/docs/guides/testing.md +5 -1
- data/docs/guides/theming.md +13 -0
- data/docs/guides/user-invites.md +10 -4
- data/docs/guides/user-profile.md +8 -0
- data/docs/index.md +10 -219
- data/docs/public/asciinema/home-scaffold.cast +305 -0
- data/docs/public/images/guides/custom-actions-bulk.png +0 -0
- data/docs/public/images/guides/multi-tenancy-dashboard.png +0 -0
- data/docs/public/images/guides/multi-tenancy-welcome.png +0 -0
- data/docs/public/images/guides/nested-inputs.png +0 -0
- data/docs/public/images/guides/nested-resources-tab.png +0 -0
- data/docs/public/images/guides/search-filtering-index.png +0 -0
- data/docs/public/images/guides/search-filtering-panel.png +0 -0
- data/docs/public/images/guides/theming-after.png +0 -0
- data/docs/public/images/guides/theming-before.png +0 -0
- data/docs/public/images/guides/user-invites-landing.png +0 -0
- data/docs/public/images/guides/user-profile-edit.png +0 -0
- data/docs/public/images/guides/user-profile-show.png +0 -0
- data/docs/public/images/home-index.png +0 -0
- data/docs/public/images/home-new.png +0 -0
- data/docs/public/images/home-show.png +0 -0
- data/docs/public/images/tutorial/02-empty-index.png +0 -0
- data/docs/public/images/tutorial/02-index-with-posts.png +0 -0
- data/docs/public/images/tutorial/02-new-form-modal.png +0 -0
- data/docs/public/images/tutorial/02-new-form.png +0 -0
- data/docs/public/images/tutorial/03-create-account.png +0 -0
- data/docs/public/images/tutorial/03-login.png +0 -0
- data/docs/public/images/tutorial/04-admin-index.png +0 -0
- data/docs/public/images/tutorial/05-actions-menu.png +0 -0
- data/docs/public/images/tutorial/05-row-actions.png +0 -0
- data/docs/public/images/tutorial/06-comments-tab.png +0 -0
- data/docs/public/images/tutorial/06-post-with-comments.png +0 -0
- data/docs/public/images/tutorial/07-author-dashboard.png +0 -0
- data/docs/public/images/tutorial/07-author-portal.png +0 -0
- data/docs/public/images/tutorial/08-customized-index.png +0 -0
- data/docs/reference/app/generators.md +4 -4
- data/docs/reference/auth/accounts.md +6 -7
- data/docs/reference/auth/index.md +1 -1
- data/docs/reference/behavior/policies.md +1 -1
- data/docs/reference/index.md +67 -55
- data/docs/reference/resource/definition.md +1 -1
- data/docs/reference/tenancy/entity-scoping.md +8 -1
- data/docs/reference/tenancy/index.md +1 -1
- data/docs/reference/tenancy/invites.md +12 -5
- data/docs/reference/ui/tables.md +8 -4
- data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md +1648 -0
- data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md.tasks.json +109 -0
- data/docs/superpowers/specs/2026-05-15-public-pages-overhaul-design.md +263 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/core/assets/assets_generator.rb +10 -0
- data/lib/generators/pu/invites/install_generator.rb +44 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +1 -0
- data/lib/generators/pu/profile/conn_generator.rb +2 -2
- data/lib/generators/pu/res/conn/conn_generator.rb +33 -6
- data/lib/generators/pu/res/model/templates/model.rb.tt +4 -0
- data/lib/generators/pu/rodauth/account_generator.rb +2 -1
- data/lib/generators/pu/rodauth/admin_generator.rb +0 -2
- data/lib/generators/pu/rodauth/migration_generator.rb +0 -2
- data/lib/generators/pu/rodauth/views_generator.rb +0 -2
- data/lib/generators/pu/saas/membership/USAGE +4 -1
- data/lib/generators/pu/saas/setup_generator.rb +16 -4
- data/lib/generators/pu/saas/welcome/templates/app/controllers/welcome_controller.rb.tt +1 -1
- data/lib/plutonium/helpers/turbo_helper.rb +19 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +4 -4
- data/lib/plutonium/resource/controllers/interactive_actions.rb +3 -3
- data/lib/plutonium/ui/component/methods.rb +1 -0
- data/lib/plutonium/ui/form/base.rb +17 -1
- data/lib/plutonium/ui/form/components/secure_association.rb +11 -6
- data/lib/plutonium/ui/form/interaction.rb +1 -1
- data/lib/plutonium/ui/form/theme.rb +1 -1
- data/lib/plutonium/ui/page/edit.rb +1 -1
- data/lib/plutonium/ui/page/new.rb +1 -1
- data/lib/plutonium/ui/table/components/filter_form.rb +12 -4
- data/lib/plutonium/version.rb +1 -1
- data/package.json +4 -1
- data/src/js/controllers/form_controller.js +5 -4
- data/yarn.lock +108 -1
- 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.
|
|
@@ -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
|
|
@@ -51,7 +51,7 @@ module Pu
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def customize_policy
|
|
54
|
-
content = <<-RUBY
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
@@ -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=
|
|
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)
|