maquina-components 0.4.4 → 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 +19 -1
- data/app/assets/stylesheets/breadcrumbs.css +21 -0
- data/app/assets/stylesheets/combobox.css +36 -1
- data/app/assets/stylesheets/drawer.css +135 -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 +4 -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/drawer_helper.rb +19 -0
- data/app/helpers/maquina_components/icons_helper.rb +68 -1
- 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 +287 -0
- data/app/javascript/controllers/drawer_trigger_controller.js +58 -0
- 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 +37 -0
- 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 +14 -0
- data/app/views/components/drawer/_content.html.erb +7 -0
- data/app/views/components/drawer/_footer.html.erb +7 -0
- data/app/views/components/drawer/_header.html.erb +12 -0
- data/app/views/components/drawer/_provider.html.erb +14 -0
- data/app/views/components/drawer/_trigger.html.erb +14 -0
- 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 +8 -0
- data/lib/generators/maquina_components/scaffold_templates/scaffold_templates_generator.rb +40 -0
- data/lib/generators/maquina_components/scaffold_templates/templates/_form.html.erb.tt +49 -0
- data/lib/generators/maquina_components/scaffold_templates/templates/edit.html.erb.tt +29 -0
- data/lib/generators/maquina_components/scaffold_templates/templates/index.html.erb.tt +58 -0
- data/lib/generators/maquina_components/scaffold_templates/templates/new.html.erb.tt +26 -0
- data/lib/generators/maquina_components/scaffold_templates/templates/partial.html.erb.tt +17 -0
- data/lib/generators/maquina_components/scaffold_templates/templates/show.html.erb.tt +53 -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 +52 -3
- data/app/assets/stylesheets/maquina_components.css +0 -178
|
@@ -26,8 +26,17 @@ module MaquinaComponents
|
|
|
26
26
|
private
|
|
27
27
|
|
|
28
28
|
def apply_icon_options(svg, options)
|
|
29
|
+
return svg.html_safe unless options&.any?
|
|
30
|
+
|
|
29
31
|
css_classes = options[:class]
|
|
30
|
-
|
|
32
|
+
if css_classes&.is_a?(String)
|
|
33
|
+
escaped_css_classes = ERB::Util.html_escape(css_classes)
|
|
34
|
+
svg = if /\bclass\s*=\s*(['"])(.*?)\1/.match?(svg)
|
|
35
|
+
svg.sub(/\bclass\s*=\s*(['"])(.*?)\1/, "class=\\1#{escaped_css_classes}\\1")
|
|
36
|
+
else
|
|
37
|
+
svg.sub(/<svg(?=(\s|>))/, "<svg class=\"#{escaped_css_classes}\"")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
31
40
|
|
|
32
41
|
if options[:stroke_width]
|
|
33
42
|
svg = svg.gsub('stroke-width="2"', "stroke-width=\"#{options[:stroke_width]}\"")
|
|
@@ -392,6 +401,64 @@ module MaquinaComponents
|
|
|
392
401
|
<rect width="7" height="7" x="3" y="14" rx="1"/>
|
|
393
402
|
</svg>
|
|
394
403
|
SVG
|
|
404
|
+
when :message_square
|
|
405
|
+
<<~SVG.freeze
|
|
406
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="">
|
|
407
|
+
<path d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/>
|
|
408
|
+
</svg>
|
|
409
|
+
SVG
|
|
410
|
+
when :arrow_up
|
|
411
|
+
<<~SVG.freeze
|
|
412
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="">
|
|
413
|
+
<path d="M7 17L17 7M17 7H7M17 7V17"/>
|
|
414
|
+
</svg>
|
|
415
|
+
SVG
|
|
416
|
+
when :arrow_down
|
|
417
|
+
<<~SVG.freeze
|
|
418
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="">
|
|
419
|
+
<path d="M17 7L7 17M7 17H17M7 17V7"/>
|
|
420
|
+
</svg>
|
|
421
|
+
SVG
|
|
422
|
+
when :lightning_bolt
|
|
423
|
+
<<~SVG.freeze
|
|
424
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="">
|
|
425
|
+
<path d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
|
426
|
+
</svg>
|
|
427
|
+
SVG
|
|
428
|
+
when :clipboard_list
|
|
429
|
+
<<~SVG.freeze
|
|
430
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="">
|
|
431
|
+
<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
|
|
432
|
+
</svg>
|
|
433
|
+
SVG
|
|
434
|
+
when :chart_bar
|
|
435
|
+
<<~SVG.freeze
|
|
436
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="">
|
|
437
|
+
<path d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
|
438
|
+
</svg>
|
|
439
|
+
SVG
|
|
440
|
+
when :circle_check
|
|
441
|
+
<<~SVG.freeze
|
|
442
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="">
|
|
443
|
+
<circle cx="12" cy="12" r="10"/>
|
|
444
|
+
<path d="m9 12 2 2 4-4"/>
|
|
445
|
+
</svg>
|
|
446
|
+
SVG
|
|
447
|
+
when :circle_x
|
|
448
|
+
<<~SVG.freeze
|
|
449
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="">
|
|
450
|
+
<circle cx="12" cy="12" r="10"/>
|
|
451
|
+
<path d="m15 9-6 6"/>
|
|
452
|
+
<path d="m9 9 6 6"/>
|
|
453
|
+
</svg>
|
|
454
|
+
SVG
|
|
455
|
+
when :x
|
|
456
|
+
<<~SVG.freeze
|
|
457
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="">
|
|
458
|
+
<path d="M18 6 6 18"/>
|
|
459
|
+
<path d="m6 6 12 12"/>
|
|
460
|
+
</svg>
|
|
461
|
+
SVG
|
|
395
462
|
end
|
|
396
463
|
end
|
|
397
464
|
end
|
|
@@ -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
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Drawer Controller
|
|
5
|
+
*
|
|
6
|
+
* Manages drawer state (slide-out panel), keyboard shortcuts, and persistence.
|
|
7
|
+
* Works with drawer_trigger_controller via Stimulus outlets.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* <div data-controller="drawer" data-outlet="drawer">
|
|
11
|
+
* <!-- drawer content -->
|
|
12
|
+
* </div>
|
|
13
|
+
*/
|
|
14
|
+
export default class extends Controller {
|
|
15
|
+
static values = {
|
|
16
|
+
open: { type: Boolean, default: false },
|
|
17
|
+
defaultOpen: { type: Boolean, default: false },
|
|
18
|
+
cookieName: { type: String, default: "drawer_state" },
|
|
19
|
+
cookieMaxAge: { type: Number, default: 60 * 60 * 24 * 365 },
|
|
20
|
+
keyboardShortcut: { type: String, default: "d" }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static targets = ["drawer", "container", "backdrop", "panel"]
|
|
24
|
+
|
|
25
|
+
initialize() {
|
|
26
|
+
const cookieValue = this.getCookie(this.cookieNameValue)
|
|
27
|
+
this.openValue = cookieValue !== null
|
|
28
|
+
? cookieValue === "true"
|
|
29
|
+
: this.defaultOpenValue
|
|
30
|
+
|
|
31
|
+
this._morphing = false
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
connect() {
|
|
35
|
+
this.resizeHandler = this.debounce(this.checkScreenSize.bind(this), 150)
|
|
36
|
+
window.addEventListener("resize", this.resizeHandler)
|
|
37
|
+
|
|
38
|
+
this.boundTeardown = this.teardown.bind(this)
|
|
39
|
+
this.boundBeforeMorphElement = this.beforeMorphElement.bind(this)
|
|
40
|
+
this.boundHandleMorph = this.handleMorph.bind(this)
|
|
41
|
+
document.addEventListener("turbo:before-cache", this.boundTeardown)
|
|
42
|
+
document.addEventListener("turbo:before-morph-element", this.boundBeforeMorphElement)
|
|
43
|
+
document.addEventListener("turbo:morph", this.boundHandleMorph)
|
|
44
|
+
|
|
45
|
+
this.updateStateImmediate()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
disconnect() {
|
|
49
|
+
window.removeEventListener("resize", this.resizeHandler)
|
|
50
|
+
document.removeEventListener("turbo:before-cache", this.boundTeardown)
|
|
51
|
+
document.removeEventListener("turbo:before-morph-element", this.boundBeforeMorphElement)
|
|
52
|
+
document.removeEventListener("turbo:morph", this.boundHandleMorph)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Turbo Cache Teardown
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
teardown() {
|
|
60
|
+
this.openValue = false
|
|
61
|
+
|
|
62
|
+
if (this.hasBackdropTarget) {
|
|
63
|
+
this.backdropTarget.setAttribute("data-state", "hidden")
|
|
64
|
+
this.backdropTarget.classList.add("hidden")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.unlockScroll()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
beforeMorphElement(event) {
|
|
71
|
+
if (event.target === this.element) {
|
|
72
|
+
this._morphing = true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
handleMorph() {
|
|
77
|
+
const cookieValue = this.getCookie(this.cookieNameValue)
|
|
78
|
+
|
|
79
|
+
if (cookieValue !== null) {
|
|
80
|
+
this.openValue = cookieValue === "true"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (this.hasDrawerTarget) {
|
|
84
|
+
this.drawerTarget.classList.remove("drawer-loading")
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.updateState()
|
|
88
|
+
this._morphing = false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// Keyboard Shortcut Actions
|
|
93
|
+
// ============================================================================
|
|
94
|
+
|
|
95
|
+
toggleWithKeyboard(event) {
|
|
96
|
+
this.toggle()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
closeOnEscape(event) {
|
|
100
|
+
if (this.openValue) {
|
|
101
|
+
this.close()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// State Management
|
|
107
|
+
// ============================================================================
|
|
108
|
+
|
|
109
|
+
openValueChanged(new_value, old_value) {
|
|
110
|
+
if (new_value === old_value) return
|
|
111
|
+
if (this._morphing) return
|
|
112
|
+
|
|
113
|
+
this.updateState()
|
|
114
|
+
this.persistState()
|
|
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
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
updateState() {
|
|
138
|
+
if (!this.hasDrawerTarget) return
|
|
139
|
+
|
|
140
|
+
const isOpen = this.openValue
|
|
141
|
+
const state = isOpen ? "open" : "closed"
|
|
142
|
+
|
|
143
|
+
this.drawerTarget.setAttribute("data-state", state)
|
|
144
|
+
|
|
145
|
+
if (this.hasBackdropTarget) {
|
|
146
|
+
const backdropState = isOpen ? "visible" : "hidden"
|
|
147
|
+
this.backdropTarget.setAttribute("data-state", backdropState)
|
|
148
|
+
|
|
149
|
+
if (backdropState === "visible") {
|
|
150
|
+
this.backdropTarget.classList.remove("hidden")
|
|
151
|
+
} else {
|
|
152
|
+
setTimeout(() => {
|
|
153
|
+
if (this.backdropTarget.getAttribute("data-state") === "hidden") {
|
|
154
|
+
this.backdropTarget.classList.add("hidden")
|
|
155
|
+
}
|
|
156
|
+
}, 300)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
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
|
+
|
|
171
|
+
if (isOpen) {
|
|
172
|
+
this.lockScroll()
|
|
173
|
+
} else {
|
|
174
|
+
this.unlockScroll()
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
updateStateImmediate() {
|
|
179
|
+
if (!this.hasDrawerTarget) return
|
|
180
|
+
|
|
181
|
+
this.updateState()
|
|
182
|
+
|
|
183
|
+
requestAnimationFrame(() => {
|
|
184
|
+
this.drawerTarget.classList.remove("drawer-loading")
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// Public Actions
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
192
|
+
toggle() {
|
|
193
|
+
this.openValue = !this.openValue
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
open() {
|
|
197
|
+
this.openValue = true
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
close() {
|
|
201
|
+
this.openValue = false
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
backdropClick(event) {
|
|
205
|
+
if (this.openValue) {
|
|
206
|
+
this.close()
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ============================================================================
|
|
211
|
+
// Persistence
|
|
212
|
+
// ============================================================================
|
|
213
|
+
|
|
214
|
+
persistState() {
|
|
215
|
+
this.setCookie(
|
|
216
|
+
this.cookieNameValue,
|
|
217
|
+
this.openValue.toString(),
|
|
218
|
+
this.cookieMaxAgeValue
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
getCookie(name) {
|
|
223
|
+
const value = `; ${document.cookie}`
|
|
224
|
+
const parts = value.split(`; ${name}=`)
|
|
225
|
+
if (parts.length === 2) {
|
|
226
|
+
return parts.pop().split(";").shift()
|
|
227
|
+
}
|
|
228
|
+
return null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
setCookie(name, value, maxAge) {
|
|
232
|
+
document.cookie = `${name}=${value}; path=/; max-age=${maxAge}; SameSite=Lax`
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ============================================================================
|
|
236
|
+
// Scroll Lock
|
|
237
|
+
// ============================================================================
|
|
238
|
+
|
|
239
|
+
// Styling lives in drawer.css (body[data-maquina-scroll-locked]); the
|
|
240
|
+
// controller only records the scroll offset and flips the attribute
|
|
241
|
+
lockScroll() {
|
|
242
|
+
this.scrollPosition = window.pageYOffset
|
|
243
|
+
document.body.style.setProperty("--maquina-scroll-lock-top", `-${this.scrollPosition}px`)
|
|
244
|
+
document.body.setAttribute("data-maquina-scroll-locked", "")
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
unlockScroll() {
|
|
248
|
+
document.body.removeAttribute("data-maquina-scroll-locked")
|
|
249
|
+
document.body.style.removeProperty("--maquina-scroll-lock-top")
|
|
250
|
+
if (this.scrollPosition !== undefined) {
|
|
251
|
+
window.scrollTo(0, this.scrollPosition)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ============================================================================
|
|
256
|
+
// Events
|
|
257
|
+
// ============================================================================
|
|
258
|
+
|
|
259
|
+
dispatchStateChange() {
|
|
260
|
+
this.dispatch("stateChanged", {
|
|
261
|
+
detail: {
|
|
262
|
+
open: this.openValue,
|
|
263
|
+
state: this.openValue ? "open" : "closed"
|
|
264
|
+
},
|
|
265
|
+
bubbles: true
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ============================================================================
|
|
270
|
+
// Utilities
|
|
271
|
+
// ============================================================================
|
|
272
|
+
|
|
273
|
+
checkScreenSize() {
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
debounce(func, wait) {
|
|
277
|
+
let timeout
|
|
278
|
+
return function executedFunction(...args) {
|
|
279
|
+
const later = () => {
|
|
280
|
+
clearTimeout(timeout)
|
|
281
|
+
func(...args)
|
|
282
|
+
}
|
|
283
|
+
clearTimeout(timeout)
|
|
284
|
+
timeout = setTimeout(later, wait)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Drawer Trigger Controller
|
|
5
|
+
*
|
|
6
|
+
* Triggers drawer toggle via Stimulus outlets and mirrors the drawer's
|
|
7
|
+
* state on the trigger (aria-expanded, aria-controls).
|
|
8
|
+
* Can be placed anywhere on the page - finds drawer via outlet selector.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* <button
|
|
12
|
+
* data-controller="drawer-trigger"
|
|
13
|
+
* data-action="click->drawer-trigger#triggerClick"
|
|
14
|
+
* data-drawer-trigger-drawer-outlet="[data-outlet='drawer']"
|
|
15
|
+
* >
|
|
16
|
+
* Toggle
|
|
17
|
+
* </button>
|
|
18
|
+
*/
|
|
19
|
+
export default class extends Controller {
|
|
20
|
+
static outlets = ["drawer"]
|
|
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
|
+
|
|
40
|
+
triggerClick() {
|
|
41
|
+
if (this.hasDrawerOutlet) {
|
|
42
|
+
this.drawerOutlets.forEach(outlet => {
|
|
43
|
+
outlet.toggle()
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
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
|
+
}
|