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.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium-definition/SKILL.md +87 -2
- data/.claude/skills/plutonium-installation/SKILL.md +6 -0
- data/.claude/skills/plutonium-views/SKILL.md +59 -0
- data/CHANGELOG.md +12 -0
- data/app/assets/plutonium.css +2 -2
- data/app/assets/plutonium.js +369 -25
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +45 -45
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/plutonium/_resource_header.html.erb +4 -4
- data/app/views/plutonium/_resource_sidebar.html.erb +9 -9
- data/app/views/resource/_resource_grid.html.erb +1 -0
- data/config/brakeman.ignore +25 -2
- data/docs/reference/definition/actions.md +14 -1
- data/docs/reference/definition/index.md +58 -0
- data/docs/reference/views/index.md +43 -0
- data/docs/superpowers/plans/2026-05-07-ui-layout-overhaul.md +841 -0
- data/docs/superpowers/plans/2026-05-07-ui-layout-overhaul.md.tasks.json +103 -0
- data/docs/superpowers/specs/2026-05-07-ui-layout-overhaul-design.md +270 -0
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/core/install/templates/config/initializers/plutonium.rb +1 -0
- data/lib/generators/pu/core/update/update_generator.rb +20 -0
- data/lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb +54 -5
- data/lib/plutonium/action/base.rb +44 -1
- data/lib/plutonium/action/interactive.rb +1 -1
- data/lib/plutonium/configuration.rb +4 -0
- data/lib/plutonium/definition/actions.rb +3 -0
- data/lib/plutonium/definition/base.rb +8 -0
- data/lib/plutonium/definition/metadata.rb +40 -0
- data/lib/plutonium/definition/views.rb +94 -0
- data/lib/plutonium/helpers/turbo_helper.rb +1 -1
- data/lib/plutonium/interaction/response/redirect.rb +1 -1
- data/lib/plutonium/query/base.rb +8 -0
- data/lib/plutonium/query/filters/association.rb +30 -8
- data/lib/plutonium/query/filters/boolean.rb +5 -0
- data/lib/plutonium/resource/controllers/presentable.rb +11 -2
- data/lib/plutonium/resource/definition.rb +42 -0
- data/lib/plutonium/resource/query_object.rb +64 -6
- data/lib/plutonium/testing/resource_definition.rb +2 -2
- data/lib/plutonium/ui/action_button.rb +4 -2
- data/lib/plutonium/ui/component/kit.rb +12 -0
- data/lib/plutonium/ui/display/base.rb +3 -1
- data/lib/plutonium/ui/display/resource.rb +109 -25
- data/lib/plutonium/ui/display/theme.rb +2 -1
- data/lib/plutonium/ui/dyna_frame/content.rb +8 -14
- data/lib/plutonium/ui/empty_card.rb +1 -1
- data/lib/plutonium/ui/form/base.rb +29 -1
- data/lib/plutonium/ui/form/components/hidden_wrapper.rb +25 -0
- data/lib/plutonium/ui/form/components/resource_select.rb +79 -1
- data/lib/plutonium/ui/form/components/secure_association.rb +7 -2
- data/lib/plutonium/ui/form/components/sticky_footer.rb +17 -0
- data/lib/plutonium/ui/form/resource.rb +48 -9
- data/lib/plutonium/ui/form/theme.rb +1 -1
- data/lib/plutonium/ui/frame_navigator_panel.rb +7 -4
- data/lib/plutonium/ui/grid/card.rb +235 -0
- data/lib/plutonium/ui/grid/resource.rb +149 -0
- data/lib/plutonium/ui/layout/base.rb +37 -1
- data/lib/plutonium/ui/layout/header.rb +1 -2
- data/lib/plutonium/ui/layout/icon_rail.rb +212 -0
- data/lib/plutonium/ui/layout/resource_layout.rb +10 -3
- data/lib/plutonium/ui/layout/sidebar.rb +12 -24
- data/lib/plutonium/ui/layout/topbar.rb +100 -0
- data/lib/plutonium/ui/modal/base.rb +109 -0
- data/lib/plutonium/ui/modal/centered.rb +21 -0
- data/lib/plutonium/ui/modal/slideover.rb +26 -0
- data/lib/plutonium/ui/page/base.rb +25 -6
- data/lib/plutonium/ui/page/edit.rb +13 -1
- data/lib/plutonium/ui/page/index.rb +40 -1
- data/lib/plutonium/ui/page/interactive_action.rb +8 -39
- data/lib/plutonium/ui/page/new.rb +13 -1
- data/lib/plutonium/ui/page/show.rb +8 -1
- data/lib/plutonium/ui/page_header.rb +8 -13
- data/lib/plutonium/ui/panel.rb +10 -19
- data/lib/plutonium/ui/sidebar_menu.rb +2 -25
- data/lib/plutonium/ui/tab_list.rb +29 -7
- data/lib/plutonium/ui/table/base.rb +106 -0
- data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +12 -4
- data/lib/plutonium/ui/table/components/filter_form.rb +171 -0
- data/lib/plutonium/ui/table/components/filter_pills.rb +89 -0
- data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +13 -12
- data/lib/plutonium/ui/table/components/scopes_pills.rb +67 -0
- data/lib/plutonium/ui/table/components/selection_column.rb +2 -11
- data/lib/plutonium/ui/table/components/toolbar.rb +104 -0
- data/lib/plutonium/ui/table/components/view_switcher.rb +81 -0
- data/lib/plutonium/ui/table/resource.rb +158 -89
- data/lib/plutonium/ui/table/theme.rb +14 -5
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium.rb +6 -0
- data/package.json +1 -1
- data/src/css/components.css +304 -131
- data/src/css/tokens.css +101 -85
- data/src/js/controllers/autosubmit_controller.js +24 -0
- data/src/js/controllers/bulk_actions_controller.js +15 -16
- data/src/js/controllers/capture_url_controller.js +14 -0
- data/src/js/controllers/filter_panel_controller.js +77 -19
- data/src/js/controllers/frame_navigator_controller.js +34 -6
- data/src/js/controllers/icon_rail_controller.js +22 -0
- data/src/js/controllers/icon_rail_flyout_controller.js +128 -0
- data/src/js/controllers/register_controllers.js +16 -0
- data/src/js/controllers/resource_tab_list_controller.js +56 -3
- data/src/js/controllers/row_click_controller.js +21 -0
- data/src/js/controllers/table_column_menu_controller.js +43 -0
- data/src/js/controllers/table_header_controller.js +16 -0
- data/src/js/controllers/view_switcher_controller.js +29 -0
- metadata +31 -3
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
# UI Layout Overhaul Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development (recommended) or superpowers-extended-cc:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Implement the UI layout overhaul per `docs/superpowers/specs/2026-05-07-ui-layout-overhaul-design.md` — icon rail + topbar shell, Stripe-style PageHeader, redesigned index/show/form pages, balanced density tokens, and centered + slideover modals.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Five phases delivered top-down: density tokens & PageHeader first (foundational), then the new shell, then per-page redesigns, then modals. Each task ends in a committable, working state. Phlex components are the primary unit of work; CSS changes flow through `src/css/components.css` + `tokens.css`. Existing `.pu-*` class names are preserved; values shift.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Ruby 3+, Rails 7/8, Phlex, Tailwind v4, Stimulus, Turbo. Test framework: Minitest with Capybara for system tests.
|
|
10
|
+
|
|
11
|
+
**User Verification:** NO — the design was validated via brainstorming. Implementation executes against the spec; no in-loop user verification needed during build.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Reference Files (read before starting)
|
|
16
|
+
|
|
17
|
+
- Spec: `docs/superpowers/specs/2026-05-07-ui-layout-overhaul-design.md`
|
|
18
|
+
- Existing layout: `lib/plutonium/ui/layout/{base,sidebar,header,resource_layout}.rb`
|
|
19
|
+
- Existing pages: `lib/plutonium/ui/page/{base,index,show,new,edit,interactive_action}.rb`
|
|
20
|
+
- Existing PageHeader: `lib/plutonium/ui/page_header.rb`
|
|
21
|
+
- Existing breadcrumbs: `lib/plutonium/ui/breadcrumbs.rb`
|
|
22
|
+
- Existing CSS: `src/css/{components,tokens}.css`
|
|
23
|
+
- Sort logic: `lib/plutonium/resource/query_object.rb:126` (`sort_params_for`)
|
|
24
|
+
- Resource definition: `lib/plutonium/resource/definition.rb` (for `modal` DSL addition)
|
|
25
|
+
- Interaction: `lib/plutonium/resource/interaction.rb` and `lib/plutonium/interaction/base.rb` (for `modal:` option)
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Pre-flight: Asset Build
|
|
30
|
+
|
|
31
|
+
Every task that edits `src/css/*.css` or `src/js/**/*.js` requires `yarn dev` to be running OR `yarn build` to be run after the change. The dummy app at `test/dummy/` reads from `src/build/` in dev. Mention this once per task that requires it.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
# Phase 1 — Foundation
|
|
36
|
+
|
|
37
|
+
### Task 0: Density tokens
|
|
38
|
+
|
|
39
|
+
**Goal:** Codify the balanced density scale (§6 of spec) so subsequent tasks reference one source of truth.
|
|
40
|
+
|
|
41
|
+
**Files:**
|
|
42
|
+
- Modify: `src/css/tokens.css`
|
|
43
|
+
- Modify: `src/css/components.css` (button, input, card sizes)
|
|
44
|
+
- Modify: `lib/plutonium/ui/component/tokens.rb` (Phlex-side mirror if used)
|
|
45
|
+
|
|
46
|
+
**Acceptance Criteria:**
|
|
47
|
+
- [ ] `--pu-row-height: 32px`, `--pu-section-gap: 16px`, `--pu-field-gap: 12px`, `--pu-page-padding: 24px` defined in `:root` and `.dark`
|
|
48
|
+
- [ ] `.pu-input` height = 36px in forms, 32px in toolbars (introduce `.pu-input-toolbar` modifier)
|
|
49
|
+
- [ ] `.pu-btn-md` = 32px height, 14px text; `.pu-btn-sm` = 28px, 13px
|
|
50
|
+
- [ ] `.pu-card` padding = 16px
|
|
51
|
+
- [ ] No regressions: `bundle exec appraisal rails-8.1 rake test` passes
|
|
52
|
+
- [ ] `yarn build` succeeds
|
|
53
|
+
|
|
54
|
+
**Verify:** `cd test/dummy && yarn build && bundle exec appraisal rails-8.1 ruby -Itest test/system -e ""`
|
|
55
|
+
|
|
56
|
+
**Steps:**
|
|
57
|
+
|
|
58
|
+
- [ ] **Step 1:** Add density variables to `src/css/tokens.css` `:root`:
|
|
59
|
+
|
|
60
|
+
```css
|
|
61
|
+
:root {
|
|
62
|
+
/* ... existing tokens ... */
|
|
63
|
+
--pu-row-height: 32px;
|
|
64
|
+
--pu-section-gap: 16px;
|
|
65
|
+
--pu-field-gap: 12px;
|
|
66
|
+
--pu-page-padding: 24px;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- [ ] **Step 2:** Update `.pu-btn-md` and `.pu-btn-sm` in `src/css/components.css`:
|
|
71
|
+
|
|
72
|
+
```css
|
|
73
|
+
.pu-btn-md { @apply px-3 h-8 text-sm; }
|
|
74
|
+
.pu-btn-sm { @apply px-2.5 h-7 text-[13px]; }
|
|
75
|
+
.pu-btn-xs { @apply px-2 h-6 text-xs; }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- [ ] **Step 3:** Update `.pu-input` and add `.pu-input-toolbar`:
|
|
79
|
+
|
|
80
|
+
```css
|
|
81
|
+
.pu-input {
|
|
82
|
+
/* existing background/border/color */
|
|
83
|
+
@apply w-full px-3 h-9 text-sm focus:outline-none;
|
|
84
|
+
}
|
|
85
|
+
.pu-input-toolbar { @apply h-8 text-sm; }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- [ ] **Step 4:** Update `.pu-card-body` padding to 16px (down from `var(--pu-space-lg)` if larger).
|
|
89
|
+
- [ ] **Step 5:** Run `yarn build` and `bundle exec appraisal rails-8.1 rake test`. Expect pass.
|
|
90
|
+
- [ ] **Step 6:** Commit:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
git add src/css/tokens.css src/css/components.css lib/plutonium/ui/component/tokens.rb
|
|
94
|
+
git commit -m "feat(ui): codify balanced density tokens"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### Task 1: PageHeader redesign (Stripe-style)
|
|
100
|
+
|
|
101
|
+
**Goal:** Replace the current `PageHeader` with a tighter, Stripe-style header — title + description + right-aligned actions on one row, tabs strip rendered underneath.
|
|
102
|
+
|
|
103
|
+
**Files:**
|
|
104
|
+
- Modify: `lib/plutonium/ui/page_header.rb`
|
|
105
|
+
- Modify: `lib/plutonium/ui/page/base.rb` (call site stays; rendering logic shifts)
|
|
106
|
+
- Test: `test/plutonium/ui/page_header_test.rb` (create if missing)
|
|
107
|
+
|
|
108
|
+
**Acceptance Criteria:**
|
|
109
|
+
- [ ] Title is 18-20px (`text-xl font-semibold`), description is 13px (`text-sm`) muted
|
|
110
|
+
- [ ] Actions render right-aligned at title vertical level
|
|
111
|
+
- [ ] Tabs (when present) render directly below the header as a connected strip
|
|
112
|
+
- [ ] No 8px margin-bottom gap between header and tabs
|
|
113
|
+
- [ ] `actions: nil` and `description: nil` render gracefully
|
|
114
|
+
|
|
115
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/ui/page_header_test.rb -v` → all pass
|
|
116
|
+
|
|
117
|
+
**Steps:**
|
|
118
|
+
|
|
119
|
+
- [ ] **Step 1:** Update `lib/plutonium/ui/page_header.rb` `view_template` markup to:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
def view_template
|
|
123
|
+
div(class: "flex items-start justify-between gap-4 mb-4") do
|
|
124
|
+
div(class: "min-w-0 flex-1") do
|
|
125
|
+
render_title @title if @title
|
|
126
|
+
render_description @description if @description.present?
|
|
127
|
+
end
|
|
128
|
+
render_actions if @actions.any?
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def render_title(title)
|
|
133
|
+
h1(class: "text-xl font-semibold leading-tight text-[var(--pu-text)] truncate") { title }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def render_description(description)
|
|
137
|
+
p(class: "mt-1 text-sm text-[var(--pu-text-muted)]") { description }
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- [ ] **Step 2:** Update `lib/plutonium/ui/page/base.rb` `render_header` so the tab strip renders directly after `PageHeader` with no gap (drop the `mb-8` from header; let tabs sit flush against a `border-b` baseline).
|
|
142
|
+
- [ ] **Step 3:** Add/update test cases:
|
|
143
|
+
- title-only renders correctly
|
|
144
|
+
- title + description renders correctly
|
|
145
|
+
- title + description + 2 actions: actions are right-aligned
|
|
146
|
+
- dropdown actions render when secondary actions exist
|
|
147
|
+
- [ ] **Step 4:** Run `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/ui/page_header_test.rb -v`. Expect pass.
|
|
148
|
+
- [ ] **Step 5:** Manual: boot dummy app (`cd test/dummy && bundle exec rails s`), navigate to `/admin/users` (or any index), confirm header layout. Then `/admin/users/1` confirm tabs render flush below.
|
|
149
|
+
- [ ] **Step 6:** Commit: `feat(ui): redesign PageHeader Stripe-style`
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
# Phase 2 — App Shell
|
|
154
|
+
|
|
155
|
+
### Task 2: IconRail component
|
|
156
|
+
|
|
157
|
+
**Goal:** New `Plutonium::UI::Layout::IconRail` Phlex component — 56px icon-only nav with tooltips on hover, replacing the expanded `SidebarMenu` for the new shell.
|
|
158
|
+
|
|
159
|
+
**Files:**
|
|
160
|
+
- Create: `lib/plutonium/ui/layout/icon_rail.rb`
|
|
161
|
+
- Create: `test/plutonium/ui/layout/icon_rail_test.rb`
|
|
162
|
+
- Modify: `src/css/components.css` (add `.pu-icon-rail`, `.pu-icon-rail-item` if needed)
|
|
163
|
+
|
|
164
|
+
**Acceptance Criteria:**
|
|
165
|
+
- [ ] Renders fixed-position aside, `width: 56px`, full-height, `var(--pu-surface)` background, right border
|
|
166
|
+
- [ ] Item slots: brand (top), nav items (middle, grouped with dividers), settings/theme (bottom)
|
|
167
|
+
- [ ] Each nav item is icon-only with a Tailwind tooltip (group-hover) showing the label
|
|
168
|
+
- [ ] Active item: filled primary tone background, primary text
|
|
169
|
+
- [ ] Mobile (`<lg`): rail hidden, drawer toggled by topbar hamburger (controller wiring deferred to Task 4)
|
|
170
|
+
- [ ] Compatible with `phlexi-menu` items (accepts an Items tree like `SidebarMenu`)
|
|
171
|
+
|
|
172
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/ui/layout/icon_rail_test.rb -v` → all pass
|
|
173
|
+
|
|
174
|
+
**Steps:**
|
|
175
|
+
|
|
176
|
+
- [ ] **Step 1:** Sketch the component structure:
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
module Plutonium
|
|
180
|
+
module UI
|
|
181
|
+
module Layout
|
|
182
|
+
class IconRail < Plutonium::UI::Component::Base
|
|
183
|
+
include Phlexi::Menu::Component
|
|
184
|
+
|
|
185
|
+
def view_template
|
|
186
|
+
aside(
|
|
187
|
+
id: "sidebar-navigation",
|
|
188
|
+
data: {controller: "sidebar"},
|
|
189
|
+
class: "fixed top-0 left-0 z-40 h-screen w-14 bg-[var(--pu-surface)] " \
|
|
190
|
+
"border-r border-[var(--pu-border)] transition-transform " \
|
|
191
|
+
"-translate-x-full lg:translate-x-0 flex flex-col"
|
|
192
|
+
) do
|
|
193
|
+
div(class: "py-3 flex flex-col items-center gap-1", data: {sidebar_target: "scroll"}) do
|
|
194
|
+
render_brand
|
|
195
|
+
render_items(@menu.items) if @menu&.items
|
|
196
|
+
end
|
|
197
|
+
div(class: "mt-auto py-3 flex flex-col items-center gap-1") do
|
|
198
|
+
render_footer_items
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
# ...
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
- [ ] **Step 2:** Implement `render_item_link` to render a single icon (Tabler icon) with a tooltip via:
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
def render_item_link(item, depth)
|
|
213
|
+
a(href: item.url, class: rail_link_classes(item),
|
|
214
|
+
title: item.label,
|
|
215
|
+
aria: {label: item.label}) do
|
|
216
|
+
render item.icon if item.icon
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def rail_link_classes(item)
|
|
221
|
+
base = "flex items-center justify-center w-10 h-10 rounded-md " \
|
|
222
|
+
"text-[var(--pu-text-muted)] hover:text-[var(--pu-text)] " \
|
|
223
|
+
"hover:bg-[var(--pu-surface-alt)] transition-colors"
|
|
224
|
+
active?(item) ? "#{base} bg-primary-100 text-primary-700 dark:bg-primary-900/40 dark:text-primary-300" : base
|
|
225
|
+
end
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
- [ ] **Step 3:** Add `.pu-icon-rail` and `.pu-icon-rail-item` helper classes to `src/css/components.css` if extracting markup is clearer. Otherwise, leave Tailwind-utility-only.
|
|
229
|
+
- [ ] **Step 4:** Write tests covering:
|
|
230
|
+
- aside renders at expected width
|
|
231
|
+
- item with `active: true` gets active classes
|
|
232
|
+
- item without icon still renders (label fallback as initials)
|
|
233
|
+
- [ ] **Step 5:** `yarn build && bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/ui/layout/icon_rail_test.rb -v`. Expect pass.
|
|
234
|
+
- [ ] **Step 6:** Commit: `feat(ui): add IconRail layout component`
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
### Task 3: Topbar component
|
|
239
|
+
|
|
240
|
+
**Goal:** New `Plutonium::UI::Layout::Topbar` — sticky 48px topbar with breadcrumbs (left), global search (center), user/notif (right). Replaces the legacy `Layout::Header`.
|
|
241
|
+
|
|
242
|
+
**Files:**
|
|
243
|
+
- Create: `lib/plutonium/ui/layout/topbar.rb`
|
|
244
|
+
- Modify: `lib/plutonium/ui/breadcrumbs.rb` (slim down for topbar context — no large title)
|
|
245
|
+
- Modify: `app/assets` if a new Stimulus controller is needed for the search omnibox (defer if not)
|
|
246
|
+
- Test: `test/plutonium/ui/layout/topbar_test.rb`
|
|
247
|
+
|
|
248
|
+
**Acceptance Criteria:**
|
|
249
|
+
- [ ] `nav` element, fixed top, height 48px, full width minus left rail (`left-14` on lg)
|
|
250
|
+
- [ ] Slots: `breadcrumbs` (left), `search` (center, max ~360px), `actions` (right — color mode toggle, user menu)
|
|
251
|
+
- [ ] Mobile: hamburger button replaces breadcrumbs/center area (toggles `#sidebar-navigation`)
|
|
252
|
+
- [ ] Renders nothing or a sensible fallback if breadcrumbs slot empty
|
|
253
|
+
|
|
254
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/ui/layout/topbar_test.rb -v`
|
|
255
|
+
|
|
256
|
+
**Steps:**
|
|
257
|
+
|
|
258
|
+
- [ ] **Step 1:** Implement Topbar:
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
class Topbar < Plutonium::UI::Component::Base
|
|
262
|
+
include Phlex::Slotable
|
|
263
|
+
slot :breadcrumbs
|
|
264
|
+
slot :search
|
|
265
|
+
slot :action, collection: true
|
|
266
|
+
|
|
267
|
+
def view_template
|
|
268
|
+
nav(
|
|
269
|
+
class: "fixed top-0 right-0 left-0 lg:left-14 z-30 h-12 " \
|
|
270
|
+
"bg-[var(--pu-surface)] border-b border-[var(--pu-border)] " \
|
|
271
|
+
"flex items-center gap-3 px-4",
|
|
272
|
+
data: {controller: "resource-header",
|
|
273
|
+
resource_header_sidebar_outlet: "#sidebar-navigation"}
|
|
274
|
+
) do
|
|
275
|
+
render_hamburger
|
|
276
|
+
render_breadcrumbs_section
|
|
277
|
+
render_search_section
|
|
278
|
+
render_actions_section
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
# ...
|
|
282
|
+
end
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
- [ ] **Step 2:** Slim `Breadcrumbs` for topbar use — drop large-title duplication; render as compact ` › `-separated path with last segment as the current page label (no link).
|
|
286
|
+
- [ ] **Step 3:** Tests:
|
|
287
|
+
- renders nav with expected classes
|
|
288
|
+
- breadcrumb slot renders into left position
|
|
289
|
+
- search slot renders centered with max-width
|
|
290
|
+
- hamburger button toggles correctly (assert `data-action` present)
|
|
291
|
+
- [ ] **Step 4:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/ui/layout/topbar_test.rb -v`. Expect pass.
|
|
292
|
+
- [ ] **Step 5:** Commit: `feat(ui): add Topbar layout component`
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
### Task 4: Wire ResourceLayout to new shell
|
|
297
|
+
|
|
298
|
+
**Goal:** Replace `ResourceLayout`'s Rails partials (`resource_header`, `resource_sidebar`) with the Phlex `IconRail` + `Topbar`. Drop the old `Layout::Header` and `Layout::Sidebar` once nothing references them.
|
|
299
|
+
|
|
300
|
+
**Files:**
|
|
301
|
+
- Modify: `lib/plutonium/ui/layout/resource_layout.rb`
|
|
302
|
+
- Delete (or deprecate): `lib/plutonium/ui/layout/header.rb`, `lib/plutonium/ui/layout/sidebar.rb`
|
|
303
|
+
- Modify: `lib/plutonium/ui/layout/base.rb` (`main_attributes` padding adjusted for 56px rail + 48px topbar)
|
|
304
|
+
- Delete (or replace): partial templates `_resource_header.*`, `_resource_sidebar.*` if Phlex-only path
|
|
305
|
+
- Update: `test/dummy/` portal config if it references old layouts
|
|
306
|
+
|
|
307
|
+
**Acceptance Criteria:**
|
|
308
|
+
- [ ] `ResourceLayout#render_before_main` renders `IconRail` + `Topbar` Phlex components instead of partials
|
|
309
|
+
- [ ] `main_attributes` padding: `pt-12 lg:pl-14 px-6`
|
|
310
|
+
- [ ] Booting dummy app and visiting any portal URL shows: 56px rail, 48px topbar, content offset correctly
|
|
311
|
+
- [ ] No references to `Layout::Header` or `Layout::Sidebar` remain (or they're deprecated with a notice)
|
|
312
|
+
- [ ] System test: at least one existing portal-page system test passes
|
|
313
|
+
|
|
314
|
+
**Verify:** `bundle exec appraisal rails-8.1 rake test` and manual smoke test in dummy
|
|
315
|
+
|
|
316
|
+
**Steps:**
|
|
317
|
+
|
|
318
|
+
- [ ] **Step 1:** Replace `render partial("resource_header")` and `render partial("resource_sidebar")` calls in `resource_layout.rb` with `render IconRail.new(menu: ...)` and `render Topbar.new { |t| ... }`.
|
|
319
|
+
- [ ] **Step 2:** Update `main_attributes`:
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
def main_attributes = mix(super, {class: "pt-12 lg:pl-14 px-6 min-h-screen"})
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
- [ ] **Step 3:** Move portal logo/brand/menu data sources from old partials into the new components. Likely a portal-level helper or initializer change.
|
|
326
|
+
- [ ] **Step 4:** Delete `lib/plutonium/ui/layout/header.rb` and `lib/plutonium/ui/layout/sidebar.rb` if no callers remain. Otherwise add a deprecation warning and a TODO.
|
|
327
|
+
- [ ] **Step 5:** Delete `_resource_header.*` and `_resource_sidebar.*` partials if no longer rendered.
|
|
328
|
+
- [ ] **Step 6:** Run full test suite: `bundle exec appraisal rails-8.1 rake test`. Fix breakages.
|
|
329
|
+
- [ ] **Step 7:** Manual smoke: `cd test/dummy && bundle exec rails s`, navigate every portal type. Confirm layout renders.
|
|
330
|
+
- [ ] **Step 8:** Commit: `refactor(ui): wire ResourceLayout to icon-rail + topbar shell`
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
# Phase 3 — Index Page
|
|
335
|
+
|
|
336
|
+
### Task 5: Index toolbar
|
|
337
|
+
|
|
338
|
+
**Goal:** New `Plutonium::UI::Table::Toolbar` Phlex component — view switcher (Grid only initially, Cards/Kanban as placeholders), Filter button (popover), Group button (popover), visible search input, column-config + overflow icon buttons.
|
|
339
|
+
|
|
340
|
+
**Files:**
|
|
341
|
+
- Create: `lib/plutonium/ui/table/components/toolbar.rb`
|
|
342
|
+
- Create: `lib/plutonium/ui/table/components/view_switcher.rb`
|
|
343
|
+
- Modify: `lib/plutonium/ui/table/resource.rb` (render Toolbar above the table)
|
|
344
|
+
- Test: `test/plutonium/ui/table/components/toolbar_test.rb`
|
|
345
|
+
|
|
346
|
+
**Acceptance Criteria:**
|
|
347
|
+
- [ ] Toolbar order (left → right): view switcher, divider, Filter button, Group button, spacer (`flex-grow`), search input, divider, column-config icon, overflow icon
|
|
348
|
+
- [ ] Search input shows current `params[:search]` value, submits on enter
|
|
349
|
+
- [ ] Filter button click opens existing filter panel as a popover (anchored under button), not a slideout
|
|
350
|
+
- [ ] Group button is a placeholder dropdown stub if grouping isn't implemented yet (greyed-out menu items "Group by …")
|
|
351
|
+
- [ ] View switcher: Grid is active by default; Cards/Kanban are visible but disabled (with `title="Coming soon"`)
|
|
352
|
+
|
|
353
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/ui/table/components/toolbar_test.rb -v`
|
|
354
|
+
|
|
355
|
+
**Steps:**
|
|
356
|
+
|
|
357
|
+
- [ ] **Step 1:** Implement `ViewSwitcher` as a 3-segment control:
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
class ViewSwitcher < Plutonium::UI::Component::Base
|
|
361
|
+
def initialize(active: :grid)
|
|
362
|
+
@active = active
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def view_template
|
|
366
|
+
div(class: "inline-flex h-8 rounded-md border border-[var(--pu-border)] bg-[var(--pu-surface)] overflow-hidden text-sm") do
|
|
367
|
+
segment(:grid, "Grid")
|
|
368
|
+
segment(:cards, "Cards", disabled: true)
|
|
369
|
+
segment(:kanban, "Kanban", disabled: true)
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
# ...
|
|
373
|
+
end
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
- [ ] **Step 2:** Implement `Toolbar`:
|
|
377
|
+
|
|
378
|
+
```ruby
|
|
379
|
+
class Toolbar < Plutonium::UI::Component::Base
|
|
380
|
+
def initialize(query:, filters_present:)
|
|
381
|
+
@query = query
|
|
382
|
+
@filters_present = filters_present
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def view_template
|
|
386
|
+
div(class: "flex items-center gap-2 px-4 py-2 border-b border-[var(--pu-border)] bg-[var(--pu-surface-alt)]") do
|
|
387
|
+
render ViewSwitcher.new
|
|
388
|
+
divider
|
|
389
|
+
filter_button
|
|
390
|
+
group_button
|
|
391
|
+
div(class: "flex-1")
|
|
392
|
+
search_input
|
|
393
|
+
divider
|
|
394
|
+
column_config_button
|
|
395
|
+
overflow_button
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
# ...
|
|
399
|
+
end
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
- [ ] **Step 3:** In `lib/plutonium/ui/table/resource.rb`, render the Toolbar above the existing table markup. Confirm filter-panel-controller still hooks correctly when clicked.
|
|
403
|
+
- [ ] **Step 4:** Tests:
|
|
404
|
+
- toolbar renders all elements in expected order
|
|
405
|
+
- search input echoes current search param
|
|
406
|
+
- disabled segments have `disabled` attribute and tooltip
|
|
407
|
+
- [ ] **Step 5:** Manual: dummy app index page shows new toolbar; clicking Filter opens existing filter UI as a popover.
|
|
408
|
+
- [ ] **Step 6:** Commit: `feat(ui): add index toolbar with view switcher`
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
### Task 6: Active filter pills + result count
|
|
413
|
+
|
|
414
|
+
**Goal:** Below the toolbar, render a strip showing active filters as removable pills, a `+ Filter` dashed pill, and a right-aligned result count.
|
|
415
|
+
|
|
416
|
+
**Files:**
|
|
417
|
+
- Create: `lib/plutonium/ui/table/components/filter_pills.rb`
|
|
418
|
+
- Modify: `lib/plutonium/ui/table/resource.rb` (render pills strip after toolbar)
|
|
419
|
+
- Modify: `lib/plutonium/resource/query_object.rb` if a helper for "active filters list" is needed
|
|
420
|
+
- Test: `test/plutonium/ui/table/components/filter_pills_test.rb`
|
|
421
|
+
|
|
422
|
+
**Acceptance Criteria:**
|
|
423
|
+
- [ ] Each active filter renders as `<field> <op> <value>` pill with `✕` (links to URL with that filter cleared)
|
|
424
|
+
- [ ] `+ Filter` dashed pill opens the same popover as the toolbar Filter button
|
|
425
|
+
- [ ] Right-aligned: total record count from pagination ("147 results")
|
|
426
|
+
- [ ] Strip is hidden entirely when no filters active and result count is 0 (or render only count)
|
|
427
|
+
|
|
428
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/ui/table/components/filter_pills_test.rb -v`
|
|
429
|
+
|
|
430
|
+
**Steps:**
|
|
431
|
+
|
|
432
|
+
- [ ] **Step 1:** Add `query_object#active_filter_descriptions` returning `[{name:, label:, op:, value:, clear_url:}]`.
|
|
433
|
+
- [ ] **Step 2:** Implement `FilterPills`:
|
|
434
|
+
|
|
435
|
+
```ruby
|
|
436
|
+
class FilterPills < Plutonium::UI::Component::Base
|
|
437
|
+
def initialize(query:, total_count:)
|
|
438
|
+
@query = query
|
|
439
|
+
@total_count = total_count
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def view_template
|
|
443
|
+
return if @query.active_filter_descriptions.empty? && @total_count.zero?
|
|
444
|
+
div(class: "flex items-center gap-1.5 px-4 py-2 border-b border-[var(--pu-border)] flex-wrap") do
|
|
445
|
+
@query.active_filter_descriptions.each { |f| render_pill(f) }
|
|
446
|
+
render_add_filter_pill
|
|
447
|
+
div(class: "ml-auto text-xs text-[var(--pu-text-muted)]") { "#{@total_count} results" }
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
# ...
|
|
451
|
+
end
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
- [ ] **Step 3:** Pill style: `h-6 px-2 rounded-full bg-primary-50 border border-primary-200 text-xs text-primary-700 inline-flex items-center gap-1.5`. Add-filter pill: `border-dashed`.
|
|
455
|
+
- [ ] **Step 4:** Wire into `lib/plutonium/ui/table/resource.rb` between Toolbar and table.
|
|
456
|
+
- [ ] **Step 5:** Tests:
|
|
457
|
+
- empty filters + zero count → renders nothing
|
|
458
|
+
- 2 active filters → 2 pills + add-pill + count
|
|
459
|
+
- clearing a pill: `clear_url` builds correctly via `query.build_url(filter: ...)` reset
|
|
460
|
+
- [ ] **Step 6:** Commit: `feat(ui): add active filter pills strip with result count`
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
### Task 7: Column-header sort with priority badges + ⋯ menu
|
|
465
|
+
|
|
466
|
+
**Goal:** Sort moves into table column headers. Click cycles asc → desc → none. Shift-click adds secondary sort. Active sort columns show ↑/↓ + priority badge (1, 2, …). Per-column `⋯` opens a menu with sort/group/filter/hide options.
|
|
467
|
+
|
|
468
|
+
**Files:**
|
|
469
|
+
- Modify: `lib/plutonium/ui/table/resource.rb` (header cell rendering)
|
|
470
|
+
- Modify: `lib/plutonium/ui/table/theme.rb` (header cell classes)
|
|
471
|
+
- Modify: `lib/plutonium/resource/query_object.rb` — `sort_params_for` returns `{url:, reset_url:, position:, direction:, multi_url:}` where `multi_url` is the shift-click target (preserves other sorts).
|
|
472
|
+
- Modify: existing sort-toggle Stimulus controller if any, or wire shift-click via JS
|
|
473
|
+
- Test: `test/plutonium/resource/query_object_test.rb`, `test/plutonium/ui/table/resource_test.rb`
|
|
474
|
+
|
|
475
|
+
**Acceptance Criteria:**
|
|
476
|
+
- [ ] Plain click navigates to `sort_params[:url]` (resets other sorts, sets this column)
|
|
477
|
+
- [ ] Shift-click navigates to `sort_params[:multi_url]` (preserves other sorts, toggles this column's direction)
|
|
478
|
+
- [ ] Active sort column shows ↑/↓ icon and priority badge
|
|
479
|
+
- [ ] Priority badge hidden when only one column is sorted
|
|
480
|
+
- [ ] `⋯` button per column reveals menu with: Sort asc, Sort desc, Clear sort, Group by …, Filter by …, Hide column
|
|
481
|
+
- [ ] Hide column persists per-user (use existing column-config mechanism if present, or `localStorage` via Stimulus)
|
|
482
|
+
|
|
483
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium -v`
|
|
484
|
+
|
|
485
|
+
**Steps:**
|
|
486
|
+
|
|
487
|
+
- [ ] **Step 1:** Update `lib/plutonium/resource/query_object.rb` `sort_params_for` to also build `multi_url` (preserves all other current sorts, toggles this one):
|
|
488
|
+
|
|
489
|
+
```ruby
|
|
490
|
+
def sort_params_for(name)
|
|
491
|
+
return unless sort_definitions[name]
|
|
492
|
+
{
|
|
493
|
+
url: build_url(sort: name, replace: true),
|
|
494
|
+
multi_url: build_url(sort: name),
|
|
495
|
+
reset_url: build_url(sort: name, reset: true),
|
|
496
|
+
position: selected_sort_fields.index(name.to_s),
|
|
497
|
+
direction: selected_sort_directions[name]
|
|
498
|
+
}
|
|
499
|
+
end
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
(Implementation of `replace: true` requires updating `build_url`/sort param parsing — sort is appended to existing list normally; `replace: true` discards prior sorts.)
|
|
503
|
+
|
|
504
|
+
- [ ] **Step 2:** Update header cell rendering in `lib/plutonium/ui/table/resource.rb` to:
|
|
505
|
+
|
|
506
|
+
```ruby
|
|
507
|
+
def render_sort_header(column, sort_params)
|
|
508
|
+
th(class: "...") do
|
|
509
|
+
a(href: sort_params[:url],
|
|
510
|
+
data: {action: "click->table#headerClick"},
|
|
511
|
+
data_multi_href: sort_params[:multi_url],
|
|
512
|
+
class: "flex items-center gap-1 cursor-pointer") do
|
|
513
|
+
span { column.label }
|
|
514
|
+
render_sort_indicator(sort_params)
|
|
515
|
+
render_column_menu_trigger(column)
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
- [ ] **Step 3:** Add Stimulus controller `table_controller.js` (or extend existing) to handle shift-click → use `data-multi-href`:
|
|
522
|
+
|
|
523
|
+
```javascript
|
|
524
|
+
headerClick(event) {
|
|
525
|
+
if (event.shiftKey) {
|
|
526
|
+
event.preventDefault();
|
|
527
|
+
const url = event.currentTarget.dataset.multiHref;
|
|
528
|
+
if (url) Turbo.visit(url);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
- [ ] **Step 4:** Implement priority badge — show only when `selected_sort_fields.size > 1`.
|
|
534
|
+
- [ ] **Step 5:** Implement column `⋯` menu (Phlex component or existing dropdown). Items navigate to URL fragments: sort asc/desc/clear use `sort_params`, group/filter open the existing popovers pre-filled with the column.
|
|
535
|
+
- [ ] **Step 6:** Tests:
|
|
536
|
+
- `sort_params_for(:name)` returns `multi_url` and `url` distinct
|
|
537
|
+
- clicking sets sort to single
|
|
538
|
+
- shift-click adds to multi-sort
|
|
539
|
+
- [ ] **Step 7:** Manual: index page, click Name → sorts; shift-click Created → priority badges 1 and 2 appear.
|
|
540
|
+
- [ ] **Step 8:** Commit: `feat(ui): column-header sort with shift-click multi-sort`
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
### Task 8: Floating bulk action bar
|
|
545
|
+
|
|
546
|
+
**Goal:** When ≥1 row is selected, hide the filter pills strip and show a tinted bulk-action bar in its place — selection count, action buttons (Export, Archive, Delete with danger tone), Clear selection.
|
|
547
|
+
|
|
548
|
+
**Files:**
|
|
549
|
+
- Create: `lib/plutonium/ui/table/components/bulk_action_bar.rb`
|
|
550
|
+
- Modify: `lib/plutonium/ui/table/resource.rb`
|
|
551
|
+
- Modify: `src/js/controllers/bulk_actions_controller.js` (toggle visibility of pills vs bulk bar based on selection count)
|
|
552
|
+
- Test: `test/plutonium/ui/table/components/bulk_action_bar_test.rb`
|
|
553
|
+
|
|
554
|
+
**Acceptance Criteria:**
|
|
555
|
+
- [ ] Bulk bar is hidden when 0 selected, visible when ≥1
|
|
556
|
+
- [ ] Pills strip is hidden when bulk bar is visible (mutually exclusive)
|
|
557
|
+
- [ ] Action buttons come from the resource's `bulk_action`s, sorted by `position`
|
|
558
|
+
- [ ] Delete action uses danger tone (red border, red text)
|
|
559
|
+
- [ ] "Clear selection" button deselects all and re-shows pills strip
|
|
560
|
+
- [ ] Background: `bg-primary-50 dark:bg-primary-950/30`
|
|
561
|
+
|
|
562
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/ui/table/components/bulk_action_bar_test.rb -v`
|
|
563
|
+
|
|
564
|
+
**Steps:**
|
|
565
|
+
|
|
566
|
+
- [ ] **Step 1:** Implement `BulkActionBar`:
|
|
567
|
+
|
|
568
|
+
```ruby
|
|
569
|
+
class BulkActionBar < Plutonium::UI::Component::Base
|
|
570
|
+
def initialize(actions:)
|
|
571
|
+
@actions = actions
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
def view_template
|
|
575
|
+
div(class: "hidden bg-primary-50 dark:bg-primary-950/30 border-b border-[var(--pu-border)] px-4 py-2 flex items-center gap-3",
|
|
576
|
+
data: {bulk_actions_target: "bar"}) do
|
|
577
|
+
span(class: "text-sm font-medium text-primary-700 dark:text-primary-300") {
|
|
578
|
+
plain_text "0 selected"
|
|
579
|
+
}
|
|
580
|
+
div(class: "flex items-center gap-1.5") { @actions.each { |a| render_action_button(a) } }
|
|
581
|
+
div(class: "flex-1")
|
|
582
|
+
button(class: "text-xs text-primary-700 hover:underline",
|
|
583
|
+
data: {action: "bulk-actions#clear"}) { "Clear selection" }
|
|
584
|
+
end
|
|
585
|
+
end
|
|
586
|
+
# ...
|
|
587
|
+
end
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
- [ ] **Step 2:** Update `bulk_actions_controller.js` to:
|
|
591
|
+
- track selection count
|
|
592
|
+
- toggle `hidden` class on the bar element AND the pills strip
|
|
593
|
+
- update count text in the bar
|
|
594
|
+
|
|
595
|
+
- [ ] **Step 3:** Wire bar into `lib/plutonium/ui/table/resource.rb` directly above the table, alongside the pills strip (only one visible at a time).
|
|
596
|
+
- [ ] **Step 4:** Tests for component rendering and Stimulus selection count update.
|
|
597
|
+
- [ ] **Step 5:** Manual: select row → pills hide, bar shows with count "1 selected"; select more → count updates; click Clear → bar hides.
|
|
598
|
+
- [ ] **Step 6:** Commit: `feat(ui): add floating bulk action bar`
|
|
599
|
+
|
|
600
|
+
---
|
|
601
|
+
|
|
602
|
+
# Phase 4 — Show + Form Pages
|
|
603
|
+
|
|
604
|
+
### Task 9: Show page redesign
|
|
605
|
+
|
|
606
|
+
**Goal:** Show page becomes a single-column layout under PageHeader, with field panels as cards and a reserved (empty) `render_aside` slot for the future metadata DSL.
|
|
607
|
+
|
|
608
|
+
**Files:**
|
|
609
|
+
- Modify: `lib/plutonium/ui/page/show.rb`
|
|
610
|
+
- Modify: `lib/plutonium/ui/page/base.rb` (add empty `render_aside` hook)
|
|
611
|
+
- Modify: `lib/plutonium/ui/display/resource.rb`
|
|
612
|
+
- Modify: `lib/plutonium/ui/panel.rb` (card styling)
|
|
613
|
+
- Test: `test/plutonium/ui/page/show_test.rb`
|
|
614
|
+
|
|
615
|
+
**Acceptance Criteria:**
|
|
616
|
+
- [ ] Show page renders single column, max-width ~960px, centered when viewport allows
|
|
617
|
+
- [ ] Field panels use `pu-card` chrome with uppercase 9px section labels
|
|
618
|
+
- [ ] `render_aside` exists in `Page::Base`, no-op by default; show layout reserves space for it (e.g., grid with `grid-cols-[1fr_240px]` when `aside_present?` else `grid-cols-1`)
|
|
619
|
+
- [ ] Tabs render flush against header (Phase 1 work continues to apply)
|
|
620
|
+
|
|
621
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/ui/page/show_test.rb -v`
|
|
622
|
+
|
|
623
|
+
**Steps:**
|
|
624
|
+
|
|
625
|
+
- [ ] **Step 1:** Add `render_aside` no-op to `Page::Base`:
|
|
626
|
+
|
|
627
|
+
```ruby
|
|
628
|
+
def render_aside
|
|
629
|
+
# no-op by default; show page reserves layout slot
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def aside_present? = false
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
- [ ] **Step 2:** Update `Page::Show`:
|
|
636
|
+
|
|
637
|
+
```ruby
|
|
638
|
+
def render_default_content
|
|
639
|
+
div(class: aside_present? ? "grid grid-cols-1 lg:grid-cols-[1fr_240px] gap-6" : "max-w-[960px] mx-auto") do
|
|
640
|
+
div { render partial("resource_details") }
|
|
641
|
+
aside(class: "hidden lg:block") { render_aside } if aside_present?
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
- [ ] **Step 3:** Update panel/section component to render with `pu-card` + uppercase 9px label:
|
|
647
|
+
|
|
648
|
+
```ruby
|
|
649
|
+
section(class: "pu-card p-4 mb-4") do
|
|
650
|
+
div(class: "text-[9px] font-semibold uppercase tracking-wider text-[var(--pu-text-muted)] mb-2") { label }
|
|
651
|
+
# ... fields ...
|
|
652
|
+
end
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
- [ ] **Step 4:** Tests covering: layout grid, aside slot empty by default, panel chrome.
|
|
656
|
+
- [ ] **Step 5:** Manual: dummy app `/admin/users/1` shows single column with card panels.
|
|
657
|
+
- [ ] **Step 6:** Commit: `feat(ui): redesign Show page single-column with reserved aside`
|
|
658
|
+
|
|
659
|
+
---
|
|
660
|
+
|
|
661
|
+
### Task 10: Form page redesign
|
|
662
|
+
|
|
663
|
+
**Goal:** New/edit/interactive-action page renders a 580px centered column with card sections and a sticky footer for Cancel/Save. Inline validation: errors render as 12px danger text directly below each field.
|
|
664
|
+
|
|
665
|
+
**Files:**
|
|
666
|
+
- Modify: `lib/plutonium/ui/page/{new,edit,interactive_action}.rb`
|
|
667
|
+
- Modify: `lib/plutonium/ui/form/resource.rb`
|
|
668
|
+
- Modify: `lib/plutonium/ui/form/interaction.rb`
|
|
669
|
+
- Modify: `lib/plutonium/ui/form/theme.rb` (section card classes)
|
|
670
|
+
- Create: `lib/plutonium/ui/form/components/sticky_footer.rb`
|
|
671
|
+
- Test: `test/plutonium/ui/form/sticky_footer_test.rb`
|
|
672
|
+
|
|
673
|
+
**Acceptance Criteria:**
|
|
674
|
+
- [ ] Form column max-width 580px, centered (`max-w-[580px] mx-auto`)
|
|
675
|
+
- [ ] Field groups render as `pu-card` panels with uppercase 9px labels
|
|
676
|
+
- [ ] Sticky footer at viewport bottom: 56px tall, white surface, top border, Cancel + Save right-aligned
|
|
677
|
+
- [ ] Inline validation: error text under field, `text-xs text-danger-600 mt-1`; no toasts for field errors
|
|
678
|
+
- [ ] Modal context: sticky footer not rendered (modal owns its footer)
|
|
679
|
+
|
|
680
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/ui/form -v`
|
|
681
|
+
|
|
682
|
+
**Steps:**
|
|
683
|
+
|
|
684
|
+
- [ ] **Step 1:** Implement `StickyFooter`:
|
|
685
|
+
|
|
686
|
+
```ruby
|
|
687
|
+
class StickyFooter < Plutonium::UI::Component::Base
|
|
688
|
+
def view_template(&)
|
|
689
|
+
div(class: "fixed bottom-0 left-0 right-0 lg:left-14 z-20 " \
|
|
690
|
+
"h-14 bg-[var(--pu-surface)] border-t border-[var(--pu-border)] " \
|
|
691
|
+
"px-4 flex items-center justify-end gap-2", &)
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
- [ ] **Step 2:** Update `Form::Resource` and `Form::Interaction` to wrap content in centered column and emit submit/cancel into `StickyFooter` (skip when `@in_modal`).
|
|
697
|
+
- [ ] **Step 3:** Add `@in_modal` initializer kwarg to forms; `Page::InteractiveAction` and remote-modal-rendered new/edit set it `true`.
|
|
698
|
+
- [ ] **Step 4:** Update form theme `error_message` to `"text-xs text-danger-600 mt-1"`. Confirm error toasts (flash messages) are unaffected — only field errors style changed.
|
|
699
|
+
- [ ] **Step 5:** Adjust page main padding to leave room for sticky footer: `pb-16` on form pages.
|
|
700
|
+
- [ ] **Step 6:** Tests:
|
|
701
|
+
- `StickyFooter` renders with expected classes
|
|
702
|
+
- form in non-modal context renders sticky footer
|
|
703
|
+
- form with `in_modal: true` does NOT render sticky footer
|
|
704
|
+
- [ ] **Step 7:** Manual: dummy app `/admin/users/new` shows centered form with sticky bottom footer; submit error → inline error under field.
|
|
705
|
+
- [ ] **Step 8:** Commit: `feat(ui): redesign form pages with centered column and sticky footer`
|
|
706
|
+
|
|
707
|
+
---
|
|
708
|
+
|
|
709
|
+
# Phase 5 — Modals
|
|
710
|
+
|
|
711
|
+
### Task 11: Slideover modal mode + per-interaction option
|
|
712
|
+
|
|
713
|
+
**Goal:** Add a slideover modal mode alongside the existing centered modal. Allow interactions to opt in via `interactive_action :name, modal: :slideover`.
|
|
714
|
+
|
|
715
|
+
**Files:**
|
|
716
|
+
- Modify: `lib/plutonium/resource/interactions/options.rb` (or wherever `interactive_action` registers options) — accept `modal:` kwarg
|
|
717
|
+
- Modify: existing modal Phlex component (likely `lib/plutonium/ui/component/...` or `app/views/layouts/_remote_modal*`) — split into `centered` and `slideover` outer containers, shared header/body/footer
|
|
718
|
+
- Modify: `src/js/controllers/remote_modal_controller.js` — read `data-modal-mode` and apply correct outer classes/animation
|
|
719
|
+
- Test: `test/plutonium/interaction/...` for the `modal:` option, plus a system test for slideover rendering
|
|
720
|
+
|
|
721
|
+
**Acceptance Criteria:**
|
|
722
|
+
- [ ] `interactive_action :reschedule, modal: :slideover` stores the mode on the action definition
|
|
723
|
+
- [ ] When this action triggers the remote modal, the modal renders as a right slideover (`right-0 top-0 h-full w-[480px]`) instead of centered
|
|
724
|
+
- [ ] Defaulted modal mode is `:centered`
|
|
725
|
+
- [ ] Slideover animates in from the right (Tailwind transition utilities)
|
|
726
|
+
- [ ] Mobile: slideover becomes full-screen
|
|
727
|
+
|
|
728
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction -v` and a new system test
|
|
729
|
+
|
|
730
|
+
**Steps:**
|
|
731
|
+
|
|
732
|
+
- [ ] **Step 1:** Locate where `interactive_action` registers — extend the option object with `modal: :centered` default and `:slideover` accepted value.
|
|
733
|
+
- [ ] **Step 2:** Pass the chosen mode to the modal renderer (likely via a controller assignment to `@modal_mode` or a turbo-stream attribute).
|
|
734
|
+
- [ ] **Step 3:** Refactor the modal markup:
|
|
735
|
+
|
|
736
|
+
```erb
|
|
737
|
+
<%# centered (default) %>
|
|
738
|
+
<dialog class="fixed inset-0 m-auto max-w-[520px] w-full max-h-[80vh] rounded-lg ..." data-modal-mode="centered">
|
|
739
|
+
...
|
|
740
|
+
</dialog>
|
|
741
|
+
|
|
742
|
+
<%# slideover %>
|
|
743
|
+
<dialog class="fixed top-0 right-0 h-screen w-full sm:w-[480px] m-0 rounded-none ..." data-modal-mode="slideover">
|
|
744
|
+
...
|
|
745
|
+
</dialog>
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
- [ ] **Step 4:** Add CSS transitions for slideover (translate-x-full → translate-x-0 on open).
|
|
749
|
+
- [ ] **Step 5:** Add a system test: define an interactive action with `modal: :slideover`, trigger it, assert `[data-modal-mode='slideover']` element is present in DOM.
|
|
750
|
+
- [ ] **Step 6:** Commit: `feat(ui): support slideover modal mode for interactions`
|
|
751
|
+
|
|
752
|
+
---
|
|
753
|
+
|
|
754
|
+
### Task 12: Per-resource modal declaration
|
|
755
|
+
|
|
756
|
+
**Goal:** Resource definitions can declare `modal :slideover` to control how new/edit forms render when triggered through the modal turbo frame.
|
|
757
|
+
|
|
758
|
+
**Files:**
|
|
759
|
+
- Modify: `lib/plutonium/resource/definition.rb` (add `modal` class-level DSL with default `:centered`)
|
|
760
|
+
- Modify: `lib/plutonium/resource/controllers/crud_actions.rb` (or whichever controller invokes new/edit) — when request targets the remote modal frame, pass `definition.modal_mode` to the layout
|
|
761
|
+
- Modify: layout for modal-rendered new/edit forms to read mode from definition
|
|
762
|
+
- Test: `test/plutonium/resource/definition_test.rb`, system test for slideover-quick-create
|
|
763
|
+
|
|
764
|
+
**Acceptance Criteria:**
|
|
765
|
+
- [ ] `class CustomerDefinition; modal :slideover; end` is accepted; `current_definition.modal_mode == :slideover`
|
|
766
|
+
- [ ] Default value is `:centered`
|
|
767
|
+
- [ ] When `+ New` from the index toolbar targets the remote modal frame and the resource declares `modal :slideover`, the modal renders as slideover
|
|
768
|
+
- [ ] Page-level (non-modal) new/edit URLs render the §5 page form regardless of declaration
|
|
769
|
+
|
|
770
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/resource/definition_test.rb -v` plus a system test
|
|
771
|
+
|
|
772
|
+
**Steps:**
|
|
773
|
+
|
|
774
|
+
- [ ] **Step 1:** Add to `lib/plutonium/resource/definition.rb`:
|
|
775
|
+
|
|
776
|
+
```ruby
|
|
777
|
+
class_attribute :modal_mode, default: :centered
|
|
778
|
+
|
|
779
|
+
def self.modal(mode)
|
|
780
|
+
raise ArgumentError, "modal must be :centered or :slideover" unless [:centered, :slideover].include?(mode)
|
|
781
|
+
self.modal_mode = mode
|
|
782
|
+
end
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
- [ ] **Step 2:** Where the remote modal renders new/edit (probably a layout/template that wraps `resource_form`), read `current_definition.modal_mode` and apply mode.
|
|
786
|
+
- [ ] **Step 3:** Update the index toolbar `+ New` button to target the remote modal frame (existing behavior; confirm).
|
|
787
|
+
- [ ] **Step 4:** Tests:
|
|
788
|
+
- `definition_test.rb`: defining `modal :slideover` sets `modal_mode`; invalid raises
|
|
789
|
+
- system test: `+ New` on a resource with `modal :slideover` opens slideover
|
|
790
|
+
- [ ] **Step 5:** Commit: `feat(ui): per-resource modal mode declaration`
|
|
791
|
+
|
|
792
|
+
---
|
|
793
|
+
|
|
794
|
+
## Final: Cleanup & Docs
|
|
795
|
+
|
|
796
|
+
### Task 13: Documentation + changelog
|
|
797
|
+
|
|
798
|
+
**Goal:** Update Plutonium docs to reflect new UI patterns; add an upgrade note for app developers.
|
|
799
|
+
|
|
800
|
+
**Files:**
|
|
801
|
+
- Modify: `docs/getting-started/*` (any screenshots/snippets that show old UI)
|
|
802
|
+
- Modify: `docs/guides/*`
|
|
803
|
+
- Create: `docs/guides/ui-overhaul-2026.md` — what changed, what apps need to do
|
|
804
|
+
- Update: `.claude/skills/plutonium-views.md` to reference new components
|
|
805
|
+
- Update: `CHANGELOG.md`
|
|
806
|
+
|
|
807
|
+
**Acceptance Criteria:**
|
|
808
|
+
- [ ] Upgrade guide explains: replaced sidebar with icon rail; new toolbar; column-header sort; sticky form footer; new modal modes
|
|
809
|
+
- [ ] Skill files updated for IconRail, Topbar, Toolbar, FilterPills, BulkActionBar, StickyFooter
|
|
810
|
+
- [ ] Changelog entry under unreleased
|
|
811
|
+
|
|
812
|
+
**Verify:** `yarn docs:build` succeeds; manual scan of docs site
|
|
813
|
+
|
|
814
|
+
**Steps:**
|
|
815
|
+
|
|
816
|
+
- [ ] **Step 1:** Write upgrade guide.
|
|
817
|
+
- [ ] **Step 2:** Update affected skill files in `.claude/skills/`.
|
|
818
|
+
- [ ] **Step 3:** Add changelog entry.
|
|
819
|
+
- [ ] **Step 4:** `yarn docs:build`.
|
|
820
|
+
- [ ] **Step 5:** Commit: `docs(ui): document UI layout overhaul`
|
|
821
|
+
|
|
822
|
+
---
|
|
823
|
+
|
|
824
|
+
## Self-Review Notes
|
|
825
|
+
|
|
826
|
+
**Spec coverage:**
|
|
827
|
+
- §1 Shell → Tasks 2, 3, 4
|
|
828
|
+
- §2 PageHeader → Task 1
|
|
829
|
+
- §3 Index → Tasks 5, 6, 7, 8
|
|
830
|
+
- §4 Show → Task 9
|
|
831
|
+
- §5 Form → Task 10
|
|
832
|
+
- §6 Density → Task 0
|
|
833
|
+
- §7 Modals → Tasks 11, 12
|
|
834
|
+
- §8 Compatibility / cleanup → Tasks 4 (drop legacy), 13 (docs)
|
|
835
|
+
- §9 Out-of-scope items not implemented (correct, by design)
|
|
836
|
+
|
|
837
|
+
**User Verification scan:** Spec does not require user-in-the-loop verification — design was validated during brainstorming. NO verification tasks needed.
|
|
838
|
+
|
|
839
|
+
**Type/name consistency:** `IconRail`, `Topbar`, `Toolbar`, `FilterPills`, `BulkActionBar`, `StickyFooter`, `ViewSwitcher` — used consistently across tasks.
|
|
840
|
+
|
|
841
|
+
**Phase boundaries are commit-able:** after each phase, the framework is in a working state. Phase 1 alone delivers visible improvement (tighter header + density). Phase 2 swaps the shell. Phase 3 transforms the index page. Phase 4 polishes show/form. Phase 5 adds modal flexibility.
|