plutonium 0.34.1 → 0.35.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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/skill.md +53 -0
  3. data/.claude/skills/{assets → plutonium-assets}/SKILL.md +13 -8
  4. data/.claude/skills/{connect-resource → plutonium-connect-resource}/SKILL.md +1 -1
  5. data/.claude/skills/{controller → plutonium-controller}/SKILL.md +27 -13
  6. data/.claude/skills/{create-resource → plutonium-create-resource}/SKILL.md +1 -1
  7. data/.claude/skills/{definition → plutonium-definition}/SKILL.md +10 -10
  8. data/.claude/skills/{definition-actions → plutonium-definition-actions}/SKILL.md +34 -9
  9. data/.claude/skills/{definition-fields → plutonium-definition-fields}/SKILL.md +38 -10
  10. data/.claude/skills/plutonium-definition-query/SKILL.md +356 -0
  11. data/.claude/skills/{forms → plutonium-forms}/SKILL.md +6 -6
  12. data/.claude/skills/{installation → plutonium-installation}/SKILL.md +9 -9
  13. data/.claude/skills/{interaction → plutonium-interaction}/SKILL.md +20 -19
  14. data/.claude/skills/{model → plutonium-model}/SKILL.md +3 -3
  15. data/.claude/skills/{model-features → plutonium-model-features}/SKILL.md +3 -3
  16. data/.claude/skills/{nested-resources → plutonium-nested-resources}/SKILL.md +5 -5
  17. data/.claude/skills/{package → plutonium-package}/SKILL.md +7 -8
  18. data/.claude/skills/{policy → plutonium-policy}/SKILL.md +26 -4
  19. data/.claude/skills/{portal → plutonium-portal}/SKILL.md +33 -31
  20. data/.claude/skills/{resource → plutonium-resource}/SKILL.md +27 -27
  21. data/.claude/skills/{rodauth → plutonium-rodauth}/SKILL.md +5 -5
  22. data/.claude/skills/plutonium-theming/SKILL.md +424 -0
  23. data/.claude/skills/{views → plutonium-views}/SKILL.md +7 -7
  24. data/CHANGELOG.md +52 -0
  25. data/CLAUDE.md +215 -0
  26. data/CONTRIBUTING.md +72 -18
  27. data/README.md +100 -19
  28. data/app/assets/plutonium.css +1 -11
  29. data/app/assets/plutonium.js +1685 -1146
  30. data/app/assets/plutonium.js.map +4 -4
  31. data/app/assets/plutonium.min.js +70 -70
  32. data/app/assets/plutonium.min.js.map +4 -4
  33. data/app/views/resource/interactive_bulk_action.html.erb +1 -5
  34. data/app/views/rodauth/_email_auth_request_form.html.erb +1 -1
  35. data/app/views/rodauth/_login_form.html.erb +15 -55
  36. data/app/views/rodauth/_login_form_footer.html.erb +2 -2
  37. data/app/views/rodauth/_password_visibility.html.erb +2 -8
  38. data/app/views/rodauth/add_recovery_codes.html.erb +2 -2
  39. data/app/views/rodauth/change_login.html.erb +36 -19
  40. data/app/views/rodauth/change_password.html.erb +34 -10
  41. data/app/views/rodauth/close_account.html.erb +12 -4
  42. data/app/views/rodauth/confirm_password.html.erb +19 -17
  43. data/app/views/rodauth/create_account.html.erb +30 -109
  44. data/app/views/rodauth/email_auth.html.erb +1 -1
  45. data/app/views/rodauth/logout.html.erb +4 -4
  46. data/app/views/rodauth/otp_auth.html.erb +13 -4
  47. data/app/views/rodauth/otp_disable.html.erb +12 -4
  48. data/app/views/rodauth/otp_setup.html.erb +29 -12
  49. data/app/views/rodauth/otp_unlock.html.erb +19 -10
  50. data/app/views/rodauth/otp_unlock_not_available.html.erb +7 -7
  51. data/app/views/rodauth/recovery_auth.html.erb +12 -4
  52. data/app/views/rodauth/recovery_codes.html.erb +12 -4
  53. data/app/views/rodauth/remember.html.erb +7 -7
  54. data/app/views/rodauth/reset_password.html.erb +23 -7
  55. data/app/views/rodauth/reset_password_request.html.erb +14 -10
  56. data/app/views/rodauth/sms_auth.html.erb +13 -4
  57. data/app/views/rodauth/sms_confirm.html.erb +13 -4
  58. data/app/views/rodauth/sms_disable.html.erb +12 -4
  59. data/app/views/rodauth/sms_request.html.erb +1 -1
  60. data/app/views/rodauth/sms_setup.html.erb +23 -7
  61. data/app/views/rodauth/two_factor_auth.html.erb +2 -2
  62. data/app/views/rodauth/two_factor_disable.html.erb +12 -4
  63. data/app/views/rodauth/two_factor_manage.html.erb +7 -7
  64. data/app/views/rodauth/unlock_account.html.erb +13 -5
  65. data/app/views/rodauth/unlock_account_request.html.erb +2 -2
  66. data/app/views/rodauth/verify_account.html.erb +25 -7
  67. data/app/views/rodauth/verify_account_resend.html.erb +14 -10
  68. data/app/views/rodauth/verify_login_change.html.erb +1 -1
  69. data/app/views/rodauth/webauthn_auth.html.erb +1 -1
  70. data/app/views/rodauth/webauthn_remove.html.erb +18 -8
  71. data/app/views/rodauth/webauthn_setup.html.erb +12 -4
  72. data/docs/.vitepress/config.ts +15 -26
  73. data/docs/.vitepress/theme/custom.css +388 -29
  74. data/docs/getting-started/index.md +1 -1
  75. data/docs/getting-started/tutorial/02-first-resource.md +9 -0
  76. data/docs/getting-started/tutorial/06-nested-resources.md +2 -2
  77. data/docs/getting-started/tutorial/07-author-portal.md +191 -0
  78. data/docs/getting-started/tutorial/{07-customizing-ui.md → 08-customizing-ui.md} +7 -7
  79. data/docs/getting-started/tutorial/index.md +5 -2
  80. data/docs/guides/authorization.md +33 -0
  81. data/docs/guides/creating-packages.md +12 -16
  82. data/docs/guides/custom-actions.md +36 -0
  83. data/docs/guides/search-filtering.md +121 -42
  84. data/docs/guides/theming.md +232 -36
  85. data/docs/index.md +203 -57
  86. data/docs/public/og-image.png +0 -0
  87. data/docs/reference/controller/index.md +14 -16
  88. data/docs/reference/definition/actions.md +38 -3
  89. data/docs/reference/definition/fields.md +3 -3
  90. data/docs/reference/definition/index.md +2 -2
  91. data/docs/reference/generators/index.md +0 -1
  92. data/docs/reference/interaction/index.md +14 -10
  93. data/docs/reference/model/index.md +0 -1
  94. data/docs/reference/portal/index.md +13 -27
  95. data/gemfiles/rails_7.gemfile.lock +1 -1
  96. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  97. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  98. data/lib/generators/pu/pkg/portal/portal_generator.rb +0 -2
  99. data/lib/generators/pu/pkg/portal/templates/app/views/package/dashboard/index.html.erb +28 -72
  100. data/lib/plutonium/action/interactive.rb +2 -2
  101. data/lib/plutonium/core/controller.rb +2 -1
  102. data/lib/plutonium/definition/actions.rb +2 -2
  103. data/lib/plutonium/lib/deep_freezer.rb +3 -7
  104. data/lib/plutonium/query/filter.rb +14 -0
  105. data/lib/plutonium/query/filters/association.rb +49 -0
  106. data/lib/plutonium/query/filters/boolean.rb +35 -0
  107. data/lib/plutonium/query/filters/date.rb +97 -0
  108. data/lib/plutonium/query/filters/date_range.rb +58 -0
  109. data/lib/plutonium/query/filters/select.rb +55 -0
  110. data/lib/plutonium/resource/controllers/crud_actions.rb +24 -6
  111. data/lib/plutonium/resource/controllers/interactive_actions.rb +76 -58
  112. data/lib/plutonium/resource/controllers/queryable.rb +4 -2
  113. data/lib/plutonium/resource/query_object.rb +1 -1
  114. data/lib/plutonium/ui/action_button.rb +23 -65
  115. data/lib/plutonium/ui/actions_dropdown.rb +103 -0
  116. data/lib/plutonium/ui/block.rb +1 -1
  117. data/lib/plutonium/ui/breadcrumbs.rb +12 -19
  118. data/lib/plutonium/ui/color_mode_selector.rb +1 -1
  119. data/lib/plutonium/ui/component/kit.rb +6 -0
  120. data/lib/plutonium/ui/component_classes.rb +102 -0
  121. data/lib/plutonium/ui/display/base.rb +15 -0
  122. data/lib/plutonium/ui/display/components/attachment.rb +6 -5
  123. data/lib/plutonium/ui/display/components/boolean.rb +23 -0
  124. data/lib/plutonium/ui/display/components/color.rb +23 -0
  125. data/lib/plutonium/ui/display/resource.rb +1 -1
  126. data/lib/plutonium/ui/display/theme.rb +29 -15
  127. data/lib/plutonium/ui/empty_card.rb +3 -3
  128. data/lib/plutonium/ui/form/base.rb +20 -0
  129. data/lib/plutonium/ui/form/components/key_value_store.rb +11 -11
  130. data/lib/plutonium/ui/form/components/resource_select.rb +31 -0
  131. data/lib/plutonium/ui/form/components/secure_association.rb +1 -2
  132. data/lib/plutonium/ui/form/components/uppy.rb +5 -4
  133. data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +4 -4
  134. data/lib/plutonium/ui/form/interaction.rb +17 -1
  135. data/lib/plutonium/ui/form/query.rb +133 -80
  136. data/lib/plutonium/ui/form/theme.rb +50 -35
  137. data/lib/plutonium/ui/frame_navigator_panel.rb +2 -2
  138. data/lib/plutonium/ui/layout/base.rb +1 -1
  139. data/lib/plutonium/ui/layout/header.rb +4 -7
  140. data/lib/plutonium/ui/layout/rodauth_layout.rb +7 -7
  141. data/lib/plutonium/ui/layout/sidebar.rb +1 -1
  142. data/lib/plutonium/ui/nav_grid_menu.rb +7 -6
  143. data/lib/plutonium/ui/nav_user.rb +9 -8
  144. data/lib/plutonium/ui/page/interactive_action.rb +5 -5
  145. data/lib/plutonium/ui/page_header.rb +29 -10
  146. data/lib/plutonium/ui/panel.rb +4 -4
  147. data/lib/plutonium/ui/sidebar_menu.rb +8 -8
  148. data/lib/plutonium/ui/skeleton_table.rb +7 -8
  149. data/lib/plutonium/ui/tab_list.rb +5 -5
  150. data/lib/plutonium/ui/table/base.rb +3 -0
  151. data/lib/plutonium/ui/table/components/attachment.rb +4 -3
  152. data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +82 -0
  153. data/lib/plutonium/ui/table/components/pagy_info.rb +2 -2
  154. data/lib/plutonium/ui/table/components/pagy_pagination.rb +13 -8
  155. data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +101 -0
  156. data/lib/plutonium/ui/table/components/scopes_bar.rb +2 -2
  157. data/lib/plutonium/ui/table/components/selection_column.rb +100 -0
  158. data/lib/plutonium/ui/table/display_theme.rb +6 -6
  159. data/lib/plutonium/ui/table/resource.rb +93 -52
  160. data/lib/plutonium/ui/table/theme.rb +28 -15
  161. data/lib/plutonium/version.rb +1 -1
  162. data/package.json +2 -2
  163. data/plutonium.gemspec +5 -4
  164. data/src/css/components.css +471 -0
  165. data/src/css/intl_tel_input.css +2 -2
  166. data/src/css/plutonium.css +2 -0
  167. data/src/css/tokens.css +149 -0
  168. data/src/js/controllers/bulk_actions_controller.js +109 -0
  169. data/src/js/controllers/filter_panel_controller.js +35 -0
  170. data/src/js/controllers/register_controllers.js +5 -1
  171. data/src/js/controllers/resource_drop_down_controller.js +25 -1
  172. data/src/js/controllers/slim_select_controller.js +6 -2
  173. data/src/js/turbo/turbo_actions.js +1 -1
  174. metadata +52 -39
  175. data/.claude/skills/definition-query/SKILL.md +0 -334
  176. data/docs/concepts/architecture.md +0 -226
  177. data/docs/concepts/auto-detection.md +0 -254
  178. data/docs/concepts/index.md +0 -61
  179. data/docs/concepts/packages-portals.md +0 -304
  180. data/docs/concepts/resources.md +0 -224
  181. data/docs/cookbook/blog.md +0 -411
  182. data/docs/cookbook/index.md +0 -289
  183. data/docs/cookbook/saas.md +0 -481
  184. data/docs/public/CLAUDE.md +0 -578
  185. data/lib/generators/pu/pkg/portal/templates/app/controllers/resource_controller.rb.tt +0 -5
