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,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Modal
|
|
6
|
+
class Base < Plutonium::UI::Component::Base
|
|
7
|
+
include Phlex::Slotable
|
|
8
|
+
|
|
9
|
+
slot :close
|
|
10
|
+
slot :footer
|
|
11
|
+
|
|
12
|
+
def initialize(title: nil, description: nil)
|
|
13
|
+
@title = title
|
|
14
|
+
@description = description
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def view_template(&block)
|
|
18
|
+
dialog(**dialog_attributes) do
|
|
19
|
+
div(class: inner_classes) do
|
|
20
|
+
render_header
|
|
21
|
+
render_body(&block)
|
|
22
|
+
render_footer if footer_slot?
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
protected
|
|
28
|
+
|
|
29
|
+
# Native <dialog>+showModal() handles the focus trap, Esc-to-close,
|
|
30
|
+
# and focus restoration on close. We just need to label the dialog
|
|
31
|
+
# so screen readers announce it on open.
|
|
32
|
+
def dialog_attributes
|
|
33
|
+
attrs = {
|
|
34
|
+
closedby: "any",
|
|
35
|
+
class: dialog_classes,
|
|
36
|
+
data: {controller: "remote-modal"},
|
|
37
|
+
"aria-modal": "true"
|
|
38
|
+
}
|
|
39
|
+
if @title
|
|
40
|
+
attrs[:"aria-labelledby"] = title_id
|
|
41
|
+
else
|
|
42
|
+
attrs[:"aria-label"] = "Dialog"
|
|
43
|
+
end
|
|
44
|
+
attrs[:"aria-describedby"] = description_id if @description.present?
|
|
45
|
+
attrs
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def title_id
|
|
49
|
+
@title_id ||= "pu-modal-title-#{SecureRandom.hex(4)}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def description_id
|
|
53
|
+
@description_id ||= "pu-modal-desc-#{SecureRandom.hex(4)}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def dialog_classes
|
|
57
|
+
raise NotImplementedError
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def inner_classes
|
|
61
|
+
"flex flex-col h-full max-h-[inherit] min-h-0"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def render_header
|
|
65
|
+
div(class: "flex items-start justify-between gap-4 px-6 pt-5 pb-4 border-b border-[var(--pu-border)]") do
|
|
66
|
+
div(class: "min-w-0 flex-1") do
|
|
67
|
+
if @title
|
|
68
|
+
h2(id: title_id, class: "text-lg font-semibold text-[var(--pu-text)] truncate") { @title }
|
|
69
|
+
end
|
|
70
|
+
if @description.present?
|
|
71
|
+
p(id: description_id, class: "mt-1 text-sm text-[var(--pu-text-muted)]") { @description }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
render_close_button
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def render_close_button
|
|
79
|
+
if close_slot?
|
|
80
|
+
render close_slot
|
|
81
|
+
else
|
|
82
|
+
button(
|
|
83
|
+
type: "button",
|
|
84
|
+
class: "p-1.5 -m-1.5 text-[var(--pu-text-muted)] hover:text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)] rounded-md transition-colors",
|
|
85
|
+
data: {action: "remote-modal#close"},
|
|
86
|
+
"aria-label": "Close dialog"
|
|
87
|
+
) do
|
|
88
|
+
render Phlex::TablerIcons::X.new(class: "w-5 h-5")
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def render_body(&block)
|
|
94
|
+
# Body is a flex column with no padding/scroll; content owns its
|
|
95
|
+
# own padding and scroll regions. This lets form-shaped content
|
|
96
|
+
# split itself into a scrollable fields region and a pinned
|
|
97
|
+
# action strip flush with the modal's bottom edge.
|
|
98
|
+
div(class: "flex-1 min-h-0 flex flex-col overflow-hidden", &block)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def render_footer
|
|
102
|
+
div(class: "flex items-center justify-end gap-2 px-6 py-4 border-t border-[var(--pu-border)]") do
|
|
103
|
+
render footer_slot
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Modal
|
|
6
|
+
class Centered < Plutonium::UI::Modal::Base
|
|
7
|
+
protected
|
|
8
|
+
|
|
9
|
+
def dialog_classes
|
|
10
|
+
"rounded-[var(--pu-radius-lg)] w-full max-w-xl " \
|
|
11
|
+
"bg-[var(--pu-surface)] border border-[var(--pu-border)] " \
|
|
12
|
+
"backdrop:bg-black/60 backdrop:backdrop-blur-sm " \
|
|
13
|
+
"top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2 " \
|
|
14
|
+
"max-h-[80vh] " \
|
|
15
|
+
"hidden open:flex flex-col p-0 " \
|
|
16
|
+
"opacity-0 open:opacity-100 transition-opacity duration-200 ease-in-out"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Modal
|
|
6
|
+
class Slideover < Plutonium::UI::Modal::Base
|
|
7
|
+
protected
|
|
8
|
+
|
|
9
|
+
def dialog_classes
|
|
10
|
+
"fixed top-0 right-0 bottom-0 left-auto m-0 h-screen w-full sm:w-[480px] max-w-full max-h-screen " \
|
|
11
|
+
"bg-[var(--pu-surface)] border-l border-[var(--pu-border)] " \
|
|
12
|
+
"backdrop:bg-black/60 backdrop:backdrop-blur-sm " \
|
|
13
|
+
"rounded-none p-0 " \
|
|
14
|
+
"hidden open:flex flex-col " \
|
|
15
|
+
"translate-x-full open:translate-x-0 " \
|
|
16
|
+
"transition-[transform,display,overlay] duration-300 ease-out " \
|
|
17
|
+
"[transition-behavior:allow-discrete] " \
|
|
18
|
+
"starting:open:translate-x-full " \
|
|
19
|
+
"backdrop:transition-[display,overlay,background-color] backdrop:duration-300 " \
|
|
20
|
+
"backdrop:[transition-behavior:allow-discrete] " \
|
|
21
|
+
"starting:open:backdrop:bg-transparent"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -11,13 +11,15 @@ module Plutonium
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def view_template(&block)
|
|
14
|
-
|
|
14
|
+
body = block || proc { render_default_content }
|
|
15
|
+
|
|
16
|
+
DynaFrameContent() do
|
|
15
17
|
render_before_header
|
|
16
18
|
render_header
|
|
17
19
|
render_after_header
|
|
18
20
|
|
|
19
21
|
render_before_content
|
|
20
|
-
|
|
22
|
+
body.call
|
|
21
23
|
render_after_content
|
|
22
24
|
|
|
23
25
|
render_before_footer
|
|
@@ -51,6 +53,11 @@ module Plutonium
|
|
|
51
53
|
end
|
|
52
54
|
|
|
53
55
|
def render_breadcrumbs?
|
|
56
|
+
# Hide breadcrumbs when rendered inside a turbo frame — the host
|
|
57
|
+
# page already provides the navigation context (e.g., association
|
|
58
|
+
# tabs on a parent show page).
|
|
59
|
+
return false if in_frame?
|
|
60
|
+
|
|
54
61
|
# Check specific page setting first, fall back to global setting
|
|
55
62
|
page_specific_setting = current_definition.send(:"#{page_type}_breadcrumbs")
|
|
56
63
|
page_specific_setting.nil? ? current_definition.breadcrumbs : page_specific_setting
|
|
@@ -66,10 +73,6 @@ module Plutonium
|
|
|
66
73
|
# Implement toolbar content
|
|
67
74
|
end
|
|
68
75
|
|
|
69
|
-
def page_content(block)
|
|
70
|
-
block || proc { render_default_content }
|
|
71
|
-
end
|
|
72
|
-
|
|
73
76
|
def render_default_content
|
|
74
77
|
raise NotImplementedError, "#{self.class}#render_default_content"
|
|
75
78
|
end
|
|
@@ -78,6 +81,22 @@ module Plutonium
|
|
|
78
81
|
# Implement footer content
|
|
79
82
|
end
|
|
80
83
|
|
|
84
|
+
# Renders the optional aside (right-side panel) on show pages.
|
|
85
|
+
# No-op by default; future metadata DSL will populate this slot.
|
|
86
|
+
def render_aside
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# True when the show layout should reserve space for the aside.
|
|
90
|
+
# Returns false by default; pages opt-in by overriding.
|
|
91
|
+
def aside_present? = false
|
|
92
|
+
|
|
93
|
+
# True when the page is rendered inside any turbo frame.
|
|
94
|
+
def in_frame? = current_turbo_frame.present?
|
|
95
|
+
|
|
96
|
+
# True when the page is rendered inside the remote_modal turbo frame.
|
|
97
|
+
# Used by form pages to suppress the sticky footer (modal owns its own footer).
|
|
98
|
+
def in_modal? = current_turbo_frame == Plutonium::REMOTE_MODAL_FRAME
|
|
99
|
+
|
|
81
100
|
# Customization hooks
|
|
82
101
|
def render_before_header
|
|
83
102
|
end
|
|
@@ -15,7 +15,19 @@ module Plutonium
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def render_default_content
|
|
18
|
-
|
|
18
|
+
if in_modal?
|
|
19
|
+
render_modal_form
|
|
20
|
+
else
|
|
21
|
+
div(class: "pb-20") { render partial("resource_form") }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def render_modal_form
|
|
26
|
+
modal_class = (current_definition.modal == :centered) ?
|
|
27
|
+
Plutonium::UI::Modal::Centered : Plutonium::UI::Modal::Slideover
|
|
28
|
+
render modal_class.new(title: page_title, description: page_description) do
|
|
29
|
+
render partial("resource_form")
|
|
30
|
+
end
|
|
19
31
|
end
|
|
20
32
|
|
|
21
33
|
def page_type = :edit_page
|
|
@@ -4,6 +4,24 @@ module Plutonium
|
|
|
4
4
|
module UI
|
|
5
5
|
module Page
|
|
6
6
|
class Index < Base
|
|
7
|
+
# Cookie name carrying a per-resource view preference. Single
|
|
8
|
+
# source of truth — Table::Resource, Grid::Resource, and the
|
|
9
|
+
# Stimulus view-switcher controller all read from here. Underscored
|
|
10
|
+
# token-only characters keep this RFC 6265-compliant (the `:` form
|
|
11
|
+
# this replaces is technically forbidden, even if browsers
|
|
12
|
+
# accept it in practice).
|
|
13
|
+
def self.view_cookie_name(resource_class)
|
|
14
|
+
"pu_view_#{resource_class.name.gsub("::", "_").underscore}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Cookie Path scoped to the engine mount point (request.script_name).
|
|
18
|
+
# Two portals mounting the same resource class get independent
|
|
19
|
+
# view preferences instead of leaking through a site-wide cookie.
|
|
20
|
+
def self.view_cookie_path(request)
|
|
21
|
+
path = request.script_name.to_s
|
|
22
|
+
path.empty? ? "/" : path
|
|
23
|
+
end
|
|
24
|
+
|
|
7
25
|
private
|
|
8
26
|
|
|
9
27
|
def page_title
|
|
@@ -19,7 +37,28 @@ module Plutonium
|
|
|
19
37
|
end
|
|
20
38
|
|
|
21
39
|
def render_default_content
|
|
22
|
-
|
|
40
|
+
case selected_view
|
|
41
|
+
when :grid then render partial("resource_grid")
|
|
42
|
+
else render partial("resource_table")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Resolution order:
|
|
47
|
+
# 1. `?view=` URL param (so a shared link can pin a view)
|
|
48
|
+
# 2. The view-preference cookie (sticky per-resource selection)
|
|
49
|
+
# 3. The resource's `default_view` (which itself defaults to
|
|
50
|
+
# `views.first`)
|
|
51
|
+
def selected_view
|
|
52
|
+
definition = current_definition
|
|
53
|
+
enabled = definition.defined_views
|
|
54
|
+
|
|
55
|
+
requested = params[:view]&.to_sym
|
|
56
|
+
return requested if requested && enabled.include?(requested)
|
|
57
|
+
|
|
58
|
+
stored = helpers.cookies[self.class.view_cookie_name(resource_class)]&.to_sym
|
|
59
|
+
return stored if stored && enabled.include?(stored)
|
|
60
|
+
|
|
61
|
+
definition.default_view
|
|
23
62
|
end
|
|
24
63
|
|
|
25
64
|
def page_type = :index_page
|
|
@@ -17,49 +17,18 @@ module Plutonium
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def render_default_content
|
|
20
|
-
if
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class: "rounded-[var(--pu-radius-lg)] w-full max-w-3xl
|
|
24
|
-
bg-[var(--pu-surface)]
|
|
25
|
-
border border-[var(--pu-border)]
|
|
26
|
-
backdrop:bg-black/60 backdrop:backdrop-blur-sm
|
|
27
|
-
top-auto md:top-1/2 md:-translate-y-1/2 left-1/2 -translate-x-1/2
|
|
28
|
-
max-h-[80%] p-6
|
|
29
|
-
hidden open:flex flex-col
|
|
30
|
-
relative opacity-0 open:opacity-100
|
|
31
|
-
transition-opacity duration-300 ease-in-out",
|
|
32
|
-
style: "box-shadow: var(--pu-shadow-lg)",
|
|
33
|
-
data: {controller: "remote-modal"}
|
|
34
|
-
) do
|
|
35
|
-
# Close button
|
|
36
|
-
button(
|
|
37
|
-
type: "button",
|
|
38
|
-
class: "absolute top-4 right-4 p-2 text-[var(--pu-text-muted)] hover:text-[var(--pu-text)] transition-colors duration-200",
|
|
39
|
-
data: {action: "remote-modal#close"},
|
|
40
|
-
"aria-label": "Close dialog"
|
|
41
|
-
) do
|
|
42
|
-
svg(
|
|
43
|
-
class: "w-5 h-5",
|
|
44
|
-
fill: "none",
|
|
45
|
-
stroke: "currentColor",
|
|
46
|
-
viewBox: "0 0 24 24",
|
|
47
|
-
xmlns: "http://www.w3.org/2000/svg"
|
|
48
|
-
) do |s|
|
|
49
|
-
s.path(
|
|
50
|
-
stroke_linecap: "round",
|
|
51
|
-
stroke_linejoin: "round",
|
|
52
|
-
stroke_width: "2",
|
|
53
|
-
d: "M6 18L18 6M6 6l12 12"
|
|
54
|
-
)
|
|
55
|
-
end
|
|
56
|
-
end
|
|
20
|
+
if in_modal?
|
|
21
|
+
modal_class = (current_interactive_action.modal == :slideover) ?
|
|
22
|
+
Plutonium::UI::Modal::Slideover : Plutonium::UI::Modal::Centered
|
|
57
23
|
|
|
58
|
-
|
|
24
|
+
render modal_class.new(
|
|
25
|
+
title: page_title,
|
|
26
|
+
description: page_description
|
|
27
|
+
) do
|
|
59
28
|
render partial("interactive_action_form")
|
|
60
29
|
end
|
|
61
30
|
else
|
|
62
|
-
render partial("interactive_action_form")
|
|
31
|
+
div(class: "pb-20") { render partial("interactive_action_form") }
|
|
63
32
|
end
|
|
64
33
|
end
|
|
65
34
|
|
|
@@ -15,7 +15,19 @@ module Plutonium
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def render_default_content
|
|
18
|
-
|
|
18
|
+
if in_modal?
|
|
19
|
+
render_modal_form
|
|
20
|
+
else
|
|
21
|
+
div(class: "pb-20") { render partial("resource_form") }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def render_modal_form
|
|
26
|
+
modal_class = (current_definition.modal == :centered) ?
|
|
27
|
+
Plutonium::UI::Modal::Centered : Plutonium::UI::Modal::Slideover
|
|
28
|
+
render modal_class.new(title: page_title, description: page_description) do
|
|
29
|
+
render partial("resource_form")
|
|
30
|
+
end
|
|
19
31
|
end
|
|
20
32
|
|
|
21
33
|
def page_type = :new_page
|
|
@@ -19,7 +19,14 @@ module Plutonium
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def render_default_content
|
|
22
|
-
|
|
22
|
+
if aside_present?
|
|
23
|
+
div(class: "grid grid-cols-1 lg:grid-cols-[minmax(0,1fr)_240px] gap-6") do
|
|
24
|
+
div { render partial("resource_details") }
|
|
25
|
+
aside(class: "hidden lg:block") { render_aside }
|
|
26
|
+
end
|
|
27
|
+
else
|
|
28
|
+
render partial("resource_details")
|
|
29
|
+
end
|
|
23
30
|
end
|
|
24
31
|
|
|
25
32
|
def page_type = :show_page
|
|
@@ -8,30 +8,25 @@ module Plutonium
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def view_template
|
|
11
|
-
div(class: "
|
|
12
|
-
div
|
|
13
|
-
phlexi_render(@title) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
phlexi_render(@description) {
|
|
18
|
-
render_description @description
|
|
19
|
-
}
|
|
20
|
-
}
|
|
11
|
+
div(class: "flex items-start justify-between gap-4 mb-4") do
|
|
12
|
+
div(class: "min-w-0 flex-1") do
|
|
13
|
+
phlexi_render(@title) { render_title @title } if @title
|
|
14
|
+
phlexi_render(@description) { render_description @description } if @description.present?
|
|
15
|
+
end
|
|
21
16
|
render_actions if @actions.any?
|
|
22
|
-
|
|
17
|
+
end
|
|
23
18
|
end
|
|
24
19
|
|
|
25
20
|
private
|
|
26
21
|
|
|
27
22
|
def render_title(title)
|
|
28
|
-
|
|
23
|
+
h1(class: "text-xl font-semibold leading-tight text-[var(--pu-text)] truncate") {
|
|
29
24
|
title
|
|
30
25
|
}
|
|
31
26
|
end
|
|
32
27
|
|
|
33
28
|
def render_description(description)
|
|
34
|
-
p(class: "text-
|
|
29
|
+
p(class: "mt-1 text-sm text-[var(--pu-text-muted)]") {
|
|
35
30
|
description
|
|
36
31
|
}
|
|
37
32
|
end
|
data/lib/plutonium/ui/panel.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
module Plutonium
|
|
2
2
|
module UI
|
|
3
|
+
# A lightweight panel: optional title + action items rendered as a small
|
|
4
|
+
# floating cluster in the top-right of the panel; content fills the panel
|
|
5
|
+
# body. No outer card chrome — the panel sits flush in its host.
|
|
3
6
|
class Panel < Plutonium::UI::Component::Base
|
|
4
7
|
def initialize
|
|
5
8
|
@items = []
|
|
@@ -23,30 +26,18 @@ module Plutonium
|
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
def view_template
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
render_content if render_content?
|
|
29
|
-
end
|
|
29
|
+
render_toolbar if render_toolbar?
|
|
30
|
+
render_content if render_content?
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
private
|
|
33
34
|
|
|
34
|
-
def wrapped(&)
|
|
35
|
-
div(class: "mt-8", &)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
35
|
def render_toolbar
|
|
39
|
-
div(class: "flex justify-
|
|
40
|
-
if @title
|
|
41
|
-
|
|
42
|
-
@title
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
div(class: "flex gap-3") do
|
|
46
|
-
@items.each do |item|
|
|
47
|
-
render item
|
|
48
|
-
end
|
|
36
|
+
div(class: "flex items-center justify-end gap-0.5 mb-2") do
|
|
37
|
+
if @title.present?
|
|
38
|
+
span(class: "mr-auto text-[10px] font-semibold uppercase tracking-wider text-[var(--pu-text-muted)]") { @title }
|
|
49
39
|
end
|
|
40
|
+
@items.each { |item| render item }
|
|
50
41
|
end
|
|
51
42
|
end
|
|
52
43
|
|
|
@@ -55,7 +46,7 @@ module Plutonium
|
|
|
55
46
|
end
|
|
56
47
|
|
|
57
48
|
def render_toolbar?
|
|
58
|
-
@title || @items
|
|
49
|
+
@title.present? || @items.any?
|
|
59
50
|
end
|
|
60
51
|
|
|
61
52
|
def render_content?
|
|
@@ -2,8 +2,8 @@ require "phlexi-menu"
|
|
|
2
2
|
|
|
3
3
|
module Plutonium
|
|
4
4
|
module UI
|
|
5
|
-
# A sidebar navigation component that renders a max depth of 2 levels
|
|
6
|
-
# Provides collapsible menu sections and is compatible with turbo-permanent
|
|
5
|
+
# A sidebar navigation component that renders a max depth of 2 levels.
|
|
6
|
+
# Provides collapsible menu sections and is compatible with turbo-permanent.
|
|
7
7
|
class SidebarMenu < Phlexi::Menu::Component
|
|
8
8
|
include Plutonium::UI::Component::Behaviour
|
|
9
9
|
|
|
@@ -64,29 +64,6 @@ module Plutonium
|
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
# def render_items(items, depth = 0)
|
|
68
|
-
# return if depth >= @max_depth
|
|
69
|
-
|
|
70
|
-
# if depth.zero?
|
|
71
|
-
# ul(class: themed(:items_container, depth)) do
|
|
72
|
-
# items.each do |item|
|
|
73
|
-
# render_item_wrapper(item, depth)
|
|
74
|
-
# end
|
|
75
|
-
# end
|
|
76
|
-
# else
|
|
77
|
-
# # Use collapsible rendering for nested levels
|
|
78
|
-
# ul(
|
|
79
|
-
# id: generate_menu_id(:root),
|
|
80
|
-
# class: themed(:sub_items_container, depth),
|
|
81
|
-
# data: {"resource-collapse-target": "menu"}
|
|
82
|
-
# ) do
|
|
83
|
-
# items.each do |item|
|
|
84
|
-
# render_item_wrapper(item, depth)
|
|
85
|
-
# end
|
|
86
|
-
# end
|
|
87
|
-
# end
|
|
88
|
-
# end
|
|
89
|
-
|
|
90
67
|
def render_item_wrapper(item, depth)
|
|
91
68
|
wrapper_attrs = {
|
|
92
69
|
class: tokens(themed(:item_wrapper, depth)),
|
|
@@ -4,6 +4,10 @@ module Plutonium
|
|
|
4
4
|
class TabDefinition
|
|
5
5
|
end
|
|
6
6
|
|
|
7
|
+
BASE_BUTTON_CLASSES = "inline-block px-5 py-3 border-b-2 rounded-t-lg transition-colors"
|
|
8
|
+
ACTIVE_CLASSES = "focus:outline-none text-primary-600 border-primary-600 dark:text-primary-400 dark:border-primary-400"
|
|
9
|
+
INACTIVE_CLASSES = "text-[var(--pu-text-muted)] hover:text-[var(--pu-text)] border-transparent hover:border-[var(--pu-border-strong)]"
|
|
10
|
+
|
|
7
11
|
def initialize(...)
|
|
8
12
|
super
|
|
9
13
|
|
|
@@ -19,25 +23,30 @@ module Plutonium
|
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
def view_template
|
|
26
|
+
default_identifier = @tabs.first&.dig(:identifier)
|
|
27
|
+
|
|
22
28
|
div(
|
|
23
29
|
data_controller: "resource-tab-list",
|
|
24
|
-
data_resource_tab_list_active_classes_value:
|
|
25
|
-
data_resource_tab_list_in_active_classes_value:
|
|
30
|
+
data_resource_tab_list_active_classes_value: ACTIVE_CLASSES,
|
|
31
|
+
data_resource_tab_list_in_active_classes_value: INACTIVE_CLASSES
|
|
26
32
|
) do
|
|
27
|
-
div(class: "mb-6 border-b border-[var(--pu-border)]") do
|
|
33
|
+
div(class: "relative mb-6 border-b border-[var(--pu-border)]") do
|
|
28
34
|
ul(
|
|
29
|
-
class: "flex flex-
|
|
35
|
+
class: "flex flex-nowrap overflow-x-auto whitespace-nowrap -mb-px text-base font-semibold gap-1 " \
|
|
36
|
+
"[scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
|
|
30
37
|
role: "tablist"
|
|
31
38
|
) do
|
|
32
39
|
@tabs.each do |tab|
|
|
40
|
+
active = tab[:identifier] == default_identifier
|
|
33
41
|
li(role: "presentation") do
|
|
34
42
|
button(
|
|
35
|
-
class:
|
|
43
|
+
class: button_classes_for(active),
|
|
36
44
|
id: "#{tab[:identifier]}-tab",
|
|
37
45
|
type: "button",
|
|
38
46
|
role: "tab",
|
|
39
47
|
aria_controls: "#{tab[:identifier]}-tabpanel",
|
|
40
|
-
aria_selected:
|
|
48
|
+
aria_selected: active.to_s,
|
|
49
|
+
tabindex: active ? "0" : "-1",
|
|
41
50
|
data_resource_tab_list_target: "btn",
|
|
42
51
|
data_target: "#{tab[:identifier]}-tabpanel",
|
|
43
52
|
data_action: "click->resource-tab-list#select"
|
|
@@ -49,15 +58,22 @@ module Plutonium
|
|
|
49
58
|
end
|
|
50
59
|
end
|
|
51
60
|
end
|
|
61
|
+
div(
|
|
62
|
+
class: "pointer-events-none absolute right-0 top-0 bottom-0 w-8 " \
|
|
63
|
+
"bg-gradient-to-l from-[var(--pu-body)] to-transparent",
|
|
64
|
+
aria_hidden: "true"
|
|
65
|
+
)
|
|
52
66
|
end
|
|
53
67
|
|
|
54
68
|
div do
|
|
55
69
|
@tabs.each do |tab|
|
|
70
|
+
active = tab[:identifier] == default_identifier
|
|
56
71
|
div(
|
|
57
|
-
hidden:
|
|
72
|
+
hidden: !active,
|
|
58
73
|
id: "#{tab[:identifier]}-tabpanel",
|
|
59
74
|
role: "tabpanel",
|
|
60
75
|
aria_labelledby: "#{tab[:identifier]}-tab",
|
|
76
|
+
aria_hidden: (!active).to_s,
|
|
61
77
|
data_resource_tab_list_target: "tab"
|
|
62
78
|
) do
|
|
63
79
|
phlexi_render tab[:block] do |val|
|
|
@@ -68,6 +84,12 @@ module Plutonium
|
|
|
68
84
|
end
|
|
69
85
|
end
|
|
70
86
|
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def button_classes_for(active)
|
|
91
|
+
"#{BASE_BUTTON_CLASSES} #{active ? ACTIVE_CLASSES : INACTIVE_CLASSES}"
|
|
92
|
+
end
|
|
71
93
|
end
|
|
72
94
|
end
|
|
73
95
|
end
|