ruby_ui 1.0.0.pre.alpha.4 → 1.0.0.rc1

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 (227) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +21 -0
  3. data/README.md +85 -0
  4. data/lib/generators/ruby_ui/component_generator.rb +94 -0
  5. data/lib/generators/ruby_ui/dependencies.yml +74 -0
  6. data/lib/generators/ruby_ui/install/install_generator.rb +89 -0
  7. data/lib/generators/ruby_ui/install/templates/ruby_ui.rb.erb +18 -0
  8. data/lib/generators/ruby_ui/install/templates/tailwind.css.erb +156 -0
  9. data/lib/generators/ruby_ui/javascript_utils.rb +57 -0
  10. data/lib/{rbui → ruby_ui}/accordion/accordion.rb +1 -1
  11. data/lib/{rbui → ruby_ui}/accordion/accordion_content.rb +2 -2
  12. data/lib/ruby_ui/accordion/accordion_controller.js +97 -0
  13. data/lib/{rbui → ruby_ui}/accordion/accordion_default_content.rb +1 -1
  14. data/lib/{rbui → ruby_ui}/accordion/accordion_default_trigger.rb +3 -3
  15. data/lib/{rbui → ruby_ui}/accordion/accordion_icon.rb +2 -2
  16. data/lib/{rbui → ruby_ui}/accordion/accordion_item.rb +4 -4
  17. data/lib/{rbui → ruby_ui}/accordion/accordion_trigger.rb +3 -2
  18. data/lib/{rbui → ruby_ui}/alert/alert.rb +3 -3
  19. data/lib/{rbui → ruby_ui}/alert/alert_description.rb +1 -1
  20. data/lib/{rbui → ruby_ui}/alert/alert_title.rb +1 -1
  21. data/lib/{rbui → ruby_ui}/alert_dialog/alert_dialog.rb +3 -3
  22. data/lib/{rbui → ruby_ui}/alert_dialog/alert_dialog_action.rb +2 -2
  23. data/lib/{rbui → ruby_ui}/alert_dialog/alert_dialog_cancel.rb +3 -3
  24. data/lib/{rbui → ruby_ui}/alert_dialog/alert_dialog_content.rb +5 -5
  25. data/lib/ruby_ui/alert_dialog/alert_dialog_controller.js +31 -0
  26. data/lib/{rbui → ruby_ui}/alert_dialog/alert_dialog_description.rb +1 -1
  27. data/lib/{rbui → ruby_ui}/alert_dialog/alert_dialog_footer.rb +2 -2
  28. data/lib/{rbui → ruby_ui}/alert_dialog/alert_dialog_header.rb +2 -2
  29. data/lib/{rbui → ruby_ui}/alert_dialog/alert_dialog_title.rb +1 -1
  30. data/lib/{rbui → ruby_ui}/alert_dialog/alert_dialog_trigger.rb +2 -2
  31. data/lib/{rbui → ruby_ui}/aspect_ratio/aspect_ratio.rb +1 -1
  32. data/lib/{rbui → ruby_ui}/avatar/avatar.rb +2 -2
  33. data/lib/{rbui → ruby_ui}/avatar/avatar_fallback.rb +1 -1
  34. data/lib/{rbui → ruby_ui}/avatar/avatar_image.rb +1 -1
  35. data/lib/{rbui → ruby_ui}/badge/badge.rb +2 -2
  36. data/lib/{rbui → ruby_ui}/base.rb +1 -8
  37. data/lib/ruby_ui/breadcrumb/breadcrumb.rb +17 -0
  38. data/lib/ruby_ui/breadcrumb/breadcrumb_ellipsis.rb +39 -0
  39. data/lib/{rbui/typography/typography_list_item.rb → ruby_ui/breadcrumb/breadcrumb_item.rb} +3 -3
  40. data/lib/ruby_ui/breadcrumb/breadcrumb_link.rb +22 -0
  41. data/lib/ruby_ui/breadcrumb/breadcrumb_list.rb +17 -0
  42. data/lib/ruby_ui/breadcrumb/breadcrumb_page.rb +19 -0
  43. data/lib/ruby_ui/breadcrumb/breadcrumb_separator.rb +38 -0
  44. data/lib/{rbui → ruby_ui}/button/button.rb +13 -13
  45. data/lib/ruby_ui/calendar/calendar.rb +39 -0
  46. data/lib/{rbui → ruby_ui}/calendar/calendar_body.rb +2 -2
  47. data/lib/ruby_ui/calendar/calendar_controller.js +249 -0
  48. data/lib/{rbui → ruby_ui}/calendar/calendar_days.rb +14 -14
  49. data/lib/{rbui → ruby_ui}/calendar/calendar_header.rb +1 -1
  50. data/lib/ruby_ui/calendar/calendar_input_controller.js +8 -0
  51. data/lib/{rbui → ruby_ui}/calendar/calendar_next.rb +2 -2
  52. data/lib/{rbui → ruby_ui}/calendar/calendar_prev.rb +2 -2
  53. data/lib/{rbui → ruby_ui}/calendar/calendar_title.rb +2 -2
  54. data/lib/{rbui → ruby_ui}/calendar/calendar_weekdays.rb +2 -2
  55. data/lib/{rbui → ruby_ui}/card/card.rb +1 -1
  56. data/lib/{rbui → ruby_ui}/card/card_content.rb +1 -1
  57. data/lib/{rbui → ruby_ui}/card/card_description.rb +1 -1
  58. data/lib/{rbui → ruby_ui}/card/card_footer.rb +1 -1
  59. data/lib/{rbui → ruby_ui}/card/card_header.rb +1 -1
  60. data/lib/{rbui → ruby_ui}/card/card_title.rb +1 -1
  61. data/lib/ruby_ui/carousel/carousel.rb +44 -0
  62. data/lib/ruby_ui/carousel/carousel_content.rb +23 -0
  63. data/lib/ruby_ui/carousel/carousel_controller.js +60 -0
  64. data/lib/ruby_ui/carousel/carousel_item.rb +23 -0
  65. data/lib/ruby_ui/carousel/carousel_next.rb +48 -0
  66. data/lib/ruby_ui/carousel/carousel_previous.rb +49 -0
  67. data/lib/{rbui → ruby_ui}/chart/chart.rb +3 -3
  68. data/lib/ruby_ui/chart/chart_controller.js +103 -0
  69. data/lib/{rbui → ruby_ui}/checkbox/checkbox.rb +4 -4
  70. data/lib/{rbui → ruby_ui}/checkbox/checkbox_group.rb +2 -2
  71. data/lib/ruby_ui/checkbox/checkbox_group_controller.js +21 -0
  72. data/lib/{rbui → ruby_ui}/clipboard/clipboard.rb +6 -6
  73. data/lib/ruby_ui/clipboard/clipboard_controller.js +54 -0
  74. data/lib/{rbui → ruby_ui}/clipboard/clipboard_popover.rb +2 -2
  75. data/lib/{rbui → ruby_ui}/clipboard/clipboard_source.rb +2 -2
  76. data/lib/{rbui → ruby_ui}/clipboard/clipboard_trigger.rb +3 -3
  77. data/lib/{rbui → ruby_ui}/codeblock/codeblock.rb +7 -10
  78. data/lib/{rbui → ruby_ui}/collapsible/collapsible.rb +3 -3
  79. data/lib/{rbui → ruby_ui}/collapsible/collapsible_content.rb +2 -2
  80. data/lib/ruby_ui/collapsible/collapsible_controller.js +47 -0
  81. data/lib/{rbui → ruby_ui}/collapsible/collapsible_trigger.rb +2 -2
  82. data/lib/ruby_ui/combobox/combobox.rb +26 -0
  83. data/lib/ruby_ui/combobox/combobox_checkbox.rb +25 -0
  84. data/lib/ruby_ui/combobox/combobox_controller.js +176 -0
  85. data/lib/{rbui/combobox/combobox_empty.rb → ruby_ui/combobox/combobox_empty_state.rb} +3 -3
  86. data/lib/ruby_ui/combobox/combobox_item.rb +25 -0
  87. data/lib/ruby_ui/combobox/combobox_list.rb +18 -0
  88. data/lib/ruby_ui/combobox/combobox_list_group.rb +20 -0
  89. data/lib/ruby_ui/combobox/combobox_popover.rb +30 -0
  90. data/lib/ruby_ui/combobox/combobox_radio.rb +26 -0
  91. data/lib/{rbui → ruby_ui}/combobox/combobox_search_input.rb +22 -25
  92. data/lib/ruby_ui/combobox/combobox_toggle_all_checkbox.rb +25 -0
  93. data/lib/{rbui → ruby_ui}/combobox/combobox_trigger.rb +26 -21
  94. data/lib/{rbui → ruby_ui}/command/command.rb +1 -1
  95. data/lib/ruby_ui/command/command_controller.js +136 -0
  96. data/lib/{rbui → ruby_ui}/command/command_dialog.rb +2 -2
  97. data/lib/{rbui → ruby_ui}/command/command_dialog_content.rb +6 -6
  98. data/lib/{rbui → ruby_ui}/command/command_dialog_trigger.rb +3 -3
  99. data/lib/{rbui → ruby_ui}/command/command_empty.rb +2 -2
  100. data/lib/{rbui → ruby_ui}/command/command_group.rb +2 -2
  101. data/lib/{rbui → ruby_ui}/command/command_input.rb +3 -3
  102. data/lib/{rbui → ruby_ui}/command/command_item.rb +2 -2
  103. data/lib/{rbui → ruby_ui}/command/command_list.rb +1 -1
  104. data/lib/{rbui → ruby_ui}/context_menu/context_menu.rb +2 -2
  105. data/lib/{rbui → ruby_ui}/context_menu/context_menu_content.rb +2 -2
  106. data/lib/ruby_ui/context_menu/context_menu_controller.js +144 -0
  107. data/lib/{rbui → ruby_ui}/context_menu/context_menu_item.rb +3 -3
  108. data/lib/{rbui → ruby_ui}/context_menu/context_menu_label.rb +2 -2
  109. data/lib/{rbui → ruby_ui}/context_menu/context_menu_separator.rb +1 -1
  110. data/lib/{rbui → ruby_ui}/context_menu/context_menu_trigger.rb +3 -3
  111. data/lib/{rbui → ruby_ui}/dialog/dialog.rb +3 -3
  112. data/lib/{rbui → ruby_ui}/dialog/dialog_content.rb +9 -9
  113. data/lib/ruby_ui/dialog/dialog_controller.js +32 -0
  114. data/lib/{rbui → ruby_ui}/dialog/dialog_description.rb +1 -1
  115. data/lib/{rbui → ruby_ui}/dialog/dialog_footer.rb +2 -2
  116. data/lib/{rbui → ruby_ui}/dialog/dialog_header.rb +2 -2
  117. data/lib/{rbui → ruby_ui}/dialog/dialog_middle.rb +1 -1
  118. data/lib/{rbui → ruby_ui}/dialog/dialog_title.rb +1 -1
  119. data/lib/{rbui → ruby_ui}/dialog/dialog_trigger.rb +2 -2
  120. data/lib/{rbui → ruby_ui}/dropdown_menu/dropdown_menu.rb +4 -4
  121. data/lib/{rbui → ruby_ui}/dropdown_menu/dropdown_menu_content.rb +2 -2
  122. data/lib/ruby_ui/dropdown_menu/dropdown_menu_controller.js +120 -0
  123. data/lib/{rbui → ruby_ui}/dropdown_menu/dropdown_menu_item.rb +3 -3
  124. data/lib/{rbui → ruby_ui}/dropdown_menu/dropdown_menu_label.rb +1 -1
  125. data/lib/{rbui → ruby_ui}/dropdown_menu/dropdown_menu_separator.rb +1 -1
  126. data/lib/{rbui → ruby_ui}/dropdown_menu/dropdown_menu_trigger.rb +2 -2
  127. data/lib/{rbui → ruby_ui}/form/form.rb +1 -1
  128. data/lib/{rbui → ruby_ui}/form/form_field.rb +2 -2
  129. data/lib/ruby_ui/form/form_field_controller.js +61 -0
  130. data/lib/{rbui → ruby_ui}/form/form_field_error.rb +2 -2
  131. data/lib/{rbui → ruby_ui}/form/form_field_hint.rb +1 -1
  132. data/lib/{rbui → ruby_ui}/form/form_field_label.rb +1 -1
  133. data/lib/{rbui → ruby_ui}/hover_card/hover_card.rb +3 -3
  134. data/lib/{rbui → ruby_ui}/hover_card/hover_card_content.rb +2 -2
  135. data/lib/ruby_ui/hover_card/hover_card_controller.js +144 -0
  136. data/lib/{rbui → ruby_ui}/hover_card/hover_card_trigger.rb +2 -2
  137. data/lib/{rbui → ruby_ui}/input/input.rb +3 -3
  138. data/lib/{rbui → ruby_ui}/link/link.rb +13 -13
  139. data/lib/ruby_ui/masked_input/masked_input.rb +15 -0
  140. data/lib/ruby_ui/masked_input/masked_input_controller.js +9 -0
  141. data/lib/{rbui → ruby_ui}/pagination/pagination.rb +1 -1
  142. data/lib/{rbui → ruby_ui}/pagination/pagination_content.rb +1 -1
  143. data/lib/{rbui → ruby_ui}/pagination/pagination_ellipsis.rb +1 -1
  144. data/lib/{rbui → ruby_ui}/pagination/pagination_item.rb +4 -4
  145. data/lib/{rbui → ruby_ui}/popover/popover.rb +4 -4
  146. data/lib/{rbui → ruby_ui}/popover/popover_content.rb +2 -2
  147. data/lib/ruby_ui/popover/popover_controller.js +107 -0
  148. data/lib/{rbui → ruby_ui}/popover/popover_trigger.rb +2 -2
  149. data/lib/ruby_ui/progress/progress.rb +37 -0
  150. data/lib/ruby_ui/radio_button/radio_button.rb +25 -0
  151. data/lib/{rbui → ruby_ui}/select/select.rb +5 -5
  152. data/lib/{rbui → ruby_ui}/select/select_content.rb +3 -3
  153. data/lib/ruby_ui/select/select_controller.js +171 -0
  154. data/lib/{rbui → ruby_ui}/select/select_group.rb +1 -1
  155. data/lib/{rbui → ruby_ui}/select/select_input.rb +4 -4
  156. data/lib/{rbui → ruby_ui}/select/select_item.rb +4 -4
  157. data/lib/ruby_ui/select/select_item_controller.js +11 -0
  158. data/lib/{rbui → ruby_ui}/select/select_label.rb +1 -1
  159. data/lib/{rbui → ruby_ui}/select/select_trigger.rb +3 -3
  160. data/lib/{rbui → ruby_ui}/select/select_value.rb +3 -3
  161. data/lib/ruby_ui/separator/separator.rb +38 -0
  162. data/lib/{rbui → ruby_ui}/sheet/sheet.rb +2 -2
  163. data/lib/{rbui → ruby_ui}/sheet/sheet_content.rb +8 -8
  164. data/lib/ruby_ui/sheet/sheet_content_controller.js +7 -0
  165. data/lib/ruby_ui/sheet/sheet_controller.js +9 -0
  166. data/lib/{rbui → ruby_ui}/sheet/sheet_description.rb +1 -1
  167. data/lib/{rbui → ruby_ui}/sheet/sheet_footer.rb +1 -1
  168. data/lib/{rbui → ruby_ui}/sheet/sheet_header.rb +1 -1
  169. data/lib/{rbui → ruby_ui}/sheet/sheet_middle.rb +1 -1
  170. data/lib/{rbui → ruby_ui}/sheet/sheet_title.rb +1 -1
  171. data/lib/{rbui → ruby_ui}/sheet/sheet_trigger.rb +2 -2
  172. data/lib/{rbui → ruby_ui}/shortcut_key/shortcut_key.rb +1 -1
  173. data/lib/ruby_ui/skeleton/skeleton.rb +17 -0
  174. data/lib/ruby_ui/switch/switch.rb +24 -0
  175. data/lib/{rbui → ruby_ui}/table/table.rb +1 -1
  176. data/lib/{rbui → ruby_ui}/table/table_body.rb +1 -1
  177. data/lib/{rbui → ruby_ui}/table/table_caption.rb +1 -1
  178. data/lib/{rbui → ruby_ui}/table/table_cell.rb +1 -1
  179. data/lib/{rbui → ruby_ui}/table/table_footer.rb +1 -1
  180. data/lib/{rbui → ruby_ui}/table/table_head.rb +1 -1
  181. data/lib/{rbui → ruby_ui}/table/table_header.rb +1 -1
  182. data/lib/{rbui → ruby_ui}/table/table_row.rb +1 -1
  183. data/lib/{rbui → ruby_ui}/tabs/tabs.rb +3 -3
  184. data/lib/{rbui → ruby_ui}/tabs/tabs_content.rb +2 -2
  185. data/lib/ruby_ui/tabs/tabs_controller.js +45 -0
  186. data/lib/{rbui → ruby_ui}/tabs/tabs_list.rb +1 -1
  187. data/lib/{rbui → ruby_ui}/tabs/tabs_trigger.rb +3 -3
  188. data/lib/{rbui → ruby_ui}/textarea/textarea.rb +3 -3
  189. data/lib/{rbui → ruby_ui}/theme_toggle/theme_toggle.rb +4 -4
  190. data/lib/ruby_ui/theme_toggle/theme_toggle_controller.js +30 -0
  191. data/lib/{rbui → ruby_ui}/tooltip/tooltip.rb +3 -3
  192. data/lib/{rbui → ruby_ui}/tooltip/tooltip_content.rb +3 -3
  193. data/lib/ruby_ui/tooltip/tooltip_controller.js +37 -0
  194. data/lib/{rbui → ruby_ui}/tooltip/tooltip_trigger.rb +2 -2
  195. data/lib/ruby_ui/typography/heading.rb +60 -0
  196. data/lib/{rbui/typography/typography_inline_code.rb → ruby_ui/typography/inline_code.rb} +2 -2
  197. data/lib/{rbui/typography/typography_inline_link.rb → ruby_ui/typography/inline_link.rb} +2 -2
  198. data/lib/ruby_ui/typography/text.rb +53 -0
  199. data/lib/{rbui → ruby_ui}/typography/typography_blockquote.rb +1 -1
  200. data/lib/ruby_ui.rb +5 -1
  201. metadata +208 -173
  202. data/lib/generators/rbui/base_generator.rb +0 -17
  203. data/lib/generators/rbui/component_generator.rb +0 -137
  204. data/lib/generators/rbui/install/install_generator.rb +0 -194
  205. data/lib/rbui/calendar/calendar.rb +0 -39
  206. data/lib/rbui/combobox/combobox.rb +0 -24
  207. data/lib/rbui/combobox/combobox_content.rb +0 -31
  208. data/lib/rbui/combobox/combobox_group.rb +0 -38
  209. data/lib/rbui/combobox/combobox_input.rb +0 -22
  210. data/lib/rbui/combobox/combobox_item.rb +0 -53
  211. data/lib/rbui/combobox/combobox_list.rb +0 -29
  212. data/lib/rbui/combobox/combobox_separator.rb +0 -15
  213. data/lib/rbui/combobox/combobox_value.rb +0 -27
  214. data/lib/rbui/radio_button/radio_button.rb +0 -22
  215. data/lib/rbui/railtie.rb +0 -52
  216. data/lib/rbui/typography/typography_h1.rb +0 -17
  217. data/lib/rbui/typography/typography_h2.rb +0 -17
  218. data/lib/rbui/typography/typography_h3.rb +0 -17
  219. data/lib/rbui/typography/typography_h4.rb +0 -17
  220. data/lib/rbui/typography/typography_large.rb +0 -17
  221. data/lib/rbui/typography/typography_lead.rb +0 -17
  222. data/lib/rbui/typography/typography_list.rb +0 -47
  223. data/lib/rbui/typography/typography_muted.rb +0 -17
  224. data/lib/rbui/typography/typography_p.rb +0 -17
  225. data/lib/rbui/typography/typography_small.rb +0 -17
  226. data/lib/rbui/version.rb +0 -5
  227. data/lib/rbui.rb +0 -57
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class ComboboxCheckbox < Base
5
+ def view_template
6
+ input(type: "checkbox", **attrs)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ class: [
14
+ "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background accent-primary",
15
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
16
+ "disabled:cursor-not-allowed disabled:opacity-50"
17
+ ],
18
+ data: {
19
+ ruby_ui__combobox_target: "input",
20
+ action: "ruby-ui--combobox#inputChanged"
21
+ }
22
+ }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,176 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ import { computePosition, autoUpdate, offset, flip } from "@floating-ui/dom";
3
+
4
+ // Connects to data-controller="ruby-ui--combobox"
5
+ export default class extends Controller {
6
+ static values = {
7
+ term: String
8
+ }
9
+
10
+ static targets = [
11
+ "input",
12
+ "toggleAll",
13
+ "popover",
14
+ "item",
15
+ "emptyState",
16
+ "searchInput",
17
+ "trigger",
18
+ "triggerContent"
19
+ ]
20
+
21
+ selectedItemIndex = null
22
+
23
+ connect() {
24
+ this.updateTriggerContent()
25
+ }
26
+
27
+ disconnect() {
28
+ if (this.cleanup) { this.cleanup() }
29
+ }
30
+
31
+ inputChanged(e) {
32
+ this.updateTriggerContent()
33
+
34
+ if (e.target.type == "radio") {
35
+ this.closePopover()
36
+ }
37
+
38
+ if (this.hasToggleAllTarget && !e.target.checked) {
39
+ this.toggleAllTarget.checked = false
40
+ }
41
+ }
42
+
43
+ inputContent(input) {
44
+ return input.dataset.text || input.parentElement.textContent
45
+ }
46
+
47
+ toggleAllItems() {
48
+ const isChecked = this.toggleAllTarget.checked
49
+ this.inputTargets.forEach(input => input.checked = isChecked)
50
+ this.updateTriggerContent()
51
+ }
52
+
53
+ updateTriggerContent() {
54
+ const checkedInputs = this.inputTargets.filter(input => input.checked)
55
+
56
+ if (checkedInputs.length == 0) {
57
+ this.triggerContentTarget.innerText = this.triggerTarget.dataset.placeholder
58
+ } else if (checkedInputs.length === 1) {
59
+ this.triggerContentTarget.innerText = this.inputContent(checkedInputs[0])
60
+ } else {
61
+ this.triggerContentTarget.innerText = `${checkedInputs.length} ${this.termValue}`
62
+ }
63
+ }
64
+
65
+ openPopover(event) {
66
+ event.preventDefault()
67
+
68
+ this.updatePopoverPosition()
69
+ this.updatePopoverWidth()
70
+ this.triggerTarget.ariaExpanded = "true"
71
+ this.selectedItemIndex = null
72
+ this.itemTargets.forEach(item => item.ariaCurrent = "false")
73
+ this.popoverTarget.showPopover()
74
+ }
75
+
76
+ closePopover() {
77
+ this.triggerTarget.ariaExpanded = "false"
78
+ this.popoverTarget.hidePopover()
79
+ }
80
+
81
+ filterItems(e) {
82
+ if (["ArrowDown", "ArrowUp", "Tab", "Enter"].includes(e.key)) {
83
+ return
84
+ }
85
+
86
+ const filterTerm = this.searchInputTarget.value.toLowerCase()
87
+
88
+ if (this.hasToggleAllTarget) {
89
+ if (filterTerm) this.toggleAllTarget.parentElement.classList.add("hidden")
90
+ else this.toggleAllTarget.parentElement.classList.remove("hidden")
91
+ }
92
+
93
+ let resultCount = 0
94
+
95
+ this.selectedItemIndex = null
96
+
97
+ this.inputTargets.forEach((input) => {
98
+ const text = this.inputContent(input).toLowerCase()
99
+
100
+ if (text.indexOf(filterTerm) > -1) {
101
+ input.parentElement.classList.remove("hidden")
102
+ resultCount++
103
+ } else {
104
+ input.parentElement.classList.add("hidden")
105
+ }
106
+ })
107
+
108
+ this.emptyStateTarget.classList.toggle("hidden", resultCount !== 0)
109
+ }
110
+
111
+ keyDownPressed() {
112
+ if (this.selectedItemIndex !== null) {
113
+ this.selectedItemIndex++
114
+ } else {
115
+ this.selectedItemIndex = 0
116
+ }
117
+
118
+ this.focusSelectedInput()
119
+ }
120
+
121
+ keyUpPressed() {
122
+ if (this.selectedItemIndex !== null) {
123
+ this.selectedItemIndex--
124
+ } else {
125
+ this.selectedItemIndex = -1
126
+ }
127
+
128
+ this.focusSelectedInput()
129
+ }
130
+
131
+ focusSelectedInput() {
132
+ const visibleInputs = this.inputTargets.filter(input => !input.parentElement.classList.contains("hidden"))
133
+
134
+ this.wrapSelectedInputIndex(visibleInputs.length)
135
+
136
+ visibleInputs.forEach((input, index) => {
137
+ if (index == this.selectedItemIndex) {
138
+ input.parentElement.ariaCurrent = "true"
139
+ input.parentElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })
140
+ } else {
141
+ input.parentElement.ariaCurrent = "false"
142
+ }
143
+ })
144
+ }
145
+
146
+ keyEnterPressed(event) {
147
+ event.preventDefault()
148
+ const option = this.itemTargets.find(item => item.ariaCurrent === "true")
149
+
150
+ if (option) {
151
+ option.click()
152
+ }
153
+ }
154
+
155
+ wrapSelectedInputIndex(length) {
156
+ this.selectedItemIndex = ((this.selectedItemIndex % length) + length) % length
157
+ }
158
+
159
+ updatePopoverPosition() {
160
+ this.cleanup = autoUpdate(this.triggerTarget, this.popoverTarget, () => {
161
+ computePosition(this.triggerTarget, this.popoverTarget, {
162
+ placement: 'bottom-start',
163
+ middleware: [offset(4), flip()],
164
+ }).then(({ x, y }) => {
165
+ Object.assign(this.popoverTarget.style, {
166
+ left: `${x}px`,
167
+ top: `${y}px`,
168
+ });
169
+ });
170
+ });
171
+ }
172
+
173
+ updatePopoverWidth() {
174
+ this.popoverTarget.style.width = `${this.triggerTarget.offsetWidth}px`
175
+ }
176
+ }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module RBUI
4
- class ComboboxEmpty < Base
3
+ module RubyUI
4
+ class ComboboxEmptyState < Base
5
5
  def view_template(&)
