plutonium 0.27.0 → 0.29.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/app/assets/plutonium.css +2 -2
  4. data/app/assets/plutonium.js +13 -3
  5. data/app/assets/plutonium.js.map +3 -3
  6. data/app/assets/plutonium.min.js +1 -1
  7. data/app/assets/plutonium.min.js.map +2 -2
  8. data/app/views/resource/_resource_details.rabl +44 -1
  9. data/app/views/resource/index.rabl +44 -1
  10. data/app/views/resource/show.rabl +44 -1
  11. data/docs/guide/theming.md +431 -0
  12. data/lib/plutonium/core/controller.rb +5 -0
  13. data/lib/plutonium/interaction/response/redirect.rb +8 -0
  14. data/lib/plutonium/resource/controller.rb +7 -3
  15. data/lib/plutonium/resource/controllers/crud_actions.rb +1 -0
  16. data/lib/plutonium/ui/action_button.rb +11 -5
  17. data/lib/plutonium/ui/block.rb +4 -1
  18. data/lib/plutonium/ui/breadcrumbs.rb +10 -8
  19. data/lib/plutonium/ui/color_mode_selector.rb +2 -2
  20. data/lib/plutonium/ui/component/behaviour.rb +8 -0
  21. data/lib/plutonium/ui/component/kit.rb +1 -1
  22. data/lib/plutonium/ui/component/theme.rb +47 -0
  23. data/lib/plutonium/ui/display/components/attachment.rb +2 -2
  24. data/lib/plutonium/ui/display/theme.rb +16 -16
  25. data/lib/plutonium/ui/empty_card.rb +5 -2
  26. data/lib/plutonium/ui/form/components/key_value_store.rb +11 -11
  27. data/lib/plutonium/ui/form/components/secure_association.rb +2 -2
  28. data/lib/plutonium/ui/form/components/uppy.rb +5 -5
  29. data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +5 -5
  30. data/lib/plutonium/ui/form/query.rb +11 -11
  31. data/lib/plutonium/ui/form/resource.rb +2 -2
  32. data/lib/plutonium/ui/form/theme.rb +17 -17
  33. data/lib/plutonium/ui/layout/base.rb +2 -2
  34. data/lib/plutonium/ui/layout/header.rb +10 -7
  35. data/lib/plutonium/ui/layout/rodauth_layout.rb +5 -5
  36. data/lib/plutonium/ui/layout/sidebar.rb +2 -2
  37. data/lib/plutonium/ui/nav_grid_menu.rb +6 -6
  38. data/lib/plutonium/ui/nav_user.rb +8 -7
  39. data/lib/plutonium/ui/page/interactive_action.rb +4 -4
  40. data/lib/plutonium/ui/page_header.rb +7 -4
  41. data/lib/plutonium/ui/panel.rb +3 -3
  42. data/lib/plutonium/ui/sidebar_menu.rb +12 -12
  43. data/lib/plutonium/ui/skeleton_table.rb +8 -8
  44. data/lib/plutonium/ui/tab_list.rb +5 -3
  45. data/lib/plutonium/ui/table/components/attachment.rb +2 -2
  46. data/lib/plutonium/ui/table/components/pagy_info.rb +3 -3
  47. data/lib/plutonium/ui/table/components/pagy_pagination.rb +3 -3
  48. data/lib/plutonium/ui/table/components/scopes_bar.rb +14 -14
  49. data/lib/plutonium/ui/table/display_theme.rb +3 -3
  50. data/lib/plutonium/ui/table/resource.rb +2 -2
  51. data/lib/plutonium/ui/table/theme.rb +13 -13
  52. data/lib/plutonium/version.rb +1 -1
  53. data/lib/tasks/release.rake +45 -5
  54. data/package.json +1 -1
  55. data/src/css/core.css +2 -2
  56. data/src/css/easymde.css +8 -8
  57. data/src/css/intl_tel_input.css +7 -7
  58. data/src/css/slim_select.css +5 -5
  59. data/src/js/controllers/color_mode_controller.js +19 -3
  60. data/tailwind.options.js +75 -47
  61. metadata +4 -2
