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,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Table
|
|
6
|
+
module Components
|
|
7
|
+
# Segmented control for switching between index views (Table /
|
|
8
|
+
# Grid). Renders nothing unless at least two views are enabled.
|
|
9
|
+
# Selection is persisted in a per-resource cookie that the
|
|
10
|
+
# server reads on the next request — no `?view=` URL pollution
|
|
11
|
+
# so filters / search / clear-x links don't have to thread it
|
|
12
|
+
# through. The Stimulus controller sets the cookie on click,
|
|
13
|
+
# then reloads.
|
|
14
|
+
class ViewSwitcher < Plutonium::UI::Component::Base
|
|
15
|
+
SEGMENT_LABELS = {
|
|
16
|
+
table: {label: "Table", icon: Phlex::TablerIcons::Table},
|
|
17
|
+
grid: {label: "Grid", icon: Phlex::TablerIcons::LayoutGrid}
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def initialize(views:, current:, cookie_name:, cookie_path: "/")
|
|
21
|
+
@views = views
|
|
22
|
+
@current = current
|
|
23
|
+
@cookie_name = cookie_name
|
|
24
|
+
@cookie_path = cookie_path
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def render?
|
|
28
|
+
@views.size > 1
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def view_template
|
|
32
|
+
div(
|
|
33
|
+
role: "tablist",
|
|
34
|
+
aria: {label: "View"},
|
|
35
|
+
class: "inline-flex h-8 rounded-md border border-[var(--pu-border)] bg-[var(--pu-surface)] overflow-hidden",
|
|
36
|
+
data: {
|
|
37
|
+
controller: "view-switcher",
|
|
38
|
+
view_switcher_cookie_name_value: @cookie_name,
|
|
39
|
+
view_switcher_cookie_path_value: @cookie_path
|
|
40
|
+
}
|
|
41
|
+
) do
|
|
42
|
+
@views.each_with_index do |key, i|
|
|
43
|
+
render_segment(key, last: i == @views.length - 1)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def render_segment(key, last:)
|
|
51
|
+
meta = SEGMENT_LABELS.fetch(key) { {label: key.to_s.titleize, icon: Phlex::TablerIcons::LayoutGrid} }
|
|
52
|
+
active = key == @current
|
|
53
|
+
|
|
54
|
+
classes = ["px-2.5 inline-flex items-center gap-1.5 text-sm transition-colors"]
|
|
55
|
+
classes << "border-r border-[var(--pu-border)]" unless last
|
|
56
|
+
classes << if active
|
|
57
|
+
"bg-primary-50 text-primary-700 dark:bg-primary-950/40 dark:text-primary-300"
|
|
58
|
+
else
|
|
59
|
+
"text-[var(--pu-text-muted)] hover:text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)]"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
button(
|
|
63
|
+
type: "button",
|
|
64
|
+
role: "tab",
|
|
65
|
+
class: classes.join(" "),
|
|
66
|
+
title: meta[:label],
|
|
67
|
+
aria: {selected: active.to_s},
|
|
68
|
+
data: {
|
|
69
|
+
action: "click->view-switcher#select",
|
|
70
|
+
view_switcher_view_param: key.to_s
|
|
71
|
+
}
|
|
72
|
+
) do
|
|
73
|
+
render meta[:icon].new(class: "w-4 h-4 shrink-0")
|
|
74
|
+
span { meta[:label] }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -13,22 +13,55 @@ module Plutonium
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def view_template
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
# filter-panel controller wraps everything so the toolbar's
|
|
17
|
+
# filter button AND the FilterPills "+ Filter" pill (a sibling
|
|
18
|
+
# below the toolbar) share the same controller scope and can
|
|
19
|
+
# toggle the slideover rendered alongside them.
|
|
20
|
+
div(data: filter_panel_controller_data) do
|
|
21
|
+
render_scopes_pills
|
|
22
|
+
render_toolbar
|
|
23
|
+
|
|
24
|
+
div(data: bulk_actions_controller_data) do
|
|
25
|
+
render_filter_pills
|
|
26
|
+
render_bulk_actions_toolbar
|
|
27
|
+
collection.empty? ? render_empty_card : render_table
|
|
28
|
+
end
|
|
18
29
|
|
|
19
|
-
|
|
30
|
+
render_filter_slideover if current_query_object.filter_definitions.present?
|
|
31
|
+
end
|
|
20
32
|
|
|
21
33
|
render_footer
|
|
22
34
|
end
|
|
23
35
|
|
|
24
36
|
private
|
|
25
37
|
|
|
26
|
-
def
|
|
27
|
-
|
|
38
|
+
def render_scopes_pills
|
|
39
|
+
TableScopesPills() if current_query_object.scope_definitions.any?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def render_toolbar
|
|
43
|
+
TableToolbar(
|
|
44
|
+
query: current_query_object,
|
|
45
|
+
search_url: current_search_url,
|
|
46
|
+
search_value: params.dig(:q, :search) || params[:search],
|
|
47
|
+
views: resource_definition.defined_views,
|
|
48
|
+
current_view: :table,
|
|
49
|
+
view_cookie_name: Plutonium::UI::Page::Index.view_cookie_name(resource_class),
|
|
50
|
+
view_cookie_path: Plutonium::UI::Page::Index.view_cookie_path(request)
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def render_filter_pills
|
|
55
|
+
TableFilterPills(query: current_query_object, total_count: pagy_instance&.count)
|
|
28
56
|
end
|
|
29
57
|
|
|
30
|
-
def
|
|
31
|
-
|
|
58
|
+
def current_search_url
|
|
59
|
+
request.path
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def render_bulk_actions_toolbar
|
|
63
|
+
return unless bulk_actions.any?
|
|
64
|
+
BulkActionsToolbar(bulk_actions:)
|
|
32
65
|
end
|
|
33
66
|
|
|
34
67
|
def render_empty_card
|
|
@@ -42,91 +75,84 @@ module Plutonium
|
|
|
42
75
|
end
|
|
43
76
|
|
|
44
77
|
def render_table
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# Selection column for bulk actions (hidden by default, Stimulus shows it)
|
|
52
|
-
# Use :_selection as column key to avoid conflicts with field columns
|
|
53
|
-
# value_key defaults to model's primary_key
|
|
78
|
+
render Plutonium::UI::Table::Base.new(collection) do |table|
|
|
79
|
+
# Selection column only renders when bulk actions exist —
|
|
80
|
+
# the server already knows, so no JS toggle is needed.
|
|
81
|
+
# Use :_selection as column key to avoid conflicts with field columns;
|
|
82
|
+
# value_key defaults to model's primary_key.
|
|
83
|
+
if bulk_actions.any?
|
|
54
84
|
table.selection_column :_selection,
|
|
55
85
|
bulk_actions:,
|
|
56
86
|
policy_resolver: ->(record) { policy_for(record:) }
|
|
87
|
+
end
|
|
57
88
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
#
|
|
89
|
-
|
|
90
|
-
->(wrapped_object, _key) { user_block.call(wrapped_object.unwrapped) }
|
|
91
|
-
else
|
|
92
|
-
->(wrapped_object, key) {
|
|
93
|
-
f = wrapped_object.field(key)
|
|
94
|
-
tag ||= f.inferred_field_component
|
|
95
|
-
f.send(:"#{tag}_tag", **tag_attributes)
|
|
96
|
-
}
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# For table columns, only extract column-level options (label and align)
|
|
100
|
-
# Field-level options like description and placeholder don't make sense in table cells
|
|
101
|
-
field_options = field_options.except(:condition).merge(**column_options.slice(:align, :label))
|
|
102
|
-
table.column name,
|
|
103
|
-
**field_options,
|
|
104
|
-
sort_params: current_query_object.sort_params_for(name),
|
|
105
|
-
&tag_block
|
|
89
|
+
@resource_fields.each do |name|
|
|
90
|
+
field_options = resource_definition.defined_fields[name] ? resource_definition.defined_fields[name][:options].dup : {}
|
|
91
|
+
|
|
92
|
+
display_definition = resource_definition.defined_displays[name] || {}
|
|
93
|
+
display_options = display_definition[:options] || {}
|
|
94
|
+
|
|
95
|
+
column_definition = resource_definition.defined_columns[name] || {}
|
|
96
|
+
column_options = column_definition[:options] || {}
|
|
97
|
+
|
|
98
|
+
# Check for conditional rendering
|
|
99
|
+
condition = column_options[:condition]
|
|
100
|
+
conditionally_hidden = condition && !instance_exec(&condition)
|
|
101
|
+
next if conditionally_hidden
|
|
102
|
+
|
|
103
|
+
tag = column_options[:as] || display_definition[:as] || field_options[:as]
|
|
104
|
+
|
|
105
|
+
# Extract field-level options from display_options and column_options
|
|
106
|
+
# These are Phlexi field options that should NOT be passed to the tag builder
|
|
107
|
+
field_level_keys = [:label, :description, :placeholder]
|
|
108
|
+
display_tag_attributes = display_options.except(:wrapper, :as, :condition, *field_level_keys)
|
|
109
|
+
column_tag_attributes = column_options.except(:wrapper, :as, :align, :condition, *field_level_keys)
|
|
110
|
+
tag_attributes = display_tag_attributes.merge(column_tag_attributes)
|
|
111
|
+
tag_block = if column_definition[:block]
|
|
112
|
+
# User-provided blocks receive the raw record for convenience
|
|
113
|
+
user_block = column_definition[:block]
|
|
114
|
+
->(wrapped_object, _key) { user_block.call(wrapped_object.unwrapped) }
|
|
115
|
+
else
|
|
116
|
+
->(wrapped_object, key) {
|
|
117
|
+
f = wrapped_object.field(key)
|
|
118
|
+
tag ||= f.inferred_field_component
|
|
119
|
+
f.send(:"#{tag}_tag", **tag_attributes)
|
|
120
|
+
}
|
|
106
121
|
end
|
|
107
122
|
|
|
108
|
-
table
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
primary_actions = actions.select { |a| a.category.primary? }.sort_by(&:position)
|
|
117
|
-
dropdown_actions = actions.reject { |a| a.category.primary? }.sort_by(&:position)
|
|
123
|
+
# For table columns, only extract column-level options (label and align)
|
|
124
|
+
# Field-level options like description and placeholder don't make sense in table cells
|
|
125
|
+
field_options = field_options.except(:condition).merge(**column_options.slice(:align, :label))
|
|
126
|
+
table.column name,
|
|
127
|
+
**field_options,
|
|
128
|
+
sort_params: current_query_object.sort_params_for(name),
|
|
129
|
+
&tag_block
|
|
130
|
+
end
|
|
118
131
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
132
|
+
table.actions do |wrapped_object|
|
|
133
|
+
record = wrapped_object.unwrapped
|
|
134
|
+
policy = policy_for(record:)
|
|
135
|
+
|
|
136
|
+
actions = resource_definition.defined_actions
|
|
137
|
+
.select { |k, a| a.collection_record_action? && policy.allowed_to?(:"#{k}?") }
|
|
138
|
+
.values
|
|
139
|
+
|
|
140
|
+
primary_actions = actions.select { |a| a.category.primary? }.sort_by(&:position)
|
|
141
|
+
dropdown_actions = actions.reject { |a| a.category.primary? }.sort_by(&:position)
|
|
142
|
+
|
|
143
|
+
div(class: "flex items-center gap-1") do
|
|
144
|
+
# Primary actions as buttons. The :show action is also
|
|
145
|
+
# tagged so the `row-click` controller on the <tr> can
|
|
146
|
+
# delegate row-body clicks to it.
|
|
147
|
+
primary_actions.each do |action|
|
|
148
|
+
url = route_options_to_url(action.route_options, record)
|
|
149
|
+
data = (action.name == :show) ? {row_click_target: "show"} : {}
|
|
150
|
+
ActionButton(action, url:, variant: :table, data: data)
|
|
151
|
+
end
|
|
125
152
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
end
|
|
153
|
+
# Secondary/danger actions in dropdown
|
|
154
|
+
if dropdown_actions.any?
|
|
155
|
+
RowActionsDropdown(actions: dropdown_actions, record:)
|
|
130
156
|
end
|
|
131
157
|
end
|
|
132
158
|
end
|
|
@@ -139,15 +165,58 @@ module Plutonium
|
|
|
139
165
|
.values
|
|
140
166
|
end
|
|
141
167
|
|
|
168
|
+
def filter_panel_controller_data
|
|
169
|
+
{controller: "filter-panel"}
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Hash of the current `q` params reduced to filter values only —
|
|
173
|
+
# used as the FilterForm's record so Phlexi prefills inputs from
|
|
174
|
+
# the URL (it reads values via `object[key]` for Hashes).
|
|
175
|
+
def filter_form_values
|
|
176
|
+
raw = params[:q]
|
|
177
|
+
return {} unless raw
|
|
178
|
+
|
|
179
|
+
hash = raw.respond_to?(:to_unsafe_h) ? raw.to_unsafe_h : raw.to_h
|
|
180
|
+
hash = hash.deep_symbolize_keys
|
|
181
|
+
hash.except(:search, :scope, :sort_fields, :sort_directions)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def render_filter_slideover
|
|
185
|
+
# Backdrop — click-to-close; transparent until panel opens.
|
|
186
|
+
div(
|
|
187
|
+
class: "fixed inset-0 z-40 bg-black/40 opacity-0 pointer-events-none " \
|
|
188
|
+
"transition-opacity duration-200 " \
|
|
189
|
+
"data-[open]:opacity-100 data-[open]:pointer-events-auto",
|
|
190
|
+
data: {filter_panel_target: "backdrop", action: "click->filter-panel#close"}
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Panel — fixed slideover from the right; the form inside owns
|
|
194
|
+
# its scroll region and pinned action strip.
|
|
195
|
+
aside(
|
|
196
|
+
class: "fixed top-0 right-0 bottom-0 z-50 w-full sm:w-[420px] max-w-full " \
|
|
197
|
+
"bg-[var(--pu-surface)] border-l border-[var(--pu-border)] " \
|
|
198
|
+
"translate-x-full transition-transform duration-300 ease-out " \
|
|
199
|
+
"data-[open]:translate-x-0 " \
|
|
200
|
+
"flex flex-col",
|
|
201
|
+
role: "dialog",
|
|
202
|
+
aria: {label: "Filters", hidden: "true", modal: "true"},
|
|
203
|
+
data: {filter_panel_target: "panel"}
|
|
204
|
+
) do
|
|
205
|
+
render Plutonium::UI::Table::Components::FilterForm.new(
|
|
206
|
+
filter_form_values,
|
|
207
|
+
query_object: current_query_object,
|
|
208
|
+
search_url: current_search_url,
|
|
209
|
+
search_value: params.dig(:q, :search) || params[:search]
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
142
214
|
def bulk_actions_controller_data
|
|
143
|
-
{
|
|
144
|
-
controller: "bulk-actions",
|
|
145
|
-
bulk_actions_has_actions_value: bulk_actions.any?
|
|
146
|
-
}
|
|
215
|
+
{controller: "bulk-actions"}
|
|
147
216
|
end
|
|
148
217
|
|
|
149
218
|
def render_footer
|
|
150
|
-
div(class: "lg:sticky
|
|
219
|
+
div(class: "lg:sticky bottom-[-2px] mt-1 p-4 pb-6 w-full z-30 bg-[var(--pu-body)]") {
|
|
151
220
|
TableInfo(pagy_instance)
|
|
152
221
|
TablePagination(pagy_instance)
|
|
153
222
|
}
|
|
@@ -25,10 +25,11 @@ module Plutonium
|
|
|
25
25
|
# Header
|
|
26
26
|
header: "pu-table-header",
|
|
27
27
|
header_grouping_cell: "pu-table-header-cell text-center text-sm border-b border-t border-r last:border-r-0 border-[var(--pu-table-border)]",
|
|
28
|
-
header_cell: "pu-table-header-cell",
|
|
28
|
+
header_cell: "pu-table-header-cell group",
|
|
29
29
|
header_cell_content_wrapper: "inline-flex items-center",
|
|
30
|
-
header_cell_sort_wrapper: "flex items-center",
|
|
30
|
+
header_cell_sort_wrapper: "flex items-center justify-between gap-1",
|
|
31
31
|
header_cell_sort_indicator: "ml-1.5",
|
|
32
|
+
header_cell_link: "flex items-center gap-1 cursor-pointer hover:text-[var(--pu-text)]",
|
|
32
33
|
|
|
33
34
|
# Body
|
|
34
35
|
body_row: "pu-table-body-row",
|
|
@@ -36,11 +37,19 @@ module Plutonium
|
|
|
36
37
|
|
|
37
38
|
# Sorting
|
|
38
39
|
sort_icon: "w-3 h-3",
|
|
39
|
-
sort_icon_active: "text-primary-600 dark:text-primary-400",
|
|
40
|
-
sort_icon_inactive: "text-[var(--pu-text-subtle)]",
|
|
40
|
+
sort_icon_active: "ml-1 inline-flex text-primary-600 dark:text-primary-400",
|
|
41
|
+
sort_icon_inactive: "ml-1 inline-flex text-[var(--pu-text-subtle)] opacity-0 group-hover:opacity-100 transition-opacity",
|
|
42
|
+
sort_priority_badge: "ml-1 inline-flex items-center justify-center w-4 h-4 text-[10px] font-semibold rounded bg-primary-100 text-primary-700 dark:bg-primary-900/40 dark:text-primary-300",
|
|
41
43
|
sort_index_clear_link: "ml-2",
|
|
42
44
|
sort_index_clear_link_text: "text-xs font-bold text-[var(--pu-text-subtle)]",
|
|
43
|
-
sort_index_clear_link_icon: "ml-1 text-danger-600 dark:text-danger-400"
|
|
45
|
+
sort_index_clear_link_icon: "ml-1 text-danger-600 dark:text-danger-400",
|
|
46
|
+
|
|
47
|
+
# Column menu
|
|
48
|
+
column_menu_trigger: "p-1 rounded text-[var(--pu-text-subtle)] hover:text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)] opacity-0 group-hover:opacity-100 transition-opacity",
|
|
49
|
+
column_menu_panel: "hidden absolute right-0 top-full mt-1 z-50 min-w-[180px] bg-[var(--pu-surface)] border border-[var(--pu-border)] rounded-md shadow-lg p-1",
|
|
50
|
+
column_menu_item: "flex items-center gap-2 px-2 py-1.5 text-sm text-[var(--pu-text)] rounded hover:bg-[var(--pu-surface-alt)] w-full",
|
|
51
|
+
column_menu_item_disabled: "flex items-center gap-2 px-2 py-1.5 text-sm text-[var(--pu-text-subtle)] opacity-60 cursor-not-allowed",
|
|
52
|
+
column_menu_separator: "my-1 border-t border-[var(--pu-border)]"
|
|
44
53
|
})
|
|
45
54
|
end
|
|
46
55
|
end
|
data/lib/plutonium/version.rb
CHANGED
data/lib/plutonium.rb
CHANGED
|
@@ -22,6 +22,12 @@ module Plutonium
|
|
|
22
22
|
# Custom error class for Plutonium-specific exceptions
|
|
23
23
|
class Error < StandardError; end
|
|
24
24
|
|
|
25
|
+
# Turbo frame id used by the modal/slideover renderer. The layout wraps
|
|
26
|
+
# itself in this frame, in_modal? checks against it, and Action and
|
|
27
|
+
# Definition default to targeting it. Kept as a single constant so the
|
|
28
|
+
# frame name lives in one place.
|
|
29
|
+
REMOTE_MODAL_FRAME = "remote_modal"
|
|
30
|
+
|
|
25
31
|
# Set up Zeitwerk loader for the Plutonium gem
|
|
26
32
|
# @return [Zeitwerk::Loader] configured Zeitwerk loader instance
|
|
27
33
|
Loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false).tap do |loader|
|
data/package.json
CHANGED