shadcn_phlexcomponents 0.1.5 → 0.1.11

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 (249) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -0
  3. data/app/javascript/controllers/accordion_controller.ts +133 -0
  4. data/app/javascript/controllers/{avatar_controller.js → avatar_controller.ts} +4 -0
  5. data/app/javascript/controllers/checkbox_controller.ts +34 -0
  6. data/app/javascript/controllers/collapsible_controller.ts +45 -0
  7. data/app/javascript/controllers/combobox_controller.ts +145 -0
  8. data/app/javascript/controllers/command_controller.ts +129 -0
  9. data/app/javascript/controllers/command_root_controller.ts +355 -0
  10. data/app/javascript/controllers/date_picker_controller.ts +274 -0
  11. data/app/javascript/controllers/date_range_picker_controller.ts +243 -0
  12. data/app/javascript/controllers/dialog_controller.ts +113 -0
  13. data/app/javascript/controllers/dropdown_menu_controller.ts +133 -0
  14. data/app/javascript/controllers/dropdown_menu_root_controller.ts +234 -0
  15. data/app/javascript/controllers/dropdown_menu_sub_controller.ts +150 -0
  16. data/app/javascript/controllers/form_field_controller.ts +22 -0
  17. data/app/javascript/controllers/hover_card_controller.ts +93 -0
  18. data/app/javascript/controllers/{loading_button_controller.js → loading_button_controller.ts} +2 -2
  19. data/app/javascript/controllers/popover_controller.ts +141 -0
  20. data/app/javascript/controllers/progress_controller.ts +17 -0
  21. data/app/javascript/controllers/radio_group_controller.ts +106 -0
  22. data/app/javascript/controllers/select_controller.ts +200 -0
  23. data/app/javascript/controllers/{sidebar_controller.js → sidebar_controller.ts} +6 -2
  24. data/app/javascript/controllers/sidebar_trigger_controller.ts +21 -0
  25. data/app/javascript/controllers/slider_controller.ts +107 -0
  26. data/app/javascript/controllers/switch_controller.ts +30 -0
  27. data/app/javascript/controllers/tabs_controller.ts +79 -0
  28. data/app/javascript/controllers/{theme_switcher_controller.js → theme_switcher_controller.ts} +12 -9
  29. data/app/javascript/controllers/toast_container_controller.ts +62 -0
  30. data/app/javascript/controllers/toast_controller.ts +28 -0
  31. data/app/javascript/controllers/tooltip_controller.ts +98 -0
  32. data/app/javascript/shadcn_phlexcomponents.ts +57 -0
  33. data/app/javascript/utils.ts +437 -0
  34. data/app/stylesheets/date_picker.css +74 -0
  35. data/app/stylesheets/nouislider.css +173 -0
  36. data/app/stylesheets/tw-animate.css +486 -0
  37. data/lib/install/install_shadcn_phlexcomponents.rb +22 -9
  38. data/lib/shadcn_phlexcomponents/alias.rb +3 -1
  39. data/lib/shadcn_phlexcomponents/components/accordion.rb +129 -0
  40. data/lib/shadcn_phlexcomponents/components/alert.rb +59 -0
  41. data/lib/shadcn_phlexcomponents/components/alert_dialog.rb +276 -0
  42. data/lib/{components → shadcn_phlexcomponents/components}/aspect_ratio.rb +2 -2
  43. data/lib/shadcn_phlexcomponents/components/avatar.rb +63 -0
  44. data/lib/shadcn_phlexcomponents/components/badge.rb +35 -0
  45. data/lib/{components → shadcn_phlexcomponents/components}/base.rb +44 -7
  46. data/lib/shadcn_phlexcomponents/components/breadcrumb.rb +150 -0
  47. data/lib/shadcn_phlexcomponents/components/button.rb +49 -0
  48. data/lib/shadcn_phlexcomponents/components/card.rb +88 -0
  49. data/lib/{components → shadcn_phlexcomponents/components}/checkbox.rb +21 -17
  50. data/lib/{components → shadcn_phlexcomponents/components}/checkbox_group.rb +27 -16
  51. data/lib/shadcn_phlexcomponents/components/collapsible.rb +91 -0
  52. data/lib/shadcn_phlexcomponents/components/combobox.rb +398 -0
  53. data/lib/shadcn_phlexcomponents/components/command.rb +351 -0
  54. data/lib/shadcn_phlexcomponents/components/date_picker.rb +264 -0
  55. data/lib/shadcn_phlexcomponents/components/date_range_picker.rb +126 -0
  56. data/lib/shadcn_phlexcomponents/components/dialog.rb +234 -0
  57. data/lib/shadcn_phlexcomponents/components/dropdown_menu.rb +282 -0
  58. data/lib/shadcn_phlexcomponents/components/dropdown_menu_sub.rb +135 -0
  59. data/lib/shadcn_phlexcomponents/components/form/form_checkbox.rb +82 -0
  60. data/lib/shadcn_phlexcomponents/components/form/form_checkbox_group.rb +116 -0
  61. data/lib/shadcn_phlexcomponents/components/form/form_date_picker.rb +46 -0
  62. data/lib/shadcn_phlexcomponents/components/form/form_date_range_picker.rb +82 -0
  63. data/lib/{components → shadcn_phlexcomponents/components/form}/form_error.rb +7 -3
  64. data/lib/shadcn_phlexcomponents/components/form/form_helpers.rb +143 -0
  65. data/lib/shadcn_phlexcomponents/components/form/form_hint.rb +21 -0
  66. data/lib/{components → shadcn_phlexcomponents/components/form}/form_input.rb +3 -4
  67. data/lib/shadcn_phlexcomponents/components/form/form_radio_group.rb +106 -0
  68. data/lib/shadcn_phlexcomponents/components/form/form_select.rb +64 -0
  69. data/lib/shadcn_phlexcomponents/components/form/form_slider.rb +91 -0
  70. data/lib/shadcn_phlexcomponents/components/form/form_switch.rb +67 -0
  71. data/lib/shadcn_phlexcomponents/components/form/form_textarea.rb +59 -0
  72. data/lib/shadcn_phlexcomponents/components/form.rb +157 -0
  73. data/lib/shadcn_phlexcomponents/components/hover_card.rb +110 -0
  74. data/lib/shadcn_phlexcomponents/components/input.rb +31 -0
  75. data/lib/shadcn_phlexcomponents/components/label.rb +16 -0
  76. data/lib/{components → shadcn_phlexcomponents/components}/link.rb +10 -3
  77. data/lib/shadcn_phlexcomponents/components/loading_button.rb +28 -0
  78. data/lib/shadcn_phlexcomponents/components/pagination.rb +166 -0
  79. data/lib/shadcn_phlexcomponents/components/popover.rb +116 -0
  80. data/lib/{components → shadcn_phlexcomponents/components}/progress.rb +5 -5
  81. data/lib/shadcn_phlexcomponents/components/radio_group.rb +155 -0
  82. data/lib/shadcn_phlexcomponents/components/select.rb +421 -0
  83. data/lib/{components → shadcn_phlexcomponents/components}/separator.rb +9 -8
  84. data/lib/shadcn_phlexcomponents/components/sheet.rb +239 -0
  85. data/lib/{components → shadcn_phlexcomponents/components}/skeleton.rb +1 -1
  86. data/lib/shadcn_phlexcomponents/components/slider.rb +72 -0
  87. data/lib/shadcn_phlexcomponents/components/switch.rb +75 -0
  88. data/lib/shadcn_phlexcomponents/components/table.rb +140 -0
  89. data/lib/shadcn_phlexcomponents/components/tabs.rb +135 -0
  90. data/lib/shadcn_phlexcomponents/components/textarea.rb +24 -0
  91. data/lib/{components → shadcn_phlexcomponents/components}/theme_switcher.rb +2 -2
  92. data/lib/shadcn_phlexcomponents/components/toast.rb +153 -0
  93. data/lib/{components → shadcn_phlexcomponents/components}/toast_container.rb +24 -5
  94. data/lib/shadcn_phlexcomponents/components/tooltip.rb +131 -0
  95. data/lib/shadcn_phlexcomponents/initializers/shadcn_phlexcomponents.rb +25 -0
  96. data/lib/shadcn_phlexcomponents/version.rb +1 -1
  97. data/lib/tasks/install.rake +1 -1
  98. metadata +92 -168
  99. data/app/assets/tailwind/choices.css +0 -324
  100. data/app/assets/tailwind/tailwindcss-animate.css +0 -318
  101. data/app/assets/tailwind/vanilla-calendar-pro.css +0 -466
  102. data/app/javascript/controllers/accordion_controller.js +0 -133
  103. data/app/javascript/controllers/alert_dialog_controller.js +0 -157
  104. data/app/javascript/controllers/checkbox_controller.js +0 -28
  105. data/app/javascript/controllers/collapsible_controller.js +0 -35
  106. data/app/javascript/controllers/combobox_controller.js +0 -34
  107. data/app/javascript/controllers/date_picker_controller.js +0 -118
  108. data/app/javascript/controllers/date_range_picker_controller.js +0 -231
  109. data/app/javascript/controllers/dialog_controller.js +0 -159
  110. data/app/javascript/controllers/dropdown_menu_controller.js +0 -193
  111. data/app/javascript/controllers/hover_card_controller.js +0 -42
  112. data/app/javascript/controllers/popover_controller.js +0 -124
  113. data/app/javascript/controllers/progress_controller.js +0 -14
  114. data/app/javascript/controllers/radio_group_controller.js +0 -90
  115. data/app/javascript/controllers/select_controller.js +0 -294
  116. data/app/javascript/controllers/sheet_controller.js +0 -159
  117. data/app/javascript/controllers/sidebar_trigger_controller.js +0 -15
  118. data/app/javascript/controllers/switch_controller.js +0 -24
  119. data/app/javascript/controllers/tabs_controller.js +0 -73
  120. data/app/javascript/controllers/toast_container_controller.js +0 -22
  121. data/app/javascript/controllers/toast_controller.js +0 -45
  122. data/app/javascript/controllers/tooltip_controller.js +0 -41
  123. data/lib/components/accordion.rb +0 -38
  124. data/lib/components/accordion_content.rb +0 -30
  125. data/lib/components/accordion_item.rb +0 -26
  126. data/lib/components/accordion_trigger.rb +0 -45
  127. data/lib/components/alert.rb +0 -40
  128. data/lib/components/alert_description.rb +0 -11
  129. data/lib/components/alert_dialog.rb +0 -60
  130. data/lib/components/alert_dialog_action.rb +0 -22
  131. data/lib/components/alert_dialog_action_to.rb +0 -40
  132. data/lib/components/alert_dialog_cancel.rb +0 -22
  133. data/lib/components/alert_dialog_content.rb +0 -40
  134. data/lib/components/alert_dialog_description.rb +0 -22
  135. data/lib/components/alert_dialog_footer.rb +0 -11
  136. data/lib/components/alert_dialog_header.rb +0 -11
  137. data/lib/components/alert_dialog_title.rb +0 -22
  138. data/lib/components/alert_dialog_trigger.rb +0 -50
  139. data/lib/components/alert_title.rb +0 -11
  140. data/lib/components/avatar.rb +0 -31
  141. data/lib/components/avatar_fallback.rb +0 -21
  142. data/lib/components/avatar_image.rb +0 -19
  143. data/lib/components/badge.rb +0 -30
  144. data/lib/components/breadcrumb.rb +0 -51
  145. data/lib/components/breadcrumb_ellipsis.rb +0 -23
  146. data/lib/components/breadcrumb_item.rb +0 -11
  147. data/lib/components/breadcrumb_link.rb +0 -7
  148. data/lib/components/breadcrumb_page.rb +0 -21
  149. data/lib/components/breadcrumb_separator.rb +0 -26
  150. data/lib/components/button.rb +0 -53
  151. data/lib/components/card.rb +0 -31
  152. data/lib/components/card_content.rb +0 -11
  153. data/lib/components/card_description.rb +0 -11
  154. data/lib/components/card_footer.rb +0 -11
  155. data/lib/components/card_header.rb +0 -11
  156. data/lib/components/card_title.rb +0 -11
  157. data/lib/components/collapsible.rb +0 -31
  158. data/lib/components/collapsible_content.rb +0 -24
  159. data/lib/components/collapsible_trigger.rb +0 -50
  160. data/lib/components/combobox.rb +0 -57
  161. data/lib/components/combobox_item.rb +0 -9
  162. data/lib/components/date_picker.rb +0 -94
  163. data/lib/components/date_range_picker.rb +0 -113
  164. data/lib/components/dialog.rb +0 -52
  165. data/lib/components/dialog_close.rb +0 -42
  166. data/lib/components/dialog_content.rb +0 -54
  167. data/lib/components/dialog_description.rb +0 -22
  168. data/lib/components/dialog_footer.rb +0 -11
  169. data/lib/components/dialog_header.rb +0 -11
  170. data/lib/components/dialog_title.rb +0 -22
  171. data/lib/components/dialog_trigger.rb +0 -50
  172. data/lib/components/dropdown_menu.rb +0 -50
  173. data/lib/components/dropdown_menu_content.rb +0 -52
  174. data/lib/components/dropdown_menu_item.rb +0 -56
  175. data/lib/components/dropdown_menu_item_to.rb +0 -28
  176. data/lib/components/dropdown_menu_label.rb +0 -11
  177. data/lib/components/dropdown_menu_separator.rb +0 -20
  178. data/lib/components/dropdown_menu_trigger.rb +0 -57
  179. data/lib/components/form.rb +0 -59
  180. data/lib/components/form_hint.rb +0 -17
  181. data/lib/components/hover_card.rb +0 -33
  182. data/lib/components/hover_card_content.rb +0 -32
  183. data/lib/components/hover_card_trigger.rb +0 -44
  184. data/lib/components/input.rb +0 -32
  185. data/lib/components/label.rb +0 -14
  186. data/lib/components/loading_button.rb +0 -21
  187. data/lib/components/pagination.rb +0 -38
  188. data/lib/components/pagination_ellipsis.rb +0 -24
  189. data/lib/components/pagination_link.rb +0 -34
  190. data/lib/components/pagination_next.rb +0 -32
  191. data/lib/components/pagination_previous.rb +0 -32
  192. data/lib/components/popover.rb +0 -34
  193. data/lib/components/popover_content.rb +0 -40
  194. data/lib/components/popover_trigger.rb +0 -51
  195. data/lib/components/radio_group.rb +0 -62
  196. data/lib/components/radio_group_item.rb +0 -66
  197. data/lib/components/select.rb +0 -184
  198. data/lib/components/select_content.rb +0 -64
  199. data/lib/components/select_group.rb +0 -23
  200. data/lib/components/select_item.rb +0 -59
  201. data/lib/components/select_label.rb +0 -24
  202. data/lib/components/select_trigger.rb +0 -56
  203. data/lib/components/sheet.rb +0 -53
  204. data/lib/components/sheet_close.rb +0 -42
  205. data/lib/components/sheet_content.rb +0 -65
  206. data/lib/components/sheet_description.rb +0 -22
  207. data/lib/components/sheet_footer.rb +0 -11
  208. data/lib/components/sheet_header.rb +0 -11
  209. data/lib/components/sheet_title.rb +0 -22
  210. data/lib/components/sheet_trigger.rb +0 -50
  211. data/lib/components/sidebar.rb +0 -108
  212. data/lib/components/sidebar_container.rb +0 -11
  213. data/lib/components/sidebar_content.rb +0 -11
  214. data/lib/components/sidebar_footer.rb +0 -11
  215. data/lib/components/sidebar_group.rb +0 -11
  216. data/lib/components/sidebar_group_content.rb +0 -11
  217. data/lib/components/sidebar_group_label.rb +0 -16
  218. data/lib/components/sidebar_header.rb +0 -11
  219. data/lib/components/sidebar_inset.rb +0 -15
  220. data/lib/components/sidebar_menu.rb +0 -11
  221. data/lib/components/sidebar_menu_button.rb +0 -61
  222. data/lib/components/sidebar_menu_item.rb +0 -9
  223. data/lib/components/sidebar_menu_sub.rb +0 -14
  224. data/lib/components/sidebar_menu_sub_button.rb +0 -48
  225. data/lib/components/sidebar_menu_sub_item.rb +0 -9
  226. data/lib/components/sidebar_trigger.rb +0 -40
  227. data/lib/components/switch.rb +0 -66
  228. data/lib/components/table.rb +0 -75
  229. data/lib/components/table_body.rb +0 -11
  230. data/lib/components/table_caption.rb +0 -11
  231. data/lib/components/table_cell.rb +0 -11
  232. data/lib/components/table_footer.rb +0 -11
  233. data/lib/components/table_head.rb +0 -14
  234. data/lib/components/table_header.rb +0 -11
  235. data/lib/components/table_row.rb +0 -11
  236. data/lib/components/tabs.rb +0 -38
  237. data/lib/components/tabs_content.rb +0 -35
  238. data/lib/components/tabs_list.rb +0 -23
  239. data/lib/components/tabs_trigger.rb +0 -45
  240. data/lib/components/textarea.rb +0 -28
  241. data/lib/components/toast.rb +0 -101
  242. data/lib/components/toast_action.rb +0 -39
  243. data/lib/components/toast_action_to.rb +0 -28
  244. data/lib/components/toast_content.rb +0 -11
  245. data/lib/components/toast_description.rb +0 -11
  246. data/lib/components/toast_title.rb +0 -11
  247. data/lib/components/tooltip.rb +0 -34
  248. data/lib/components/tooltip_content.rb +0 -39
  249. data/lib/components/tooltip_trigger.rb +0 -48
