plutonium 0.49.1 → 0.50.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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium-definition/SKILL.md +87 -2
  3. data/.claude/skills/plutonium-installation/SKILL.md +6 -0
  4. data/.claude/skills/plutonium-views/SKILL.md +59 -0
  5. data/CHANGELOG.md +12 -0
  6. data/app/assets/plutonium.css +2 -2
  7. data/app/assets/plutonium.js +369 -25
  8. data/app/assets/plutonium.js.map +4 -4
  9. data/app/assets/plutonium.min.js +45 -45
  10. data/app/assets/plutonium.min.js.map +4 -4
  11. data/app/views/plutonium/_resource_header.html.erb +4 -4
  12. data/app/views/plutonium/_resource_sidebar.html.erb +9 -9
  13. data/app/views/resource/_resource_grid.html.erb +1 -0
  14. data/config/brakeman.ignore +25 -2
  15. data/docs/reference/definition/actions.md +14 -1
  16. data/docs/reference/definition/index.md +58 -0
  17. data/docs/reference/views/index.md +43 -0
  18. data/docs/superpowers/plans/2026-05-07-ui-layout-overhaul.md +841 -0
  19. data/docs/superpowers/plans/2026-05-07-ui-layout-overhaul.md.tasks.json +103 -0
  20. data/docs/superpowers/specs/2026-05-07-ui-layout-overhaul-design.md +270 -0
  21. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  22. data/lib/generators/pu/core/install/templates/config/initializers/plutonium.rb +1 -0
  23. data/lib/generators/pu/core/update/update_generator.rb +20 -0
  24. data/lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb +54 -5
  25. data/lib/plutonium/action/base.rb +44 -1
  26. data/lib/plutonium/action/interactive.rb +1 -1
  27. data/lib/plutonium/configuration.rb +4 -0
  28. data/lib/plutonium/definition/actions.rb +3 -0
  29. data/lib/plutonium/definition/base.rb +8 -0
  30. data/lib/plutonium/definition/metadata.rb +40 -0
  31. data/lib/plutonium/definition/views.rb +94 -0
  32. data/lib/plutonium/helpers/turbo_helper.rb +1 -1
  33. data/lib/plutonium/interaction/response/redirect.rb +1 -1
  34. data/lib/plutonium/query/base.rb +8 -0
  35. data/lib/plutonium/query/filters/association.rb +30 -8
  36. data/lib/plutonium/query/filters/boolean.rb +5 -0
  37. data/lib/plutonium/resource/controllers/presentable.rb +11 -2
  38. data/lib/plutonium/resource/definition.rb +42 -0
  39. data/lib/plutonium/resource/query_object.rb +64 -6
  40. data/lib/plutonium/testing/resource_definition.rb +2 -2
  41. data/lib/plutonium/ui/action_button.rb +4 -2
  42. data/lib/plutonium/ui/component/kit.rb +12 -0
  43. data/lib/plutonium/ui/display/base.rb +3 -1
  44. data/lib/plutonium/ui/display/resource.rb +109 -25
  45. data/lib/plutonium/ui/display/theme.rb +2 -1
  46. data/lib/plutonium/ui/dyna_frame/content.rb +8 -14
  47. data/lib/plutonium/ui/empty_card.rb +1 -1
  48. data/lib/plutonium/ui/form/base.rb +29 -1
  49. data/lib/plutonium/ui/form/components/hidden_wrapper.rb +25 -0
  50. data/lib/plutonium/ui/form/components/resource_select.rb +79 -1
  51. data/lib/plutonium/ui/form/components/secure_association.rb +7 -2
  52. data/lib/plutonium/ui/form/components/sticky_footer.rb +17 -0
  53. data/lib/plutonium/ui/form/resource.rb +48 -9
  54. data/lib/plutonium/ui/form/theme.rb +1 -1
  55. data/lib/plutonium/ui/frame_navigator_panel.rb +7 -4
  56. data/lib/plutonium/ui/grid/card.rb +235 -0
  57. data/lib/plutonium/ui/grid/resource.rb +149 -0
  58. data/lib/plutonium/ui/layout/base.rb +37 -1
  59. data/lib/plutonium/ui/layout/header.rb +1 -2
  60. data/lib/plutonium/ui/layout/icon_rail.rb +212 -0
  61. data/lib/plutonium/ui/layout/resource_layout.rb +10 -3
  62. data/lib/plutonium/ui/layout/sidebar.rb +12 -24
  63. data/lib/plutonium/ui/layout/topbar.rb +100 -0
  64. data/lib/plutonium/ui/modal/base.rb +109 -0
  65. data/lib/plutonium/ui/modal/centered.rb +21 -0
  66. data/lib/plutonium/ui/modal/slideover.rb +26 -0
  67. data/lib/plutonium/ui/page/base.rb +25 -6
  68. data/lib/plutonium/ui/page/edit.rb +13 -1
  69. data/lib/plutonium/ui/page/index.rb +40 -1
  70. data/lib/plutonium/ui/page/interactive_action.rb +8 -39
  71. data/lib/plutonium/ui/page/new.rb +13 -1
  72. data/lib/plutonium/ui/page/show.rb +8 -1
  73. data/lib/plutonium/ui/page_header.rb +8 -13
  74. data/lib/plutonium/ui/panel.rb +10 -19
  75. data/lib/plutonium/ui/sidebar_menu.rb +2 -25
  76. data/lib/plutonium/ui/tab_list.rb +29 -7
  77. data/lib/plutonium/ui/table/base.rb +106 -0
  78. data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +12 -4
  79. data/lib/plutonium/ui/table/components/filter_form.rb +171 -0
  80. data/lib/plutonium/ui/table/components/filter_pills.rb +89 -0
  81. data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +13 -12
  82. data/lib/plutonium/ui/table/components/scopes_pills.rb +67 -0
  83. data/lib/plutonium/ui/table/components/selection_column.rb +2 -11
  84. data/lib/plutonium/ui/table/components/toolbar.rb +104 -0
  85. data/lib/plutonium/ui/table/components/view_switcher.rb +81 -0
  86. data/lib/plutonium/ui/table/resource.rb +158 -89
  87. data/lib/plutonium/ui/table/theme.rb +14 -5
  88. data/lib/plutonium/version.rb +1 -1
  89. data/lib/plutonium.rb +6 -0
  90. data/package.json +1 -1
  91. data/src/css/components.css +304 -131
  92. data/src/css/tokens.css +101 -85
  93. data/src/js/controllers/autosubmit_controller.js +24 -0
  94. data/src/js/controllers/bulk_actions_controller.js +15 -16
  95. data/src/js/controllers/capture_url_controller.js +14 -0
  96. data/src/js/controllers/filter_panel_controller.js +77 -19
  97. data/src/js/controllers/frame_navigator_controller.js +34 -6
  98. data/src/js/controllers/icon_rail_controller.js +22 -0
  99. data/src/js/controllers/icon_rail_flyout_controller.js +128 -0
  100. data/src/js/controllers/register_controllers.js +16 -0
  101. data/src/js/controllers/resource_tab_list_controller.js +56 -3
  102. data/src/js/controllers/row_click_controller.js +21 -0
  103. data/src/js/controllers/table_column_menu_controller.js +43 -0
  104. data/src/js/controllers/table_header_controller.js +16 -0
  105. data/src/js/controllers/view_switcher_controller.js +29 -0
  106. metadata +31 -3
