maquina-components 0.5.0 → 0.5.1
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/README.md +3 -1
- data/app/assets/stylesheets/breadcrumbs.css +21 -0
- data/app/assets/stylesheets/combobox.css +36 -1
- data/app/assets/stylesheets/drawer.css +15 -0
- data/app/assets/stylesheets/menu_button.css +155 -0
- data/app/assets/stylesheets/separator.css +20 -0
- data/app/assets/stylesheets/sidebar.css +12 -0
- data/app/assets/stylesheets/stats.css +101 -0
- data/app/assets/tailwind/maquina_components_engine/engine.css +3 -0
- data/app/helpers/maquina_components/combobox_helper.rb +2 -1
- data/app/helpers/maquina_components/components_helper.rb +33 -0
- data/app/helpers/maquina_components/icons_helper.rb +58 -0
- data/app/javascript/controllers/breadcrumb_controller.js +39 -30
- data/app/javascript/controllers/combobox_controller.js +9 -0
- data/app/javascript/controllers/drawer_controller.js +43 -9
- data/app/javascript/controllers/drawer_trigger_controller.js +33 -3
- data/app/javascript/controllers/menu_button_controller.js +107 -29
- data/app/javascript/controllers/sidebar_controller.js +6 -8
- data/app/javascript/controllers/sidebar_trigger_controller.js +33 -4
- data/app/javascript/controllers/toaster_controller.js +12 -0
- data/app/views/components/_alert.html.erb +3 -1
- data/app/views/components/_badge.html.erb +7 -1
- data/app/views/components/_breadcrumbs.html.erb +1 -1
- data/app/views/components/_calendar.html.erb +1 -1
- data/app/views/components/_card.html.erb +1 -1
- data/app/views/components/_combobox.html.erb +3 -2
- data/app/views/components/_date_picker.html.erb +4 -3
- data/app/views/components/_drawer.html.erb +8 -2
- data/app/views/components/_dropdown.html.erb +21 -24
- data/app/views/components/_dropdown_menu.html.erb +1 -1
- data/app/views/components/_empty.html.erb +1 -1
- data/app/views/components/_header.html.erb +1 -1
- data/app/views/components/_menu_button.html.erb +29 -34
- data/app/views/components/_pagination.html.erb +1 -1
- data/app/views/components/_separator.html.erb +9 -10
- data/app/views/components/_sidebar.html.erb +1 -1
- data/app/views/components/_simple_table.html.erb +3 -1
- data/app/views/components/_table.html.erb +1 -1
- data/app/views/components/_toast.html.erb +6 -1
- data/app/views/components/_toaster.html.erb +1 -1
- data/app/views/components/_toggle_group.html.erb +1 -1
- data/app/views/components/alert/_description.html.erb +1 -1
- data/app/views/components/alert/_title.html.erb +1 -1
- data/app/views/components/breadcrumbs/_ellipsis.html.erb +1 -1
- data/app/views/components/breadcrumbs/_item.html.erb +1 -1
- data/app/views/components/breadcrumbs/_link.html.erb +1 -1
- data/app/views/components/breadcrumbs/_list.html.erb +1 -1
- data/app/views/components/breadcrumbs/_page.html.erb +1 -1
- data/app/views/components/breadcrumbs/_separator.html.erb +1 -1
- data/app/views/components/calendar/_header.html.erb +1 -1
- data/app/views/components/calendar/_week.html.erb +1 -1
- data/app/views/components/card/_action.html.erb +1 -1
- data/app/views/components/card/_content.html.erb +1 -1
- data/app/views/components/card/_description.html.erb +1 -1
- data/app/views/components/card/_footer.html.erb +1 -1
- data/app/views/components/card/_header.html.erb +1 -1
- data/app/views/components/card/_title.html.erb +1 -1
- data/app/views/components/combobox/_content.html.erb +1 -1
- data/app/views/components/combobox/_empty.html.erb +3 -3
- data/app/views/components/combobox/_group.html.erb +1 -1
- data/app/views/components/combobox/_input.html.erb +1 -1
- data/app/views/components/combobox/_label.html.erb +1 -1
- data/app/views/components/combobox/_list.html.erb +1 -1
- data/app/views/components/combobox/_option.html.erb +1 -1
- data/app/views/components/combobox/_separator.html.erb +1 -1
- data/app/views/components/combobox/_trigger.html.erb +1 -1
- data/app/views/components/drawer/_close.html.erb +1 -1
- data/app/views/components/drawer/_content.html.erb +1 -1
- data/app/views/components/drawer/_footer.html.erb +1 -1
- data/app/views/components/drawer/_header.html.erb +2 -2
- data/app/views/components/drawer/_provider.html.erb +2 -2
- data/app/views/components/drawer/_trigger.html.erb +1 -1
- data/app/views/components/dropdown_menu/_content.html.erb +1 -1
- data/app/views/components/dropdown_menu/_group.html.erb +1 -1
- data/app/views/components/dropdown_menu/_item.html.erb +1 -1
- data/app/views/components/dropdown_menu/_label.html.erb +3 -7
- data/app/views/components/dropdown_menu/_separator.html.erb +1 -1
- data/app/views/components/dropdown_menu/_shortcut.html.erb +3 -7
- data/app/views/components/dropdown_menu/_trigger.html.erb +1 -1
- data/app/views/components/empty/_content.html.erb +1 -1
- data/app/views/components/empty/_description.html.erb +3 -7
- data/app/views/components/empty/_header.html.erb +1 -1
- data/app/views/components/empty/_media.html.erb +5 -3
- data/app/views/components/empty/_title.html.erb +3 -7
- data/app/views/components/pagination/_content.html.erb +1 -1
- data/app/views/components/pagination/_item.html.erb +1 -1
- data/app/views/components/pagination/_link.html.erb +1 -1
- data/app/views/components/pagination/_next.html.erb +1 -1
- data/app/views/components/pagination/_previous.html.erb +1 -1
- data/app/views/components/sidebar/_content.html.erb +1 -1
- data/app/views/components/sidebar/_footer.html.erb +1 -1
- data/app/views/components/sidebar/_group.html.erb +1 -1
- data/app/views/components/sidebar/_header.html.erb +1 -1
- data/app/views/components/sidebar/_inset.html.erb +1 -1
- data/app/views/components/sidebar/_menu.html.erb +1 -1
- data/app/views/components/sidebar/_menu_button.html.erb +1 -1
- data/app/views/components/sidebar/_menu_item.html.erb +1 -1
- data/app/views/components/sidebar/_menu_link.html.erb +4 -7
- data/app/views/components/sidebar/_provider.html.erb +1 -1
- data/app/views/components/sidebar/_trigger.html.erb +1 -1
- data/app/views/components/stats/_stats_card.html.erb +32 -92
- data/app/views/components/stats/_stats_grid.html.erb +18 -24
- data/app/views/components/table/_body.html.erb +1 -1
- data/app/views/components/table/_caption.html.erb +3 -3
- data/app/views/components/table/_cell.html.erb +3 -3
- data/app/views/components/table/_footer.html.erb +1 -1
- data/app/views/components/table/_head.html.erb +3 -3
- data/app/views/components/table/_header.html.erb +1 -1
- data/app/views/components/table/_row.html.erb +1 -1
- data/app/views/components/toast/_action.html.erb +1 -1
- data/app/views/components/toast/_description.html.erb +1 -1
- data/app/views/components/toast/_title.html.erb +1 -1
- data/app/views/components/toggle_group/_item.html.erb +1 -1
- data/lib/generators/maquina_components/install/templates/maquina_components_helper.rb.tt +1 -0
- data/lib/maquina-components.rb +5 -0
- data/lib/maquina_components/engine.rb +7 -3
- data/lib/maquina_components/version.rb +1 -1
- metadata +33 -2
- data/app/assets/stylesheets/maquina_components.css +0 -178
|
@@ -4,10 +4,15 @@ export default class extends Controller {
|
|
|
4
4
|
static targets = ["item", "ellipsis", "ellipsisSeparator"]
|
|
5
5
|
static values = { collapseAfter: { type: Number, default: 0 } }
|
|
6
6
|
|
|
7
|
+
// True when CSS anchor positioning (breadcrumbs.css) places the popover
|
|
8
|
+
static supportsAnchorPositioning =
|
|
9
|
+
typeof CSS !== "undefined" &&
|
|
10
|
+
CSS.supports("anchor-name: --a") &&
|
|
11
|
+
CSS.supports("position-area: block-end")
|
|
12
|
+
|
|
7
13
|
connect() {
|
|
8
14
|
this._dropdown = null
|
|
9
|
-
this.
|
|
10
|
-
this._escapeHandler = this._handleEscape.bind(this)
|
|
15
|
+
this._popoverToggleHandler = this._handlePopoverToggle.bind(this)
|
|
11
16
|
this._teardownHandler = this._teardown.bind(this)
|
|
12
17
|
|
|
13
18
|
document.addEventListener("turbo:before-cache", this._teardownHandler)
|
|
@@ -29,7 +34,6 @@ export default class extends Controller {
|
|
|
29
34
|
this._ellipsisTrigger = trigger
|
|
30
35
|
this._toggleHandler = this._toggleDropdown.bind(this)
|
|
31
36
|
trigger.addEventListener('click', this._toggleHandler)
|
|
32
|
-
trigger.style.cursor = 'pointer'
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
|
|
@@ -133,7 +137,7 @@ export default class extends Controller {
|
|
|
133
137
|
_toggleDropdown(event) {
|
|
134
138
|
event.stopPropagation()
|
|
135
139
|
|
|
136
|
-
if (this._dropdown && this._dropdown.
|
|
140
|
+
if (this._dropdown && this._dropdown.matches(":popover-open")) {
|
|
137
141
|
this._closeDropdown()
|
|
138
142
|
} else {
|
|
139
143
|
this._openDropdown()
|
|
@@ -144,47 +148,55 @@ export default class extends Controller {
|
|
|
144
148
|
if (!this._hiddenLinks || this._hiddenLinks.length === 0) return
|
|
145
149
|
|
|
146
150
|
if (!this._dropdown) {
|
|
151
|
+
// popover="auto" gives top-layer rendering, light dismiss, and
|
|
152
|
+
// Escape-to-close natively — no document listeners needed
|
|
147
153
|
this._dropdown = document.createElement('div')
|
|
148
154
|
this._dropdown.setAttribute('role', 'menu')
|
|
155
|
+
this._dropdown.setAttribute('popover', 'auto')
|
|
149
156
|
this._dropdown.dataset.dropdownMenuPart = 'content'
|
|
150
|
-
this._dropdown.
|
|
151
|
-
this._dropdown.
|
|
152
|
-
|
|
157
|
+
this._dropdown.dataset.breadcrumbPart = 'dropdown'
|
|
158
|
+
this._dropdown.addEventListener('beforetoggle', this._popoverToggleHandler)
|
|
159
|
+
// Next to the trigger so CSS anchor positioning can resolve it
|
|
160
|
+
this._ellipsisTrigger.insertAdjacentElement('afterend', this._dropdown)
|
|
153
161
|
}
|
|
154
162
|
|
|
155
163
|
this._buildDropdownContent()
|
|
156
164
|
this._positionDropdown()
|
|
157
|
-
this._dropdown.
|
|
158
|
-
|
|
159
|
-
if (this._ellipsisTrigger) {
|
|
160
|
-
this._ellipsisTrigger.dataset.state = 'open'
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Defer listeners so the current click doesn't immediately close
|
|
164
|
-
requestAnimationFrame(() => {
|
|
165
|
-
document.addEventListener('click', this._clickOutsideHandler)
|
|
166
|
-
document.addEventListener('keydown', this._escapeHandler)
|
|
167
|
-
})
|
|
165
|
+
this._dropdown.showPopover()
|
|
168
166
|
}
|
|
169
167
|
|
|
170
168
|
_closeDropdown() {
|
|
171
|
-
if (this._dropdown) {
|
|
172
|
-
this._dropdown.
|
|
169
|
+
if (this._dropdown && this._dropdown.matches(":popover-open")) {
|
|
170
|
+
this._dropdown.hidePopover()
|
|
173
171
|
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Mirrors the native popover state onto the data attributes the CSS
|
|
175
|
+
// animations and the trigger styling key on
|
|
176
|
+
_handlePopoverToggle(event) {
|
|
177
|
+
const open = event.newState === 'open'
|
|
178
|
+
|
|
179
|
+
this._dropdown.dataset.state = open ? 'open' : 'closed'
|
|
180
|
+
|
|
174
181
|
if (this._ellipsisTrigger) {
|
|
175
|
-
|
|
182
|
+
if (open) {
|
|
183
|
+
this._ellipsisTrigger.dataset.state = 'open'
|
|
184
|
+
} else {
|
|
185
|
+
delete this._ellipsisTrigger.dataset.state
|
|
186
|
+
}
|
|
176
187
|
}
|
|
177
|
-
|
|
178
|
-
document.removeEventListener('click', this._clickOutsideHandler)
|
|
179
|
-
document.removeEventListener('keydown', this._escapeHandler)
|
|
180
188
|
}
|
|
181
189
|
|
|
182
190
|
_removeDropdown() {
|
|
183
191
|
this._closeDropdown()
|
|
184
192
|
if (this._dropdown) {
|
|
193
|
+
this._dropdown.removeEventListener('beforetoggle', this._popoverToggleHandler)
|
|
185
194
|
this._dropdown.remove()
|
|
186
195
|
this._dropdown = null
|
|
187
196
|
}
|
|
197
|
+
if (this._ellipsisTrigger) {
|
|
198
|
+
delete this._ellipsisTrigger.dataset.state
|
|
199
|
+
}
|
|
188
200
|
}
|
|
189
201
|
|
|
190
202
|
_buildDropdownContent() {
|
|
@@ -203,19 +215,16 @@ export default class extends Controller {
|
|
|
203
215
|
}
|
|
204
216
|
|
|
205
217
|
_positionDropdown() {
|
|
218
|
+
// Modern browsers position the popover declaratively via anchor CSS
|
|
219
|
+
if (this.constructor.supportsAnchorPositioning) return
|
|
206
220
|
if (!this._dropdown || !this._ellipsisTrigger) return
|
|
207
221
|
|
|
208
222
|
const rect = this._ellipsisTrigger.getBoundingClientRect()
|
|
223
|
+
this._dropdown.style.position = 'fixed'
|
|
209
224
|
this._dropdown.style.top = `${rect.bottom + 4}px`
|
|
210
225
|
this._dropdown.style.left = `${rect.left}px`
|
|
211
226
|
}
|
|
212
227
|
|
|
213
|
-
_handleEscape(event) {
|
|
214
|
-
if (event.key === 'Escape') {
|
|
215
|
-
this._closeDropdown()
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
228
|
_teardown() {
|
|
220
229
|
this._removeDropdown()
|
|
221
230
|
this._hiddenLinks = null
|
|
@@ -72,7 +72,16 @@ export default class extends Controller {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// True when CSS anchor positioning (combobox.css) places the popover
|
|
76
|
+
static supportsAnchorPositioning =
|
|
77
|
+
typeof CSS !== "undefined" &&
|
|
78
|
+
CSS.supports("anchor-name: --a") &&
|
|
79
|
+
CSS.supports("position-area: block-end")
|
|
80
|
+
|
|
75
81
|
positionPopover() {
|
|
82
|
+
// Modern browsers position the popover declaratively via anchor CSS
|
|
83
|
+
if (this.constructor.supportsAnchorPositioning) return
|
|
84
|
+
|
|
76
85
|
if (!this.hasTriggerTarget || !this.hasContentTarget) return
|
|
77
86
|
|
|
78
87
|
const trigger = this.triggerTarget
|
|
@@ -20,7 +20,7 @@ export default class extends Controller {
|
|
|
20
20
|
keyboardShortcut: { type: String, default: "d" }
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
static targets = ["drawer", "container", "backdrop"]
|
|
23
|
+
static targets = ["drawer", "container", "backdrop", "panel"]
|
|
24
24
|
|
|
25
25
|
initialize() {
|
|
26
26
|
const cookieValue = this.getCookie(this.cookieNameValue)
|
|
@@ -96,6 +96,12 @@ export default class extends Controller {
|
|
|
96
96
|
this.toggle()
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
closeOnEscape(event) {
|
|
100
|
+
if (this.openValue) {
|
|
101
|
+
this.close()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
99
105
|
// ============================================================================
|
|
100
106
|
// State Management
|
|
101
107
|
// ============================================================================
|
|
@@ -107,6 +113,25 @@ export default class extends Controller {
|
|
|
107
113
|
this.updateState()
|
|
108
114
|
this.persistState()
|
|
109
115
|
this.dispatchStateChange()
|
|
116
|
+
|
|
117
|
+
// Only manage focus on user-driven changes, not the initial sync
|
|
118
|
+
if (old_value !== undefined) {
|
|
119
|
+
this.manageFocus(new_value)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
manageFocus(isOpen) {
|
|
124
|
+
if (!this.hasPanelTarget) return
|
|
125
|
+
|
|
126
|
+
if (isOpen) {
|
|
127
|
+
this._previouslyFocused = document.activeElement
|
|
128
|
+
this.panelTarget.focus()
|
|
129
|
+
} else {
|
|
130
|
+
if (this._previouslyFocused?.isConnected) {
|
|
131
|
+
this._previouslyFocused.focus()
|
|
132
|
+
}
|
|
133
|
+
this._previouslyFocused = null
|
|
134
|
+
}
|
|
110
135
|
}
|
|
111
136
|
|
|
112
137
|
updateState() {
|
|
@@ -132,6 +157,17 @@ export default class extends Controller {
|
|
|
132
157
|
}
|
|
133
158
|
}
|
|
134
159
|
|
|
160
|
+
if (this.hasPanelTarget) {
|
|
161
|
+
// The closed panel stays in the DOM: hide it from AT and block focus
|
|
162
|
+
if (isOpen) {
|
|
163
|
+
this.panelTarget.removeAttribute("aria-hidden")
|
|
164
|
+
this.panelTarget.removeAttribute("inert")
|
|
165
|
+
} else {
|
|
166
|
+
this.panelTarget.setAttribute("aria-hidden", "true")
|
|
167
|
+
this.panelTarget.setAttribute("inert", "")
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
135
171
|
if (isOpen) {
|
|
136
172
|
this.lockScroll()
|
|
137
173
|
} else {
|
|
@@ -200,19 +236,17 @@ export default class extends Controller {
|
|
|
200
236
|
// Scroll Lock
|
|
201
237
|
// ============================================================================
|
|
202
238
|
|
|
239
|
+
// Styling lives in drawer.css (body[data-maquina-scroll-locked]); the
|
|
240
|
+
// controller only records the scroll offset and flips the attribute
|
|
203
241
|
lockScroll() {
|
|
204
242
|
this.scrollPosition = window.pageYOffset
|
|
205
|
-
document.body.style.
|
|
206
|
-
document.body.
|
|
207
|
-
document.body.style.top = `-${this.scrollPosition}px`
|
|
208
|
-
document.body.style.width = "100%"
|
|
243
|
+
document.body.style.setProperty("--maquina-scroll-lock-top", `-${this.scrollPosition}px`)
|
|
244
|
+
document.body.setAttribute("data-maquina-scroll-locked", "")
|
|
209
245
|
}
|
|
210
246
|
|
|
211
247
|
unlockScroll() {
|
|
212
|
-
document.body.
|
|
213
|
-
document.body.style.
|
|
214
|
-
document.body.style.top = ""
|
|
215
|
-
document.body.style.width = ""
|
|
248
|
+
document.body.removeAttribute("data-maquina-scroll-locked")
|
|
249
|
+
document.body.style.removeProperty("--maquina-scroll-lock-top")
|
|
216
250
|
if (this.scrollPosition !== undefined) {
|
|
217
251
|
window.scrollTo(0, this.scrollPosition)
|
|
218
252
|
}
|
|
@@ -3,11 +3,12 @@ import { Controller } from "@hotwired/stimulus";
|
|
|
3
3
|
/**
|
|
4
4
|
* Drawer Trigger Controller
|
|
5
5
|
*
|
|
6
|
-
* Triggers drawer toggle via Stimulus outlets
|
|
6
|
+
* Triggers drawer toggle via Stimulus outlets and mirrors the drawer's
|
|
7
|
+
* state on the trigger (aria-expanded, aria-controls).
|
|
7
8
|
* Can be placed anywhere on the page - finds drawer via outlet selector.
|
|
8
9
|
*
|
|
9
10
|
* @example
|
|
10
|
-
* <button
|
|
11
|
+
* <button
|
|
11
12
|
* data-controller="drawer-trigger"
|
|
12
13
|
* data-action="click->drawer-trigger#triggerClick"
|
|
13
14
|
* data-drawer-trigger-drawer-outlet="[data-outlet='drawer']"
|
|
@@ -18,6 +19,24 @@ import { Controller } from "@hotwired/stimulus";
|
|
|
18
19
|
export default class extends Controller {
|
|
19
20
|
static outlets = ["drawer"]
|
|
20
21
|
|
|
22
|
+
connect() {
|
|
23
|
+
this.handleStateChanged = this.handleStateChanged.bind(this)
|
|
24
|
+
window.addEventListener("drawer:stateChanged", this.handleStateChanged)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
disconnect() {
|
|
28
|
+
window.removeEventListener("drawer:stateChanged", this.handleStateChanged)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
drawerOutletConnected(outlet) {
|
|
32
|
+
this.element.setAttribute("aria-haspopup", "dialog")
|
|
33
|
+
this.syncExpanded(outlet.openValue)
|
|
34
|
+
|
|
35
|
+
if (outlet.hasPanelTarget && outlet.panelTarget.id) {
|
|
36
|
+
this.element.setAttribute("aria-controls", outlet.panelTarget.id)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
21
40
|
triggerClick() {
|
|
22
41
|
if (this.hasDrawerOutlet) {
|
|
23
42
|
this.drawerOutlets.forEach(outlet => {
|
|
@@ -25,4 +44,15 @@ export default class extends Controller {
|
|
|
25
44
|
})
|
|
26
45
|
}
|
|
27
46
|
}
|
|
28
|
-
|
|
47
|
+
|
|
48
|
+
handleStateChanged(event) {
|
|
49
|
+
if (!this.hasDrawerOutlet) return
|
|
50
|
+
if (!this.drawerOutletElements.includes(event.target)) return
|
|
51
|
+
|
|
52
|
+
this.syncExpanded(event.detail.open)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
syncExpanded(open) {
|
|
56
|
+
this.element.setAttribute("aria-expanded", open ? "true" : "false")
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -1,59 +1,137 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* MenuButton Controller
|
|
5
|
+
*
|
|
6
|
+
* Sidebar-flavored button that toggles a dropdown panel with:
|
|
7
|
+
* - Click to toggle
|
|
8
|
+
* - Click outside to close
|
|
9
|
+
* - Escape key to close
|
|
10
|
+
* - aria-expanded state on the trigger
|
|
11
|
+
* - Animation states via data-state
|
|
12
|
+
*/
|
|
3
13
|
export default class extends Controller {
|
|
4
14
|
static targets = ["button", "content"]
|
|
5
15
|
|
|
16
|
+
static values = {
|
|
17
|
+
open: { type: Boolean, default: false }
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
connect() {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
21
|
+
this.handleClickOutside = this.handleClickOutside.bind(this)
|
|
22
|
+
this.handleKeydown = this.handleKeydown.bind(this)
|
|
23
|
+
this.boundTeardown = this.teardown.bind(this)
|
|
10
24
|
|
|
11
|
-
|
|
12
|
-
this.isOpen = this.buttonTarget.dataset.state === "open"
|
|
25
|
+
document.addEventListener("turbo:before-cache", this.boundTeardown)
|
|
13
26
|
|
|
14
|
-
if (this.
|
|
15
|
-
this.
|
|
27
|
+
if (this.hasContentTarget) {
|
|
28
|
+
this.contentTarget.hidden = !this.openValue
|
|
16
29
|
}
|
|
17
30
|
}
|
|
18
31
|
|
|
19
32
|
disconnect() {
|
|
20
|
-
this.
|
|
33
|
+
this.removeEventListeners()
|
|
34
|
+
document.removeEventListener("turbo:before-cache", this.boundTeardown)
|
|
21
35
|
}
|
|
22
36
|
|
|
23
|
-
toggle() {
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
toggle(event) {
|
|
38
|
+
event?.preventDefault()
|
|
39
|
+
|
|
40
|
+
if (this.openValue) {
|
|
41
|
+
this.close()
|
|
42
|
+
} else {
|
|
43
|
+
this.open()
|
|
26
44
|
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
open() {
|
|
48
|
+
if (this.openValue || !this.hasContentTarget) return
|
|
27
49
|
|
|
50
|
+
this.openValue = true
|
|
51
|
+
this.buttonTarget.dataset.state = "open"
|
|
52
|
+
this.buttonTarget.setAttribute("aria-expanded", "true")
|
|
53
|
+
this.contentTarget.dataset.state = "open"
|
|
28
54
|
this.contentTarget.classList.remove("hidden")
|
|
55
|
+
this.contentTarget.hidden = false
|
|
29
56
|
|
|
30
|
-
this.
|
|
31
|
-
|
|
57
|
+
this.addEventListeners()
|
|
58
|
+
}
|
|
32
59
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
60
|
+
close() {
|
|
61
|
+
if (!this.openValue || !this.hasContentTarget) return
|
|
62
|
+
|
|
63
|
+
// Start closing animation, then hide once it finishes
|
|
64
|
+
this.contentTarget.dataset.state = "closing"
|
|
65
|
+
|
|
66
|
+
const animationDuration = 100 // matches CSS animation duration
|
|
67
|
+
|
|
68
|
+
this._closeTimeout = setTimeout(() => {
|
|
69
|
+
this._closeTimeout = null
|
|
70
|
+
this.openValue = false
|
|
71
|
+
this.buttonTarget.dataset.state = "closed"
|
|
72
|
+
this.buttonTarget.setAttribute("aria-expanded", "false")
|
|
73
|
+
this.contentTarget.dataset.state = "closed"
|
|
74
|
+
this.contentTarget.hidden = true
|
|
75
|
+
|
|
76
|
+
this.removeEventListeners()
|
|
77
|
+
this.buttonTarget.focus()
|
|
78
|
+
}, animationDuration)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Turbo Cache Teardown
|
|
82
|
+
|
|
83
|
+
teardown() {
|
|
84
|
+
if (this._closeTimeout) {
|
|
85
|
+
clearTimeout(this._closeTimeout)
|
|
86
|
+
this._closeTimeout = null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.openValue = false
|
|
90
|
+
|
|
91
|
+
if (this.hasButtonTarget) {
|
|
92
|
+
this.buttonTarget.dataset.state = "closed"
|
|
93
|
+
this.buttonTarget.setAttribute("aria-expanded", "false")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (this.hasContentTarget) {
|
|
97
|
+
this.contentTarget.dataset.state = "closed"
|
|
98
|
+
this.contentTarget.hidden = true
|
|
40
99
|
}
|
|
100
|
+
|
|
101
|
+
this.removeEventListeners()
|
|
41
102
|
}
|
|
42
103
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
104
|
+
// Event Handlers
|
|
105
|
+
|
|
106
|
+
handleClickOutside(event) {
|
|
107
|
+
if (!this.openValue) return
|
|
108
|
+
if (this.element.contains(event.target)) return
|
|
109
|
+
|
|
110
|
+
this.close()
|
|
111
|
+
}
|
|
46
112
|
|
|
47
|
-
|
|
48
|
-
|
|
113
|
+
handleKeydown(event) {
|
|
114
|
+
if (!this.openValue) return
|
|
115
|
+
|
|
116
|
+
if (event.key === "Escape") {
|
|
117
|
+
event.preventDefault()
|
|
118
|
+
this.close()
|
|
49
119
|
}
|
|
50
120
|
}
|
|
51
121
|
|
|
52
|
-
|
|
53
|
-
|
|
122
|
+
// Event Listener Management
|
|
123
|
+
|
|
124
|
+
addEventListeners() {
|
|
125
|
+
// Delay adding click outside listener to prevent immediate close
|
|
126
|
+
setTimeout(() => {
|
|
127
|
+
document.addEventListener("click", this.handleClickOutside)
|
|
128
|
+
}, 0)
|
|
129
|
+
|
|
130
|
+
document.addEventListener("keydown", this.handleKeydown)
|
|
54
131
|
}
|
|
55
132
|
|
|
56
|
-
|
|
57
|
-
document.removeEventListener(
|
|
133
|
+
removeEventListeners() {
|
|
134
|
+
document.removeEventListener("click", this.handleClickOutside)
|
|
135
|
+
document.removeEventListener("keydown", this.handleKeydown)
|
|
58
136
|
}
|
|
59
137
|
}
|
|
@@ -318,19 +318,17 @@ export default class extends Controller {
|
|
|
318
318
|
// Scroll Lock (for mobile overlay)
|
|
319
319
|
// ============================================================================
|
|
320
320
|
|
|
321
|
+
// Styling lives in drawer.css (body[data-maquina-scroll-locked]); the
|
|
322
|
+
// controller only records the scroll offset and flips the attribute
|
|
321
323
|
lockScroll() {
|
|
322
324
|
this.scrollPosition = window.pageYOffset
|
|
323
|
-
document.body.style.
|
|
324
|
-
document.body.
|
|
325
|
-
document.body.style.top = `-${this.scrollPosition}px`
|
|
326
|
-
document.body.style.width = "100%"
|
|
325
|
+
document.body.style.setProperty("--maquina-scroll-lock-top", `-${this.scrollPosition}px`)
|
|
326
|
+
document.body.setAttribute("data-maquina-scroll-locked", "")
|
|
327
327
|
}
|
|
328
328
|
|
|
329
329
|
unlockScroll() {
|
|
330
|
-
document.body.
|
|
331
|
-
document.body.style.
|
|
332
|
-
document.body.style.top = ""
|
|
333
|
-
document.body.style.width = ""
|
|
330
|
+
document.body.removeAttribute("data-maquina-scroll-locked")
|
|
331
|
+
document.body.style.removeProperty("--maquina-scroll-lock-top")
|
|
334
332
|
if (this.scrollPosition !== undefined) {
|
|
335
333
|
window.scrollTo(0, this.scrollPosition)
|
|
336
334
|
}
|
|
@@ -2,12 +2,13 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Sidebar Trigger Controller
|
|
5
|
-
*
|
|
6
|
-
* Triggers sidebar toggle via Stimulus outlets
|
|
5
|
+
*
|
|
6
|
+
* Triggers sidebar toggle via Stimulus outlets and mirrors the sidebar's
|
|
7
|
+
* state on the trigger (aria-expanded, aria-controls).
|
|
7
8
|
* Can be placed anywhere on the page - finds sidebar via outlet selector.
|
|
8
|
-
*
|
|
9
|
+
*
|
|
9
10
|
* @example
|
|
10
|
-
* <button
|
|
11
|
+
* <button
|
|
11
12
|
* data-controller="sidebar-trigger"
|
|
12
13
|
* data-action="click->sidebar-trigger#triggerClick"
|
|
13
14
|
* data-sidebar-trigger-sidebar-outlet="[data-outlet='sidebar']"
|
|
@@ -18,6 +19,23 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
18
19
|
export default class extends Controller {
|
|
19
20
|
static outlets = ["sidebar"]
|
|
20
21
|
|
|
22
|
+
connect() {
|
|
23
|
+
this.handleStateChanged = this.handleStateChanged.bind(this)
|
|
24
|
+
window.addEventListener("sidebar:stateChanged", this.handleStateChanged)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
disconnect() {
|
|
28
|
+
window.removeEventListener("sidebar:stateChanged", this.handleStateChanged)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
sidebarOutletConnected(outlet) {
|
|
32
|
+
this.syncExpanded(outlet.openValue)
|
|
33
|
+
|
|
34
|
+
if (outlet.hasSidebarTarget && outlet.sidebarTarget.id) {
|
|
35
|
+
this.element.setAttribute("aria-controls", outlet.sidebarTarget.id)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
/**
|
|
22
40
|
* Toggle sidebar when trigger is clicked
|
|
23
41
|
* Works with multiple sidebars if multiple outlets are connected
|
|
@@ -29,4 +47,15 @@ export default class extends Controller {
|
|
|
29
47
|
})
|
|
30
48
|
}
|
|
31
49
|
}
|
|
50
|
+
|
|
51
|
+
handleStateChanged(event) {
|
|
52
|
+
if (!this.hasSidebarOutlet) return
|
|
53
|
+
if (!this.sidebarOutletElements.includes(event.target)) return
|
|
54
|
+
|
|
55
|
+
this.syncExpanded(event.detail.open)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
syncExpanded(open) {
|
|
59
|
+
this.element.setAttribute("aria-expanded", open ? "true" : "false")
|
|
60
|
+
}
|
|
32
61
|
}
|
|
@@ -60,6 +60,10 @@ export default class extends Controller {
|
|
|
60
60
|
return controller.createToast({ title, variant: "error", ...options })
|
|
61
61
|
},
|
|
62
62
|
|
|
63
|
+
destructive(title, options = {}) {
|
|
64
|
+
return controller.createToast({ title, variant: "error", ...options })
|
|
65
|
+
},
|
|
66
|
+
|
|
63
67
|
dismiss(toastId) {
|
|
64
68
|
controller.dismissToast(toastId)
|
|
65
69
|
},
|
|
@@ -115,6 +119,14 @@ export default class extends Controller {
|
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
buildToastHtml({ id, title, description, variant, duration, dismissible, action }) {
|
|
122
|
+
// Titles/descriptions are escaped below; variant and duration are
|
|
123
|
+
// interpolated into attributes, so constrain them to safe values.
|
|
124
|
+
// :destructive is the library-wide alias for toast's error variant.
|
|
125
|
+
if (variant === "destructive") variant = "error"
|
|
126
|
+
const knownVariants = ["default", "success", "info", "warning", "error"]
|
|
127
|
+
variant = knownVariants.includes(variant) ? variant : "default"
|
|
128
|
+
duration = Number(duration) || 0
|
|
129
|
+
|
|
118
130
|
const icons = {
|
|
119
131
|
default: "",
|
|
120
132
|
success: this.getIconSvg("circle_check"),
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<%# locals: (variant: :default, icon: nil, css_classes: "", **html_options) %>
|
|
2
|
-
|
|
2
|
+
<%# :error is accepted as an alias of the canonical :destructive variant %>
|
|
3
|
+
<% variant = :destructive if variant.to_s == "error" %>
|
|
4
|
+
<% merged_data = merge_component_data(html_options,
|
|
3
5
|
component: :alert,
|
|
4
6
|
variant: variant,
|
|
5
7
|
has_icon: icon.present? || nil
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
<%# locals: (variant: :default, size: :md, css_classes: "", **html_options) %>
|
|
2
|
-
<%
|
|
2
|
+
<%
|
|
3
|
+
# Library-wide vocabulary aliases: :default is the canonical middle
|
|
4
|
+
# size (badge's own is :md), :destructive the canonical danger variant
|
|
5
|
+
size = :md if size.to_s == "default"
|
|
6
|
+
variant = :destructive if variant.to_s == "error"
|
|
7
|
+
%>
|
|
8
|
+
<% merged_data = merge_component_data(html_options,
|
|
3
9
|
component: :badge,
|
|
4
10
|
variant: variant,
|
|
5
11
|
size: size
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<%# locals: (css_classes: "", **html_options) %>
|
|
2
|
-
<% merged_data = (html_options
|
|
2
|
+
<% merged_data = merge_component_data(html_options, component: :card) %>
|
|
3
3
|
|
|
4
4
|
<%= content_tag :div, class: css_classes.presence, data: merged_data, **html_options do %>
|
|
5
5
|
<%= yield %>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<%# locals: (id: nil, name: nil, value: nil, placeholder: "Select...", css_classes: "", **html_options) %>
|
|
2
|
-
|
|
3
|
-
<%
|
|
2
|
+
<%# Deterministic fallback id: random ids break idiomorph matching across Turbo morphs %>
|
|
3
|
+
<% combobox_id = id || "combobox-#{name.to_s.parameterize.presence || "default"}" %>
|
|
4
|
+
<% merged_data = merge_component_data(html_options,
|
|
4
5
|
component: :combobox,
|
|
5
6
|
controller: "combobox",
|
|
6
7
|
combobox_value_value: value,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<%# locals: (selected: nil, selected_end: nil, mode: :single, min_date: nil, max_date: nil, disabled_dates: [], show_outside_days: true, week_starts_on: :sunday, placeholder: nil, input_name: nil, input_name_end: nil, id: nil, disabled: false, required: false, css_classes: "", **html_options) %>
|
|
2
2
|
<%
|
|
3
|
-
#
|
|
4
|
-
|
|
3
|
+
# Deterministic ID for popover targeting: random ids break idiomorph
|
|
4
|
+
# matching across Turbo morphs
|
|
5
|
+
component_id = id || "date-picker-#{input_name.to_s.parameterize.presence || "default"}"
|
|
5
6
|
popover_id = "#{component_id}-popover"
|
|
6
7
|
|
|
7
8
|
# Parse selected dates for display
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
|
|
29
30
|
default_placeholder = mode == :range ? "Select date range" : "Select date"
|
|
30
31
|
|
|
31
|
-
merged_data = (html_options
|
|
32
|
+
merged_data = merge_component_data(html_options,
|
|
32
33
|
controller: "date-picker",
|
|
33
34
|
component: "date-picker",
|
|
34
35
|
"date-picker-mode-value": mode,
|