ruby_ui 1.0.2 → 1.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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +4 -0
  4. data/lib/generators/ruby_ui/component_generator.rb +5 -1
  5. data/lib/generators/ruby_ui/dependencies.yml +10 -0
  6. data/lib/generators/ruby_ui/install/docs_generator.rb +33 -0
  7. data/lib/generators/ruby_ui/install/install_generator.rb +1 -1
  8. data/lib/generators/ruby_ui/javascript_utils.rb +4 -0
  9. data/lib/ruby_ui/accordion/accordion_docs.rb +53 -0
  10. data/lib/ruby_ui/alert/alert_docs.rb +135 -0
  11. data/lib/ruby_ui/alert_dialog/alert_dialog_docs.rb +35 -0
  12. data/lib/ruby_ui/aspect_ratio/aspect_ratio_docs.rb +64 -0
  13. data/lib/ruby_ui/avatar/avatar_docs.rb +92 -0
  14. data/lib/ruby_ui/badge/badge_docs.rb +80 -0
  15. data/lib/ruby_ui/breadcrumb/breadcrumb_docs.rb +116 -0
  16. data/lib/ruby_ui/button/button_docs.rb +143 -0
  17. data/lib/ruby_ui/calendar/calendar_docs.rb +34 -0
  18. data/lib/ruby_ui/card/card_docs.rb +114 -0
  19. data/lib/ruby_ui/carousel/carousel_docs.rb +104 -0
  20. data/lib/ruby_ui/chart/chart_docs.rb +115 -0
  21. data/lib/ruby_ui/checkbox/checkbox.rb +2 -2
  22. data/lib/ruby_ui/checkbox/checkbox_docs.rb +41 -0
  23. data/lib/ruby_ui/clipboard/clipboard_docs.rb +30 -0
  24. data/lib/ruby_ui/codeblock/codeblock_docs.rb +55 -0
  25. data/lib/ruby_ui/collapsible/collapsible_docs.rb +96 -0
  26. data/lib/ruby_ui/combobox/combobox.rb +7 -1
  27. data/lib/ruby_ui/combobox/combobox_badge.rb +17 -0
  28. data/lib/ruby_ui/combobox/combobox_badge_trigger.rb +47 -0
  29. data/lib/ruby_ui/combobox/combobox_checkbox.rb +1 -7
  30. data/lib/ruby_ui/combobox/combobox_clear_button.rb +40 -0
  31. data/lib/ruby_ui/combobox/combobox_controller.js +252 -47
  32. data/lib/ruby_ui/combobox/combobox_docs.rb +286 -0
  33. data/lib/ruby_ui/combobox/combobox_input_trigger.rb +64 -0
  34. data/lib/ruby_ui/combobox/combobox_item.rb +5 -7
  35. data/lib/ruby_ui/combobox/combobox_item_indicator.rb +30 -0
  36. data/lib/ruby_ui/combobox/combobox_list_group.rb +1 -1
  37. data/lib/ruby_ui/combobox/combobox_popover.rb +1 -5
  38. data/lib/ruby_ui/combobox/combobox_radio.rb +1 -8
  39. data/lib/ruby_ui/combobox/combobox_toggle_all_checkbox.rb +1 -6
  40. data/lib/ruby_ui/combobox/combobox_trigger.rb +19 -19
  41. data/lib/ruby_ui/command/command_docs.rb +154 -0
  42. data/lib/ruby_ui/context_menu/context_menu.rb +1 -1
  43. data/lib/ruby_ui/context_menu/context_menu_docs.rb +85 -0
  44. data/lib/ruby_ui/data_table/data_table.rb +29 -0
  45. data/lib/ruby_ui/data_table/data_table_bulk_actions.rb +18 -0
  46. data/lib/ruby_ui/data_table/data_table_column_toggle.rb +62 -0
  47. data/lib/ruby_ui/data_table/data_table_column_visibility_controller.js +14 -0
  48. data/lib/ruby_ui/data_table/data_table_controller.js +57 -0
  49. data/lib/ruby_ui/data_table/data_table_docs.rb +180 -0
  50. data/lib/ruby_ui/data_table/data_table_expand_toggle.rb +53 -0
  51. data/lib/ruby_ui/data_table/data_table_form.rb +39 -0
  52. data/lib/ruby_ui/data_table/data_table_kaminari_adapter.rb +17 -0
  53. data/lib/ruby_ui/data_table/data_table_manual_adapter.rb +17 -0
  54. data/lib/ruby_ui/data_table/data_table_pagination.rb +100 -0
  55. data/lib/ruby_ui/data_table/data_table_pagination_bar.rb +15 -0
  56. data/lib/ruby_ui/data_table/data_table_pagy_adapter.rb +17 -0
  57. data/lib/ruby_ui/data_table/data_table_per_page_select.rb +35 -0
  58. data/lib/ruby_ui/data_table/data_table_row_checkbox.rb +30 -0
  59. data/lib/ruby_ui/data_table/data_table_search.rb +57 -0
  60. data/lib/ruby_ui/data_table/data_table_search_controller.js +62 -0
  61. data/lib/ruby_ui/data_table/data_table_select_all_checkbox.rb +21 -0
  62. data/lib/ruby_ui/data_table/data_table_selection_summary.rb +25 -0
  63. data/lib/ruby_ui/data_table/data_table_sort_head.rb +112 -0
  64. data/lib/ruby_ui/data_table/data_table_toolbar.rb +15 -0
  65. data/lib/ruby_ui/dialog/dialog_docs.rb +102 -0
  66. data/lib/ruby_ui/docs/base.rb +90 -0
  67. data/lib/ruby_ui/docs/component_setup_tabs.rb +15 -0
  68. data/lib/ruby_ui/docs/components_table.rb +13 -0
  69. data/lib/ruby_ui/docs/header.rb +17 -0
  70. data/lib/ruby_ui/docs/sidebar_examples.rb +22 -0
  71. data/lib/ruby_ui/docs/visual_code_example.rb +22 -0
  72. data/lib/ruby_ui/dropdown_menu/dropdown_menu_docs.rb +212 -0
  73. data/lib/ruby_ui/form/form_docs.rb +178 -0
  74. data/lib/ruby_ui/form/form_field.rb +1 -1
  75. data/lib/ruby_ui/form/form_field_error.rb +1 -1
  76. data/lib/ruby_ui/form/form_field_hint.rb +1 -1
  77. data/lib/ruby_ui/form/form_field_label.rb +1 -1
  78. data/lib/ruby_ui/hover_card/hover_card_docs.rb +71 -0
  79. data/lib/ruby_ui/input/input.rb +4 -3
  80. data/lib/ruby_ui/input/input_docs.rb +68 -0
  81. data/lib/ruby_ui/link/link_docs.rb +106 -0
  82. data/lib/ruby_ui/masked_input/masked_input.rb +11 -1
  83. data/lib/ruby_ui/masked_input/masked_input_controller.js +13 -0
  84. data/lib/ruby_ui/masked_input/masked_input_docs.rb +47 -0
  85. data/lib/ruby_ui/native_select/native_select.rb +39 -0
  86. data/lib/ruby_ui/native_select/native_select_docs.rb +83 -0
  87. data/lib/ruby_ui/native_select/native_select_group.rb +15 -0
  88. data/lib/ruby_ui/native_select/native_select_icon.rb +39 -0
  89. data/lib/ruby_ui/native_select/native_select_option.rb +15 -0
  90. data/lib/ruby_ui/pagination/pagination_docs.rb +127 -0
  91. data/lib/ruby_ui/popover/popover_docs.rb +971 -0
  92. data/lib/ruby_ui/progress/progress_docs.rb +27 -0
  93. data/lib/ruby_ui/radio_button/radio_button.rb +1 -1
  94. data/lib/ruby_ui/radio_button/radio_button_docs.rb +53 -0
  95. data/lib/ruby_ui/select/select_docs.rb +129 -0
  96. data/lib/ruby_ui/separator/separator_docs.rb +36 -0
  97. data/lib/ruby_ui/sheet/sheet_content.rb +1 -1
  98. data/lib/ruby_ui/sheet/sheet_docs.rb +76 -0
  99. data/lib/ruby_ui/shortcut_key/shortcut_key_docs.rb +29 -0
  100. data/lib/ruby_ui/sidebar/sidebar_docs.rb +176 -0
  101. data/lib/ruby_ui/skeleton/skeleton_docs.rb +29 -0
  102. data/lib/ruby_ui/switch/switch_docs.rb +46 -0
  103. data/lib/ruby_ui/table/table_docs.rb +102 -0
  104. data/lib/ruby_ui/tabs/tabs_docs.rb +211 -0
  105. data/lib/ruby_ui/textarea/textarea_docs.rb +54 -0
  106. data/lib/ruby_ui/theme_toggle/theme_toggle_docs.rb +71 -0
  107. data/lib/ruby_ui/tooltip/tooltip_docs.rb +52 -0
  108. data/lib/ruby_ui/typography/typography_docs.rb +107 -0
  109. data/lib/ruby_ui.rb +1 -1
  110. metadata +90 -3