@@ -9,37 +9,33 @@
9
9
  =================== */
10
10
 
11
11
  .pu-btn {
12
- @apply inline-flex items-center justify-center gap-2
13
- font-semibold
14
- transition-all duration-200 ease-out
15
- focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
12
+ @apply inline-flex items-center justify-center gap-2 font-semibold transition-all duration-200 ease-out focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
16
13
  border-radius: var(--pu-radius-md);
17
14
  }
18
15
 
19
16
  .pu-btn-md {
20
- @apply px-5 py-2.5 text-sm;
17
+ @apply px-3 h-8 text-sm;
21
18
  }
22
19
 
23
20
  .pu-btn-sm {
24
- @apply px-4 py-2 text-sm;
21
+ @apply px-2.5 h-7 text-[13px];
25
22
  }
26
23
 
27
24
  .pu-btn-xs {
28
- @apply px-3 py-1.5 text-xs;
25
+ @apply px-2 h-6 text-xs;
29
26
  }
30
27
 
31
28
  /* Primary - Solid with subtle gradient */
32
29
  .pu-btn-primary {
33
- @apply bg-primary-600 text-white
34
- hover:bg-primary-500
35
- active:bg-primary-700
36
- focus-visible:ring-primary-500;
30
+ @apply bg-primary-600 text-white hover:bg-primary-500 active:bg-primary-700 focus-visible:ring-primary-500;
37
31
  box-shadow: var(--pu-shadow-sm), inset 0 1px 0 0 rgb(255 255 255 / 0.1);
38
32
  }
33
+
39
34
  .pu-btn-primary:hover {
40
35
  box-shadow: var(--pu-shadow-md), inset 0 1px 0 0 rgb(255 255 255 / 0.1);
41
36
  transform: translateY(-1px);
42
37
  }