6
6
  div(**attrs, &)
7
7
  end
@@ -13,7 +13,7 @@ module RBUI
13
13
  role: "presentation",
14
14
  class: "hidden py-6 text-center text-sm",
15
15
  data: {
16
- rbui__combobox_content_target: "empty"
16
+ ruby_ui__combobox_target: "emptyState"
17
17
  }
18
18
  }
19
19
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class ComboboxItem < Base
5
+ def view_template(&)
6
+ label(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ class: [
14
+ "flex flex-row w-full text-wrap [&>span,&>div]:truncate gap-2 items-center rounded-sm px-2 py-1 text-sm outline-none cursor-pointer",
15
+ "select-none has-[:checked]:bg-accent hover:bg-accent p-2",
16
+ "[&>svg]:pointer-events-none [&>svg]:size-4 [&>svg]:shrink-0 aria-[current=true]:bg-accent aria-[current=true]:ring aria-[current=true]:ring-offset-2"
17
+ ],
18
+ role: "option",
19
+ data: {
20
+ ruby_ui__combobox_target: "item"
21
+ }
22
+ }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class ComboboxList < Base
5
+ def view_template(&)
6
+ div(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ class: "flex flex-col gap-1 p-1 max-h-72 overflow-y-auto text-foreground",
14
+ role: "listbox"
15
+ }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class ComboboxListGroup < Base
5
+ LABEL_CLASSES = "before:content-[attr(label)] before:px-2 before:py-1.5 before:text-xs before:font-medium before:text-muted-foreground before:not-italic"
6
+
7
+ def view_template(&)
8
+ div(**attrs, &)
9
+ end
10
+
11
+ private
12
+
13
+ def default_attrs
14
+ {
15
+ class: ["hidden has-[label:not(.hidden)]:flex flex-col py-1 gap-1 border-b", LABEL_CLASSES],
16
+ role: "group"
17
+ }
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class ComboboxPopover < Base
5
+ def view_template(&)
6
+ div(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ class: "inset-auto m-0 absolute border bg-background shadow-lg rounded-lg",
14
+ role: "popover",
15
+ autofocus: true,
16
+ popover: true,
17
+ data: {
18
+ ruby_ui__combobox_target: "popover",
19
+ action: %w[
20
+ keydown.down->ruby-ui--combobox#keyDownPressed
21
+ keydown.up->ruby-ui--combobox#keyUpPressed
22
+ keydown.enter->ruby-ui--combobox#keyEnterPressed
23
+ keydown.esc->ruby-ui--combobox#closeDialog:prevent
24
+ resize@window->ruby-ui--combobox#updatePopoverWidth
25
+ ]
26
+ }
27
+ }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class ComboboxRadio < Base
5
+ def view_template
6
+ input(type: "radio", **attrs)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ class: "aspect-square h-4 w-4 rounded-full border border-primary accent-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
14
+ data: {
15
+ ruby_ui__combobox_target: "input",
16
+ ruby_ui__form_field_target: "input",
17
+ action: %w[
18
+ ruby-ui--combobox#inputChanged
19
+ input->ruby-ui--form-field#onInput
20
+ invalid->ruby-ui--form-field#onInvalid
21
+ ]
22
+ }
23
+ }
24
+ end
25
+ end
26
+ end
@@ -1,22 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module RBUI
3
+ module RubyUI
4
4
  class ComboboxSearchInput < Base
