ruby_ui 1.0.0.beta1 → 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 (78) 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 +4 -40
  5. data/lib/generators/ruby_ui/dependencies.yml +74 -0
  6. data/lib/generators/ruby_ui/install/install_generator.rb +21 -22
  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 +21 -0
  10. data/lib/ruby_ui/accordion/accordion_controller.js +97 -0
  11. data/lib/ruby_ui/alert/alert.rb +1 -1
  12. data/lib/ruby_ui/alert_dialog/alert_dialog_content.rb +1 -1
  13. data/lib/ruby_ui/alert_dialog/alert_dialog_controller.js +31 -0
  14. data/lib/ruby_ui/alert_dialog/alert_dialog_footer.rb +1 -1
  15. data/lib/ruby_ui/alert_dialog/alert_dialog_header.rb +1 -1
  16. data/lib/ruby_ui/breadcrumb/breadcrumb.rb +17 -0
  17. data/lib/ruby_ui/breadcrumb/breadcrumb_ellipsis.rb +39 -0
  18. data/lib/ruby_ui/breadcrumb/breadcrumb_item.rb +17 -0
  19. data/lib/ruby_ui/breadcrumb/breadcrumb_link.rb +22 -0
  20. data/lib/ruby_ui/breadcrumb/breadcrumb_list.rb +17 -0
  21. data/lib/ruby_ui/breadcrumb/breadcrumb_page.rb +19 -0
  22. data/lib/ruby_ui/breadcrumb/breadcrumb_separator.rb +38 -0
  23. data/lib/ruby_ui/calendar/calendar_controller.js +249 -0
  24. data/lib/ruby_ui/calendar/calendar_input_controller.js +8 -0
  25. data/lib/ruby_ui/carousel/carousel.rb +44 -0
  26. data/lib/ruby_ui/carousel/carousel_content.rb +23 -0
  27. data/lib/ruby_ui/carousel/carousel_controller.js +60 -0
  28. data/lib/ruby_ui/carousel/carousel_item.rb +23 -0
  29. data/lib/ruby_ui/carousel/carousel_next.rb +48 -0
  30. data/lib/ruby_ui/carousel/carousel_previous.rb +49 -0
  31. data/lib/ruby_ui/chart/chart_controller.js +103 -0
  32. data/lib/ruby_ui/checkbox/checkbox_group_controller.js +21 -0
  33. data/lib/ruby_ui/clipboard/clipboard_controller.js +54 -0
  34. data/lib/ruby_ui/collapsible/collapsible_controller.js +47 -0
  35. data/lib/ruby_ui/combobox/combobox.rb +8 -6
  36. data/lib/ruby_ui/combobox/combobox_checkbox.rb +25 -0
  37. data/lib/ruby_ui/combobox/combobox_controller.js +176 -0
  38. data/lib/ruby_ui/combobox/{combobox_empty.rb → combobox_empty_state.rb} +2 -2
  39. data/lib/ruby_ui/combobox/combobox_item.rb +9 -37
  40. data/lib/ruby_ui/combobox/combobox_list.rb +2 -11
  41. data/lib/ruby_ui/combobox/combobox_list_group.rb +20 -0
  42. data/lib/ruby_ui/combobox/combobox_popover.rb +30 -0
  43. data/lib/ruby_ui/combobox/combobox_radio.rb +26 -0
  44. data/lib/ruby_ui/combobox/combobox_search_input.rb +21 -24
  45. data/lib/ruby_ui/combobox/combobox_toggle_all_checkbox.rb +25 -0
  46. data/lib/ruby_ui/combobox/combobox_trigger.rb +25 -20
  47. data/lib/ruby_ui/command/command_controller.js +136 -0
  48. data/lib/ruby_ui/context_menu/context_menu_controller.js +144 -0
  49. data/lib/ruby_ui/dialog/dialog_content.rb +2 -2
  50. data/lib/ruby_ui/dialog/dialog_controller.js +32 -0
  51. data/lib/ruby_ui/dialog/dialog_footer.rb +1 -1
  52. data/lib/ruby_ui/dialog/dialog_header.rb +1 -1
  53. data/lib/ruby_ui/dropdown_menu/dropdown_menu_controller.js +120 -0
  54. data/lib/ruby_ui/form/form_field_controller.js +61 -0
  55. data/lib/ruby_ui/hover_card/hover_card_controller.js +144 -0
  56. data/lib/ruby_ui/masked_input/masked_input_controller.js +9 -0
  57. data/lib/ruby_ui/popover/popover_controller.js +107 -0
  58. data/lib/ruby_ui/progress/progress.rb +37 -0
  59. data/lib/ruby_ui/radio_button/radio_button.rb +4 -1
  60. data/lib/ruby_ui/select/select_content.rb +1 -1
  61. data/lib/ruby_ui/select/select_controller.js +171 -0
  62. data/lib/ruby_ui/select/select_item_controller.js +11 -0
  63. data/lib/ruby_ui/select/select_value.rb +1 -1
  64. data/lib/ruby_ui/separator/separator.rb +38 -0
  65. data/lib/ruby_ui/sheet/sheet_content.rb +1 -1
  66. data/lib/ruby_ui/sheet/sheet_content_controller.js +7 -0
  67. data/lib/ruby_ui/sheet/sheet_controller.js +9 -0
  68. data/lib/ruby_ui/{combobox/combobox_separator.rb → skeleton/skeleton.rb} +4 -2
  69. data/lib/ruby_ui/switch/switch.rb +24 -0
  70. data/lib/ruby_ui/tabs/tabs_controller.js +45 -0
  71. data/lib/ruby_ui/theme_toggle/theme_toggle_controller.js +30 -0
  72. data/lib/ruby_ui/tooltip/tooltip_controller.js +37 -0
  73. data/lib/ruby_ui.rb +1 -1
  74. metadata +57 -11
  75. data/lib/ruby_ui/combobox/combobox_content.rb +0 -31
  76. data/lib/ruby_ui/combobox/combobox_group.rb +0 -38
  77. data/lib/ruby_ui/combobox/combobox_input.rb +0 -22
  78. data/lib/ruby_ui/combobox/combobox_value.rb +0 -27
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class CarouselPrevious < Base
5
+ def view_template(&)
6
+ Button(**attrs) do
7
+ icon
8
+ span(class: "sr-only") { "Next slide" }
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def default_attrs
15
+ {
16
+ variant: :outline,
17
+ icon: true,
18
+ class: [
19
+ "absolute h-8 w-8 rounded-full",
20
+ "group-[.is-horizontal]:-left-12 group-[.is-horizontal]:top-1/2 group-[.is-horizontal]:-translate-y-1/2",
21
+ "group-[.is-vertical]:-top-12 group-[.is-vertical]:left-1/2 group-[.is-vertical]:-translate-x-1/2 group-[.is-vertical]:rotate-90"
22
+ ],
23
+ disabled: true,
24
+ data: {
25
+ action: "click->ruby-ui--carousel#scrollPrev",
26
+ ruby_ui__carousel_target: "prevButton"
27
+ }
28
+ }
29
+ end
30
+
31
+ def icon
32
+ svg(
33
+ width: "24",
34
+ height: "24",
35
+ viewBox: "0 0 24 24",
36
+ fill: "none",
37
+ stroke: "currentColor",
38
+ stroke_width: "2",
39
+ stroke_linecap: "round",
40
+ stroke_linejoin: "round",
41
+ xmlns: "http://www.w3.org/2000/svg",
42
+ class: "w-4 h-4"
43
+ ) do |s|
44
+ s.path(d: "m12 19-7-7 7-7")
45
+ s.path(d: "M19 12H5")
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,103 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import Chart from 'chart.js/auto'
3
+
4
+ // Chart controller
5
+ export default class extends Controller {
6
+ static values = {
7
+ options: {
8
+ type: Object,
9
+ default: {},
10
+ }
11
+ }
12
+
13
+ // Function to initialize the chart when the controller is connected
14
+ connect() {
15
+ this.initDarkModeObserver()
16
+ this.initChart()
17
+ }
18
+
19
+ disconnect() {
20
+ this.darkModeObserver?.disconnect()
21
+ this.chart?.destroy()
22
+ }
23
+
24
+ // Function to initialize the chart
25
+ initChart() {
26
+ this.setColors()
27
+ const ctx = this.element.getContext('2d');
28
+ this.chart = new Chart(ctx, this.mergeOptionsWithDefaults());
29
+ }
30
+
31
+ setColors() {
32
+ this.setDefaultColorsForChart()
33
+ }
34
+
35
+ getThemeColor(name) {
36
+ const color = getComputedStyle(document.documentElement).getPropertyValue(`--${name}`)
37
+ const [hue, saturation, lightness] = color.split(' ')
38
+ return `hsl(${hue}, ${saturation}, ${lightness})`
39
+ }
40
+
41
+ defaultThemeColor() {
42
+ return {
43
+ backgroundColor: this.getThemeColor('background'),
44
+ hoverBackgroundColor: this.getThemeColor('accent'),
45
+ borderColor: this.getThemeColor('primary'),
46
+ borderWidth: 1,
47
+ }
48
+ }
49
+
50
+ // Function to set chart default colors
51
+ setDefaultColorsForChart() {
52
+ Chart.defaults.color = this.getThemeColor('muted-foreground') // font color
53
+ Chart.defaults.borderColor = this.getThemeColor('border') // border color
54
+ Chart.defaults.backgroundColor = this.getThemeColor('background') // background color
55
+
56
+ // tooltip colors
57
+ Chart.defaults.plugins.tooltip.backgroundColor = this.getThemeColor('background')
58
+ Chart.defaults.plugins.tooltip.borderColor = this.getThemeColor('border')
59
+ Chart.defaults.plugins.tooltip.titleColor = this.getThemeColor('foreground')
60
+ Chart.defaults.plugins.tooltip.bodyColor = this.getThemeColor('muted-foreground')
61
+ Chart.defaults.plugins.tooltip.borderWidth = 1
62
+
63
+ // legend
64
+ // options.plugins.legend.labels
65
+ Chart.defaults.plugins.legend.labels.boxWidth = 12
66
+ Chart.defaults.plugins.legend.labels.boxHeight = 12
67
+ Chart.defaults.plugins.legend.labels.borderWidth = 0
68
+ Chart.defaults.plugins.legend.labels.useBorderRadius = true
69
+ Chart.defaults.plugins.legend.labels.borderRadius = this.getThemeColor('radius')
70
+ }
71
+
72
+ // Function to refresh the chart
73
+ refreshChart() {
74
+ // Destroy the chart if it's a valid Chart.js instance
75
+ this.chart?.destroy()
76
+ // Reinitialize the chart
77
+ this.initChart()
78
+ }
79
+
80
+ // Function to initialize the dark mode observer
81
+ initDarkModeObserver() {
82
+ this.darkModeObserver = new MutationObserver(() => {
83
+ this.refreshChart()
84
+ })
85
+ this.darkModeObserver.observe(document.documentElement, { attributeFilter: ['class'] })
86
+ }
87
+
88
+ // Function to merge the options with the defaults
89
+ mergeOptionsWithDefaults() {
90
+ return {
91
+ ...this.optionsValue,
92
+ data: {
93
+ ...this.optionsValue.data,
94
+ datasets: this.optionsValue.data.datasets.map((dataset) => {
95
+ return {
96
+ ...this.defaultThemeColor(),
97
+ ...dataset,
98
+ }
99
+ })
100
+ }
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,21 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["checkbox"];
5
+
6
+ connect() {
7
+ this.#handleRequired();
8
+ }
9
+
10
+ onChange() {
11
+ this.#handleRequired();
12
+ }
13
+
14
+ #handleRequired() {
15
+ if (!this.element.hasAttribute("data-required")) return;
16
+
17
+ const checked = this.checkboxTargets.some(({ checked }) => checked);
18
+
19
+ this.checkboxTargets.forEach((checkbox) => (checkbox.required = !checked));
20
+ }
21
+ }
@@ -0,0 +1,54 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { computePosition, flip, shift } from "@floating-ui/dom";
3
+
4
+ // Connects to data-controller="accordion"
5
+ export default class extends Controller {
6
+ static targets = ['trigger', 'source', 'successPopover', 'errorPopover']
7
+ static values = {
8
+ options: {
9
+ type: Object,
10
+ default: {},
11
+ },
12
+ }
13
+
14
+ copy() {
15
+ let sourceElement = this.sourceTarget.children[0];
16
+ if (!sourceElement) {
17
+ this.showErrorPopover();
18
+ return;
19
+ }
20
+ let textToCopy = sourceElement.tagName === 'INPUT' ? sourceElement.value : sourceElement.innerText;
21
+ navigator.clipboard.writeText(textToCopy).then(() => {
22
+ this.#showSuccessPopover();
23
+ }).catch(() => {
24
+ this.#showErrorPopover();
25
+ })
26
+ }
27
+
28
+ onClickOutside() {
29
+ if (!this.successPopoverTarget.classList.contains("hidden")) this.successPopoverTarget.classList.add("hidden");
30
+ if (!this.errorPopoverTarget.classList.contains("hidden")) this.errorPopoverTarget.classList.add("hidden");
31
+ }
32
+
33
+ #computeTooltip(popoverElement) {
34
+ computePosition(this.triggerTarget, popoverElement, {
35
+ placement: this.optionsValue.placement || "top",
36
+ middleware: [flip(), shift()],
37
+ }).then(({ x, y }) => {
38
+ Object.assign(popoverElement.style, {
39
+ left: `${x}px`,
40
+ top: `${y}px`,
41
+ });
42
+ });
43
+ }
44
+
45
+ #showSuccessPopover() {
46
+ this.#computeTooltip(this.successPopoverTarget);
47
+ this.successPopoverTarget.classList.remove("hidden");
48
+ }
49
+
50
+ #showErrorPopover() {
51
+ this.#computeTooltip(this.errorPopoverTarget);
52
+ this.errorPopoverTarget.classList.remove("hidden");
53
+ }
54
+ }
@@ -0,0 +1,47 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="accordion"
4
+ export default class extends Controller {
5
+ static targets = ['content']
6
+ static values = {
7
+ open: {
8
+ type: Boolean,
9
+ default: false,
10
+ },
11
+ }
12
+
13
+ connect() {
14
+ // Set the initial state of the accordion
15
+ this.openValue ? this.open() : this.close()
16
+ }
17
+
18
+ // Toggle the 'open' value
19
+ toggle() {
20
+ this.openValue = !this.openValue
21
+ }
22
+
23
+ // Handle changes in the 'open' value
24
+ openValueChanged(isOpen, wasOpen) {
25
+ if (isOpen) {
26
+ this.open()
27
+ } else {
28
+ this.close()
29
+ }
30
+ }
31
+
32
+ // Open the accordion content
33
+ open() {
34
+ if (this.hasContentTarget) {
35
+ this.contentTarget.classList.remove('hidden')
36
+ this.openValue = true
37
+ }
38
+ }
39
+
40
+ // Close the accordion content
41
+ close() {
42
+ if (this.hasContentTarget) {
43
+ this.contentTarget.classList.add('hidden')
44
+ this.openValue = false
45
+ }
46
+ }
47
+ }
@@ -2,6 +2,11 @@
2
2
 
