ruby_ui 1.3.0 → 1.6.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.
- checksums.yaml +4 -4
- data/README.md +6 -0
- data/lib/generators/ruby_ui/component/all_generator.rb +6 -4
- data/lib/generators/ruby_ui/component_generator.rb +51 -31
- data/lib/generators/ruby_ui/dependencies.yml +12 -2
- data/lib/generators/ruby_ui/javascript_utils.rb +0 -13
- data/lib/ruby_ui/accordion/accordion_content.rb +4 -2
- data/lib/ruby_ui/accordion/accordion_controller.js +28 -6
- data/lib/ruby_ui/avatar/avatar_image.rb +5 -1
- data/lib/ruby_ui/bubble/bubble.rb +37 -0
- data/lib/ruby_ui/bubble/bubble_content.rb +23 -0
- data/lib/ruby_ui/bubble/bubble_docs.rb +167 -0
- data/lib/ruby_ui/bubble/bubble_group.rb +18 -0
- data/lib/ruby_ui/bubble/bubble_reactions.rb +38 -0
- data/lib/ruby_ui/combobox/combobox.rb +3 -1
- data/lib/ruby_ui/combobox/combobox_checkbox.rb +8 -1
- data/lib/ruby_ui/combobox/combobox_controller.js +3 -2
- data/lib/ruby_ui/combobox/combobox_trigger.rb +1 -4
- data/lib/ruby_ui/context_menu/context_menu_content.rb +5 -6
- data/lib/ruby_ui/context_menu/context_menu_controller.js +87 -75
- data/lib/ruby_ui/data_table/data_table_column_toggle.rb +8 -4
- data/lib/ruby_ui/data_table/data_table_column_visibility_controller.js +12 -2
- data/lib/ruby_ui/data_table/data_table_docs.rb +1 -1
- data/lib/ruby_ui/dialog/dialog_content.rb +7 -19
- data/lib/ruby_ui/dialog/dialog_controller.js +22 -10
- data/lib/ruby_ui/dropdown_menu/dropdown_menu.rb +0 -1
- data/lib/ruby_ui/dropdown_menu/dropdown_menu_controller.js +5 -1
- data/lib/ruby_ui/dropdown_menu/dropdown_menu_docs.rb +1 -1
- data/lib/ruby_ui/dropdown_menu/dropdown_menu_item.rb +10 -4
- data/lib/ruby_ui/empty/empty.rb +18 -0
- data/lib/ruby_ui/empty/empty_content.rb +18 -0
- data/lib/ruby_ui/empty/empty_description.rb +18 -0
- data/lib/ruby_ui/empty/empty_docs.rb +69 -0
- data/lib/ruby_ui/empty/empty_header.rb +18 -0
- data/lib/ruby_ui/empty/empty_media.rb +31 -0
- data/lib/ruby_ui/empty/empty_title.rb +18 -0
- data/lib/ruby_ui/form/form_docs.rb +89 -0
- data/lib/ruby_ui/hover_card/hover_card_content.rb +5 -6
- data/lib/ruby_ui/hover_card/hover_card_controller.js +113 -76
- data/lib/ruby_ui/input_otp/input_otp.rb +42 -0
- data/lib/ruby_ui/input_otp/input_otp_controller.js +128 -0
- data/lib/ruby_ui/input_otp/input_otp_docs.rb +213 -0
- data/lib/ruby_ui/input_otp/input_otp_group.rb +15 -0
- data/lib/ruby_ui/input_otp/input_otp_separator.rb +39 -0
- data/lib/ruby_ui/input_otp/input_otp_slot.rb +33 -0
- data/lib/ruby_ui/message/message.rb +23 -0
- data/lib/ruby_ui/message/message_avatar.rb +18 -0
- data/lib/ruby_ui/message/message_content.rb +18 -0
- data/lib/ruby_ui/message/message_docs.rb +173 -0
- data/lib/ruby_ui/message/message_footer.rb +18 -0
- data/lib/ruby_ui/message/message_group.rb +18 -0
- data/lib/ruby_ui/message/message_header.rb +18 -0
- data/lib/ruby_ui/message_scroller/message_scroller.rb +18 -0
- data/lib/ruby_ui/message_scroller/message_scroller_button.rb +56 -0
- data/lib/ruby_ui/message_scroller/message_scroller_content.rb +23 -0
- data/lib/ruby_ui/message_scroller/message_scroller_controller.js +335 -0
- data/lib/ruby_ui/message_scroller/message_scroller_docs.rb +208 -0
- data/lib/ruby_ui/message_scroller/message_scroller_item.rb +28 -0
- data/lib/ruby_ui/message_scroller/message_scroller_provider.rb +34 -0
- data/lib/ruby_ui/message_scroller/message_scroller_viewport.rb +22 -0
- data/lib/ruby_ui/native_select/native_select_icon.rb +1 -1
- data/lib/ruby_ui/popover/popover_content.rb +2 -1
- data/lib/ruby_ui/popover/popover_controller.js +95 -32
- data/lib/ruby_ui/table/table_docs.rb +2 -2
- data/lib/ruby_ui/tabs/tabs_docs.rb +1 -1
- data/lib/ruby_ui/tabs/tabs_trigger.rb +10 -4
- data/lib/ruby_ui/toast/toaster_controller.js +24 -8
- data/lib/ruby_ui.rb +1 -1
- metadata +35 -2
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyUI
|
|
4
4
|
class ContextMenuContent < Base
|
|
5
|
-
def view_template(&
|
|
6
|
-
|
|
7
|
-
div(**attrs, &block)
|
|
8
|
-
end
|
|
5
|
+
def view_template(&)
|
|
6
|
+
div(**attrs, &)
|
|
9
7
|
end
|
|
10
8
|
|
|
11
9
|
private
|
|
@@ -14,9 +12,10 @@ module RubyUI
|
|
|
14
12
|
{
|
|
15
13
|
role: "menu",
|
|
16
14
|
aria_orientation: "vertical",
|
|
17
|
-
data_state: "
|
|
15
|
+
data_state: "closed",
|
|
16
|
+
data: {ruby_ui__context_menu_target: "content"},
|
|
18
17
|
class:
|
|
19
|
-
"z-50 min-w-[8rem] outline-none pointer-events-auto overflow-hidden rounded-md border bg-background p-1 text-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
18
|
+
"hidden absolute z-50 min-w-[8rem] outline-none pointer-events-auto overflow-hidden rounded-md border bg-background p-1 text-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
20
19
|
tabindex: "-1",
|
|
21
20
|
data_orientation: "vertical"
|
|
22
21
|
}
|
|
@@ -1,106 +1,125 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus";
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
computePosition,
|
|
4
|
+
flip,
|
|
5
|
+
shift,
|
|
6
|
+
offset,
|
|
7
|
+
autoUpdate,
|
|
8
|
+
} from "@floating-ui/dom";
|
|
3
9
|
|
|
4
10
|
export default class extends Controller {
|
|
5
11
|
static targets = ["trigger", "content", "menuItem"];
|
|
6
12
|
static values = {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
},
|
|
11
|
-
|
|
12
|
-
matchWidth: {
|
|
13
|
-
type: Boolean,
|
|
14
|
-
default: false,
|
|
15
|
-
}
|
|
16
|
-
}
|
|
13
|
+
open: { type: Boolean, default: false },
|
|
14
|
+
options: { type: Object, default: {} },
|
|
15
|
+
// make content width match the trigger element (true/false)
|
|
16
|
+
matchWidth: { type: Boolean, default: false },
|
|
17
|
+
};
|
|
17
18
|
|
|
18
19
|
connect() {
|
|
19
|
-
this.
|
|
20
|
-
this.initializeTippy();
|
|
20
|
+
this.cleanup = null;
|
|
21
21
|
this.selectedIndex = -1;
|
|
22
|
+
this.boundHandleKeydown = this.handleKeydown.bind(this);
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
disconnect() {
|
|
25
|
-
this.
|
|
26
|
+
this.hide();
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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);
|
|
29
|
+
handleContextMenu(event) {
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
this.open();
|
|
55
32
|
}
|
|
56
33
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
34
|
+
open() {
|
|
35
|
+
this.openValue = true;
|
|
36
|
+
this.contentTarget.classList.remove("hidden");
|
|
37
|
+
this.contentTarget.dataset.state = "open";
|
|
38
|
+
if (this.matchWidthValue) {
|
|
39
|
+
this.contentTarget.style.width = `${this.triggerTarget.offsetWidth}px`;
|
|
60
40
|
}
|
|
41
|
+
this.addEventListeners();
|
|
42
|
+
this.updatePosition();
|
|
61
43
|
}
|
|
62
44
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
45
|
+
close() {
|
|
46
|
+
this.hide();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
hide() {
|
|
50
|
+
if (!this.openValue) return;
|
|
51
|
+
this.openValue = false;
|
|
52
|
+
this.contentTarget.classList.add("hidden");
|
|
53
|
+
this.contentTarget.dataset.state = "closed";
|
|
54
|
+
this.removeEventListeners();
|
|
55
|
+
this.deselectAll();
|
|
56
|
+
if (this.cleanup) {
|
|
57
|
+
this.cleanup();
|
|
58
|
+
this.cleanup = null;
|
|
68
59
|
}
|
|
69
60
|
}
|
|
70
61
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
62
|
+
updatePosition() {
|
|
63
|
+
if (this.cleanup) this.cleanup();
|
|
64
|
+
|
|
65
|
+
this.cleanup = autoUpdate(this.triggerTarget, this.contentTarget, () => {
|
|
66
|
+
computePosition(this.triggerTarget, this.contentTarget, {
|
|
67
|
+
placement: this.optionsValue.placement || "bottom-start",
|
|
68
|
+
middleware: [offset(4), flip(), shift({ padding: 8 })],
|
|
69
|
+
}).then(({ x, y, placement }) => {
|
|
70
|
+
Object.assign(this.contentTarget.style, {
|
|
71
|
+
left: `${x}px`,
|
|
72
|
+
top: `${y}px`,
|
|
73
|
+
});
|
|
74
|
+
this.contentTarget.dataset.side = placement.split("-")[0];
|
|
75
|
+
});
|
|
76
|
+
});
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
|
|
77
|
-
this.
|
|
79
|
+
addEventListeners() {
|
|
80
|
+
document.addEventListener("keydown", this.boundHandleKeydown);
|
|
81
|
+
document.addEventListener("click", this.handleOutsidePointer);
|
|
82
|
+
// A right-click outside should dismiss this menu and let the native
|
|
83
|
+
// context menu (or another trigger's menu) take over.
|
|
84
|
+
document.addEventListener("contextmenu", this.handleOutsidePointer);
|
|
78
85
|
}
|
|
79
86
|
|
|
80
|
-
|
|
81
|
-
this.
|
|
87
|
+
removeEventListeners() {
|
|
88
|
+
document.removeEventListener("keydown", this.boundHandleKeydown);
|
|
89
|
+
document.removeEventListener("click", this.handleOutsidePointer);
|
|
90
|
+
document.removeEventListener("contextmenu", this.handleOutsidePointer);
|
|
82
91
|
}
|
|
83
92
|
|
|
93
|
+
handleOutsidePointer = (event) => {
|
|
94
|
+
if (!this.element.contains(event.target)) {
|
|
95
|
+
this.hide();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
84
99
|
handleKeydown(e) {
|
|
85
|
-
|
|
86
|
-
|
|
100
|
+
if (e.key === "Escape") {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
this.hide();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
87
105
|
|
|
88
|
-
if (
|
|
106
|
+
if (this.menuItemTargets.length === 0) return;
|
|
107
|
+
|
|
108
|
+
if (e.key === "ArrowDown") {
|
|
89
109
|
e.preventDefault();
|
|
90
110
|
this.updateSelectedItem(1);
|
|
91
|
-
} else if (e.key ===
|
|
111
|
+
} else if (e.key === "ArrowUp") {
|
|
92
112
|
e.preventDefault();
|
|
93
113
|
this.updateSelectedItem(-1);
|
|
94
|
-
} else if (e.key ===
|
|
114
|
+
} else if (e.key === "Enter" && this.selectedIndex !== -1) {
|
|
95
115
|
e.preventDefault();
|
|
96
116
|
this.menuItemTargets[this.selectedIndex].click();
|
|
97
117
|
}
|
|
98
118
|
}
|
|
99
119
|
|
|
100
120
|
updateSelectedItem(direction) {
|
|
101
|
-
// Check if any of the menuItemTargets have aria-selected="true" and set the selectedIndex to that index
|
|
102
121
|
this.menuItemTargets.forEach((item, index) => {
|
|
103
|
-
if (item.getAttribute(
|
|
122
|
+
if (item.getAttribute("aria-selected") === "true") {
|
|
104
123
|
this.selectedIndex = index;
|
|
105
124
|
}
|
|
106
125
|
});
|
|
@@ -121,24 +140,17 @@ export default class extends Controller {
|
|
|
121
140
|
}
|
|
122
141
|
|
|
123
142
|
toggleAriaSelected(element, isSelected) {
|
|
124
|
-
// Add or remove attribute
|
|
125
143
|
if (isSelected) {
|
|
126
|
-
element.setAttribute(
|
|
144
|
+
element.setAttribute("aria-selected", "true");
|
|
127
145
|
} else {
|
|
128
|
-
element.removeAttribute(
|
|
146
|
+
element.removeAttribute("aria-selected");
|
|
129
147
|
}
|
|
130
148
|
}
|
|
131
149
|
|
|
132
150
|
deselectAll() {
|
|
133
|
-
this.menuItemTargets.forEach(item =>
|
|
151
|
+
this.menuItemTargets.forEach((item) =>
|
|
152
|
+
this.toggleAriaSelected(item, false)
|
|
153
|
+
);
|
|
134
154
|
this.selectedIndex = -1;
|
|
135
155
|
}
|
|
136
|
-
|
|
137
|
-
addEventListeners() {
|
|
138
|
-
document.addEventListener('keydown', this.boundHandleKeydown);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
removeEventListeners() {
|
|
142
|
-
document.removeEventListener('keydown', this.boundHandleKeydown);
|
|
143
|
-
}
|
|
144
156
|
}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyUI
|
|
4
4
|
class DataTableColumnToggle < Base
|
|
5
|
-
def initialize(columns:, **attrs)
|
|
5
|
+
def initialize(columns:, label: "Columns", **attrs)
|
|
6
6
|
@columns = columns
|
|
7
|
+
@label = label
|
|
7
8
|
super(**attrs)
|
|
8
9
|
end
|
|
9
10
|
|
|
@@ -12,7 +13,7 @@ module RubyUI
|
|
|
12
13
|
render RubyUI::DropdownMenu.new do
|
|
13
14
|
render RubyUI::DropdownMenuTrigger.new do
|
|
14
15
|
render RubyUI::Button.new(variant: :outline, size: :sm) do
|
|
15
|
-
plain
|
|
16
|
+
plain @label
|
|
16
17
|
# inline chevron-down SVG (lucide 24px, 1px stroke)
|
|
17
18
|
svg(
|
|
18
19
|
xmlns: "http://www.w3.org/2000/svg",
|
|
@@ -35,8 +36,11 @@ module RubyUI
|
|
|
35
36
|
label(class: "flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm cursor-pointer hover:bg-accent") do
|
|
36
37
|
input(
|
|
37
38
|
type: "checkbox",
|
|
38
|
-
checked: true,
|
|
39
|
-
class:
|
|
39
|
+
checked: col.fetch(:visible, true),
|
|
40
|
+
class: [
|
|
41
|
+
"h-4 w-4 rounded border border-input accent-primary cursor-pointer",
|
|
42
|
+
"checked:bg-primary checked:text-primary-foreground dark:checked:bg-secondary checked:text-primary checked:border-primary"
|
|
43
|
+
],
|
|
40
44
|
data: {
|
|
41
45
|
column_key: col[:key].to_s,
|
|
42
46
|
action: "change->ruby-ui--data-table-column-visibility#toggle"
|
|
@@ -2,9 +2,19 @@
|
|
|
2
2
|
import { Controller } from "@hotwired/stimulus";
|
|
3
3
|
|
|
4
4
|
export default class extends Controller {
|
|
5
|
+
connect() {
|
|
6
|
+
this.element
|
|
7
|
+
.querySelectorAll("input[type=checkbox][data-column-key]")
|
|
8
|
+
.forEach((checkbox) =>
|
|
9
|
+
this._apply(checkbox.dataset.columnKey, checkbox.checked)
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
toggle(event) {
|
|
6
|
-
|
|
7
|
-
|
|
14
|
+
this._apply(event.target.dataset.columnKey, event.target.checked);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
_apply(key, visible) {
|
|
8
18
|
const root = this.element.closest('[data-controller~="ruby-ui--data-table"]');
|
|
9
19
|
if (!root) return;
|
|
10
20
|
root
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Views::Docs::DataTable < Views::Base
|
|
4
|
-
Row = Struct.new(:id, :name, :email, :salary, :status
|
|
4
|
+
Row = Struct.new(:id, :name, :email, :salary, :status)
|
|
5
5
|
|
|
6
6
|
SAMPLE_ROWS = [
|
|
7
7
|
Row.new(id: 1, name: "Alice", email: "alice@example.com", salary: 90_000, status: "Active"),
|
|
@@ -17,14 +17,9 @@ module RubyUI
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def view_template
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
div(**attrs) do
|
|
24
|
-
yield
|
|
25
|
-
close_button
|
|
26
|
-
end
|
|
27
|
-
end
|
|
20
|
+
dialog(**attrs) do
|
|
21
|
+
yield
|
|
22
|
+
close_button
|
|
28
23
|
end
|
|
29
24
|
end
|
|
30
25
|
|
|
@@ -32,9 +27,10 @@ module RubyUI
|
|
|
32
27
|
|
|
33
28
|
def default_attrs
|
|
34
29
|
{
|
|
35
|
-
|
|
30
|
+
data_ruby_ui__dialog_target: "dialog",
|
|
31
|
+
data_action: "click->ruby-ui--dialog#backdropClick",
|
|
36
32
|
class: [
|
|
37
|
-
"fixed flex flex-col pointer-events-auto left-[50%] top-[50%] z-50 w-full max-h-screen overflow-y-auto translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200
|
|
33
|
+
"fixed open:flex flex-col pointer-events-auto left-[50%] top-[50%] z-50 w-full max-h-screen overflow-y-auto translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 backdrop:bg-background/80 backdrop:backdrop-blur-sm open:animate-in open:fade-in-0 open:zoom-in-95 sm:rounded-lg md:w-full",
|
|
38
34
|
SIZES[@size]
|
|
39
35
|
]
|
|
40
36
|
}
|
|
@@ -43,7 +39,7 @@ module RubyUI
|
|
|
43
39
|
def close_button
|
|
44
40
|
button(
|
|
45
41
|
type: "button",
|
|
46
|
-
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
|
|
42
|
+
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",
|
|
47
43
|
data_action: "click->ruby-ui--dialog#dismiss"
|
|
48
44
|
) do
|
|
49
45
|
svg(
|
|
@@ -65,13 +61,5 @@ module RubyUI
|
|
|
65
61
|
span(class: "sr-only") { "Close" }
|
|
66
62
|
end
|
|
67
63
|
end
|
|
68
|
-
|
|
69
|
-
def backdrop
|
|
70
|
-
div(
|
|
71
|
-
data_state: "open",
|
|
72
|
-
data_action: "click->ruby-ui--dialog#dismiss esc->ruby-ui--dialog#dismiss",
|
|
73
|
-
class: "fixed pointer-events-auto inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=open]:fade-in-0"
|
|
74
|
-
)
|
|
75
|
-
end
|
|
76
64
|
end
|
|
77
65
|
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
|
|
3
|
-
// Connects to data-controller="dialog"
|
|
3
|
+
// Connects to data-controller="ruby-ui--dialog"
|
|
4
4
|
export default class extends Controller {
|
|
5
|
-
static targets = ["
|
|
5
|
+
static targets = ["dialog"]
|
|
6
6
|
static values = {
|
|
7
7
|
open: {
|
|
8
8
|
type: Boolean,
|
|
@@ -11,22 +11,34 @@ export default class extends Controller {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
connect() {
|
|
14
|
+
this.dialogTarget.addEventListener("close", this.handleClose)
|
|
14
15
|
if (this.openValue) {
|
|
15
16
|
this.open()
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
disconnect() {
|
|
21
|
+
this.dialogTarget.removeEventListener("close", this.handleClose)
|
|
22
|
+
document.body.classList.remove("overflow-hidden")
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
open(e) {
|
|
20
|
-
e?.preventDefault()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
document.body.classList.add('overflow-hidden')
|
|
26
|
+
e?.preventDefault()
|
|
27
|
+
this.dialogTarget.showModal()
|
|
28
|
+
document.body.classList.add("overflow-hidden")
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
dismiss() {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
this.dialogTarget.close()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
backdropClick(e) {
|
|
36
|
+
if (e.target === this.dialogTarget) {
|
|
37
|
+
this.dismiss()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
handleClose = () => {
|
|
42
|
+
document.body.classList.remove("overflow-hidden")
|
|
31
43
|
}
|
|
32
44
|
}
|
|
@@ -43,7 +43,7 @@ export default class extends Controller {
|
|
|
43
43
|
|
|
44
44
|
#computeTooltip() {
|
|
45
45
|
computePosition(this.triggerTarget, this.contentTarget, {
|
|
46
|
-
placement: this.optionsValue.placement || "
|
|
46
|
+
placement: this.optionsValue.placement || "bottom",
|
|
47
47
|
middleware: [flip(), shift(), offset(8)],
|
|
48
48
|
strategy: this.optionsValue.strategy || "absolute",
|
|
49
49
|
}).then(({ x, y }) => {
|
|
@@ -73,12 +73,16 @@ export default class extends Controller {
|
|
|
73
73
|
this.#deselectAll();
|
|
74
74
|
this.#addEventListeners();
|
|
75
75
|
this.#computeTooltip();
|
|
76
|
+
// Lift the open menu above sibling dropdowns/elements. The container has no
|
|
77
|
+
// static z-index, so closed siblings stack in normal flow and never cover it.
|
|
78
|
+
this.element.style.zIndex = "50";
|
|
76
79
|
this.contentTarget.classList.remove("hidden");
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
close() {
|
|
80
83
|
this.openValue = false;
|
|
81
84
|
this.#removeEventListeners();
|
|
85
|
+
this.element.style.zIndex = "";
|
|
82
86
|
this.contentTarget.classList.add("hidden");
|
|
83
87
|
}
|
|
84
88
|
|
|
@@ -11,7 +11,7 @@ class Views::Docs::DropdownMenu < Views::Base
|
|
|
11
11
|
render Docs::VisualCodeExample.new(title: "Example", context: self) do
|
|
12
12
|
<<~RUBY
|
|
13
13
|
DropdownMenu do
|
|
14
|
-
DropdownMenuTrigger
|
|
14
|
+
DropdownMenuTrigger do
|
|
15
15
|
Button(variant: :outline) { "Open" }
|
|
16
16
|
end
|
|
17
17
|
DropdownMenuContent do
|
|
@@ -2,20 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyUI
|
|
4
4
|
class DropdownMenuItem < Base
|
|
5
|
-
def initialize(href: "#", **attrs)
|
|
5
|
+
def initialize(as: :a, href: "#", **attrs)
|
|
6
|
+
@as = as
|
|
6
7
|
@href = href
|
|
7
8
|
super(**attrs)
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
def view_template(&)
|
|
11
|
-
|
|
12
|
+
if @as == :div
|
|
13
|
+
div(**attrs, &)
|
|
14
|
+
else
|
|
15
|
+
a(**attrs, &)
|
|
16
|
+
end
|
|
12
17
|
end
|
|
13
18
|
|
|
14
19
|
private
|
|
15
20
|
|
|
16
21
|
def default_attrs
|
|
17
|
-
{
|
|
18
|
-
href: @href,
|
|
22
|
+
base = {
|
|
19
23
|
role: "menuitem",
|
|
20
24
|
class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
21
25
|
data_action: "click->ruby-ui--dropdown-menu#close",
|
|
@@ -23,6 +27,8 @@ module RubyUI
|
|
|
23
27
|
tabindex: "-1",
|
|
24
28
|
data_orientation: "vertical"
|
|
25
29
|
}
|
|
30
|
+
base[:href] = @href unless @as == :div
|
|
31
|
+
base
|
|
26
32
|
end
|
|
27
33
|
end
|
|
28
34
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class Empty < Base
|
|
5
|
+
def view_template(&)
|
|
6
|
+
div(**attrs, &)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def default_attrs
|
|
12
|
+
{
|
|
13
|
+
data: {slot: "empty"},
|
|
14
|
+
class: "flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-3xl border border-dashed p-12 text-center text-balance"
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class EmptyContent < Base
|
|
5
|
+
def view_template(&)
|
|
6
|
+
div(**attrs, &)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def default_attrs
|
|
12
|
+
{
|
|
13
|
+
data: {slot: "empty-content"},
|
|
14
|
+
class: "flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance"
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class EmptyDescription < Base
|
|
5
|
+
def view_template(&)
|
|
6
|
+
div(**attrs, &)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def default_attrs
|
|
12
|
+
{
|
|
13
|
+
data: {slot: "empty-description"},
|
|
14
|
+
class: "text-sm leading-relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary"
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Views::Docs::Empty < Views::Base
|
|
4
|
+
def view_template
|
|
5
|
+
component = "Empty"
|
|
6
|
+
|
|
7
|
+
div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
|
|
8
|
+
render Docs::Header.new(title: "Empty", description: "Use the empty component to display a state when there is no data or content.")
|
|
9
|
+
|
|
10
|
+
Heading(level: 2) { "Usage" }
|
|
11
|
+
|
|
12
|
+
render Docs::VisualCodeExample.new(title: "Default", context: self) do
|
|
13
|
+
<<~RUBY
|
|
14
|
+
Empty do
|
|
15
|
+
EmptyHeader do
|
|
16
|
+
EmptyMedia(variant: :icon) do
|
|
17
|
+
svg(xmlns: "http://www.w3.org/2000/svg", fill: "none", viewbox: "0 0 24 24", stroke_width: "1.5", stroke: "currentColor", class: "size-6") do |s|
|
|
18
|
+
s.path(stroke_linecap: "round", stroke_linejoin: "round", d: "M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
EmptyTitle { "No messages yet" }
|
|
22
|
+
EmptyDescription { "Start a conversation to see your messages here." }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
RUBY
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
render Docs::VisualCodeExample.new(title: "With action", context: self) do
|
|
29
|
+
<<~RUBY
|
|
30
|
+
Empty do
|
|
31
|
+
EmptyHeader do
|
|
32
|
+
EmptyMedia(variant: :icon) do
|
|
33
|
+
svg(xmlns: "http://www.w3.org/2000/svg", fill: "none", viewbox: "0 0 24 24", stroke_width: "1.5", stroke: "currentColor", class: "size-6") do |s|
|
|
34
|
+
s.path(stroke_linecap: "round", stroke_linejoin: "round", d: "M2.25 12.76c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 0 1 .865-.501 48.172 48.172 0 0 0 3.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
EmptyTitle { "No projects" }
|
|
38
|
+
EmptyDescription { "Get started by creating your first project." }
|
|
39
|
+
end
|
|
40
|
+
EmptyContent do
|
|
41
|
+
Button { "Create project" }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
RUBY
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
render Docs::VisualCodeExample.new(title: "Default media", context: self) do
|
|
48
|
+
<<~RUBY
|
|
49
|
+
Empty(class: "border-none") do
|
|
50
|
+
EmptyHeader do
|
|
51
|
+
EmptyMedia(variant: :default) do
|
|
52
|
+
Avatar(size: :lg) do
|
|
53
|
+
AvatarFallback { "RU" }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
EmptyTitle { "No team members" }
|
|
57
|
+
EmptyDescription { "Invite your team to start collaborating." }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
RUBY
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
render Components::ComponentSetup::Tabs.new(component_name: component)
|
|
64
|
+
|
|
65
|
+
# components
|
|
66
|
+
render Docs::ComponentsTable.new(component_files(component))
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class EmptyHeader < Base
|
|
5
|
+
def view_template(&)
|
|
6
|
+
div(**attrs, &)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def default_attrs
|
|
12
|
+
{
|
|
13
|
+
data: {slot: "empty-header"},
|
|
14
|
+
class: "flex max-w-sm flex-col items-center gap-2"
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|