shadcn-rails 0.1.0 → 0.2.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/CHANGELOG.md +69 -2
- data/README.md +102 -1398
- data/__mocks__/@floating-ui/dom.js +67 -0
- data/app/assets/javascripts/shadcn/controllers/base_menu_controller.js +266 -0
- data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +34 -8
- data/app/assets/javascripts/shadcn/controllers/command_controller.js +5 -1
- data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +64 -135
- data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +56 -186
- data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +29 -55
- data/app/assets/javascripts/shadcn/controllers/menubar_controller.js +10 -7
- data/app/assets/javascripts/shadcn/controllers/navigation_menu_controller.js +10 -6
- data/app/assets/javascripts/shadcn/controllers/popover_controller.js +35 -60
- data/app/assets/javascripts/shadcn/controllers/select_controller.js +37 -17
- data/app/assets/javascripts/shadcn/controllers/sidebar_controller.js +24 -14
- data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +28 -59
- data/app/assets/javascripts/shadcn/index.js +9 -1
- data/app/assets/javascripts/shadcn/utils/floating.js +179 -0
- data/app/assets/stylesheets/shadcn/base.css +32 -0
- data/app/assets/stylesheets/shadcn/components.css +12 -0
- data/app/components/shadcn/accordion_component.html.erb +8 -0
- data/app/components/shadcn/accordion_component.rb +6 -15
- data/app/components/shadcn/alert_component.html.erb +6 -0
- data/app/components/shadcn/alert_component.rb +0 -18
- data/app/components/shadcn/alert_dialog_component.html.erb +12 -0
- data/app/components/shadcn/alert_dialog_component.rb +7 -27
- data/app/components/shadcn/aspect_ratio_component.html.erb +7 -0
- data/app/components/shadcn/aspect_ratio_component.rb +4 -19
- data/app/components/shadcn/avatar_component.html.erb +20 -0
- data/app/components/shadcn/avatar_component.rb +8 -36
- data/app/components/shadcn/badge_component.html.erb +1 -0
- data/app/components/shadcn/badge_component.rb +0 -11
- data/app/components/shadcn/base_component.rb +15 -2
- data/app/components/shadcn/breadcrumb_component.html.erb +5 -0
- data/app/components/shadcn/breadcrumb_component.rb +6 -16
- data/app/components/shadcn/button_component.html.erb +18 -0
- data/app/components/shadcn/button_component.rb +1 -41
- data/app/components/shadcn/card_component.html.erb +8 -0
- data/app/components/shadcn/card_component.rb +2 -6
- data/app/components/shadcn/checkbox_component.html.erb +32 -0
- data/app/components/shadcn/checkbox_component.rb +4 -43
- data/app/components/shadcn/collapsible_component.html.erb +8 -0
- data/app/components/shadcn/collapsible_component.rb +6 -15
- data/app/components/shadcn/command_list_component.rb +29 -14
- data/app/components/shadcn/context_menu_checkbox_item_component.rb +76 -0
- data/app/components/shadcn/context_menu_component.html.erb +11 -0
- data/app/components/shadcn/context_menu_component.rb +6 -26
- data/app/components/shadcn/context_menu_content_component.rb +37 -14
- data/app/components/shadcn/context_menu_item_component.rb +3 -2
- data/app/components/shadcn/context_menu_radio_group_component.rb +42 -0
- data/app/components/shadcn/context_menu_radio_item_component.rb +76 -0
- data/app/components/shadcn/dialog_component.html.erb +14 -0
- data/app/components/shadcn/dialog_component.rb +8 -29
- data/app/components/shadcn/drawer_component.html.erb +12 -0
- data/app/components/shadcn/drawer_component.rb +7 -27
- data/app/components/shadcn/dropdown_menu_checkbox_item_component.rb +76 -0
- data/app/components/shadcn/dropdown_menu_component.html.erb +14 -0
- data/app/components/shadcn/dropdown_menu_component.rb +9 -29
- data/app/components/shadcn/dropdown_menu_content_component.rb +45 -16
- data/app/components/shadcn/dropdown_menu_radio_group_component.rb +42 -0
- data/app/components/shadcn/dropdown_menu_radio_item_component.rb +76 -0
- data/app/components/shadcn/field_component.rb +7 -8
- data/app/components/shadcn/hover_card_component.html.erb +12 -0
- data/app/components/shadcn/hover_card_component.rb +7 -26
- data/app/components/shadcn/input_component.html.erb +18 -0
- data/app/components/shadcn/input_component.rb +2 -27
- data/app/components/shadcn/input_otp_component.rb +3 -3
- data/app/components/shadcn/kbd_component.html.erb +1 -0
- data/app/components/shadcn/kbd_component.rb +3 -10
- data/app/components/shadcn/label_component.html.erb +3 -0
- data/app/components/shadcn/label_component.rb +2 -18
- data/app/components/shadcn/menubar_component.html.erb +6 -0
- data/app/components/shadcn/menubar_component.rb +4 -15
- data/app/components/shadcn/menubar_content_component.rb +45 -20
- data/app/components/shadcn/menubar_sub_content_component.rb +21 -8
- data/app/components/shadcn/native_select_component.html.erb +22 -0
- data/app/components/shadcn/native_select_component.rb +9 -39
- data/app/components/shadcn/navigation_menu_component.html.erb +6 -0
- data/app/components/shadcn/navigation_menu_component.rb +4 -15
- data/app/components/shadcn/pagination_component.html.erb +5 -0
- data/app/components/shadcn/pagination_component.rb +11 -15
- data/app/components/shadcn/popover_component.html.erb +15 -0
- data/app/components/shadcn/popover_component.rb +10 -30
- data/app/components/shadcn/progress_component.html.erb +13 -0
- data/app/components/shadcn/progress_component.rb +6 -26
- data/app/components/shadcn/radio_group_component.html.erb +8 -0
- data/app/components/shadcn/radio_group_component.rb +12 -26
- data/app/components/shadcn/radio_group_item_component.rb +32 -6
- data/app/components/shadcn/resizable_panel_group_component.rb +27 -16
- data/app/components/shadcn/scroll_area_component.html.erb +7 -0
- data/app/components/shadcn/scroll_area_component.rb +4 -16
- data/app/components/shadcn/select_component.html.erb +46 -0
- data/app/components/shadcn/select_component.rb +29 -86
- data/app/components/shadcn/separator_component.html.erb +5 -0
- data/app/components/shadcn/separator_component.rb +6 -14
- data/app/components/shadcn/sheet_component.html.erb +12 -0
- data/app/components/shadcn/sheet_component.rb +7 -27
- data/app/components/shadcn/sidebar_component.rb +2 -2
- data/app/components/shadcn/skeleton_component.html.erb +1 -0
- data/app/components/shadcn/skeleton_component.rb +4 -2
- data/app/components/shadcn/slider_component.html.erb +12 -0
- data/app/components/shadcn/slider_component.rb +2 -21
- data/app/components/shadcn/spinner_component.html.erb +18 -0
- data/app/components/shadcn/spinner_component.rb +2 -30
- data/app/components/shadcn/switch_component.html.erb +72 -0
- data/app/components/shadcn/switch_component.rb +4 -82
- data/app/components/shadcn/table_component.html.erb +9 -0
- data/app/components/shadcn/table_component.rb +2 -10
- data/app/components/shadcn/tabs_component.html.erb +8 -0
- data/app/components/shadcn/tabs_component.rb +4 -17
- data/app/components/shadcn/textarea_component.html.erb +13 -0
- data/app/components/shadcn/textarea_component.rb +6 -22
- data/app/components/shadcn/toast_component.html.erb +36 -0
- data/app/components/shadcn/toast_component.rb +6 -54
- data/app/components/shadcn/toggle_component.html.erb +12 -0
- data/app/components/shadcn/toggle_component.rb +6 -21
- data/app/components/shadcn/toggle_group_component.html.erb +14 -0
- data/app/components/shadcn/toggle_group_component.rb +6 -29
- data/app/components/shadcn/tooltip_component.html.erb +20 -0
- data/app/components/shadcn/tooltip_component.rb +13 -38
- data/lib/generators/shadcn/add/USAGE +24 -0
- data/lib/generators/shadcn/add/add_generator.rb +279 -0
- data/lib/generators/shadcn/install/USAGE +22 -0
- data/lib/generators/shadcn/install/install_generator.rb +8 -3
- data/lib/generators/shadcn/install/templates/initializer.rb.tt +7 -27
- data/lib/generators/shadcn/install/templates/shadcn.yml.tt +15 -31
- data/lib/shadcn/rails/version.rb +1 -1
- metadata +54 -42
- data/.dockerignore +0 -40
- data/CLAUDE.md +0 -463
- data/PROGRESS.md +0 -485
- data/Rakefile +0 -29
- data/__tests__/controllers/__snapshots__/calendar_controller.test.js.snap +0 -13
- data/__tests__/controllers/__snapshots__/popover_controller.test.js.snap +0 -46
- data/__tests__/controllers/__snapshots__/sheet_controller.test.js.snap +0 -111
- data/__tests__/controllers/__snapshots__/tabs_controller.test.js.snap +0 -27
- data/__tests__/controllers/accordion_controller.test.js +0 -904
- data/__tests__/controllers/calendar_controller.test.js +0 -1370
- data/__tests__/controllers/carousel_controller.test.js +0 -912
- data/__tests__/controllers/checkbox_controller.test.js +0 -454
- data/__tests__/controllers/collapsible_controller.test.js +0 -407
- data/__tests__/controllers/combobox_controller.test.js +0 -966
- data/__tests__/controllers/context_menu_controller.test.js +0 -627
- data/__tests__/controllers/date_picker_controller.test.js +0 -636
- data/__tests__/controllers/dialog_controller.test.js +0 -878
- data/__tests__/controllers/drawer_controller.test.js +0 -995
- data/__tests__/controllers/menubar_controller.test.js +0 -736
- data/__tests__/controllers/navigation_menu_controller.test.js +0 -598
- data/__tests__/controllers/popover_controller.test.js +0 -1007
- data/__tests__/controllers/radio_group_controller.test.js +0 -640
- data/__tests__/controllers/resizable_controller.test.js +0 -680
- data/__tests__/controllers/select_controller.test.js +0 -674
- data/__tests__/controllers/sheet_controller.test.js +0 -986
- data/__tests__/controllers/slider_controller.test.js +0 -1036
- data/__tests__/controllers/switch_controller.test.js +0 -424
- data/__tests__/controllers/tabs_controller.test.js +0 -907
- data/__tests__/controllers/toggle_group_controller.test.js +0 -839
- data/__tests__/controllers/tooltip_controller.test.js +0 -808
- data/__tests__/helpers/stimulus-test-helper.js +0 -203
- data/babel.config.cjs +0 -5
- data/bin/console +0 -11
- data/bin/setup +0 -8
- data/jest.config.js +0 -19
- data/jest.setup.js +0 -8
- data/lib/generators/shadcn/component/component_generator.rb +0 -188
- data/lib/generators/shadcn/theme/theme_generator.rb +0 -128
- data/package-lock.json +0 -7415
- data/package.json +0 -68
- data/rollup.config.js +0 -29
|
@@ -1,225 +1,95 @@
|
|
|
1
|
-
import
|
|
1
|
+
import BaseMenuController from "./base_menu_controller"
|
|
2
|
+
import { positionFloating } from "../utils/floating"
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Dropdown controller for dropdown menus
|
|
5
|
-
*
|
|
6
|
+
* Extends BaseMenuController with Floating UI positioning
|
|
6
7
|
*/
|
|
7
|
-
export default class extends
|
|
8
|
-
static targets = [
|
|
8
|
+
export default class extends BaseMenuController {
|
|
9
|
+
static targets = [...BaseMenuController.targets]
|
|
9
10
|
static values = {
|
|
10
|
-
|
|
11
|
+
...BaseMenuController.values,
|
|
11
12
|
align: { type: String, default: "end" },
|
|
12
13
|
side: { type: String, default: "bottom" }
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
connect() {
|
|
16
|
-
this.
|
|
17
|
-
|
|
18
|
-
this.boundHandleKeydown = this.handleKeydown.bind(this)
|
|
19
|
-
|
|
20
|
-
if (this.openValue) {
|
|
21
|
-
this.show()
|
|
22
|
-
}
|
|
17
|
+
this.cleanupFloating = null
|
|
18
|
+
super.connect()
|
|
23
19
|
}
|
|
24
20
|
|
|
25
21
|
disconnect() {
|
|
26
|
-
this.
|
|
22
|
+
this.cleanupPositioning()
|
|
23
|
+
super.disconnect()
|
|
27
24
|
}
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
this.
|
|
33
|
-
} else {
|
|
34
|
-
this.show()
|
|
26
|
+
cleanupPositioning() {
|
|
27
|
+
if (this.cleanupFloating) {
|
|
28
|
+
this.cleanupFloating()
|
|
29
|
+
this.cleanupFloating = null
|
|
35
30
|
}
|
|
36
31
|
}
|
|
37
32
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this.
|
|
42
|
-
|
|
43
|
-
if (this.hasContentTarget) {
|
|
44
|
-
this.contentTarget.hidden = false
|
|
45
|
-
this.contentTarget.dataset.state = "open"
|
|
46
|
-
this.contentTarget.dataset.side = this.sideValue
|
|
47
|
-
this.positionContent()
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (this.hasTriggerTarget) {
|
|
51
|
-
this.triggerTarget.setAttribute("aria-expanded", "true")
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Add event listeners
|
|
55
|
-
document.addEventListener("click", this.boundHandleClickOutside)
|
|
56
|
-
document.addEventListener("keydown", this.boundHandleKeydown)
|
|
57
|
-
|
|
58
|
-
// Focus first item
|
|
59
|
-
this.focusedIndex = -1
|
|
60
|
-
this.focusNextItem()
|
|
61
|
-
|
|
62
|
-
this.dispatch("opened")
|
|
33
|
+
get placement() {
|
|
34
|
+
// Convert side/align to Floating UI placement
|
|
35
|
+
const align = this.alignValue === "center" ? "" : `-${this.alignValue}`
|
|
36
|
+
return `${this.sideValue}${align}`
|
|
63
37
|
}
|
|
64
38
|
|
|
65
|
-
|
|
66
|
-
if (!this.
|
|
67
|
-
|
|
68
|
-
this.openValue = false
|
|
69
|
-
|
|
70
|
-
if (this.hasContentTarget) {
|
|
71
|
-
this.contentTarget.dataset.state = "closed"
|
|
72
|
-
// Hide after animation
|
|
73
|
-
setTimeout(() => {
|
|
74
|
-
if (!this.openValue) {
|
|
75
|
-
this.contentTarget.hidden = true
|
|
76
|
-
}
|
|
77
|
-
}, 150)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (this.hasTriggerTarget) {
|
|
81
|
-
this.triggerTarget.setAttribute("aria-expanded", "false")
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Remove event listeners
|
|
85
|
-
document.removeEventListener("click", this.boundHandleClickOutside)
|
|
86
|
-
document.removeEventListener("keydown", this.boundHandleKeydown)
|
|
87
|
-
|
|
88
|
-
// Reset focus index
|
|
89
|
-
this.focusedIndex = -1
|
|
39
|
+
positionContent() {
|
|
40
|
+
if (!this.hasContentTarget || !this.hasTriggerTarget) return
|
|
90
41
|
|
|
91
|
-
|
|
42
|
+
// Use Floating UI for smart positioning
|
|
43
|
+
this.cleanupFloating = positionFloating(this.triggerTarget, this.contentTarget, {
|
|
44
|
+
placement: this.placement,
|
|
45
|
+
offset: 4,
|
|
46
|
+
sameWidth: false
|
|
47
|
+
})
|
|
92
48
|
}
|
|
93
49
|
|
|
94
|
-
|
|
95
|
-
this.
|
|
50
|
+
hideMenu() {
|
|
51
|
+
this.cleanupPositioning()
|
|
52
|
+
super.hideMenu()
|
|
96
53
|
}
|
|
97
54
|
|
|
98
|
-
|
|
55
|
+
toggleCheckbox(event) {
|
|
99
56
|
const item = event.currentTarget
|
|
100
57
|
if (item.dataset.disabled !== undefined) return
|
|
101
58
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
59
|
+
const isChecked = item.dataset.state === "checked"
|
|
60
|
+
item.dataset.state = isChecked ? "unchecked" : "checked"
|
|
61
|
+
item.setAttribute("aria-checked", (!isChecked).toString())
|
|
105
62
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
63
|
+
// Toggle the check icon visibility
|
|
64
|
+
const indicator = item.querySelector("span svg")
|
|
65
|
+
if (indicator) {
|
|
66
|
+
indicator.style.display = isChecked ? "none" : "block"
|
|
109
67
|
}
|
|
110
|
-
}
|
|
111
68
|
|
|
112
|
-
|
|
113
|
-
switch (event.key) {
|
|
114
|
-
case "Escape":
|
|
115
|
-
this.hide()
|
|
116
|
-
this.triggerTarget?.focus()
|
|
117
|
-
break
|
|
118
|
-
case "ArrowDown":
|
|
119
|
-
event.preventDefault()
|
|
120
|
-
this.focusNextItem()
|
|
121
|
-
break
|
|
122
|
-
case "ArrowUp":
|
|
123
|
-
event.preventDefault()
|
|
124
|
-
this.focusPreviousItem()
|
|
125
|
-
break
|
|
126
|
-
case "Home":
|
|
127
|
-
event.preventDefault()
|
|
128
|
-
this.focusFirstItem()
|
|
129
|
-
break
|
|
130
|
-
case "End":
|
|
131
|
-
event.preventDefault()
|
|
132
|
-
this.focusLastItem()
|
|
133
|
-
break
|
|
134
|
-
case "Enter":
|
|
135
|
-
case " ":
|
|
136
|
-
event.preventDefault()
|
|
137
|
-
this.selectFocusedItem()
|
|
138
|
-
break
|
|
139
|
-
}
|
|
69
|
+
this.dispatch("check", { detail: { item, checked: !isChecked } })
|
|
140
70
|
}
|
|
141
71
|
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
this.focusedIndex = (this.focusedIndex + 1) % items.length
|
|
147
|
-
items[this.focusedIndex].focus()
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
focusPreviousItem() {
|
|
151
|
-
const items = this.enabledItems
|
|
152
|
-
if (items.length === 0) return
|
|
153
|
-
|
|
154
|
-
this.focusedIndex = this.focusedIndex <= 0 ? items.length - 1 : this.focusedIndex - 1
|
|
155
|
-
items[this.focusedIndex].focus()
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
focusFirstItem() {
|
|
159
|
-
const items = this.enabledItems
|
|
160
|
-
if (items.length === 0) return
|
|
161
|
-
|
|
162
|
-
this.focusedIndex = 0
|
|
163
|
-
items[0].focus()
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
focusLastItem() {
|
|
167
|
-
const items = this.enabledItems
|
|
168
|
-
if (items.length === 0) return
|
|
169
|
-
|
|
170
|
-
this.focusedIndex = items.length - 1
|
|
171
|
-
items[this.focusedIndex].focus()
|
|
172
|
-
}
|
|
72
|
+
selectRadio(event) {
|
|
73
|
+
const item = event.currentTarget
|
|
74
|
+
if (item.dataset.disabled !== undefined) return
|
|
173
75
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
76
|
+
const group = item.closest("[role='group']")
|
|
77
|
+
if (group) {
|
|
78
|
+
// Uncheck all radio items in the group
|
|
79
|
+
group.querySelectorAll("[role='menuitemradio']").forEach(radio => {
|
|
80
|
+
radio.dataset.state = "unchecked"
|
|
81
|
+
radio.setAttribute("aria-checked", "false")
|
|
82
|
+
const indicator = radio.querySelector("span svg")
|
|
83
|
+
if (indicator) indicator.style.display = "none"
|
|
84
|
+
})
|
|
178
85
|
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
get enabledItems() {
|
|
182
|
-
return this.itemTargets.filter(item => item.dataset.disabled === undefined)
|
|
183
|
-
}
|
|
184
86
|
|
|
185
|
-
|
|
186
|
-
|
|
87
|
+
// Check this item
|
|
88
|
+
item.dataset.state = "checked"
|
|
89
|
+
item.setAttribute("aria-checked", "true")
|
|
90
|
+
const indicator = item.querySelector("span svg")
|
|
91
|
+
if (indicator) indicator.style.display = "block"
|
|
187
92
|
|
|
188
|
-
|
|
189
|
-
const content = this.contentTarget
|
|
190
|
-
|
|
191
|
-
// Position based on side and align
|
|
192
|
-
content.style.position = "absolute"
|
|
193
|
-
content.style.minWidth = `${trigger.width}px`
|
|
194
|
-
|
|
195
|
-
switch (this.sideValue) {
|
|
196
|
-
case "top":
|
|
197
|
-
content.style.bottom = "100%"
|
|
198
|
-
content.style.top = "auto"
|
|
199
|
-
content.style.marginBottom = "4px"
|
|
200
|
-
break
|
|
201
|
-
case "bottom":
|
|
202
|
-
default:
|
|
203
|
-
content.style.top = "100%"
|
|
204
|
-
content.style.bottom = "auto"
|
|
205
|
-
content.style.marginTop = "4px"
|
|
206
|
-
break
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
switch (this.alignValue) {
|
|
210
|
-
case "start":
|
|
211
|
-
content.style.left = "0"
|
|
212
|
-
content.style.right = "auto"
|
|
213
|
-
break
|
|
214
|
-
case "center":
|
|
215
|
-
content.style.left = "50%"
|
|
216
|
-
content.style.transform = "translateX(-50%)"
|
|
217
|
-
break
|
|
218
|
-
case "end":
|
|
219
|
-
default:
|
|
220
|
-
content.style.right = "0"
|
|
221
|
-
content.style.left = "auto"
|
|
222
|
-
break
|
|
223
|
-
}
|
|
93
|
+
this.dispatch("radioChange", { detail: { item, value: item.dataset.value } })
|
|
224
94
|
}
|
|
225
95
|
}
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { positionFloating } from "../utils/floating"
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Hover Card Controller
|
|
5
6
|
* Handles showing/hiding content on hover with delays
|
|
7
|
+
* Uses Floating UI for smart positioning
|
|
6
8
|
*/
|
|
7
9
|
export default class extends Controller {
|
|
8
10
|
static targets = ["trigger", "content"]
|
|
9
11
|
static values = {
|
|
10
12
|
openDelay: { type: Number, default: 700 },
|
|
11
|
-
closeDelay: { type: Number, default: 300 }
|
|
13
|
+
closeDelay: { type: Number, default: 300 },
|
|
14
|
+
side: { type: String, default: "bottom" },
|
|
15
|
+
align: { type: String, default: "center" }
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
connect() {
|
|
15
19
|
this.openTimeout = null
|
|
16
20
|
this.closeTimeout = null
|
|
17
21
|
this.isOpen = false
|
|
22
|
+
this.cleanupFloating = null
|
|
18
23
|
|
|
19
24
|
this.triggerTarget.addEventListener("mouseenter", this.scheduleOpen.bind(this))
|
|
20
25
|
this.triggerTarget.addEventListener("mouseleave", this.scheduleClose.bind(this))
|
|
@@ -27,6 +32,20 @@ export default class extends Controller {
|
|
|
27
32
|
|
|
28
33
|
disconnect() {
|
|
29
34
|
this.clearTimeouts()
|
|
35
|
+
this.cleanupPositioning()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
cleanupPositioning() {
|
|
39
|
+
if (this.cleanupFloating) {
|
|
40
|
+
this.cleanupFloating()
|
|
41
|
+
this.cleanupFloating = null
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get placement() {
|
|
46
|
+
// Convert side/align to Floating UI placement
|
|
47
|
+
const align = this.alignValue === "center" ? "" : `-${this.alignValue}`
|
|
48
|
+
return `${this.sideValue}${align}`
|
|
30
49
|
}
|
|
31
50
|
|
|
32
51
|
scheduleOpen() {
|
|
@@ -67,7 +86,12 @@ export default class extends Controller {
|
|
|
67
86
|
this.isOpen = true
|
|
68
87
|
this.contentTarget.style.display = "block"
|
|
69
88
|
this.contentTarget.setAttribute("data-state", "open")
|
|
70
|
-
|
|
89
|
+
|
|
90
|
+
// Use Floating UI for smart positioning
|
|
91
|
+
this.cleanupFloating = positionFloating(this.triggerTarget, this.contentTarget, {
|
|
92
|
+
placement: this.placement,
|
|
93
|
+
offset: 8
|
|
94
|
+
})
|
|
71
95
|
|
|
72
96
|
this.dispatch("open")
|
|
73
97
|
}
|
|
@@ -78,6 +102,9 @@ export default class extends Controller {
|
|
|
78
102
|
this.isOpen = false
|
|
79
103
|
this.contentTarget.setAttribute("data-state", "closed")
|
|
80
104
|
|
|
105
|
+
// Cleanup Floating UI
|
|
106
|
+
this.cleanupPositioning()
|
|
107
|
+
|
|
81
108
|
// Wait for animation to complete
|
|
82
109
|
setTimeout(() => {
|
|
83
110
|
if (!this.isOpen) {
|
|
@@ -87,57 +114,4 @@ export default class extends Controller {
|
|
|
87
114
|
|
|
88
115
|
this.dispatch("close")
|
|
89
116
|
}
|
|
90
|
-
|
|
91
|
-
positionContent() {
|
|
92
|
-
const trigger = this.triggerTarget.getBoundingClientRect()
|
|
93
|
-
const content = this.contentTarget
|
|
94
|
-
const side = content.dataset.side || "bottom"
|
|
95
|
-
const align = content.dataset.align || "center"
|
|
96
|
-
|
|
97
|
-
// Reset position
|
|
98
|
-
content.style.top = ""
|
|
99
|
-
content.style.left = ""
|
|
100
|
-
content.style.right = ""
|
|
101
|
-
content.style.bottom = ""
|
|
102
|
-
|
|
103
|
-
const gap = 8 // Gap between trigger and content
|
|
104
|
-
|
|
105
|
-
switch (side) {
|
|
106
|
-
case "top":
|
|
107
|
-
content.style.bottom = "100%"
|
|
108
|
-
content.style.marginBottom = `${gap}px`
|
|
109
|
-
break
|
|
110
|
-
case "bottom":
|
|
111
|
-
content.style.top = "100%"
|
|
112
|
-
content.style.marginTop = `${gap}px`
|
|
113
|
-
break
|
|
114
|
-
case "left":
|
|
115
|
-
content.style.right = "100%"
|
|
116
|
-
content.style.marginRight = `${gap}px`
|
|
117
|
-
content.style.top = "0"
|
|
118
|
-
break
|
|
119
|
-
case "right":
|
|
120
|
-
content.style.left = "100%"
|
|
121
|
-
content.style.marginLeft = `${gap}px`
|
|
122
|
-
content.style.top = "0"
|
|
123
|
-
break
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Handle alignment for top/bottom
|
|
127
|
-
if (side === "top" || side === "bottom") {
|
|
128
|
-
switch (align) {
|
|
129
|
-
case "start":
|
|
130
|
-
content.style.left = "0"
|
|
131
|
-
break
|
|
132
|
-
case "end":
|
|
133
|
-
content.style.right = "0"
|
|
134
|
-
break
|
|
135
|
-
case "center":
|
|
136
|
-
default:
|
|
137
|
-
content.style.left = "50%"
|
|
138
|
-
content.style.transform = "translateX(-50%)"
|
|
139
|
-
break
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
117
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { useClickOutside } from "stimulus-use"
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Menubar controller
|
|
5
6
|
* Handles menu opening/closing, keyboard navigation, hover behavior
|
|
7
|
+
* Uses stimulus-use for click outside detection
|
|
6
8
|
*/
|
|
7
9
|
export default class extends Controller {
|
|
8
10
|
static targets = ["menu", "trigger", "content", "item", "sub", "subTrigger", "subContent"]
|
|
@@ -13,14 +15,15 @@ export default class extends Controller {
|
|
|
13
15
|
connect() {
|
|
14
16
|
this.focusedIndex = -1
|
|
15
17
|
this.isMenuOpen = false
|
|
16
|
-
this.boundHandleClickOutside = this.handleClickOutside.bind(this)
|
|
17
18
|
this.boundHandleKeydown = this.handleKeydown.bind(this)
|
|
18
19
|
this.closeSubTimer = null
|
|
20
|
+
|
|
21
|
+
// Use stimulus-use for click outside detection
|
|
22
|
+
useClickOutside(this)
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
disconnect() {
|
|
22
26
|
this.closeAll()
|
|
23
|
-
document.removeEventListener("click", this.boundHandleClickOutside)
|
|
24
27
|
document.removeEventListener("keydown", this.boundHandleKeydown)
|
|
25
28
|
}
|
|
26
29
|
|
|
@@ -72,8 +75,7 @@ export default class extends Controller {
|
|
|
72
75
|
this.isMenuOpen = true
|
|
73
76
|
this.focusedIndex = -1
|
|
74
77
|
|
|
75
|
-
// Add event
|
|
76
|
-
document.addEventListener("click", this.boundHandleClickOutside)
|
|
78
|
+
// Add keydown event listener (click outside is handled by stimulus-use)
|
|
77
79
|
document.addEventListener("keydown", this.boundHandleKeydown)
|
|
78
80
|
|
|
79
81
|
// Focus first item
|
|
@@ -100,7 +102,7 @@ export default class extends Controller {
|
|
|
100
102
|
this.isMenuOpen = false
|
|
101
103
|
this.focusedIndex = -1
|
|
102
104
|
|
|
103
|
-
|
|
105
|
+
// Remove keydown listener (click outside is handled by stimulus-use)
|
|
104
106
|
document.removeEventListener("keydown", this.boundHandleKeydown)
|
|
105
107
|
}
|
|
106
108
|
|
|
@@ -198,8 +200,9 @@ export default class extends Controller {
|
|
|
198
200
|
})
|
|
199
201
|
}
|
|
200
202
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
+
// Called by stimulus-use when clicking outside the element
|
|
204
|
+
clickOutside(event) {
|
|
205
|
+
if (this.isMenuOpen) {
|
|
203
206
|
this.closeAll()
|
|
204
207
|
}
|
|
205
208
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { useClickOutside } from "stimulus-use"
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Navigation Menu Controller
|
|
5
6
|
* Handles navigation menu interactions with dropdown content areas
|
|
7
|
+
* Uses stimulus-use for click outside detection
|
|
6
8
|
*/
|
|
7
9
|
export default class extends Controller {
|
|
8
10
|
static targets = ["list", "item", "trigger", "content", "viewport"]
|
|
@@ -19,8 +21,10 @@ export default class extends Controller {
|
|
|
19
21
|
this.closeTimer = null
|
|
20
22
|
this.wasClickOpened = false
|
|
21
23
|
|
|
22
|
-
this.boundHandleClickOutside = this.handleClickOutside.bind(this)
|
|
23
24
|
this.boundHandleKeydown = this.handleKeydown.bind(this)
|
|
25
|
+
|
|
26
|
+
// Use stimulus-use for click outside detection
|
|
27
|
+
useClickOutside(this)
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
disconnect() {
|
|
@@ -125,8 +129,7 @@ export default class extends Controller {
|
|
|
125
129
|
|
|
126
130
|
this.isOpen = true
|
|
127
131
|
|
|
128
|
-
// Add event
|
|
129
|
-
document.addEventListener("click", this.boundHandleClickOutside)
|
|
132
|
+
// Add keydown event listener (click outside is handled by stimulus-use)
|
|
130
133
|
document.addEventListener("keydown", this.boundHandleKeydown)
|
|
131
134
|
}
|
|
132
135
|
|
|
@@ -183,12 +186,13 @@ export default class extends Controller {
|
|
|
183
186
|
this.isOpen = false
|
|
184
187
|
this.wasClickOpened = false
|
|
185
188
|
|
|
186
|
-
|
|
189
|
+
// Remove keydown listener (click outside is handled by stimulus-use)
|
|
187
190
|
document.removeEventListener("keydown", this.boundHandleKeydown)
|
|
188
191
|
}
|
|
189
192
|
|
|
190
|
-
|
|
191
|
-
|
|
193
|
+
// Called by stimulus-use when clicking outside the element
|
|
194
|
+
clickOutside(event) {
|
|
195
|
+
if (this.isOpen) {
|
|
192
196
|
this.closeAll()
|
|
193
197
|
}
|
|
194
198
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { useClickOutside } from "stimulus-use"
|
|
3
|
+
import { positionFloating } from "../utils/floating"
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Popover controller for rich content overlays
|
|
7
|
+
* Uses Floating UI for smart positioning and stimulus-use for click outside detection
|
|
5
8
|
*/
|
|
6
9
|
export default class extends Controller {
|
|
7
10
|
static targets = ["trigger", "content"]
|
|
@@ -13,7 +16,10 @@ export default class extends Controller {
|
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
connect() {
|
|
16
|
-
this.
|
|
19
|
+
this.cleanupFloating = null
|
|
20
|
+
|
|
21
|
+
// Use stimulus-use for click outside detection
|
|
22
|
+
useClickOutside(this)
|
|
17
23
|
|
|
18
24
|
if (this.openValue) {
|
|
19
25
|
this.show()
|
|
@@ -22,6 +28,20 @@ export default class extends Controller {
|
|
|
22
28
|
|
|
23
29
|
disconnect() {
|
|
24
30
|
this.hide()
|
|
31
|
+
this.cleanupPositioning()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
cleanupPositioning() {
|
|
35
|
+
if (this.cleanupFloating) {
|
|
36
|
+
this.cleanupFloating()
|
|
37
|
+
this.cleanupFloating = null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get placement() {
|
|
42
|
+
// Convert side/align to Floating UI placement
|
|
43
|
+
const align = this.alignValue === "center" ? "" : `-${this.alignValue}`
|
|
44
|
+
return `${this.sideValue}${align}`
|
|
25
45
|
}
|
|
26
46
|
|
|
27
47
|
toggle(event) {
|
|
@@ -41,11 +61,15 @@ export default class extends Controller {
|
|
|
41
61
|
if (this.hasContentTarget) {
|
|
42
62
|
this.contentTarget.hidden = false
|
|
43
63
|
this.contentTarget.dataset.state = "open"
|
|
44
|
-
this.contentTarget.dataset.side = this.sideValue
|
|
45
|
-
this.positionContent()
|
|
46
|
-
}
|
|
47
64
|
|
|
48
|
-
|
|
65
|
+
// Use Floating UI for smart positioning
|
|
66
|
+
if (this.hasTriggerTarget) {
|
|
67
|
+
this.cleanupFloating = positionFloating(this.triggerTarget, this.contentTarget, {
|
|
68
|
+
placement: this.placement,
|
|
69
|
+
offset: 8
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
}
|
|
49
73
|
|
|
50
74
|
if (this.modalValue) {
|
|
51
75
|
document.body.style.pointerEvents = "none"
|
|
@@ -60,6 +84,9 @@ export default class extends Controller {
|
|
|
60
84
|
|
|
61
85
|
this.openValue = false
|
|
62
86
|
|
|
87
|
+
// Cleanup Floating UI auto-update
|
|
88
|
+
this.cleanupPositioning()
|
|
89
|
+
|
|
63
90
|
if (this.hasContentTarget) {
|
|
64
91
|
this.contentTarget.dataset.state = "closed"
|
|
65
92
|
setTimeout(() => {
|
|
@@ -69,8 +96,6 @@ export default class extends Controller {
|
|
|
69
96
|
}, 150)
|
|
70
97
|
}
|
|
71
98
|
|
|
72
|
-
document.removeEventListener("click", this.boundHandleClickOutside)
|
|
73
|
-
|
|
74
99
|
if (this.modalValue) {
|
|
75
100
|
document.body.style.pointerEvents = ""
|
|
76
101
|
}
|
|
@@ -82,60 +107,10 @@ export default class extends Controller {
|
|
|
82
107
|
this.hide()
|
|
83
108
|
}
|
|
84
109
|
|
|
85
|
-
|
|
86
|
-
|
|
110
|
+
// Called by stimulus-use when clicking outside the element
|
|
111
|
+
clickOutside(event) {
|
|
112
|
+
if (this.openValue) {
|
|
87
113
|
this.hide()
|
|
88
114
|
}
|
|
89
115
|
}
|
|
90
|
-
|
|
91
|
-
positionContent() {
|
|
92
|
-
if (!this.hasContentTarget || !this.hasTriggerTarget) return
|
|
93
|
-
|
|
94
|
-
const trigger = this.triggerTarget.getBoundingClientRect()
|
|
95
|
-
const content = this.contentTarget
|
|
96
|
-
|
|
97
|
-
content.style.position = "absolute"
|
|
98
|
-
|
|
99
|
-
const gap = 8
|
|
100
|
-
|
|
101
|
-
switch (this.sideValue) {
|
|
102
|
-
case "top":
|
|
103
|
-
content.style.bottom = "100%"
|
|
104
|
-
content.style.top = "auto"
|
|
105
|
-
content.style.marginBottom = `${gap}px`
|
|
106
|
-
break
|
|
107
|
-
case "bottom":
|
|
108
|
-
content.style.top = "100%"
|
|
109
|
-
content.style.bottom = "auto"
|
|
110
|
-
content.style.marginTop = `${gap}px`
|
|
111
|
-
break
|
|
112
|
-
case "left":
|
|
113
|
-
content.style.right = "100%"
|
|
114
|
-
content.style.left = "auto"
|
|
115
|
-
content.style.marginRight = `${gap}px`
|
|
116
|
-
break
|
|
117
|
-
case "right":
|
|
118
|
-
content.style.left = "100%"
|
|
119
|
-
content.style.right = "auto"
|
|
120
|
-
content.style.marginLeft = `${gap}px`
|
|
121
|
-
break
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
switch (this.alignValue) {
|
|
125
|
-
case "start":
|
|
126
|
-
content.style.left = "0"
|
|
127
|
-
content.style.right = "auto"
|
|
128
|
-
break
|
|
129
|
-
case "center":
|
|
130
|
-
if (this.sideValue === "top" || this.sideValue === "bottom") {
|
|
131
|
-
content.style.left = "50%"
|
|
132
|
-
content.style.transform = "translateX(-50%)"
|
|
133
|
-
}
|
|
134
|
-
break
|
|
135
|
-
case "end":
|
|
136
|
-
content.style.right = "0"
|
|
137
|
-
content.style.left = "auto"
|
|
138
|
-
break
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
116
|
}
|