@@ -1,44 +1,23 @@
1
1
  /*
2
-
3
- Primary Colors:
4
-
5
- Coral/Orange Red: #FF4D4D (for main CTAs and primary elements)
6
- Deep Turquoise: #00CED1 (for secondary elements)
7
- Deep Blue: #1E3D59 (for text and accents)
8
-
9
- Accent Colors:
10
-
11
- Soft Pink: #FF9EAA (for highlights)
12
- Light Blue: #B8E3E9 (for backgrounds)
13
- White: #FFFFFF (for contrast and readability)
14
-
2
+ Brand Colors:
3
+ - Coral/Orange Red: #FF4D4D
4
+ - Deep Turquoise: #00CED1
5
+ - Deep Blue: #1E3D59
15
6
  */
16
7
 
17
8
  :root {
18
- /* Primary Brand Colors - Inspired by the vibrant gradient */
19
9
  --vp-c-brand-1: #FF4D4D;
20
- /* Vibrant coral red */
21
10
  --vp-c-brand-2: #FF6B4A;
22
- /* Lighter coral */
23
11
  --vp-c-brand-3: #FF8347;
24
- /* Soft orange */
25
12
  --vp-c-brand-soft: rgba(255, 77, 77, 0.14);
26
13
 
27
- /* Accent Colors */
28
14
  --vp-c-accent-1: #00CED1;
29
- /* Turquoise */
30
15
  --vp-c-accent-2: #1E90FF;
31
- /* Bright blue */
32
16
  --vp-c-accent-3: #4169E1;
33
- /* Royal blue */
34
17
  --vp-c-accent-soft: rgba(0, 206, 209, 0.14);
35
18
 
36
- /* Custom Background Gradients */
37
- --vp-home-hero-name-background: linear-gradient(120deg,
38
- #FF4D4D 30%,
39
- #00CED1);
19
+ --vp-home-hero-name-background: linear-gradient(120deg, #FF4D4D 30%, #00CED1);
40
20
 
41
- /* Updating existing color references */
42
21
  --vp-c-tip-1: var(--vp-c-accent-1);
43
22
  --vp-c-tip-2: var(--vp-c-accent-2);
44
23
  --vp-c-tip-3: var(--vp-c-accent-3);
@@ -46,16 +25,396 @@ White: #FFFFFF (for contrast and readability)
46
25
  }
47
26
 
48
27
  .dark {
49
- /* Dark mode adjustments */
50
28
  --vp-c-brand-1: #FF6B4A;
51
- /* Slightly lighter coral for dark mode */
52
29
  --vp-c-brand-2: #FF4D4D;
53
30
  --vp-c-brand-3: #FF3333;
54
31
  --vp-c-brand-soft: rgba(255, 107, 74, 0.16);
55
32
 
56
33
  --vp-c-accent-1: #40E0E3;
57
- /* Brighter turquoise for dark mode */
58
34
  --vp-c-accent-2: #45A3FF;
59
35
  --vp-c-accent-3: #6182E3;
60
36
  --vp-c-accent-soft: rgba(64, 224, 227, 0.16);
61
37
  }
38
+
39
+ /* ============================================
40
+ Landing Page Styles
41
+ ============================================ */
42
+
43
+ .landing-content {
44
+ max-width: 1152px;
45
+ margin: 0 auto;
46
+ padding: 0 24px;
47
+ }
48
+
49
+ .landing-content section {
50
+ padding: 80px 0;
51
+ }
52
+
53
+ .landing-content h2 {
54
+ font-size: 2.25rem;
55
+ font-weight: 700;
56
+ text-align: center;
57
+ margin-bottom: 48px;
58
+ background: linear-gradient(120deg, var(--vp-c-brand-1), var(--vp-c-accent-1));
59
+ -webkit-background-clip: text;
60
+ -webkit-text-fill-color: transparent;
61
+ background-clip: text;
62
+ }
63
+
64
+ /* Before/After Comparison */
65
+ .before-after {
66
+ border-bottom: 1px solid var(--vp-c-divider);
67
+ }
68
+
69
+ .comparison {
70
+ display: flex;
71
+ align-items: center;
72
+ justify-content: center;
73
+ gap: 32px;
74
+ flex-wrap: wrap;
75
+ }
76
+
77
+ .before, .after {
78
+ flex: 1;
79
+ min-width: 300px;
80
+ max-width: 450px;
81
+ padding: 24px;
82
+ border-radius: 12px;
83
+ background: var(--vp-c-bg-soft);
84
+ }
85
+
86
+ .before h3, .after h3 {
87
+ font-size: 1.1rem;
88
+ font-weight: 600;
89
+ margin-bottom: 16px;
90
+ color: var(--vp-c-text-1);
91
+ }
92
+
93
+ .file-tree {
94
+ font-family: var(--vp-font-family-mono);
95
+ font-size: 0.85rem;
96
+ line-height: 1.8;
97
+ }
98
+
99
+ .file {
100
+ color: var(--vp-c-text-2);
101
+ padding: 2px 0;
102
+ }
103
+
104
+ .file.dim {
105
+ color: var(--vp-c-text-3);
106
+ font-style: italic;
107
+ }
108
+
109
+ .stats {
110
+ display: flex;
111
+ gap: 16px;
112
+ margin-top: 16px;
113
+ padding-top: 16px;
114
+ border-top: 1px solid var(--vp-c-divider);
115
+ }
116
+
117
+ .stats span {
118
+ font-size: 0.85rem;
119
+ color: var(--vp-c-text-3);
120
+ padding: 4px 12px;
121
+ background: var(--vp-c-bg-mute);
122
+ border-radius: 20px;
123
+ }
124
+
125
+ .stats.success span {
126
+ color: var(--vp-c-brand-1);
127
+ background: var(--vp-c-brand-soft);
128
+ font-weight: 500;
129
+ }
130
+
131
+ .arrow {
132
+ font-size: 2rem;
133
+ color: var(--vp-c-brand-1);
134
+ font-weight: bold;
135
+ }
136
+
137
+ @media (max-width: 768px) {
138
+ .arrow {
139
+ transform: rotate(90deg);
140
+ }
141
+ }
142
+
143
+ /* Feature Rows */
144
+ .features-detailed {
145
+ border-bottom: 1px solid var(--vp-c-divider);
146
+ }
147
+
148
+ .feature-row {
149
+ display: flex;
150
+ align-items: center;
151
+ gap: 48px;
152
+ margin-bottom: 64px;
153
+ }
154
+
155
+ .feature-row:last-child {
156
+ margin-bottom: 0;
157
+ }
158
+
159
+ .feature-row.reverse {
160
+ flex-direction: row-reverse;
161
+ }
162
+
163
+ .feature-text {
164
+ flex: 1;
165
+ min-width: 280px;
166
+ }
167
+
168
+ .feature-text h3 {
169
+ font-size: 1.5rem;
170
+ font-weight: 600;
171
+ margin-bottom: 12px;
172
+ color: var(--vp-c-text-1);
173
+ }
174
+
175
+ .feature-text p {
176
+ font-size: 1.05rem;
177
+ color: var(--vp-c-text-2);
178
+ line-height: 1.7;
179
+ }
180
+
181
+ .feature-text code {
182
+ font-size: 0.9rem;
183
+ padding: 2px 6px;
184
+ background: var(--vp-c-bg-soft);
185
+ border-radius: 4px;
186
+ }
187
+
188
+ .feature-code {
189
+ flex: 1;
190
+ min-width: 320px;
191
+ }
192
+
193
+ .feature-code div[class*="language-"] {
194
+ margin: 0 !important;
195
+ border-radius: 12px;
196
+ }
197
+
198
+ @media (max-width: 900px) {
199
+ .feature-row,
200
+ .feature-row.reverse {
201
+ flex-direction: column;
202
+ }
203
+
204
+ .feature-text {
205
+ text-align: center;
206
+ }
207
+
208
+ .feature-code {
209
+ width: 100%;
210
+ }
211
+ }
212
+
213
+ /* Feature Grid */
214
+ .feature-grid {
215
+ display: grid;
216
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
217
+ gap: 24px;
218
+ border-bottom: 1px solid var(--vp-c-divider);
219
+ }
220
+
221
+ .grid-item {
222
+ padding: 32px 24px;
223
+ background: var(--vp-c-bg-soft);
224
+ border-radius: 12px;
225
+ text-align: center;
226
+ transition: transform 0.2s, box-shadow 0.2s;
227
+ }
228
+
229
+ .grid-item:hover {
230
+ transform: translateY(-4px);
231
+ box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
232
+ }
233
+
234
+ .grid-item .icon {
235
+ font-size: 2.5rem;
236
+ margin-bottom: 16px;
237
+ }
238
+
239
+ .grid-item h3 {
240
+ font-size: 1.15rem;
241
+ font-weight: 600;
242
+ margin-bottom: 8px;
243
+ color: var(--vp-c-text-1);
244
+ }
245
+
246
+ .grid-item p {
247
+ font-size: 0.95rem;
248
+ color: var(--vp-c-text-2);
249
+ line-height: 1.6;
250
+ }
251
+
252
+ /* CTA Section */
253
+ .cta-section {
254
+ text-align: center;
255
+ padding-top: 80px !important;
256
+ padding-bottom: 120px !important;
257
+ }
258
+
259
+ .cta-section h2 {
260
+ margin-bottom: 16px;
261
+ }
262
+
263
+ .cta-section > p {
264
+ font-size: 1.15rem;
265
+ color: var(--vp-c-text-2);
266
+ margin-bottom: 32px;
267
+ }
268
+
269
+ .cta-buttons {
270
+ display: flex;
271
+ justify-content: center;
272
+ gap: 16px;
273
+ flex-wrap: wrap;
274
+ }
275
+
276
+ .cta-primary,
277
+ .cta-secondary {
278
+ display: inline-block;
279
+ padding: 14px 32px;
280
+ font-size: 1rem;
281
+ font-weight: 600;
282
+ border-radius: 8px;
283
+ text-decoration: none;
284
+ transition: all 0.2s;
285
+ }
286
+
287
+ .cta-primary {
288
+ background: linear-gradient(135deg, var(--vp-c-brand-1), var(--vp-c-brand-2));
289
+ color: #ffffff !important;
290
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
291
+ }
292
+
293
+ .cta-primary:hover {
294
+ transform: translateY(-2px);
295
+ box-shadow: 0 8px 20px rgba(255, 77, 77, 0.3);
296
+ }
297
+
298
+ .cta-secondary {
299
+ background: var(--vp-c-bg-soft);
300
+ color: var(--vp-c-text-1);
301
+ border: 1px solid var(--vp-c-divider);
302
+ }
303
+
304
+ .cta-secondary:hover {
305
+ background: var(--vp-c-bg-mute);
306
+ border-color: var(--vp-c-brand-1);
307
+ }
308
+
309
+ /* After section - code block styling */
310
+ .after div[class*="language-"] {
311
+ margin: 0 0 16px 0 !important;
312
+ border-radius: 8px;
313
+ }
314
+
315
+ /* AI Section */
316
+ .ai-section {
317
+ border-bottom: 1px solid var(--vp-c-divider);
318
+ background: linear-gradient(180deg, var(--vp-c-bg) 0%, var(--vp-c-bg-soft) 100%);
319
+ margin: 0 -24px;
320
+ padding-left: 24px;
321
+ padding-right: 24px;
322
+ display: flex;
323
+ flex-direction: column;
324
+ align-items: center;
325
+ }
326
+
327
+ .ai-intro {
328
+ text-align: center;
329
+ font-size: 1.2rem;
330
+ color: var(--vp-c-text-2);
331
+ max-width: 700px;
332
+ margin: -24px auto 48px;
333
+ line-height: 1.7;
334
+ }
335
+
336
+ .ai-features {
337
+ display: grid;
338
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
339
+ gap: 32px;
340
+ margin-bottom: 48px;
341
+ width: 100%;
342
+ max-width: 1100px;
343
+ }
344
+
345
+ .ai-feature {
346
+ text-align: center;
347
+ padding: 24px;
348
+ }
349
+
350
+ .ai-icon {
351
+ font-size: 3rem;
352
+ margin-bottom: 16px;
353
+ }
354
+
355
+ .ai-feature h3 {
356
+ font-size: 1.25rem;
357
+ font-weight: 600;
358
+ margin-bottom: 12px;
359
+ color: var(--vp-c-text-1);
360
+ }
361
+
362
+ .ai-feature p {
363
+ font-size: 1rem;
364
+ color: var(--vp-c-text-2);
365
+ line-height: 1.6;
366
+ }
367
+
368
+ .ai-example {
369
+ max-width: 700px;
370
+ margin: 0 auto;
371
+ background: var(--vp-c-bg);
372
+ border-radius: 12px;
373
+ overflow: hidden;
374
+ border: 1px solid var(--vp-c-divider);
375
+ }
376
+
377
+ .ai-prompt,
378
+ .ai-result {
379
+ padding: 24px;
380
+ }
381
+
382
+ .ai-prompt {
383
+ border-bottom: 1px solid var(--vp-c-divider);
384
+ }
385
+
386
+ .prompt-label,
387
+ .result-label {
388
+ display: block;
389
+ font-size: 0.8rem;
390
+ font-weight: 600;
391
+ text-transform: uppercase;
392
+ letter-spacing: 0.05em;
393
+ margin-bottom: 8px;
394
+ }
395
+
396
+ .prompt-label {
397
+ color: var(--vp-c-brand-1);
398
+ }
399
+
400
+ .result-label {
401
+ color: var(--vp-c-accent-1);
402
+ }
403
+
404
+ .ai-prompt p,
405
+ .ai-result p {
406
+ font-size: 1.05rem;
407
+ color: var(--vp-c-text-1);
408
+ margin: 0;
409
+ line-height: 1.6;
410
+ }
411
+
412
+ .ai-prompt p {
413
+ font-style: italic;
414
+ }
415
+
416
+ /* Fix VitePress doc h2 text cutoff at bottom */
417
+ .vp-doc h2 {
418
+ line-height: 1.5;
419
+ padding-bottom: 4px;
420
+ }
@@ -13,7 +13,7 @@ Welcome to Plutonium! This guide will help you get up and running quickly.
13
13
  Before you begin, make sure you have:
14
14
 
15
15
  - **Ruby 3.2+** installed
16
- - **Rails 7.1+** (Rails 8 recommended)
16
+ - **Rails 7.2+** (Rails 8 recommended)
17
17
  - **Node.js 18+** (for asset compilation)
18
18
  - Basic familiarity with Ruby on Rails
19
19
 
@@ -51,6 +51,15 @@ class Blogging::Post < Blogging::ResourceRecord
51
51
  end
52
52
  ```
53
53
 
54
+ Add scopes for filtering posts by publication status:
55
+
56
+ ```ruby
57
+ class Blogging::Post < Blogging::ResourceRecord
58
+ scope :published, -> { where(published: true) }
59
+ scope :drafts, -> { where(published: [false, nil]) }
60
+ end
61
+ ```
62
+
54
63
  ### Definition (`packages/blogging/app/definitions/blogging/post_definition.rb`)
55
64
 
56
65
  ```ruby
@@ -142,6 +142,6 @@ Plutonium supports one level of nesting. For deeper hierarchies (e.g., Replies t
142
142
 
143
143
  ## What's Next
144
144
 
145
- Our blog has posts and comments. In the final chapter, we'll customize the UI to make it look polished.
145
+ Our blog has posts and comments. In the next chapter, we'll create an Author Portal to show how multiple portals can share resources with different access levels.
146
146
 
147
- [Continue to Chapter 7: Customizing the UI →](./07-customizing-ui)
147
+ [Continue to Chapter 7: Creating an Author Portal →](./07-author-portal)
@@ -0,0 +1,191 @@
1
+ # Chapter 7: Creating an Author Portal
2
+
3
+ In this chapter, you'll create a second portal for content authors. This demonstrates how multiple portals can share the same feature package while providing different access levels and experiences.
4
+
5
+ ## Why Multiple Portals?
6
+
7
+ Real applications often need different interfaces for different user types:
8
+
9
+ - **Admin Portal** - Full access for administrators
10
+ - **Author Portal** - Limited access for content creators
11
+ - **Customer Portal** - Public-facing for end users
12
+
13
+ Each portal can have:
14
+ - Different authentication (Admin vs User accounts)
15
+ - Different authorization rules (admins see everything, authors see only their own)
16
+ - Different UI customizations
17
+
18
+ ## Creating the Author Portal
19
+
20
+ Generate a new portal package:
21
+
22
+ ```bash
23
+ rails generate pu:pkg:portal author --auth=user
24
+ ```
25
+
26
+ This creates:
27
+
28
+ ```
29
+ packages/author_portal/
30
+ ├── app/
31
+ │ ├── controllers/author_portal/
32
+ │ │ ├── concerns/controller.rb
33
+ │ │ ├── plutonium_controller.rb
34
+ │ │ └── dashboard_controller.rb
35
+ │ ├── definitions/author_portal/
36
+ │ ├── policies/author_portal/
37
+ │ └── views/author_portal/
38
+ ├── config/
39
+ │ └── routes.rb
40
+ └── lib/
41
+ └── engine.rb
42
+ ```
43
+
44
+ ## Configuring Authentication
45
+
46
+ The Author Portal uses the `User` account type (created in Chapter 3), while the Admin Portal uses `Admin`. Update the portal's controller concern:
47
+
48
+ ```ruby
49
+ # packages/author_portal/app/controllers/author_portal/concerns/controller.rb
50
+ module AuthorPortal
51
+ module Concerns
52
+ module Controller
53
+ extend ActiveSupport::Concern
54
+ include Plutonium::Portal::Controller
55
+ include Plutonium::Auth::Rodauth(:user) # Uses User accounts
56
+ end
57
+ end
58
+ end
59
+ ```
60
+
61
+ Compare this to the Admin Portal which uses `:admin`:
62
+
63
+ ```ruby
64
+ # packages/admin_portal/app/controllers/admin_portal/concerns/controller.rb
65
+ include Plutonium::Auth::Rodauth(:admin) # Uses Admin accounts
66
+ ```
67
+
68
+ ## Connecting Resources
69
+
70
+ Connect the Post resource to the Author Portal:
71
+
72
+ ```bash
73
+ rails generate pu:res:conn Blogging::Post --dest=author_portal
74
+ ```
75
+
76
+ This adds routes and creates a portal-specific controller.
77
+
78
+ ## Portal-Specific Authorization
79
+
80
+ Authors should only see and manage their own posts. Create a portal-specific policy:
81
+
82
+ ```ruby
83
+ # packages/author_portal/app/policies/author_portal/blogging/post_policy.rb
84
+ class AuthorPortal::Blogging::PostPolicy < ::Blogging::PostPolicy
85
+ # Authors can only see their own posts
86
+ def relation_scope(relation)
87
+ relation.where(user_id: user.id)
88
+ end
89
+
90
+ # Authors can always create posts
91
+ def create?
92
+ true
93
+ end
94
+
95
+ # Authors can only update their own posts
96
+ def update?
97
+ owner?
98
+ end
99
+
100
+ # Authors can only delete their own posts
101
+ def destroy?
102
+ owner?
103
+ end
104
+
105
+ # Don't show user_id field - it's automatically set
106
+ def permitted_attributes_for_create
107
+ [:title, :body]
108
+ end
109
+ end
110
+ ```
111
+
112
+ Plutonium automatically uses the portal-specific policy (`AuthorPortal::Blogging::PostPolicy`) when accessing posts through the Author Portal.
113
+
114
+ ## Auto-Assigning the Author
115
+
116
+ When authors create posts, we want to automatically set themselves as the author. The `pu:res:conn` generator creates a portal-specific controller that extends the feature package's controller. Override the `resource_params` method to merge in the current user:
117
+
118
+ ```ruby
119
+ # packages/author_portal/app/controllers/author_portal/blogging/posts_controller.rb
120
+ class AuthorPortal::Blogging::PostsController < ::Blogging::PostsController
121
+ include AuthorPortal::Concerns::Controller
122
+
123
+ private
124
+
125
+ # Override resource_params to automatically include current_user
126
+ def resource_params
127
+ super.merge(user: current_user)
128
+ end
129
+ end
130
+ ```
131
+
132
+ Notice that the controller inherits from `::Blogging::PostsController` (the feature package's controller), not `AuthorPortal::ResourceController`. This allows portal controllers to share behavior defined in the feature package while adding portal-specific customizations.
133
+
134
+ Now when an author creates a post, they're automatically set as the owner.
135
+
136
+ ## Configuring Routes
137
+
138
+ The Author Portal routes are configured to use User authentication:
139
+
140
+ ```ruby
141
+ # packages/author_portal/config/routes.rb
142
+ AuthorPortal::Engine.routes.draw do
143
+ root to: "dashboard#index"
144
+
145
+ register_resource ::Blogging::Post
146
+ end
147
+
148
+ Rails.application.routes.draw do
149
+ constraints Rodauth::Rails.authenticate(:user) do
150
+ mount AuthorPortal::Engine, at: "/author"
151
+ end
152
+ end
153
+ ```
154
+
155
+ ## Testing the Portal
156
+
157
+ Start the server:
158
+
159
+ ```bash
160
+ bin/dev
161
+ ```
162
+
163
+ Now you have two portals:
164
+
165
+ | Portal | URL | Account Type | Access |
166
+ |--------|-----|--------------|--------|
167
+ | Admin | `/admin` | Admin | All posts |
168
+ | Author | `/author` | User | Own posts only |
169
+
170
+ ### Test the difference:
171
+
172
+ 1. **Create an Admin account** at `/admin/register`
173
+ 2. **Create a User account** at `/auth/register`
174
+ 3. **As Admin**: Create several posts at `/admin/blogging/posts`
175
+ 4. **As User**: Visit `/author/blogging/posts` - you'll only see posts you created
176
+
177
+ ## How It Works
178
+
179
+ The same `Blogging::Post` resource is used by both portals, but:
180
+
181
+ 1. **Different authentication** - Admin Portal requires Admin accounts, Author Portal requires User accounts
182
+ 2. **Different policies** - `AuthorPortal::Blogging::PostPolicy` scopes posts to the current user
183
+ 3. **Different controllers** - Author Portal auto-assigns the current user as author
184
+
185
+ This is the power of Plutonium's portal system - share business logic while customizing access.
186
+
187
+ ## What's Next
188
+
189
+ In the next chapter, we'll customize the UI with custom forms, tables, and views.
190
+
191
+ [Continue to Chapter 8: Customizing the UI →](./08-customizing-ui)