plutonium 0.50.0 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +85 -102
  3. data/.claude/skills/plutonium-app/SKILL.md +574 -0
  4. data/.claude/skills/plutonium-auth/SKILL.md +167 -302
  5. data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
  6. data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
  7. data/.claude/skills/plutonium-tenancy/SKILL.md +674 -0
  8. data/.claude/skills/plutonium-testing/SKILL.md +9 -6
  9. data/.claude/skills/plutonium-ui/SKILL.md +900 -0
  10. data/CHANGELOG.md +44 -2
  11. data/Rakefile +2 -1
  12. data/app/assets/plutonium.css +1 -11
  13. data/app/assets/plutonium.js +1010 -1214
  14. data/app/assets/plutonium.js.map +3 -3
  15. data/app/assets/plutonium.min.js +52 -51
  16. data/app/assets/plutonium.min.js.map +3 -3
  17. data/docs/.vitepress/config.ts +38 -29
  18. data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +53 -0
  19. data/docs/.vitepress/theme/components/HomeCta.vue +108 -0
  20. data/docs/.vitepress/theme/components/HomeHero.vue +70 -0
  21. data/docs/.vitepress/theme/components/HomeInTheBox.vue +74 -0
  22. data/docs/.vitepress/theme/components/HomePillars.vue +42 -0
  23. data/docs/.vitepress/theme/components/HomeStopWriting.vue +49 -0
  24. data/docs/.vitepress/theme/components/HomeWalkthrough.vue +111 -0
  25. data/docs/.vitepress/theme/components/SectionLanding.vue +115 -0
  26. data/docs/.vitepress/theme/custom.css +144 -0
  27. data/docs/.vitepress/theme/index.ts +58 -1
  28. data/docs/getting-started/index.md +33 -57
  29. data/docs/getting-started/installation.md +37 -80
  30. data/docs/getting-started/tutorial/02-first-resource.md +17 -8
  31. data/docs/getting-started/tutorial/03-authentication.md +31 -23
  32. data/docs/getting-started/tutorial/05-custom-actions.md +9 -4
  33. data/docs/getting-started/tutorial/06-nested-resources.md +7 -1
  34. data/docs/getting-started/tutorial/07-author-portal.md +8 -0
  35. data/docs/getting-started/tutorial/08-customizing-ui.md +4 -0
  36. data/docs/getting-started/tutorial/index.md +4 -5
  37. data/docs/guides/adding-resources.md +66 -377
  38. data/docs/guides/authentication.md +98 -462
  39. data/docs/guides/authorization.md +124 -370
  40. data/docs/guides/creating-packages.md +93 -298
  41. data/docs/guides/custom-actions.md +126 -441
  42. data/docs/guides/customizing-ui.md +258 -0
  43. data/docs/guides/index.md +49 -52
  44. data/docs/guides/multi-tenancy.md +123 -186
  45. data/docs/guides/nested-resources.md +137 -396
  46. data/docs/guides/search-filtering.md +127 -238
  47. data/docs/guides/testing.md +10 -5
  48. data/docs/guides/theming.md +168 -405
  49. data/docs/guides/troubleshooting.md +5 -3
  50. data/docs/guides/user-invites.md +112 -425
  51. data/docs/guides/user-profile.md +82 -241
  52. data/docs/index.md +10 -219
  53. data/docs/public/asciinema/home-scaffold.cast +305 -0
  54. data/docs/public/images/guides/custom-actions-bulk.png +0 -0
  55. data/docs/public/images/guides/multi-tenancy-dashboard.png +0 -0
  56. data/docs/public/images/guides/multi-tenancy-welcome.png +0 -0
  57. data/docs/public/images/guides/nested-inputs.png +0 -0
  58. data/docs/public/images/guides/nested-resources-tab.png +0 -0
  59. data/docs/public/images/guides/search-filtering-index.png +0 -0
  60. data/docs/public/images/guides/search-filtering-panel.png +0 -0
  61. data/docs/public/images/guides/theming-after.png +0 -0
  62. data/docs/public/images/guides/theming-before.png +0 -0
  63. data/docs/public/images/guides/user-invites-landing.png +0 -0
  64. data/docs/public/images/guides/user-profile-edit.png +0 -0
  65. data/docs/public/images/guides/user-profile-show.png +0 -0
  66. data/docs/public/images/home-index.png +0 -0
  67. data/docs/public/images/home-new.png +0 -0
  68. data/docs/public/images/home-show.png +0 -0
  69. data/docs/public/images/tutorial/02-empty-index.png +0 -0
  70. data/docs/public/images/tutorial/02-index-with-posts.png +0 -0
  71. data/docs/public/images/tutorial/02-new-form-modal.png +0 -0
  72. data/docs/public/images/tutorial/02-new-form.png +0 -0
  73. data/docs/public/images/tutorial/03-create-account.png +0 -0
  74. data/docs/public/images/tutorial/03-login.png +0 -0
  75. data/docs/public/images/tutorial/04-admin-index.png +0 -0
  76. data/docs/public/images/tutorial/05-actions-menu.png +0 -0
  77. data/docs/public/images/tutorial/05-row-actions.png +0 -0
  78. data/docs/public/images/tutorial/06-comments-tab.png +0 -0
  79. data/docs/public/images/tutorial/06-post-with-comments.png +0 -0
  80. data/docs/public/images/tutorial/07-author-dashboard.png +0 -0
  81. data/docs/public/images/tutorial/07-author-portal.png +0 -0
  82. data/docs/public/images/tutorial/08-customized-index.png +0 -0
  83. data/docs/reference/app/generators.md +517 -0
  84. data/docs/reference/app/index.md +158 -0
  85. data/docs/reference/app/packages.md +146 -0
  86. data/docs/reference/app/portals.md +377 -0
  87. data/docs/reference/auth/accounts.md +229 -0
  88. data/docs/reference/auth/index.md +88 -0
  89. data/docs/reference/auth/profile.md +185 -0
  90. data/docs/reference/behavior/controllers.md +395 -0
  91. data/docs/reference/behavior/index.md +22 -0
  92. data/docs/reference/behavior/interactions.md +341 -0
  93. data/docs/reference/behavior/policies.md +417 -0
  94. data/docs/reference/index.md +67 -48
  95. data/docs/reference/resource/actions.md +423 -0
  96. data/docs/reference/resource/definition.md +508 -0
  97. data/docs/reference/resource/index.md +50 -0
  98. data/docs/reference/resource/model.md +348 -0
  99. data/docs/reference/resource/query.md +305 -0
  100. data/docs/reference/tenancy/entity-scoping.md +368 -0
  101. data/docs/reference/tenancy/index.md +36 -0
  102. data/docs/reference/tenancy/invites.md +400 -0
  103. data/docs/reference/tenancy/nested-resources.md +267 -0
  104. data/docs/reference/testing/index.md +287 -0
  105. data/docs/reference/ui/assets.md +400 -0
  106. data/docs/reference/ui/components.md +165 -0
  107. data/docs/reference/ui/displays.md +104 -0
  108. data/docs/reference/ui/forms.md +284 -0
  109. data/docs/reference/ui/index.md +30 -0
  110. data/docs/reference/ui/layouts.md +106 -0
  111. data/docs/reference/ui/pages.md +189 -0
  112. data/docs/reference/ui/tables.md +121 -0
  113. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md +1648 -0
  114. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md.tasks.json +109 -0
  115. data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
  116. data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
  117. data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
  118. data/docs/superpowers/specs/2026-05-15-public-pages-overhaul-design.md +263 -0
  119. data/gemfiles/rails_7.gemfile.lock +1 -1
  120. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  121. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  122. data/lib/generators/pu/core/assets/assets_generator.rb +10 -0
  123. data/lib/generators/pu/core/update/update_generator.rb +0 -20
  124. data/lib/generators/pu/invites/install_generator.rb +45 -0
  125. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +1 -0
  126. data/lib/generators/pu/profile/conn_generator.rb +2 -2
  127. data/lib/generators/pu/res/conn/conn_generator.rb +33 -6
  128. data/lib/generators/pu/res/model/templates/model.rb.tt +4 -0
  129. data/lib/generators/pu/rodauth/account_generator.rb +2 -1
  130. data/lib/generators/pu/rodauth/admin_generator.rb +0 -2
  131. data/lib/generators/pu/rodauth/migration_generator.rb +0 -2
  132. data/lib/generators/pu/rodauth/views_generator.rb +0 -2
  133. data/lib/generators/pu/saas/membership/USAGE +4 -1
  134. data/lib/generators/pu/saas/setup_generator.rb +16 -4
  135. data/lib/generators/pu/saas/welcome/templates/app/controllers/welcome_controller.rb.tt +1 -1
  136. data/lib/plutonium/definition/base.rb +1 -1
  137. data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
  138. data/lib/plutonium/helpers/turbo_helper.rb +30 -0
  139. data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
  140. data/lib/plutonium/resource/controller.rb +1 -0
  141. data/lib/plutonium/resource/controllers/crud_actions.rb +23 -5
  142. data/lib/plutonium/resource/controllers/interactive_actions.rb +3 -3
  143. data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
  144. data/lib/plutonium/resource/policy.rb +7 -0
  145. data/lib/plutonium/routing/mapper_extensions.rb +15 -0
  146. data/lib/plutonium/ui/component/methods.rb +5 -0
  147. data/lib/plutonium/ui/form/base.rb +23 -3
  148. data/lib/plutonium/ui/form/components/json.rb +58 -0
  149. data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
  150. data/lib/plutonium/ui/form/components/secure_association.rb +103 -22
  151. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
  152. data/lib/plutonium/ui/form/interaction.rb +1 -1
  153. data/lib/plutonium/ui/form/resource.rb +0 -4
  154. data/lib/plutonium/ui/form/theme.rb +1 -1
  155. data/lib/plutonium/ui/grid/resource.rb +1 -1
  156. data/lib/plutonium/ui/layout/base.rb +1 -0
  157. data/lib/plutonium/ui/page/base.rb +0 -7
  158. data/lib/plutonium/ui/page/edit.rb +1 -1
  159. data/lib/plutonium/ui/page/index.rb +4 -4
  160. data/lib/plutonium/ui/page/new.rb +1 -1
  161. data/lib/plutonium/ui/table/components/filter_form.rb +12 -4
  162. data/lib/plutonium/ui/table/resource.rb +1 -1
  163. data/lib/plutonium/version.rb +1 -1
  164. data/lib/plutonium.rb +8 -0
  165. data/lib/tasks/release.rake +15 -1
  166. data/package.json +13 -10
  167. data/src/css/slim_select.css +4 -0
  168. data/src/js/controllers/form_controller.js +5 -4
  169. data/src/js/controllers/slim_select_controller.js +61 -0
  170. data/src/js/turbo/turbo_actions.js +33 -0
  171. data/yarn.lock +661 -544
  172. metadata +86 -33
  173. data/.claude/skills/plutonium-assets/SKILL.md +0 -512
  174. data/.claude/skills/plutonium-controller/SKILL.md +0 -396
  175. data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
  176. data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
  177. data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
  178. data/.claude/skills/plutonium-forms/SKILL.md +0 -465
  179. data/.claude/skills/plutonium-installation/SKILL.md +0 -331
  180. data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
  181. data/.claude/skills/plutonium-invites/SKILL.md +0 -408
  182. data/.claude/skills/plutonium-model/SKILL.md +0 -440
  183. data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
  184. data/.claude/skills/plutonium-package/SKILL.md +0 -198
  185. data/.claude/skills/plutonium-policy/SKILL.md +0 -456
  186. data/.claude/skills/plutonium-portal/SKILL.md +0 -410
  187. data/.claude/skills/plutonium-views/SKILL.md +0 -651
  188. data/docs/reference/assets/index.md +0 -496
  189. data/docs/reference/controller/index.md +0 -412
  190. data/docs/reference/definition/actions.md +0 -462
  191. data/docs/reference/definition/fields.md +0 -383
  192. data/docs/reference/definition/index.md +0 -326
  193. data/docs/reference/definition/query.md +0 -351
  194. data/docs/reference/generators/index.md +0 -648
  195. data/docs/reference/interaction/index.md +0 -449
  196. data/docs/reference/model/features.md +0 -248
  197. data/docs/reference/model/index.md +0 -218
  198. data/docs/reference/policy/index.md +0 -456
  199. data/docs/reference/portal/index.md +0 -379
  200. data/docs/reference/views/forms.md +0 -411
  201. data/docs/reference/views/index.md +0 -544
