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,144 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ import tippy from "tippy.js";
3
+
4
+ export default class extends Controller {
5
+ static targets = ["trigger", "content", "menuItem"];
6
+ static values = {
7
+ options: {
8
+ type: Object,
9
+ default: {},
10
+ },
11
+ // make content width of the trigger element (true/false)
12
+ matchWidth: {
13
+ type: Boolean,
14
+ default: false,
15
+ }
16
+ }
17
+
18
+ connect() {
19
+ this.boundHandleKeydown = this.handleKeydown.bind(this); // Bind the function so we can remove it later
20
+ this.initializeTippy();
21
+ this.selectedIndex = -1;
22
+ }
23
+
24
+ disconnect() {
25
+ this.destroyTippy();
26
+ }
27
+
28
+ initializeTippy() {
29
+ const defaultOptions = {
30
+ content: this.contentTarget.innerHTML,
31
+ allowHTML: true,
32
+ interactive: true,
33
+ onShow: (instance) => {
34
+ this.matchWidthValue && this.setContentWidth(instance); // ensure content width matches trigger width
35
+ this.addEventListeners();
36
+ },
37
+ onHide: () => {
38
+ this.removeEventListeners();
39
+ this.deselectAll();
40
+ },
41
+ popperOptions: {
42
+ modifiers: [
43
+ {
44
+ name: "offset",
45
+ options: {
46
+ offset: [0, 4]
47
+ },
48
+ },
49
+ ],
50
+ }
51
+ };
52
+
53
+ const mergedOptions = { ...this.optionsValue, ...defaultOptions };
54
+ this.tippy = tippy(this.triggerTarget, mergedOptions);
55
+ }
56
+
57
+ destroyTippy() {
58
+ if (this.tippy) {
59
+ this.tippy.destroy();
60
+ }
61
+ }
62
+
63
+ setContentWidth(instance) {
64
+ // box-sizing: border-box
65
+ const content = instance.popper.querySelector('.tippy-content');
66
+ if (content) {
67
+ content.style.width = `${instance.reference.offsetWidth}px`;
68
+ }
69
+ }
70
+
71
+ handleContextMenu(event) {
72
+ event.preventDefault();
73
+ this.open();
74
+ }
75
+
76
+ open() {
77
+ this.tippy.show();
78
+ }
79
+
80
+ close() {
81
+ this.tippy.hide();
82
+ }
83
+
84
+ handleKeydown(e) {
85
+ // return if no menu items (one line fix for)
86
+ if (this.menuItemTargets.length === 0) { return; }
87
+
88
+ if (e.key === 'ArrowDown') {
89
+ e.preventDefault();
90
+ this.updateSelectedItem(1);
91
+ } else if (e.key === 'ArrowUp') {
92
+ e.preventDefault();
93
+ this.updateSelectedItem(-1);
94
+ } else if (e.key === 'Enter' && this.selectedIndex !== -1) {
95
+ e.preventDefault();
96
+ this.menuItemTargets[this.selectedIndex].click();
97
+ }
98
+ }
99
+
100
+ updateSelectedItem(direction) {
101
+ // Check if any of the menuItemTargets have aria-selected="true" and set the selectedIndex to that index
102
+ this.menuItemTargets.forEach((item, index) => {
103
+ if (item.getAttribute('aria-selected') === 'true') {
104
+ this.selectedIndex = index;
105
+ }
106
+ });
107
+
108
+ if (this.selectedIndex >= 0) {
109
+ this.toggleAriaSelected(this.menuItemTargets[this.selectedIndex], false);
110
+ }
111
+
112
+ this.selectedIndex += direction;
113
+
114
+ if (this.selectedIndex < 0) {
115
+ this.selectedIndex = this.menuItemTargets.length - 1;
116
+ } else if (this.selectedIndex >= this.menuItemTargets.length) {
117
+ this.selectedIndex = 0;
118
+ }
119
+
120
+ this.toggleAriaSelected(this.menuItemTargets[this.selectedIndex], true);
121
+ }
122
+
123
+ toggleAriaSelected(element, isSelected) {
124
+ // Add or remove attribute
125
+ if (isSelected) {
126
+ element.setAttribute('aria-selected', 'true');
127
+ } else {
128
+ element.removeAttribute('aria-selected');
129
+ }
130
+ }
131
+
132
+ deselectAll() {
133
+ this.menuItemTargets.forEach(item => this.toggleAriaSelected(item, false));
134
+ this.selectedIndex = -1;
135
+ }
136
+
137
+ addEventListeners() {
138
+ document.addEventListener('keydown', this.boundHandleKeydown);
139
+ }
140
+
141
+ removeEventListeners() {
142
+ document.removeEventListener('keydown', this.boundHandleKeydown);
143
+ }
144
+ }
@@ -0,0 +1,9 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ import { MaskInput } from "maska";
3
+
4
+ // Connects to data-controller="ruby-ui--masked-input"
5
+ export default class extends Controller {
6
+ connect() {
7
+ new MaskInput(this.element)
8
+ }
9
+ }
@@ -0,0 +1,107 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ import {
3
+ computePosition,
4
+ flip,
5
+ shift,
6
+ offset,
7
+ autoUpdate,
8
+ } from "@floating-ui/dom";
9
+
10
+ export default class extends Controller {
11
+ static targets = ["trigger", "content"];
12
+ static values = {
13
+ open: { type: Boolean, default: false },
14
+ options: { type: Object, default: {} },
15
+ trigger: { type: String, default: "hover" },
16
+ };
17
+
18
+ connect() {
19
+ this.closeTimeout = null;
20
+ this.cleanup = null;
21
+ this.addEventListeners();
22
+ }
23
+
24
+ disconnect() {
25
+ this.removeEventListeners();
26
+ if (this.cleanup) {
27
+ this.cleanup();
28
+ }
29
+ }
30
+
31
+ addEventListeners() {
32
+ if (this.triggerValue === "hover") {
33
+ this.triggerTarget.addEventListener("mouseenter", this.handleMouseEnter);
34
+ this.triggerTarget.addEventListener("mouseleave", this.handleMouseLeave);
35
+ this.contentTarget.addEventListener("mouseenter", this.handleMouseEnter);
36
+ this.contentTarget.addEventListener("mouseleave", this.handleMouseLeave);
37
+ } else if (this.triggerValue === "click") {
38
+ this.triggerTarget.addEventListener("click", this.handleClick);
39
+ document.addEventListener("click", this.handleOutsideClick);
40
+ }
41
+ }
42
+
43
+ removeEventListeners() {
44
+ this.triggerTarget.removeEventListener("mouseenter", this.handleMouseEnter);
45
+ this.triggerTarget.removeEventListener("mouseleave", this.handleMouseLeave);
46
+ this.contentTarget.removeEventListener("mouseenter", this.handleMouseEnter);
47
+ this.contentTarget.removeEventListener("mouseleave", this.handleMouseLeave);
48
+ this.triggerTarget.removeEventListener("click", this.handleClick);
49
+ document.removeEventListener("click", this.handleOutsideClick);
50
+ }
51
+
52
+ handleMouseEnter = () => {
53
+ clearTimeout(this.closeTimeout);
54
+ this.openValue = true;
55
+ this.showPopover();
56
+ };
57
+
58
+ handleMouseLeave = () => {
59
+ this.closeTimeout = setTimeout(() => {
60
+ this.openValue = false;
61
+ this.hidePopover();
62
+ }, 100);
63
+ };
64
+
65
+ handleClick = (event) => {
66
+ event.stopPropagation();
67
+ this.openValue = !this.openValue;
68
+ this.openValue ? this.showPopover() : this.hidePopover();
69
+ };
70
+
71
+ handleOutsideClick = (event) => {
72
+ if (!this.element.contains(event.target) && this.openValue) {
73
+ this.openValue = false;
74
+ this.hidePopover();
75
+ }
76
+ };
77
+
78
+ showPopover() {
79
+ this.contentTarget.classList.remove("hidden");
80
+ this.updatePosition();
81
+ }
82
+
83
+ hidePopover() {
84
+ this.contentTarget.classList.add("hidden");
85
+ if (this.cleanup) {
86
+ this.cleanup();
87
+ }
88
+ }
89
+
90
+ updatePosition() {
91
+ if (this.cleanup) {
92
+ this.cleanup();
93
+ }
94
+
95
+ this.cleanup = autoUpdate(this.triggerTarget, this.contentTarget, () => {
96
+ computePosition(this.triggerTarget, this.contentTarget, {
97
+ placement: this.optionsValue.placement || "bottom",
98
+ middleware: [flip(), shift(), offset(8)],
99
+ }).then(({ x, y }) => {
100
+ Object.assign(this.contentTarget.style, {
101
+ left: `${x}px`,
102
+ top: `${y}px`,
103
+ });
104
+ });
105
+ });
106
+ }
107
+ }
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class Progress < Base
5
+ def initialize(value: 0, **attrs)
6
+ @value = value.to_f.clamp(0, 100)
7
+
8
+ super(**attrs)
9
+ end
10
+
11
+ def view_template
12
+ div(**attrs) do
13
+ div(**indicator_attrs)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def default_attrs
20
+ {
21
+ role: "progressbar",
22
+ aria_valuenow: @value,
23
+ aria_valuemin: 0,
24
+ aria_valuemax: 100,
25
+ aria_valuetext: "#{@value}%",
26
+ class: "relative h-2 overflow-hidden rounded-full bg-primary/20 [&>*]:bg-primary"
27
+ }
28
+ end
29
+
30
+ def indicator_attrs
31
+ {
32
+ class: "h-full w-full flex-1",
33
+ style: "transform: translateX(-#{100 - @value}%)"
34
+ }
35
+ end
36
+ end
37
+ end
@@ -15,7 +15,10 @@ module RubyUI
15
15
  ruby_ui__form_field_target: "input",
