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.
- checksums.yaml +4 -4
- data/LICENSE.txt +21 -0
- data/README.md +85 -0
- data/lib/generators/ruby_ui/component_generator.rb +4 -40
- data/lib/generators/ruby_ui/dependencies.yml +74 -0
- data/lib/generators/ruby_ui/install/install_generator.rb +21 -22
- data/lib/generators/ruby_ui/install/templates/ruby_ui.rb.erb +18 -0
- data/lib/generators/ruby_ui/install/templates/tailwind.css.erb +156 -0
- data/lib/generators/ruby_ui/javascript_utils.rb +21 -0
- data/lib/ruby_ui/accordion/accordion_controller.js +97 -0
- data/lib/ruby_ui/alert/alert.rb +1 -1
- data/lib/ruby_ui/alert_dialog/alert_dialog_content.rb +1 -1
- data/lib/ruby_ui/alert_dialog/alert_dialog_controller.js +31 -0
- data/lib/ruby_ui/alert_dialog/alert_dialog_footer.rb +1 -1
- data/lib/ruby_ui/alert_dialog/alert_dialog_header.rb +1 -1
- data/lib/ruby_ui/breadcrumb/breadcrumb.rb +17 -0
- data/lib/ruby_ui/breadcrumb/breadcrumb_ellipsis.rb +39 -0
- data/lib/ruby_ui/breadcrumb/breadcrumb_item.rb +17 -0
- data/lib/ruby_ui/breadcrumb/breadcrumb_link.rb +22 -0
- data/lib/ruby_ui/breadcrumb/breadcrumb_list.rb +17 -0
- data/lib/ruby_ui/breadcrumb/breadcrumb_page.rb +19 -0
- data/lib/ruby_ui/breadcrumb/breadcrumb_separator.rb +38 -0
- data/lib/ruby_ui/calendar/calendar_controller.js +249 -0
- data/lib/ruby_ui/calendar/calendar_input_controller.js +8 -0
- data/lib/ruby_ui/carousel/carousel.rb +44 -0
- data/lib/ruby_ui/carousel/carousel_content.rb +23 -0
- data/lib/ruby_ui/carousel/carousel_controller.js +60 -0
- data/lib/ruby_ui/carousel/carousel_item.rb +23 -0
- data/lib/ruby_ui/carousel/carousel_next.rb +48 -0
- data/lib/ruby_ui/carousel/carousel_previous.rb +49 -0
- data/lib/ruby_ui/chart/chart_controller.js +103 -0
- data/lib/ruby_ui/checkbox/checkbox_group_controller.js +21 -0
- data/lib/ruby_ui/clipboard/clipboard_controller.js +54 -0
- data/lib/ruby_ui/collapsible/collapsible_controller.js +47 -0
- data/lib/ruby_ui/combobox/combobox.rb +8 -6
- data/lib/ruby_ui/combobox/combobox_checkbox.rb +25 -0
- data/lib/ruby_ui/combobox/combobox_controller.js +176 -0
- data/lib/ruby_ui/combobox/{combobox_empty.rb → combobox_empty_state.rb} +2 -2
- data/lib/ruby_ui/combobox/combobox_item.rb +9 -37
- data/lib/ruby_ui/combobox/combobox_list.rb +2 -11
- data/lib/ruby_ui/combobox/combobox_list_group.rb +20 -0
- data/lib/ruby_ui/combobox/combobox_popover.rb +30 -0
- data/lib/ruby_ui/combobox/combobox_radio.rb +26 -0
- data/lib/ruby_ui/combobox/combobox_search_input.rb +21 -24
- data/lib/ruby_ui/combobox/combobox_toggle_all_checkbox.rb +25 -0
- data/lib/ruby_ui/combobox/combobox_trigger.rb +25 -20
- data/lib/ruby_ui/command/command_controller.js +136 -0
- data/lib/ruby_ui/context_menu/context_menu_controller.js +144 -0
- data/lib/ruby_ui/dialog/dialog_content.rb +2 -2
- data/lib/ruby_ui/dialog/dialog_controller.js +32 -0
- data/lib/ruby_ui/dialog/dialog_footer.rb +1 -1
- data/lib/ruby_ui/dialog/dialog_header.rb +1 -1
- data/lib/ruby_ui/dropdown_menu/dropdown_menu_controller.js +120 -0
- data/lib/ruby_ui/form/form_field_controller.js +61 -0
- data/lib/ruby_ui/hover_card/hover_card_controller.js +144 -0
- data/lib/ruby_ui/masked_input/masked_input_controller.js +9 -0
- data/lib/ruby_ui/popover/popover_controller.js +107 -0
- data/lib/ruby_ui/progress/progress.rb +37 -0
- data/lib/ruby_ui/radio_button/radio_button.rb +4 -1
- data/lib/ruby_ui/select/select_content.rb +1 -1
- data/lib/ruby_ui/select/select_controller.js +171 -0
- data/lib/ruby_ui/select/select_item_controller.js +11 -0
- data/lib/ruby_ui/select/select_value.rb +1 -1
- data/lib/ruby_ui/separator/separator.rb +38 -0
- data/lib/ruby_ui/sheet/sheet_content.rb +1 -1
- data/lib/ruby_ui/sheet/sheet_content_controller.js +7 -0
- data/lib/ruby_ui/sheet/sheet_controller.js +9 -0
- data/lib/ruby_ui/{combobox/combobox_separator.rb → skeleton/skeleton.rb} +4 -2
- data/lib/ruby_ui/switch/switch.rb +24 -0
- data/lib/ruby_ui/tabs/tabs_controller.js +45 -0
- data/lib/ruby_ui/theme_toggle/theme_toggle_controller.js +30 -0
- data/lib/ruby_ui/tooltip/tooltip_controller.js +37 -0
- data/lib/ruby_ui.rb +1 -1
- metadata +57 -11
- data/lib/ruby_ui/combobox/combobox_content.rb +0 -31
- data/lib/ruby_ui/combobox/combobox_group.rb +0 -38
- data/lib/ruby_ui/combobox/combobox_input.rb +0 -22
- 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,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:
|
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
|
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
|
+
}
|
@@ -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
|
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(
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module RubyUI
|
4
|
-
class
|
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
|
-
{
|
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
|
+
}
|