3
3
  module RubyUI
4
4
  class Combobox < Base
5
+ def initialize(term: "items", **)
6
+ @term = term
7
+ super(**)
8
+ end
9
+
5
10
  def view_template(&)
6
11
  div(**attrs, &)
7
12
  end
@@ -10,14 +15,11 @@ module RubyUI
10
15
 
11
16
  def default_attrs
12
17
  {
18
+ role: "combobox",
13
19
  data: {
14
20
  controller: "ruby-ui--combobox",
15
- ruby_ui__combobox_open_value: "false",
16
- action: "click@window->ruby-ui--combobox#onClickOutside",
17
- ruby_ui__combobox_ruby_ui__combobox_content_outlet: ".combobox-content",
18
- ruby_ui__combobox_ruby_ui__combobox_item_outlet: ".combobox-item"
19
- },
20
- class: "group/combobox w-full relative"
21
+ ruby_ui__combobox_term_value: @term.to_s
22
+ }
21
23
  }
22
24
  end
23
25
  end
@@ -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
3
  module RubyUI
4
- class ComboboxEmpty < Base
4
+ class ComboboxEmptyState < Base
5
5
  def view_template(&)
6
6
  div(**attrs, &)
7
7
  end
@@ -13,7 +13,7 @@ module RubyUI
13
13
  role: "presentation",