16
16
  action: "change->ruby-ui--form-field#onInput invalid->ruby-ui--form-field#onInvalid"
17
17
  },
18
- class: "h-4 w-4 p-0 border-primary rounded-full flex-none"
18
+ class: [
19
+ "h-4 w-4 p-0 border-primary rounded-full flex-none",
20
+ "disabled:cursor-not-allowed disabled:opacity-50"
21
+ ]
19
22
  }
20
23
  end
21
24
  end
@@ -10,7 +10,7 @@ module RubyUI
10
10
  def view_template(&block)
11
11
  div(**attrs) do
12
12
  div(
13
- class: "max-h-96 min-w-max overflow-auto rounded-md border bg-background p-1 text-foreground shadow-md animate-out group-data-[ruby-ui--select-open-value=true]/select:animate-in fade-out-0 group-data-[ruby-ui--select-open-value=true]/select:fade-in-0 zoom-out-95 group-data-[ruby-ui--select-open-value=true]/select:zoom-in-95 slide-in-from-top-2", &block
13
+ class: "max-h-96 w-full text-wrap overflow-auto rounded-md border bg-background p-1 text-foreground shadow-md animate-out group-data-[ruby-ui--select-open-value=true]/select:animate-in fade-out-0 group-data-[ruby-ui--select-open-value=true]/select:fade-in-0 zoom-out-95 group-data-[ruby-ui--select-open-value=true]/select:zoom-in-95 slide-in-from-top-2", &block
14
14
  )
