plutonium 0.50.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 (201) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +85 -102
  3. data/.claude/skills/plutonium-app/SKILL.md +574 -0
  4. data/.claude/skills/plutonium-auth/SKILL.md +167 -302
  5. data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
  6. data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
  7. data/.claude/skills/plutonium-tenancy/SKILL.md +674 -0
  8. data/.claude/skills/plutonium-testing/SKILL.md +9 -6
  9. data/.claude/skills/plutonium-ui/SKILL.md +900 -0
  10. data/CHANGELOG.md +44 -2
  11. data/Rakefile +2 -1
  12. data/app/assets/plutonium.css +1 -11
  13. data/app/assets/plutonium.js +1010 -1214
  14. data/app/assets/plutonium.js.map +3 -3
  15. data/app/assets/plutonium.min.js +52 -51
  16. data/app/assets/plutonium.min.js.map +3 -3
  17. data/docs/.vitepress/config.ts +38 -29
  18. data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +53 -0
  19. data/docs/.vitepress/theme/components/HomeCta.vue +108 -0
  20. data/docs/.vitepress/theme/components/HomeHero.vue +70 -0
  21. data/docs/.vitepress/theme/components/HomeInTheBox.vue +74 -0
  22. data/docs/.vitepress/theme/components/HomePillars.vue +42 -0
  23. data/docs/.vitepress/theme/components/HomeStopWriting.vue +49 -0
  24. data/docs/.vitepress/theme/components/HomeWalkthrough.vue +111 -0
  25. data/docs/.vitepress/theme/components/SectionLanding.vue +115 -0
  26. data/docs/.vitepress/theme/custom.css +144 -0
  27. data/docs/.vitepress/theme/index.ts +58 -1
  28. data/docs/getting-started/index.md +33 -57
  29. data/docs/getting-started/installation.md +37 -80
  30. data/docs/getting-started/tutorial/02-first-resource.md +17 -8
  31. data/docs/getting-started/tutorial/03-authentication.md +31 -23
  32. data/docs/getting-started/tutorial/05-custom-actions.md +9 -4
  33. data/docs/getting-started/tutorial/06-nested-resources.md +7 -1
  34. data/docs/getting-started/tutorial/07-author-portal.md +8 -0
  35. data/docs/getting-started/tutorial/08-customizing-ui.md +4 -0
  36. data/docs/getting-started/tutorial/index.md +4 -5
  37. data/docs/guides/adding-resources.md +66 -377
  38. data/docs/guides/authentication.md +98 -462
  39. data/docs/guides/authorization.md +124 -370
  40. data/docs/guides/creating-packages.md +93 -298
  41. data/docs/guides/custom-actions.md +126 -441
  42. data/docs/guides/customizing-ui.md +258 -0
  43. data/docs/guides/index.md +49 -52
  44. data/docs/guides/multi-tenancy.md +123 -186
  45. data/docs/guides/nested-resources.md +137 -396
  46. data/docs/guides/search-filtering.md +127 -238
  47. data/docs/guides/testing.md +10 -5
  48. data/docs/guides/theming.md +168 -405
  49. data/docs/guides/troubleshooting.md +5 -3
  50. data/docs/guides/user-invites.md +112 -425
  51. data/docs/guides/user-profile.md +82 -241
  52. data/docs/index.md +10 -219
  53. data/docs/public/asciinema/home-scaffold.cast +305 -0
  54. data/docs/public/images/guides/custom-actions-bulk.png +0 -0
  55. data/docs/public/images/guides/multi-tenancy-dashboard.png +0 -0
  56. data/docs/public/images/guides/multi-tenancy-welcome.png +0 -0
  57. data/docs/public/images/guides/nested-inputs.png +0 -0
  58. data/docs/public/images/guides/nested-resources-tab.png +0 -0
  59. data/docs/public/images/guides/search-filtering-index.png +0 -0
  60. data/docs/public/images/guides/search-filtering-panel.png +0 -0
  61. data/docs/public/images/guides/theming-after.png +0 -0
  62. data/docs/public/images/guides/theming-before.png +0 -0
  63. data/docs/public/images/guides/user-invites-landing.png +0 -0
  64. data/docs/public/images/guides/user-profile-edit.png +0 -0
  65. data/docs/public/images/guides/user-profile-show.png +0 -0
  66. data/docs/public/images/home-index.png +0 -0
  67. data/docs/public/images/home-new.png +0 -0
  68. data/docs/public/images/home-show.png +0 -0
  69. data/docs/public/images/tutorial/02-empty-index.png +0 -0
  70. data/docs/public/images/tutorial/02-index-with-posts.png +0 -0
  71. data/docs/public/images/tutorial/02-new-form-modal.png +0 -0
  72. data/docs/public/images/tutorial/02-new-form.png +0 -0
  73. data/docs/public/images/tutorial/03-create-account.png +0 -0
  74. data/docs/public/images/tutorial/03-login.png +0 -0
  75. data/docs/public/images/tutorial/04-admin-index.png +0 -0
  76. data/docs/public/images/tutorial/05-actions-menu.png +0 -0
  77. data/docs/public/images/tutorial/05-row-actions.png +0 -0
  78. data/docs/public/images/tutorial/06-comments-tab.png +0 -0
  79. data/docs/public/images/tutorial/06-post-with-comments.png +0 -0
  80. data/docs/public/images/tutorial/07-author-dashboard.png +0 -0
  81. data/docs/public/images/tutorial/07-author-portal.png +0 -0
  82. data/docs/public/images/tutorial/08-customized-index.png +0 -0
  83. data/docs/reference/app/generators.md +517 -0
  84. data/docs/reference/app/index.md +158 -0
  85. data/docs/reference/app/packages.md +146 -0
  86. data/docs/reference/app/portals.md +377 -0
  87. data/docs/reference/auth/accounts.md +229 -0
  88. data/docs/reference/auth/index.md +88 -0
  89. data/docs/reference/auth/profile.md +185 -0
  90. data/docs/reference/behavior/controllers.md +395 -0
  91. data/docs/reference/behavior/index.md +22 -0
  92. data/docs/reference/behavior/interactions.md +341 -0
  93. data/docs/reference/behavior/policies.md +417 -0
  94. data/docs/reference/index.md +67 -48
  95. data/docs/reference/resource/actions.md +423 -0
  96. data/docs/reference/resource/definition.md +508 -0
  97. data/docs/reference/resource/index.md +50 -0
  98. data/docs/reference/resource/model.md +348 -0
  99. data/docs/reference/resource/query.md +305 -0
  100. data/docs/reference/tenancy/entity-scoping.md +368 -0
  101. data/docs/reference/tenancy/index.md +36 -0
  102. data/docs/reference/tenancy/invites.md +400 -0
  103. data/docs/reference/tenancy/nested-resources.md +267 -0
  104. data/docs/reference/testing/index.md +287 -0
  105. data/docs/reference/ui/assets.md +400 -0
  106. data/docs/reference/ui/components.md +165 -0
  107. data/docs/reference/ui/displays.md +104 -0
  108. data/docs/reference/ui/forms.md +284 -0
  109. data/docs/reference/ui/index.md +30 -0
  110. data/docs/reference/ui/layouts.md +106 -0
  111. data/docs/reference/ui/pages.md +189 -0
  112. data/docs/reference/ui/tables.md +121 -0
  113. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md +1648 -0
  114. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md.tasks.json +109 -0
  115. data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
  116. data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
  117. data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
  118. data/docs/superpowers/specs/2026-05-15-public-pages-overhaul-design.md +263 -0
  119. data/gemfiles/rails_7.gemfile.lock +1 -1
  120. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  121. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  122. data/lib/generators/pu/core/assets/assets_generator.rb +10 -0
  123. data/lib/generators/pu/core/update/update_generator.rb +0 -20
  124. data/lib/generators/pu/invites/install_generator.rb +45 -0
  125. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +1 -0
  126. data/lib/generators/pu/profile/conn_generator.rb +2 -2
  127. data/lib/generators/pu/res/conn/conn_generator.rb +33 -6
  128. data/lib/generators/pu/res/model/templates/model.rb.tt +4 -0
  129. data/lib/generators/pu/rodauth/account_generator.rb +2 -1
  130. data/lib/generators/pu/rodauth/admin_generator.rb +0 -2
  131. data/lib/generators/pu/rodauth/migration_generator.rb +0 -2
  132. data/lib/generators/pu/rodauth/views_generator.rb +0 -2
  133. data/lib/generators/pu/saas/membership/USAGE +4 -1
  134. data/lib/generators/pu/saas/setup_generator.rb +16 -4
  135. data/lib/generators/pu/saas/welcome/templates/app/controllers/welcome_controller.rb.tt +1 -1
  136. data/lib/plutonium/definition/base.rb +1 -1
  137. data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
  138. data/lib/plutonium/helpers/turbo_helper.rb +30 -0
  139. data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
  140. data/lib/plutonium/resource/controller.rb +1 -0
  141. data/lib/plutonium/resource/controllers/crud_actions.rb +23 -5
  142. data/lib/plutonium/resource/controllers/interactive_actions.rb +3 -3
  143. data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
  144. data/lib/plutonium/resource/policy.rb +7 -0
  145. data/lib/plutonium/routing/mapper_extensions.rb +15 -0
  146. data/lib/plutonium/ui/component/methods.rb +5 -0
  147. data/lib/plutonium/ui/form/base.rb +23 -3
  148. data/lib/plutonium/ui/form/components/json.rb +58 -0
  149. data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
  150. data/lib/plutonium/ui/form/components/secure_association.rb +103 -22
  151. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
  152. data/lib/plutonium/ui/form/interaction.rb +1 -1
  153. data/lib/plutonium/ui/form/resource.rb +0 -4
  154. data/lib/plutonium/ui/form/theme.rb +1 -1
  155. data/lib/plutonium/ui/grid/resource.rb +1 -1
  156. data/lib/plutonium/ui/layout/base.rb +1 -0
  157. data/lib/plutonium/ui/page/base.rb +0 -7
  158. data/lib/plutonium/ui/page/edit.rb +1 -1
  159. data/lib/plutonium/ui/page/index.rb +4 -4
  160. data/lib/plutonium/ui/page/new.rb +1 -1
  161. data/lib/plutonium/ui/table/components/filter_form.rb +12 -4
  162. data/lib/plutonium/ui/table/resource.rb +1 -1
  163. data/lib/plutonium/version.rb +1 -1
  164. data/lib/plutonium.rb +8 -0
  165. data/lib/tasks/release.rake +15 -1
  166. data/package.json +13 -10
  167. data/src/css/slim_select.css +4 -0
  168. data/src/js/controllers/form_controller.js +5 -4
  169. data/src/js/controllers/slim_select_controller.js +61 -0
  170. data/src/js/turbo/turbo_actions.js +33 -0
  171. data/yarn.lock +661 -544
  172. metadata +86 -33
  173. data/.claude/skills/plutonium-assets/SKILL.md +0 -512
  174. data/.claude/skills/plutonium-controller/SKILL.md +0 -396
  175. data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
  176. data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
  177. data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
  178. data/.claude/skills/plutonium-forms/SKILL.md +0 -465
  179. data/.claude/skills/plutonium-installation/SKILL.md +0 -331
  180. data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
  181. data/.claude/skills/plutonium-invites/SKILL.md +0 -408
  182. data/.claude/skills/plutonium-model/SKILL.md +0 -440
  183. data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
  184. data/.claude/skills/plutonium-package/SKILL.md +0 -198
  185. data/.claude/skills/plutonium-policy/SKILL.md +0 -456
  186. data/.claude/skills/plutonium-portal/SKILL.md +0 -410
  187. data/.claude/skills/plutonium-views/SKILL.md +0 -651
  188. data/docs/reference/assets/index.md +0 -496
  189. data/docs/reference/controller/index.md +0 -412
  190. data/docs/reference/definition/actions.md +0 -462
  191. data/docs/reference/definition/fields.md +0 -383
  192. data/docs/reference/definition/index.md +0 -326
  193. data/docs/reference/definition/query.md +0 -351
  194. data/docs/reference/generators/index.md +0 -648
  195. data/docs/reference/interaction/index.md +0 -449
  196. data/docs/reference/model/features.md +0 -248
  197. data/docs/reference/model/index.md +0 -218
  198. data/docs/reference/policy/index.md +0 -456
  199. data/docs/reference/portal/index.md +0 -379
  200. data/docs/reference/views/forms.md +0 -411
  201. data/docs/reference/views/index.md +0 -544