@@ -0,0 +1,200 @@
1
+ import DropdownMenuRootController from './dropdown_menu_root_controller'
2
+
3
+ export default class extends DropdownMenuRootController {
4
+ static targets = [
5
+ 'trigger',
6
+ 'contentContainer',
7
+ 'content',
8
+ 'item',
9
+ 'triggerText',
10
+ 'group',
11
+ 'label',
12
+ 'select',
13
+ ]
14
+
15
+ static values = {
16
+ isOpen: Boolean,
17
+ selected: String,
18
+ setEqualWidth: { type: Boolean, default: true },
19
+ closestContentSelector: {
20
+ type: String,
21
+ default: '[data-select-target="content"]',
22
+ },
23
+ }
24
+
25
+ declare selectedValue: string
26
+ declare searchString: string
27
+ declare searchTimeout: number
28
+ declare groupTargets: HTMLElement[]
29
+ declare triggerTextTarget: HTMLElement
30
+ declare selectTarget: HTMLSelectElement
31
+ declare itemsInnerText: string[]
32
+
33
+ connect() {
34
+ super.connect()
35
+ this.itemsInnerText = this.items.map((i) => i.innerText.trim())
36
+ this.setAriaLabelledby()
37
+ this.searchString = ''
38
+ }
39
+
40
+ setAriaLabelledby() {
41
+ this.groupTargets.forEach((g) => {
42
+ const label = g.querySelector(
43
+ '[data-select-target="label"]',
44
+ ) as HTMLElement
45
+
46
+ if (label) {
47
+ label.id = g.getAttribute('aria-labelledby') as string
48
+ }
49
+ })
50
+ }
51
+
52
+ onOpenFocusedElement(event: MouseEvent | KeyboardEvent) {
53
+ let itemIndex = null as number | null
54
+
55
+ if (this.selectedValue) {
56
+ const item = this.itemTargets.find(
57
+ (i) => i.dataset.value === this.selectedValue,
58
+ )
59
+
60
+ if (item && !item.dataset.disabled) {
61
+ itemIndex = this.items.indexOf(item)
62
+ }
63
+ } else {
64
+ if (event instanceof KeyboardEvent) {
65
+ const key = event.key
66
+
67
+ if (['ArrowDown', 'Enter', ' '].includes(key)) {
68
+ itemIndex = 0
69
+ }
70
+ }
71
+ }
72
+
73
+ if (itemIndex !== null) {
74
+ return this.items[itemIndex]
75
+ } else {
76
+ return this.contentTarget
77
+ }
78
+ }
79
+
80
+ onSelect(event: MouseEvent | KeyboardEvent) {
81
+ const item = event.currentTarget as HTMLElement
82
+ const value = item.dataset.value as string
83
+ this.selectedValue = value
84
+ }
85
+
86
+ onDOMKeydown(event: KeyboardEvent) {
87
+ super.onDOMKeydown(event)
88
+
89
+ const { key, altKey, ctrlKey, metaKey } = event
90
+
91
+ if (
92
+ key === 'Backspace' ||
93
+ key === 'Clear' ||
94
+ (key.length === 1 && key !== ' ' && !altKey && !ctrlKey && !metaKey)
95
+ ) {
96
+ this.handleSearch(key)
97
+ }
98
+ }
99
+
100
+ // https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/
101
+ handleSearch(char: string) {
102
+ const searchString = this.getSearchString(char)
103
+ const focusedItem = this.items.find(
104
+ (item) => document.activeElement === item,
105
+ )
106
+ const focusedIndex = focusedItem ? this.items.indexOf(focusedItem) : 0
107
+ const searchIndex = this.getIndexByLetter(searchString, focusedIndex + 1)
108
+
109
+ // if a match was found, go to it
110
+ if (searchIndex >= 0) {
111
+ this.focusItemByIndex(null, searchIndex)
112
+ }
113
+ // if no matches, clear the timeout and search string
114
+ else {
115
+ window.clearTimeout(this.searchTimeout)
116
+ this.searchString = ''
117
+ }
118
+ }
119
+
120
+ filterItemsInnerText(items: string[], filter: string) {
121
+ return items.filter((item) => {
122
+ const matches = item.toLowerCase().indexOf(filter.toLowerCase()) === 0
123
+ return matches
124
+ })
125
+ }
126
+
127
+ getSearchString(char: string) {
128
+ // reset typing timeout and start new timeout
129
+ // this allows us to make multiple-letter matches, like a native select
130
+ if (typeof this.searchTimeout === 'number') {
131
+ window.clearTimeout(this.searchTimeout)
132
+ }
133
+
134
+ this.searchTimeout = window.setTimeout(() => {
135
+ this.searchString = ''
136
+ }, 500)
137
+
138
+ // add most recent letter to saved search string
139
+ this.searchString += char
140
+ return this.searchString
141
+ }
142
+
143
+ // return the index of an option from an array of options, based on a search string
144
+ // if the filter is multiple iterations of the same letter (e.g "aaa"), then cycle through first-letter matches
145
+ getIndexByLetter(filter: string, startIndex: number) {
146
+ const orderedItems = [
147
+ ...this.itemsInnerText.slice(startIndex),
148
+ ...this.itemsInnerText.slice(0, startIndex),
149
+ ]
150
+
151
+ const firstMatch = this.filterItemsInnerText(orderedItems, filter)[0]
152
+
153
+ const allSameLetter = (array: string[]) =>
154
+ array.every((letter) => letter === array[0])
155
+
156
+ // first check if there is an exact match for the typed string
157
+ if (firstMatch) {
158
+ const index = this.itemsInnerText.indexOf(firstMatch)
159
+ return index
160
+ }
161
+
162
+ // if the same letter is being repeated, cycle through first-letter matches
163
+ else if (allSameLetter(filter.split(''))) {
164
+ const matches = this.filterItemsInnerText(orderedItems, filter[0])
165
+ const index = this.itemsInnerText.indexOf(matches[0])
166
+ return index
167
+ }
168
+
169
+ // if no matches, return -1
170
+ else {
171
+ return -1
172
+ }
173
+ }
174
+
175
+ selectedValueChanged(value: string) {
176
+ const item = this.itemTargets.find((i) => i.dataset.value === value)
177
+
178
+ if (item) {
179
+ this.triggerTextTarget.textContent = item.textContent
180
+
181
+ this.itemTargets.forEach((i) => {
182
+ if (i.dataset.value === value) {
183
+ i.setAttribute('aria-selected', 'true')
184
+ } else {
185
+ i.setAttribute('aria-selected', 'false')
186
+ }
187
+ })
188
+
189
+ this.selectTarget.value = value
190
+ }
191
+
192
+ this.triggerTarget.dataset.hasValue = `${!!value && value.length > 0}`
193
+
194
+ const placeholder = this.triggerTarget.dataset.placeholder
195
+
196
+ if (placeholder && this.triggerTarget.dataset.hasValue === 'false') {
197
+ this.triggerTextTarget.textContent = placeholder
198
+ }
199
+ }
200
+ }
@@ -1,8 +1,12 @@
1
1
  import { Controller } from '@hotwired/stimulus'