@@ -1,5 +1,48 @@
1
1
  attributes :id
2
- attributes(*permitted_attributes)
3
2
  attributes :created_at, :updated_at
4
3
 
4
+ node(:sgid) { |resource| resource.to_signed_global_id.to_s }
5
+
6
+ # Serialize attributes, converting associations to nested objects
7
+ permitted_attributes.each do |attr|
8
+ reflection = resource_class.reflect_on_association(attr)
9
+
10
+ if reflection
11
+ # Serialize association as ID(s) and sgid(s)
12
+ case reflection.macro
13
+ when :belongs_to
14
+ # Use foreign key directly for belongs_to
15
+ node(:"#{attr}_id") do |resource|
16
+ resource.public_send(reflection.foreign_key)
17
+ end
18
+ # Include sgid for form submissions
19
+ node(:"#{attr}_sgid") do |resource|
20
+ resource.public_send(:"#{attr}_sgid")&.to_s
21
+ end
22
+ when :has_many, :has_and_belongs_to_many
23
+ # Return array of IDs for collections
24
+ node(:"#{attr.to_s.singularize}_ids") do |resource|
25
+ resource.public_send(attr).pluck(:id)
26
+ end
27
+ # Include sgids for form submissions
28
+ node(:"#{attr.to_s.singularize}_sgids") do |resource|
29
+ resource.public_send(:"#{attr.to_s.singularize}_sgids").map(&:to_s)
30
+ end
31
+ when :has_one
32
+ # Return single ID for has_one
33
+ node(:"#{attr}_id") do |resource|
34
+ associated_record = resource.public_send(attr)
35
+ associated_record&.id
36
+ end
37
+ # Include sgid for form submissions
38
+ node(:"#{attr}_sgid") do |resource|
39
+ resource.public_send(:"#{attr}_sgid")&.to_s
40
+ end
41
+ end
42
+ else
43
+ # Regular attribute
44
+ attributes attr
45
+ end
46
+ end
47
+
5
48
  node(:url) { |resource| resource_url_for(resource) }
@@ -1,7 +1,50 @@
1
1
  collection @resource_records, root: resource_class.to_s.demodulize.underscore.pluralize.to_sym, object_root: false
2
2
 
3
3
  attributes :id
4
- attributes(*current_policy.permitted_attributes_for_index)
5
4
  attributes :created_at, :updated_at
6
5
 
6
+ node(:sgid) { |resource| resource.to_signed_global_id.to_s }
7
+
8
+ # Serialize attributes, converting associations to nested objects
9
+ current_policy.permitted_attributes_for_index.each do |attr|
10
+ reflection = resource_class.reflect_on_association(attr)
11
+
12
+ if reflection
13
+ # Serialize association as ID(s) and sgid(s)
14
+ case reflection.macro
15
+ when :belongs_to
16
+ # Use foreign key directly for belongs_to
17
+ node(:"#{attr}_id") do |resource|
18
+ resource.public_send(reflection.foreign_key)
19
+ end
20
+ # Include sgid for form submissions
21
+ node(:"#{attr}_sgid") do |resource|
22
+ resource.public_send(:"#{attr}_sgid")&.to_s
23
+ end
24
+ when :has_many, :has_and_belongs_to_many
25
+ # Return array of IDs for collections
26
+ node(:"#{attr.to_s.singularize}_ids") do |resource|
27
+ resource.public_send(attr).pluck(:id)
28
+ end
29
+ # Include sgids for form submissions
30
+ node(:"#{attr.to_s.singularize}_sgids") do |resource|
31
+ resource.public_send(:"#{attr.to_s.singularize}_sgids").map(&:to_s)
32
+ end
33
+ when :has_one
34
+ # Return single ID for has_one
35
+ node(:"#{attr}_id") do |resource|
36
+ associated_record = resource.public_send(attr)
37
+ associated_record&.id
38
+ end
39
+ # Include sgid for form submissions
40
+ node(:"#{attr}_sgid") do |resource|
41
+ resource.public_send(:"#{attr}_sgid")&.to_s
42
+ end
43
+ end
44
+ else
45
+ # Regular attribute
46
+ attributes attr
47
+ end
48
+ end
49
+
7
50
  node(:url) { |resource| resource_url_for(resource) }