15
15
  end
16
16
  end
@@ -0,0 +1,171 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ import { computePosition, autoUpdate, offset, flip } from "@floating-ui/dom";
3
+
4
+ export default class extends Controller {
5
+ static targets = ["trigger", "content", "input", "value", "item"];
6
+ static values = { open: Boolean };
7
+ static outlets = ["ruby-ui--select-item"];
8
+
9
+ constructor(...args) {
10
+ super(...args);
11
+ this.cleanup;
12
+ }
13
+
14
+ connect() {
15
+ this.setFloatingElement();
16
+ this.generateItemsIds();
17
+ }
18
+
19
+ disconnect() {
20
+ this.cleanup();
21
+ }
22
+
23
+ selectItem(event) {
24
+ event.preventDefault();
25
+
26
+ this.rubyUiSelectItemOutlets.forEach((item) =>
27
+ item.handleSelectItem(event),
28
+ );
29
+
30
+ const oldValue = this.inputTarget.value;
31
+ const newValue = event.target.dataset.value;
32
+
33
+ this.inputTarget.value = newValue;
34
+ this.valueTarget.innerText = event.target.innerText;
35
+
36
+ this.dispatchOnChange(oldValue, newValue);
37
+ this.closeContent();
38
+ }
39
+
40
+ onClick() {
41
+ this.toogleContent();
42
+
43
+ if (this.openValue) {
44
+ this.setFocusAndCurrent();
45
+ } else {
46
+ this.resetCurrent();
47
+ }
48
+ }
49
+
50
+ handleKeyDown(event) {
51
+ event.preventDefault();
52
+
53
+ const currentIndex = this.itemTargets.findIndex(
54
+ (item) => item.getAttribute("aria-current") === "true",
55
+ );
56
+
57
+ if (currentIndex + 1 < this.itemTargets.length) {
58
+ this.itemTargets[currentIndex].removeAttribute("aria-current");
59
+ this.setAriaCurrentAndActiveDescendant(currentIndex + 1);
60
+ }
61
+ }
62
+
63
+ handleKeyUp(event) {
64
+ event.preventDefault();
65
+
66
+ const currentIndex = this.itemTargets.findIndex(
67
+ (item) => item.getAttribute("aria-current") === "true",
68
+ );
69
+
70
+ if (currentIndex > 0) {
71
+ this.itemTargets[currentIndex].removeAttribute("aria-current");
72
+ this.setAriaCurrentAndActiveDescendant(currentIndex - 1);
73
+ }
74
+ }
75
+
76
+ handleEsc(event) {
77
+ event.preventDefault();
78
+ this.closeContent();
79
+ }
80
+
81
+ setFocusAndCurrent() {
82
+ const selectedItem = this.itemTargets.find(
83
+ (item) => item.getAttribute("aria-selected") === "true",
84
+ );
85
+
86
+ if (selectedItem) {
87
+ selectedItem.focus({ preventScroll: true });
88
+ selectedItem.setAttribute("aria-current", "true");
89
+ this.triggerTarget.setAttribute(
90
+ "aria-activedescendant",
91
+ selectedItem.getAttribute("id"),
92
+ );
93
+ } else {
94
+ this.itemTarget.focus({ preventScroll: true });
95
+ this.itemTarget.setAttribute("aria-current", "true");
96
+ this.triggerTarget.setAttribute(
97
+ "aria-activedescendant",
98
+ this.itemTarget.getAttribute("id"),
99
+ );
100
+ }
101
+ }
102
+
103
+ resetCurrent() {
104
+ this.itemTargets.forEach((item) => item.removeAttribute("aria-current"));
105
+ }
106
+
107
+ clickOutside(event) {
108
+ if (!this.openValue) return;
109
+ if (this.element.contains(event.target)) return;
110
+
111
+ event.preventDefault();
112
+ this.toogleContent();
113
+ }
114
+
115
+ toogleContent() {
116
+ this.openValue = !this.openValue;
117
+ this.contentTarget.classList.toggle("hidden");
118
+ this.triggerTarget.setAttribute("aria-expanded", this.openValue);
119
+ }
120
+
121
+ setFloatingElement() {
122
+ this.cleanup = autoUpdate(this.triggerTarget, this.contentTarget, () => {
123
+ computePosition(this.triggerTarget, this.contentTarget, {
124
+ middleware: [offset(4), flip()],
125
+ }).then(({ x, y }) => {
126
+ Object.assign(this.contentTarget.style, {
127
+ left: `${x}px`,
128
+ top: `${y}px`,
129
+ });
130
+ });
131
+ });
132
+ }
133
+
134
+ generateItemsIds() {
135
+ const contentId = this.contentTarget.getAttribute("id");
136
+ this.triggerTarget.setAttribute("aria-controls", contentId);
137
+
138
+ this.itemTargets.forEach((item, index) => {
139
+ item.id = `${contentId}-${index}`;
140
+ });
141
+ }
142
+
143
+ setAriaCurrentAndActiveDescendant(currentIndex) {
144
+ const currentItem = this.itemTargets[currentIndex];
145
+ currentItem.focus({ preventScroll: true });
146
+ currentItem.setAttribute("aria-current", "true");
147
+ this.triggerTarget.setAttribute(
148
+ "aria-activedescendant",
149
+ currentItem.getAttribute("id"),
150
+ );
151
+ }
152
+
153
+ closeContent() {
154
+ this.toogleContent();
155
+ this.resetCurrent();
156
+
157
+ this.triggerTarget.setAttribute("aria-activedescendant", true);
158
+ this.triggerTarget.focus({ preventScroll: true });
159
+ }
160
+
161
+ dispatchOnChange(oldValue, newValue) {
162
+ if (oldValue === newValue) return;
163
+
164
+ const event = new InputEvent("change", {
165
+ bubbles: true,
166
+ cancelable: true,
167
+ });
168
+
169
+ this.inputTarget.dispatchEvent(event);
170
+ }
171
+ }
@@ -0,0 +1,11 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ export default class extends Controller {
3
+
4
+ handleSelectItem({ target }) {
5
+ if (this.element.dataset.value == target.dataset.value) {
6
+ this.element.setAttribute("aria-selected", true);
7
+ } else {
8
+ this.element.removeAttribute("aria-selected");
9
+ }
10
+ }
11
+ }
@@ -20,7 +20,7 @@ module RubyUI
20
20
  data: {
21
21
  ruby_ui__select_target: "value"
22
22
  },