@@ -15,75 +15,276 @@ export default class extends Controller {
15
15
  "emptyState",
16
16
  "searchInput",
17
17
  "trigger",
18
- "triggerContent"
18
+ "triggerContent",
19
+ "badgeContainer",
20
+ "clearButton",
21
+ "badgeInput",
22
+ "inputTrigger"
19
23
  ]
20
24
 
21
25
  selectedItemIndex = null
22
26
 
23
27
  connect() {
24
28
  this.updateTriggerContent()
29
+ this.updateBadges()
30
+ this.updateClearButton()
31
+ this.updateInputTrigger()
32
+
33
+ // Track mouse state to distinguish click-focus from tab-focus
34
+ this._mouseDown = false
35
+ this.element.addEventListener("mousedown", () => { this._mouseDown = true })
36
+ this.element.addEventListener("mouseup", () => { setTimeout(() => { this._mouseDown = false }, 0) })
25
37
  }
26
38
 
27
39
  disconnect() {
28
40
  if (this.cleanup) { this.cleanup() }
29
41
  }
30
42
 
43
+ // Popover
44
+
45
+ togglePopover(event) {
46
+ event.preventDefault()
47
+
48
+ if (this.triggerTarget.ariaExpanded === "true") {
49
+ this.closePopover()
50
+ } else {
51
+ this.openPopover(event)
52
+ }
53
+ }
54
+
55
+ openPopover(event) {
56
+ if (event && event.type !== "focusin" && event.type !== "focus") event.preventDefault()
57
+
58
+ // focusin/focus: only open on keyboard focus (tab), not mouse click
59
+ if (event && (event.type === "focusin" || event.type === "focus")) {
60
+ if (this._mouseDown || this.triggerTarget.ariaExpanded === "true" || this._closingPopover) return
61
+ }
62
+
63
+ this.updatePopoverPosition()
64
+ this.updatePopoverWidth()
65
+ this.triggerTarget.ariaExpanded = "true"
66
+ this.selectedItemIndex = null
67
+ this.itemTargets.forEach(item => item.ariaCurrent = "false")
68
+ this.popoverTarget.showPopover()
69
+
70
+ // Always show all items on open; filter only on user typing
71
+ this.applyFilter("")
72
+
73
+ if (this.hasBadgeInputTarget) {
74
+ this.badgeInputTarget.value = ""
75
+ }
76
+ }
77
+
78
+ closePopover() {
79
+ this._closingPopover = true
80
+ this.triggerTarget.ariaExpanded = "false"
81
+ this.popoverTarget.hidePopover()
82
+ setTimeout(() => this._closingPopover = false, 200)
83
+ }
84
+
85
+ handlePopoverToggle(event) {
86
+ // Keep ariaExpanded in sync with the actual popover state
87
+ this.triggerTarget.ariaExpanded = event.newState === 'open' ? 'true' : 'false'
88
+ }
89
+
90
+ updatePopoverPosition() {
91
+ this.cleanup = autoUpdate(this.triggerTarget, this.popoverTarget, () => {
92
+ computePosition(this.triggerTarget, this.popoverTarget, {
93
+ placement: 'bottom-start',
94
+ middleware: [offset(4), flip()],
95
+ }).then(({ x, y }) => {
96
+ Object.assign(this.popoverTarget.style, {
97
+ left: `${x}px`,
98
+ top: `${y}px`,
99
+ });
100
+ });
101
+ });
102
+ }
103
+
104
+ updatePopoverWidth() {
105
+ this.popoverTarget.style.width = `${this.triggerTarget.offsetWidth}px`
106
+ }
107
+
108
+ // Selection
109
+
31
110
  inputChanged(e) {
32
111
  this.updateTriggerContent()
33
112
 
34
113
  if (e.target.type == "radio") {
35
114
  this.closePopover()
115
+ this.updateInputTrigger()
36
116
  }
37
117
 
38
118
  if (this.hasToggleAllTarget && !e.target.checked) {
39
119
  this.toggleAllTarget.checked = false
40
120
  }
41
- }
42
121
 