@@ -1,7 +1,50 @@
1
1
  object @resource_record
2
2
 
3
3
  attributes :id
4
- attributes(*current_policy.permitted_attributes_for_show)
5
4
  attributes :created_at, :updated_at
6
5
 
6
+ node(:sgid) { |resource| resource.to_signed_global_id.to_s }
7
+
8
+ # Serialize attributes, converting associations to nested objects
9
+ current_policy.permitted_attributes_for_show.each do |attr|
10
+ reflection = resource_class.reflect_on_association(attr)
11
+
12
+ if reflection
13
+ # Serialize association as ID(s) and sgid(s)
14
+ case reflection.macro
15
+ when :belongs_to
16
+ # Use foreign key directly for belongs_to
17
+ node(:"#{attr}_id") do |resource|
18
+ resource.public_send(reflection.foreign_key)
19
+ end
20
+ # Include sgid for form submissions
21
+ node(:"#{attr}_sgid") do |resource|
22
+ resource.public_send(:"#{attr}_sgid")&.to_s
23
+ end
24
+ when :has_many, :has_and_belongs_to_many
25
+ # Return array of IDs for collections
26
+ node(:"#{attr.to_s.singularize}_ids") do |resource|
27
+ resource.public_send(attr).pluck(:id)
28
+ end
29
+ # Include sgids for form submissions
30
+ node(:"#{attr.to_s.singularize}_sgids") do |resource|
31
+ resource.public_send(:"#{attr.to_s.singularize}_sgids").map(&:to_s)
32
+ end
33
+ when :has_one
34
+ # Return single ID for has_one
35
+ node(:"#{attr}_id") do |resource|
36
+ associated_record = resource.public_send(attr)
37
+ associated_record&.id
38
+ end
39
+ # Include sgid for form submissions
40
+ node(:"#{attr}_sgid") do |resource|
41
+ resource.public_send(:"#{attr}_sgid")&.to_s
42
+ end
43
+ end
44
+ else
45
+ # Regular attribute
46
+ attributes attr
47
+ end
48
+ end
49
+
7
50
  node(:url) { |resource| resource_url_for(resource) }