14
14
  class: "hidden py-6 text-center text-sm",
15
15
  data: {
16
- ruby_ui__combobox_content_target: "empty"
16
+ ruby_ui__combobox_target: "emptyState"
17
17
  }
18
18
  }
19
19
  end
@@ -2,51 +2,23 @@
2
2
 
3
3
  module RubyUI
4
4
  class ComboboxItem < Base
5
- def initialize(value: nil, **attrs)
6
- @value = value
7
- super(**attrs)
8
- end
9
-
10
- def view_template(&block)
11
- div(**attrs) do
12
- div(class: "invisible group-aria-selected:visible") { icon }
13
- block.call
14
- end
5
+ def view_template(&)
6
+ label(**attrs, &)
15
7
  end
16
8
 
17
9
  private
18
10
 
19
- def icon
20
- svg(
21
- xmlns: "http://www.w3.org/2000/svg",
22
- viewbox: "0 0 24 24",
23
- fill: "none",
24
- stroke: "currentColor",
25
- class: "mr-2 h-4 w-4",
26
- stroke_width: "2",
27
- stroke_linecap: "round",
28
- stroke_linejoin: "round"
29
- ) do |s|
30
- s.path(
31
- d: "M20 6 9 17l-5-5"
32
- )
33
- end
34
- end
35
-
36
11
  def default_attrs
