plutonium 0.50.0 → 0.51.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 +85 -102
- data/.claude/skills/plutonium-app/SKILL.md +572 -0
- data/.claude/skills/plutonium-auth/SKILL.md +163 -300
- data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
- data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
- data/.claude/skills/plutonium-tenancy/SKILL.md +655 -0
- data/.claude/skills/plutonium-testing/SKILL.md +6 -5
- data/.claude/skills/plutonium-ui/SKILL.md +900 -0
- data/CHANGELOG.md +27 -2
- data/Rakefile +2 -1
- data/app/assets/plutonium.css +1 -11
- data/app/assets/plutonium.js +1009 -1214
- data/app/assets/plutonium.js.map +3 -3
- data/app/assets/plutonium.min.js +52 -51
- data/app/assets/plutonium.min.js.map +3 -3
- data/docs/.vitepress/config.ts +37 -27
- data/docs/getting-started/index.md +22 -29
- data/docs/getting-started/installation.md +37 -80
- data/docs/getting-started/tutorial/index.md +4 -5
- data/docs/guides/adding-resources.md +66 -377
- data/docs/guides/authentication.md +94 -463
- data/docs/guides/authorization.md +124 -370
- data/docs/guides/creating-packages.md +94 -296
- data/docs/guides/custom-actions.md +121 -441
- data/docs/guides/index.md +22 -42
- data/docs/guides/multi-tenancy.md +116 -187
- data/docs/guides/nested-resources.md +103 -431
- data/docs/guides/search-filtering.md +123 -240
- data/docs/guides/testing.md +5 -4
- data/docs/guides/theming.md +157 -407
- data/docs/guides/troubleshooting.md +5 -3
- data/docs/guides/user-invites.md +106 -425
- data/docs/guides/user-profile.md +76 -243
- data/docs/index.md +1 -1
- data/docs/reference/app/generators.md +517 -0
- data/docs/reference/app/index.md +158 -0
- data/docs/reference/app/packages.md +146 -0
- data/docs/reference/app/portals.md +377 -0
- data/docs/reference/auth/accounts.md +230 -0
- data/docs/reference/auth/index.md +88 -0
- data/docs/reference/auth/profile.md +185 -0
- data/docs/reference/behavior/controllers.md +395 -0
- data/docs/reference/behavior/index.md +22 -0
- data/docs/reference/behavior/interactions.md +341 -0
- data/docs/reference/behavior/policies.md +417 -0
- data/docs/reference/index.md +56 -49
- data/docs/reference/resource/actions.md +423 -0
- data/docs/reference/resource/definition.md +508 -0
- data/docs/reference/resource/index.md +50 -0
- data/docs/reference/resource/model.md +348 -0
- data/docs/reference/resource/query.md +305 -0
- data/docs/reference/tenancy/entity-scoping.md +361 -0
- data/docs/reference/tenancy/index.md +36 -0
- data/docs/reference/tenancy/invites.md +393 -0
- data/docs/reference/tenancy/nested-resources.md +267 -0
- data/docs/reference/testing/index.md +287 -0
- data/docs/reference/ui/assets.md +400 -0
- data/docs/reference/ui/components.md +165 -0
- data/docs/reference/ui/displays.md +104 -0
- data/docs/reference/ui/forms.md +284 -0
- data/docs/reference/ui/index.md +30 -0
- data/docs/reference/ui/layouts.md +106 -0
- data/docs/reference/ui/pages.md +189 -0
- data/docs/reference/ui/tables.md +117 -0
- data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
- data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
- data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/core/update/update_generator.rb +0 -20
- data/lib/generators/pu/invites/install_generator.rb +1 -0
- data/lib/plutonium/definition/base.rb +1 -1
- data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
- data/lib/plutonium/helpers/turbo_helper.rb +11 -0
- data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
- data/lib/plutonium/resource/controller.rb +1 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +19 -1
- data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
- data/lib/plutonium/resource/policy.rb +7 -0
- data/lib/plutonium/routing/mapper_extensions.rb +15 -0
- data/lib/plutonium/ui/component/methods.rb +4 -0
- data/lib/plutonium/ui/form/base.rb +6 -2
- data/lib/plutonium/ui/form/components/json.rb +58 -0
- data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
- data/lib/plutonium/ui/form/components/secure_association.rb +98 -22
- data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
- data/lib/plutonium/ui/form/resource.rb +0 -4
- data/lib/plutonium/ui/grid/resource.rb +1 -1
- data/lib/plutonium/ui/layout/base.rb +1 -0
- data/lib/plutonium/ui/page/base.rb +0 -7
- data/lib/plutonium/ui/page/index.rb +4 -4
- data/lib/plutonium/ui/table/resource.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium.rb +8 -0
- data/lib/tasks/release.rake +15 -1
- data/package.json +10 -10
- data/src/css/slim_select.css +4 -0
- data/src/js/controllers/slim_select_controller.js +61 -0
- data/src/js/turbo/turbo_actions.js +33 -0
- data/yarn.lock +553 -543
- metadata +44 -33
- data/.claude/skills/plutonium-assets/SKILL.md +0 -512
- data/.claude/skills/plutonium-controller/SKILL.md +0 -396
- data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
- data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
- data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
- data/.claude/skills/plutonium-forms/SKILL.md +0 -465
- data/.claude/skills/plutonium-installation/SKILL.md +0 -331
- data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
- data/.claude/skills/plutonium-invites/SKILL.md +0 -408
- data/.claude/skills/plutonium-model/SKILL.md +0 -440
- data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
- data/.claude/skills/plutonium-package/SKILL.md +0 -198
- data/.claude/skills/plutonium-policy/SKILL.md +0 -456
- data/.claude/skills/plutonium-portal/SKILL.md +0 -410
- data/.claude/skills/plutonium-views/SKILL.md +0 -651
- data/docs/reference/assets/index.md +0 -496
- data/docs/reference/controller/index.md +0 -412
- data/docs/reference/definition/actions.md +0 -462
- data/docs/reference/definition/fields.md +0 -383
- data/docs/reference/definition/index.md +0 -326
- data/docs/reference/definition/query.md +0 -351
- data/docs/reference/generators/index.md +0 -648
- data/docs/reference/interaction/index.md +0 -449
- data/docs/reference/model/features.md +0 -248
- data/docs/reference/model/index.md +0 -218
- data/docs/reference/policy/index.md +0 -456
- data/docs/reference/portal/index.md +0 -379
- data/docs/reference/views/forms.md +0 -411
- data/docs/reference/views/index.md +0 -544
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# Testing Reference
|
|
2
|
+
|
|
3
|
+
`Plutonium::Testing` provides scaffolded integration tests that assert a resource × portal pairing — CRUD, policy matrix, definition smoke tests, model concerns (associated_with, SGID, has_cents), nested-resource scope boundaries, cross-portal access, and interaction outcomes. All optional, all opt-in.
|
|
4
|
+
|
|
5
|
+
## 🚨 Critical
|
|
6
|
+
|
|
7
|
+
- **Use the generators.** `pu:test:install` once per app, then `pu:test:scaffold ResourceClass --portals=...` per resource × portal. Hand-written test files drift from conventions.
|
|
8
|
+
- **Tests are opt-in.** `Plutonium::Testing` is only loaded when `require "plutonium/testing"` runs — it's never autoloaded, never present in production.
|
|
9
|
+
- **One file per (resource × portal).** Same model in admin and org portals = two test files. Each portal has different auth, scoping, and allowed actions.
|
|
10
|
+
- **Stub methods are required.** Concerns ship with `NotImplementedError` stubs — your test class supplies the test data via `create_resource!`, `valid_create_params`, `policy_roles`, etc.
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Once per app
|
|
16
|
+
rails g pu:test:install
|
|
17
|
+
|
|
18
|
+
# Per resource × portal pairing
|
|
19
|
+
rails g pu:test:scaffold Blogging::Post --portals=admin,org
|
|
20
|
+
|
|
21
|
+
# Run
|
|
22
|
+
bin/rails test
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`pu:test:install` adds `require "plutonium/testing"` to `test/test_helper.rb` and creates `test/support/plutonium_testing.rb` (a stub for non-Rodauth auth overrides).
|
|
26
|
+
|
|
27
|
+
## The DSL
|
|
28
|
+
|
|
29
|
+
Every concern uses the same class-level DSL:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
resource_tests_for ResourceClass,
|
|
33
|
+
portal: :admin, # required
|
|
34
|
+
path_prefix: "/admin", # optional override
|
|
35
|
+
parent: :organization, # for nested resources
|
|
36
|
+
actions: %i[index show new create edit update destroy],
|
|
37
|
+
skip: %i[destroy],
|
|
38
|
+
associated_with: :organization, # ResourceModel only
|
|
39
|
+
sgid_routing: true, # ResourceModel only
|
|
40
|
+
has_cents: %i[price] # ResourceModel only
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The **portal symbol** drives:
|
|
44
|
+
|
|
45
|
+
| Derived | `:admin` example | `:org` example |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `path_prefix` | `/admin` | `/org` |
|
|
48
|
+
| Default sign-in helper | admin Rodauth | user Rodauth |
|
|
49
|
+
| Allowed action set | from definition | from definition |
|
|
50
|
+
|
|
51
|
+
`path_prefix` is auto-resolved from the mounted portal engine. For mounts inside `constraints` (typical Plutonium setup), the resolver walks the route tree and finds the engine.
|
|
52
|
+
|
|
53
|
+
## Concerns
|
|
54
|
+
|
|
55
|
+
Each concern is `include`d separately. Pick the ones you need.
|
|
56
|
+
|
|
57
|
+
### `Plutonium::Testing::ResourceCrud`
|
|
58
|
+
|
|
59
|
+
Generates index / show / new / create / edit / update / destroy integration tests against the portal-mounted resource.
|
|
60
|
+
|
|
61
|
+
**Stubs:**
|
|
62
|
+
|
|
63
|
+
- `create_resource!` → persisted record
|
|
64
|
+
- `valid_create_params` → Hash for POST
|
|
65
|
+
- `valid_update_params` → Hash for PATCH
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
class AdminPortal::BloggingPostsTest < ActionDispatch::IntegrationTest
|
|
69
|
+
include IntegrationTestHelper
|
|
70
|
+
include Plutonium::Testing::ResourceCrud
|
|
71
|
+
|
|
72
|
+
resource_tests_for Blogging::Post, portal: :admin
|
|
73
|
+
|
|
74
|
+
setup do
|
|
75
|
+
@admin = create_admin!
|
|
76
|
+
@user = create_user!
|
|
77
|
+
@org = create_organization!
|
|
78
|
+
login_as(@admin)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def create_resource! = create_post!(user: @user, organization: @org)
|
|
82
|
+
|
|
83
|
+
def valid_create_params
|
|
84
|
+
{title: "x", body: "y", status: :draft, user: @user.to_sgid.to_s, organization: @org.to_sgid.to_s}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def valid_update_params = {title: "Updated"}
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### `Plutonium::Testing::ResourcePolicy`
|
|
92
|
+
|
|
93
|
+
Asserts the `permit?` matrix across action × role and verifies `relation_scope` returns an `ActiveRecord::Relation`.
|
|
94
|
+
|
|
95
|
+
**Stubs:**
|
|
96
|
+
|
|
97
|
+
- `policy_roles` → `{role_sym => -> { account }}`
|
|
98
|
+
- `policy_record` → persisted record under test
|
|
99
|
+
- `policy_matrix` → `{action_sym => [allowed_role_syms]}`
|
|
100
|
+
- `policy_context` (optional) → extra kwargs (defaults to `{entity_scope: nil}`)
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
def policy_roles
|
|
104
|
+
{admin: -> { @admin }, member: -> { @user }}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def policy_record
|
|
108
|
+
create_post!(user: @user, organization: @org)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def policy_matrix
|
|
112
|
+
{
|
|
113
|
+
index: %i[admin member],
|
|
114
|
+
show: %i[admin member],
|
|
115
|
+
create: %i[admin],
|
|
116
|
+
update: %i[admin],
|
|
117
|
+
destroy: %i[admin]
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### `Plutonium::Testing::ResourceDefinition`
|
|
123
|
+
|
|
124
|
+
Smoke-tests the resource definition: the class is constantize-able, every defineable prop dictionary (fields/inputs/displays/columns/scopes/filters/sorts/actions) is queryable, and declared fields exist on the model.
|
|
125
|
+
|
|
126
|
+
**No stubs required** for the happy path.
|
|
127
|
+
|
|
128
|
+
### `Plutonium::Testing::ResourceInteraction`
|
|
129
|
+
|
|
130
|
+
Outcome-assertion helpers for `Plutonium::Resource::Interaction` subclasses.
|
|
131
|
+
|
|
132
|
+
**Helpers:**
|
|
133
|
+
|
|
134
|
+
- `assert_interaction_success(klass, **input)` → returns the success outcome
|
|
135
|
+
- `assert_interaction_failure(klass, **input)` → returns the failure outcome
|
|
136
|
+
- `interaction_view_context` (overridable) → defaults to a mock view context
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
test "RebuildSearchInteraction succeeds" do
|
|
140
|
+
outcome = assert_interaction_success(RebuildSearchInteraction, since: 1.day.ago)
|
|
141
|
+
assert_equal 42, outcome.value[:rebuilt_count]
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### `Plutonium::Testing::ResourceModel`
|
|
146
|
+
|
|
147
|
+
Tests `associated_with` scope, SGID routing, and `has_cents` accessors — gated by DSL flags.
|
|
148
|
+
|
|
149
|
+
**Stubs:**
|
|
150
|
+
|
|
151
|
+
- `model_test_record` → persisted record
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
resource_tests_for Catalog::Product, portal: :admin,
|
|
155
|
+
associated_with: :organization,
|
|
156
|
+
sgid_routing: true,
|
|
157
|
+
has_cents: %i[price]
|
|
158
|
+
|
|
159
|
+
def model_test_record = create_product!(user: @user, organization: @org)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Only the flagged features generate tests.
|
|
163
|
+
|
|
164
|
+
### `Plutonium::Testing::NestedResource`
|
|
165
|
+
|
|
166
|
+
Asserts CRUD under a parent + scope-boundary tests (sibling tenants invisible).
|
|
167
|
+
|
|
168
|
+
**Stubs:**
|
|
169
|
+
|
|
170
|
+
- `parent_record!` → current tenant
|
|
171
|
+
- `other_parent_record!` → sibling tenant
|
|
172
|
+
- `create_resource!(parent:)` → persisted record under given parent
|
|
173
|
+
|
|
174
|
+
### `Plutonium::Testing::PortalAccess`
|
|
175
|
+
|
|
176
|
+
Cross-portal access boundaries. Uses its own DSL — NOT `resource_tests_for`.
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
class PortalAccessTest < ActionDispatch::IntegrationTest
|
|
180
|
+
include IntegrationTestHelper
|
|
181
|
+
include Plutonium::Testing::PortalAccess
|
|
182
|
+
|
|
183
|
+
portal_access_for portals: %i[admin org],
|
|
184
|
+
matrix: {admin: %i[admin], member: %i[org]}
|
|
185
|
+
|
|
186
|
+
setup do
|
|
187
|
+
@admin = create_admin!
|
|
188
|
+
@user = create_user!
|
|
189
|
+
@org = create_organization!
|
|
190
|
+
create_membership!(organization: @org, user: @user)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def login_as_role(role)
|
|
194
|
+
case role
|
|
195
|
+
when :admin then login_as(@admin, portal: :admin)
|
|
196
|
+
when :member then login_as(@user, portal: :user)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def portal_root_path(portal)
|
|
201
|
+
case portal
|
|
202
|
+
when :admin then "/admin"
|
|
203
|
+
when :org then "/org/#{@org.id}"
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Generates one test per (role × portal). Allowed = `200 | 302`; blocked = `302 | 401 | 403 | 404`.
|
|
210
|
+
|
|
211
|
+
## Auth helpers
|
|
212
|
+
|
|
213
|
+
`Plutonium::Testing::AuthHelpers` is included transitively by every concern.
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
login_as(account) # uses portal from the DSL
|
|
217
|
+
login_as(account, portal: :admin) # explicit override
|
|
218
|
+
sign_out # uses portal from the DSL
|
|
219
|
+
sign_out(portal: :admin)
|
|
220
|
+
current_account # uses portal from the DSL
|
|
221
|
+
current_account(portal: :admin)
|
|
222
|
+
with_portal(:org) { ... } # scoped portal switch
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Override hook for non-Rodauth apps
|
|
226
|
+
|
|
227
|
+
Define `sign_in_for_tests(account, portal:)` in your test class (or in `test/support/plutonium_testing.rb` for project-wide use). `AuthHelpers` will defer to it.
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
def sign_in_for_tests(account, portal:)
|
|
231
|
+
# your custom auth flow here
|
|
232
|
+
end
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Generators
|
|
236
|
+
|
|
237
|
+
### `pu:test:install`
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
rails g pu:test:install
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
- Adds `require "plutonium/testing"` to `test/test_helper.rb` (idempotent)
|
|
244
|
+
- Creates `test/support/plutonium_testing.rb` with override stub
|
|
245
|
+
|
|
246
|
+
### `pu:test:scaffold`
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
rails g pu:test:scaffold Blogging::Post --portals=admin,org
|
|
250
|
+
rails g pu:test:scaffold Blogging::Post --portals=admin --concerns=crud,policy,definition
|
|
251
|
+
rails g pu:test:scaffold Blogging::Post --portals=org --parent=organization --dest=blogging
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
| Flag | Default | Purpose |
|
|
255
|
+
|---|---|---|
|
|
256
|
+
| `--portals=admin,org` | required | Emit one file per portal |
|
|
257
|
+
| `--concerns=...` | `crud,policy,definition` | Concerns to include (`crud`, `policy`, `definition`, `nested`, `model`, `interaction`, `portal_access`) |
|
|
258
|
+
| `--parent=organization` | | Wires `NestedResource` parent |
|
|
259
|
+
| `--dest=main_app\|<package>` | `main_app` | Output destination |
|
|
260
|
+
|
|
261
|
+
Output path: `test/integration/<portal>_portal/<resource_underscored>_test.rb`.
|
|
262
|
+
|
|
263
|
+
## Customization & escape hatches
|
|
264
|
+
|
|
265
|
+
- **Skip individual tests:** `resource_tests_for Klass, portal: :admin, skip: %i[destroy]`
|
|
266
|
+
- **Restrict action set:** `resource_tests_for Klass, portal: :admin, actions: %i[index show]`
|
|
267
|
+
- **Custom assertions:** add regular `test "..."` blocks alongside the generated matrix — they coexist.
|
|
268
|
+
- **Non-Rodauth auth:** override `sign_in_for_tests`. See [AuthHelpers](#auth-helpers).
|
|
269
|
+
- **Custom path prefix:** `path_prefix: "/v2/admin"` overrides portal resolution.
|
|
270
|
+
|
|
271
|
+
## Common pitfalls
|
|
272
|
+
|
|
273
|
+
- **Forgotten stubs raise `NotImplementedError`** with the stub name. Look for the missing method in your test class.
|
|
274
|
+
- **Portal mismatch:** `:admin` portal expects `AdminPortal::Engine` constant. If your portal is named differently, pass `path_prefix:` explicitly.
|
|
275
|
+
- **Tenant leakage in stubs:** `create_resource!` for an org portal must return a record bound to the test's `@org`. Otherwise scope filtering tests pass for the wrong reason.
|
|
276
|
+
- **`policy_record` for tenant-scoped resources** must belong to a tenant the role has access to — otherwise even allowed roles will see `false`.
|
|
277
|
+
- **Nested resources need `parent: :foo`** in the DSL AND a real parent record from `parent_record!`. Without both, path interpolation fails.
|
|
278
|
+
- **`PortalAccess` doesn't use `resource_tests_for`** — use `portal_access_for` instead. Mixing them on the same class is undefined behavior.
|
|
279
|
+
|
|
280
|
+
## Related
|
|
281
|
+
|
|
282
|
+
- [Behavior › Policy](/reference/behavior/policies) — the policy methods `ResourcePolicy` verifies
|
|
283
|
+
- [Behavior › Interaction](/reference/behavior/interactions) — interaction outcomes asserted by `ResourceInteraction`
|
|
284
|
+
- [Resource › Definition](/reference/resource/definition) — definition props the smoke test introspects
|
|
285
|
+
- [Tenancy](/reference/tenancy/) — parent scoping (`NestedResource`), entity strategies (drive auth/scoping)
|
|
286
|
+
- [Auth](/reference/auth/) — Rodauth setup behind the default `sign_in_for_tests`
|
|
287
|
+
- [Guides › Testing](/guides/testing) — task-oriented walkthrough
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# Assets
|
|
2
|
+
|
|
3
|
+
TailwindCSS 4 + Stimulus toolchain. CSS design tokens for theming, `.pu-*` component classes for consistent styling, and a Phlexi theme system for component-level overrides.
|
|
4
|
+
|
|
5
|
+
## 🚨 Critical
|
|
6
|
+
|
|
7
|
+
- **Always register Stimulus controllers** — `registerControllers(application)` is required. Without it, Plutonium's controllers (color-mode, form, slim-select, flatpickr, easymde, etc.) are dead.
|
|
8
|
+
- **Use `plutoniumTailwindConfig.merge`** when overriding the theme — plain object spread drops Plutonium's defaults.
|
|
9
|
+
- **Tokens are CSS variables**, not Tailwind keys — `bg-[var(--pu-surface)]`, NOT `bg-pu-surface`.
|
|
10
|
+
- **Dark mode uses `selector`** strategy — toggle `dark` on `<html>`. The bundled `color-mode` controller does this.
|
|
11
|
+
- **Prefer `.pu-*` classes and `var(--pu-*)` tokens** over hardcoded `gray-X/dark:gray-Y` pairs — they switch with dark mode automatically.
|
|
12
|
+
|
|
13
|
+
## Asset configuration
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# config/initializers/plutonium.rb
|
|
17
|
+
Plutonium.configure do |config|
|
|
18
|
+
config.load_defaults 1.0
|
|
19
|
+
|
|
20
|
+
config.assets.stylesheet = "application" # your CSS file
|
|
21
|
+
config.assets.script = "application" # your JS file
|
|
22
|
+
config.assets.logo = "my_logo.png"
|
|
23
|
+
config.assets.favicon = "my_favicon.ico"
|
|
24
|
+
end
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Generator
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
rails generate pu:core:assets
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This:
|
|
34
|
+
|
|
35
|
+
1. Installs npm packages (`@radioactive-labs/plutonium`, TailwindCSS plugins).
|
|
36
|
+
2. Creates `tailwind.config.js` extending Plutonium's config.
|
|
37
|
+
3. Imports Plutonium CSS into `application.tailwind.css`.
|
|
38
|
+
4. Registers Plutonium's Stimulus controllers.
|
|
39
|
+
5. Updates Plutonium config to point at your asset files.
|
|
40
|
+
|
|
41
|
+
## Tailwind config
|
|
42
|
+
|
|
43
|
+
Generated `tailwind.config.js`:
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
const { execSync } = require('child_process');
|
|
47
|
+
const plutoniumGemPath = execSync("bundle show plutonium").toString().trim();
|
|
48
|
+
const plutoniumTailwindConfig = require(`${plutoniumGemPath}/tailwind.options.js`);
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
darkMode: plutoniumTailwindConfig.darkMode, // 'selector'
|
|
52
|
+
plugins: [].concat(plutoniumTailwindConfig.plugins),
|
|
53
|
+
theme: plutoniumTailwindConfig.merge(
|
|
54
|
+
plutoniumTailwindConfig.theme,
|
|
55
|
+
{ /* your overrides */ },
|
|
56
|
+
),
|
|
57
|
+
content: [
|
|
58
|
+
`${__dirname}/app/**/*.{erb,haml,html,slim,rb}`,
|
|
59
|
+
`${__dirname}/app/javascript/**/*.js`,
|
|
60
|
+
`${__dirname}/packages/**/app/**/*.{erb,haml,html,slim,rb}`,
|
|
61
|
+
].concat(plutoniumTailwindConfig.content),
|
|
62
|
+
};
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
::: danger Use `plutoniumTailwindConfig.merge`
|
|
66
|
+
A plain spread (`...plutoniumTailwindConfig.theme`) drops the merge logic and you lose Plutonium's defaults. Always use `merge(...)`.
|
|
67
|
+
:::
|
|
68
|
+
|
|
69
|
+
### Customizing colors
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
theme: plutoniumTailwindConfig.merge(plutoniumTailwindConfig.theme, {
|
|
73
|
+
extend: {
|
|
74
|
+
colors: {
|
|
75
|
+
primary: { 50: '#eff6ff', 500: '#3b82f6', 900: '#1e3a8a' },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Default color palette
|
|
82
|
+
|
|
83
|
+
| Color | Usage |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `primary` | Brand primary (turquoise default) |
|
|
86
|
+
| `secondary` | Brand secondary (navy default) |
|
|
87
|
+
| `success` | Success states (green) |
|
|
88
|
+
| `info` | Informational (blue) |
|
|
89
|
+
| `warning` | Warning (amber) |
|
|
90
|
+
| `danger` | Error (red) |
|
|
91
|
+
| `accent` | Highlight (coral pink) |
|
|
92
|
+
|
|
93
|
+
## CSS imports
|
|
94
|
+
|
|
95
|
+
```css
|
|
96
|
+
/* app/assets/stylesheets/application.tailwind.css */
|
|
97
|
+
@import "gem:plutonium/src/css/plutonium.css";
|
|
98
|
+
|
|
99
|
+
@import "tailwindcss";
|
|
100
|
+
@config '../../../tailwind.config.js';
|
|
101
|
+
|
|
102
|
+
/* your styles */
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Plutonium CSS includes core utility classes, EasyMDE (markdown editor), Slim Select, intl-tel-input, Flatpickr (date picker).
|
|
106
|
+
|
|
107
|
+
## Stimulus
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
// app/javascript/controllers/index.js
|
|
111
|
+
import { application } from "./application"
|
|
112
|
+
import { registerControllers } from "@radioactive-labs/plutonium"
|
|
113
|
+
|
|
114
|
+
registerControllers(application)
|
|
115
|
+
|
|
116
|
+
// Your custom controllers...
|
|
117
|
+
import CustomController from "./custom_controller"
|
|
118
|
+
application.register("custom", CustomController)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Bundled controllers
|
|
122
|
+
|
|
123
|
+
- `color-mode` — dark/light mode toggle
|
|
124
|
+
- `form` — form handling (pre-submit, etc.)
|
|
125
|
+
- `nested-resource-form-fields` — nested form management
|
|
126
|
+
- `slim-select` — enhanced select boxes
|
|
127
|
+
- `flatpickr` — date/time pickers
|
|
128
|
+
- `easymde` — markdown editor
|
|
129
|
+
- Various internal UI controllers
|
|
130
|
+
|
|
131
|
+
### Custom Stimulus controller — standard pattern
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
// app/javascript/controllers/custom_controller.js
|
|
135
|
+
import { Controller } from "@hotwired/stimulus"
|
|
136
|
+
|
|
137
|
+
export default class extends Controller {
|
|
138
|
+
connect() {
|
|
139
|
+
console.log("Custom controller connected")
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```javascript
|
|
145
|
+
// Register
|
|
146
|
+
application.register("custom", CustomController)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Design tokens
|
|
150
|
+
|
|
151
|
+
Plutonium uses a comprehensive CSS custom-property system for consistent, themeable UI components. Tokens auto-switch with dark mode. Source: `src/css/tokens.css`.
|
|
152
|
+
|
|
153
|
+
### Surface & backgrounds
|
|
154
|
+
|
|
155
|
+
```css
|
|
156
|
+
/* Light */
|
|
157
|
+
--pu-body: #f8fafc;
|
|
158
|
+
--pu-surface: #ffffff;
|
|
159
|
+
--pu-surface-alt: #f1f5f9;
|
|
160
|
+
--pu-surface-raised: #ffffff;
|
|
161
|
+
--pu-surface-overlay: rgba(255, 255, 255, 0.95);
|
|
162
|
+
|
|
163
|
+
/* Dark (.dark class) */
|
|
164
|
+
--pu-body: #0f172a;
|
|
165
|
+
--pu-surface: #1e293b;
|
|
166
|
+
--pu-surface-alt: #0f172a;
|
|
167
|
+
--pu-surface-raised: #334155;
|
|
168
|
+
--pu-surface-overlay: rgba(30, 41, 59, 0.95);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Text
|
|
172
|
+
|
|
173
|
+
```css
|
|
174
|
+
/* Light */
|
|
175
|
+
--pu-text: #0f172a;
|
|
176
|
+
--pu-text-muted: #64748b;
|
|
177
|
+
--pu-text-subtle: #94a3b8;
|
|
178
|
+
|
|
179
|
+
/* Dark */
|
|
180
|
+
--pu-text: #f8fafc;
|
|
181
|
+
--pu-text-muted: #94a3b8;
|
|
182
|
+
--pu-text-subtle: #64748b;
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Borders, forms, cards
|
|
186
|
+
|
|
187
|
+
```css
|
|
188
|
+
--pu-border: #e2e8f0;
|
|
189
|
+
--pu-border-muted: #f1f5f9;
|
|
190
|
+
--pu-border-strong: #cbd5e1;
|
|
191
|
+
|
|
192
|
+
--pu-input-bg: #ffffff;
|
|
193
|
+
--pu-input-border: #e2e8f0;
|
|
194
|
+
--pu-input-focus-ring: theme(colors.primary.500);
|
|
195
|
+
--pu-input-placeholder: #94a3b8;
|
|
196
|
+
|
|
197
|
+
--pu-card-bg: #ffffff;
|
|
198
|
+
--pu-card-border: #e2e8f0;
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Shadows, radii, spacing, transitions
|
|
202
|
+
|
|
203
|
+
```css
|
|
204
|
+
--pu-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.03), 0 1px 3px 0 rgb(0 0 0 / 0.05);
|
|
205
|
+
--pu-shadow-md: 0 2px 4px -1px rgb(0 0 0 / 0.04), 0 4px 6px -1px rgb(0 0 0 / 0.06);
|
|
206
|
+
--pu-shadow-lg: 0 4px 6px -2px rgb(0 0 0 / 0.03), 0 10px 15px -3px rgb(0 0 0 / 0.08);
|
|
207
|
+
|
|
208
|
+
--pu-radius-sm: 0.375rem;
|
|
209
|
+
--pu-radius-md: 0.5rem;
|
|
210
|
+
--pu-radius-lg: 0.75rem;
|
|
211
|
+
--pu-radius-xl: 1rem;
|
|
212
|
+
--pu-radius-full: 9999px;
|
|
213
|
+
|
|
214
|
+
--pu-space-xs: 0.25rem;
|
|
215
|
+
--pu-space-sm: 0.5rem;
|
|
216
|
+
--pu-space-md: 1rem;
|
|
217
|
+
--pu-space-lg: 1.5rem;
|
|
218
|
+
--pu-space-xl: 2rem;
|
|
219
|
+
|
|
220
|
+
--pu-transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
221
|
+
--pu-transition-normal: 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
222
|
+
--pu-transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Customizing tokens
|
|
226
|
+
|
|
227
|
+
```css
|
|
228
|
+
/* app/assets/stylesheets/application.tailwind.css */
|
|
229
|
+
@import "gem:plutonium/src/css/plutonium.css";
|
|
230
|
+
@import "tailwindcss";
|
|
231
|
+
|
|
232
|
+
:root {
|
|
233
|
+
--pu-surface: #fafafa;
|
|
234
|
+
--pu-border: #d1d5db;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.dark {
|
|
238
|
+
--pu-surface: #111827;
|
|
239
|
+
--pu-border: #374151;
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Using tokens in templates
|
|
244
|
+
|
|
245
|
+
```erb
|
|
246
|
+
<h1 class="text-[var(--pu-text)]">Title</h1>
|
|
247
|
+
<p class="text-[var(--pu-text-muted)]">Description</p>
|
|
248
|
+
|
|
249
|
+
<div class="bg-[var(--pu-surface)] border border-[var(--pu-border)] rounded-[var(--pu-radius-lg)]">
|
|
250
|
+
Content
|
|
251
|
+
</div>
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
class MyComponent < Plutonium::UI::Component::Base
|
|
256
|
+
def view_template
|
|
257
|
+
div(
|
|
258
|
+
class: "bg-[var(--pu-surface)] border border-[var(--pu-border)] rounded-[var(--pu-radius-lg)]",
|
|
259
|
+
style: "box-shadow: var(--pu-shadow-md)"
|
|
260
|
+
) do
|
|
261
|
+
h2(class: "text-lg font-semibold text-[var(--pu-text)]") { "Title" }
|
|
262
|
+
p(class: "text-[var(--pu-text-muted)]") { "Description" }
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Component classes (`.pu-*`)
|
|
269
|
+
|
|
270
|
+
Ready-to-use styled components in `src/css/components.css`. **Prefer these over hardcoded `gray-X/dark:gray-Y` pairs** — they auto-switch with dark mode.
|
|
271
|
+
|
|
272
|
+
### Buttons
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
.pu-btn (base)
|
|
276
|
+
.pu-btn-md / -sm / -xs (size)
|
|
277
|
+
.pu-btn-primary / -secondary / -danger / -success / -warning / -info / -accent
|
|
278
|
+
.pu-btn-ghost / -outline
|
|
279
|
+
.pu-btn-soft-primary / -soft-danger / ...
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
```erb
|
|
283
|
+
<%= form.submit "Save", class: "pu-btn pu-btn-md pu-btn-primary" %>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Inputs, labels, hints, errors
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
.pu-input / -invalid / -valid
|
|
290
|
+
.pu-label / -required
|
|
291
|
+
.pu-hint / .pu-error
|
|
292
|
+
.pu-checkbox
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Cards, panels, tables, toolbars, empty states
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
.pu-card / .pu-card-body
|
|
299
|
+
.pu-panel-header / -title / -description
|
|
300
|
+
.pu-table-wrapper / .pu-table / -header / -header-cell / -body-row / -body-row-selected / -body-cell / .pu-selection-cell
|
|
301
|
+
.pu-toolbar / -text / -actions
|
|
302
|
+
.pu-empty-state / -icon / -title / -description
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Ruby constants
|
|
306
|
+
|
|
307
|
+
`Plutonium::UI::ComponentClasses` (in `lib/plutonium/ui/component_classes.rb`):
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
ComponentClasses::Button.classes(variant: :primary, size: :default, soft: false)
|
|
311
|
+
# => "pu-btn pu-btn-md pu-btn-primary"
|
|
312
|
+
|
|
313
|
+
ComponentClasses::Form::INPUT # "pu-input"
|
|
314
|
+
ComponentClasses::Form::LABEL # "pu-label"
|
|
315
|
+
ComponentClasses::Table::WRAPPER # "pu-table-wrapper"
|
|
316
|
+
ComponentClasses::Card::BASE # "pu-card"
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Migration from hardcoded classes
|
|
320
|
+
|
|
321
|
+
| Old | New |
|
|
322
|
+
|---|---|
|
|
323
|
+
| `text-gray-900 dark:text-white` | `text-[var(--pu-text)]` |
|
|
324
|
+
| `text-gray-500 dark:text-gray-400` | `text-[var(--pu-text-muted)]` |
|
|
325
|
+
| `bg-gray-50 dark:bg-gray-700` | `bg-[var(--pu-surface)]` |
|
|
326
|
+
| `border-gray-300 dark:border-gray-600` | `border-[var(--pu-border)]` |
|
|
327
|
+
| Long input class chain | `pu-input` |
|
|
328
|
+
| `block mb-2 text-sm font-semibold ...` | `pu-label` |
|
|
329
|
+
| `text-red-600 dark:text-red-400` | `pu-error` |
|
|
330
|
+
| Long button class chain | `pu-btn pu-btn-md pu-btn-primary` |
|
|
331
|
+
|
|
332
|
+
## Phlexi component themes
|
|
333
|
+
|
|
334
|
+
Plutonium components use a Phlexi-based theme system for customizing Form, Display, and Table components. Each has a theme class with named style tokens.
|
|
335
|
+
|
|
336
|
+
### Form theme
|
|
337
|
+
|
|
338
|
+
See [Forms › Theming](./forms#theming) for the full Form theme surface.
|
|
339
|
+
|
|
340
|
+
### Display theme
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
class PostDefinition < ResourceDefinition
|
|
344
|
+
class Display < Display
|
|
345
|
+
class Theme < Plutonium::UI::Display::Theme
|
|
346
|
+
def self.theme
|
|
347
|
+
super.merge(
|
|
348
|
+
fields_wrapper: "grid grid-cols-3 gap-8",
|
|
349
|
+
label: "text-sm font-bold text-[var(--pu-text-muted)] mb-1",
|
|
350
|
+
string: "text-lg text-[var(--pu-text)]",
|
|
351
|
+
markdown: "prose dark:prose-invert max-w-none"
|
|
352
|
+
)
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Theme keys:** `fields_wrapper`, `label`, `description`, `string`, `text`, `link`, `email`, `phone`, `markdown`, `json`.
|
|
360
|
+
|
|
361
|
+
### Table theme
|
|
362
|
+
|
|
363
|
+
```ruby
|
|
364
|
+
class PostDefinition < ResourceDefinition
|
|
365
|
+
class Table < Table
|
|
366
|
+
class Theme < Plutonium::UI::Table::Theme
|
|
367
|
+
def self.theme
|
|
368
|
+
super.merge(
|
|
369
|
+
wrapper: "pu-table-wrapper",
|
|
370
|
+
base: "pu-table",
|
|
371
|
+
header: "pu-table-header",
|
|
372
|
+
header_cell: "pu-table-header-cell",
|
|
373
|
+
body_row: "pu-table-body-row",
|
|
374
|
+
body_cell: "pu-table-body-cell"
|
|
375
|
+
)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**Theme keys:** `wrapper`, `base`, `header`, `header_cell`, `body_row`, `body_cell`, `sort_icon`.
|
|
383
|
+
|
|
384
|
+
::: warning Always `super.merge(...)`
|
|
385
|
+
Don't replace the theme wholesale. Plutonium's defaults handle invalid states, focus rings, and dark mode — `super.merge` keeps them.
|
|
386
|
+
:::
|
|
387
|
+
|
|
388
|
+
## Gotchas
|
|
389
|
+
|
|
390
|
+
- **Stimulus controllers register silently fails.** If `registerControllers(application)` isn't called, the entire UI's interactive layer is dead (color-mode toggle, slim-select, flatpickr, easymde, pre-submit). No error — just no behavior.
|
|
391
|
+
- **`plutoniumTailwindConfig.merge` is mandatory.** Plain spread drops defaults silently.
|
|
392
|
+
- **Tokens are CSS variables, not Tailwind keys.** Use `bg-[var(--pu-surface)]`, not `bg-pu-surface`.
|
|
393
|
+
- **Dark mode is `selector`, not `class`.** Toggle via `document.documentElement.classList.toggle('dark')`.
|
|
394
|
+
- **`.pu-*` classes auto-switch with dark mode.** Hardcoded `gray-X/dark:gray-Y` pairs don't get auto-updated when tokens change.
|
|
395
|
+
|
|
396
|
+
## Related
|
|
397
|
+
|
|
398
|
+
- [Forms › Theming](./forms#theming) — Form theme keys + override pattern
|
|
399
|
+
- [Components](./components) — `tokens` and `classes` helpers for conditional class composition
|
|
400
|
+
- [Layouts](./layouts) — fonts, dark-mode toggle, body attributes
|