5
- def initialize(placeholder:, **attrs)
5
+ def initialize(placeholder:, **)
6
6
  @placeholder = placeholder
7
- super(**attrs)
7
+ super(**)
8
8
  end
9
9
 
10
10
  def view_template
11
- input_container do
12
- search_icon
11
+ div class: "flex text-muted-foreground items-center border-b px-3" do
12
+ icon
13
13
  input(**attrs)
14
14
  end
15
15
  end
16
16
 
17
17
  private
18
18
 
19
- def search_icon
19
+ def default_attrs
20
+ {
21
+ type: "search",
22
+ class: "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none border-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
23
+ role: "searchbox",
24
+ placeholder: @placeholder,
25
+ data: {
26
+ ruby_ui__combobox_target: "searchInput",
27
+ action: "keyup->ruby-ui--combobox#filterItems search->ruby-ui--combobox#filterItems"
28
+ },
29
+ autocomplete: "off",
30
+ autocorrect: "off",
31
+ spellcheck: "false"
32
+ }
33
+ end
34
+
35
+ def icon
20
36
  svg(
21
37
  xmlns: "http://www.w3.org/2000/svg",
22
38
  viewbox: "0 0 24 24",
@@ -33,24 +49,5 @@ module RBUI
33
49
  )
34
50
  end