@@ -21,6 +21,7 @@ export default defineConfig(withMermaid({
21
21
  ["meta", { name: "twitter:image", content: "https://radioactive-labs.github.io/plutonium-core/og-image.png" }],
22
22
  ],
23
23
  ignoreDeadLinks: 'localhostLinks',
24
+ srcExclude: ['superpowers/**'],
24
25
  themeConfig: {
25
26
  // https://vitepress.dev/reference/default-theme-config
26
27
  logo: "/plutonium.png",
@@ -31,8 +32,7 @@ export default defineConfig(withMermaid({
31
32
  { text: "Home", link: "/" },
32
33
  { text: "Getting Started", link: "/getting-started/" },
33
34
  { text: "Guides", link: "/guides/" },
34
- { text: "Reference", link: "/reference/" },
35
- { text: "Demo", link: "https://github.com/radioactive-labs/plutonium-core/tree/master/test/dummy" }
35
+ { text: "Reference", link: "/reference/" }
36
36
  ],
37
37
  sidebar: {
38
38
  '/getting-started/': [
@@ -111,65 +111,74 @@ export default defineConfig(withMermaid({
111
111
  ]
112
112
  },
113
113
  {
114
- text: "Model",
114
+ text: "App",
115
115
  collapsed: false,
116
116
  items: [
117
- { text: "Model", link: "/reference/model/" },
118
- { text: "Features", link: "/reference/model/features" },
117
+ { text: "Overview", link: "/reference/app/" },
118
+ { text: "Packages", link: "/reference/app/packages" },
119
+ { text: "Portals", link: "/reference/app/portals" },
120
+ { text: "Generators", link: "/reference/app/generators" },
119
121
  ]
120
122
  },
121
123
  {
122
- text: "Definition",
124
+ text: "Resource",
123
125
  collapsed: false,
124
126
  items: [
125
- { text: "Definition", link: "/reference/definition/" },
126
- { text: "Fields", link: "/reference/definition/fields" },
127
- { text: "Actions", link: "/reference/definition/actions" },
128
- { text: "Query", link: "/reference/definition/query" },
127
+ { text: "Overview", link: "/reference/resource/" },
128
+ { text: "Model", link: "/reference/resource/model" },
129
+ { text: "Definition", link: "/reference/resource/definition" },
130
+ { text: "Query", link: "/reference/resource/query" },
131
+ { text: "Actions", link: "/reference/resource/actions" },
129
132
  ]
130
133
  },
131
134
  {
132
- text: "Policy",
135
+ text: "Behavior",
133
136
  collapsed: false,
134
137
  items: [
135
- { text: "Policy", link: "/reference/policy/" },
138
+ { text: "Overview", link: "/reference/behavior/" },
139
+ { text: "Controllers", link: "/reference/behavior/controllers" },
140
+ { text: "Policies", link: "/reference/behavior/policies" },
141
+ { text: "Interactions", link: "/reference/behavior/interactions" },
136
142
  ]
137
143
  },
138
144
  {
139
- text: "Controller",
145
+ text: "UI",
140
146
  collapsed: false,
141
147
  items: [
142
- { text: "Controller", link: "/reference/controller/" },
148
+ { text: "Overview", link: "/reference/ui/" },
149
+ { text: "Pages", link: "/reference/ui/pages" },
150
+ { text: "Forms", link: "/reference/ui/forms" },
151
+ { text: "Displays", link: "/reference/ui/displays" },
152
+ { text: "Tables", link: "/reference/ui/tables" },
153
+ { text: "Components", link: "/reference/ui/components" },
154
+ { text: "Layouts", link: "/reference/ui/layouts" },
155
+ { text: "Assets", link: "/reference/ui/assets" },
143
156
  ]
144
157
  },
145
158
  {
146
- text: "Interaction",
147
- collapsed: false,
148
- items: [
149
- { text: "Interaction", link: "/reference/interaction/" },
150
- ]
151
- },
152
- {
153
- text: "Views",
159
+ text: "Auth",
154
160
  collapsed: false,
155
161
  items: [
156
- { text: "Views", link: "/reference/views/" },
157
- { text: "Forms", link: "/reference/views/forms" },
162
+ { text: "Overview", link: "/reference/auth/" },
163
+ { text: "Accounts", link: "/reference/auth/accounts" },
164
+ { text: "Profile", link: "/reference/auth/profile" },
158
165
  ]
159
166
  },
160
167
  {
161
- text: "Assets",
168
+ text: "Tenancy",
162
169
  collapsed: false,
163
170
  items: [
164
- { text: "Assets", link: "/reference/assets/" },
171
+ { text: "Overview", link: "/reference/tenancy/" },
172
+ { text: "Entity scoping", link: "/reference/tenancy/entity-scoping" },
173
+ { text: "Nested resources", link: "/reference/tenancy/nested-resources" },
174
+ { text: "Invites", link: "/reference/tenancy/invites" },
165
175
  ]
166
176
  },
167
177
  {
168
- text: "Infrastructure",
178
+ text: "Testing",
169
179
  collapsed: false,
170
180
  items: [
171
- { text: "Generators", link: "/reference/generators/" },
172
- { text: "Portal", link: "/reference/portal/" },
181
+ { text: "Overview", link: "/reference/testing/" },
173
182
  ]
174
183
  }
175
184
  ],
@@ -0,0 +1,53 @@
1
+ <template>
2
+ <section class="pu-section pu-section--band">
3
+ <div class="pu-section-inner">
4
+ <h2 class="pu-section-title">Plutonium fits two kinds of teams.</h2>
5
+ <div class="ha-grid">
6
+ <div class="ha-col">
7
+ <div class="ha-head">For Rails developers</div>
8
+ <p class="ha-lede">The missing layer between Rails and the apps you keep building.</p>
9
+ <ul class="ha-list">
10
+ <li><IconArrowRight class="ha-arr" :size="16" :stroke-width="2.25" /><span>Convention extended to CRUD, policies, and portals</span></li>
11
+ <li><IconArrowRight class="ha-arr" :size="16" :stroke-width="2.25" /><span>Generated code lives in your repo — edit anything</span></li>
12
+ <li><IconArrowRight class="ha-arr" :size="16" :stroke-width="2.25" /><span>Mountable Rails engines for packages and portals</span></li>
13
+ <li><IconArrowRight class="ha-arr" :size="16" :stroke-width="2.25" /><span>ActionPolicy authorization, baked in</span></li>
14
+ </ul>
15
+ </div>
16
+ <div class="ha-col ha-col--right">
17
+ <div class="ha-head">For founders &amp; teams</div>
18
+ <p class="ha-lede">Skip the SaaS template debate. Plutonium turns Rails into a SaaS toolkit.</p>
19
+ <ul class="ha-list">
20
+ <li><IconArrowRight class="ha-arr" :size="16" :stroke-width="2.25" /><span>Admin panel, signup, and invites on day one</span></li>
21
+ <li><IconArrowRight class="ha-arr" :size="16" :stroke-width="2.25" /><span>Multi-tenant scoping when you need it</span></li>
22
+ <li><IconArrowRight class="ha-arr" :size="16" :stroke-width="2.25" /><span>No template lock-in — it's just your Rails app</span></li>
23
+ <li><IconArrowRight class="ha-arr" :size="16" :stroke-width="2.25" /><span>Ship faster with AI tools that understand your code</span></li>
24
+ </ul>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ </section>
29
+ </template>
30
+
31
+ <script setup>
32
+ import { IconArrowRight } from "@tabler/icons-vue"
33
+ </script>
34
+
35
+ <style scoped>
36
+ .ha-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; }
37
+ .ha-col--right { border-left: 1px solid var(--pu-border); padding-left: 32px; }
38
+ .ha-head {
39
+ font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em;
40
+ color: var(--pu-accent); font-weight: 600; margin-bottom: 8px;
41
+ }
42
+ .ha-lede {
43
+ font-size: 17px; line-height: 1.35; font-weight: 500; color: var(--pu-text);
44
+ margin: 0 0 14px; letter-spacing: -0.01em;
45
+ }
46
+ .ha-list { list-style: none; padding: 0; margin: 0; font-size: 14px; line-height: 1.6; color: var(--pu-text-muted); }
47
+ .ha-list li { display: flex; gap: 10px; align-items: flex-start; padding: 5px 0; }
48
+ .ha-arr { color: var(--pu-accent); flex-shrink: 0; margin-top: 3px; }
49
+ @media (max-width: 768px) {
50
+ .ha-grid { grid-template-columns: 1fr; }
51
+ .ha-col--right { border-left: none; padding-left: 0; border-top: 1px solid var(--pu-border); padding-top: 24px; }
52
+ }
53
+ </style>
@@ -0,0 +1,108 @@
1
+ <template>
2
+ <section class="pu-section pu-section--band hc-section">
3
+ <div class="hc-inner">
4
+ <p class="hc-quote">
5
+ “Stop writing the parts of every Rails app you've already written.
6
+ Plutonium is what should have been there all along.”
7
+ </p>
8
+
9
+ <div class="hc-pills" role="tablist">
10
+ <button
11
+ v-for="opt in options"
12
+ :key="opt.id"
13
+ :class="['hc-pill', { 'hc-pill--active': selected === opt.id }]"
14
+ role="tab"
15
+ :aria-selected="selected === opt.id"
16
+ @click="selected = opt.id"
17
+ >
18
+ <span class="hc-pill-name">{{ opt.name }}</span>
19
+ <small class="hc-pill-sub">{{ opt.sub }}</small>
20
+ </button>
21
+ </div>
22
+
23
+ <div class="hc-term-wrap">
24
+ <pre class="pu-term hc-term"><span class="prompt">$</span> rails new my_app -m {{ activeUrl }}<span class="pu-term-cursor"></span></pre>
25
+ <button class="hc-copy" :class="{ 'hc-copy--ok': copied }" @click="copy" :title="copied ? 'Copied' : 'Copy command'" :aria-label="copied ? 'Copied' : 'Copy command'">
26
+ <component :is="copied ? IconCheck : IconCopy" :size="16" :stroke-width="2" />
27
+ </button>
28
+ </div>
29
+
30
+ <div class="hc-ctas">
31
+ <a class="pu-btn pu-btn-primary" href="/plutonium-core/getting-started/">Get started <IconArrowRight :size="16" :stroke-width="2.25" /></a>
32
+ <a class="pu-btn pu-btn-ghost" href="https://github.com/radioactive-labs/plutonium-core" target="_blank" rel="noopener">
33
+ <IconBrandGithub :size="16" :stroke-width="2" /> GitHub
34
+ </a>
35
+ </div>
36
+ </div>
37
+ </section>
38
+ </template>
39
+
40
+ <script setup>
41
+ import { ref, computed } from "vue"
42
+ import { IconArrowRight, IconBrandGithub, IconCopy, IconCheck } from "@tabler/icons-vue"
43
+
44
+ const options = [
45
+ { id: "plutonium", name: "plutonium", sub: "core + portals",
46
+ url: "https://radioactive-labs.github.io/plutonium-core/templates/plutonium.rb" },
47
+ { id: "pluton8", name: "pluton8", sub: "+ SaaS lite stack",
48
+ url: "https://radioactive-labs.github.io/plutonium-core/templates/pluton8.rb" },
49
+ ]
50
+ const selected = ref("plutonium")
51
+ const activeUrl = computed(() => options.find(o => o.id === selected.value).url)
52
+ const activeCommand = computed(() => `rails new my_app -m ${activeUrl.value}`)
53
+ const copied = ref(false)
54
+
55
+ async function copy() {
56
+ try {
57
+ await navigator.clipboard.writeText(activeCommand.value)
58
+ copied.value = true
59
+ setTimeout(() => { copied.value = false }, 1600)
60
+ } catch (e) {
61
+ // clipboard API unavailable; do nothing
62
+ }
63
+ }
64
+ </script>
65
+
66
+ <style scoped>
67
+ .hc-section { padding: 96px 24px; }
68
+ .hc-inner {
69
+ max-width: 760px; margin: 0 auto; text-align: center;
70
+ background: linear-gradient(180deg, var(--pu-bg-band), var(--pu-bg-light));
71
+ border: 1px solid var(--pu-border); border-radius: 12px; padding: 56px 32px;
72
+ }
73
+ .hc-quote {
74
+ font-size: 28px; letter-spacing: -0.02em; line-height: 1.25;
75
+ color: var(--pu-text); font-weight: 500;
76
+ margin: 0 auto 28px; max-width: 600px;
77
+ }
78
+ .hc-pills {
79
+ display: inline-flex; background: rgba(0,0,0,0.05); border-radius: 999px;
80
+ padding: 4px; gap: 2px; margin-bottom: 14px;
81
+ }
82
+ .hc-pill {
83
+ background: transparent; border: 0; padding: 8px 16px; border-radius: 999px;
84
+ font-size: 12.5px; color: var(--pu-text-muted); cursor: pointer;
85
+ display: flex; flex-direction: column; align-items: center; line-height: 1.1;
86
+ font-family: inherit;
87
+ }
88
+ .hc-pill-sub { font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--pu-text-faint); margin-top: 2px; }
89
+ .hc-pill--active {
90
+ background: var(--pu-bg-light); color: var(--pu-text); font-weight: 600;
91
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
92
+ }
93
+ .hc-term-wrap { position: relative; max-width: 640px; margin: 0 auto 24px; }
94
+ .hc-term { margin: 0; text-align: left; white-space: pre-wrap; word-break: break-all; padding-right: 48px; }
95
+ .hc-copy {
96
+ position: absolute; top: 8px; right: 8px;
97
+ background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.12);
98
+ color: var(--pu-term-text); border-radius: 6px;
99
+ width: 32px; height: 32px; display: inline-flex; align-items: center; justify-content: center;
100
+ cursor: pointer; transition: background 0.15s ease, color 0.15s ease;
101
+ padding: 0;
102
+ }
103
+ .hc-copy:hover { background: rgba(255,255,255,0.16); }
104
+ .hc-copy--ok { background: var(--pu-success-bg); color: var(--pu-success-fg); border-color: transparent; }
105
+ .hc-ctas { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
106
+ .hc-ctas .pu-btn { display: inline-flex; align-items: center; gap: 6px; }
107
+ @media (max-width: 600px) { .hc-quote { font-size: 22px; } }
108
+ </style>
@@ -0,0 +1,70 @@
1
+ <template>
2
+ <section class="pu-section pu-section--dark home-hero">
3
+ <div class="pu-section-inner home-hero-grid">
4
+ <div class="home-hero-text">
5
+ <div class="pu-eyebrow">Plutonium · The Rails RAD framework</div>
6
+ <h1 class="home-hero-headline">
7
+ The Rails framework for things you should never write again.
8
+ </h1>
9
+ <p class="home-hero-lede">
10
+ Convention over configuration, extended to everything you keep rebuilding.
11
+ </p>
12
+ <p class="home-hero-pillars">
13
+ <b>CRUD.</b> <b>Auth.</b> <b>Authorization.</b> <b>Multi-tenancy.</b>
14
+ <b>Admin portals.</b> <b>Search, filters, bulk actions.</b>
15
+ All generated. All customizable. All Rails.
16
+ </p>
17
+ <div class="home-hero-ctas">
18
+ <a class="pu-btn pu-btn-primary" href="/plutonium-core/getting-started/">Get started →</a>
19
+ <a class="pu-btn pu-btn-ghost on-dark" href="/plutonium-core/getting-started/tutorial/">Tutorial</a>
20
+ </div>
21
+ </div>
22
+ <pre class="pu-term home-hero-term"><span class="prompt">$</span> rails g pu:pkg:portal admin
23
+ <span class="dim"> create packages/admin_portal/...</span>
24
+ <span class="prompt">$</span> rails g pu:res:scaffold Post title:string body:text published_at:datetime --dest=main_app
25
+ <span class="dim"> create app/models/post.rb</span>
26
+ <span class="dim"> create app/resource_registries/post_definition.rb</span>
27
+ <span class="prompt">$</span> rails g pu:res:conn Post --dest=admin_portal
28
+ <span class="dim"> ✓ Connected Post to AdminPortal</span>
29
+ <span class="prompt">$</span> <span class="pu-term-cursor"></span></pre>
30
+ </div>
31
+ </section>
32
+ </template>
33
+
34
+ <style scoped>
35
+ .home-hero { padding: 96px 24px; }
36
+ .home-hero-grid {
37
+ display: grid;
38
+ grid-template-columns: 1.05fr 1fr;
39
+ gap: 48px;
40
+ align-items: center;
41
+ }
42
+ .home-hero-headline {
43
+ font-size: 48px;
44
+ line-height: 1.05;
45
+ letter-spacing: -0.025em;
46
+ margin: 0 0 18px;
47
+ font-weight: 700;
48
+ }
49
+ .home-hero-lede {
50
+ font-size: 18px;
51
+ line-height: 1.5;
52
+ opacity: 0.78;
53
+ margin: 0 0 14px;
54
+ max-width: 540px;
55
+ }
56
+ .home-hero-pillars {
57
+ font-size: 14.5px;
58
+ line-height: 1.6;
59
+ opacity: 0.65;
60
+ margin: 0 0 28px;
61
+ max-width: 540px;
62
+ }
63
+ .home-hero-pillars b { color: var(--pu-term-text); font-weight: 600; opacity: 1; }
64
+ .home-hero-ctas { display: flex; gap: 12px; flex-wrap: wrap; }
65
+ .home-hero-term { margin: 0; white-space: pre; }
66
+ @media (max-width: 768px) {
67
+ .home-hero-grid { grid-template-columns: 1fr; gap: 28px; }
68
+ .home-hero-headline { font-size: 36px; }
69
+ }
70
+ </style>
@@ -0,0 +1,74 @@
1
+ <template>
2
+ <section class="pu-section">
3
+ <div class="pu-section-inner">
4
+ <h2 class="pu-section-title">Organized the way you'll use it.</h2>
5
+ <div v-for="cat in cats" :key="cat.name" class="hb-cat">
6
+ <div class="hb-cat-name">{{ cat.name }}</div>
7
+ <div class="hb-row">
8
+ <a v-for="item in cat.items" :key="item.name" :href="item.link" class="hb-item">
9
+ <span class="hb-item-body">
10
+ <b>{{ item.name }}</b>
11
+ <small>{{ item.desc }}</small>
12
+ </span>
13
+ <IconArrowUpRight class="hb-arrow" :size="16" :stroke-width="2" />
14
+ </a>
15
+ </div>
16
+ </div>
17
+ </div>
18
+ </section>
19
+ </template>
20
+
21
+ <script setup>
22
+ import { IconArrowUpRight } from "@tabler/icons-vue"
23
+
24
+ const cats = [
25
+ { name: "Resources", items: [
26
+ { name: "Scaffolds", desc: "Model, definition, policy, routes",
27
+ link: "/plutonium-core/reference/app/generators" },
28
+ { name: "Search & filters", desc: "Declarative on the definition",
29
+ link: "/plutonium-core/guides/search-filtering" },
30
+ { name: "Custom & bulk actions", desc: "Resource-scoped interactions",
31
+ link: "/plutonium-core/guides/custom-actions" },
32
+ ]},
33
+ { name: "App structure", items: [
34
+ { name: "Portals", desc: "Themed, mountable engines",
35
+ link: "/plutonium-core/reference/app/portals" },
36
+ { name: "Packages", desc: "Feature engines under your app",
37
+ link: "/plutonium-core/guides/creating-packages" },
38
+ { name: "Multi-tenancy", desc: "Path or domain scoping",
39
+ link: "/plutonium-core/guides/multi-tenancy" },
40
+ ]},
41
+ { name: "People & access", items: [
42
+ { name: "Auth (Rodauth)", desc: "Login, signup, password reset",
43
+ link: "/plutonium-core/guides/authentication" },
44
+ { name: "Authorization", desc: "ActionPolicy per resource",
45
+ link: "/plutonium-core/guides/authorization" },
46
+ { name: "Invites & memberships", desc: "Token lifecycle, mailers, onboarding",
47
+ link: "/plutonium-core/guides/user-invites" },
48
+ ]},
49
+ ]
50
+ </script>
51
+
52
+ <style scoped>
53
+ .hb-cat { margin-bottom: 28px; }
54
+ .hb-cat:last-child { margin-bottom: 0; }
55
+ .hb-cat-name {
56
+ font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em;
57
+ color: var(--pu-accent); font-weight: 600; margin-bottom: 12px;
58
+ }
59
+ .hb-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
60
+ .hb-item {
61
+ display: flex; align-items: flex-start; justify-content: space-between; gap: 10px;
62
+ padding: 14px 16px; border: 1px solid var(--pu-border-soft); border-radius: 8px;
63
+ background: var(--pu-bg-light); color: inherit; text-decoration: none;
64
+ transition: border-color 0.15s ease, transform 0.15s ease;
65
+ }
66
+ .hb-item:hover { border-color: var(--pu-accent); }
67
+ .hb-item:hover b { color: var(--pu-accent); }
68
+ .hb-item:hover .hb-arrow { color: var(--pu-accent); transform: translate(2px, -2px); }
69
+ .hb-item-body { display: flex; flex-direction: column; min-width: 0; }
70
+ .hb-item b { display: block; color: var(--pu-text); font-weight: 600; margin-bottom: 2px; font-size: 14px; transition: color 0.15s ease; }
71
+ .hb-item small { font-size: 12px; color: var(--pu-text-faint); }
72
+ .hb-arrow { color: var(--pu-text-faint); flex-shrink: 0; transition: color 0.15s ease, transform 0.15s ease; margin-top: 2px; }
73
+ @media (max-width: 768px) { .hb-row { grid-template-columns: 1fr; } }
74
+ </style>
@@ -0,0 +1,42 @@
1
+ <template>
2
+ <section class="pu-section pu-section--band">
3
+ <div class="pu-section-inner">
4
+ <h2 class="pu-section-title">Built on Rails. Wired for shipping.</h2>
5
+ <div class="hp-grid">
6
+ <div class="hp-card" v-for="p in pillars" :key="p.name">
7
+ <component :is="p.icon" class="hp-icon" :size="22" :stroke-width="1.75" />
8
+ <div class="hp-name">{{ p.name }}</div>
9
+ <div class="hp-desc">{{ p.desc }}</div>
10
+ </div>
11
+ </div>
12
+ </div>
13
+ </section>
14
+ </template>
15
+
16
+ <script setup>
17
+ import { IconRoute, IconCode, IconBuildingSkyscraper, IconRobot } from "@tabler/icons-vue"
18
+
19
+ const pillars = [
20
+ { icon: IconRoute, name: "Convention over configuration",
21
+ desc: "Extended to resources, policies, portals, and tenancy — not just routes and views." },
22
+ { icon: IconCode, name: "It's just Rails",
23
+ desc: 'Generated code lives in your repo. Edit it, override it, delete it. The “magic” is regular Ruby mixins you can read.' },
24
+ { icon: IconBuildingSkyscraper, name: "Multi-tenant ready",
25
+ desc: "Path or domain tenancy. Scoped relations. Invites and memberships out of the box." },
26
+ { icon: IconRobot, name: "AI-readable",
27
+ desc: "Predictable file layout and naming. Built-in skills teach AI assistants the patterns." },
28
+ ]
29
+ </script>
30
+
31
+ <style scoped>
32
+ .hp-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
33
+ .hp-card {
34
+ padding: 18px; border: 1px solid var(--pu-border-soft); border-radius: 8px;
35
+ background: var(--pu-bg-light);
36
+ }
37
+ .hp-icon { color: var(--pu-accent); margin-bottom: 10px; display: block; }
38
+ .hp-name { font-weight: 600; font-size: 15px; color: var(--pu-text); margin-bottom: 6px; line-height: 1.25; }
39
+ .hp-desc { font-size: 13px; color: var(--pu-text-muted); line-height: 1.5; }
40
+ @media (max-width: 900px) { .hp-grid { grid-template-columns: repeat(2, 1fr); } }
41
+ @media (max-width: 480px) { .hp-grid { grid-template-columns: 1fr; } }
42
+ </style>
@@ -0,0 +1,49 @@
1
+ <template>
2
+ <section class="pu-section home-stop-writing">
3
+ <div class="pu-section-inner">
4
+ <h2 class="pu-section-title">What you stop writing.</h2>
5
+ <p class="hsw-sub">Same scaffold command. Two starting points. Very different surface area.</p>
6
+
7
+ <div class="hsw-grid">
8
+ <div>
9
+ <span class="hsw-label hsw-label--bad">Rails scaffold</span>
10
+ <pre class="pu-term pu-term--inline hsw-term"><span class="prompt">$</span> rails g scaffold Post title:string body:text published_at:datetime</pre>
11
+ <div class="hsw-stats">
12
+ <span>Just <b>CRUD</b></span>
13
+ </div>
14
+ </div>
15
+ <div>
16
+ <span class="hsw-label hsw-label--good">Plutonium</span>
17
+ <pre class="pu-term pu-term--inline hsw-term"><span class="prompt">$</span> rails g pu:res:scaffold Post title:string body:text published_at:datetime --dest=main_app</pre>
18
+ <div class="hsw-stats hsw-stats--win">
19
+ <span><b>Full CRUD</b></span>
20
+ <span><b>+ Search</b></span>
21
+ <span><b>+ Filters</b></span>
22
+ <span><b>+ Bulk actions</b></span>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </section>
28
+ </template>
29
+
30
+ <style scoped>
31
+ .hsw-sub { color: var(--pu-text-muted); font-size: 15px; margin: -16px 0 32px; }
32
+ .hsw-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; align-items: stretch; }
33
+ .hsw-grid > div { display: flex; flex-direction: column; min-width: 0; }
34
+ .hsw-stats { margin-top: auto; }
35
+ .hsw-label {
36
+ display: inline-block; font-size: 11px; padding: 3px 8px; border-radius: 4px;
37
+ text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600;
38
+ }
39
+ .hsw-label--bad { background: #fff0f0; color: var(--pu-accent); }
40
+ .hsw-label--good { background: var(--pu-success-bg); color: var(--pu-success-fg); }
41
+ .hsw-term { margin-top: 10px; }
42
+ .hsw-stats {
43
+ padding-top: 14px; display: flex; gap: 14px; flex-wrap: wrap;
44
+ font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--pu-text-faint);
45
+ }
46
+ .hsw-stats b { color: var(--pu-text); font-weight: 600; }
47
+ .hsw-stats--win b { color: var(--pu-success-fg); }
48
+ @media (max-width: 768px) { .hsw-grid { grid-template-columns: 1fr; } }
49
+ </style>
@@ -0,0 +1,111 @@
1
+ <template>
2
+ <section class="pu-section">
3
+ <div class="pu-section-inner">
4
+ <h2 class="pu-section-title">Scaffold a portal in minutes.</h2>
5
+
6
+ <div class="hw-cast">
7
+ <div class="hw-browser-bar hw-browser-bar--term">
8
+ <span></span><span></span><span></span>
9
+ <code>asciinema · scaffold a blog</code>
10
+ </div>
11
+ <div ref="castEl" class="hw-cast-player"></div>
12
+ </div>
13
+
14
+ <div class="hw-strip">
15
+ <figure v-for="shot in shots" :key="shot.label">
16
+ <div class="hw-label">{{ shot.label }}</div>
17
+ <div class="hw-browser">
18
+ <div class="hw-browser-bar"><span></span><span></span><code>{{ shot.url }}</code></div>
19
+ <img :src="shot.src" :alt="shot.alt" class="hw-shot pu-zoomable" />
20
+ </div>
21
+ </figure>
22
+ </div>
23
+ </div>
24
+ </section>
25
+ </template>
26
+
27
+ <script setup>
28
+ import { ref, onMounted, onBeforeUnmount } from "vue"
29
+ import { withBase } from "vitepress"
30
+
31
+ const shots = [
32
+ { label: "Index", url: "/admin/posts", src: withBase("/images/home-index.png"), alt: "Posts index" },
33
+ { label: "New", url: "/admin/posts/new", src: withBase("/images/home-new.png"), alt: "New post form" },
34
+ { label: "Show", url: "/admin/posts/1", src: withBase("/images/home-show.png"), alt: "Post show page" },
35
+ ]
36
+ const castUrl = withBase("/asciinema/home-scaffold.cast")
37
+
38
+ const castEl = ref(null)
39
+ let player = null
40
+
41
+ onMounted(async () => {
42
+ if (typeof window === "undefined" || !castEl.value) return
43
+
44
+ const [{ create }] = await Promise.all([
45
+ import("asciinema-player"),
46
+ import("asciinema-player/dist/bundle/asciinema-player.css"),
47
+ ])
48
+
49
+ player = create(castUrl, castEl.value, {
50
+ autoPlay: true,
51
+ loop: true,
52
+ controls: true,
53
+ fit: "width",
54
+ rows: 14,
55
+ terminalFontSize: "small",
56
+ idleTimeLimit: 1.5,
57
+ })
58
+ })
59
+
60
+ onBeforeUnmount(() => {
61
+ player?.dispose?.()
62
+ })
63
+ </script>
64
+
65
+ <style scoped>
66
+ .hw-cast {
67
+ border: 1px solid var(--pu-border); border-radius: 10px; overflow: hidden;
68
+ background: var(--pu-bg-dark); margin-bottom: 18px;
69
+ }
70
+ .hw-cast-player { padding: 8px; background: var(--pu-bg-dark); }
71
+ .hw-cast-player :deep(.ap-player) { background: var(--pu-bg-dark); }
72
+
73
+ .hw-browser-bar {
74
+ background: var(--pu-bg-band); padding: 8px 12px; display: flex; align-items: center; gap: 5px;
75
+ border-bottom: 1px solid var(--pu-border-soft);
76
+ }
77
+ .hw-browser-bar--term { background: #161b22; border-bottom-color: #30363d; }
78
+ .hw-browser-bar span {
79
+ width: 10px; height: 10px; border-radius: 50%; background: var(--pu-border);
80
+ }
81
+ .hw-browser-bar--term span { background: #30363d; }
82
+ .hw-browser-bar code {
83
+ margin-left: 12px; background: var(--pu-bg-light); padding: 3px 8px; border-radius: 4px;
84
+ font-size: 11px; color: var(--pu-text-faint);
85
+ }
86
+ .hw-browser-bar--term code { background: #21262d; color: #8b949e; }
87
+
88
+ .hw-strip {
89
+ display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; align-items: stretch;
90
+ }
91
+ .hw-strip figure { margin: 0; display: flex; flex-direction: column; }
92
+ .hw-label {
93
+ font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em;
94
+ color: var(--pu-text-faint); margin-bottom: 8px;
95
+ }
96
+ .hw-browser {
97
+ display: block; width: 100%;
98
+ border: 1px solid var(--pu-border-soft); border-radius: 8px; overflow: hidden;
99
+ background: var(--pu-bg-light);
100
+ transition: border-color 0.15s ease;
101
+ }
102
+ .hw-browser:hover { border-color: var(--pu-accent); }
103
+ .hw-browser .hw-browser-bar { padding: 5px 8px; }
104
+ .hw-browser .hw-browser-bar span { width: 8px; height: 8px; }
105
+ .hw-browser .hw-browser-bar code { margin-left: 6px; font-size: 10px; }
106
+ .hw-shot { display: block; width: 100%; height: auto; }
107
+
108
+ @media (max-width: 768px) {
109
+ .hw-strip { grid-template-columns: 1fr; }
110
+ }
111
+ </style>