23
- class: "pointer-events-none"
23
+ class: "truncate pointer-events-none"
24
24
  }
25
25
  end
26
26
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class Separator < Base
5
+ ORIENTATIONS = %i[horizontal vertical].freeze
6
+
7
+ def initialize(as: :div, orientation: :horizontal, decorative: true, **attrs)
8
+ raise ArgumentError, "Invalid orientation: #{orientation}" unless ORIENTATIONS.include?(orientation.to_sym)
9
+
10
+ @as = as.to_sym
11
+ @orientation = orientation.to_sym
12
+ @decorative = decorative
13
+ super(**attrs)
14
+ end
15
+
16
+ def view_template(&)
17
+ tag(@as, **attrs, &)
18
+ end
19
+
20
+ private
21
+
22
+ def default_attrs
23
+ {
24
+ role: (@decorative ? "none" : "separator"),
25
+ class: [
26
+ "shrink-0 bg-border",
27
+ orientation_classes
28
+ ]
29
+ }
30
+ end
31
+
32
+ def orientation_classes
33
+ return "h-[1px] w-full" if @orientation == :horizontal
34
+
35
+ "h-full w-[1px]"
36
+ end
37
+ end
38
+ end
@@ -42,7 +42,7 @@ module RubyUI
42
42
  def close_button
