shadcn_view_components 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +590 -0
  3. data/LICENSE +49 -0
  4. data/README.md +542 -0
  5. data/app/assets/javascripts/shadcn/aria_relationships.js +88 -0
  6. data/app/assets/javascripts/shadcn/controllers/accordion_controller.js +241 -0
  7. data/app/assets/javascripts/shadcn/controllers/calendar_controller.js +98 -0
  8. data/app/assets/javascripts/shadcn/controllers/carousel_controller.js +305 -0
  9. data/app/assets/javascripts/shadcn/controllers/checked_state_controller.js +121 -0
  10. data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +613 -0
  11. data/app/assets/javascripts/shadcn/controllers/command_controller.js +87 -0
  12. data/app/assets/javascripts/shadcn/controllers/dialog_controller.js +208 -0
  13. data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +145 -0
  14. data/app/assets/javascripts/shadcn/controllers/input_otp_controller.js +416 -0
  15. data/app/assets/javascripts/shadcn/controllers/menu_controller.js +286 -0
  16. data/app/assets/javascripts/shadcn/controllers/menubar_controller.js +430 -0
  17. data/app/assets/javascripts/shadcn/controllers/message_scroller_controller.js +37 -0
  18. data/app/assets/javascripts/shadcn/controllers/navigation_menu_controller.js +254 -0
  19. data/app/assets/javascripts/shadcn/controllers/popover_controller.js +117 -0
  20. data/app/assets/javascripts/shadcn/controllers/resizable_controller.js +256 -0
  21. data/app/assets/javascripts/shadcn/controllers/select_controller.js +491 -0
  22. data/app/assets/javascripts/shadcn/controllers/sidebar_controller.js +30 -0
  23. data/app/assets/javascripts/shadcn/controllers/slider_controller.js +74 -0
  24. data/app/assets/javascripts/shadcn/controllers/tabs_controller.js +146 -0
  25. data/app/assets/javascripts/shadcn/controllers/toast_controller.js +81 -0
  26. data/app/assets/javascripts/shadcn/controllers/toggle_controller.js +21 -0
  27. data/app/assets/javascripts/shadcn/controllers/toggle_group_controller.js +37 -0
  28. data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +123 -0
  29. data/app/assets/javascripts/shadcn/floating_position.js +660 -0
  30. data/app/assets/javascripts/shadcn/hide_after_exit.js +53 -0
  31. data/app/assets/javascripts/shadcn/index.js +65 -0
  32. data/app/assets/javascripts/shadcn/menu_popover.js +352 -0
  33. data/app/assets/javascripts/shadcn/package.json +17 -0
  34. data/app/assets/javascripts/shadcn/state_attrs.js +13 -0
  35. data/app/assets/stylesheets/shadcn/shadcn.css +766 -0
  36. data/app/assets/tailwind/shadcn_view_components/engine.css +11 -0
  37. data/app/components/shadcn/accordion.rb +121 -0
  38. data/app/components/shadcn/alert.rb +31 -0
  39. data/app/components/shadcn/alert_dialog.rb +165 -0
  40. data/app/components/shadcn/aspect_ratio.rb +34 -0
  41. data/app/components/shadcn/attachment.rb +86 -0
  42. data/app/components/shadcn/avatar.rb +55 -0
  43. data/app/components/shadcn/badge.rb +32 -0
  44. data/app/components/shadcn/base_component.rb +239 -0
  45. data/app/components/shadcn/breadcrumb.rb +70 -0
  46. data/app/components/shadcn/bubble.rb +48 -0
  47. data/app/components/shadcn/button.html.erb +1 -0
  48. data/app/components/shadcn/button.rb +41 -0
  49. data/app/components/shadcn/button_group.rb +41 -0
  50. data/app/components/shadcn/button_styled.rb +51 -0
  51. data/app/components/shadcn/calendar.rb +276 -0
  52. data/app/components/shadcn/card.rb +18 -0
  53. data/app/components/shadcn/carousel.rb +236 -0
  54. data/app/components/shadcn/chart.rb +57 -0
  55. data/app/components/shadcn/checkbox.rb +64 -0
  56. data/app/components/shadcn/collapsible.rb +25 -0
  57. data/app/components/shadcn/combobox.rb +552 -0
  58. data/app/components/shadcn/command.rb +149 -0
  59. data/app/components/shadcn/context_menu.rb +79 -0
  60. data/app/components/shadcn/dialog.rb +180 -0
  61. data/app/components/shadcn/direction_provider.rb +18 -0
  62. data/app/components/shadcn/drawer.rb +150 -0
  63. data/app/components/shadcn/dropdown_menu.rb +242 -0
  64. data/app/components/shadcn/empty.rb +40 -0
  65. data/app/components/shadcn/field.rb +106 -0
  66. data/app/components/shadcn/floating_position_options.rb +132 -0
  67. data/app/components/shadcn/form.rb +105 -0
  68. data/app/components/shadcn/hover_card.rb +66 -0
  69. data/app/components/shadcn/input.rb +13 -0
  70. data/app/components/shadcn/input_group/button.rb +48 -0
  71. data/app/components/shadcn/input_group.rb +51 -0
  72. data/app/components/shadcn/input_otp.rb +265 -0
  73. data/app/components/shadcn/item.rb +108 -0
  74. data/app/components/shadcn/kbd.rb +9 -0
  75. data/app/components/shadcn/label.rb +12 -0
  76. data/app/components/shadcn/marker.rb +37 -0
  77. data/app/components/shadcn/menubar.rb +110 -0
  78. data/app/components/shadcn/message.rb +29 -0
  79. data/app/components/shadcn/message_scroller.rb +52 -0
  80. data/app/components/shadcn/native_checked_state.rb +74 -0
  81. data/app/components/shadcn/native_select.rb +78 -0
  82. data/app/components/shadcn/navigation_menu.rb +122 -0
  83. data/app/components/shadcn/pagination.rb +160 -0
  84. data/app/components/shadcn/popover.rb +68 -0
  85. data/app/components/shadcn/preview_base.rb +107 -0
  86. data/app/components/shadcn/progress.rb +49 -0
  87. data/app/components/shadcn/radio_group.rb +74 -0
  88. data/app/components/shadcn/resizable.rb +77 -0
  89. data/app/components/shadcn/scroll_area.rb +59 -0
  90. data/app/components/shadcn/select.rb +356 -0
  91. data/app/components/shadcn/separator.rb +42 -0
  92. data/app/components/shadcn/sheet.rb +157 -0
  93. data/app/components/shadcn/sidebar.rb +332 -0
  94. data/app/components/shadcn/skeleton.rb +13 -0
  95. data/app/components/shadcn/slider.rb +279 -0
  96. data/app/components/shadcn/sonner.rb +30 -0
  97. data/app/components/shadcn/spinner.rb +32 -0
  98. data/app/components/shadcn/switch.rb +79 -0
  99. data/app/components/shadcn/table.rb +46 -0
  100. data/app/components/shadcn/tabs.rb +119 -0
  101. data/app/components/shadcn/textarea.rb +7 -0
  102. data/app/components/shadcn/toggle.rb +66 -0
  103. data/app/components/shadcn/toggle_group.rb +127 -0
  104. data/app/components/shadcn/tooltip.rb +74 -0
  105. data/config/importmap.rb +5 -0
  106. data/lib/generators/shadcn_view_components/install_generator.rb +182 -0
  107. data/lib/shadcn_view_components/classes.rb +89 -0
  108. data/lib/shadcn_view_components/component_naming.rb +21 -0
  109. data/lib/shadcn_view_components/contracts/calendar.rb +95 -0
  110. data/lib/shadcn_view_components/engine.rb +56 -0
  111. data/lib/shadcn_view_components/generated/contracts/accordion.rb +112 -0
  112. data/lib/shadcn_view_components/generated/contracts/alert-dialog.rb +300 -0
  113. data/lib/shadcn_view_components/generated/contracts/alert.rb +104 -0
  114. data/lib/shadcn_view_components/generated/contracts/aspect-ratio.rb +34 -0
  115. data/lib/shadcn_view_components/generated/contracts/attachment.rb +224 -0
  116. data/lib/shadcn_view_components/generated/contracts/avatar.rb +149 -0
  117. data/lib/shadcn_view_components/generated/contracts/badge.rb +39 -0
  118. data/lib/shadcn_view_components/generated/contracts/breadcrumb.rb +172 -0
  119. data/lib/shadcn_view_components/generated/contracts/bubble.rb +112 -0
  120. data/lib/shadcn_view_components/generated/contracts/button-group.rb +81 -0
  121. data/lib/shadcn_view_components/generated/contracts/button.rb +81 -0
  122. data/lib/shadcn_view_components/generated/contracts/card.rb +172 -0
  123. data/lib/shadcn_view_components/generated/contracts/carousel.rb +133 -0
  124. data/lib/shadcn_view_components/generated/contracts/chart.rb +109 -0
  125. data/lib/shadcn_view_components/generated/contracts/checkbox.rb +35 -0
  126. data/lib/shadcn_view_components/generated/contracts/collapsible.rb +89 -0
  127. data/lib/shadcn_view_components/generated/contracts/combobox.rb +368 -0
  128. data/lib/shadcn_view_components/generated/contracts/command.rb +222 -0
  129. data/lib/shadcn_view_components/generated/contracts/context-menu.rb +378 -0
  130. data/lib/shadcn_view_components/generated/contracts/dialog.rb +258 -0
  131. data/lib/shadcn_view_components/generated/contracts/direction.rb +34 -0
  132. data/lib/shadcn_view_components/generated/contracts/drawer.rb +280 -0
  133. data/lib/shadcn_view_components/generated/contracts/dropdown-menu.rb +380 -0
  134. data/lib/shadcn_view_components/generated/contracts/empty.rb +150 -0
  135. data/lib/shadcn_view_components/generated/contracts/field.rb +244 -0
  136. data/lib/shadcn_view_components/generated/contracts/form.rb +62 -0
  137. data/lib/shadcn_view_components/generated/contracts/hover-card.rb +90 -0
  138. data/lib/shadcn_view_components/generated/contracts/input-group.rb +155 -0
  139. data/lib/shadcn_view_components/generated/contracts/input-otp.rb +106 -0
  140. data/lib/shadcn_view_components/generated/contracts/input.rb +34 -0
  141. data/lib/shadcn_view_components/generated/contracts/item.rb +251 -0
  142. data/lib/shadcn_view_components/generated/contracts/kbd.rb +57 -0
  143. data/lib/shadcn_view_components/generated/contracts/label.rb +34 -0
  144. data/lib/shadcn_view_components/generated/contracts/marker.rb +82 -0
  145. data/lib/shadcn_view_components/generated/contracts/menubar.rb +394 -0
  146. data/lib/shadcn_view_components/generated/contracts/message-scroller.rb +149 -0
  147. data/lib/shadcn_view_components/generated/contracts/message.rb +149 -0
  148. data/lib/shadcn_view_components/generated/contracts/native-select.rb +82 -0
  149. data/lib/shadcn_view_components/generated/contracts/navigation-menu.rb +195 -0
  150. data/lib/shadcn_view_components/generated/contracts/pagination.rb +176 -0
  151. data/lib/shadcn_view_components/generated/contracts/popover.rb +159 -0
  152. data/lib/shadcn_view_components/generated/contracts/progress.rb +126 -0
  153. data/lib/shadcn_view_components/generated/contracts/radio-group.rb +58 -0
  154. data/lib/shadcn_view_components/generated/contracts/resizable.rb +85 -0
  155. data/lib/shadcn_view_components/generated/contracts/scroll-area.rb +59 -0
  156. data/lib/shadcn_view_components/generated/contracts/select.rb +245 -0
  157. data/lib/shadcn_view_components/generated/contracts/separator.rb +34 -0
  158. data/lib/shadcn_view_components/generated/contracts/sheet.rb +209 -0
  159. data/lib/shadcn_view_components/generated/contracts/sidebar.rb +558 -0
  160. data/lib/shadcn_view_components/generated/contracts/skeleton.rb +34 -0
  161. data/lib/shadcn_view_components/generated/contracts/slider.rb +38 -0
  162. data/lib/shadcn_view_components/generated/contracts/sonner.rb +36 -0
  163. data/lib/shadcn_view_components/generated/contracts/spinner.rb +34 -0
  164. data/lib/shadcn_view_components/generated/contracts/switch.rb +35 -0
  165. data/lib/shadcn_view_components/generated/contracts/table.rb +199 -0
  166. data/lib/shadcn_view_components/generated/contracts/tabs.rb +104 -0
  167. data/lib/shadcn_view_components/generated/contracts/textarea.rb +34 -0
  168. data/lib/shadcn_view_components/generated/contracts/toggle-group.rb +62 -0
  169. data/lib/shadcn_view_components/generated/contracts/toggle.rb +39 -0
  170. data/lib/shadcn_view_components/generated/contracts/tooltip.rb +116 -0
  171. data/lib/shadcn_view_components/numeric_property_validation.rb +124 -0
  172. data/lib/shadcn_view_components/property_contract_definitions.rb +92 -0
  173. data/lib/shadcn_view_components/property_contracts.rb +42 -0
  174. data/lib/shadcn_view_components/property_validation.rb +55 -0
  175. data/lib/shadcn_view_components/version.rb +6 -0
  176. data/lib/shadcn_view_components.rb +21 -0
  177. metadata +446 -0