@@ -0,0 +1,431 @@
1
+ # Theming Guide
2
+
3
+ Plutonium uses a semantic design token system built on Tailwind CSS, making it easy to customize the appearance of your application while maintaining design consistency.
4
+
5
+ ## Design Philosophy
6
+
7
+ Plutonium's theming system is built on these principles:
8
+
9
+ - **Semantic tokens** - Colors and spacing have meaningful names (e.g., `surface`, `elevated`) rather than generic values
10
+ - **Cross-property consistency** - The same size name means the same value across all utilities (`p-md` = `gap-md` = `my-md`)
11
+ - **Minimal, modern aesthetic** - Subtle corners, clean lines, and restrained use of shadows
12
+ - **Easy customization** - Override tokens in your Tailwind config to theme the entire application
13
+
14
+ ## Semantic Design Tokens
15
+
16
+ ### Spacing Scale
17
+
18
+ Plutonium extends Tailwind's spacing scale with semantic values:
19
+
20
+ ```javascript
21
+ spacing: {
22
+ 'xs': '0.5rem', // 8px - extra small spacing
23
+ 'sm': '0.75rem', // 12px - small spacing (inputs, buttons, small gaps)
24
+ 'md': '1rem', // 16px - medium spacing (cards, tabs, standard gaps)
25
+ 'lg': '1.5rem', // 24px - large spacing (forms, displays, large spacing)
26
+ 'xl': '2rem', // 32px - extra large spacing
27
+ '2xl': '2.5rem', // 40px - 2x extra large spacing
28
+ '3xl': '3rem', // 48px - 3x extra large spacing
29
+ }
30
+ ```
31
+
32
+ These work across **all** spacing utilities:
33
+ - Padding: `p-md`, `px-sm`, `py-lg`
34
+ - Margin: `m-md`, `mx-sm`, `my-lg`
35
+ - Gap: `gap-md`, `gap-x-sm`, `gap-y-lg`
36
+ - Space: `space-x-md`, `space-y-sm`
37
+
38
+ ### Background Colors
39
+
40
+ Semantic background colors provide consistent theming across light and dark modes:
41
+
42
+ ```javascript
43
+ colors: {
44
+ // Semantic background colors
45
+ surface: {
46
+ DEFAULT: '#ffffff', // Light mode: cards, forms, tables, panels
47
+ dark: '#1f2937', // Dark mode: gray-800
48
+ },
49
+ page: {
50
+ DEFAULT: 'rgb(248 248 248)', // Light mode: page background
51
+ dark: '#111827', // Dark mode: gray-900
52
+ },
53
+ elevated: {
54
+ DEFAULT: 'rgb(244 244 245)', // Light mode: subtle elevation
55
+ dark: '#374151', // Dark mode: gray-700
56
+ },
57
+ interactive: {
58
+ DEFAULT: 'rgb(244 244 245)', // Light mode: hover states
59
+ dark: '#374151', // Dark mode: gray-700
60
+ },
61
+ }
62
+ ```
63
+
64
+ **Where each color is used:**
65
+ - `surface` - Cards, forms, tables, panels, modals
66
+ - `page` - Main page background
67
+ - `elevated` - Slightly elevated elements like dropdowns, selected items
68
+ - `interactive` - Hover and focus states
69
+
70
+ ### Border Radius
71
+
72
+ Plutonium uses subtle border radius values for a modern, minimal look:
73
+
74
+ - `rounded-sm` (2px) - Most UI elements (buttons, inputs, cards)
75
+ - `rounded` (4px) - Slightly larger elements
76
+ - `rounded-lg` (8px) - Special cases needing more rounding
77
+
78
+ ## Getting Started
79
+
80
+ By default, Plutonium is completely self-contained and uses its own bundled assets - you don't need to do anything to get started. The gem includes pre-compiled CSS and JavaScript that work out of the box.
81
+
82
+ ### When to Integrate Assets
83
+
84
+ You only need to integrate Plutonium's assets into your application if you want to:
85
+ - Customize the theme (colors, spacing, fonts)
86
+ - Override component styles
87
+ - Extend Tailwind with your own utilities
88
+
89
+ To integrate Plutonium's assets with your Rails application, run:
90
+
91
+ ```bash
92
+ rails generate pu:core:assets
93
+ ```
94
+
95
+ This generator will:
96
+ - Install required npm packages (`@radioactive-labs/plutonium`, Tailwind CSS, PostCSS)
97
+ - Create a `tailwind.config.js` that loads Plutonium's theme configuration
98
+ - Configure your application to import Plutonium's CSS and register its Stimulus controllers
99
+ - Set up the build pipeline to compile everything together
100
+
101
+ After running the generator, you can customize the theme as described below.
102
+
103
+ ## Customizing Your Theme
104
+
105
+ To customize Plutonium's theme, you need to extend the Tailwind configuration in your application (created by `rails generate pu:core:assets`):
106
+
107
+ ```javascript
108
+ // tailwind.config.js
109
+ const { execSync } = require('child_process');
110
+ const plutoniumGemPath = execSync("bundle show plutonium").toString().trim();
111
+ const plutoniumTailwindConfig = require(`${plutoniumGemPath}/tailwind.options.js`)
112
+ const tailwindPlugin = require('tailwindcss/plugin')
113
+
114
+ module.exports = {
115
+ darkMode: 'class',
116
+ plugins: [
117
+ // Add your custom plugins here
118
+ ].concat(plutoniumTailwindConfig.plugins.map(function (plugin) {
119
+ switch (typeof plugin) {
120
+ case "function":
121
+ return tailwindPlugin(plugin)
122
+ case "string":
123
+ return require(plugin)
124
+ default:
125
+ throw Error(`unsupported plugin: ${plugin}: ${(typeof plugin)}`)
126
+ }
127
+ })),
128
+ theme: plutoniumTailwindConfig.merge(
129
+ plutoniumTailwindConfig.theme,
130
+ {
131
+ extend: {
132
+ colors: {
133
+ // Override brand colors - example using blue
134
+ primary: {
135
+ 50: '#eff6ff',
136
+ 100: '#dbeafe',
137
+ 200: '#bfdbfe',
138
+ 300: '#93c5fd',
139
+ 400: '#60a5fa',
140
+ 500: '#3b82f6',
141
+ 600: '#2563eb',
142
+ 700: '#1d4ed8',
143
+ 800: '#1e40af',
144
+ 900: '#1e3a8a',
145
+ 950: '#172554',
146
+ },
147
+ },
148
+ },
149
+ },
150
+ ),
151
+ content: [
152
+ `${__dirname}/app/**/*.{erb,haml,html,slim,rb}`,
153
+ `${__dirname}/app/javascript/**/*.js`,
154
+ ].concat(plutoniumTailwindConfig.content),
155
+ }
156
+ ```
157
+
158
+ ### Dark Mode Customization
159
+
160
+ Customize just the dark mode colors:
161
+
162
+ ```javascript
163
+ theme: plutoniumTailwindConfig.merge(
164
+ plutoniumTailwindConfig.theme,
165
+ {
166
+ extend: {
167
+ colors: {
168
+ surface: {
169
+ dark: '#1e293b', // slate-800 for a cooler dark mode
170
+ },
171
+ page: {
172
+ dark: '#0f172a', // slate-900
173
+ },
174
+ elevated: {
175
+ dark: '#334155', // slate-700
176
+ },
177
+ interactive: {
178
+ dark: '#475569', // slate-600 for more contrast on hover
179
+ },
180
+ },
181
+ },
182
+ }
183
+ )
184
+ ```
185
+
186
+ ### Brand Color Customization
187
+
188
+ Replace Plutonium's turquoise/navy palette with your brand colors:
189
+
190
+ ```javascript
191
+ theme: plutoniumTailwindConfig.merge(
192
+ plutoniumTailwindConfig.theme,
193
+ {
194
+ extend: {
195
+ colors: {
196
+ primary: {
197
+ // Your primary brand color scale
198
+ 50: '#fef2f2',
199
+ 100: '#fee2e2',
200
+ 500: '#ef4444', // Your primary color
201
+ 900: '#7f1d1d',
202
+ },
203
+ secondary: {
204
+ // Your secondary brand color scale
205
+ 50: '#f0fdf4',
206
+ 100: '#dcfce7',
207
+ 500: '#22c55e', // Your secondary color
208
+ 900: '#14532d',
209
+ },
210
+ },
211
+ },
212
+ }
213
+ )
214
+ ```
215
+
216
+ ### Component-Level Theming
217
+
218
+ Every Plutonium component supports CSS class customization through the theme system. Component classes follow the pattern:
219
+
220
+ ```
221
+ pu-{component}[-{variant}][-{element}]
222
+ ```
223
+
224
+ #### Available Component Classes
225
+
226
+ **Forms:**
227
+ - `pu-form` - Form container
228
+ - `pu-form-input` - Text input fields
229
+ - `pu-form-label` - Input labels
230
+ - `pu-form-hint` - Help text below inputs
231
+ - `pu-form-error` - Validation error messages
232
+ - `pu-form-button` - Primary form button
233
+ - `pu-form-button_secondary` - Secondary form button
234
+ - `pu-form-fieldset` - Field grouping container
235
+ - `pu-form-fields` - Fields wrapper
236
+
237
+ **Tables:**
238
+ - `pu-table` - Table container
239
+ - `pu-table-wrapper` - Scrollable wrapper
240
+ - `pu-table-header` - Table header row
241
+ - `pu-table-header-cell` - Header cell
242
+ - `pu-table-body` - Table body
243
+ - `pu-table-row` - Table row
244
+ - `pu-table-cell` - Table cell
245
+ - `pu-table-footer` - Footer with pagination
246
+
247
+ **Display (Detail Views):**
248
+ - `pu-display` - Display container
249
+ - `pu-display-fields` - Fields grid wrapper
250
+ - `pu-display-field` - Individual field wrapper
251
+ - `pu-display-label` - Field label
252
+ - `pu-display-value` - Field value
253
+
254
+ **Layout:**
255
+ - `pu-layout-body` - Main layout body
256
+ - `pu-layout-main` - Main content area
257
+ - `pu-layout-header` - Page header
258
+ - `pu-layout-sidebar` - Sidebar navigation
259
+
260
+ **Navigation:**
261
+ - `pu-breadcrumbs` - Breadcrumb navigation
262
+ - `pu-nav-menu` - Navigation menu
263
+ - `pu-nav-user` - User menu dropdown
264
+ - `pu-sidebar-menu` - Sidebar menu items
265
+
266
+ **Panels:**
267
+ - `pu-panel` - Generic panel container
268
+ - `pu-panel-header` - Panel header
269
+ - `pu-panel-body` - Panel content
270
+
271
+ #### Customization Examples
272
+
273
+ ```css
274
+ /* Custom form styling */
275
+ .pu-form {
276
+ @apply shadow-xl border-2;
277
+ }
278
+
279
+ .pu-form-input {
280
+ @apply text-lg rounded-lg;
281
+ }
282
+
283
+ .pu-form-error {
284
+ @apply text-sm font-semibold;
285
+ }
286
+
287
+ /* Custom table styling */
288
+ .pu-table-row:hover {
289
+ @apply bg-primary-50 dark:bg-primary-950;
290
+ }
291
+
292
+ .pu-table-header-cell {
293
+ @apply font-bold uppercase tracking-wider;
294
+ }
295
+
296
+ /* Custom display styling */
297
+ .pu-display-field {
298
+ @apply border-l-4 border-primary-500 pl-4;
299
+ }
300
+
301
+ .pu-display-label {
302
+ @apply text-xs uppercase tracking-wide text-gray-500;
303
+ }
304
+ ```
305
+
306
+ You can also use Ruby to customize theme classes programmatically:
307
+
308
+ ```ruby
309
+ # In your resource controller or view
310
+ class Admin::UsersController < Plutonium::ResourceController
311
+ def self.form_theme
312
+ {
313
+ input: "pu-form-input w-full rounded-lg border-2 border-primary-300"
314
+ }
315
+ end
316
+ end
317
+ ```
318
+
319
+ ## Common Theming Scenarios
320
+
321
+ ### Warmer Color Palette
322
+
323
+ ```javascript
324
+ theme: plutoniumTailwindConfig.merge(
325
+ plutoniumTailwindConfig.theme,
326
+ {
327
+ extend: {
328
+ colors: {
329
+ surface: {
330
+ DEFAULT: '#fefefe',
331
+ dark: '#1c1917', // stone-900
332
+ },
333
+ page: {
334
+ DEFAULT: '#fafaf9', // stone-50
335
+ dark: '#0c0a09', // stone-950
336
+ },
337
+ elevated: {
338
+ DEFAULT: '#f5f5f4', // stone-100
339
+ dark: '#292524', // stone-800
340
+ },
341
+ },
342
+ },
343
+ }
344
+ )
345
+ ```
346
+
347
+ ### High Contrast Mode
348
+
349
+ ```javascript
350
+ theme: plutoniumTailwindConfig.merge(
351
+ plutoniumTailwindConfig.theme,
352
+ {
353
+ extend: {
354
+ colors: {
355
+ surface: {
356
+ DEFAULT: '#ffffff',
357
+ dark: '#000000', // Pure black
358
+ },
359
+ page: {
360
+ DEFAULT: '#fafafa',
361
+ dark: '#0a0a0a', // Almost black
362
+ },
363
+ elevated: {
364
+ DEFAULT: '#f0f0f0',
365
+ dark: '#1a1a1a',
366
+ },
367
+ interactive: {
368
+ DEFAULT: '#e5e5e5',
369
+ dark: '#2a2a2a',
370
+ },
371
+ },
372
+ },
373
+ }
374
+ )
375
+ ```
376
+
377
+ ### Monochrome Theme
378
+
379
+ ```javascript
380
+ theme: plutoniumTailwindConfig.merge(
381
+ plutoniumTailwindConfig.theme,
382
+ {
383
+ extend: {
384
+ colors: {
385
+ primary: {
386
+ 50: '#fafafa',
387
+ 100: '#f5f5f5',
388
+ 500: '#737373',
389
+ 900: '#171717',
390
+ },
391
+ secondary: {
392
+ 50: '#f5f5f5',
393
+ 100: '#e5e5e5',
394
+ 500: '#525252',
395
+ 900: '#0a0a0a',
396
+ },
397
+ },
398
+ },
399
+ }
400
+ )
401
+ ```
402
+
403
+ ## Tips and Best Practices
404
+
405
+ 1. **Use semantic tokens** - Prefer `bg-surface` over `bg-white` so dark mode works automatically
406
+ 2. **Test both modes** - Always verify your changes in both light and dark mode
407
+ 3. **Maintain contrast** - Ensure text remains readable against your background colors
408
+ 4. **Extend, don't replace** - Use the `merge` helper to extend rather than replace the theme
409
+ 5. **Stay consistent** - Use the same spacing scale throughout your application
410
+
411
+ ## Migration from Hardcoded Values
412
+
413
+ If you have existing components using hardcoded Tailwind values, migrate them to semantic tokens:
414
+
415
+ **Before:**
416
+ ```ruby
417
+ div(class: "bg-white dark:bg-gray-800 p-4 rounded-lg")
418
+ ```
419
+
420
+ **After:**
421
+ ```ruby
422
+ div(class: "bg-surface dark:bg-surface-dark p-md rounded-sm")
423
+ ```
424
+
425
+ This ensures your components automatically adapt to theme changes.
426
+
427
+ ## Further Resources
428
+
429
+ - [Tailwind CSS Customization](https://tailwindcss.com/docs/configuration)
430
+ - [Dark Mode Guide](https://tailwindcss.com/docs/dark-mode)
431
+ - [Color Palette Generator](https://uicolors.app/create)
@@ -100,6 +100,11 @@ module Plutonium
100
100
  url_args[scoped_entity_param_key] = current_scoped_entity
101
101
  end
102
102
 
103
+ # Preserve the request format unless explicitly specified
104
+ if !url_args.key?(:format) && request.present? && request.format.present? && request.format.symbol != :html
105
+ url_args[:format] = request.format.symbol
106
+ end
107
+
103
108
  url_args
104
109
  end
105
110
 
@@ -17,6 +17,14 @@ module Plutonium
17
17
  redirect_options = @options
18
18
 
19
19
  controller.instance_eval do
20
+ # Preserve the request format unless explicitly specified
21
+ url_options = redirect_args.last.is_a?(Hash) ? redirect_args.last : {}
22
+ if !url_options.key?(:format) && request.format.symbol != :html
23
+ url_options = url_options.merge(format: request.format.symbol)
24
+ redirect_args = [*redirect_args[0...-1], url_options] if redirect_args.last.is_a?(Hash)
25
+ redirect_args = [*redirect_args, url_options] unless redirect_args.last.is_a?(Hash)
26
+ end
27
+
20
28
  url = url_for(*redirect_args)
21
29
 
22
30
  respond_to do |format|
@@ -20,6 +20,9 @@ module Plutonium
20
20
  after_action { pagy_headers_merge(@pagy) if @pagy }
21
21
 
22
22
  helper_method :current_parent, :resource_record!, :resource_record?, :resource_param_key, :resource_class
23
+
24
+ # Use class_attribute for proper inheritance
25
+ class_attribute :_resource_class, instance_accessor: false
23
26
  end
24
27
 
25
28
  class_methods do
@@ -28,15 +31,16 @@ module Plutonium
28
31
  # Sets the resource class for the controller
29
32
  # @param [ActiveRecord::Base] resource_class The resource class
30
33
  def controller_for(resource_class)
31
- @resource_class = resource_class
34
+ self._resource_class = resource_class
32
35
  end
33
36
 
34
37
  # Gets the resource class for the controller
35
38
  # @return [ActiveRecord::Base] The resource class
36
39
  def resource_class
37
- return @resource_class if @resource_class
40
+ return _resource_class if _resource_class
38
41
 
39
- name.to_s.gsub(/^#{current_package}::/, "").gsub(/Controller$/, "").classify.constantize
42
+ # Use singularize + camelize to respect custom inflections
43
+ name.to_s.gsub(/^#{current_package}::/, "").gsub(/Controller$/, "").singularize.camelize.constantize
40
44
  rescue NameError
41
45
  raise NameError, "Failed to determine the resource class. Please call `controller_for(MyResource)` in #{name}."
42
46
  end
@@ -54,6 +54,7 @@ module Plutonium
54
54
  notice: "#{resource_class.model_name.human} was successfully created."
55
55
  end
56
56
  format.any do
57
+ @current_policy = nil # Reset cached policy so it uses the instance instead of class
57
58
  render :show,
58
59
  status: :created,
59
60
  location: redirect_url_after_submit
@@ -70,23 +70,29 @@ module Plutonium
70
70
  base_classes,
71
71
  color_classes,
72
72
  size_classes,
73
- -> { @action.icon && @variant != :table } => "space-x-1"
73
+ -> { @action.icon && @variant != :table } => "space-x-xs"
74
74
  )
75
75
  end
76
76
 
77
77
  def base_classes
78
78
  if @variant == :table
79
- "inline-flex items-center justify-center py-1 px-2 rounded-lg focus:outline-none focus:ring-2"
79
+ tokens(
80
+ theme_class(:button, variant: :table),
81
+ "inline-flex items-center justify-center py-xs px-sm rounded-sm focus:outline-none focus:ring-2"
82
+ )
80
83
  else
81
- "flex items-center justify-center px-4 py-2 text-sm font-medium rounded-lg focus:outline-none focus:ring-4"
84
+ tokens(
85
+ theme_class(:button),
86
+ "flex items-center justify-center px-md py-sm text-sm font-medium rounded-sm focus:outline-none focus:ring-4"
87
+ )
82
88
  end
83
89
  end
84
90
 
85
91
  def icon_classes
86
92
  if @variant == :table
87
- "h-4 w-4 mr-1"
93
+ "h-4 w-4 mr-xs"
88
94
  else
89
- "h-3.5 w-3.5 -ml-1"
95
+ "h-3.5 w-3.5 -ml-xs"
90
96
  end
91
97
  end
92
98
 
@@ -4,7 +4,10 @@ module Plutonium
4
4
  def view_template(&)
5
5
  raise ArgumentError, "Block requires a content block" unless block_given?
6
6
 
7
- div class: "relative bg-white dark:bg-gray-800 shadow-md sm:rounded-lg my-3" do
7
+ div class: tokens(
8
+ theme_class(:block),
9
+ "relative bg-surface dark:bg-surface-dark shadow-md sm:rounded-sm my-sm"
10
+ ) do
8
11
  yield
9
12
  end
10
13
  end