shadcn_phlexcomponents 0.1.11 → 0.1.14

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/app/javascript/controllers/accordion_controller.ts +65 -62
  3. data/app/javascript/controllers/alert_dialog_controller.ts +12 -0
  4. data/app/javascript/controllers/avatar_controller.ts +7 -2
  5. data/app/javascript/controllers/checkbox_controller.ts +11 -4
  6. data/app/javascript/controllers/collapsible_controller.ts +12 -5
  7. data/app/javascript/controllers/combobox_controller.ts +270 -39
  8. data/app/javascript/controllers/command_controller.ts +223 -51
  9. data/app/javascript/controllers/date_picker_controller.ts +185 -125
  10. data/app/javascript/controllers/date_range_picker_controller.ts +89 -79
  11. data/app/javascript/controllers/dialog_controller.ts +59 -57
  12. data/app/javascript/controllers/dropdown_menu_controller.ts +212 -36
  13. data/app/javascript/controllers/dropdown_menu_sub_controller.ts +31 -29
  14. data/app/javascript/controllers/form_field_controller.ts +6 -1
  15. data/app/javascript/controllers/hover_card_controller.ts +36 -26
  16. data/app/javascript/controllers/loading_button_controller.ts +6 -1
  17. data/app/javascript/controllers/popover_controller.ts +42 -65
  18. data/app/javascript/controllers/progress_controller.ts +9 -3
  19. data/app/javascript/controllers/radio_group_controller.ts +16 -9
  20. data/app/javascript/controllers/select_controller.ts +206 -65
  21. data/app/javascript/controllers/slider_controller.ts +23 -16
  22. data/app/javascript/controllers/switch_controller.ts +11 -4
  23. data/app/javascript/controllers/tabs_controller.ts +26 -18
  24. data/app/javascript/controllers/theme_switcher_controller.ts +6 -1
  25. data/app/javascript/controllers/toast_container_controller.ts +6 -1
  26. data/app/javascript/controllers/toast_controller.ts +7 -1
  27. data/app/javascript/controllers/toggle_controller.ts +28 -0
  28. data/app/javascript/controllers/toggle_group_controller.ts +28 -0
  29. data/app/javascript/controllers/tooltip_controller.ts +43 -31
  30. data/app/javascript/shadcn_phlexcomponents.ts +29 -25
  31. data/app/javascript/utils/command.ts +544 -0
  32. data/app/javascript/utils/floating_ui.ts +196 -0
  33. data/app/javascript/utils/index.ts +417 -0
  34. data/app/stylesheets/date_picker.css +118 -0
  35. data/lib/shadcn_phlexcomponents/alias.rb +3 -0
  36. data/lib/shadcn_phlexcomponents/components/accordion.rb +2 -1
  37. data/lib/shadcn_phlexcomponents/components/alert_dialog.rb +18 -15
  38. data/lib/shadcn_phlexcomponents/components/base.rb +14 -0
  39. data/lib/shadcn_phlexcomponents/components/collapsible.rb +1 -2
  40. data/lib/shadcn_phlexcomponents/components/combobox.rb +87 -57
  41. data/lib/shadcn_phlexcomponents/components/command.rb +77 -47
  42. data/lib/shadcn_phlexcomponents/components/date_picker.rb +25 -81
  43. data/lib/shadcn_phlexcomponents/components/date_range_picker.rb +21 -4
  44. data/lib/shadcn_phlexcomponents/components/dialog.rb +14 -12
  45. data/lib/shadcn_phlexcomponents/components/dropdown_menu.rb +5 -4
  46. data/lib/shadcn_phlexcomponents/components/dropdown_menu_sub.rb +2 -1
  47. data/lib/shadcn_phlexcomponents/components/form/form_combobox.rb +64 -0
  48. data/lib/shadcn_phlexcomponents/components/form.rb +14 -0
  49. data/lib/shadcn_phlexcomponents/components/hover_card.rb +3 -2
  50. data/lib/shadcn_phlexcomponents/components/popover.rb +3 -3
  51. data/lib/shadcn_phlexcomponents/components/select.rb +10 -25
  52. data/lib/shadcn_phlexcomponents/components/sheet.rb +15 -11
  53. data/lib/shadcn_phlexcomponents/components/table.rb +1 -1
  54. data/lib/shadcn_phlexcomponents/components/tabs.rb +1 -1
  55. data/lib/shadcn_phlexcomponents/components/toast_container.rb +1 -1
  56. data/lib/shadcn_phlexcomponents/components/toggle.rb +54 -0
  57. data/lib/shadcn_phlexcomponents/components/tooltip.rb +3 -2
  58. data/lib/shadcn_phlexcomponents/engine.rb +1 -5
  59. data/lib/shadcn_phlexcomponents/version.rb +1 -1
  60. metadata +9 -4
  61. data/app/javascript/controllers/command_root_controller.ts +0 -355
  62. data/app/javascript/controllers/dropdown_menu_root_controller.ts +0 -234
  63. data/app/javascript/utils.ts +0 -437