43
- inputContent(input) {
44
- return input.dataset.text || input.parentElement.textContent
122
+ this.updateBadges()
123
+ this.updateClearButton()
45
124
  }
46
125
 
47
126
  toggleAllItems() {
48
127
  const isChecked = this.toggleAllTarget.checked
49
128
  this.inputTargets.forEach(input => input.checked = isChecked)
50
129
  this.updateTriggerContent()
130
+ this.updateBadges()
131
+ this.updateClearButton()
132
+ }
133
+
134
+ clearAll(event) {
135
+ if (event) event.preventDefault()
136
+
137
+ this.inputTargets.forEach(input => input.checked = false)
138
+ this.updateBadges()
139
+ this.updateClearButton()
140
+ this.updateTriggerContent()
141
+ this.updateInputTrigger()
142
+ }
143
+
144
+ removeBadge(event) {
145
+ event.preventDefault()
146
+ event.stopPropagation()
147
+
148
+ const value = event.currentTarget.closest('[data-value]').dataset.value
149
+ const input = this.inputTargets.find(input => input.value === value)
150
+
151
+ if (input) {
152
+ input.checked = false
153
+ input.dispatchEvent(new Event("change", { bubbles: true }))
154
+ }
155
+ }
156
+
157
+ // Display
158
+
159
+ inputContent(input) {
160
+ return input.dataset.text || input.parentElement.textContent
51
161
  }