37
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
+ ],
38
18
  role: "option",
39
- tabindex: "0",
40
- class:
41
- "combobox-item group relative flex cursor-pointer select-none items-center gap-x-2 rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground aria-[current]:bg-accent aria-[current]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
42
19
  data: {
43
- value: @value,
44
- ruby_ui__combobox_target: "item",
45
- ruby_ui__combobox_content_target: "item",
46
- controller: "ruby-ui--combobox-item",
47
- action: "click->ruby-ui--combobox#onItemSelected"
48
- },
49
- aria_selected: "false"
20
+ ruby_ui__combobox_target: "item"
21
+ }
50
22
  }
51
23
  end
52
24
  end
@@ -2,11 +2,6 @@
2
2
 
3
3
  module RubyUI
4
4
  class ComboboxList < Base
5
- def initialize(**attrs)
6
- @id = "list#{SecureRandom.hex(4)}"
7
- super
8
- end
9
-
10
5
  def view_template(&)
11
6
  div(**attrs, &)
12
7
  end
@@ -15,12 +10,8 @@ module RubyUI
15
10
 
16
11
  def default_attrs
17
12
  {
18
- id: @id,
19
- data: {
20
- ruby_ui__combobox_target: "list"
21
- },
22
- role: "listbox",
23
- tabindex: "-1"
13
+ class: "flex flex-col gap-1 p-1 max-h-72 overflow-y-auto text-foreground",
14
+ role: "listbox"
24
15
  }
25
16
  end
26
17
  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