@@ -1,60 +1,239 @@
1
1
  import hotkeys from 'hotkeys-js'
2
+ import { Controller } from '@hotwired/stimulus'
2
3
  import {
3
- openWithOverlay,
4
4
  showContent,
5
- closeWithOverlay,
6
5
  hideContent,
7
6
  focusTrigger,
7
+ ON_OPEN_FOCUS_DELAY,
8
+ setGroupLabelsId,
8
9
  } from '../utils'
9
- import CommandRootController from './command_root_controller'
10
+ import {
11
+ scrollToItem,
12
+ highlightItem,
13
+ highlightItemByIndex,
14
+ filteredItemsChanged,
15
+ setItemsGroupId,
16
+ search,
17
+ clearRemoteResults,
18
+ resetState,
19
+ hideError,
20
+ showList,
21
+ } from '../utils/command'
22
+ import { useDebounce, useClickOutside } from 'stimulus-use'
23
+ import Fuse from 'fuse.js'
10
24
 
11
25
  declare global {
12
26
  interface Window {
13
- Turbo: any
27
+ Turbo: {
28
+ visit: (path: string) => void
29
+ }
14
30
  }
15
31
  }
16
32
 
17
- export default class extends CommandRootController {
33
+ const CommandController = class extends Controller<HTMLElement> {
34
+ // targets
18
35
  static targets = [
19
36
  'trigger',
20
37
  'content',
38
+ 'overlay',
21
39
  'item',
22
40
  'group',
23
- 'label',
24
41
  'searchInput',
25
- 'results',
42
+ 'list',
43
+ 'listContainer',
26
44
  'empty',
27
45
  'modifierKey',
46
+ 'loading',
47
+ 'error',
28
48
  ]
29
-
49
+ declare readonly triggerTarget: HTMLElement
50
+ declare readonly contentTarget: HTMLElement
51
+ declare readonly overlayTarget: HTMLElement
52
+ declare readonly itemTargets: HTMLInputElement[]
53
+ declare readonly groupTargets: HTMLElement[]
54
+ declare readonly searchInputTarget: HTMLInputElement
55
+ declare readonly listTarget: HTMLElement
56
+ declare readonly listContainerTarget: HTMLElement
57
+ declare readonly emptyTarget: HTMLElement
30
58
  declare readonly modifierKeyTarget: HTMLElement
31
59
  declare readonly hasModifierKeyTarget: boolean
60
+ declare readonly loadingTarget: HTMLElement
61
+ declare readonly errorTarget: HTMLElement
62
+
63
+ // values
64
+ static values = {
65
+ isOpen: Boolean,
66
+ filteredItemIndexes: Array,
67
+ searchUrl: String,
68
+ }
69
+ declare isOpenValue: boolean
70
+ declare filteredItemIndexesValue: number[]
71
+
72
+ // custom properties
73
+ declare trigger: HTMLElement
74
+ declare orderedItems: HTMLElement[]
75
+ declare itemsInnerText: string[]
76
+ declare filteredItems: HTMLElement[]
77
+ declare fuse: Fuse<string>
78
+ declare scrollingViaKeyboard: boolean
79
+ declare keyboardScrollTimeout: number
32
80
  declare modifierKey?: string
33
81
  declare shortcutKey?: string
34
- declare resultsTarget: HTMLElement
35
- declare searchInputTarget: HTMLInputElement
36
- declare emptyTarget: HTMLElement
37
- declare filteredItems: HTMLElement[]
38
- declare hotkeyListener: (event: KeyboardEvent) => void
39
82
  declare keybinds: string
83
+ declare abortController?: AbortController
84
+ declare searchPath?: string
85
+ declare isDirty: boolean
86
+ declare isLoading: boolean
87
+ declare hotkeyListener: (event: KeyboardEvent) => void
88
+ declare DOMKeydownListener: (event: KeyboardEvent) => void
89
+ declare DOMClickListener: (event: MouseEvent) => void
90
+
91
+ static debounces = ['search']
40
92
 
41
93
  connect() {
42
- super.connect()
94
+ this.orderedItems = [...this.itemTargets]
95
+ this.itemsInnerText = this.orderedItems.map((i) => i.innerText.trim())
96
+ this.fuse = new Fuse(this.itemsInnerText)
97
+ this.filteredItemIndexesValue = Array.from(
98
+ { length: this.itemTargets.length },
99
+ (_, i) => i,
100
+ )
101
+ this.isLoading = false
102
+ this.filteredItems = this.itemTargets
103
+ this.isDirty = false
104
+ this.searchPath = this.element.dataset.searchPath
105
+
106
+ setGroupLabelsId(this)
107
+ setItemsGroupId(this)
108
+ useDebounce(this)
109
+ useClickOutside(this, { element: this.contentTarget, dispatchEvent: false })
43
110
  this.hotkeyListener = this.onHotkeyPressed.bind(this)
111
+ this.DOMKeydownListener = this.onDOMKeydown.bind(this)
44
112
  this.setupHotkeys()
45
113
  this.replaceModifierKeyIcon()
46
114
  }
47
115
 
48
- setupHotkeys() {
116
+ open() {
117
+ this.isOpenValue = true
118
+ this.highlightItemByIndex(0)
119
+
120
+ setTimeout(() => {
121
+ this.searchInputTarget.focus()
122
+ }, ON_OPEN_FOCUS_DELAY)
123
+ }
124
+
125
+ close() {
126
+ this.isOpenValue = false
127
+ resetState(this)
128
+ }
129
+
130
+ scrollToItem(index: number) {
131
+ scrollToItem(this, index)
132
+ }
133
+
134
+ highlightItem(
135
+ event: MouseEvent | KeyboardEvent | null = null,
136
+ index: number | null = null,
137
+ ) {
138
+ highlightItem(this, event, index)
139
+ }
140
+
141
+ highlightItemByIndex(index: number) {
142
+ highlightItemByIndex(this, index)
143
+ }
144
+
145
+ select(event: MouseEvent | KeyboardEvent) {
146
+ let value = null as null | string
147
+
148
+ if (event instanceof KeyboardEvent) {
149
+ const item = this.filteredItems.find(
150
+ (i) => i.dataset.highlighted === 'true',
151
+ )
152
+
153
+ if (item) {
154
+ value = item.dataset.value as string
155
+ }
156
+ } else {
157
+ // mouse event
158
+ const item = event.currentTarget as HTMLElement
159
+ value = item.dataset.value as string
160
+ }
161
+
162
+ if (value) {
163
+ window.Turbo.visit(value)
164
+ this.close()
165
+ }
166
+ }
167
+
168
+ inputKeydown(event: KeyboardEvent) {
169
+ if (event.key === ' ' && this.searchInputTarget.value.length === 0) {
170
+ event.preventDefault()
171
+ }
172
+
173
+ hideError(this)
174
+ showList(this)
175
+ }
176
+
177
+ search(event: InputEvent) {
178
+ this.isDirty = true
179
+ clearRemoteResults(this)
180
+ search(this, event)
181
+ }
182
+
183
+ clickOutside() {
184
+ this.close()
185
+ }
186
+
187
+ isOpenValueChanged(isOpen: boolean, previousIsOpen: boolean) {
188
+ if (isOpen) {
189
+ showContent({
190
+ trigger: this.triggerTarget,
191
+ content: this.contentTarget,
192
+ contentContainer: this.contentTarget,
193
+ overlay: this.overlayTarget,
194
+ })
195
+
196
+ this.setupEventListeners()
197
+ } else {
198
+ hideContent({
199
+ trigger: this.triggerTarget,
200
+ content: this.contentTarget,
201
+ contentContainer: this.contentTarget,
202
+ overlay: this.overlayTarget,
203
+ })
204
+
205
+ if (previousIsOpen) {
206
+ focusTrigger(this.triggerTarget)
207
+ }
208
+
209
+ this.cleanupEventListeners()
210
+ }
211
+ }
212
+
213
+ filteredItemIndexesValueChanged(filteredItemIndexes: number[]) {
214
+ filteredItemsChanged(this, filteredItemIndexes)
215
+ }
216
+
217
+ disconnect() {
218
+ this.cleanupEventListeners()
219
+ resetState(this)
220
+
221
+ if (this.keybinds) {
222
+ hotkeys.unbind(this.keybinds)
223
+ }
224
+ }
225
+
226
+ protected setupHotkeys() {
49
227
  const modifierKey = this.element.dataset.modifierKey
50
228
  const shortcutKey = this.element.dataset.shortcutKey
229
+
51
230
  let keybinds = ''
52
231
 
53
232
  if (modifierKey && shortcutKey) {
54
233
  keybinds = `${modifierKey}+${shortcutKey}`
55
234
 
56
235
  if (modifierKey === 'ctrl') {
57
- keybinds + `,cmd-${shortcutKey}`
236
+ keybinds += `,cmd-${shortcutKey}`
58
237
  }
59
238
  } else if (shortcutKey) {
60
239
  keybinds = shortcutKey
@@ -64,19 +243,24 @@ export default class extends CommandRootController {
64
243
  hotkeys(keybinds, this.hotkeyListener)
65
244
  }
66
245
 
67
- onHotkeyPressed(event: KeyboardEvent) {
246
+ protected onHotkeyPressed(event: KeyboardEvent) {
68
247
  event.preventDefault()
69
248
  this.open()
70
249
  }
71
250
 
72
- replaceModifierKeyIcon() {
251
+ protected replaceModifierKeyIcon() {
73
252
  if (this.hasModifierKeyTarget && this.isMac()) {
74
253
  this.modifierKeyTarget.innerHTML = '⌘'
75
254
  }
76
255
  }
77
256
 
78
- isMac() {
79
- const navigator = window.navigator as any
257
+ protected isMac() {
258
+ const navigator = window.navigator as unknown as {
259
+ platform: string
260
+ userAgentData: {
261
+ platform: string
262
+ }
263
+ }
80
264
 
81
265
  if (navigator.userAgentData) {
82
266
  return navigator.userAgentData.platform === 'macOS'
@@ -86,44 +270,32 @@ export default class extends CommandRootController {
86
270
  return navigator.platform.toUpperCase().indexOf('MAC') >= 0
87
271
  }
88
272
 
89
- onSelect(value: string) {
90
- window.Turbo.visit(value)
91
- }
273
+ protected onDOMKeydown(event: KeyboardEvent) {
274
+ if (!this.isOpenValue) return
92
275
 
93
- disconnect() {
94
- super.disconnect()
276
+ const key = event.key
95
277
 
96
- if (this.keybinds) {
97
- hotkeys.unbind(this.keybinds)
278
+ if (['Tab', 'Enter'].includes(key)) event.preventDefault()
279
+
280
+ if (key === 'Escape') {
281
+ this.close()
98
282
  }
99
283
  }
100
284
 
101
- isOpenValueChanged(isOpen: boolean, previousIsOpen: boolean) {
102
- if (isOpen) {
103
- openWithOverlay(this.contentTarget.id)
104
-
105
- showContent({
106
- trigger: this.triggerTarget,
107
- content: this.contentTarget,
108
- contentContainer: this.contentTarget,
109
- })
110
-
111
- this.setupEventListeners()
112
- } else {
113
- closeWithOverlay(this.contentTarget.id)
114
-
115
- hideContent({
116
- trigger: this.triggerTarget,
117
- content: this.contentTarget,
118
- contentContainer: this.contentTarget,
119
- })
285
+ protected setupEventListeners() {
286
+ document.addEventListener('keydown', this.DOMKeydownListener)
287
+ }
120
288
 
121
- this.cleanupEventListeners()
289
+ protected cleanupEventListeners() {
290
+ document.removeEventListener('keydown', this.DOMKeydownListener)
122
291
 
123
- // Only focus trigger when is previously opened
124
- if (previousIsOpen) {
125
- focusTrigger(this.triggerTarget)
126
- }
292
+ if (this.abortController) {
293
+ this.abortController.abort()
127
294
  }
128
295
  }
129
296
  }
297
+
298
+ type Command = InstanceType<typeof CommandController>
299
+
300
+ export { CommandController }
301
+ export type { Command }