43
43
  button(
44
44
  type: "button",
45
- class: "absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",
45
+ class: "absolute end-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",
46
46
  data_action: "click->ruby-ui--sheet-content#close"
47
47
  ) do
48
48
  svg(
@@ -0,0 +1,7 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ close() {
5
+ this.element.remove()
6
+ }
7
+ }
@@ -0,0 +1,9 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["content"]
5
+
6
+ open() {
7
+ document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML)
8
+ }
9
+ }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyUI
4
- class ComboboxSeparator < Base
4
+ class Skeleton < Base
5
5
  def view_template(&)
6
6
  div(**attrs, &)
7
7
  end
@@ -9,7 +9,9 @@ module RubyUI
9
9
  private
10
10
 
11
11
  def default_attrs
12
- {class: "-mx-1 h-px bg-border"}
12
+ {
13
+ class: "animate-pulse rounded-md bg-primary/10"
14
+ }
13
15
  end
14
16
  end
15
17
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class Switch < Base
5
+ def initialize(include_hidden: true, checked_value: "1", unchecked_value: "0", **attrs)
6
+ @include_hidden = include_hidden
7
+ @checked_value = checked_value
8
+ @unchecked_value = unchecked_value
9
+ super(**attrs)
10
+ end
11
+
12
+ def view_template
13
+ label(
14
+ role: "switch",
15
+ class: "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50 bg-input has-[:checked]:bg-primary"
16
+ ) do
17
+ input(type: "hidden", name: attrs[:name], value: @unchecked_value) if @include_hidden
18
+ input(**attrs.merge(type: "checkbox", class: "hidden peer", value: @checked_value))
19
+
20
+ span(class: "pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform translate-x-0 peer-checked:translate-x-5 ")
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,45 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ // Connects to data-controller="ruby-ui--tabs"
4
+ export default class extends Controller {
5
+ static targets = ["trigger", "content"];
6
+ static values = { active: String };
7
+
8
+ connect() {
9
+ if (!this.hasActiveValue && this.triggerTargets.length > 0) {
10
+ this.activeValue = this.triggerTargets[0].dataset.value;
11
+ }
12
+ }
13
+
14
+ show(e) {
15
+ this.activeValue = e.currentTarget.dataset.value;
16
+ }
17
+
18
+ activeValueChanged(currentValue, previousValue) {
19
+ if (currentValue == "" || currentValue == previousValue) return;
20
+
21
+ this.contentTargets.forEach((el) => {
22
+ el.classList.add("hidden");
23
+ });
24
+
25
+ this.triggerTargets.forEach((el) => {
26
+ el.dataset.state = "inactive";
27
+ });
28
+
29
+ this.activeContentTarget() &&
30
+ this.activeContentTarget().classList.remove("hidden");
31
+ this.activeTriggerTarget().dataset.state = "active";
32
+ }
33
+
34
+ activeTriggerTarget() {
35
+ return this.triggerTargets.find(
36
+ (el) => el.dataset.value == this.activeValue,
37
+ );
38
+ }
39
+
40
+ activeContentTarget() {
41
+ return this.contentTargets.find(
42
+ (el) => el.dataset.value == this.activeValue,
43
+ );
44
+ }
45
+ }