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,146 @@
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
+
10
+ const IDENTIFIER = "shadcn--tabs"
11
+
12
+ // ARIA tabs パターン(05-stimulus-hotwire §4)。
13
+ // - クリック / Enter / Space で選択
14
+ // - 左右(縦布局は上下)矢印で roving tabindex による移動
15
+ // - 選択状態は data-active(属性の存在)、パネル表示は hidden 属性で表現する
16
+ /** @extends {Controller<HTMLElement>} */
17
+ export default class TabsController extends Controller {
18
+ connect() {
19
+ this.ensureRelationships()
20
+ const triggers = this.triggers
21
+ if (triggers.length === 0) return
22
+ // サーバ側で active 指定があればそれを、無ければ先頭を採用してパネル表示を同期する
23
+ const active = triggers.find((trigger) => trigger.hasAttribute("data-active")) || triggers[0]
24
+ this.activate(active)
25
+ }
26
+
27
+ /** @param {Event} event */
28
+ select(event) {
29
+ const selected = /** @type {HTMLElement} */ (event.currentTarget)
30
+ if (this.triggers.includes(selected)) this.activate(selected)
31
+ }
32
+
33
+ /** @param {KeyboardEvent} event */
34
+ navigate(event) {
35
+ const keys = this.vertical ? ["ArrowUp", "ArrowDown"] : ["ArrowLeft", "ArrowRight"]
36
+ if (![...keys, "Home", "End"].includes(event.key)) return
37
+ event.preventDefault()
38
+
39
+ const triggers = this.triggers
40
+ const current = triggers.indexOf(/** @type {HTMLElement} */ (event.currentTarget))
41
+ if (current < 0) return
42
+
43
+ let next
44
+ if (event.key === "Home") {
45
+ next = triggers[0]
46
+ } else if (event.key === "End") {
47
+ next = triggers[triggers.length - 1]
48
+ } else {
49
+ const offset = event.key === keys[1] ? 1 : -1
50
+ next = triggers[(current + offset + triggers.length) % triggers.length]
51
+ }
52
+ next.focus()
53
+ this.activate(next)
54
+ }
55
+
56
+ /** @param {HTMLElement} selected */
57
+ activate(selected) {
58
+ if (!this.triggers.includes(selected)) return
59
+
60
+ for (const trigger of this.allTriggers) {
61
+ const active = trigger === selected
62
+ trigger.toggleAttribute("data-active", active)
63
+ trigger.setAttribute("aria-selected", String(active))
64
+ trigger.tabIndex = active ? 0 : -1
65
+ }
66
+
67
+ const value = selected.dataset.value
68
+ for (const panel of this.panels) {
69
+ panel.hidden = panel.dataset.value !== value
70
+ }
71
+ }
72
+
73
+ ensureRelationships() {
74
+ const rootId = ensureRootId(this.root, "tabs")
75
+ this.validateRelationships()
76
+
77
+ for (const [index, trigger] of this.allTriggers.entries()) {
78
+ const panel = this.panels.find((candidate) => candidate.dataset.value === trigger.dataset.value)
79
+ if (!panel) continue
80
+
81
+ const triggerId = ensureId(trigger, `${rootId}-trigger-${index + 1}`)
82
+ const panelId = ensureId(panel, `${rootId}-panel-${index + 1}`)
83
+ setDefaultAttribute(trigger, "aria-controls", panelId)
84
+ setDefaultAttribute(panel, "aria-labelledby", triggerId)
85
+ setDefaultAttribute(panel, "tabindex", "0")
86
+ }
87
+
88
+ for (const list of this.lists) {
89
+ setDefaultAttribute(list, "aria-orientation", this.vertical ? "vertical" : "horizontal")
90
+ }
91
+ }
92
+
93
+ validateRelationships() {
94
+ const triggerCounts = this.valueCounts(this.allTriggers)
95
+ const panelCounts = this.valueCounts(this.panels)
96
+ const values = new Set([...triggerCounts.keys(), ...panelCounts.keys()])
97
+
98
+ for (const value of values) {
99
+ if (value === "" || triggerCounts.get(value) !== 1 || panelCounts.get(value) !== 1) {
100
+ throw new Error(`Tabs value ${JSON.stringify(value)} must identify exactly one trigger and one panel`)
101
+ }
102
+ }
103
+ }
104
+
105
+ /** @param {HTMLElement[]} elements @returns {Map<string, number>} */
106
+ valueCounts(elements) {
107
+ const counts = new Map()
108
+ for (const element of elements) {
109
+ const value = element.dataset.value || ""
110
+ counts.set(value, (counts.get(value) || 0) + 1)
111
+ }
112
+ return counts
113
+ }
114
+
115
+ /** @returns {HTMLElement[]} */
116
+ get triggers() {
117
+ return this.allTriggers.filter(
118
+ (trigger) => !trigger.hasAttribute("disabled") && trigger.getAttribute("aria-disabled") !== "true",
119
+ )
120
+ }
121
+
122
+ /** @returns {HTMLElement[]} */
123
+ get allTriggers() {
124
+ return ownedElements(this.root, "[data-slot='tabs-trigger'][role='tab']", IDENTIFIER)
125
+ }
126
+
127
+ /** @returns {HTMLElement[]} */
128
+ get panels() {
129
+ return ownedElements(this.root, "[data-slot='tabs-content'][role='tabpanel']", IDENTIFIER)
130
+ }
131
+
132
+ /** @returns {HTMLElement[]} */
133
+ get lists() {
134
+ return ownedElements(this.root, "[data-slot='tabs-list'][role='tablist']", IDENTIFIER)
135
+ }
136
+
137
+ /** @returns {boolean} */
138
+ get vertical() {
139
+ return this.element.dataset.orientation === "vertical"
140
+ }
141
+
142
+ /** @returns {HTMLElement} */
143
+ get root() {
144
+ return /** @type {HTMLElement} */ (this.element)
145
+ }
146
+ }
@@ -0,0 +1,81 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * @typedef {object} ToastDetail
5
+ * @property {string=} title
6
+ * @property {string=} description
7
+ * @property {number=} duration
8
+ */
9
+
10
+ // 通知領域(sonner相当)。shadcn:toast CustomEvent でトーストを追加する:
11
+ // dispatchEvent(new CustomEvent("shadcn:toast", { detail: { title, description } }))
12
+ export default class ToastController extends Controller {
13
+ /** @type {Map<HTMLElement, ReturnType<typeof setTimeout>>} */
14
+ timers = new Map()
15
+
16
+ /** @type {Set<HTMLElement>} */
17
+ toasts = new Set()
18
+
19
+ /** @type {(event: Event) => void} */
20
+ onToast = (event) => {
21
+ if (!("detail" in event)) return
22
+ const customEvent = /** @type {CustomEvent<ToastDetail>} */ (event)
23
+ this.push(customEvent.detail ?? {})
24
+ }
25
+
26
+ connect() {
27
+ window.addEventListener("shadcn:toast", this.onToast)
28
+ }
29
+
30
+ disconnect() {
31
+ window.removeEventListener("shadcn:toast", this.onToast)
32
+ for (const toast of [...this.toasts]) this.removeToast(toast)
33
+ }
34
+
35
+ /** @param {ToastDetail} detail */
36
+ push({ title, description, duration = 4000 }) {
37
+ const toast = document.createElement("article")
38
+ toast.className = "rounded-md border bg-background px-4 py-3 text-sm shadow-lg"
39
+ toast.setAttribute("role", "status")
40
+ if (title) {
41
+ const heading = document.createElement("div")
42
+ heading.className = "font-medium"
43
+ heading.textContent = title
44
+ toast.appendChild(heading)
45
+ }
46
+ if (description) {
47
+ const body = document.createElement("div")
48
+ body.className = "text-muted-foreground"
49
+ body.textContent = description
50
+ toast.appendChild(body)
51
+ }
52
+ this.root.appendChild(toast)
53
+ this.toasts.add(toast)
54
+ if (duration > 0) {
55
+ const timer = setTimeout(() => this.removeToast(toast), duration)
56
+ this.timers.set(toast, timer)
57
+ }
58
+ }
59
+
60
+ /** @param {Event} event */
61
+ dismiss(event) {
62
+ if (!(event.currentTarget instanceof Element)) return
63
+
64
+ const toast = event.currentTarget.closest("article")
65
+ if (toast) this.removeToast(/** @type {HTMLElement} */ (toast))
66
+ }
67
+
68
+ /** @param {HTMLElement} toast */
69
+ removeToast(toast) {
70
+ const timer = this.timers.get(toast)
71
+ if (timer !== undefined) clearTimeout(timer)
72
+ this.timers.delete(toast)
73
+ this.toasts.delete(toast)
74
+ toast.remove()
75
+ }
76
+
77
+ /** @returns {HTMLElement} */
78
+ get root() {
79
+ return /** @type {HTMLElement} */ (this.element)
80
+ }
81
+ }
@@ -0,0 +1,21 @@
1
+ // shadcn--toggle: toggle の開閉状態(on/off)を data-state / aria-pressed で管理する。
2
+ // 状態はDOM自身に持たせ、destroy時の後始末は不要(05-stimulus-hotwire §2.3)。
3
+ // Stimulus本体はホストのものをimportmap等で解決する(本gemはバンドルしない)
4
+ import { Controller } from "@hotwired/stimulus"
5
+
6
+ /** @extends {Controller<HTMLElement>} */
7
+ export default class ToggleController extends Controller {
8
+ connect() {
9
+ this.element.setAttribute("aria-pressed", this.isOn ? "true" : "false")
10
+ }
11
+
12
+ toggle() {
13
+ this.element.dataset.state = this.isOn ? "off" : "on"
14
+ this.element.setAttribute("aria-pressed", String(this.isOn))
15
+ }
16
+
17
+ /** @returns {boolean} */
18
+ get isOn() {
19
+ return this.element.dataset.state === "on"
20
+ }
21
+ }
@@ -0,0 +1,37 @@
1
+ // shadcn--toggle-group: グループ内の item(data-slot="toggle-group-item")の開閉を管理する。
2
+ // type: single では常に1つのみ on(排他)。multiple では独立してトグルする。
3
+ // 状態はDOM自身(data-type / data-state)に持たせる(05-stimulus-hotwire §2.3)
4
+ import { Controller } from "@hotwired/stimulus"
5
+
6
+ /** @extends {Controller<HTMLElement>} */
7
+ export default class ToggleGroupController extends Controller {
8
+ /** @param {Event} event */
9
+ toggleItem(event) {
10
+ const item = /** @type {HTMLElement} */ (event.currentTarget)
11
+ const isOn = item.dataset.state === "on"
12
+
13
+ if (this.isSingle && !isOn) {
14
+ this.items.forEach((element) => this.setState(element, false))
15
+ }
16
+ this.setState(item, !isOn)
17
+ }
18
+
19
+ /** @returns {boolean} */
20
+ get isSingle() {
21
+ return this.element.dataset.type === "single"
22
+ }
23
+
24
+ /** @returns {NodeListOf<HTMLElement>} */
25
+ get items() {
26
+ return this.element.querySelectorAll("[data-slot='toggle-group-item']")
27
+ }
28
+
29
+ /**
30
+ * @param {HTMLElement} element
31
+ * @param {boolean} on
32
+ */
33
+ setState(element, on) {
34
+ element.dataset.state = on ? "on" : "off"
35
+ element.setAttribute("aria-pressed", String(on))
36
+ }
37
+ }
@@ -0,0 +1,123 @@
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
+ // ツールチップの遅延制御(既定 delayDuration=0 — upstream と同じ)。
10
+ // aria-describedby で trigger と内容を結合する(05 §4)。
11
+ // 閉じる際は退出アニメーション(data-[state=closed]:animate-out)を待ってから
12
+ // hidden 属性を付ける(即時 hidden にするとアニメーションが見えない)
13
+ export default class TooltipController 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 {() => void} */
24
+ cancelExit = noop
25
+
26
+ /** @type {{ update: () => void, destroy: () => void } | null} */
27
+ positioning = null
28
+
29
+ connect() {
30
+ this.cancelPendingWork()
31
+ this.stopPositioning()
32
+ this.content = this.element.querySelector("[data-slot='tooltip-content']")
33
+ this.trigger = this.element.querySelector("[data-slot='tooltip-trigger']")
34
+ if (this.content?.id && this.trigger) this.trigger.setAttribute("aria-describedby", this.content.id)
35
+ }
36
+
37
+ disconnect() {
38
+ const content = this.content
39
+ const finishExit = content?.dataset.state === "closed" && !content.hidden
40
+ this.cancelPendingWork()
41
+ this.stopPositioning()
42
+ if (finishExit) content.hidden = true
43
+ this.content = null
44
+ this.trigger = null
45
+ }
46
+
47
+ show() {
48
+ this.cancelPendingExit()
49
+ if (!this.content || this.content.dataset.state === "open") return
50
+
51
+ this.clearShowTimer()
52
+ const content = this.content
53
+ this.showTimer = setTimeout(() => {
54
+ this.showTimer = undefined
55
+ if (this.content === content) this.setState("open")
56
+ }, 0)
57
+ }
58
+
59
+ hide() {
60
+ this.clearShowTimer()
61
+ const content = this.content
62
+ if (!content || content.hidden || content.dataset.state === "closed") return
63
+
64
+ this.cancelPendingExit()
65
+ content.dataset.state = "closed"
66
+ applyStateAttrs(content, "closed")
67
+ this.cancelExit = hideAfterExit(content, () => {
68
+ if (this.content !== content || content.dataset.state === "open") return
69
+ this.stopPositioning()
70
+ content.hidden = true
71
+ })
72
+ }
73
+
74
+ /** @param {"open" | "closed"} state */
75
+ setState(state) {
76
+ const content = this.content
77
+ if (!content) return
78
+ if (state === "open") this.cancelPendingExit()
79
+
80
+ content.dataset.state = state
81
+ applyStateAttrs(content, state)
82
+ content.hidden = state === "closed"
83
+ if (state === "open") this.startPositioning()
84
+ else this.stopPositioning()
85
+ }
86
+
87
+ startPositioning() {
88
+ const content = this.content
89
+ const trigger = this.trigger
90
+ if (!content || !trigger) return
91
+
92
+ this.stopPositioning()
93
+ this.positioning = startFloatingPosition({
94
+ align: "center",
95
+ anchor: trigger,
96
+ collisionPadding: 5,
97
+ floating: content,
98
+ side: "top",
99
+ sideOffset: 4,
100
+ })
101
+ }
102
+
103
+ stopPositioning() {
104
+ this.positioning?.destroy()
105
+ this.positioning = null
106
+ }
107
+
108
+ clearShowTimer() {
109
+ if (this.showTimer === undefined) return
110
+ clearTimeout(this.showTimer)
111
+ this.showTimer = undefined
112
+ }
113
+
114
+ cancelPendingExit() {
115
+ this.cancelExit()
116
+ this.cancelExit = noop
117
+ }
118
+
119
+ cancelPendingWork() {
120
+ this.clearShowTimer()
121
+ this.cancelPendingExit()
122
+ }
123
+ }