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.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium/skill.md +53 -0
- data/.claude/skills/{assets → plutonium-assets}/SKILL.md +13 -8
- data/.claude/skills/{connect-resource → plutonium-connect-resource}/SKILL.md +1 -1
- data/.claude/skills/{controller → plutonium-controller}/SKILL.md +27 -13
- data/.claude/skills/{create-resource → plutonium-create-resource}/SKILL.md +1 -1
- data/.claude/skills/{definition → plutonium-definition}/SKILL.md +10 -10
- data/.claude/skills/{definition-actions → plutonium-definition-actions}/SKILL.md +34 -9
- data/.claude/skills/{definition-fields → plutonium-definition-fields}/SKILL.md +38 -10
- data/.claude/skills/plutonium-definition-query/SKILL.md +356 -0
- data/.claude/skills/{forms → plutonium-forms}/SKILL.md +6 -6
- data/.claude/skills/{installation → plutonium-installation}/SKILL.md +9 -9
- data/.claude/skills/{interaction → plutonium-interaction}/SKILL.md +20 -19
- data/.claude/skills/{model → plutonium-model}/SKILL.md +3 -3
- data/.claude/skills/{model-features → plutonium-model-features}/SKILL.md +3 -3
- data/.claude/skills/{nested-resources → plutonium-nested-resources}/SKILL.md +5 -5
- data/.claude/skills/{package → plutonium-package}/SKILL.md +7 -8
- data/.claude/skills/{policy → plutonium-policy}/SKILL.md +26 -4
- data/.claude/skills/{portal → plutonium-portal}/SKILL.md +33 -31
- data/.claude/skills/{resource → plutonium-resource}/SKILL.md +27 -27
- data/.claude/skills/{rodauth → plutonium-rodauth}/SKILL.md +5 -5
- data/.claude/skills/plutonium-theming/SKILL.md +424 -0
- data/.claude/skills/{views → plutonium-views}/SKILL.md +7 -7
- data/CHANGELOG.md +52 -0
- data/CLAUDE.md +215 -0
- data/CONTRIBUTING.md +72 -18
- data/README.md +100 -19
- data/app/assets/plutonium.css +1 -11
- data/app/assets/plutonium.js +1685 -1146
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +70 -70
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/resource/interactive_bulk_action.html.erb +1 -5
- data/app/views/rodauth/_email_auth_request_form.html.erb +1 -1
- data/app/views/rodauth/_login_form.html.erb +15 -55
- data/app/views/rodauth/_login_form_footer.html.erb +2 -2
- data/app/views/rodauth/_password_visibility.html.erb +2 -8
- data/app/views/rodauth/add_recovery_codes.html.erb +2 -2
- data/app/views/rodauth/change_login.html.erb +36 -19
- data/app/views/rodauth/change_password.html.erb +34 -10
- data/app/views/rodauth/close_account.html.erb +12 -4
- data/app/views/rodauth/confirm_password.html.erb +19 -17
- data/app/views/rodauth/create_account.html.erb +30 -109
- data/app/views/rodauth/email_auth.html.erb +1 -1
- data/app/views/rodauth/logout.html.erb +4 -4
- data/app/views/rodauth/otp_auth.html.erb +13 -4
- data/app/views/rodauth/otp_disable.html.erb +12 -4
- data/app/views/rodauth/otp_setup.html.erb +29 -12
- data/app/views/rodauth/otp_unlock.html.erb +19 -10
- data/app/views/rodauth/otp_unlock_not_available.html.erb +7 -7
- data/app/views/rodauth/recovery_auth.html.erb +12 -4
- data/app/views/rodauth/recovery_codes.html.erb +12 -4
- data/app/views/rodauth/remember.html.erb +7 -7
- data/app/views/rodauth/reset_password.html.erb +23 -7
- data/app/views/rodauth/reset_password_request.html.erb +14 -10
- data/app/views/rodauth/sms_auth.html.erb +13 -4
- data/app/views/rodauth/sms_confirm.html.erb +13 -4
- data/app/views/rodauth/sms_disable.html.erb +12 -4
- data/app/views/rodauth/sms_request.html.erb +1 -1
- data/app/views/rodauth/sms_setup.html.erb +23 -7
- data/app/views/rodauth/two_factor_auth.html.erb +2 -2
- data/app/views/rodauth/two_factor_disable.html.erb +12 -4
- data/app/views/rodauth/two_factor_manage.html.erb +7 -7
- data/app/views/rodauth/unlock_account.html.erb +13 -5
- data/app/views/rodauth/unlock_account_request.html.erb +2 -2
- data/app/views/rodauth/verify_account.html.erb +25 -7
- data/app/views/rodauth/verify_account_resend.html.erb +14 -10
- data/app/views/rodauth/verify_login_change.html.erb +1 -1
- data/app/views/rodauth/webauthn_auth.html.erb +1 -1
- data/app/views/rodauth/webauthn_remove.html.erb +18 -8
- data/app/views/rodauth/webauthn_setup.html.erb +12 -4
- data/docs/.vitepress/config.ts +15 -26
- data/docs/.vitepress/theme/custom.css +388 -29
- data/docs/getting-started/index.md +1 -1
- data/docs/getting-started/tutorial/02-first-resource.md +9 -0
- data/docs/getting-started/tutorial/06-nested-resources.md +2 -2
- data/docs/getting-started/tutorial/07-author-portal.md +191 -0
- data/docs/getting-started/tutorial/{07-customizing-ui.md → 08-customizing-ui.md} +7 -7
- data/docs/getting-started/tutorial/index.md +5 -2
- data/docs/guides/authorization.md +33 -0
- data/docs/guides/creating-packages.md +12 -16
- data/docs/guides/custom-actions.md +36 -0
- data/docs/guides/search-filtering.md +121 -42
- data/docs/guides/theming.md +232 -36
- data/docs/index.md +203 -57
- data/docs/public/og-image.png +0 -0
- data/docs/reference/controller/index.md +14 -16
- data/docs/reference/definition/actions.md +38 -3
- data/docs/reference/definition/fields.md +3 -3
- data/docs/reference/definition/index.md +2 -2
- data/docs/reference/generators/index.md +0 -1
- data/docs/reference/interaction/index.md +14 -10
- data/docs/reference/model/index.md +0 -1
- data/docs/reference/portal/index.md +13 -27
- 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/pkg/portal/portal_generator.rb +0 -2
- data/lib/generators/pu/pkg/portal/templates/app/views/package/dashboard/index.html.erb +28 -72
- data/lib/plutonium/action/interactive.rb +2 -2
- data/lib/plutonium/core/controller.rb +2 -1
- data/lib/plutonium/definition/actions.rb +2 -2
- data/lib/plutonium/lib/deep_freezer.rb +3 -7
- data/lib/plutonium/query/filter.rb +14 -0
- data/lib/plutonium/query/filters/association.rb +49 -0
- data/lib/plutonium/query/filters/boolean.rb +35 -0
- data/lib/plutonium/query/filters/date.rb +97 -0
- data/lib/plutonium/query/filters/date_range.rb +58 -0
- data/lib/plutonium/query/filters/select.rb +55 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +24 -6
- data/lib/plutonium/resource/controllers/interactive_actions.rb +76 -58
- data/lib/plutonium/resource/controllers/queryable.rb +4 -2
- data/lib/plutonium/resource/query_object.rb +1 -1
- data/lib/plutonium/ui/action_button.rb +23 -65
- data/lib/plutonium/ui/actions_dropdown.rb +103 -0
- data/lib/plutonium/ui/block.rb +1 -1
- data/lib/plutonium/ui/breadcrumbs.rb +12 -19
- data/lib/plutonium/ui/color_mode_selector.rb +1 -1
- data/lib/plutonium/ui/component/kit.rb +6 -0
- data/lib/plutonium/ui/component_classes.rb +102 -0
- data/lib/plutonium/ui/display/base.rb +15 -0
- data/lib/plutonium/ui/display/components/attachment.rb +6 -5
- data/lib/plutonium/ui/display/components/boolean.rb +23 -0
- data/lib/plutonium/ui/display/components/color.rb +23 -0
- data/lib/plutonium/ui/display/resource.rb +1 -1
- data/lib/plutonium/ui/display/theme.rb +29 -15
- data/lib/plutonium/ui/empty_card.rb +3 -3
- data/lib/plutonium/ui/form/base.rb +20 -0
- data/lib/plutonium/ui/form/components/key_value_store.rb +11 -11
- data/lib/plutonium/ui/form/components/resource_select.rb +31 -0
- data/lib/plutonium/ui/form/components/secure_association.rb +1 -2
- data/lib/plutonium/ui/form/components/uppy.rb +5 -4
- data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +4 -4
- data/lib/plutonium/ui/form/interaction.rb +17 -1
- data/lib/plutonium/ui/form/query.rb +133 -80
- data/lib/plutonium/ui/form/theme.rb +50 -35
- data/lib/plutonium/ui/frame_navigator_panel.rb +2 -2
- data/lib/plutonium/ui/layout/base.rb +1 -1
- data/lib/plutonium/ui/layout/header.rb +4 -7
- data/lib/plutonium/ui/layout/rodauth_layout.rb +7 -7
- data/lib/plutonium/ui/layout/sidebar.rb +1 -1
- data/lib/plutonium/ui/nav_grid_menu.rb +7 -6
- data/lib/plutonium/ui/nav_user.rb +9 -8
- data/lib/plutonium/ui/page/interactive_action.rb +5 -5
- data/lib/plutonium/ui/page_header.rb +29 -10
- data/lib/plutonium/ui/panel.rb +4 -4
- data/lib/plutonium/ui/sidebar_menu.rb +8 -8
- data/lib/plutonium/ui/skeleton_table.rb +7 -8
- data/lib/plutonium/ui/tab_list.rb +5 -5
- data/lib/plutonium/ui/table/base.rb +3 -0
- data/lib/plutonium/ui/table/components/attachment.rb +4 -3
- data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +82 -0
- data/lib/plutonium/ui/table/components/pagy_info.rb +2 -2
- data/lib/plutonium/ui/table/components/pagy_pagination.rb +13 -8
- data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +101 -0
- data/lib/plutonium/ui/table/components/scopes_bar.rb +2 -2
- data/lib/plutonium/ui/table/components/selection_column.rb +100 -0
- data/lib/plutonium/ui/table/display_theme.rb +6 -6
- data/lib/plutonium/ui/table/resource.rb +93 -52
- data/lib/plutonium/ui/table/theme.rb +28 -15
- data/lib/plutonium/version.rb +1 -1
- data/package.json +2 -2
- data/plutonium.gemspec +5 -4
- data/src/css/components.css +471 -0
- data/src/css/intl_tel_input.css +2 -2
- data/src/css/plutonium.css +2 -0
- data/src/css/tokens.css +149 -0
- data/src/js/controllers/bulk_actions_controller.js +109 -0
- data/src/js/controllers/filter_panel_controller.js +35 -0
- data/src/js/controllers/register_controllers.js +5 -1
- data/src/js/controllers/resource_drop_down_controller.js +25 -1
- data/src/js/controllers/slim_select_controller.js +6 -2
- data/src/js/turbo/turbo_actions.js +1 -1
- metadata +52 -39
- data/.claude/skills/definition-query/SKILL.md +0 -334
- data/docs/concepts/architecture.md +0 -226
- data/docs/concepts/auto-detection.md +0 -254
- data/docs/concepts/index.md +0 -61
- data/docs/concepts/packages-portals.md +0 -304
- data/docs/concepts/resources.md +0 -224
- data/docs/cookbook/blog.md +0 -411
- data/docs/cookbook/index.md +0 -289
- data/docs/cookbook/saas.md +0 -481
- data/docs/public/CLAUDE.md +0 -578
- data/lib/generators/pu/pkg/portal/templates/app/controllers/resource_controller.rb.tt +0 -5
|
@@ -1,44 +1,23 @@
|
|
|
1
1
|
/*
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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:
|
|
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)
|