plutonium 0.34.1 → 0.35.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 +53 -0
- data/.claude/skills/{assets → plutonium-assets}/SKILL.md +13 -8
- data/.claude/skills/{connect-resource → plutonium-connect-resource}/SKILL.md +1 -1
- data/.claude/skills/{controller → plutonium-controller}/SKILL.md +27 -13
- data/.claude/skills/{create-resource → plutonium-create-resource}/SKILL.md +1 -1
- data/.claude/skills/{definition → plutonium-definition}/SKILL.md +10 -10
- data/.claude/skills/{definition-actions → plutonium-definition-actions}/SKILL.md +34 -9
- data/.claude/skills/{definition-fields → plutonium-definition-fields}/SKILL.md +38 -10
- data/.claude/skills/plutonium-definition-query/SKILL.md +356 -0
- data/.claude/skills/{forms → plutonium-forms}/SKILL.md +6 -6
- data/.claude/skills/{installation → plutonium-installation}/SKILL.md +9 -9
- data/.claude/skills/{interaction → plutonium-interaction}/SKILL.md +20 -19
- data/.claude/skills/{model → plutonium-model}/SKILL.md +3 -3
- data/.claude/skills/{model-features → plutonium-model-features}/SKILL.md +3 -3
- data/.claude/skills/{nested-resources → plutonium-nested-resources}/SKILL.md +5 -5
- data/.claude/skills/{package → plutonium-package}/SKILL.md +7 -8
- data/.claude/skills/{policy → plutonium-policy}/SKILL.md +26 -4
- data/.claude/skills/{portal → plutonium-portal}/SKILL.md +33 -31
- data/.claude/skills/{resource → plutonium-resource}/SKILL.md +27 -27
- data/.claude/skills/{rodauth → plutonium-rodauth}/SKILL.md +5 -5
- data/.claude/skills/plutonium-theming/SKILL.md +424 -0
- data/.claude/skills/{views → plutonium-views}/SKILL.md +7 -7
- data/CHANGELOG.md +52 -0
- data/CLAUDE.md +215 -0
- data/CONTRIBUTING.md +72 -18
- data/README.md +100 -19
- data/app/assets/plutonium.css +1 -11
- data/app/assets/plutonium.js +1685 -1146
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +70 -70
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/resource/interactive_bulk_action.html.erb +1 -5
- data/app/views/rodauth/_email_auth_request_form.html.erb +1 -1
- data/app/views/rodauth/_login_form.html.erb +15 -55
- data/app/views/rodauth/_login_form_footer.html.erb +2 -2
- data/app/views/rodauth/_password_visibility.html.erb +2 -8
- data/app/views/rodauth/add_recovery_codes.html.erb +2 -2
- data/app/views/rodauth/change_login.html.erb +36 -19
- data/app/views/rodauth/change_password.html.erb +34 -10
- data/app/views/rodauth/close_account.html.erb +12 -4
- data/app/views/rodauth/confirm_password.html.erb +19 -17
- data/app/views/rodauth/create_account.html.erb +30 -109
- data/app/views/rodauth/email_auth.html.erb +1 -1
- data/app/views/rodauth/logout.html.erb +4 -4
- data/app/views/rodauth/otp_auth.html.erb +13 -4
- data/app/views/rodauth/otp_disable.html.erb +12 -4
- data/app/views/rodauth/otp_setup.html.erb +29 -12
- data/app/views/rodauth/otp_unlock.html.erb +19 -10
- data/app/views/rodauth/otp_unlock_not_available.html.erb +7 -7
- data/app/views/rodauth/recovery_auth.html.erb +12 -4
- data/app/views/rodauth/recovery_codes.html.erb +12 -4
- data/app/views/rodauth/remember.html.erb +7 -7
- data/app/views/rodauth/reset_password.html.erb +23 -7
- data/app/views/rodauth/reset_password_request.html.erb +14 -10
- data/app/views/rodauth/sms_auth.html.erb +13 -4
- data/app/views/rodauth/sms_confirm.html.erb +13 -4
- data/app/views/rodauth/sms_disable.html.erb +12 -4
- data/app/views/rodauth/sms_request.html.erb +1 -1
- data/app/views/rodauth/sms_setup.html.erb +23 -7
- data/app/views/rodauth/two_factor_auth.html.erb +2 -2
- data/app/views/rodauth/two_factor_disable.html.erb +12 -4
- data/app/views/rodauth/two_factor_manage.html.erb +7 -7
- data/app/views/rodauth/unlock_account.html.erb +13 -5
- data/app/views/rodauth/unlock_account_request.html.erb +2 -2
- data/app/views/rodauth/verify_account.html.erb +25 -7
- data/app/views/rodauth/verify_account_resend.html.erb +14 -10
- data/app/views/rodauth/verify_login_change.html.erb +1 -1
- data/app/views/rodauth/webauthn_auth.html.erb +1 -1
- data/app/views/rodauth/webauthn_remove.html.erb +18 -8
- data/app/views/rodauth/webauthn_setup.html.erb +12 -4
- data/docs/.vitepress/config.ts +15 -26
- data/docs/.vitepress/theme/custom.css +388 -29
- data/docs/getting-started/index.md +1 -1
- data/docs/getting-started/tutorial/02-first-resource.md +9 -0
- data/docs/getting-started/tutorial/06-nested-resources.md +2 -2
- data/docs/getting-started/tutorial/07-author-portal.md +191 -0
- data/docs/getting-started/tutorial/{07-customizing-ui.md → 08-customizing-ui.md} +7 -7
- data/docs/getting-started/tutorial/index.md +5 -2
- data/docs/guides/authorization.md +33 -0
- data/docs/guides/creating-packages.md +12 -16
- data/docs/guides/custom-actions.md +36 -0
- data/docs/guides/search-filtering.md +121 -42
- data/docs/guides/theming.md +232 -36
- data/docs/index.md +203 -57
- data/docs/public/og-image.png +0 -0
- data/docs/reference/controller/index.md +14 -16
- data/docs/reference/definition/actions.md +38 -3
- data/docs/reference/definition/fields.md +3 -3
- data/docs/reference/definition/index.md +2 -2
- data/docs/reference/generators/index.md +0 -1
- data/docs/reference/interaction/index.md +14 -10
- data/docs/reference/model/index.md +0 -1
- data/docs/reference/portal/index.md +13 -27
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/pkg/portal/portal_generator.rb +0 -2
- data/lib/generators/pu/pkg/portal/templates/app/views/package/dashboard/index.html.erb +28 -72
- data/lib/plutonium/action/interactive.rb +2 -2
- data/lib/plutonium/core/controller.rb +2 -1
- data/lib/plutonium/definition/actions.rb +2 -2
- data/lib/plutonium/lib/deep_freezer.rb +3 -7
- data/lib/plutonium/query/filter.rb +14 -0
- data/lib/plutonium/query/filters/association.rb +49 -0
- data/lib/plutonium/query/filters/boolean.rb +35 -0
- data/lib/plutonium/query/filters/date.rb +97 -0
- data/lib/plutonium/query/filters/date_range.rb +58 -0
- data/lib/plutonium/query/filters/select.rb +55 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +24 -6
- data/lib/plutonium/resource/controllers/interactive_actions.rb +76 -58
- data/lib/plutonium/resource/controllers/queryable.rb +4 -2
- data/lib/plutonium/resource/query_object.rb +1 -1
- data/lib/plutonium/ui/action_button.rb +23 -65
- data/lib/plutonium/ui/actions_dropdown.rb +103 -0
- data/lib/plutonium/ui/block.rb +1 -1
- data/lib/plutonium/ui/breadcrumbs.rb +12 -19
- data/lib/plutonium/ui/color_mode_selector.rb +1 -1
- data/lib/plutonium/ui/component/kit.rb +6 -0
- data/lib/plutonium/ui/component_classes.rb +102 -0
- data/lib/plutonium/ui/display/base.rb +15 -0
- data/lib/plutonium/ui/display/components/attachment.rb +6 -5
- data/lib/plutonium/ui/display/components/boolean.rb +23 -0
- data/lib/plutonium/ui/display/components/color.rb +23 -0
- data/lib/plutonium/ui/display/resource.rb +1 -1
- data/lib/plutonium/ui/display/theme.rb +29 -15
- data/lib/plutonium/ui/empty_card.rb +3 -3
- data/lib/plutonium/ui/form/base.rb +20 -0
- data/lib/plutonium/ui/form/components/key_value_store.rb +11 -11
- data/lib/plutonium/ui/form/components/resource_select.rb +31 -0
- data/lib/plutonium/ui/form/components/secure_association.rb +1 -2
- data/lib/plutonium/ui/form/components/uppy.rb +5 -4
- data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +4 -4
- data/lib/plutonium/ui/form/interaction.rb +17 -1
- data/lib/plutonium/ui/form/query.rb +133 -80
- data/lib/plutonium/ui/form/theme.rb +50 -35
- data/lib/plutonium/ui/frame_navigator_panel.rb +2 -2
- data/lib/plutonium/ui/layout/base.rb +1 -1
- data/lib/plutonium/ui/layout/header.rb +4 -7
- data/lib/plutonium/ui/layout/rodauth_layout.rb +7 -7
- data/lib/plutonium/ui/layout/sidebar.rb +1 -1
- data/lib/plutonium/ui/nav_grid_menu.rb +7 -6
- data/lib/plutonium/ui/nav_user.rb +9 -8
- data/lib/plutonium/ui/page/interactive_action.rb +5 -5
- data/lib/plutonium/ui/page_header.rb +29 -10
- data/lib/plutonium/ui/panel.rb +4 -4
- data/lib/plutonium/ui/sidebar_menu.rb +8 -8
- data/lib/plutonium/ui/skeleton_table.rb +7 -8
- data/lib/plutonium/ui/tab_list.rb +5 -5
- data/lib/plutonium/ui/table/base.rb +3 -0
- data/lib/plutonium/ui/table/components/attachment.rb +4 -3
- data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +82 -0
- data/lib/plutonium/ui/table/components/pagy_info.rb +2 -2
- data/lib/plutonium/ui/table/components/pagy_pagination.rb +13 -8
- data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +101 -0
- data/lib/plutonium/ui/table/components/scopes_bar.rb +2 -2
- data/lib/plutonium/ui/table/components/selection_column.rb +100 -0
- data/lib/plutonium/ui/table/display_theme.rb +6 -6
- data/lib/plutonium/ui/table/resource.rb +93 -52
- data/lib/plutonium/ui/table/theme.rb +28 -15
- data/lib/plutonium/version.rb +1 -1
- data/package.json +2 -2
- data/plutonium.gemspec +5 -4
- data/src/css/components.css +471 -0
- data/src/css/intl_tel_input.css +2 -2
- data/src/css/plutonium.css +2 -0
- data/src/css/tokens.css +149 -0
- data/src/js/controllers/bulk_actions_controller.js +109 -0
- data/src/js/controllers/filter_panel_controller.js +35 -0
- data/src/js/controllers/register_controllers.js +5 -1
- data/src/js/controllers/resource_drop_down_controller.js +25 -1
- data/src/js/controllers/slim_select_controller.js +6 -2
- data/src/js/turbo/turbo_actions.js +1 -1
- metadata +52 -39
- data/.claude/skills/definition-query/SKILL.md +0 -334
- data/docs/concepts/architecture.md +0 -226
- data/docs/concepts/auto-detection.md +0 -254
- data/docs/concepts/index.md +0 -61
- data/docs/concepts/packages-portals.md +0 -304
- data/docs/concepts/resources.md +0 -224
- data/docs/cookbook/blog.md +0 -411
- data/docs/cookbook/index.md +0 -289
- data/docs/cookbook/saas.md +0 -481
- data/docs/public/CLAUDE.md +0 -578
- data/lib/generators/pu/pkg/portal/templates/app/controllers/resource_controller.rb.tt +0 -5
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Table
|
|
6
|
+
module Components
|
|
7
|
+
class BulkActionsToolbar < Plutonium::UI::Component::Base
|
|
8
|
+
include Phlex::Rails::Helpers::LinkTo
|
|
9
|
+
|
|
10
|
+
# Color to CSS class mapping for soft button variants
|
|
11
|
+
COLOR_CLASSES = {
|
|
12
|
+
primary: "pu-btn-soft-primary",
|
|
13
|
+
success: "pu-btn-soft-success",
|
|
14
|
+
warning: "pu-btn-soft-warning",
|
|
15
|
+
danger: "pu-btn-soft-danger",
|
|
16
|
+
info: "pu-btn-soft-info",
|
|
17
|
+
accent: "pu-btn-soft-accent",
|
|
18
|
+
secondary: "pu-btn-soft-secondary"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def initialize(bulk_actions:)
|
|
22
|
+
@bulk_actions = bulk_actions
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def view_template
|
|
26
|
+
# Always render toolbar - hidden by default, Stimulus shows it when items are selected
|
|
27
|
+
div(
|
|
28
|
+
class: "hidden flex pu-toolbar",
|
|
29
|
+
data: {bulk_actions_target: "toolbar"}
|
|
30
|
+
) do
|
|
31
|
+
render_selected_count
|
|
32
|
+
render_action_buttons
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def render_selected_count
|
|
39
|
+
span(class: "pu-toolbar-text") do
|
|
40
|
+
span(data: {bulk_actions_target: "selectedCount"}) { "0" }
|
|
41
|
+
plain " selected"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def render_action_buttons
|
|
46
|
+
div(class: "pu-toolbar-actions") do
|
|
47
|
+
@bulk_actions.each do |action|
|
|
48
|
+
render_action_button(action)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def render_action_button(action)
|
|
54
|
+
url = route_options_to_url(action.route_options, resource_class)
|
|
55
|
+
|
|
56
|
+
link_to(
|
|
57
|
+
url,
|
|
58
|
+
class: button_classes(action),
|
|
59
|
+
data: {
|
|
60
|
+
bulk_actions_target: "actionButton",
|
|
61
|
+
bulk_action_name: action.name,
|
|
62
|
+
bulk_action_url: url,
|
|
63
|
+
turbo_frame: action.turbo_frame
|
|
64
|
+
}
|
|
65
|
+
) do
|
|
66
|
+
if action.icon
|
|
67
|
+
render action.icon.new(class: "h-4 w-4")
|
|
68
|
+
end
|
|
69
|
+
span { action.label }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def button_classes(action)
|
|
74
|
+
color_key = (action.color || action.category)&.to_sym || :primary
|
|
75
|
+
color_class = COLOR_CLASSES[color_key] || COLOR_CLASSES[:primary]
|
|
76
|
+
"pu-btn pu-btn-sm #{color_class}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -13,7 +13,7 @@ module Plutonium
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def view_template
|
|
16
|
-
div(class: "flex flex-col md:flex-row justify-between items-center text-sm text-
|
|
16
|
+
div(class: "flex flex-col md:flex-row justify-between items-center text-sm text-[var(--pu-text-muted)]") do
|
|
17
17
|
results_info
|
|
18
18
|
per_page_selector
|
|
19
19
|
end
|
|
@@ -53,7 +53,7 @@ module Plutonium
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def select_classes
|
|
56
|
-
"bg-
|
|
56
|
+
"bg-[var(--pu-surface)] border border-[var(--pu-border)] text-[var(--pu-text)] text-sm rounded-[var(--pu-radius-md)] focus:ring-2 focus:ring-primary-500 focus:border-primary-500 block p-2.5 min-w-[5em]"
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def page_url(limit)
|
|
@@ -12,8 +12,8 @@ module Plutonium
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def view_template
|
|
15
|
-
nav(aria_label: "Page navigation", class: "flex justify-center mt-
|
|
16
|
-
ul(class: "inline-flex -
|
|
15
|
+
nav(aria_label: "Page navigation", class: "flex justify-center mt-6") do
|
|
16
|
+
ul(class: "inline-flex items-center gap-1 text-sm") do
|
|
17
17
|
prev_link
|
|
18
18
|
page_links
|
|
19
19
|
next_link
|
|
@@ -79,19 +79,24 @@ module Plutonium
|
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
def link_classes(first = false, last = false)
|
|
82
|
-
|
|
83
|
-
classes
|
|
84
|
-
classes << "rounded-
|
|
82
|
+
base = "flex items-center justify-center w-9 h-9 text-[var(--pu-text-muted)] bg-[var(--pu-surface)] border border-[var(--pu-border)] hover:bg-[var(--pu-surface-alt)] hover:text-[var(--pu-text)] transition-colors"
|
|
83
|
+
classes = [base]
|
|
84
|
+
classes << "rounded-l-lg" if first
|
|
85
|
+
classes << "rounded-r-lg" if last
|
|
86
|
+
classes << "rounded-lg" if !first && !last
|
|
85
87
|
classes.join(" ")
|
|
86
88
|
end
|
|
87
89
|
|
|
88
90
|
def current_link_classes
|
|
89
|
-
"flex items-center justify-center
|
|
91
|
+
"flex items-center justify-center w-9 h-9 text-white bg-primary-600 border border-primary-600 rounded-lg font-medium cursor-default"
|
|
90
92
|
end
|
|
91
93
|
|
|
92
94
|
def disabled_link_classes(first = false, last = false)
|
|
93
|
-
|
|
94
|
-
classes
|
|
95
|
+
base = "flex items-center justify-center w-9 h-9 text-[var(--pu-text-subtle)] bg-[var(--pu-surface-alt)] border border-[var(--pu-border)] opacity-50 cursor-not-allowed"
|
|
96
|
+
classes = [base]
|
|
97
|
+
classes << "rounded-l-lg" if first
|
|
98
|
+
classes << "rounded-r-lg" if last
|
|
99
|
+
classes << "rounded-lg" if !first && !last
|
|
95
100
|
classes.join(" ")
|
|
96
101
|
end
|
|
97
102
|
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Table
|
|
6
|
+
module Components
|
|
7
|
+
# Dropdown menu for secondary and danger row actions in tables
|
|
8
|
+
# Shows a compact icon trigger with grouped actions
|
|
9
|
+
class RowActionsDropdown < Plutonium::UI::Component::Base
|
|
10
|
+
def initialize(actions:, record:)
|
|
11
|
+
@actions = actions
|
|
12
|
+
@record = record
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def view_template
|
|
16
|
+
div(class: "relative", data: {controller: "resource-drop-down"}) do
|
|
17
|
+
render_trigger_button
|
|
18
|
+
render_dropdown_menu
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def render_trigger_button
|
|
25
|
+
button(
|
|
26
|
+
type: "button",
|
|
27
|
+
class: "p-1.5 rounded-[var(--pu-radius-md)] border border-[var(--pu-border)] bg-[var(--pu-surface)] text-[var(--pu-text-muted)] hover:text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)] hover:border-[var(--pu-border-strong)] transition-colors",
|
|
28
|
+
aria: {expanded: "false", haspopup: "true", label: "More actions"},
|
|
29
|
+
data: {resource_drop_down_target: "trigger"}
|
|
30
|
+
) do
|
|
31
|
+
render Phlex::TablerIcons::DotsVertical.new(class: "w-4 h-4")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def render_dropdown_menu
|
|
36
|
+
div(
|
|
37
|
+
class: "hidden absolute right-0 z-50 mt-1 w-40 origin-top-right bg-[var(--pu-surface)] border border-[var(--pu-border)] rounded-[var(--pu-radius-md)] overflow-hidden",
|
|
38
|
+
style: "box-shadow: var(--pu-shadow-lg)",
|
|
39
|
+
data: {resource_drop_down_target: "menu"}
|
|
40
|
+
) do
|
|
41
|
+
render_secondary_actions if secondary_actions.any?
|
|
42
|
+
render_danger_divider if secondary_actions.any? && danger_actions.any?
|
|
43
|
+
render_danger_actions if danger_actions.any?
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def render_secondary_actions
|
|
48
|
+
div(class: "py-1") do
|
|
49
|
+
secondary_actions.each { |action| render_action_item(action) }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def render_danger_divider
|
|
54
|
+
div(class: "border-t border-[var(--pu-border-muted)]")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def render_danger_actions
|
|
58
|
+
div(class: "py-1") do
|
|
59
|
+
danger_actions.each { |action| render_action_item(action, danger: true) }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def render_action_item(action, danger: false)
|
|
64
|
+
url = route_options_to_url(action.route_options, @record)
|
|
65
|
+
|
|
66
|
+
link_attrs = {
|
|
67
|
+
href: url,
|
|
68
|
+
class: tokens(
|
|
69
|
+
"flex items-center gap-2 px-3 py-1.5 text-sm transition-colors",
|
|
70
|
+
danger ? "text-danger-600 dark:text-danger-400 hover:bg-danger-50 dark:hover:bg-danger-900/30" : "text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)]"
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Add turbo frame if specified
|
|
75
|
+
link_attrs[:data] = {turbo_frame: action.turbo_frame} if action.turbo_frame
|
|
76
|
+
|
|
77
|
+
# Add confirmation if specified
|
|
78
|
+
if action.confirmation
|
|
79
|
+
link_attrs[:data] ||= {}
|
|
80
|
+
link_attrs[:data][:turbo_method] = action.route_options.method if action.route_options.method
|
|
81
|
+
link_attrs[:data][:turbo_confirm] = action.confirmation
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
a(**link_attrs) do
|
|
85
|
+
render action.icon.new(class: "w-4 h-4") if action.icon
|
|
86
|
+
span { action.label }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def secondary_actions
|
|
91
|
+
@secondary_actions ||= @actions.select { |a| a.category.secondary? }.sort_by(&:position)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def danger_actions
|
|
95
|
+
@danger_actions ||= @actions.select { |a| a.category.danger? }.sort_by(&:position)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -124,11 +124,11 @@ module Plutonium
|
|
|
124
124
|
end
|
|
125
125
|
|
|
126
126
|
def active_scope_class
|
|
127
|
-
"
|
|
127
|
+
"pu-btn pu-btn-sm pu-btn-primary"
|
|
128
128
|
end
|
|
129
129
|
|
|
130
130
|
def inactive_scope_class
|
|
131
|
-
"
|
|
131
|
+
"pu-btn pu-btn-sm pu-btn-ghost"
|
|
132
132
|
end
|
|
133
133
|
|
|
134
134
|
def render?
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Table
|
|
6
|
+
module Components
|
|
7
|
+
# Custom selection column with Stimulus data attributes for bulk actions
|
|
8
|
+
class SelectionColumn < Phlexi::Table::Components::SelectionColumn
|
|
9
|
+
def header_cell
|
|
10
|
+
SelectionHeaderCell.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def data_cell(wrapped_object)
|
|
14
|
+
allowed_actions = compute_allowed_actions(wrapped_object.unwrapped)
|
|
15
|
+
SelectionDataCell.new(wrapped_object.field(key).dom.value, allowed_actions)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Add hidden class and Stimulus target to header cell
|
|
19
|
+
def header_cell_attributes
|
|
20
|
+
{
|
|
21
|
+
class: "hidden w-12",
|
|
22
|
+
data: {bulk_actions_target: "selectionCell"}
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Add hidden class and Stimulus target to data cell
|
|
27
|
+
def data_cell_attributes(wrapped_object)
|
|
28
|
+
{
|
|
29
|
+
scope: :row,
|
|
30
|
+
class: "hidden",
|
|
31
|
+
data: {bulk_actions_target: "selectionCell"}
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def bulk_actions
|
|
38
|
+
options[:bulk_actions] || []
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def policy_resolver
|
|
42
|
+
options[:policy_resolver]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def compute_allowed_actions(record)
|
|
46
|
+
return bulk_action_names unless policy_resolver
|
|
47
|
+
|
|
48
|
+
policy = policy_resolver.call(record)
|
|
49
|
+
bulk_actions.select { |action|
|
|
50
|
+
policy.allowed_to?(:"#{action.name}?")
|
|
51
|
+
}.map { |a| a.name.to_s }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Header cell checkbox with "select all" functionality
|
|
56
|
+
class SelectionHeaderCell < Phlexi::Table::HTML
|
|
57
|
+
def view_template
|
|
58
|
+
input(
|
|
59
|
+
type: :checkbox,
|
|
60
|
+
class: themed(:selection_checkbox),
|
|
61
|
+
data: {
|
|
62
|
+
bulk_actions_target: "checkboxAll",
|
|
63
|
+
action: "bulk-actions#toggleAll"
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Data cell checkbox for individual row selection
|
|
70
|
+
class SelectionDataCell < Phlexi::Table::HTML
|
|
71
|
+
def initialize(value, allowed_actions)
|
|
72
|
+
@value = value
|
|
73
|
+
@allowed_actions = allowed_actions
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def view_template
|
|
77
|
+
if @allowed_actions.empty?
|
|
78
|
+
# Show X when no actions available for this record
|
|
79
|
+
span(
|
|
80
|
+
class: "inline-flex items-center justify-center size-4 text-[var(--pu-text-subtle)]",
|
|
81
|
+
title: "No bulk actions available"
|
|
82
|
+
) { "✕" }
|
|
83
|
+
else
|
|
84
|
+
input(
|
|
85
|
+
type: :checkbox,
|
|
86
|
+
value: @value,
|
|
87
|
+
class: themed(:selection_checkbox),
|
|
88
|
+
data: {
|
|
89
|
+
bulk_actions_target: "checkbox",
|
|
90
|
+
action: "bulk-actions#toggle",
|
|
91
|
+
allowed_actions: @allowed_actions.join(",")
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -7,13 +7,13 @@ module Plutonium
|
|
|
7
7
|
def self.theme
|
|
8
8
|
super.merge({
|
|
9
9
|
value_wrapper: "max-h-[150px] overflow-y-auto",
|
|
10
|
-
prefixed_icon: "w-4 h-4 mr-1",
|
|
11
|
-
link: "text-primary-600 dark:text-primary-500",
|
|
10
|
+
prefixed_icon: "w-4 h-4 mr-1 text-[var(--pu-text-muted)]",
|
|
11
|
+
link: "text-primary-600 dark:text-primary-400 hover:text-primary-500 dark:hover:text-primary-300 transition-colors",
|
|
12
12
|
color: "flex items-center",
|
|
13
|
-
color_indicator: "w-
|
|
14
|
-
email: "flex items-center text-primary-600 dark:text-primary-500 whitespace-nowrap",
|
|
15
|
-
phone: "flex items-center text-primary-600 dark:text-primary-500 whitespace-nowrap",
|
|
16
|
-
json: "
|
|
13
|
+
color_indicator: "w-8 h-8 rounded-md mr-2 shadow-sm border border-[var(--pu-border)]",
|
|
14
|
+
email: "flex items-center gap-1 text-primary-600 dark:text-primary-400 hover:text-primary-500 whitespace-nowrap transition-colors",
|
|
15
|
+
phone: "flex items-center gap-1 text-primary-600 dark:text-primary-400 hover:text-primary-500 whitespace-nowrap transition-colors",
|
|
16
|
+
json: "whitespace-pre font-mono text-xs bg-[var(--pu-surface-alt)] border border-[var(--pu-border-muted)] rounded-[var(--pu-radius-sm)] p-2 overflow-x-auto",
|
|
17
17
|
attachment_value_wrapper: "flex flex-wrap gap-1"
|
|
18
18
|
})
|
|
19
19
|
end
|
|
@@ -32,7 +32,7 @@ module Plutonium
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def render_empty_card
|
|
35
|
-
EmptyCard("No #{resource_name_plural(resource_class)}
|
|
35
|
+
EmptyCard("No #{resource_name_plural(resource_class).downcase} available") {
|
|
36
36
|
action = resource_definition.defined_actions[:new]
|
|
37
37
|
if action&.permitted_by?(current_policy)
|
|
38
38
|
url = route_options_to_url(action.route_options, resource_class)
|
|
@@ -42,70 +42,111 @@ module Plutonium
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def render_table
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
45
|
+
# Wrap table in Stimulus controller for bulk actions
|
|
46
|
+
div(data: bulk_actions_controller_data) do
|
|
47
|
+
# Bulk actions toolbar (hidden by default, shown when items selected)
|
|
48
|
+
BulkActionsToolbar(bulk_actions:) if bulk_actions.any?
|
|
49
|
+
|
|
50
|
+
render Plutonium::UI::Table::Base.new(collection) do |table|
|
|
51
|
+
# Selection column for bulk actions (hidden by default, Stimulus shows it)
|
|
52
|
+
# Pass bulk actions and policy resolver for per-record authorization
|
|
53
|
+
table.selection_column :id,
|
|
54
|
+
bulk_actions:,
|
|
55
|
+
policy_resolver: ->(record) { policy_for(record:) }
|
|
56
|
+
|
|
57
|
+
@resource_fields.each do |name|
|
|
58
|
+
# field :name, as: :string
|
|
59
|
+
# column :description, class: "text-red-700"
|
|
60
|
+
# column :age, align: :end
|
|
61
|
+
# column :description do |record|
|
|
62
|
+
# record.description&.truncate(50)
|
|
63
|
+
# end
|
|
64
|
+
|
|
65
|
+
field_options = resource_definition.defined_fields[name] ? resource_definition.defined_fields[name][:options].dup : {}
|
|
66
|
+
|
|
67
|
+
display_definition = resource_definition.defined_displays[name] || {}
|
|
68
|
+
display_options = display_definition[:options] || {}
|
|
69
|
+
|
|
70
|
+
column_definition = resource_definition.defined_columns[name] || {}
|
|
71
|
+
column_options = column_definition[:options] || {}
|
|
72
|
+
|
|
73
|
+
# Check for conditional rendering
|
|
74
|
+
condition = column_options[:condition]
|
|
75
|
+
conditionally_hidden = condition && !instance_exec(&condition)
|
|
76
|
+
next if conditionally_hidden
|
|
77
|
+
|
|
78
|
+
tag = column_options[:as] || display_definition[:as] || field_options[:as]
|
|
79
|
+
|
|
80
|
+
# Extract field-level options from display_options and column_options
|
|
81
|
+
# These are Phlexi field options that should NOT be passed to the tag builder
|
|
82
|
+
field_level_keys = [:label, :description, :placeholder]
|
|
83
|
+
display_tag_attributes = display_options.except(:wrapper, :as, :condition, *field_level_keys)
|
|
84
|
+
column_tag_attributes = column_options.except(:wrapper, :as, :align, :condition, *field_level_keys)
|
|
85
|
+
tag_attributes = display_tag_attributes.merge(column_tag_attributes)
|
|
86
|
+
tag_block = if column_definition[:block]
|
|
87
|
+
# User-provided blocks receive the raw record for convenience
|
|
88
|
+
user_block = column_definition[:block]
|
|
89
|
+
->(wrapped_object, _key) { user_block.call(wrapped_object.unwrapped) }
|
|
90
|
+
else
|
|
91
|
+
->(wrapped_object, key) {
|
|
92
|
+
f = wrapped_object.field(key)
|
|
93
|
+
tag ||= f.inferred_field_component
|
|
94
|
+
f.send(:"#{tag}_tag", **tag_attributes)
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# For table columns, only extract column-level options (label and align)
|
|
99
|
+
# Field-level options like description and placeholder don't make sense in table cells
|
|
100
|
+
field_options = field_options.except(:condition).merge(**column_options.slice(:align, :label))
|
|
101
|
+
table.column name,
|
|
102
|
+
**field_options,
|
|
103
|
+
sort_params: current_query_object.sort_params_for(name),
|
|
104
|
+
&tag_block
|
|
105
|
+
end
|
|
89
106
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
107
|
+
table.actions do |wrapped_object|
|
|
108
|
+
record = wrapped_object.unwrapped
|
|
109
|
+
policy = policy_for(record:)
|
|
93
110
|
|
|
94
|
-
|
|
95
|
-
resource_definition.defined_actions
|
|
111
|
+
actions = resource_definition.defined_actions
|
|
96
112
|
.select { |k, a| a.collection_record_action? && policy.allowed_to?(:"#{k}?") }
|
|
97
113
|
.values
|
|
98
|
-
|
|
114
|
+
|
|
115
|
+
primary_actions = actions.select { |a| a.category.primary? }.sort_by(&:position)
|
|
116
|
+
dropdown_actions = actions.reject { |a| a.category.primary? }.sort_by(&:position)
|
|
117
|
+
|
|
118
|
+
div(class: "flex items-center gap-1") do
|
|
119
|
+
# Primary actions as buttons
|
|
120
|
+
primary_actions.each do |action|
|
|
99
121
|
url = route_options_to_url(action.route_options, record)
|
|
100
122
|
ActionButton(action, url:, variant: :table)
|
|
101
123
|
end
|
|
124
|
+
|
|
125
|
+
# Secondary/danger actions in dropdown
|
|
126
|
+
if dropdown_actions.any?
|
|
127
|
+
RowActionsDropdown(actions: dropdown_actions, record:)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
102
130
|
end
|
|
103
131
|
end
|
|
104
132
|
end
|
|
105
133
|
end
|
|
106
134
|
|
|
135
|
+
def bulk_actions
|
|
136
|
+
@bulk_actions ||= resource_definition.defined_actions
|
|
137
|
+
.select { |k, a| a.bulk_action? }
|
|
138
|
+
.values
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def bulk_actions_controller_data
|
|
142
|
+
{
|
|
143
|
+
controller: "bulk-actions",
|
|
144
|
+
bulk_actions_has_actions_value: bulk_actions.any?
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
|
|
107
148
|
def render_footer
|
|
108
|
-
div(class: "lg:sticky lg:dyna:static bottom-[-2px] mt-1 p-4 pb-6 w-full z-30 bg-
|
|
149
|
+
div(class: "lg:sticky lg:dyna:static bottom-[-2px] mt-1 p-4 pb-6 w-full z-30 bg-[var(--pu-body)]") {
|
|
109
150
|
TableInfo(pagy_instance)
|
|
110
151
|
TablePagination(pagy_instance)
|
|
111
152
|
}
|
|
@@ -6,28 +6,41 @@ module Plutonium
|
|
|
6
6
|
class Theme < Phlexi::Table::Theme
|
|
7
7
|
def self.theme
|
|
8
8
|
super.merge({
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
# Selection
|
|
10
|
+
selection_checkbox: "pu-checkbox",
|
|
11
|
+
selection_header_cell: "pu-selection-cell",
|
|
12
|
+
selection_body_cell: "pu-selection-cell py-4",
|
|
13
|
+
|
|
14
|
+
# Column formatting
|
|
15
|
+
name_column: "font-medium text-[var(--pu-text)] whitespace-nowrap",
|
|
11
16
|
align_start: "text-start",
|
|
12
17
|
align_end: "text-end",
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
|
|
19
|
+
# Table structure
|
|
20
|
+
wrapper: "pu-table-wrapper overflow-x-auto",
|
|
21
|
+
base: "pu-table rtl:text-right",
|
|
22
|
+
caption: "pu-panel-header text-lg font-semibold text-left rtl:text-right",
|
|
23
|
+
description: "mt-1 text-sm font-normal text-[var(--pu-text-muted)]",
|
|
24
|
+
|
|
25
|
+
# Header
|
|
26
|
+
header: "pu-table-header",
|
|
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",
|
|
20
29
|
header_cell_content_wrapper: "inline-flex items-center",
|
|
21
30
|
header_cell_sort_wrapper: "flex items-center",
|
|
22
31
|
header_cell_sort_indicator: "ml-1.5",
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
|
|
33
|
+
# Body
|
|
34
|
+
body_row: "pu-table-body-row",
|
|
35
|
+
body_cell: "pu-table-body-cell whitespace-pre max-w-[450px] overflow-hidden text-ellipsis",
|
|
36
|
+
|
|
37
|
+
# Sorting
|
|
25
38
|
sort_icon: "w-3 h-3",
|
|
26
|
-
sort_icon_active: "text-primary-600",
|
|
27
|
-
sort_icon_inactive: "text-
|
|
39
|
+
sort_icon_active: "text-primary-600 dark:text-primary-400",
|
|
40
|
+
sort_icon_inactive: "text-[var(--pu-text-subtle)]",
|
|
28
41
|
sort_index_clear_link: "ml-2",
|
|
29
|
-
sort_index_clear_link_text: "text-xs font-bold text-
|
|
30
|
-
sort_index_clear_link_icon: "ml-1 text-
|
|
42
|
+
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"
|
|
31
44
|
})
|
|
32
45
|
end
|
|
33
46
|
end
|
data/lib/plutonium/version.rb
CHANGED
data/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@radioactive-labs/plutonium",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.35.0",
|
|
4
|
+
"description": "Build production-ready Rails apps in minutes, not days. Convention-driven, fully customizable, AI-ready.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/js/core.js",
|
|
7
7
|
"files": [
|