@@ -0,0 +1,189 @@
1
+ # Pages
2
+
3
+ Each definition has nested page classes for index / show / new / edit / interactive-action. Override the ones you need.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ Definition
9
+ ├── IndexPage → renders Table
10
+ ├── ShowPage → renders Display
11
+ ├── NewPage → renders Form
12
+ ├── EditPage → renders Form
13
+ └── InteractiveActionPage → renders Form
14
+ ```
15
+
16
+ ## Page classes
17
+
18
+ ```ruby
19
+ class PostDefinition < ResourceDefinition
20
+ class IndexPage < IndexPage; end
21
+ class ShowPage < ShowPage; end
22
+ class NewPage < NewPage; end
23
+ class EditPage < EditPage; end
24
+ class InteractiveActionPage < InteractiveActionPage; end
25
+ class Form < Form; end
26
+ class Table < Table; end
27
+ class Display < Display; end
28
+ end
29
+ ```
30
+
31
+ ## Page titles, descriptions, breadcrumbs
32
+
33
+ ```ruby
34
+ class PostDefinition < ResourceDefinition
35
+ index_page_title "Blog Posts"
36
+ index_page_description "Manage all published articles"
37
+ show_page_title "Article Details"
38
+ show_page_title -> { current_record!.title } # dynamic
39
+ new_page_title "Create Post"
40
+ edit_page_title -> { "Edit: #{current_record!.title}" }
41
+
42
+ breadcrumbs true # global default
43
+ index_page_breadcrumbs false # per-page override
44
+ show_page_breadcrumbs true
45
+ new_page_breadcrumbs true
46
+ edit_page_breadcrumbs true
47
+ interactive_action_page_breadcrumbs true
48
+ end
49
+ ```
50
+
51
+ ## Page hooks (preferred over `view_template`)
52
+
53
+ Every page inherits these — use them instead of overriding `view_template` to preserve breadcrumbs, header, and DynaFrame behavior:
54
+
55
+ | Hook | Position |
56
+ |---|---|
57
+ | `render_before_header` / `_after_header` | wraps the entire header section |
58
+ | `render_before_breadcrumbs` / `_after_breadcrumbs` | around the breadcrumb row |
59
+ | `render_before_page_header` / `_after_page_header` | around the title + actions block |
60
+ | `render_before_toolbar` / `_after_toolbar` | around the action toolbar |
61
+ | `render_before_content` / `_after_content` | around main content |
62
+ | `render_before_footer` / `_after_footer` | around footer/pagination |
63
+
64
+ Example:
65
+
66
+ ```ruby
67
+ class ShowPage < ShowPage
68
+ private
69
+
70
+ def page_title
71
+ "#{object.title} — #{object.author.name}"
72
+ end
73
+
74
+ def render_before_content
75
+ div(class: "alert alert-info") do
76
+ plain "This post has #{object.comments.count} comments"
77
+ end
78
+ end
79
+
80
+ def render_after_content
81
+ render RelatedPostsComponent.new(post: object)
82
+ end
83
+
84
+ def render_toolbar
85
+ div(class: "flex gap-2") do
86
+ button(class: "pu-btn pu-btn-md pu-btn-secondary") { "Preview" }
87
+ button(class: "pu-btn pu-btn-md pu-btn-primary") { "Publish" }
88
+ end
89
+ end
90
+ end
91
+ ```
92
+
93
+ ## Custom ERB views (full replacement)
94
+
95
+ For total control, drop the page class entirely with an ERB view at the controller path:
96
+
97
+ ```
98
+ app/views/posts/show.html.erb
99
+ packages/admin_portal/app/views/admin_portal/posts/show.html.erb
100
+ ```
101
+
102
+ The default view simply renders the page class:
103
+
104
+ ```erb
105
+ <%= render current_definition.show_page_class.new %>
106
+ ```
107
+
108
+ Mix — keep the default and add chrome around it:
109
+
110
+ ```erb
111
+ <div class="announcement-banner">Special announcement</div>
112
+ <%= render current_definition.show_page_class.new %>
113
+ <div class="related"><%= render partial: "related" %></div>
114
+ ```
115
+
116
+ ## Detecting render context
117
+
118
+ | Helper | True when |
119
+ |---|---|
120
+ | `in_frame?` | Request targets a turbo-frame |
121
+ | `in_modal?` | Request renders inside a modal/slideover (primary or secondary) |
122
+ | `in_secondary_modal?` | Request renders inside the stacked secondary modal |
123
+
124
+ Use to pin action strips, omit nav chrome, or swap layouts.
125
+
126
+ ### Stacked modals (secondary frame)
127
+
128
+ Association inputs include an inline `+` button. When the parent form is itself rendered in a modal, the `+` opens a **second stacked modal** in `Plutonium::REMOTE_MODAL_SECONDARY_FRAME` instead of replacing the primary modal. On successful create, the secondary closes and the primary frame reloads so the new record appears in the select — no developer wiring.
129
+
130
+ For custom flows: `helpers.turbo_stream_close_frame(frame_id)` and `helpers.turbo_stream_reload_frame(frame_id)` are available.
131
+
132
+ See [Forms › Association inputs](./forms#association-inputs).
133
+
134
+ ## Modals & slideovers
135
+
136
+ The framework's `:new` / `:edit` actions render inline inside a modal. Choose the chrome per-resource via the definition:
137
+
138
+ ```ruby
139
+ class PostDefinition < ResourceDefinition
140
+ modal :slideover # default — slide-in panel from the right
141
+ # modal :centered # centered dialog
142
+ # modal false # full standalone pages (no modal)
143
+ end
144
+ ```
145
+
146
+ Custom interactive actions render in their own dialog with their own per-action `modal:` option (`:centered` default, or `:slideover`). See [Resource › Actions](/reference/resource/actions#action-options).
147
+
148
+ ## Tabs on the show page
149
+
150
+ Show pages with `permitted_associations` (see [Behavior › Policy](/reference/behavior/policies#association-permissions)) render a tablist: **Details** tab first, then one tab per association. The active tab is reflected in the URL hash (`#products`, `#refund-requests`) so the page deep-links and the active state survives reload / back navigation. Tab rows scroll horizontally on narrow viewports — they don't wrap.
151
+
152
+ ## Portal-specific overrides
153
+
154
+ Each portal can override page classes independently. The portal definition inherits from the base definition, and its nested classes inherit from the base's nested classes:
155
+
156
+ ```ruby
157
+ class AdminPortal::PostDefinition < ::PostDefinition
158
+ class ShowPage < ShowPage # inherits from ::PostDefinition::ShowPage
159
+ def render_after_content
160
+ super
161
+ render AdminOnlySection.new(post: object)
162
+ end
163
+ end
164
+ end
165
+ ```
166
+
167
+ ## Available context
168
+
169
+ Inside any page / form / display / Phlex component, the same set of helpers is available — model accessors, definition/policy methods, URL helpers, `current_user`. For the full list, see [Behavior › Controllers › Key methods](/reference/behavior/controllers#key-methods) — pages inherit the same surface.
170
+
171
+ In Phlex components, Rails helpers are accessed via the `helpers` proxy:
172
+
173
+ ```ruby
174
+ class MyComponent < Plutonium::UI::Component::Base
175
+ def view_template
176
+ helpers.link_to(...)
177
+ helpers.number_to_currency(...)
178
+ end
179
+ end
180
+ ```
181
+
182
+ ## Related
183
+
184
+ - [Forms](./forms) — Form class, field builder, themes
185
+ - [Displays](./displays) — show-page Display class
186
+ - [Tables](./tables) — index-page Table class
187
+ - [Components](./components) — built-in component kit, custom Phlex components, DynaFrame
188
+ - [Layouts](./layouts) — overall shell, eject, ResourceLayout
189
+ - [Resource › Definition](/reference/resource/definition) — page titles, breadcrumbs, modal mode, metadata panel
@@ -0,0 +1,121 @@
1
+ # Tables
2
+
3
+ The index page's table rendering. Override the `Table` nested class in your definition for custom layouts (e.g. card grids).
4
+
5
+ ## Custom table template
6
+
7
+ ```ruby
8
+ class PostDefinition < ResourceDefinition
9
+ class Table < Table
10
+ def view_template
11
+ render_toolbar # search + view toggle + filter buttons
12
+ render_scopes_pills # scope chips (if any scopes defined)
13
+ render_filter_pills # active-filter chips
14
+
15
+ if collection.empty?
16
+ render_empty_card
17
+ else
18
+ # Replace the table with a card grid
19
+ div(class: "grid grid-cols-3 gap-4") do
20
+ collection.each { |post| render PostCardComponent.new(post:) }
21
+ end
22
+ end
23
+
24
+ render_bulk_actions_toolbar
25
+ render_footer
26
+ end
27
+ end
28
+ end
29
+ ```
30
+
31
+ ## Methods
32
+
33
+ | Method | Purpose |
34
+ |---|---|
35
+ | `render_toolbar` | Search input + view toggle + filter button |
36
+ | `render_scopes_pills` | Quick-filter scope chips (only renders if scopes defined) |
37
+ | `render_filter_pills` | Active-filter chips |
38
+ | `render_bulk_actions_toolbar` | Bulk action bar (only renders when rows selected) |
39
+ | `render_table` | Default table rendering |
40
+ | `render_empty_card` | Empty state |
41
+ | `render_footer` | Pagination |
42
+ | `collection` | Paginated records |
43
+ | `resource_fields` | Column field names |
44
+
45
+ ## Per-column customization
46
+
47
+ Prefer declaring column behavior in the **definition** rather than overriding the entire `Table`:
48
+
49
+ ```ruby
50
+ class PostDefinition < ResourceDefinition
51
+ column :title, align: :start # default
52
+ column :status, align: :center
53
+ column :amount, align: :end
54
+
55
+ # formatter — receives just the value
56
+ column :description, formatter: ->(value) { value&.truncate(30) }
57
+ column :price, formatter: ->(value) { "$%.2f" % value if value }
58
+
59
+ # block — receives the full record
60
+ column :full_name do |record|
61
+ "#{record.first_name} #{record.last_name}"
62
+ end
63
+ end
64
+ ```
65
+
66
+ See [Resource › Definition › Column options](/reference/resource/definition#column-options).
67
+
68
+ ## Grid view
69
+
70
+ For card-based layouts as a switchable alternative to the table, use the built-in Grid view — declare `grid_fields` in the definition:
71
+
72
+ ```ruby
73
+ class UserDefinition < ResourceDefinition
74
+ grid_fields(
75
+ image: :avatar,
76
+ header: :name,
77
+ subheader: :email,
78
+ body: :bio,
79
+ meta: [:role, :status],
80
+ footer: :last_seen_at
81
+ )
82
+
83
+ default_index_view :grid
84
+ end
85
+ ```
86
+
87
+ See [Resource › Definition › Index views](/reference/resource/definition#index-views-table-grid). You only need a custom `Table` class when you want something neither Table nor Grid covers.
88
+
89
+ ## Theming
90
+
91
+ Override the theme via a nested `Theme` class:
92
+
93
+ ```ruby
94
+ class PostDefinition < ResourceDefinition
95
+ class Table < Table
96
+ class Theme < Plutonium::UI::Table::Theme
97
+ def self.theme
98
+ super.merge(
99
+ wrapper: "pu-table-wrapper",
100
+ base: "pu-table",
101
+ header: "pu-table-header",
102
+ header_cell: "pu-table-header-cell",
103
+ body_row: "pu-table-body-row",
104
+ body_cell: "pu-table-body-cell"
105
+ )
106
+ end
107
+ end
108
+ end
109
+ end
110
+ ```
111
+
112
+ ### Theme keys
113
+
114
+ `wrapper`, `base`, `header`, `header_cell`, `body_row`, `body_cell`, `sort_icon`.
115
+
116
+ ## Related
117
+
118
+ - [Pages](./pages) — `IndexPage` render hooks (a lighter alternative for top/bottom chrome)
119
+ - [Components](./components) — `PostCardComponent` and other reusable Phlex pieces
120
+ - [Resource › Definition](/reference/resource/definition) — column configuration, grid view
121
+ - [Resource › Query](/reference/resource/query) — search, filters, scopes, sort