38
+
43
39
  .pu-btn-primary:active {
44
40
  transform: translateY(0);
45
41
  box-shadow: var(--pu-shadow-sm);
@@ -47,12 +43,10 @@
47
43
 
48
44
  /* Secondary */
49
45
  .pu-btn-secondary {
50
- @apply bg-secondary-600 text-white
51
- hover:bg-secondary-500
52
- active:bg-secondary-700
53
- focus-visible:ring-secondary-500;
46
+ @apply bg-secondary-600 text-white hover:bg-secondary-500 active:bg-secondary-700 focus-visible:ring-secondary-500;
54
47
  box-shadow: var(--pu-shadow-sm), inset 0 1px 0 0 rgb(255 255 255 / 0.1);
55
48
  }
49
+
56
50
  .pu-btn-secondary:hover {
57
51
  box-shadow: var(--pu-shadow-md), inset 0 1px 0 0 rgb(255 255 255 / 0.1);
58
52
  transform: translateY(-1px);
@@ -60,12 +54,10 @@
60
54
 
61
55
  /* Danger */
62
56
  .pu-btn-danger {
63
- @apply bg-danger-600 text-white
64
- hover:bg-danger-500
65
- active:bg-danger-700
66
- focus-visible:ring-danger-500;
57
+ @apply bg-danger-600 text-white hover:bg-danger-500 active:bg-danger-700 focus-visible:ring-danger-500;
67
58
  box-shadow: var(--pu-shadow-sm), inset 0 1px 0 0 rgb(255 255 255 / 0.1);
68
59
  }
60
+
69
61
  .pu-btn-danger:hover {
70
62
  box-shadow: var(--pu-shadow-md), inset 0 1px 0 0 rgb(255 255 255 / 0.1);
71
63
  transform: translateY(-1px);
@@ -73,12 +65,10 @@
73
65
 
74
66
  /* Success */
75
67
  .pu-btn-success {
76
- @apply bg-success-600 text-white
77
- hover:bg-success-500
78
- active:bg-success-700
79
- focus-visible:ring-success-500;
68
+ @apply bg-success-600 text-white hover:bg-success-500 active:bg-success-700 focus-visible:ring-success-500;
80
69
  box-shadow: var(--pu-shadow-sm), inset 0 1px 0 0 rgb(255 255 255 / 0.1);
81
70
  }
71
+
82
72
  .pu-btn-success:hover {
83
73
  box-shadow: var(--pu-shadow-md), inset 0 1px 0 0 rgb(255 255 255 / 0.1);
84
74
  transform: translateY(-1px);
@@ -86,12 +76,10 @@
86
76
 
87
77
  /* Warning */
88
78
  .pu-btn-warning {
89
- @apply bg-warning-600 text-white
90
- hover:bg-warning-500
91
- active:bg-warning-700
92
- focus-visible:ring-warning-500;
79
+ @apply bg-warning-600 text-white hover:bg-warning-500 active:bg-warning-700 focus-visible:ring-warning-500;
93
80
  box-shadow: var(--pu-shadow-sm), inset 0 1px 0 0 rgb(255 255 255 / 0.1);
94
81
  }
82
+
95
83
  .pu-btn-warning:hover {
96
84
  box-shadow: var(--pu-shadow-md), inset 0 1px 0 0 rgb(255 255 255 / 0.1);
97
85
  transform: translateY(-1px);
@@ -99,12 +87,10 @@
99
87
 
100
88
  /* Info */
101
89
  .pu-btn-info {
102
- @apply bg-info-600 text-white
103
- hover:bg-info-500
104
- active:bg-info-700
105
- focus-visible:ring-info-500;
90
+ @apply bg-info-600 text-white hover:bg-info-500 active:bg-info-700 focus-visible:ring-info-500;
106
91
  box-shadow: var(--pu-shadow-sm), inset 0 1px 0 0 rgb(255 255 255 / 0.1);
107
92
  }
93
+
108
94
  .pu-btn-info:hover {
109
95
  box-shadow: var(--pu-shadow-md), inset 0 1px 0 0 rgb(255 255 255 / 0.1);
110
96
  transform: translateY(-1px);
@@ -112,12 +98,10 @@
112
98
 
113
99
  /* Accent */
114
100
  .pu-btn-accent {
115
- @apply bg-accent-600 text-white
116
- hover:bg-accent-500
117
- active:bg-accent-700
118
- focus-visible:ring-accent-500;
101
+ @apply bg-accent-600 text-white hover:bg-accent-500 active:bg-accent-700 focus-visible:ring-accent-500;
119
102
  box-shadow: var(--pu-shadow-sm), inset 0 1px 0 0 rgb(255 255 255 / 0.1);
120
103
  }
104
+
121
105
  .pu-btn-accent:hover {
122
106
  box-shadow: var(--pu-shadow-md), inset 0 1px 0 0 rgb(255 255 255 / 0.1);
123
107
  transform: translateY(-1px);
@@ -125,113 +109,73 @@
125
109
 
126
110
  /* Ghost - Minimal, text-like */
127
111
  .pu-btn-ghost {
128
- @apply bg-transparent
129
- text-slate-600 hover:text-slate-900
130
- hover:bg-slate-100
131
- active:bg-slate-200
132
- focus-visible:ring-slate-500;
112
+ @apply bg-transparent text-slate-600 hover:text-slate-900 hover:bg-slate-100 active:bg-slate-200 focus-visible:ring-slate-500;
133
113
  }
114
+
134
115
  .dark .pu-btn-ghost {
135
- @apply text-slate-400 hover:text-slate-100
136
- hover:bg-slate-700
137
- active:bg-slate-600;
116
+ @apply text-slate-400 hover:text-slate-100 hover:bg-slate-700 active:bg-slate-600;
138
117
  }
139
118
 
140
119
  /* Outline - Bordered, transparent background */
141
120
  .pu-btn-outline {
142
- @apply bg-transparent
143
- border border-[var(--pu-border)]
144
- text-[var(--pu-text-muted)]
145
- hover:text-[var(--pu-text)]
146
- hover:bg-[var(--pu-surface-alt)]
147
- hover:border-[var(--pu-border-strong)]
148
- active:bg-[var(--pu-border-muted)]
149
- focus-visible:ring-[var(--pu-border)];
121
+ @apply bg-transparent border border-[var(--pu-border)] text-[var(--pu-text-muted)] hover:text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)] hover:border-[var(--pu-border-strong)] active:bg-[var(--pu-border-muted)] focus-visible:ring-[var(--pu-border)];
150
122
  }
151
123
 
152
124
  /* Soft variants - Tinted backgrounds */
153
125
  .pu-btn-soft-primary {
154
- @apply bg-primary-50 text-primary-700
155
- hover:bg-primary-100
156
- active:bg-primary-200
157
- focus-visible:ring-primary-500;
126
+ @apply bg-primary-50 text-primary-700 hover:bg-primary-100 active:bg-primary-200 focus-visible:ring-primary-500;
158
127
  }
128
+
159
129
  .dark .pu-btn-soft-primary {
160
- @apply bg-primary-950/50 text-primary-300
161
- hover:bg-primary-900/60
162
- active:bg-primary-900/80;
130
+ @apply bg-primary-950/50 text-primary-300 hover:bg-primary-900/60 active:bg-primary-900/80;
163
131
  }
164
132
 
165
133
  .pu-btn-soft-danger {
166
- @apply bg-danger-50 text-danger-700
167
- hover:bg-danger-100
168
- active:bg-danger-200
169
- focus-visible:ring-danger-500;
134
+ @apply bg-danger-50 text-danger-700 hover:bg-danger-100 active:bg-danger-200 focus-visible:ring-danger-500;
170
135
  }
136
+
171
137
  .dark .pu-btn-soft-danger {
172
- @apply bg-danger-950/50 text-danger-300
173
- hover:bg-danger-900/60
174
- active:bg-danger-900/80;
138
+ @apply bg-danger-950/50 text-danger-300 hover:bg-danger-900/60 active:bg-danger-900/80;
175
139
  }
176
140
 
177
141
  .pu-btn-soft-success {
178
- @apply bg-success-50 text-success-700
179
- hover:bg-success-100
180
- active:bg-success-200
181
- focus-visible:ring-success-500;
142
+ @apply bg-success-50 text-success-700 hover:bg-success-100 active:bg-success-200 focus-visible:ring-success-500;
182
143
  }
144
+
183
145
  .dark .pu-btn-soft-success {
184
- @apply bg-success-950/50 text-success-300
185
- hover:bg-success-900/60
186
- active:bg-success-900/80;
146
+ @apply bg-success-950/50 text-success-300 hover:bg-success-900/60 active:bg-success-900/80;
187
147
  }
188
148
 
189
149
  .pu-btn-soft-warning {
190
- @apply bg-warning-50 text-warning-700
191
- hover:bg-warning-100
192
- active:bg-warning-200
193
- focus-visible:ring-warning-500;
150
+ @apply bg-warning-50 text-warning-700 hover:bg-warning-100 active:bg-warning-200 focus-visible:ring-warning-500;
194
151
  }
152
+
195
153
  .dark .pu-btn-soft-warning {
196
- @apply bg-warning-950/50 text-warning-300
197
- hover:bg-warning-900/60
198
- active:bg-warning-900/80;
154
+ @apply bg-warning-950/50 text-warning-300 hover:bg-warning-900/60 active:bg-warning-900/80;
199
155
  }
200
156
 
201
157
  .pu-btn-soft-info {
202
- @apply bg-info-50 text-info-700
203
- hover:bg-info-100
204
- active:bg-info-200
205
- focus-visible:ring-info-500;
158
+ @apply bg-info-50 text-info-700 hover:bg-info-100 active:bg-info-200 focus-visible:ring-info-500;
206
159
  }
160
+
207
161
  .dark .pu-btn-soft-info {
208
- @apply bg-info-950/50 text-info-300
209
- hover:bg-info-900/60
210
- active:bg-info-900/80;
162
+ @apply bg-info-950/50 text-info-300 hover:bg-info-900/60 active:bg-info-900/80;
211
163
  }
212
164
 
213
165
  .pu-btn-soft-secondary {
214
- @apply bg-secondary-50 text-secondary-700
215
- hover:bg-secondary-100
216
- active:bg-secondary-200
217
- focus-visible:ring-secondary-500;
166
+ @apply bg-secondary-50 text-secondary-700 hover:bg-secondary-100 active:bg-secondary-200 focus-visible:ring-secondary-500;
218
167
  }
168
+
219
169
  .dark .pu-btn-soft-secondary {
220
- @apply bg-secondary-950/50 text-secondary-300
221
- hover:bg-secondary-900/60
222
- active:bg-secondary-900/80;
170
+ @apply bg-secondary-950/50 text-secondary-300 hover:bg-secondary-900/60 active:bg-secondary-900/80;
223
171
  }
224
172
 
225
173
  .pu-btn-soft-accent {
226
- @apply bg-accent-50 text-accent-700
227
- hover:bg-accent-100
228
- active:bg-accent-200
229
- focus-visible:ring-accent-500;
174
+ @apply bg-accent-50 text-accent-700 hover:bg-accent-100 active:bg-accent-200 focus-visible:ring-accent-500;
230
175
  }
176
+
231
177
  .dark .pu-btn-soft-accent {
232
- @apply bg-accent-950/50 text-accent-300
233
- hover:bg-accent-900/60
234
- active:bg-accent-900/80;
178
+ @apply bg-accent-950/50 text-accent-300 hover:bg-accent-900/60 active:bg-accent-900/80;
235
179
  }
236
180
 
237
181
  /* ===================
@@ -246,7 +190,7 @@
246
190
  }
247
191
 
248
192
  .pu-card-body {
249
- padding: var(--pu-space-lg);
193
+ padding: 16px;
250
194
  }
251
195
 
252
196
  /* ===================
@@ -259,46 +203,128 @@
259
203
  border-radius: var(--pu-radius-md);
260
204
  color: var(--pu-text);
261
205
  transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
262
- @apply w-full px-4 py-3
263
- text-base
264
- focus:outline-none;
206
+ @apply w-full px-3 h-9 text-sm focus:outline-none;
265
207
  }
208
+
209
+ .pu-input-toolbar {
210
+ @apply h-8 text-sm;
211
+ }
212
+
266
213
  .pu-input-icon-left {
267
214
  @apply pl-10;
268
215
  }
216
+
269
217
  .pu-input::placeholder {
270
218
  color: var(--pu-input-placeholder);
271
219
  }
220
+
272
221
  .pu-input:hover {
273
222
  border-color: var(--pu-border-strong);
274
223
  }
224
+
275
225
  .pu-input:focus {
276
226
  border-color: var(--pu-input-focus-ring);
277
227
  box-shadow: 0 0 0 3px theme(colors.primary.500 / 15%);
278
228
  }
279
229
 
230
+ .pu-input:-webkit-autofill,
231
+ .pu-input:-webkit-autofill:hover,
232
+ .pu-input:-webkit-autofill:focus,
233
+ .pu-input:-webkit-autofill:active {
234
+ -webkit-box-shadow: 0 0 0 1000px var(--pu-input-bg) inset;
235
+ -webkit-text-fill-color: var(--pu-text);
236
+ caret-color: var(--pu-text);
237
+ transition: background-color 5000s ease-in-out 0s;
238
+ }
239
+
240
+ .pu-input:-webkit-autofill:focus {
241
+ -webkit-box-shadow: 0 0 0 1000px var(--pu-input-bg) inset,
242
+ 0 0 0 3px theme(colors.primary.500 / 15%);
243
+ }
244
+
280
245
  .pu-input-invalid {
281
- @apply border-danger-500 bg-danger-50/50
282
- text-danger-900 placeholder:text-danger-400;
246
+ @apply border-danger-500 bg-danger-50/50 text-danger-900 placeholder:text-danger-400;
283
247
  }
248
+
284
249
  .pu-input-invalid:focus {
285
250
  box-shadow: 0 0 0 3px theme(colors.danger.500 / 15%);
286
251
  }
252
+
287
253
  .dark .pu-input-invalid {
288
- @apply bg-danger-950/20 border-danger-500/70
289
- text-danger-200 placeholder:text-danger-400;
254
+ @apply bg-danger-950/20 border-danger-500/70 text-danger-200 placeholder:text-danger-400;
290
255
  }
291
256
 
292
257
  .pu-input-valid {
293
- @apply border-success-500 bg-success-50/50
294
- text-success-900 placeholder:text-success-400;
258
+ @apply border-success-500 bg-success-50/50 text-success-900 placeholder:text-success-400;
295
259
  }
260
+
296
261
  .pu-input-valid:focus {
297
262
  box-shadow: 0 0 0 3px theme(colors.success.500 / 15%);
298
263
  }
264
+
299
265
  .dark .pu-input-valid {
300
- @apply bg-success-950/20 border-success-500/70
301
- text-success-200 placeholder:text-success-400;
266
+ @apply bg-success-950/20 border-success-500/70 text-success-200 placeholder:text-success-400;
267
+ }
268
+
269
+ /* Autofill: preserve validation tint + theme-aware text color */
270
+ .pu-input-invalid:-webkit-autofill,
271
+ .pu-input-invalid:-webkit-autofill:hover,
272
+ .pu-input-invalid:-webkit-autofill:active {
273
+ -webkit-box-shadow: 0 0 0 1000px theme(colors.danger.50 / 50%) inset,
274
+ 0 0 0 1000px var(--pu-input-bg) inset;
275
+ -webkit-text-fill-color: theme(colors.danger.900);
276
+ caret-color: theme(colors.danger.900);
277
+ }
278
+
279
+ .pu-input-invalid:-webkit-autofill:focus {
280
+ -webkit-box-shadow: 0 0 0 1000px theme(colors.danger.50 / 50%) inset,
281
+ 0 0 0 1000px var(--pu-input-bg) inset,
282
+ 0 0 0 3px theme(colors.danger.500 / 15%);
283
+ }
284
+
285
+ .dark .pu-input-invalid:-webkit-autofill,
286
+ .dark .pu-input-invalid:-webkit-autofill:hover,
287
+ .dark .pu-input-invalid:-webkit-autofill:active {
288
+ -webkit-box-shadow: 0 0 0 1000px theme(colors.danger.950 / 20%) inset,
289
+ 0 0 0 1000px var(--pu-input-bg) inset;
290
+ -webkit-text-fill-color: theme(colors.danger.200);
291
+ caret-color: theme(colors.danger.200);
292
+ }
293
+
294
+ .dark .pu-input-invalid:-webkit-autofill:focus {
295
+ -webkit-box-shadow: 0 0 0 1000px theme(colors.danger.950 / 20%) inset,
296
+ 0 0 0 1000px var(--pu-input-bg) inset,
297
+ 0 0 0 3px theme(colors.danger.500 / 15%);
298
+ }
299
+
300
+ .pu-input-valid:-webkit-autofill,
301
+ .pu-input-valid:-webkit-autofill:hover,
302
+ .pu-input-valid:-webkit-autofill:active {
303
+ -webkit-box-shadow: 0 0 0 1000px theme(colors.success.50 / 50%) inset,
304
+ 0 0 0 1000px var(--pu-input-bg) inset;
305
+ -webkit-text-fill-color: theme(colors.success.900);
306
+ caret-color: theme(colors.success.900);
307
+ }
308
+
309
+ .pu-input-valid:-webkit-autofill:focus {
310
+ -webkit-box-shadow: 0 0 0 1000px theme(colors.success.50 / 50%) inset,
311
+ 0 0 0 1000px var(--pu-input-bg) inset,
312
+ 0 0 0 3px theme(colors.success.500 / 15%);
313
+ }
314
+
315
+ .dark .pu-input-valid:-webkit-autofill,
316
+ .dark .pu-input-valid:-webkit-autofill:hover,
317
+ .dark .pu-input-valid:-webkit-autofill:active {
318
+ -webkit-box-shadow: 0 0 0 1000px theme(colors.success.950 / 20%) inset,
319
+ 0 0 0 1000px var(--pu-input-bg) inset;
320
+ -webkit-text-fill-color: theme(colors.success.200);
321
+ caret-color: theme(colors.success.200);
322
+ }
323
+
324
+ .dark .pu-input-valid:-webkit-autofill:focus {
325
+ -webkit-box-shadow: 0 0 0 1000px theme(colors.success.950 / 20%) inset,
326
+ 0 0 0 1000px var(--pu-input-bg) inset,
327
+ 0 0 0 3px theme(colors.success.500 / 15%);
302
328
  }
303
329
 
304
330
  .pu-label {
@@ -319,6 +345,7 @@
319
345
  .pu-error {
320
346
  @apply mt-2 text-sm font-medium text-danger-600;
321
347
  }
348
+
322
349
  .dark .pu-error {
323
350
  @apply text-danger-400;
324
351
  }
@@ -329,9 +356,6 @@
329
356
 
330
357
  .pu-table-wrapper {
331
358
  background-color: var(--pu-card-bg);
332
- border: 1px solid var(--pu-card-border);
333
- border-radius: var(--pu-radius-lg);
334
- box-shadow: var(--pu-shadow-sm);
335
359
  @apply overflow-x-auto;
336
360
  }
337
361
 
@@ -357,6 +381,7 @@
357
381
  transition: background-color 150ms ease;
358
382
  @apply last:border-b-0;
359
383
  }
384
+
360
385
  .pu-table-body-row:hover {
361
386
  background-color: var(--pu-table-row-hover);
362
387
  }
@@ -378,9 +403,9 @@
378
403
  background: linear-gradient(to right, theme(colors.primary.50), theme(colors.primary.100/50%));
379
404
  border: 1px solid theme(colors.primary.200);
380
405
  border-radius: var(--pu-radius-lg);
381
- @apply items-center gap-4
382
- px-5 py-3 mb-4;
406
+ @apply items-center gap-4 px-5 py-3 mb-4;
383
407
  }
408
+
384
409
  .dark .pu-toolbar {
385
410
  background: linear-gradient(to right, theme(colors.primary.950/40%), theme(colors.primary.900/30%));
386
411
  border-color: theme(colors.primary.800/50%);
@@ -389,6 +414,7 @@
389
414
  .pu-toolbar-text {
390
415
  @apply text-base font-semibold text-primary-700;
391
416
  }
417
+
392
418
  .dark .pu-toolbar-text {
393
419
  @apply text-primary-300;
394
420
  }
@@ -402,19 +428,11 @@
402
428
  =================== */
403
429
 
404
430
  .pu-checkbox {
405
- @apply size-5 rounded-md
406
- bg-white border-2 border-slate-300
407
- accent-primary-600
408
- focus:ring-2 focus:ring-primary-500/30 focus:ring-offset-0
409
- cursor-pointer
410
- transition-all duration-150
411
- checked:bg-primary-600 checked:border-primary-600
412
- indeterminate:bg-primary-600 indeterminate:border-primary-600
413
- hover:border-primary-400;
431
+ @apply size-5 rounded-md bg-white border-2 border-slate-300 accent-primary-600 focus:ring-2 focus:ring-primary-500/30 focus:ring-offset-0 cursor-pointer transition-all duration-150 checked:bg-primary-600 checked:border-primary-600 indeterminate:bg-primary-600 indeterminate:border-primary-600 hover:border-primary-400;
414
432
  }
433
+
415
434
  .dark .pu-checkbox {
416
- @apply bg-slate-700 border-slate-500
417
- hover:border-primary-400;
435
+ @apply bg-slate-700 border-slate-500 hover:border-primary-400;
418
436
  }
419
437
 
420
438
  /* ===================
@@ -422,9 +440,7 @@
422
440
  =================== */
423
441
 
424
442
  .pu-empty-state {
425
- @apply flex flex-col items-center justify-center
426
- py-16 px-8
427
- text-center;
443
+ @apply flex flex-col items-center justify-center py-16 px-8 text-center;
428
444
  }
429
445
 
430
446
  .pu-empty-state-icon {
@@ -469,3 +485,160 @@
469
485
  .pu-selection-cell {
470
486
  @apply w-14 px-6 py-4;
471
487
  }
488
+
489
+ /* ===================
490
+ ICON RAIL — modern shell
491
+ =================== */
492
+
493
+ /* Hide labels by default; revealed only when rail is pinned */
494
+ .icon-rail-label {
495
+ display: none;
496
+ }
497
+
498
+ /* Caret indicator on parents with children. Default (collapsed):
499
+ a tiny badge in the bottom-right corner of the trigger button. */
500
+ .icon-rail-chevron {
501
+ position: absolute;
502
+ bottom: 1px;
503
+ right: 1px;
504
+ width: 12px;
505
+ height: 12px;
506
+ padding: 1px;
507
+ border-radius: 9999px;
508
+ background: var(--pu-surface);
509
+ color: var(--pu-text-muted);
510
+ display: inline-flex;
511
+ align-items: center;
512
+ justify-content: center;
513
+ pointer-events: none;
514
+ box-shadow: 0 0 0 1px var(--pu-border);
515
+ }
516
+
517
+ /* Modern rail width (controlled by CSS, not Tailwind utility) */
518
+ aside[data-controller~="icon-rail"] {
519
+ width: 3.5rem; /* 56px collapsed */
520
+ overflow-x: hidden;
521
+ }
522
+
523
+ /* Pinned mode: rail expands, labels appear */
524
+ body.pu-rail-pinned aside[data-controller~="icon-rail"] {
525
+ width: 14rem !important; /* 224px */
526
+ }
527
+
528
+ body.pu-rail-pinned .icon-rail-label {
529
+ display: inline-flex !important;
530
+ }
531
+
532
+ body.pu-rail-pinned .icon-rail-leaf,
533
+ body.pu-rail-pinned .icon-rail-parent-trigger {
534
+ width: 100%;
535
+ justify-content: flex-start;
536
+ gap: 8px;
537
+ padding: 6px 10px;
538
+ }
539
+
540
+ /* When pinned, chevron flows inline after the label (right-aligned)
541
+ and rotates to point down — mirrors a typical disclosure caret. */
542
+ body.pu-rail-pinned .icon-rail-chevron {
543
+ position: static;
544
+ background: transparent;
545
+ box-shadow: none;
546
+ margin-left: auto;
547
+ width: 16px;
548
+ height: 16px;
549
+ padding: 0;
550
+ color: inherit;
551
+ opacity: 0.7;
552
+ }
553
+
554
+ /* When pinned, items stretch full width so labels read left-to-right */
555
+ body.pu-rail-pinned #sidebar-navigation-content {
556
+ align-items: stretch;
557
+ padding-left: 8px;
558
+ padding-right: 8px;
559
+ }
560
+
561
+ /* When pinned, brand area aligns to the start so it reads as part of the
562
+ left-aligned column instead of staying centered while items go left. */
563
+ body.pu-rail-pinned aside[data-controller~="icon-rail"] > div:first-child {
564
+ justify-content: flex-start;
565
+ padding-left: 14px;
566
+ }
567
+
568
+ /* When pinned, pin button right-aligns to anchor the collapse affordance
569
+ to the rail edge it'll snap toward. */
570
+ body.pu-rail-pinned aside[data-controller~="icon-rail"] > div:last-child {
571
+ justify-content: flex-end;
572
+ padding-right: 8px;
573
+ }
574
+
575
+ /* Pin button icon swap */
576
+ body.pu-rail-pinned .icon-rail-pin-collapse {
577
+ display: inline-flex !important;
578
+ }
579
+
580
+ body.pu-rail-pinned .icon-rail-pin-expand {
581
+ display: none !important;
582
+ }
583
+
584
+ /* Main content padding when pinned: rail width + 1.5rem breathing room
585
+ (matches the collapsed-state lg:pl-20 = rail 3.5rem + 1.5rem gap). */
586
+ @media (min-width: 1024px) {
587
+ body.pu-rail-pinned main {
588
+ padding-left: 15.5rem !important;
589
+ }
590
+ }
591
+
592
+ /* Flyout: visibility controlled by Stimulus (data-flyout-open) via position:fixed */
593
+ .icon-rail-parent {
594
+ position: relative;
595
+ }
596
+
597
+ .icon-rail-flyout {
598
+ display: none;
599
+ z-index: 50;
600
+ min-width: 200px;
601
+ /* position is set inline by the Stimulus controller */
602
+ }
603
+
604
+ .icon-rail-parent[data-flyout-open="true"] .icon-rail-flyout {
605
+ display: block;
606
+ }
607
+
608
+ .icon-rail-flyout-inner {
609
+ background: var(--pu-surface);
610
+ border: 1px solid var(--pu-border);
611
+ border-radius: var(--pu-radius-lg);
612
+ box-shadow: var(--pu-shadow-lg);
613
+ padding: 6px;
614
+ animation: pu-rail-flyout-in 120ms ease-out;
615
+ }
616
+
617
+ @keyframes pu-rail-flyout-in {
618
+ from { opacity: 0; transform: translateX(-4px); }
619
+ to { opacity: 1; transform: translateX(0); }
620
+ }
621
+
622
+ .icon-rail-flyout-label {
623
+ padding: 6px 10px 4px;
624
+ font-size: 10px;
625
+ font-weight: 600;
626
+ text-transform: uppercase;
627
+ letter-spacing: 0.6px;
628
+ color: var(--pu-text-muted);
629
+ }
630
+
631
+ .icon-rail-flyout-item {
632
+ display: block;
633
+ padding: 7px 10px;
634
+ font-size: 13px;
635
+ color: var(--pu-text);
636
+ border-radius: var(--pu-radius-md);
637
+ white-space: nowrap;
638
+ transition: background-color 120ms ease, color 120ms ease;
639
+ }
640
+
641
+ .icon-rail-flyout-item:hover {
642
+ background: var(--pu-surface-alt);
643
+ color: var(--pu-text);
644
+ }