2
2
 
3
- export default class extends Controller {
3
+ export default class extends Controller<HTMLElement> {
4
4
  static targets = ['panel', 'panelOffset']
5
5
 
6
+ declare readonly panelTarget: HTMLElement
7
+ declare readonly panelOffsetTarget: HTMLElement
8
+ declare width: number
9
+
6
10
  connect() {
7
11
  this.width = this.element.offsetWidth
8
12
  }
@@ -26,7 +30,7 @@ export default class extends Controller {
26
30
  this.element.dataset.state = 'collapsed'
27
31
  this.element.dataset.collapsible = 'offcanvas'
28
32
  this.panelTarget.style.left = `-${this.width}px`
29
- this.panelOffsetTarget.style.width = 0
33
+ this.panelOffsetTarget.style.width = `${0}`
30
34
  }
31
35
 
32
36
  isOpen() {
@@ -0,0 +1,21 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+ import SidebarController from './sidebar_controller'
3
+
4
+ export default class extends Controller<HTMLElement> {
5
+ declare sidebarId: string | undefined
6
+
7
+ connect() {
8
+ this.sidebarId = this.element.dataset.sidebarId
9
+ }
10
+
11
+ toggle() {
12
+ const sidebar = this.application.getControllerForElementAndIdentifier(
13
+ document.querySelector(`#${this.sidebarId}`) as HTMLElement,
14
+ 'sidebar',
15
+ ) as SidebarController
16
+
17
+ if (sidebar) {
18
+ sidebar.toggle()
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,107 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+ import noUiSlider, { API, Options } from 'nouislider'
3
+
4
+ export default class extends Controller<HTMLElement> {
5
+ static targets = ['slider', 'hiddenInput', 'endHiddenInput']
6
+
7
+ declare readonly sliderTarget: HTMLInputElement
8
+ declare readonly hiddenInputTarget: HTMLInputElement
9
+ declare readonly endHiddenInputTarget: HTMLInputElement
10
+ declare readonly hasEndHiddenInputTarget: boolean
11
+ declare onUpdateValuesListener: (values: (string | number)[]) => void
12
+ declare DOMClickListener: (event: MouseEvent) => void
13
+ declare range: boolean
14
+ declare slider: API
15
+
16
+ connect() {
17
+ this.range = this.element.dataset.range === 'true'
18
+ this.DOMClickListener = this.onDOMClick.bind(this)
19
+ this.onUpdateValuesListener = this.onUpdateValues.bind(this)
20
+ const options = this.getOptions()
21
+ this.slider = noUiSlider.create(this.sliderTarget, options)
22
+
23
+ if (this.element.dataset.disabled === 'true') {
24
+ this.slider.disable()
25
+ }
26
+
27
+ if (this.element.dataset.id) {
28
+ const lowerHandle = this.slider.target.querySelector('.noUi-handle-lower')
29
+ if (lowerHandle) {
30
+ lowerHandle.id = this.element.dataset.id
31
+ }
32
+ }
33
+ this.slider.on('update', this.onUpdateValuesListener)
34
+
35
+ document.addEventListener('click', this.DOMClickListener)
36
+ }
37
+
38
+ onUpdateValues(values: (string | number)[]) {
39
+ this.hiddenInputTarget.value = `${values[0]}`
40
+
41
+ if (this.range && this.hasEndHiddenInputTarget) {
42
+ this.endHiddenInputTarget.value = `${values[1]}`
43
+ }
44
+ }
45
+
46
+ getOptions() {
47
+ const defaultOptions = {
48
+ connect: this.range ? true : 'lower',
49
+ tooltips: true,
50
+ } as Options
51
+
52
+ defaultOptions.range = {
53
+ min: [parseFloat(this.element.dataset.min || '0')],
54
+ max: [parseFloat(this.element.dataset.max || '100')],
55
+ }
56
+
57
+ defaultOptions.step = parseFloat(this.element.dataset.step || '1')
58
+
59
+ if (this.range) {
60
+ defaultOptions.start = [
61
+ parseFloat(this.element.dataset.value || '0'),
62
+ parseFloat(this.element.dataset.endValue || '0'),
63
+ ]
64
+
65
+ defaultOptions.handleAttributes = [
66
+ { 'aria-label': 'lower' },
67
+ { 'aria-label': 'upper' },
68
+ ]
69
+ } else {
70
+ defaultOptions.start = [parseFloat(this.element.dataset.value || '0')]
71
+ defaultOptions.handleAttributes = [{ 'aria-label': 'lower' }]
72
+ }
73
+
74
+ defaultOptions.orientation = (this.element.dataset.orientation ||
75
+ 'horizontal') as Options['orientation']
76
+
77
+ try {
78
+ return {
79
+ ...defaultOptions,
80
+ ...JSON.parse(this.element.dataset.options || ''),
81
+ }
82
+ } catch {
83
+ return defaultOptions
84
+ }
85
+ }
86
+
87
+ onDOMClick(event: MouseEvent) {
88
+ const target = event.target
89
+
90
+ // Focus handle of slider when label is clicked.
91
+ if (target instanceof HTMLLabelElement) {
92
+ const id = target.getAttribute('for')
93
+
94
+ if (id === this.element.dataset.id) {
95
+ const handle = document.querySelector(`#${id}`) as HTMLElement
96
+
97
+ if (handle) {
98
+ handle.focus()
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ disconnect() {
105
+ document.removeEventListener('click', this.DOMClickListener)
106
+ }
107
+ }
@@ -0,0 +1,30 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ export default class extends Controller<HTMLElement> {
4
+ static targets = ['input', 'thumb']
5
+ static values = {
6
+ isChecked: Boolean,
7
+ }
8
+
9
+ declare readonly inputTarget: HTMLInputElement
10
+ declare readonly thumbTarget: HTMLElement
11
+ declare isCheckedValue: boolean
12
+
13
+ toggle() {
14
+ this.isCheckedValue = !this.isCheckedValue
15
+ }
16
+
17
+ isCheckedValueChanged(value: boolean) {
18
+ if (value) {
19
+ this.element.ariaChecked = 'true'
20
+ this.element.dataset.state = 'checked'
21
+ this.thumbTarget.dataset.state = 'checked'
22
+ this.inputTarget.checked = true
23
+ } else {
24
+ this.element.ariaChecked = 'false'
25
+ this.element.dataset.state = 'unchecked'
26
+ this.thumbTarget.dataset.state = 'unchecked'
27
+ this.inputTarget.checked = false
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,79 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ export default class extends Controller {
4
+ static targets = ['trigger', 'content']
5
+ static values = {
6
+ active: String,
7
+ }
8
+
9
+ declare readonly triggerTargets: HTMLButtonElement[]
10
+ declare readonly contentTargets: HTMLElement[]
11
+ declare activeValue: string | undefined
12
+
13
+ connect() {
14
+ if (!this.activeValue) {
15
+ this.activeValue = this.triggerTargets[0].dataset.value
16
+ }
17
+ }
18
+
19
+ setActiveTab(event: MouseEvent | KeyboardEvent) {
20
+ const target = event.currentTarget as HTMLButtonElement
21
+
22
+ if (event instanceof MouseEvent) {
23
+ this.activeValue = target.dataset.value
24
+ } else {
25
+ const key = event.key
26
+
27
+ const focusableTriggers = this.triggerTargets.filter(
28
+ (t) => !t.disabled,
29
+ ) as HTMLButtonElement[]
30
+
31
+ const index = focusableTriggers.indexOf(target)
32
+ let newIndex = 0
33
+
34
+ if (key === 'ArrowLeft') {
35
+ newIndex = index - 1
36
+
37
+ if (newIndex < 0) {
38
+ newIndex = focusableTriggers.length - 1
39
+ }
40
+ } else {
41
+ newIndex = index + 1
42
+
43
+ if (newIndex > focusableTriggers.length - 1) {
44
+ newIndex = 0
45
+ }
46
+ }
47
+
48
+ this.activeValue = focusableTriggers[newIndex].dataset.value
49
+ focusableTriggers[newIndex].focus()
50
+ }
51
+ }
52
+
53
+ activeValueChanged(value: string) {
54
+ this.triggerTargets.forEach((trigger) => {
55
+ const triggerValue = trigger.dataset.value
56
+ const content = this.contentTargets.find((c) => {
57
+ return c.dataset.value === triggerValue
58
+ })
59
+
60
+ if (!content) {
61
+ throw new Error(
62
+ `Could not find TabsContent with value "${triggerValue}"`,
63
+ )
64
+ }
65
+
66
+ if (triggerValue === value) {
67
+ trigger.ariaSelected = 'true'
68
+ trigger.tabIndex = 0
69
+ trigger.dataset.state = 'active'
70
+ content.classList.remove('hidden')
71
+ } else {
72
+ trigger.ariaSelected = 'false'
73
+ trigger.tabIndex = -1
74
+ trigger.dataset.state = 'inactive'
75
+ content.classList.add('hidden')
76
+ }
77
+ })
78
+ }
79
+ }
@@ -2,31 +2,34 @@ import { Controller } from '@hotwired/stimulus'
2
2
 
3
3
  export default class extends Controller {
4
4
  initialize() {
5
- document.documentElement.classList.toggle(
6
- 'dark',
5
+ if (
7
6
  localStorage.theme === 'dark' ||
8
- (!('theme' in localStorage) &&
9
- window.matchMedia('(prefers-color-scheme: dark)').matches),
10
- )
7
+ (!('theme' in localStorage) &&
8
+ window.matchMedia('(prefers-color-scheme: dark)').matches)
9
+ ) {
10
+ this.setDarkMode()
11
+ } else {
12
+ this.setLightMode()
13
+ }
11
14
  }
12
15
 
13
16
  toggle() {
14
17
  if (document.documentElement.classList.contains('dark')) {
15
- localStorage.theme = 'light'
16
- document.documentElement.classList.remove('dark')
18
+ this.setLightMode()
17
19
  } else {
18
- localStorage.theme = 'dark'
19
- document.documentElement.classList.add('dark')
20
+ this.setDarkMode()
20
21
  }
21
22
  }
22
23
 
23
24
  setLightMode() {
24
25
  localStorage.theme = 'light'
25
26
  document.documentElement.classList.remove('dark')
27
+ document.documentElement.style.colorScheme = ''
26
28
  }
27
29
 
28
30
  setDarkMode() {
29
31
  localStorage.theme = 'dark'
30
32
  document.documentElement.classList.add('dark')
33
+ document.documentElement.style.colorScheme = 'dark'
31
34
  }
32
35
  }
@@ -0,0 +1,62 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+ import DOMPurify from 'dompurify'
3
+
4
+ export default class extends Controller<HTMLElement> {
5
+ addToast({
6
+ title,
7
+ description,
8
+ action,
9
+ variant = 'default',
10
+ duration = 5000,
11
+ }: {
12
+ title: string
13
+ description?: string
14
+ action?: string
15
+ variant: string
16
+ duration?: number
17
+ }) {
18
+ const template = (
19
+ variant === 'default'
20
+ ? this.element.querySelector('[data-variant="default"]')
21
+ : this.element.querySelector('[data-variant="destructive"]')
22
+ ) as HTMLTemplateElement
23
+
24
+ const clone = template.content.cloneNode(true) as HTMLElement
25
+
26
+ const toastTemplate = clone.querySelector(
27
+ '[data-shadcn-phlexcomponents="toast"]',
28
+ ) as HTMLElement
29
+ toastTemplate.dataset.duration = String(duration)
30
+
31
+ const titleTemplate = clone.querySelector(
32
+ '[data-shadcn-phlexcomponents="toast-title"]',
33
+ ) as HTMLElement
34
+ const descriptionTemplate = clone.querySelector(
35
+ '[data-shadcn-phlexcomponents="toast-description"]',
36
+ ) as HTMLElement
37
+ const actionTemplate = clone.querySelector(
38
+ '[data-shadcn-phlexcomponents="toast-action"]',
39
+ ) as HTMLElement
40
+
41
+ titleTemplate.innerHTML = DOMPurify.sanitize(title)
42
+
43
+ if (description) {
44
+ descriptionTemplate.innerHTML = DOMPurify.sanitize(description)
45
+ } else {
46
+ descriptionTemplate.remove()
47
+ }
48
+
49
+ if (action) {
50
+ const element = document.createElement('div')
51
+ element.innerHTML = DOMPurify.sanitize(action)
52
+ const actionElement = element.firstElementChild as HTMLElement
53
+ const classes = actionTemplate.classList
54
+ actionElement.classList.add(...classes)
55
+ actionTemplate.replaceWith(actionElement)
56
+ } else {
57
+ actionTemplate.remove()
58
+ }
59
+
60
+ this.element.append(clone)
61
+ }
62
+ }
@@ -0,0 +1,28 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+ import { ANIMATION_OUT_DELAY } from '../utils'
3
+
4
+ export default class extends Controller<HTMLElement> {
5
+ declare duration: number
6
+ declare closeTimeout: number
7
+
8
+ connect() {
9
+ this.duration = Number(this.element.dataset.duration)
10
+ this.close()
11
+ }
12
+
13
+ cancelClose() {
14
+ window.clearTimeout(this.closeTimeout)
15
+ }
16
+
17
+ close() {
18
+ if (this.duration > 0) {
19
+ this.closeTimeout = window.setTimeout(() => {
20
+ this.element.dataset.state = 'closed'
21
+
22
+ setTimeout(() => {
23
+ this.element.remove()
24
+ }, ANIMATION_OUT_DELAY)
25
+ }, this.duration)
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,98 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+ import { useHover } from 'stimulus-use'
3
+ import { initFloatingUi, ANIMATION_OUT_DELAY } from '../utils'
4
+
5
+ export default class extends Controller<HTMLElement> {
6
+ static targets = ['trigger', 'content', 'contentContainer', 'arrow']
7
+ static values = {
8
+ isOpen: Boolean,
9
+ }
10
+
11
+ declare isOpenValue: boolean
12
+ declare readonly triggerTarget: HTMLElement
13
+ declare readonly contentTarget: HTMLElement
14
+ declare readonly contentContainerTarget: HTMLElement
15
+ declare readonly arrowTarget: HTMLElement
16
+ declare closeTimeout: number
17
+ declare cleanup: () => void
18
+ declare DOMKeydownListener: (event: KeyboardEvent) => void
19
+
20
+ connect() {
21
+ this.DOMKeydownListener = this.onDOMKeydown.bind(this)
22
+ useHover(this, { element: this.triggerTarget, dispatchEvent: false })
23
+ }
24
+
25
+ open() {
26
+ window.clearTimeout(this.closeTimeout)
27
+ this.isOpenValue = true
28
+ }
29
+
30
+ close() {
31
+ this.closeTimeout = window.setTimeout(() => {
32
+ this.isOpenValue = false
33
+ }, 250)
34
+ }
35
+
36
+ closeImmediately() {
37
+ this.isOpenValue = false
38
+ }
39
+
40
+ // for useHover
41
+ mouseEnter() {
42
+ this.open()
43
+ }
44
+
45
+ // for useHover
46
+ mouseLeave() {
47
+ this.close()
48
+ }
49
+
50
+ onDOMKeydown(event: KeyboardEvent) {
51
+ if (!this.isOpenValue) return
52
+
53
+ const key = event.key
54
+
55
+ if (['Escape', 'Enter', ' '].includes(key)) {
56
+ event.preventDefault()
57
+ event.stopImmediatePropagation()
58
+ this.closeImmediately()
59
+ }
60
+ }
61
+
62
+ isOpenValueChanged(isOpen: boolean) {
63
+ if (isOpen) {
64
+ this.contentContainerTarget.classList.remove('hidden')
65
+ this.contentTarget.dataset.state = 'open'
66
+ this.setupEventListeners()
67
+
68
+ this.cleanup = initFloatingUi({
69
+ referenceElement: this.triggerTarget,
70
+ floatingElement: this.contentContainerTarget,
71
+ arrowElement: this.arrowTarget,
72
+ side: this.contentTarget.dataset.side,
73
+ align: this.contentTarget.dataset.align,
74
+ sideOffset: 8,
75
+ })
76
+ } else {
77
+ this.contentTarget.dataset.state = 'closed'
78
+ this.cleanupEventListeners()
79
+
80
+ setTimeout(() => {
81
+ this.contentContainerTarget.classList.add('hidden')
82
+ }, ANIMATION_OUT_DELAY)
83
+ }
84
+ }
85
+
86
+ setupEventListeners() {
87
+ document.addEventListener('keydown', this.DOMKeydownListener)
88
+ }
89
+
90
+ cleanupEventListeners() {
91
+ if (this.cleanup) this.cleanup()
92
+ document.removeEventListener('keydown', this.DOMKeydownListener)
93
+ }
94
+
95
+ disconnect() {
96
+ this.cleanupEventListeners()
97
+ }
98
+ }