52
162
 
53
163
  updateTriggerContent() {
164
+ if (!this.hasTriggerContentTarget) return
165
+
54
166
  const checkedInputs = this.inputTargets.filter(input => input.checked)
55
167
 
56
168
  if (checkedInputs.length === 0) {
57
169
  this.triggerContentTarget.innerText = this.triggerTarget.dataset.placeholder
170
+ this.triggerContentTarget.classList.add("text-muted-foreground")
58
171
  } else if (this.termValue && checkedInputs.length > 1) {
59
172
  this.triggerContentTarget.innerText = `${checkedInputs.length} ${this.termValue}`
173
+ this.triggerContentTarget.classList.remove("text-muted-foreground")
60
174
  } else {
61
175
  this.triggerContentTarget.innerText = checkedInputs.map((input) => this.inputContent(input)).join(", ")
176
+ this.triggerContentTarget.classList.remove("text-muted-foreground")
62
177
  }
63
178
  }
64
179
 
65
- openPopover(event) {
66
- event.preventDefault()
180
+ updateInputTrigger() {
181
+ if (!this.hasInputTriggerTarget) return
182
+ const checked = this.inputTargets.find(i => i.checked)
183
+ this.inputTriggerTarget.value = checked ? this.inputContent(checked) : ""
184
+ }
67
185
 
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()
186
+ // NOTE: badge classes mirror ComboboxBadge Ruby component. Update both if styles change.
187
+ updateBadges() {
188
+ if (!this.hasBadgeContainerTarget) return
189
+
190
+ // Remove existing badges
191
+ this.triggerTarget.querySelectorAll("[data-combobox-badge]").forEach(el => el.remove())
192
+
193
+ const checkedInputs = this.inputTargets.filter(input => input.checked)
194
+
195
+ // Toggle trigger height: h-9 when empty, h-auto min-h-9 when badges exist
196
+ if (checkedInputs.length > 0) {
197
+ this.triggerTarget.classList.remove("h-9")
198
+ this.triggerTarget.classList.add("h-auto", "min-h-9")
199
+ } else {
200
+ this.triggerTarget.classList.remove("h-auto", "min-h-9", "pt-1.5")
201
+ this.triggerTarget.classList.add("h-9")
202
+ }
203
+
204
+ checkedInputs.forEach(input => {
205
+ const badge = document.createElement("span")
206
+ badge.setAttribute("data-combobox-badge", "")
207
+ badge.className = "inline-flex items-center gap-1 rounded-md border bg-secondary px-1.5 py-0.5 text-xs font-medium text-secondary-foreground"
208
+ badge.dataset.value = input.value
209
+
210
+ badge.appendChild(document.createTextNode(this.inputContent(input).trim()))
211
+
212
+ const btn = document.createElement("button")
213
+ btn.type = "button"
214
+ btn.setAttribute("aria-label", "Remove")
215
+ btn.className = "rounded-sm opacity-50 hover:opacity-100 focus-visible:outline-none"
216
+
217
+ btn.addEventListener("click", (e) => {
218
+ e.preventDefault()
219
+ e.stopPropagation()
220
+ e.stopImmediatePropagation()
221
+ const target = this.inputTargets.find(i => i.value === input.value)
222
+ if (target) {
223
+ target.checked = false
224
+ this.updateBadges()
225
+ this.updateClearButton()
226
+ }
227
+ })
228
+
229
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
230
+ svg.setAttribute("xmlns", "http://www.w3.org/2000/svg")
231
+ svg.setAttribute("width", "12")
232
+ svg.setAttribute("height", "12")
233
+ svg.setAttribute("viewBox", "0 0 24 24")
234
+ svg.setAttribute("fill", "none")
235
+ svg.setAttribute("stroke", "currentColor")
236
+ svg.setAttribute("stroke-width", "2")
237
+ svg.setAttribute("stroke-linecap", "round")
238
+ svg.setAttribute("stroke-linejoin", "round")
239
+ svg.classList.add("pointer-events-none")
240
+
241
+ const path1 = document.createElementNS("http://www.w3.org/2000/svg", "path")
242
+ path1.setAttribute("d", "M18 6 6 18")
243
+ const path2 = document.createElementNS("http://www.w3.org/2000/svg", "path")
244
+ path2.setAttribute("d", "m6 6 12 12")
245
+
246
+ svg.appendChild(path1)
247
+ svg.appendChild(path2)
248
+ btn.appendChild(svg)
249
+ badge.appendChild(btn)
250
+
251
+ // Insert badge directly in trigger, before the text input
252
+ this.badgeInputTarget.insertAdjacentElement("beforebegin", badge)
253
+ })
254
+
255
+ // Add top padding only when badges wrap to multiple lines
256
+ // Class "pt-1.5" is referenced in ComboboxBadgeTrigger for Tailwind to compile it
257
+ const badges = this.triggerTarget.querySelectorAll("[data-combobox-badge]")
258
+ if (badges.length > 0 && this.badgeInputTarget.offsetTop > badges[0].offsetTop) {
259
+ this.triggerTarget.classList.add("pt-1.5")
260
+ } else {
261
+ this.triggerTarget.classList.remove("pt-1.5")
262
+ }
74
263
  }
75
264
 
76
- closePopover() {
77
- this.triggerTarget.ariaExpanded = "false"
78
- this.popoverTarget.hidePopover()
265
+ updateClearButton() {
266
+ if (!this.hasClearButtonTarget) return
267
+
268
+ const hasChecked = this.inputTargets.some(input => input.checked)
269
+ this.clearButtonTarget.classList.toggle("hidden", !hasChecked)
79
270
  }
80
271
 
272
+ // Filter
273
+
81
274
  filterItems(e) {
82
- if (["ArrowDown", "ArrowUp", "Tab", "Enter"].includes(e.key)) {
83
- return
84
- }
275
+ if (["ArrowDown", "ArrowUp", "Tab", "Enter"].includes(e.key)) return
85
276
 
86
- const filterTerm = this.searchInputTarget.value.toLowerCase()
277
+ const term = this.hasBadgeInputTarget
278
+ ? this.badgeInputTarget.value
279
+ : this.hasInputTriggerTarget
280
+ ? this.inputTriggerTarget.value
281
+ : this.searchInputTarget.value
282
+
283
+ this.applyFilter(term)
284
+ }
285
+
286
+ applyFilter(term) {
287
+ const filterTerm = term.toLowerCase()
87
288
 
88
289
  if (this.hasToggleAllTarget) {
89
290
  if (filterTerm) this.toggleAllTarget.parentElement.classList.add("hidden")
@@ -91,12 +292,10 @@ export default class extends Controller {
91
292
  }
92
293
 
93
294
  let resultCount = 0
94
-
95
295
  this.selectedItemIndex = null
96
296
 
97
297
  this.inputTargets.forEach((input) => {
98
298
  const text = this.inputContent(input).toLowerCase()
99
-
100
299
  if (text.indexOf(filterTerm) > -1) {
101
300
  input.parentElement.classList.remove("hidden")
102
301
  resultCount++
@@ -106,9 +305,20 @@ export default class extends Controller {
106
305
  })
107
306
 
108
307
  this.emptyStateTarget.classList.toggle("hidden", resultCount !== 0)
308
+
309
+ // Auto-highlight first visible result (without scrolling to avoid page jump)
310
+ this.itemTargets.forEach(item => item.ariaCurrent = "false")
311
+ const firstVisible = this.inputTargets.find(i => !i.parentElement.classList.contains("hidden"))
312
+ if (firstVisible) {
313
+ this.selectedItemIndex = 0
314
+ firstVisible.parentElement.ariaCurrent = "true"
315
+ }
109
316
  }
110
317
 
111
- keyDownPressed() {
318
+ // Keyboard
319
+
320
+ keyDownPressed(event) {
321
+ event.preventDefault()
112
322
  if (this.selectedItemIndex !== null) {
113
323
  this.selectedItemIndex++
114
324
  } else {
@@ -118,7 +328,8 @@ export default class extends Controller {
118
328
  this.focusSelectedInput()
119
329
  }
120
330
 
121
- keyUpPressed() {
331
+ keyUpPressed(event) {
332
+ event.preventDefault()
122
333
  if (this.selectedItemIndex !== null) {
123
334
  this.selectedItemIndex--
124
335
  } else {
@@ -128,6 +339,15 @@ export default class extends Controller {
128
339
  this.focusSelectedInput()
129
340
  }
130
341
 
342
+ keyEnterPressed(event) {
343
+ event.preventDefault()
344
+ const option = this.itemTargets.find(item => item.ariaCurrent === "true")
345
+
346
+ if (option) {
347
+ option.click()
348
+ }
349
+ }
350
+
131
351
  focusSelectedInput() {
132
352
  const visibleInputs = this.inputTargets.filter(input => !input.parentElement.classList.contains("hidden"))
133
353
 
@@ -143,34 +363,19 @@ export default class extends Controller {
143
363
  })
144
364
  }
145
365
 
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
366
  wrapSelectedInputIndex(length) {
156
367
  this.selectedItemIndex = ((this.selectedItemIndex % length) + length) % length
157
368
  }
158
369
 
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
- }
370
+ handleBadgeInputBackspace(event) {
371
+ if (this.badgeInputTarget.value !== "") return
172
372
 
173
- updatePopoverWidth() {
174
- this.popoverTarget.style.width = `${this.triggerTarget.offsetWidth}px`
373
+ const checkedInputs = this.inputTargets.filter(input => input.checked)
374
+ const lastChecked = checkedInputs[checkedInputs.length - 1]
375
+
376
+ if (lastChecked) {
377
+ lastChecked.checked = false
378
+ lastChecked.dispatchEvent(new Event("change", { bubbles: true }))
379
+ }
175
380
  }
176
381
  }
@@ -0,0 +1,286 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Views::Docs::Combobox < Views::Base
4
+ def view_template
5
+ component = "Combobox"
6
+ div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
7
+ render Docs::Header.new(title: component, description: "Autocomplete input and command palette with a list of suggestions.")
8
+
9
+ Heading(level: 2) { "Usage" }
10
+
11
+ render Docs::VisualCodeExample.new(title: "Basic", context: self) do
12
+ <<~RUBY
13
+ div(class: "w-96") do
14
+ Combobox do
15
+ ComboboxInputTrigger(placeholder: "Select framework...")
16
+
17
+ ComboboxPopover do
18
+ ComboboxList do
19
+ ComboboxEmptyState { "No results found." }
20
+
21
+ ComboboxItem do
22
+ ComboboxRadio(name: "framework", value: "rails")
23
+ span { "Rails" }
24
+ end
25
+ ComboboxItem do
26
+ ComboboxRadio(name: "framework", value: "hanami")
27
+ span { "Hanami" }
28
+ end
29
+ ComboboxItem do
30
+ ComboboxRadio(name: "framework", value: "nextjs")
31
+ span { "Next.js" }
32
+ end
33
+ ComboboxItem do
34
+ ComboboxRadio(name: "framework", value: "nuxt")
35
+ span { "Nuxt" }
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ RUBY
42
+ end
43
+
44
+ render Docs::VisualCodeExample.new(title: "Popup", context: self) do
45
+ <<~RUBY
46
+ div(class: "w-96") do
47
+ Combobox do
48
+ ComboboxTrigger(placeholder: "Select framework...")
49
+
50
+ ComboboxPopover do
51
+ ComboboxSearchInput(placeholder: "Search framework...")
52
+
53
+ ComboboxList do
54
+ ComboboxEmptyState { "No results found." }
55
+
56
+ ComboboxItem do
57
+ ComboboxRadio(name: "fw2", value: "rails")
58
+ span { "Rails" }
59
+ end
60
+ ComboboxItem do
61
+ ComboboxRadio(name: "fw2", value: "hanami")
62
+ span { "Hanami" }
63
+ end
64
+ ComboboxItem do
65
+ ComboboxRadio(name: "fw2", value: "nextjs")
66
+ span { "Next.js" }
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ RUBY
73
+ end
74
+
75
+ render Docs::VisualCodeExample.new(title: "Multiple", context: self) do
76
+ <<~RUBY
77
+ div(class: "w-96") do
78
+ Combobox do
79
+ ComboboxBadgeTrigger(clear_button: true)
80
+
81
+ ComboboxPopover do
82
+ ComboboxList do
83
+ ComboboxEmptyState { "No results found." }
84
+
85
+ ComboboxItem do
86
+ ComboboxCheckbox(name: "frameworks[]", value: "rails")
87
+ span { "Rails" }
88
+ end
89
+ ComboboxItem do
90
+ ComboboxCheckbox(name: "frameworks[]", value: "hanami")
91
+ span { "Hanami" }
92
+ end
93
+ ComboboxItem do
94
+ ComboboxCheckbox(name: "frameworks[]", value: "sinatra")
95
+ span { "Sinatra" }
96
+ end
97
+ ComboboxItem do
98
+ ComboboxCheckbox(name: "frameworks[]", value: "nextjs", checked: true)
99
+ span { "Next.js" }
100
+ end
101
+ ComboboxItem do
102
+ ComboboxCheckbox(name: "frameworks[]", value: "nuxt")
103
+ span { "Nuxt" }
104
+ end
105
+ ComboboxItem do
106
+ ComboboxCheckbox(name: "frameworks[]", value: "svelte")
107
+ span { "SvelteKit" }
108
+ end
109
+ ComboboxItem do
110
+ ComboboxCheckbox(name: "frameworks[]", value: "remix")
111
+ span { "Remix" }
112
+ end
113
+ ComboboxItem do
114
+ ComboboxCheckbox(name: "frameworks[]", value: "astro")
115
+ span { "Astro" }
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ RUBY
122
+ end
123
+
124
+ render Docs::VisualCodeExample.new(title: "Groups", context: self) do
125
+ <<~RUBY
126
+ div(class: "w-96") do
127
+ Combobox do
128
+ ComboboxInputTrigger(placeholder: "Select food...")
129
+
130
+ ComboboxPopover do
131
+ ComboboxList do
132
+ ComboboxEmptyState { "No results found." }
133
+
134
+ ComboboxListGroup(label: "Fruits") do
135
+ ComboboxItem do
136
+ ComboboxRadio(name: "food", value: "apple")
137
+ span { "Apple" }
138
+ end
139
+ ComboboxItem do
140
+ ComboboxRadio(name: "food", value: "banana")
141
+ span { "Banana" }
142
+ end
143
+ end
144
+
145
+ ComboboxListGroup(label: "Vegetables") do
146
+ ComboboxItem do
147
+ ComboboxRadio(name: "food", value: "broccoli")
148
+ span { "Broccoli" }
149
+ end
150
+ ComboboxItem do
151
+ ComboboxRadio(name: "food", value: "carrot")
152
+ span { "Carrot" }
153
+ end
154
+ end
155
+
156
+ ComboboxListGroup(label: "Grains") do
157
+ ComboboxItem do
158
+ ComboboxRadio(name: "food", value: "rice")
159
+ span { "Rice" }
160
+ end
161
+ ComboboxItem do
162
+ ComboboxRadio(name: "food", value: "wheat")
163
+ span { "Wheat" }
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+ RUBY
171
+ end
172
+
173
+ render Docs::VisualCodeExample.new(title: "Custom Items", context: self) do
174
+ <<~RUBY
175
+ div(class: "w-96") do
176
+ Combobox do
177
+ ComboboxInputTrigger(placeholder: "Select status...")
178
+
179
+ ComboboxPopover do
180
+ ComboboxList do
181
+ ComboboxEmptyState { "No results found." }
182
+
183
+ ComboboxItem do
184
+ ComboboxRadio(name: "status", value: "backlog", data: {text: "Backlog"})
185
+ svg(xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewbox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", class: "text-muted-foreground") { |s| s.circle(cx: "12", cy: "12", r: "10") }
186
+ span { "Backlog" }
187
+ end
188
+ ComboboxItem do
189
+ ComboboxRadio(name: "status", value: "todo", data: {text: "Todo"})
190
+ svg(xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewbox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", class: "text-blue-500") { |s| s.circle(cx: "12", cy: "12", r: "10") }
191
+ span { "Todo" }
192
+ end
193
+ ComboboxItem do
194
+ ComboboxRadio(name: "status", value: "done", data: {text: "Done"})
195
+ svg(xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewbox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", class: "text-green-500") { |s| s.path(d: "M22 11.08V12a10 10 0 1 1-5.93-9.14"); s.path(d: "m9 11 3 3L22 4") }
196
+ span { "Done" }
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
202
+ RUBY
203
+ end
204
+
205
+ render Docs::VisualCodeExample.new(title: "Invalid", context: self) do
206
+ <<~RUBY
207
+ div(class: "w-96") do
208
+ Combobox do
209
+ ComboboxInputTrigger(placeholder: "Required field", aria: {invalid: "true"})
210
+
211
+ ComboboxPopover do
212
+ ComboboxList do
213
+ ComboboxEmptyState { "No results found." }
214
+
215
+ ComboboxItem do
216
+ ComboboxRadio(name: "req", value: "option1")
217
+ span { "Option 1" }
218
+ end
219
+ ComboboxItem do
220
+ ComboboxRadio(name: "req", value: "option2")
221
+ span { "Option 2" }
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
227
+ RUBY
228
+ end
229
+
230
+ render Docs::VisualCodeExample.new(title: "Disabled", context: self) do
231
+ <<~RUBY
232
+ div(class: "w-96 space-y-2") do
233
+ Combobox do
234
+ ComboboxTrigger(disabled: true, placeholder: "Disabled trigger")
235
+ end
236
+
237
+ Combobox do
238
+ ComboboxInputTrigger(placeholder: "Disabled input", disabled: true)
239
+ end
240
+ end
241
+ RUBY
242
+ end
243
+
244
+ render Docs::VisualCodeExample.new(title: "Auto Highlight", context: self) do
245
+ <<~RUBY
246
+ div(class: "w-96") do
247
+ Combobox do
248
+ ComboboxInputTrigger(placeholder: "Type to search...")
249
+
250
+ ComboboxPopover do
251
+ ComboboxList do
252
+ ComboboxEmptyState { "No results found." }
253
+
254
+ ComboboxItem do
255
+ ComboboxRadio(name: "color", value: "red")
256
+ span { "Red" }
257
+ end
258
+ ComboboxItem do
259
+ ComboboxRadio(name: "color", value: "green")
260
+ span { "Green" }
261
+ end
262
+ ComboboxItem do
263
+ ComboboxRadio(name: "color", value: "blue")
264
+ span { "Blue" }
265
+ end
266
+ ComboboxItem do
267
+ ComboboxRadio(name: "color", value: "yellow")
268
+ span { "Yellow" }
269
+ end
270
+ ComboboxItem do
271
+ ComboboxRadio(name: "color", value: "purple")
272
+ span { "Purple" }
273
+ end
274
+ end
275
+ end
276
+ end
277
+ end
278
+ RUBY
279
+ end
280
+
281
+ render Components::ComponentSetup::Tabs.new(component_name: component)
282
+
283
+ render Docs::ComponentsTable.new(component_files(component))
284
+ end
285
+ end
286
+ end