plutonium 0.60.5 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium/SKILL.md +19 -1
- data/.claude/skills/plutonium-app/SKILL.md +41 -0
- data/.claude/skills/plutonium-auth/SKILL.md +40 -0
- data/.claude/skills/plutonium-behavior/SKILL.md +47 -1
- data/.claude/skills/plutonium-kanban/SKILL.md +313 -0
- data/.claude/skills/plutonium-resource/SKILL.md +40 -0
- data/.claude/skills/plutonium-tenancy/SKILL.md +43 -0
- data/.claude/skills/plutonium-testing/SKILL.md +38 -0
- data/.claude/skills/plutonium-ui/SKILL.md +51 -0
- data/.claude/skills/plutonium-wizard/SKILL.md +469 -0
- data/.cliff.toml +6 -0
- data/Appraisals +3 -0
- data/CHANGELOG.md +549 -439
- data/CLAUDE.md +15 -7
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +895 -193
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +53 -53
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/layouts/basic.html.erb +7 -0
- data/app/views/plutonium/_flash_toasts.html.erb +2 -46
- data/app/views/plutonium/_toast.html.erb +52 -0
- data/app/views/resource/_resource_kanban.html.erb +1 -0
- data/db/migrate/wizard/20260615000001_create_plutonium_wizard_sessions.rb +57 -0
- data/docs/.vitepress/config.ts +24 -0
- data/docs/guides/index.md +2 -0
- data/docs/guides/kanban.md +447 -0
- data/docs/guides/wizards.md +447 -0
- data/docs/public/images/guides/kanban-after-move.png +0 -0
- data/docs/public/images/guides/kanban-board-light.png +0 -0
- data/docs/public/images/guides/kanban-board.png +0 -0
- data/docs/public/images/guides/kanban-show-centered-modal.png +0 -0
- data/docs/public/images/guides/kanban-wip-toast.png +0 -0
- data/docs/public/images/guides/wizards-chooser.png +0 -0
- data/docs/public/images/guides/wizards-completed.png +0 -0
- data/docs/public/images/guides/wizards-index-action.png +0 -0
- data/docs/public/images/guides/wizards-repeater.png +0 -0
- data/docs/public/images/guides/wizards-review.png +0 -0
- data/docs/public/images/guides/wizards-step.png +0 -0
- data/docs/reference/behavior/policies.md +1 -1
- data/docs/reference/index.md +14 -0
- data/docs/reference/kanban/authorization.md +62 -0
- data/docs/reference/kanban/dsl.md +293 -0
- data/docs/reference/kanban/index.md +40 -0
- data/docs/reference/kanban/positioning.md +162 -0
- data/docs/reference/resource/definition.md +16 -0
- data/docs/reference/ui/forms.md +36 -0
- data/docs/reference/ui/pages.md +2 -0
- data/docs/reference/wizard/anchoring-resume.md +194 -0
- data/docs/reference/wizard/dsl.md +332 -0
- data/docs/reference/wizard/index.md +33 -0
- data/docs/reference/wizard/one-time.md +129 -0
- data/docs/reference/wizard/registration-launch.md +177 -0
- data/docs/reference/wizard/storage-config.md +151 -0
- data/docs/superpowers/plans/2026-06-14-form-sectioning.md +2 -2
- data/docs/superpowers/plans/2026-06-15-wizard-dsl.md +1619 -0
- data/docs/superpowers/plans/2026-06-15-wizard-dsl.md.tasks.json +68 -0
- data/docs/superpowers/plans/2026-06-26-kanban-dsl.md +1128 -0
- data/docs/superpowers/plans/2026-06-26-kanban-dsl.md.tasks.json +24 -0
- data/docs/superpowers/specs/2026-06-15-wizard-dsl-design.md +836 -0
- data/docs/superpowers/specs/2026-06-15-wizard-dsl-examples.rb +245 -0
- data/docs/superpowers/specs/2026-06-17-wizard-relaunch-prompt-design.md +86 -0
- data/docs/superpowers/specs/2026-06-18-wizard-attachments-design.md +101 -0
- data/docs/superpowers/specs/2026-06-18-wizard-hosting-design.md +220 -0
- data/docs/superpowers/specs/2026-06-26-kanban-dsl-design.md +388 -0
- data/gemfiles/postgres.gemfile +8 -0
- data/gemfiles/postgres.gemfile.lock +321 -0
- data/gemfiles/rails_7.gemfile +1 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile +1 -0
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile +1 -0
- data/gemfiles/rails_8.1.gemfile.lock +14 -1
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +6 -1
- data/lib/plutonium/action/base.rb +9 -0
- data/lib/plutonium/auth/rodauth.rb +1 -2
- data/lib/plutonium/configuration.rb +4 -0
- data/lib/plutonium/core/controller.rb +20 -1
- data/lib/plutonium/definition/base.rb +25 -0
- data/lib/plutonium/definition/form_layout.rb +54 -35
- data/lib/plutonium/definition/index_views.rb +54 -1
- data/lib/plutonium/definition/wizards.rb +209 -0
- data/lib/plutonium/invites/concerns/invite_token.rb +9 -0
- data/lib/plutonium/invites/concerns/invite_user.rb +9 -0
- data/lib/plutonium/invites/controller.rb +4 -1
- data/lib/plutonium/kanban/action.rb +7 -0
- data/lib/plutonium/kanban/board.rb +40 -0
- data/lib/plutonium/kanban/broadcaster.rb +54 -0
- data/lib/plutonium/kanban/column.rb +69 -0
- data/lib/plutonium/kanban/context.rb +15 -0
- data/lib/plutonium/kanban/dsl.rb +71 -0
- data/lib/plutonium/kanban/grouping.rb +51 -0
- data/lib/plutonium/kanban/positioning.rb +75 -0
- data/lib/plutonium/kanban.rb +11 -0
- data/lib/plutonium/migrations.rb +40 -0
- data/lib/plutonium/positioning.rb +146 -0
- data/lib/plutonium/railtie.rb +33 -0
- data/lib/plutonium/resource/controller.rb +2 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +1 -1
- data/lib/plutonium/resource/controllers/kanban_actions.rb +455 -0
- data/lib/plutonium/resource/controllers/wizard_actions.rb +165 -0
- data/lib/plutonium/resource/policy.rb +8 -0
- data/lib/plutonium/routing/mapper_extensions.rb +44 -0
- data/lib/plutonium/routing/wizard_registration.rb +289 -0
- data/lib/plutonium/ui/display/resource.rb +17 -12
- data/lib/plutonium/ui/form/base.rb +19 -5
- data/lib/plutonium/ui/form/components/password.rb +126 -0
- data/lib/plutonium/ui/form/components/uppy.rb +6 -3
- data/lib/plutonium/ui/form/options/inferred_types.rb +20 -0
- data/lib/plutonium/ui/form/resource.rb +1 -1
- data/lib/plutonium/ui/form/wizard.rb +63 -0
- data/lib/plutonium/ui/grid/card.rb +16 -5
- data/lib/plutonium/ui/kanban/card.rb +67 -0
- data/lib/plutonium/ui/kanban/color_dot.rb +36 -0
- data/lib/plutonium/ui/kanban/column.rb +324 -0
- data/lib/plutonium/ui/kanban/resource.rb +212 -0
- data/lib/plutonium/ui/layout/resource_layout.rb +7 -1
- data/lib/plutonium/ui/modal/base.rb +30 -3
- data/lib/plutonium/ui/modal/centered.rb +5 -2
- data/lib/plutonium/ui/page/index.rb +1 -0
- data/lib/plutonium/ui/page/show.rb +23 -0
- data/lib/plutonium/ui/page/wizard.rb +371 -0
- data/lib/plutonium/ui/page/wizard_chooser.rb +97 -0
- data/lib/plutonium/ui/page/wizard_completed.rb +86 -0
- data/lib/plutonium/ui/table/base.rb +1 -1
- data/lib/plutonium/ui/table/components/view_switcher.rb +2 -1
- data/lib/plutonium/ui/wizard/review.rb +196 -0
- data/lib/plutonium/ui/wizard/stepper.rb +122 -0
- data/lib/plutonium/ui/wizard/summary_display.rb +59 -0
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium/wizard/attachment_data.rb +42 -0
- data/lib/plutonium/wizard/attachments.rb +226 -0
- data/lib/plutonium/wizard/base.rb +216 -0
- data/lib/plutonium/wizard/base_controller.rb +31 -0
- data/lib/plutonium/wizard/configuration.rb +42 -0
- data/lib/plutonium/wizard/controller.rb +162 -0
- data/lib/plutonium/wizard/data.rb +134 -0
- data/lib/plutonium/wizard/driving.rb +639 -0
- data/lib/plutonium/wizard/dsl.rb +336 -0
- data/lib/plutonium/wizard/errors.rb +27 -0
- data/lib/plutonium/wizard/field_capture.rb +157 -0
- data/lib/plutonium/wizard/field_importer.rb +208 -0
- data/lib/plutonium/wizard/gate.rb +171 -0
- data/lib/plutonium/wizard/instance_key.rb +97 -0
- data/lib/plutonium/wizard/lazy_persisted.rb +77 -0
- data/lib/plutonium/wizard/resume.rb +250 -0
- data/lib/plutonium/wizard/review_step.rb +48 -0
- data/lib/plutonium/wizard/route_resolution.rb +40 -0
- data/lib/plutonium/wizard/runner.rb +684 -0
- data/lib/plutonium/wizard/session.rb +53 -0
- data/lib/plutonium/wizard/state.rb +35 -0
- data/lib/plutonium/wizard/step.rb +61 -0
- data/lib/plutonium/wizard/step_adapter.rb +103 -0
- data/lib/plutonium/wizard/store/active_record.rb +174 -0
- data/lib/plutonium/wizard/store/base.rb +42 -0
- data/lib/plutonium/wizard/store/memory.rb +44 -0
- data/lib/plutonium/wizard/sweep_job.rb +76 -0
- data/lib/plutonium/wizard.rb +86 -0
- data/lib/plutonium.rb +5 -0
- data/lib/rodauth/features/case_insensitive_login.rb +1 -1
- data/lib/tasks/release.rake +144 -191
- data/package.json +3 -3
- data/src/css/components.css +132 -0
- data/src/js/controllers/attachment_input_controller.js +15 -1
- data/src/js/controllers/dirty_form_guard_controller.js +155 -27
- data/src/js/controllers/kanban_controller.js +330 -0
- data/src/js/controllers/password_sentinel_controller.js +39 -0
- data/src/js/controllers/register_controllers.js +6 -0
- data/src/js/controllers/remote_modal_controller.js +10 -0
- data/src/js/controllers/row_click_controller.js +14 -1
- data/src/js/controllers/wizard_controller.js +54 -0
- data/src/js/turbo/turbo_confirm.js +1 -1
- data/yarn.lock +271 -282
- metadata +100 -1
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Kanban
|
|
6
|
+
# Board shell — renders one lazy turbo-frame per column.
|
|
7
|
+
#
|
|
8
|
+
# Each frame id is "kanban-col-<key>". Its src merges view=kanban and
|
|
9
|
+
# column=<key> into the current request URL so Task 6's column endpoint
|
|
10
|
+
# fills the body on demand. The frame already contains the column header
|
|
11
|
+
# (color dot + label) so the shell is meaningful while the body loads. The
|
|
12
|
+
# header deliberately omits a card-count badge: the shell has no card data,
|
|
13
|
+
# so a "0" would flash then vanish when Kanban::Column (which renders no
|
|
14
|
+
# count badge) replaces the frame body.
|
|
15
|
+
#
|
|
16
|
+
# The outer wrapper carries data-controller="kanban" for the Stimulus
|
|
17
|
+
# drag controller wired in Task 11.
|
|
18
|
+
#
|
|
19
|
+
# Realtime subscription: when board.realtime? is true,
|
|
20
|
+
# render_realtime_subscription is called before the board wrapper.
|
|
21
|
+
# Task 14 implements the broadcaster and fills in that method body.
|
|
22
|
+
class Resource < Plutonium::UI::Component::Base
|
|
23
|
+
include ColorDot
|
|
24
|
+
include Phlex::Rails::Helpers::TurboFrameTag
|
|
25
|
+
include Phlex::Rails::Helpers::TurboStreamFrom
|
|
26
|
+
|
|
27
|
+
attr_reader :board, :grouped_data, :resource_definition, :resource_fields,
|
|
28
|
+
:resource_class, :scoped_entity
|
|
29
|
+
|
|
30
|
+
def initialize(board:, grouped_data:, resource_definition:, resource_fields:,
|
|
31
|
+
resource_class: nil, scoped_entity: nil)
|
|
32
|
+
@board = board
|
|
33
|
+
@grouped_data = grouped_data
|
|
34
|
+
@resource_definition = resource_definition
|
|
35
|
+
# TODO (Tasks 6/10): resource_fields arrives ALREADY resolved — the
|
|
36
|
+
# column endpoint / index page is responsible for the
|
|
37
|
+
# `board.card_fields || definition.grid_fields` fallback before
|
|
38
|
+
# constructing this component. Kanban::Resource (and the Card it
|
|
39
|
+
# eventually builds) just receives the final field list and renders
|
|
40
|
+
# it; it does not resolve card_fields itself.
|
|
41
|
+
@resource_fields = resource_fields
|
|
42
|
+
# Used by render_realtime_subscription (Task 14) to scope the
|
|
43
|
+
# ActionCable stream to the correct tenant + resource.
|
|
44
|
+
@resource_class = resource_class
|
|
45
|
+
@scoped_entity = scoped_entity
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def view_template
|
|
49
|
+
render_realtime_subscription if board.realtime?
|
|
50
|
+
|
|
51
|
+
# Wrap in the filter-panel controller so the toolbar's Filter button
|
|
52
|
+
# can open the slideover — same structure as Grid/Table::Resource, so
|
|
53
|
+
# the board carries the shared view switcher (Table | Grid | Board),
|
|
54
|
+
# search, scopes, and filters rather than rendering bare columns.
|
|
55
|
+
div(data: filter_panel_controller_data) do
|
|
56
|
+
# Persistent append target for move-rejection toasts. It lives
|
|
57
|
+
# OUTSIDE the per-column turbo-frames so it survives the frame swaps
|
|
58
|
+
# a move triggers; toasts are position:fixed, so its location in the
|
|
59
|
+
# DOM has no layout effect. The move handler appends a flash toast
|
|
60
|
+
# here via turbo_stream when a drop is rejected (WIP / accepts /
|
|
61
|
+
# locked) so the snap-back is explained rather than silent.
|
|
62
|
+
div(id: "kanban-flash")
|
|
63
|
+
render_scopes_pills
|
|
64
|
+
render_toolbar
|
|
65
|
+
|
|
66
|
+
div(
|
|
67
|
+
class: "pu-kanban-board flex gap-4 overflow-x-auto p-4 min-h-0",
|
|
68
|
+
data: {
|
|
69
|
+
controller: "kanban",
|
|
70
|
+
# Stimulus value consumed by the drag controller to build the
|
|
71
|
+
# per-record move URL at drop time. The collection path comes from
|
|
72
|
+
# request.path so tenant / engine scoping is preserved automatically.
|
|
73
|
+
# Example: /admin/tasks/__ID__/kanban_move
|
|
74
|
+
kanban_move_url_template_value: kanban_move_url_template
|
|
75
|
+
}
|
|
76
|
+
) do
|
|
77
|
+
grouped_data.each do |entry|
|
|
78
|
+
render_column_frame(entry[:column])
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
render_filter_slideover if current_query_object.filter_definitions.present?
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def filter_panel_controller_data
|
|
89
|
+
{controller: "filter-panel"}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def render_scopes_pills
|
|
93
|
+
TableScopesPills() if current_query_object.scope_definitions.any?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The shared index toolbar — view switcher (Table | Grid | Board),
|
|
97
|
+
# search, and the filter button. `current_view: :kanban` keeps the
|
|
98
|
+
# Board segment highlighted.
|
|
99
|
+
def render_toolbar
|
|
100
|
+
TableToolbar(
|
|
101
|
+
query: current_query_object,
|
|
102
|
+
search_url: request.path,
|
|
103
|
+
search_value: params.dig(:q, :search) || params[:search],
|
|
104
|
+
views: resource_definition.defined_index_views,
|
|
105
|
+
current_view: :kanban,
|
|
106
|
+
view_cookie_name: Plutonium::UI::Page::Index.view_cookie_name(resource_class),
|
|
107
|
+
view_cookie_path: Plutonium::UI::Page::Index.view_cookie_path(request)
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def render_filter_slideover
|
|
112
|
+
div(
|
|
113
|
+
class: "fixed inset-0 z-40 bg-black/40 opacity-0 pointer-events-none " \
|
|
114
|
+
"transition-opacity duration-200 " \
|
|
115
|
+
"data-[open]:opacity-100 data-[open]:pointer-events-auto",
|
|
116
|
+
data: {filter_panel_target: "backdrop", action: "click->filter-panel#close"}
|
|
117
|
+
)
|
|
118
|
+
aside(
|
|
119
|
+
class: "fixed top-0 right-0 bottom-0 z-50 w-full sm:w-[420px] max-w-full " \
|
|
120
|
+
"bg-[var(--pu-surface)] border-l border-[var(--pu-border)] " \
|
|
121
|
+
"translate-x-full transition-transform duration-300 ease-out " \
|
|
122
|
+
"data-[open]:translate-x-0 " \
|
|
123
|
+
"flex flex-col",
|
|
124
|
+
role: "dialog",
|
|
125
|
+
aria: {label: "Filters", hidden: "true", modal: "true"},
|
|
126
|
+
data: {filter_panel_target: "panel"}
|
|
127
|
+
) do
|
|
128
|
+
render Plutonium::UI::Table::Components::FilterForm.new(
|
|
129
|
+
filter_form_values,
|
|
130
|
+
query_object: current_query_object,
|
|
131
|
+
search_url: request.path,
|
|
132
|
+
search_value: params.dig(:q, :search) || params[:search]
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def filter_form_values
|
|
138
|
+
raw = params[:q]
|
|
139
|
+
return {} unless raw
|
|
140
|
+
hash = raw.respond_to?(:to_unsafe_h) ? raw.to_unsafe_h : raw.to_h
|
|
141
|
+
hash.deep_symbolize_keys.except(:search, :scope, :sort_fields, :sort_directions)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Emits a <turbo-cable-stream-source> element that subscribes this page
|
|
145
|
+
# to the kanban board's tenant-scoped ActionCable stream.
|
|
146
|
+
#
|
|
147
|
+
# Only called when board.realtime? is true (gated by the caller in
|
|
148
|
+
# view_template). The stream name matches the one used by
|
|
149
|
+
# Plutonium::Kanban::Broadcaster#broadcast so move events reach exactly
|
|
150
|
+
# the right subscribers.
|
|
151
|
+
def render_realtime_subscription
|
|
152
|
+
turbo_stream_from(
|
|
153
|
+
*Plutonium::Kanban::Broadcaster.stream_name(
|
|
154
|
+
resource_class: resource_class,
|
|
155
|
+
scoped_entity: scoped_entity
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def render_column_frame(column)
|
|
161
|
+
attrs = {src: column_frame_src(column)}
|
|
162
|
+
attrs[:loading] = "lazy" if board.lazy?
|
|
163
|
+
|
|
164
|
+
turbo_frame_tag(column_frame_id(column), **attrs) do
|
|
165
|
+
# Header is inside the frame so the shell is meaningful while the
|
|
166
|
+
# body (card list) loads. Task 6 replaces the frame contents with
|
|
167
|
+
# the full column body rendered by Kanban::Column.
|
|
168
|
+
render_column_header(column)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Mirrors Kanban::Column#render_header structurally (no count badge) so
|
|
173
|
+
# the shell→loaded transition doesn't flash a stale count or restructure.
|
|
174
|
+
def render_column_header(column)
|
|
175
|
+
div(class: "px-3 py-2 flex items-center justify-between border-b border-[var(--pu-border)] bg-[var(--pu-surface)]") do
|
|
176
|
+
div(class: "flex items-center gap-2 min-w-0") do
|
|
177
|
+
render_color_dot(column.color) if column.color
|
|
178
|
+
span(class: "font-semibold text-sm text-[var(--pu-text)] truncate") { plain column.label }
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Builds the move URL template for the Stimulus drag controller.
|
|
184
|
+
# The collection path from the current request is used so engine
|
|
185
|
+
# mounting and path-scoped tenancy are automatically preserved.
|
|
186
|
+
# The literal string "__ID__" is a placeholder; the JS controller
|
|
187
|
+
# replaces it with the dragged card's record id at drop time.
|
|
188
|
+
def kanban_move_url_template
|
|
189
|
+
base = request.path.delete_suffix("/")
|
|
190
|
+
"#{base}/__ID__/kanban_move"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Returns the turbo-frame element id for a column.
|
|
194
|
+
# Plain id — not turbo_scoped_dom_id which is for modal-frame scoping.
|
|
195
|
+
def column_frame_id(column)
|
|
196
|
+
"kanban-col-#{column.key}"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Builds the frame src URL from the current request path + merged params.
|
|
200
|
+
# Merges view=kanban and column=<key> so the column endpoint (Task 6)
|
|
201
|
+
# knows which column to render without requiring an explicit route param.
|
|
202
|
+
def column_frame_src(column)
|
|
203
|
+
merged = request.query_parameters.merge(
|
|
204
|
+
"view" => "kanban",
|
|
205
|
+
"column" => column.key.to_s
|
|
206
|
+
)
|
|
207
|
+
"#{request.path}?#{merged.to_query}"
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -61,11 +61,17 @@ module Plutonium
|
|
|
61
61
|
)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
# The classic shell always renders its sidebar; the modern family
|
|
65
|
+
# renders the icon rail only when the rail is active. Mirrors the
|
|
66
|
+
# classic special-casing in main_attributes/html_attributes, whose
|
|
67
|
+
# offsets are likewise rail-independent for :classic.
|
|
68
|
+
def render_sidebar? = shell == :classic || rail?
|
|
69
|
+
|
|
64
70
|
def render_before_main
|
|
65
71
|
super
|
|
66
72
|
|
|
67
73
|
render partial("resource_header")
|
|
68
|
-
render partial("resource_sidebar") if
|
|
74
|
+
render partial("resource_sidebar") if render_sidebar?
|
|
69
75
|
end
|
|
70
76
|
end
|
|
71
77
|
end
|
|
@@ -24,10 +24,14 @@ module Plutonium
|
|
|
24
24
|
(mode == :centered) ? Plutonium::UI::Modal::Centered : Plutonium::UI::Modal::Slideover
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
def initialize(title: nil, description: nil, size: :md)
|
|
27
|
+
def initialize(title: nil, description: nil, size: :md, open_full_url: nil)
|
|
28
28
|
@title = title
|
|
29
29
|
@description = description
|
|
30
30
|
@size = size
|
|
31
|
+
# When set, the header shows an "open full page" affordance that opens
|
|
32
|
+
# this URL in a new tab as a standalone page (no modal frame). The show
|
|
33
|
+
# modal uses it so a user can pop the record out to its full page.
|
|
34
|
+
@open_full_url = open_full_url
|
|
31
35
|
validate_size!
|
|
32
36
|
end
|
|
33
37
|
|
|
@@ -106,7 +110,30 @@ module Plutonium
|
|
|
106
110
|
p(id: description_id, class: "mt-1 text-sm text-[var(--pu-text-muted)]") { @description }
|
|
107
111
|
end
|
|
108
112
|
end
|
|
109
|
-
|
|
113
|
+
# The corner-hugging offset lives on the GROUP (not the individual
|
|
114
|
+
# buttons) so the buttons' own margins don't collapse the gap when
|
|
115
|
+
# there are two of them (open-full + close).
|
|
116
|
+
div(class: "flex items-center gap-1 shrink-0 -mt-1.5 -mr-1.5") do
|
|
117
|
+
render_open_full_link if @open_full_url
|
|
118
|
+
render_close_button
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Opens the modal's content as a standalone full page in a new tab.
|
|
124
|
+
# target=_blank gives a fresh browsing context with no Turbo-Frame
|
|
125
|
+
# header, so the destination renders full-page rather than re-entering
|
|
126
|
+
# the modal frame.
|
|
127
|
+
def render_open_full_link
|
|
128
|
+
a(
|
|
129
|
+
href: @open_full_url,
|
|
130
|
+
target: "_blank",
|
|
131
|
+
rel: "noopener",
|
|
132
|
+
class: "p-1.5 text-[var(--pu-text-muted)] hover:text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)] rounded-md transition-colors",
|
|
133
|
+
"aria-label": "Open full page in a new tab",
|
|
134
|
+
title: "Open full page"
|
|
135
|
+
) do
|
|
136
|
+
render Phlex::TablerIcons::ArrowsDiagonal.new(class: "w-5 h-5")
|
|
110
137
|
end
|
|
111
138
|
end
|
|
112
139
|
|
|
@@ -116,7 +143,7 @@ module Plutonium
|
|
|
116
143
|
else
|
|
117
144
|
button(
|
|
118
145
|
type: "button",
|
|
119
|
-
class: "p-1.5
|
|
146
|
+
class: "p-1.5 text-[var(--pu-text-muted)] hover:text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)] rounded-md transition-colors",
|
|
120
147
|
data: {action: "remote-modal#close"},
|
|
121
148
|
"aria-label": "Close dialog"
|
|
122
149
|
) do
|
|
@@ -22,16 +22,19 @@ module Plutonium
|
|
|
22
22
|
# Surface (bg, border, radius, backdrop) lives in `.pu-dialog` so
|
|
23
23
|
# the centered modal, dirty-form-guard prompt, and Turbo confirm
|
|
24
24
|
# can't drift on design tokens. The remaining utilities are
|
|
25
|
-
# positioning, sizing, and the open/close
|
|
25
|
+
# positioning, sizing, and the open/close opacity+scale animation —
|
|
26
26
|
# driven by [data-open] (set on the frame after showModal() by
|
|
27
27
|
# remote_modal_controller); avoids the @starting-style spec dance.
|
|
28
|
+
# The transition must name `scale` (not `transform`): Tailwind v4's
|
|
29
|
+
# `scale-*` sets the discrete `scale` CSS property, so a
|
|
30
|
+
# `transition-[...,transform]` would leave the scale pop un-animated.
|
|
28
31
|
def base_dialog_classes
|
|
29
32
|
"pu-dialog " \
|
|
30
33
|
"top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2 " \
|
|
31
34
|
"max-h-[80vh] " \
|
|
32
35
|
"open:flex flex-col p-0 " \
|
|
33
36
|
"opacity-0 scale-95 data-[open]:opacity-100 data-[open]:scale-100 " \
|
|
34
|
-
"transition-[opacity,
|
|
37
|
+
"transition-[opacity,scale] duration-200 ease-out"
|
|
35
38
|
end
|
|
36
39
|
end
|
|
37
40
|
end
|
|
@@ -19,6 +19,12 @@ module Plutonium
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def render_default_content
|
|
22
|
+
# When the show request arrives via a modal frame (e.g. a kanban card
|
|
23
|
+
# with `show_in :modal`), wrap the details in the modal chrome — the
|
|
24
|
+
# same path New/Edit use for their forms. The aside is dropped in the
|
|
25
|
+
# modal: a slideover/centered dialog has no room for a side rail.
|
|
26
|
+
return render_modal_details if in_modal?
|
|
27
|
+
|
|
22
28
|
if aside_present?
|
|
23
29
|
div(class: "grid grid-cols-1 lg:grid-cols-[minmax(0,1fr)_240px] gap-6") do
|
|
24
30
|
div { render partial("resource_details") }
|
|
@@ -29,6 +35,23 @@ module Plutonium
|
|
|
29
35
|
end
|
|
30
36
|
end
|
|
31
37
|
|
|
38
|
+
# The show page is ALWAYS a centered dialog when shown in a modal —
|
|
39
|
+
# deliberately not the definition's modal_mode (which styles :new/:edit
|
|
40
|
+
# as a slideover by default). A centered dialog reads as a focused
|
|
41
|
+
# "detail card" and leaves a launching board/table visible around it.
|
|
42
|
+
# `open_full_url` is the current show URL (request.path is the show
|
|
43
|
+
# route here), letting the user pop the record out to its full page.
|
|
44
|
+
def render_modal_details
|
|
45
|
+
render Plutonium::UI::Modal::Centered.new(
|
|
46
|
+
title: page_title,
|
|
47
|
+
description: page_description,
|
|
48
|
+
size: :lg,
|
|
49
|
+
open_full_url: request.path
|
|
50
|
+
) do
|
|
51
|
+
render partial("resource_details")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
32
55
|
def page_type = :show_page
|
|
33
56
|
end
|
|
34
57
|
end
|