@@ -0,0 +1,208 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ import {
4
+ ensureId,
5
+ ensureRootId,
6
+ ownedElements,
7
+ setDefaultAttribute,
8
+ } from "@supermomonga/shadcn-view-components/aria_relationships"
9
+ import { hideAfterExit } from "@supermomonga/shadcn-view-components/hide_after_exit"
10
+
11
+ const noop = () => {}
12
+ const IDENTIFIER = "shadcn--dialog"
13
+ const TRIGGER_SELECTOR = [
14
+ "[data-slot='dialog-trigger']",
15
+ "[data-slot='alert-dialog-trigger']",
16
+ "[data-slot='sheet-trigger']",
17
+ "[data-slot='drawer-trigger']",
18
+ ].join(", ")
19
+ const TITLE_SELECTOR = [
20
+ "[data-slot='dialog-title']",
21
+ "[data-slot='alert-dialog-title']",
22
+ "[data-slot='sheet-title']",
23
+ "[data-slot='drawer-title']",
24
+ ].join(", ")
25
+ const DESCRIPTION_SELECTOR = [
26
+ "[data-slot='dialog-description']",
27
+ "[data-slot='alert-dialog-description']",
28
+ "[data-slot='sheet-description']",
29
+ "[data-slot='drawer-description']",
30
+ ].join(", ")
31
+ const CONTENT_SELECTOR = [
32
+ "[data-slot='dialog-content']",
33
+ "[data-slot='alert-dialog-content']",
34
+ "[data-slot='sheet-content']",
35
+ "[data-slot='drawer-content']",
36
+ ].join(", ")
37
+ const STATE_SELECTOR = [
38
+ CONTENT_SELECTOR,
39
+ "[data-slot='dialog-overlay']",
40
+ "[data-slot='alert-dialog-overlay']",
41
+ "[data-slot='sheet-overlay']",
42
+ "[data-slot='drawer-overlay']",
43
+ ].join(", ")
44
+
45
+ // ネイティブ <dialog> の開閉(05-stimulus-hotwire §3)。
46
+ // showModal がフォーカストラップ・背景inert・EscClose・フォーカス復帰を提供するため、
47
+ // このコントローラは「開く」「閉じる」「状態属性の同期」だけを担う。
48
+ // 閉じる際は契約クラスの退出アニメーション(data-[state=closed]:animate-out)を
49
+ // 待ってから dialog.close() する(即時 close するとアニメーションが見えない)
50
+ export default class DialogController extends Controller {
51
+ /** @type {HTMLDialogElement | null} */
52
+ dialog = null
53
+
54
+ /** @type {() => void} */
55
+ cancelExit = noop
56
+
57
+ /** @type {() => void} */
58
+ onClose = () => {
59
+ this.cancelPendingExit()
60
+ this.syncState("closed")
61
+ }
62
+
63
+ /** @type {(event: Event) => void} */
64
+ onSubmitEnd = (event) => {
65
+ if (!(event.target instanceof Element) || !this.dialog?.contains(event.target)) return
66
+ if (event.target.closest(`[data-controller~='${IDENTIFIER}']`) !== this.root) return
67
+
68
+ this.close()
69
+ }
70
+
71
+ connect() {
72
+ this.cancelPendingExit()
73
+ this.dialog = /** @type {HTMLDialogElement | null} */ (
74
+ ownedElements(this.root, "dialog", IDENTIFIER)[0] || null
75
+ )
76
+ this.ensureRelationships()
77
+ this.dialog?.addEventListener("close", this.onClose)
78
+ // 標準フック: dialog内のフォーム送信完了(turbo:submit-end)で閉じる(10-roadmap Phase 3)
79
+ this.root.addEventListener("turbo:submit-end", this.onSubmitEnd)
80
+ this.syncState("closed")
81
+ }
82
+
83
+ disconnect() {
84
+ const dialog = this.dialog
85
+ const content = this.contentTarget()
86
+ const finishExit = dialog?.open && content?.hasAttribute("data-closed")
87
+ this.cancelPendingExit()
88
+ dialog?.removeEventListener("close", this.onClose)
89
+ this.root.removeEventListener("turbo:submit-end", this.onSubmitEnd)
90
+ if (finishExit) dialog.close()
91
+ this.dialog = null
92
+ }
93
+
94
+ show() {
95
+ const dialog = this.dialog
96
+ if (!dialog) return
97
+
98
+ this.ensureRelationships()
99
+ this.cancelPendingExit()
100
+ if (!dialog.open) dialog.showModal()
101
+ this.syncState("open")
102
+ }
103
+
104
+ // 状態属性の対象: dialog自身と、その中の content / overlay 系スロット。
105
+ // SSRでは状態属性(data-open / data-closed)を出さないためスロット名で収集する
106
+ /** @returns {HTMLElement[]} */
107
+ stateTargets() {
108
+ if (!this.dialog) return []
109
+
110
+ const inner = ownedElements(
111
+ this.root,
112
+ STATE_SELECTOR,
113
+ IDENTIFIER,
114
+ ).filter((element) => element !== this.dialog && this.dialog?.contains(element))
115
+ return [...inner, this.dialog]
116
+ }
117
+
118
+ close() {
119
+ const dialog = this.dialog
120
+ if (!dialog?.open) return
121
+
122
+ this.cancelPendingExit()
123
+ // 先に閉状態へ(animate-out のトリガ)。dialog.open はまだ true のため
124
+ // syncState の実開状態ガードは使えず、ここでは直接書き換える
125
+ for (const element of this.stateTargets()) this.applyState(element, "closed")
126
+
127
+ const content = this.contentTarget()
128
+ this.cancelExit = hideAfterExit(content, () => {
129
+ if (this.dialog !== dialog || content?.hasAttribute("data-open")) return
130
+ dialog.close()
131
+ })
132
+ }
133
+
134
+ cancelPendingExit() {
135
+ this.cancelExit()
136
+ this.cancelExit = noop
137
+ }
138
+
139
+ // Turboキャッシュ復帰に備え、状態属性は冪等に再計算する(05 §6.2)
140
+ /** @param {"open" | "closed"} state */
141
+ syncState(state) {
142
+ if (this.dialog && this.dialog.open !== (state === "open")) {
143
+ // closeイベントを経由せず状態がずれた場合(ブラウザバック等)は実際の開状態を優先する
144
+ state = this.dialog.open ? "open" : "closed"
145
+ }
146
+ for (const element of this.stateTargets()) this.applyState(element, state)
147
+ for (const trigger of this.triggers()) {
148
+ trigger.setAttribute("aria-expanded", String(state === "open"))
149
+ }
150
+ }
151
+
152
+ ensureRelationships() {
153
+ const dialog = this.dialog
154
+ if (!dialog) return
155
+
156
+ const rootId = ensureRootId(this.root, "dialog")
157
+ const dialogId = ensureId(dialog, `${rootId}-content`)
158
+ const title = this.ownedWithinDialog(TITLE_SELECTOR)[0]
159
+ const description = this.ownedWithinDialog(DESCRIPTION_SELECTOR)[0]
160
+
161
+ for (const trigger of this.triggers()) setDefaultAttribute(trigger, "aria-controls", dialogId)
162
+
163
+ if (title) {
164
+ const titleId = ensureId(title, `${rootId}-title`)
165
+ if (!dialog.hasAttribute("aria-label")) setDefaultAttribute(dialog, "aria-labelledby", titleId)
166
+ }
167
+ if (description) {
168
+ const descriptionId = ensureId(description, `${rootId}-description`)
169
+ if (!dialog.hasAttribute("aria-description")) {
170
+ setDefaultAttribute(dialog, "aria-describedby", descriptionId)
171
+ }
172
+ }
173
+ }
174
+
175
+ /** @returns {HTMLElement[]} */
176
+ triggers() {
177
+ return ownedElements(this.root, TRIGGER_SELECTOR, IDENTIFIER)
178
+ }
179
+
180
+ /** @param {string} selector @returns {HTMLElement[]} */
181
+ ownedWithinDialog(selector) {
182
+ return ownedElements(this.root, selector, IDENTIFIER)
183
+ .filter((element) => this.dialog?.contains(element))
184
+ }
185
+
186
+ /** @returns {HTMLElement | null} */
187
+ contentTarget() {
188
+ if (this.dialog?.matches(CONTENT_SELECTOR)) return this.dialog
189
+
190
+ return this.ownedWithinDialog(CONTENT_SELECTOR)[0] || null
191
+ }
192
+
193
+ // base-nova の契約クラスは data-open / data-closed(属性の存在)を参照する
194
+ // (data-open:animate-in data-closed:animate-out 等)
195
+ /**
196
+ * @param {HTMLElement} element
197
+ * @param {"open" | "closed"} state
198
+ */
199
+ applyState(element, state) {
200
+ element.toggleAttribute("data-open", state === "open")
201
+ element.toggleAttribute("data-closed", state !== "open")
202
+ }
203
+
204
+ /** @returns {HTMLElement} */
205
+ get root() {
206
+ return /** @type {HTMLElement} */ (this.element)
207
+ }
208
+ }
@@ -0,0 +1,145 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ import { startFloatingPosition } from "@supermomonga/shadcn-view-components/floating_position"
4
+ import { hideAfterExit } from "@supermomonga/shadcn-view-components/hide_after_exit"
5
+ import { applyStateAttrs } from "@supermomonga/shadcn-view-components/state_attrs"
6
+
7
+ const noop = () => {}
8
+
9
+ // ホバーインテント: trigger と content のどちらにいるかを遅延つきで判定し、
10
+ // 素早い通り抜けでは表示しない(05-stimulus-hotwire §3)。
11
+ // 閉じる際は退出アニメーション(data-[state=closed]:animate-out)を待ってから
12
+ // hidden 属性を付ける
13
+ export default class HoverCardController extends Controller {
14
+ /** @type {HTMLElement | null} */
15
+ content = null
16
+
17
+ /** @type {HTMLElement | null} */
18
+ trigger = null
19
+
20
+ /** @type {ReturnType<typeof setTimeout> | undefined} */
21
+ showTimer
22
+
23
+ /** @type {ReturnType<typeof setTimeout> | undefined} */
24
+ hideTimer
25
+
26
+ /** @type {() => void} */
27
+ cancelExit = noop
28
+
29
+ /** @type {{ update: () => void, destroy: () => void } | null} */
30
+ positioning = null
31
+
32
+ connect() {
33
+ this.cancelPendingWork()
34
+ this.stopPositioning()
35
+ this.content = this.element.querySelector("[data-slot='hover-card-content']")
36
+ this.trigger = this.element.querySelector("[data-slot='hover-card-trigger']")
37
+ }
38
+
39
+ disconnect() {
40
+ const content = this.content
41
+ const finishHide = this.hideTimer !== undefined
42
+ const finishExit = content?.dataset.state === "closed" && !content.hidden
43
+ this.cancelPendingWork()
44
+ this.stopPositioning()
45
+ if (content && (finishHide || finishExit)) {
46
+ content.dataset.state = "closed"
47
+ applyStateAttrs(content, "closed")
48
+ content.hidden = true
49
+ }
50
+ this.content = null
51
+ this.trigger = null
52
+ }
53
+
54
+ show() {
55
+ this.clearHideTimer()
56
+ this.cancelPendingExit()
57
+ if (!this.content || this.content.dataset.state === "open") return
58
+
59
+ this.clearShowTimer()
60
+ const content = this.content
61
+ this.showTimer = setTimeout(() => {
62
+ this.showTimer = undefined
63
+ if (this.content === content) this.setState("open")
64
+ }, 100)
65
+ }
66
+
67
+ hide() {
68
+ this.clearShowTimer()
69
+ if (!this.content || this.content.dataset.state === "closed") return
70
+
71
+ this.clearHideTimer()
72
+ const content = this.content
73
+ this.hideTimer = setTimeout(() => {
74
+ this.hideTimer = undefined
75
+ if (this.content !== content) return
76
+
77
+ content.dataset.state = "closed"
78
+ applyStateAttrs(content, "closed")
79
+ this.cancelPendingExit()
80
+ this.cancelExit = hideAfterExit(content, () => {
81
+ if (this.content !== content || content.dataset.state === "open") return
82
+ this.stopPositioning()
83
+ content.hidden = true
84
+ })
85
+ }, 150)
86
+ }
87
+
88
+ /** @param {"open" | "closed"} state */
89
+ setState(state) {
90
+ const content = this.content
91
+ if (!content) return
92
+ if (state === "open") this.cancelPendingExit()
93
+
94
+ content.dataset.state = state
95
+ applyStateAttrs(content, state)
96
+ content.hidden = state === "closed"
97
+ if (state === "open") this.startPositioning()
98
+ else this.stopPositioning()
99
+ }
100
+
101
+ startPositioning() {
102
+ const content = this.content
103
+ const trigger = this.trigger
104
+ if (!content || !trigger) return
105
+
106
+ this.stopPositioning()
107
+ this.positioning = startFloatingPosition({
108
+ align: "center",
109
+ alignOffset: 4,
110
+ anchor: trigger,
111
+ collisionPadding: 5,
112
+ floating: content,
113
+ side: "bottom",
114
+ sideOffset: 4,
115
+ })
116
+ }
117
+
118
+ stopPositioning() {
119
+ this.positioning?.destroy()
120
+ this.positioning = null
121
+ }
122
+
123
+ clearShowTimer() {
124
+ if (this.showTimer === undefined) return
125
+ clearTimeout(this.showTimer)
126
+ this.showTimer = undefined
127
+ }
128
+
129
+ clearHideTimer() {
130
+ if (this.hideTimer === undefined) return
131
+ clearTimeout(this.hideTimer)
132
+ this.hideTimer = undefined
133
+ }
134
+
135
+ cancelPendingExit() {
136
+ this.cancelExit()
137
+ this.cancelExit = noop
138
+ }
139
+
140
+ cancelPendingWork() {
141
+ this.clearShowTimer()
142
+ this.clearHideTimer()
143
+ this.cancelPendingExit()
144
+ }
145
+ }