35
51
  end
36
-
37
- def input_container(&)
38
- div(class: "flex items-center border-b px-3", &)
39
- end
40
-
41
- def default_attrs
42
- {
43
- class:
44
- "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
45
- placeholder: @placeholder,
46
- data: {
47
- action: "input->rbui--combobox#onSearchInput",
48
- rbui__combobox_target: "search"
49
- },
50
- autocomplete: "off",
51
- autocorrect: "off",
52
- spellcheck: false
53
- }
54
- end
55
52
  end
56
53
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class ComboboxToggleAllCheckbox < Base
5
+ def view_template
6
+ input(type: "checkbox", **attrs)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ class: [
14
+ "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background accent-primary",
15
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
16
+ "disabled:cursor-not-allowed disabled:opacity-50"
17
+ ],
18
+ data: {
19
+ ruby_ui__combobox_target: "toggleAll",
20
+ action: "change->ruby-ui--combobox#toggleAllItems"
21
+ }
22
+ }
23
+ end
24
+ end
25
+ end
@@ -1,16 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module RBUI
3
+ module RubyUI
4
4
  class ComboboxTrigger < Base
5
- def view_template(&block)
5
+ def initialize(placeholder: "", **)
6
+ @placeholder = placeholder
7
+ super(**)
8
+ end
9
+
10
+ def view_template
6
11
  button(**attrs) do
