plutonium 0.49.0 → 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-invites/SKILL.md +41 -0
- data/.claude/skills/plutonium-views/SKILL.md +59 -0
- data/CHANGELOG.md +27 -0
- data/app/assets/plutonium.css +2 -2
- data/app/assets/plutonium.js +404 -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/_flash.html.erb +1 -1
- 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/guides/user-invites.md +64 -0
- 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-06-multi-invite-model-support.md +1487 -0
- data/docs/superpowers/plans/2026-05-06-multi-invite-model-support.md.tasks.json +15 -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_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/core/install/templates/config/initializers/plutonium.rb +1 -0
- data/lib/generators/pu/core/update/update_generator.rb +20 -0
- data/lib/generators/pu/invites/install_generator.rb +136 -35
- data/lib/generators/pu/invites/templates/app/interactions/invite_user_interaction.rb.tt +8 -2
- data/lib/generators/pu/invites/templates/app/interactions/user_invite_user_interaction.rb.tt +7 -1
- data/lib/generators/pu/invites/templates/db/migrate/create_user_invites.rb.tt +4 -4
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +9 -4
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/welcome_controller.rb.tt +2 -2
- data/lib/generators/pu/invites/templates/packages/invites/app/definitions/invites/user_invite_definition.rb.tt +1 -1
- data/lib/generators/pu/invites/templates/packages/invites/app/mailers/invites/user_invite_mailer.rb.tt +8 -8
- data/lib/generators/pu/invites/templates/packages/invites/app/models/invites/user_invite.rb.tt +13 -4
- data/lib/generators/pu/invites/templates/packages/invites/app/policies/invites/user_invite_policy.rb.tt +3 -3
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/landing.html.erb.tt +1 -1
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/show.html.erb.tt +1 -1
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/signup.html.erb.tt +2 -2
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invite_mailer/invitation.html.erb.tt +4 -4
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invite_mailer/invitation.text.erb.tt +4 -4
- data/lib/generators/pu/invites/templates/packages/invites/app/views/layouts/invites/invitation.html.erb.tt +5 -1
- data/lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb +54 -5
- data/lib/generators/pu/saas/welcome/templates/app/views/layouts/welcome.html.erb.tt +5 -1
- 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/invites/concerns/invite_token.rb +11 -3
- data/lib/plutonium/invites/concerns/invite_user.rb +13 -4
- data/lib/plutonium/invites/controller.rb +14 -1
- data/lib/plutonium/invites/pending_invite_check.rb +37 -28
- 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/interactive_actions.rb +13 -9
- data/lib/plutonium/resource/controllers/presentable.rb +11 -2
- data/lib/plutonium/resource/definition.rb +42 -0
- data/lib/plutonium/resource/policy.rb +23 -8
- 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/flatpickr_controller.js +23 -0
- 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/sidebar_controller.js +28 -1
- 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 +33 -3
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Connects to data-controller="icon-rail-flyout"
|
|
4
|
+
// Manages a flyout panel anchored to a trigger element.
|
|
5
|
+
// - Hover or focus the wrapper → open
|
|
6
|
+
// - Mouse leave (with delay) or blur → close
|
|
7
|
+
// - Esc → close immediately
|
|
8
|
+
// - Click trigger → toggle (touch-friendly)
|
|
9
|
+
//
|
|
10
|
+
// On open, the panel is portaled to <body> so it escapes any ancestor
|
|
11
|
+
// transform / overflow:hidden (the rail aside has both). On close,
|
|
12
|
+
// the panel returns to its original parent.
|
|
13
|
+
//
|
|
14
|
+
// IMPORTANT: once portaled, the panel is OUTSIDE the controller's
|
|
15
|
+
// element scope, so this.panelTarget stops resolving. We capture the
|
|
16
|
+
// node into _panel before moving it.
|
|
17
|
+
export default class extends Controller {
|
|
18
|
+
static targets = ["trigger", "panel"]
|
|
19
|
+
static values = {
|
|
20
|
+
closeDelay: { type: Number, default: 150 }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
connect() {
|
|
24
|
+
this._closeTimer = null
|
|
25
|
+
this._open = false
|
|
26
|
+
this._panel = null
|
|
27
|
+
this._panelHome = null
|
|
28
|
+
this._onPanelEnter = () => {
|
|
29
|
+
if (this._closeTimer) {
|
|
30
|
+
clearTimeout(this._closeTimer)
|
|
31
|
+
this._closeTimer = null
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
this._onPanelLeave = () => this.scheduleClose()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
disconnect() {
|
|
38
|
+
this._returnPanel()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
open() {
|
|
42
|
+
if (this._closeTimer) {
|
|
43
|
+
clearTimeout(this._closeTimer)
|
|
44
|
+
this._closeTimer = null
|
|
45
|
+
}
|
|
46
|
+
if (this._open) return
|
|
47
|
+
if (!this._panel && !this.hasPanelTarget) return
|
|
48
|
+
this._open = true
|
|
49
|
+
this.element.dataset.flyoutOpen = "true"
|
|
50
|
+
this._portalPanel()
|
|
51
|
+
this._position()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
scheduleClose() {
|
|
55
|
+
if (this._closeTimer) clearTimeout(this._closeTimer)
|
|
56
|
+
this._closeTimer = setTimeout(() => this.close(), this.closeDelayValue)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
close() {
|
|
60
|
+
if (!this._open) return
|
|
61
|
+
this._open = false
|
|
62
|
+
delete this.element.dataset.flyoutOpen
|
|
63
|
+
this._returnPanel()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
toggle(event) {
|
|
67
|
+
event.preventDefault()
|
|
68
|
+
this._open ? this.close() : this.open()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
closeOnEsc(event) {
|
|
72
|
+
if (event.key === "Escape") this.close()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_portalPanel() {
|
|
76
|
+
if (this._panel) return
|
|
77
|
+
// Capture the panel BEFORE moving it — once it leaves the
|
|
78
|
+
// controller element, this.panelTarget no longer resolves.
|
|
79
|
+
const panel = this.panelTarget
|
|
80
|
+
if (!panel) return
|
|
81
|
+
this._panel = panel
|
|
82
|
+
this._panelHome = panel.parentElement
|
|
83
|
+
panel.addEventListener("mouseenter", this._onPanelEnter)
|
|
84
|
+
panel.addEventListener("mouseleave", this._onPanelLeave)
|
|
85
|
+
document.body.appendChild(panel)
|
|
86
|
+
panel.style.display = "block"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
_returnPanel() {
|
|
90
|
+
if (!this._panel) return
|
|
91
|
+
const panel = this._panel
|
|
92
|
+
panel.removeEventListener("mouseenter", this._onPanelEnter)
|
|
93
|
+
panel.removeEventListener("mouseleave", this._onPanelLeave)
|
|
94
|
+
panel.style.position = ""
|
|
95
|
+
panel.style.left = ""
|
|
96
|
+
panel.style.top = ""
|
|
97
|
+
panel.style.display = ""
|
|
98
|
+
// If the original parent has been morphed away, the panel would
|
|
99
|
+
// orphan in <body>. Drop it instead of re-attaching to a detached
|
|
100
|
+
// home node.
|
|
101
|
+
if (this._panelHome && document.contains(this._panelHome)) {
|
|
102
|
+
this._panelHome.appendChild(panel)
|
|
103
|
+
} else {
|
|
104
|
+
panel.remove()
|
|
105
|
+
}
|
|
106
|
+
this._panel = null
|
|
107
|
+
this._panelHome = null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_position() {
|
|
111
|
+
if (!this._panel || !this.hasTriggerTarget) return
|
|
112
|
+
const panel = this._panel
|
|
113
|
+
const triggerRect = this.triggerTarget.getBoundingClientRect()
|
|
114
|
+
panel.style.position = "fixed"
|
|
115
|
+
panel.style.left = `${triggerRect.right + 4}px`
|
|
116
|
+
panel.style.top = `${triggerRect.top}px`
|
|
117
|
+
|
|
118
|
+
// Shift up if the panel would overflow the viewport bottom.
|
|
119
|
+
requestAnimationFrame(() => {
|
|
120
|
+
const panelRect = panel.getBoundingClientRect()
|
|
121
|
+
const viewportH = window.innerHeight
|
|
122
|
+
if (panelRect.bottom > viewportH - 8) {
|
|
123
|
+
const overflow = panelRect.bottom - (viewportH - 8)
|
|
124
|
+
panel.style.top = `${parseFloat(panel.style.top) - overflow}px`
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -24,6 +24,14 @@ import BulkActionsController from "./bulk_actions_controller.js"
|
|
|
24
24
|
import FilterPanelController from "./filter_panel_controller.js"
|
|
25
25
|
import TextareaAutogrowController from "./textarea_autogrow_controller.js"
|
|
26
26
|
import ClipboardController from "./clipboard_controller.js"
|
|
27
|
+
import IconRailController from "./icon_rail_controller.js"
|
|
28
|
+
import IconRailFlyoutController from "./icon_rail_flyout_controller.js"
|
|
29
|
+
import TableHeaderController from "./table_header_controller.js"
|
|
30
|
+
import TableColumnMenuController from "./table_column_menu_controller.js"
|
|
31
|
+
import CaptureUrlController from "./capture_url_controller.js"
|
|
32
|
+
import RowClickController from "./row_click_controller.js"
|
|
33
|
+
import ViewSwitcherController from "./view_switcher_controller.js"
|
|
34
|
+
import AutosubmitController from "./autosubmit_controller.js"
|
|
27
35
|
|
|
28
36
|
export default function (application) {
|
|
29
37
|
// Register controllers here
|
|
@@ -52,4 +60,12 @@ export default function (application) {
|
|
|
52
60
|
application.register("filter-panel", FilterPanelController)
|
|
53
61
|
application.register("textarea-autogrow", TextareaAutogrowController)
|
|
54
62
|
application.register("clipboard", ClipboardController)
|
|
63
|
+
application.register("icon-rail", IconRailController)
|
|
64
|
+
application.register("icon-rail-flyout", IconRailFlyoutController)
|
|
65
|
+
application.register("table-header", TableHeaderController)
|
|
66
|
+
application.register("table-column-menu", TableColumnMenuController)
|
|
67
|
+
application.register("capture-url", CaptureUrlController)
|
|
68
|
+
application.register("row-click", RowClickController)
|
|
69
|
+
application.register("view-switcher", ViewSwitcherController)
|
|
70
|
+
application.register("autosubmit", AutosubmitController)
|
|
55
71
|
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
|
|
3
3
|
// Connects to data-controller="resource-tab-list"
|
|
4
|
+
//
|
|
5
|
+
// URL hash sync:
|
|
6
|
+
// - On connect, if location.hash matches a tab identifier, that tab is
|
|
7
|
+
// selected (overrides defaultTabValue). Falls back to defaultTabValue
|
|
8
|
+
// or the first tab.
|
|
9
|
+
// - On user click, the URL hash is updated via history.replaceState
|
|
10
|
+
// (no scroll, no back-button entry).
|
|
11
|
+
// - On hashchange (back/forward, manual hash edit), the matching tab
|
|
12
|
+
// is re-selected.
|
|
4
13
|
export default class extends Controller {
|
|
5
14
|
static targets = ["btn", "tab"]
|
|
6
15
|
static values = {
|
|
@@ -13,14 +22,37 @@ export default class extends Controller {
|
|
|
13
22
|
this.activeClasses = this.hasActiveClassesValue ? this.activeClassesValue.split(" ") : []
|
|
14
23
|
this.inActiveClasses = this.hasInActiveClassesValue ? this.inActiveClassesValue.split(" ") : []
|
|
15
24
|
|
|
16
|
-
this.#
|
|
25
|
+
const fromHash = this.#buttonIdFromHash()
|
|
26
|
+
const initialId = fromHash || this.defaultTabValue || this.btnTargets[0]?.id
|
|
27
|
+
this.#selectInternal(initialId, { skipFocus: true, skipHashUpdate: true })
|
|
28
|
+
|
|
29
|
+
this._syncFromHash = this._syncFromHash.bind(this)
|
|
30
|
+
// hashchange covers manual hash edits and back/forward.
|
|
31
|
+
window.addEventListener("hashchange", this._syncFromHash)
|
|
32
|
+
// turbo:load covers Turbo navigations (including morph) where the URL
|
|
33
|
+
// changes via pushState — which doesn't fire hashchange. Without this,
|
|
34
|
+
// a second submit landing via morph leaves the previously-active tab
|
|
35
|
+
// selected even though the URL hash points elsewhere.
|
|
36
|
+
document.addEventListener("turbo:load", this._syncFromHash)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
disconnect() {
|
|
40
|
+
if (this._syncFromHash) {
|
|
41
|
+
window.removeEventListener("hashchange", this._syncFromHash)
|
|
42
|
+
document.removeEventListener("turbo:load", this._syncFromHash)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_syncFromHash() {
|
|
47
|
+
const id = this.#buttonIdFromHash()
|
|
48
|
+
if (id) this.#selectInternal(id, { skipFocus: true, skipHashUpdate: true })
|
|
17
49
|
}
|
|
18
50
|
|
|
19
51
|
select(event) {
|
|
20
52
|
this.#selectInternal(event.currentTarget.id)
|
|
21
53
|
}
|
|
22
54
|
|
|
23
|
-
#selectInternal(id) {
|
|
55
|
+
#selectInternal(id, options = {}) {
|
|
24
56
|
const selectedBtn = this.btnTargets.find(element => element.id === id)
|
|
25
57
|
if (!selectedBtn) {
|
|
26
58
|
console.error(`Tab Button with id "${id}" not found`)
|
|
@@ -56,9 +88,30 @@ export default class extends Controller {
|
|
|
56
88
|
selectedTab.hidden = false
|
|
57
89
|
selectedTab.setAttribute('aria-hidden', 'false')
|
|
58
90
|
|
|
91
|
+
// Sync URL hash so the tab is shareable / restorable on reload.
|
|
92
|
+
if (!options.skipHashUpdate) this.#updateHash(id)
|
|
93
|
+
|
|
59
94
|
// Focus management
|
|
60
|
-
if (selectedBtn !== document.activeElement) {
|
|
95
|
+
if (!options.skipFocus && selectedBtn !== document.activeElement) {
|
|
61
96
|
selectedBtn.focus()
|
|
62
97
|
}
|
|
63
98
|
}
|
|
99
|
+
|
|
100
|
+
// Button ids follow `${identifier}-tab`. The URL hash carries just
|
|
101
|
+
// the identifier (e.g., #details, #orders).
|
|
102
|
+
#buttonIdFromHash() {
|
|
103
|
+
const hash = window.location.hash.replace(/^#/, "")
|
|
104
|
+
if (!hash) return null
|
|
105
|
+
const candidateId = `${hash}-tab`
|
|
106
|
+
const exists = this.btnTargets.some(btn => btn.id === candidateId)
|
|
107
|
+
return exists ? candidateId : null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#updateHash(buttonId) {
|
|
111
|
+
const identifier = buttonId.replace(/-tab$/, "")
|
|
112
|
+
const newHash = `#${identifier}`
|
|
113
|
+
if (window.location.hash !== newHash) {
|
|
114
|
+
history.replaceState(null, "", newHash)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
64
117
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Connects to data-controller="row-click"
|
|
4
|
+
//
|
|
5
|
+
// Makes a table row or grid card behave like the resource's Show
|
|
6
|
+
// affordance: clicking anywhere except a real interactive element
|
|
7
|
+
// triggers the row's existing Show button. Single source of truth for
|
|
8
|
+
// the URL — turbo-frame targets, modal opening, and any other
|
|
9
|
+
// configuration on the Show action are inherited automatically.
|
|
10
|
+
//
|
|
11
|
+
// Mark the show element with `data-row-click-target="show"`.
|
|
12
|
+
// Opt out of triggering for a specific element with
|
|
13
|
+
// `data-row-click-ignore`.
|
|
14
|
+
export default class extends Controller {
|
|
15
|
+
click(event) {
|
|
16
|
+
if (event.target.closest("a, button, input, label, select, textarea, [data-row-click-ignore]")) {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
this.element.querySelector('[data-row-click-target="show"]')?.click()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -1,3 +1,30 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Persists across controller reconnects so the value saved on
|
|
4
|
+
// turbo:before-render is still available on turbo:render, even though
|
|
5
|
+
// the <aside> hosting this controller is replaced during navigation.
|
|
6
|
+
let savedScrollTop = 0;
|
|
7
|
+
|
|
8
|
+
export default class extends Controller {
|
|
9
|
+
static targets = ["scroll"];
|
|
10
|
+
|
|
11
|
+
connect() {
|
|
12
|
+
this.beforeRender = this.beforeRender.bind(this);
|
|
13
|
+
this.afterRender = this.afterRender.bind(this);
|
|
14
|
+
document.addEventListener("turbo:before-render", this.beforeRender);
|
|
15
|
+
document.addEventListener("turbo:render", this.afterRender);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
disconnect() {
|
|
19
|
+
document.removeEventListener("turbo:before-render", this.beforeRender);
|
|
20
|
+
document.removeEventListener("turbo:render", this.afterRender);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
beforeRender() {
|
|
24
|
+
if (this.hasScrollTarget) savedScrollTop = this.scrollTarget.scrollTop;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
afterRender() {
|
|
28
|
+
if (this.hasScrollTarget) this.scrollTarget.scrollTop = savedScrollTop;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Connects to data-controller="table-column-menu"
|
|
4
|
+
// Toggles the column ⋯ menu panel; closes on outside click and Escape.
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ["panel"]
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
this._onDocClick = this._onDocClick.bind(this)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
toggle(event) {
|
|
13
|
+
event.preventDefault()
|
|
14
|
+
event.stopPropagation()
|
|
15
|
+
if (this.hasPanelTarget) {
|
|
16
|
+
const isNowVisible = !this.panelTarget.classList.toggle("hidden")
|
|
17
|
+
if (isNowVisible) {
|
|
18
|
+
document.addEventListener("click", this._onDocClick)
|
|
19
|
+
this._onKey = (e) => { if (e.key === "Escape") this._close() }
|
|
20
|
+
document.addEventListener("keydown", this._onKey)
|
|
21
|
+
} else {
|
|
22
|
+
this._unbind()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_close() {
|
|
28
|
+
if (this.hasPanelTarget) this.panelTarget.classList.add("hidden")
|
|
29
|
+
this._unbind()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_unbind() {
|
|
33
|
+
document.removeEventListener("click", this._onDocClick)
|
|
34
|
+
if (this._onKey) {
|
|
35
|
+
document.removeEventListener("keydown", this._onKey)
|
|
36
|
+
this._onKey = null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_onDocClick(event) {
|
|
41
|
+
if (!this.element.contains(event.target)) this._close()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Connects to data-controller="table-header"
|
|
4
|
+
// Routes column-header clicks: shift-click navigates to multi-href instead of href,
|
|
5
|
+
// adding the column to the existing sort stack (multi-sort).
|
|
6
|
+
// Plain click lets the default link navigation happen, which replaces all sorts.
|
|
7
|
+
export default class extends Controller {
|
|
8
|
+
headerClick(event) {
|
|
9
|
+
if (!event.shiftKey) return // plain click: let the link navigate normally
|
|
10
|
+
const link = event.currentTarget
|
|
11
|
+
const multiHref = link.dataset.tableHeaderMultiHref
|
|
12
|
+
if (!multiHref) return
|
|
13
|
+
event.preventDefault()
|
|
14
|
+
Turbo.visit(multiHref)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Connects to data-controller="view-switcher"
|
|
4
|
+
// Persists the user's chosen index view in a cookie so the server can
|
|
5
|
+
// render the right shape on next request. Reload after writing so the
|
|
6
|
+
// page comes back with everything (toolbar, filters, table/grid)
|
|
7
|
+
// matching the new view.
|
|
8
|
+
export default class extends Controller {
|
|
9
|
+
static values = { cookieName: String, cookiePath: { type: String, default: "/" } }
|
|
10
|
+
|
|
11
|
+
select(event) {
|
|
12
|
+
const view = event.params.view
|
|
13
|
+
if (!view || !this.cookieNameValue) return
|
|
14
|
+
|
|
15
|
+
// 1 year, scoped to the portal's mount path so different portals
|
|
16
|
+
// can hold different view preferences for the same resource.
|
|
17
|
+
// SameSite=Lax keeps it on top-level navigations but blocks
|
|
18
|
+
// cross-site requests from carrying it along.
|
|
19
|
+
const maxAge = 60 * 60 * 24 * 365
|
|
20
|
+
const path = this.cookiePathValue || "/"
|
|
21
|
+
document.cookie = `${this.cookieNameValue}=${encodeURIComponent(view)}; Path=${path}; Max-Age=${maxAge}; SameSite=Lax`
|
|
22
|
+
|
|
23
|
+
// Strip any legacy `?view=` param so the cookie is the source of
|
|
24
|
+
// truth from now on.
|
|
25
|
+
const url = new URL(window.location.href)
|
|
26
|
+
url.searchParams.delete("view")
|
|
27
|
+
window.location.href = url.toString()
|
|
28
|
+
}
|
|
29
|
+
}
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: plutonium
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.50.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stefan Froelich
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-05-
|
|
10
|
+
date: 2026-05-11 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: zeitwerk
|
|
@@ -480,6 +480,7 @@ files:
|
|
|
480
480
|
- app/views/resource/_resource_details.html.erb
|
|
481
481
|
- app/views/resource/_resource_details.rabl
|
|
482
482
|
- app/views/resource/_resource_form.html.erb
|
|
483
|
+
- app/views/resource/_resource_grid.html.erb
|
|
483
484
|
- app/views/resource/_resource_table.html.erb
|
|
484
485
|
- app/views/resource/edit.html.erb
|
|
485
486
|
- app/views/resource/errors.rabl
|
|
@@ -610,8 +611,13 @@ files:
|
|
|
610
611
|
- docs/superpowers/plans/2026-04-08-plutonium-skills-overhaul.md
|
|
611
612
|
- docs/superpowers/plans/2026-04-14-plutonium-testing.md
|
|
612
613
|
- docs/superpowers/plans/2026-04-14-plutonium-testing.md.tasks.json
|
|
614
|
+
- docs/superpowers/plans/2026-05-06-multi-invite-model-support.md
|
|
615
|
+
- docs/superpowers/plans/2026-05-06-multi-invite-model-support.md.tasks.json
|
|
616
|
+
- docs/superpowers/plans/2026-05-07-ui-layout-overhaul.md
|
|
617
|
+
- docs/superpowers/plans/2026-05-07-ui-layout-overhaul.md.tasks.json
|
|
613
618
|
- docs/superpowers/specs/2026-04-08-plutonium-skills-overhaul-design.md
|
|
614
619
|
- docs/superpowers/specs/2026-04-14-plutonium-testing-design.md
|
|
620
|
+
- docs/superpowers/specs/2026-05-07-ui-layout-overhaul-design.md
|
|
615
621
|
- esbuild.config.js
|
|
616
622
|
- exe/pug
|
|
617
623
|
- gemfiles/rails_7.gemfile
|
|
@@ -907,11 +913,13 @@ files:
|
|
|
907
913
|
- lib/plutonium/definition/config_attr.rb
|
|
908
914
|
- lib/plutonium/definition/defineable_props.rb
|
|
909
915
|
- lib/plutonium/definition/inheritable_config_attr.rb
|
|
916
|
+
- lib/plutonium/definition/metadata.rb
|
|
910
917
|
- lib/plutonium/definition/nested_inputs.rb
|
|
911
918
|
- lib/plutonium/definition/presentable.rb
|
|
912
919
|
- lib/plutonium/definition/scoping.rb
|
|
913
920
|
- lib/plutonium/definition/search.rb
|
|
914
921
|
- lib/plutonium/definition/sorting.rb
|
|
922
|
+
- lib/plutonium/definition/views.rb
|
|
915
923
|
- lib/plutonium/engine.rb
|
|
916
924
|
- lib/plutonium/engine/validator.rb
|
|
917
925
|
- lib/plutonium/helpers.rb
|
|
@@ -1029,11 +1037,13 @@ files:
|
|
|
1029
1037
|
- lib/plutonium/ui/form/base.rb
|
|
1030
1038
|
- lib/plutonium/ui/form/components/easymde.rb
|
|
1031
1039
|
- lib/plutonium/ui/form/components/flatpickr.rb
|
|
1040
|
+
- lib/plutonium/ui/form/components/hidden_wrapper.rb
|
|
1032
1041
|
- lib/plutonium/ui/form/components/intl_tel_input.rb
|
|
1033
1042
|
- lib/plutonium/ui/form/components/key_value_store.rb
|
|
1034
1043
|
- lib/plutonium/ui/form/components/resource_select.rb
|
|
1035
1044
|
- lib/plutonium/ui/form/components/secure_association.rb
|
|
1036
1045
|
- lib/plutonium/ui/form/components/secure_polymorphic_association.rb
|
|
1046
|
+
- lib/plutonium/ui/form/components/sticky_footer.rb
|
|
1037
1047
|
- lib/plutonium/ui/form/components/uppy.rb
|
|
1038
1048
|
- lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb
|
|
1039
1049
|
- lib/plutonium/ui/form/interaction.rb
|
|
@@ -1042,12 +1052,19 @@ files:
|
|
|
1042
1052
|
- lib/plutonium/ui/form/resource.rb
|
|
1043
1053
|
- lib/plutonium/ui/form/theme.rb
|
|
1044
1054
|
- lib/plutonium/ui/frame_navigator_panel.rb
|
|
1055
|
+
- lib/plutonium/ui/grid/card.rb
|
|
1056
|
+
- lib/plutonium/ui/grid/resource.rb
|
|
1045
1057
|
- lib/plutonium/ui/layout/base.rb
|
|
1046
1058
|
- lib/plutonium/ui/layout/basic_layout.rb
|
|
1047
1059
|
- lib/plutonium/ui/layout/header.rb
|
|
1060
|
+
- lib/plutonium/ui/layout/icon_rail.rb
|
|
1048
1061
|
- lib/plutonium/ui/layout/resource_layout.rb
|
|
1049
1062
|
- lib/plutonium/ui/layout/rodauth_layout.rb
|
|
1050
1063
|
- lib/plutonium/ui/layout/sidebar.rb
|
|
1064
|
+
- lib/plutonium/ui/layout/topbar.rb
|
|
1065
|
+
- lib/plutonium/ui/modal/base.rb
|
|
1066
|
+
- lib/plutonium/ui/modal/centered.rb
|
|
1067
|
+
- lib/plutonium/ui/modal/slideover.rb
|
|
1051
1068
|
- lib/plutonium/ui/nav_grid_menu.rb
|
|
1052
1069
|
- lib/plutonium/ui/nav_user.rb
|
|
1053
1070
|
- lib/plutonium/ui/page/base.rb
|
|
@@ -1064,12 +1081,17 @@ files:
|
|
|
1064
1081
|
- lib/plutonium/ui/table/base.rb
|
|
1065
1082
|
- lib/plutonium/ui/table/components/attachment.rb
|
|
1066
1083
|
- lib/plutonium/ui/table/components/bulk_actions_toolbar.rb
|
|
1084
|
+
- lib/plutonium/ui/table/components/filter_form.rb
|
|
1085
|
+
- lib/plutonium/ui/table/components/filter_pills.rb
|
|
1067
1086
|
- lib/plutonium/ui/table/components/pagy_info.rb
|
|
1068
1087
|
- lib/plutonium/ui/table/components/pagy_pagination.rb
|
|
1069
1088
|
- lib/plutonium/ui/table/components/row_actions_dropdown.rb
|
|
1070
1089
|
- lib/plutonium/ui/table/components/scopes_bar.rb
|
|
1090
|
+
- lib/plutonium/ui/table/components/scopes_pills.rb
|
|
1071
1091
|
- lib/plutonium/ui/table/components/search_bar.rb
|
|
1072
1092
|
- lib/plutonium/ui/table/components/selection_column.rb
|
|
1093
|
+
- lib/plutonium/ui/table/components/toolbar.rb
|
|
1094
|
+
- lib/plutonium/ui/table/components/view_switcher.rb
|
|
1073
1095
|
- lib/plutonium/ui/table/display_theme.rb
|
|
1074
1096
|
- lib/plutonium/ui/table/resource.rb
|
|
1075
1097
|
- lib/plutonium/ui/table/theme.rb
|
|
@@ -1100,7 +1122,9 @@ files:
|
|
|
1100
1122
|
- src/js/controllers/attachment_input_controller.js
|
|
1101
1123
|
- src/js/controllers/attachment_preview_container_controller.js
|
|
1102
1124
|
- src/js/controllers/attachment_preview_controller.js
|
|
1125
|
+
- src/js/controllers/autosubmit_controller.js
|
|
1103
1126
|
- src/js/controllers/bulk_actions_controller.js
|
|
1127
|
+
- src/js/controllers/capture_url_controller.js
|
|
1104
1128
|
- src/js/controllers/clipboard_controller.js
|
|
1105
1129
|
- src/js/controllers/color_mode_controller.js
|
|
1106
1130
|
- src/js/controllers/easymde_controller.js
|
|
@@ -1108,6 +1132,8 @@ files:
|
|
|
1108
1132
|
- src/js/controllers/flatpickr_controller.js
|
|
1109
1133
|
- src/js/controllers/form_controller.js
|
|
1110
1134
|
- src/js/controllers/frame_navigator_controller.js
|
|
1135
|
+
- src/js/controllers/icon_rail_controller.js
|
|
1136
|
+
- src/js/controllers/icon_rail_flyout_controller.js
|
|
1111
1137
|
- src/js/controllers/intl_tel_input_controller.js
|
|
1112
1138
|
- src/js/controllers/key_value_store_controller.js
|
|
1113
1139
|
- src/js/controllers/nested_resource_form_fields_controller.js
|
|
@@ -1119,10 +1145,14 @@ files:
|
|
|
1119
1145
|
- src/js/controllers/resource_drop_down_controller.js
|
|
1120
1146
|
- src/js/controllers/resource_header_controller.js
|
|
1121
1147
|
- src/js/controllers/resource_tab_list_controller.js
|
|
1148
|
+
- src/js/controllers/row_click_controller.js
|
|
1122
1149
|
- src/js/controllers/select_navigator.js
|
|
1123
1150
|
- src/js/controllers/sidebar_controller.js
|
|
1124
1151
|
- src/js/controllers/slim_select_controller.js
|
|
1152
|
+
- src/js/controllers/table_column_menu_controller.js
|
|
1153
|
+
- src/js/controllers/table_header_controller.js
|
|
1125
1154
|
- src/js/controllers/textarea_autogrow_controller.js
|
|
1155
|
+
- src/js/controllers/view_switcher_controller.js
|
|
1126
1156
|
- src/js/core.js
|
|
1127
1157
|
- src/js/plutonium.js
|
|
1128
1158
|
- src/js/support/dom_element.js
|
|
@@ -1142,7 +1172,7 @@ metadata:
|
|
|
1142
1172
|
homepage_uri: https://radioactive-labs.github.io/plutonium-core/
|
|
1143
1173
|
source_code_uri: https://github.com/radioactive-labs/plutonium-core
|
|
1144
1174
|
post_install_message: |
|
|
1145
|
-
⚠️ Plutonium 0.
|
|
1175
|
+
⚠️ Plutonium 0.50.0 — breaking change
|
|
1146
1176
|
|
|
1147
1177
|
Entity-scoped URL helpers and path params have been renamed from
|
|
1148
1178
|
`<entity>_scope_*` to `<entity>_scoped_*`.
|