7
- block&.call
12
+ span(class: "truncate", data: {ruby_ui__combobox_target: "triggerContent"}) do
13
+ @placeholder
14
+ end
8
15
  icon
9
16
  end
10
17
  end
11
18
 
12
19
  private
13
20
 
21
+ def default_attrs
22
+ {
23
+ type: "button",
24
+ class: "flex h-full w-full items-center whitespace-nowrap rounded-md text-sm ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2 justify-between",
25
+ data: {
26
+ placeholder: @placeholder,
27
+ ruby_ui__combobox_target: "trigger",
28
+ action: "ruby-ui--combobox#openPopover"
29
+ },
30
+ aria: {
31
+ haspopup: "listbox",
32
+ expanded: "false"
33
+ }
34
+ }
35
+ end
36
+
14
37
  def icon
15
38
  svg(
16
39
  xmlns: "http://www.w3.org/2000/svg",
@@ -30,23 +53,5 @@ module RBUI
30
53
  )
31
54
  end
32
55
  end
33
-
34
- def default_attrs
35
- {
36
- data: {
37
- action: "rbui--combobox#onTriggerClick",
38
- rbui__combobox_target: "trigger"
39
- },
40
- type: "button",
41
- role: "combobox",
42
- aria: {
43
- expanded: "false",
44
- haspopup: "listbox",
45
- autocomplete: "none",
46
- activedescendant: true
47
- },
48
- class: "flex h-full w-full items-center whitespace-nowrap rounded-md text-sm ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2 w-[200px] justify-between"
49
- }
50
- end
51
56
  end
52
57
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module RBUI
3
+ module RubyUI
4
4
  class Command < Base
5
5
  def view_template(&)
6
6
  div(**attrs, &)
@@ -0,0 +1,136 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ import Fuse from "fuse.js";
3
+
4
+ // Connects to data-controller="ruby-ui--command"
5
+ export default class extends Controller {
6
+ static targets = ["input", "group", "item", "empty", "content"];
7
+
8
+ static values = {
9
+ open: {
10
+ type: Boolean,
11
+ default: false,
12
+ },
13
+ };
14
+
15
+ connect() {
16
+ this.inputTarget.focus();
17
+ this.searchIndex = this.buildSearchIndex();
18
+ this.toggleVisibility(this.emptyTargets, false);
19
+ this.selectedIndex = -1;
20
+
21
+ if (this.openValue) {
22
+ this.open();
23
+ }
24
+ }
25
+
26
+ open(e) {
27
+ e.preventDefault();
28
+ document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML);
29
+ // prevent scroll on body
30
+ document.body.classList.add("overflow-hidden");
31
+ }
32
+
33
+ dismiss() {
34
+ // allow scroll on body
35
+ document.body.classList.remove("overflow-hidden");
36
+ // remove the element
37
+ console.log("this.element", this.element);
38
+ this.element.remove();
39
+ }
40
+
41
+ filter(e) {
42
+ // Deselect any previously selected item
43
+ this.deselectAll();
44
+
45
+ const query = e.target.value.toLowerCase();
46
+ if (query.length === 0) {
47
+ this.resetVisibility();
48
+ return;
49
+ }
50
+
51
+ this.toggleVisibility(this.itemTargets, false);
52
+
53
+ const results = this.searchIndex.search(query);
54
+ results.forEach((result) =>
55
+ this.toggleVisibility([result.item.element], true),
56
+ );
57
+
58
+ this.toggleVisibility(this.emptyTargets, results.length === 0);
59
+ this.updateGroupVisibility();
60
+ }
61
+
62
+ toggleVisibility(elements, isVisible) {
63
+ elements.forEach((el) => el.classList.toggle("hidden", !isVisible));
64
+ }
65
+
66
+ updateGroupVisibility() {
67
+ this.groupTargets.forEach((group) => {
68
+ const hasVisibleItems =
69
+ group.querySelectorAll(
70
+ "[data-ruby-ui--command-target='item']:not(.hidden)",
71
+ ).length > 0;
72
+ this.toggleVisibility([group], hasVisibleItems);
73
+ });
74
+ }
75
+
76
+ resetVisibility() {
77
+ this.toggleVisibility(this.itemTargets, true);
78
+ this.toggleVisibility(this.groupTargets, true);
79
+ this.toggleVisibility(this.emptyTargets, false);
80
+ }
81
+
82
+ buildSearchIndex() {
83
+ const options = {
84
+ keys: ["value"],
85
+ threshold: 0.2,
86
+ includeMatches: true,
87
+ };
88
+ const items = this.itemTargets.map((el) => ({
89
+ value: el.dataset.value,
90
+ element: el,
91
+ }));
92
+ return new Fuse(items, options);
93
+ }
94
+
95
+ handleKeydown(e) {
96
+ const visibleItems = this.itemTargets.filter(
97
+ (item) => !item.classList.contains("hidden"),
98
+ );
99
+ if (e.key === "ArrowDown") {
100
+ e.preventDefault();
101
+ this.updateSelectedItem(visibleItems, 1);
102
+ } else if (e.key === "ArrowUp") {
103
+ e.preventDefault();
104
+ this.updateSelectedItem(visibleItems, -1);
105
+ } else if (e.key === "Enter" && this.selectedIndex !== -1) {
106
+ e.preventDefault();
107
+ visibleItems[this.selectedIndex].click();
108
+ }
109
+ }
110
+
111
+ updateSelectedItem(visibleItems, direction) {
112
+ if (this.selectedIndex >= 0) {
113
+ this.toggleAriaSelected(visibleItems[this.selectedIndex], false);
114
+ }
115
+
116
+ this.selectedIndex += direction;
117
+
118
+ // Ensure the selected index is within the bounds of the visible items
119
+ if (this.selectedIndex < 0) {
120
+ this.selectedIndex = visibleItems.length - 1;
121
+ } else if (this.selectedIndex >= visibleItems.length) {
122
+ this.selectedIndex = 0;
123
+ }
124
+
125
+ this.toggleAriaSelected(visibleItems[this.selectedIndex], true);
126
+ }
127
+
128
+ toggleAriaSelected(element, isSelected) {
129
+ element.setAttribute("aria-selected", isSelected.toString());
130
+ }
131
+
132
+ deselectAll() {
133
+ this.itemTargets.forEach((item) => this.toggleAriaSelected(item, false));
134
+ this.selectedIndex = -1;
135
+ }
136
+ }
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module RBUI
3
+ module RubyUI
4
4
  class CommandDialog < Base
5
5
  def view_template(&)
6
6
  div(**attrs, &)
@@ -10,7 +10,7 @@ module RBUI
10
10
 
11
11
  def default_attrs
12
12
  {
13
- data: {controller: "rbui--command"}
13
+ data: {controller: "ruby-ui--command"}
14
14
  }
15
15
  end
16
16
  end