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,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyUI
|
4
|
+
class BreadcrumbEllipsis < Base
|
5
|
+
def view_template(&)
|
6
|
+
span(**attrs) do
|
7
|
+
icon
|
8
|
+
span(class: "sr-only") { "More" }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def icon
|
15
|
+
svg(
|
16
|
+
xmlns: "http://www.w3.org/2000/svg",
|
17
|
+
class: "w-4 h-4",
|
18
|
+
viewbox: "0 0 24 24",
|
19
|
+
fill: "none",
|
20
|
+
stroke: "currentColor",
|
21
|
+
stroke_width: "2",
|
22
|
+
stroke_linecap: "round",
|
23
|
+
stroke_linejoin: "round"
|
24
|
+
) do |s|
|
25
|
+
s.circle(cx: "12", cy: "12", r: "1")
|
26
|
+
s.circle(cx: "19", cy: "12", r: "1")
|
27
|
+
s.circle(cx: "5", cy: "12", r: "1")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def default_attrs
|
32
|
+
{
|
33
|
+
aria: {hidden: true},
|
34
|
+
class: "flex h-9 w-9 items-center justify-center",
|
35
|
+
role: "presentation"
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyUI
|
4
|
+
class BreadcrumbLink < Base
|
5
|
+
def initialize(href: "#", **attrs)
|
6
|
+
@href = href
|
7
|
+
super(**attrs)
|
8
|
+
end
|
9
|
+
|
10
|
+
def view_template(&)
|
11
|
+
a(href: @href, **attrs, &)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def default_attrs
|
17
|
+
{
|
18
|
+
class: "transition-colors hover:text-foreground"
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyUI
|
4
|
+
class BreadcrumbList < Base
|
5
|
+
def view_template(&)
|
6
|
+
ol(**attrs, &)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def default_attrs
|
12
|
+
{
|
13
|
+
class: "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5"
|
14
|
+
}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyUI
|
4
|
+
class BreadcrumbPage < Base
|
5
|
+
def view_template(&)
|
6
|
+
span(**attrs, &)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def default_attrs
|
12
|
+
{
|
13
|
+
aria: {disabled: true, current: "page"},
|
14
|
+
class: "font-normal text-foreground",
|
15
|
+
role: "link"
|
16
|
+
}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyUI
|
4
|
+
class BreadcrumbSeparator < Base
|
5
|
+
def view_template(&block)
|
6
|
+
li(**attrs) do
|
7
|
+
if block
|
8
|
+
block.call
|
9
|
+
else
|
10
|
+
icon
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def icon
|
18
|
+
svg(
|
19
|
+
xmlns: "http://www.w3.org/2000/svg",
|
20
|
+
class: "w-4 h-4",
|
21
|
+
viewbox: "0 0 24 24",
|
22
|
+
fill: "none",
|
23
|
+
stroke: "currentColor",
|
24
|
+
stroke_width: "2",
|
25
|
+
stroke_linecap: "round",
|
26
|
+
stroke_linejoin: "round"
|
27
|
+
) { |s| s.path(d: "m9 18 6-6-6-6") }
|
28
|
+
end
|
29
|
+
|
30
|
+
def default_attrs
|
31
|
+
{
|
32
|
+
aria: {hidden: true},
|
33
|
+
class: "[&>svg]:w-3.5 [&>svg]:h-3.5",
|
34
|
+
role: "presentation"
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,249 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
import Mustache from "mustache";
|
3
|
+
|
4
|
+
export default class extends Controller {
|
5
|
+
static targets = [
|
6
|
+
"calendar",
|
7
|
+
"title",
|
8
|
+
"weekdaysTemplate",
|
9
|
+
"selectedDateTemplate",
|
10
|
+
"todayDateTemplate",
|
11
|
+
"currentMonthDateTemplate",
|
12
|
+
"otherMonthDateTemplate",
|
13
|
+
];
|
14
|
+
static values = {
|
15
|
+
selectedDate: {
|
16
|
+
type: String,
|
17
|
+
default: null,
|
18
|
+
},
|
19
|
+
viewDate: {
|
20
|
+
type: String,
|
21
|
+
default: new Date().toISOString().slice(0, 10),
|
22
|
+
},
|
23
|
+
format: {
|
24
|
+
type: String,
|
25
|
+
default: "yyyy-MM-dd", // Default format
|
26
|
+
},
|
27
|
+
};
|
28
|
+
static outlets = ["ruby-ui--calendar-input"];
|
29
|
+
|
30
|
+
initialize() {
|
31
|
+
this.updateCalendar(); // Initial calendar render
|
32
|
+
}
|
33
|
+
|
34
|
+
nextMonth(e) {
|
35
|
+
e.preventDefault();
|
36
|
+
this.viewDateValue = this.adjustMonth(1);
|
37
|
+
}
|
38
|
+
|
39
|
+
prevMonth(e) {
|
40
|
+
e.preventDefault();
|
41
|
+
this.viewDateValue = this.adjustMonth(-1);
|
42
|
+
}
|
43
|
+
|
44
|
+
selectDay(e) {
|
45
|
+
e.preventDefault();
|
46
|
+
// Set the selected date value
|
47
|
+
this.selectedDateValue = e.currentTarget.dataset.day;
|
48
|
+
}
|
49
|
+
|
50
|
+
selectedDateValueChanged(value, prevValue) {
|
51
|
+
// update the viewDateValue to the first day of month of the selected date (This will trigger updateCalendar() function)
|
52
|
+
const newViewDate = new Date(this.selectedDateValue);
|
53
|
+
newViewDate.setDate(2); // set the day to the 2nd (to avoid issues with months with different number of days and timezones)
|
54
|
+
this.viewDateValue = newViewDate.toISOString().slice(0, 10);
|
55
|
+
|
56
|
+
// Re-render the calendar
|
57
|
+
this.updateCalendar();
|
58
|
+
|
59
|
+
// update the input value
|
60
|
+
this.rubyUiCalendarInputOutlets.forEach((outlet) => {
|
61
|
+
const formattedDate = this.formatDate(this.selectedDate());
|
62
|
+
outlet.setValue(formattedDate);
|
63
|
+
});
|
64
|
+
}
|
65
|
+
|
66
|
+
viewDateValueChanged(value, prevValue) {
|
67
|
+
this.updateCalendar();
|
68
|
+
}
|
69
|
+
|
70
|
+
adjustMonth(adjustment) {
|
71
|
+
const date = this.viewDate();
|
72
|
+
date.setDate(2); // set the day to the 2nd (to avoid issues with months with different number of days and timezones)
|
73
|
+
date.setMonth(date.getMonth() + adjustment);
|
74
|
+
return date.toISOString().slice(0, 10);
|
75
|
+
}
|
76
|
+
|
77
|
+
updateCalendar() {
|
78
|
+
// Update the title with month and year
|
79
|
+
this.titleTarget.textContent = this.monthAndYear();
|
80
|
+
this.calendarTarget.innerHTML = this.calendarHTML();
|
81
|
+
}
|
82
|
+
|
83
|
+
calendarHTML() {
|
84
|
+
return this.weekdaysTemplateTarget.innerHTML + this.calendarDays();
|
85
|
+
}
|
86
|
+
|
87
|
+
calendarDays() {
|
88
|
+
return this.getFullWeeksStartAndEndInMonth()
|
89
|
+
.map((week) => this.renderWeek(week))
|
90
|
+
.join("");
|
91
|
+
}
|
92
|
+
|
93
|
+
renderWeek(week) {
|
94
|
+
const days = week
|
95
|
+
.map((day) => {
|
96
|
+
return this.renderDay(day);
|
97
|
+
})
|
98
|
+
.join("");
|
99
|
+
return `<tr class="flex w-full mt-2">${days}</tr>`;
|
100
|
+
}
|
101
|
+
|
102
|
+
renderDay(day) {
|
103
|
+
const today = new Date();
|
104
|
+
let dateHTML = "";
|
105
|
+
const data = { day: day, dayDate: day.getDate() };
|
106
|
+
|
107
|
+
if (day.toDateString() === this.selectedDate().toDateString()) {
|
108
|
+
// selectedDate
|
109
|
+
// Render the selected date template target innerHTML with Mustache
|
110
|
+
dateHTML = Mustache.render(
|
111
|
+
this.selectedDateTemplateTarget.innerHTML,
|
112
|
+
data,
|
113
|
+
);
|
114
|
+
} else if (day.toDateString() === today.toDateString()) {
|
115
|
+
// todayDate
|
116
|
+
dateHTML = Mustache.render(this.todayDateTemplateTarget.innerHTML, data);
|
117
|
+
} else if (day.getMonth() === this.viewDate().getMonth()) {
|
118
|
+
// currentMonthDate
|
119
|
+
dateHTML = Mustache.render(
|
120
|
+
this.currentMonthDateTemplateTarget.innerHTML,
|
121
|
+
data,
|
122
|
+
);
|
123
|
+
} else {
|
124
|
+
// otherMonthDate
|
125
|
+
dateHTML = Mustache.render(
|
126
|
+
this.otherMonthDateTemplateTarget.innerHTML,
|
127
|
+
data,
|
128
|
+
);
|
129
|
+
}
|
130
|
+
return dateHTML;
|
131
|
+
}
|
132
|
+
|
133
|
+
monthAndYear() {
|
134
|
+
const month = this.viewDate().toLocaleString("en-US", { month: "long" });
|
135
|
+
const year = this.viewDate().getFullYear();
|
136
|
+
return `${month} ${year}`;
|
137
|
+
}
|
138
|
+
|
139
|
+
selectedDate() {
|
140
|
+
return new Date(this.selectedDateValue);
|
141
|
+
}
|
142
|
+
|
143
|
+
viewDate() {
|
144
|
+
return this.viewDateValue
|
145
|
+
? new Date(this.viewDateValue)
|
146
|
+
: this.selectedDate();
|
147
|
+
}
|
148
|
+
|
149
|
+
getFullWeeksStartAndEndInMonth() {
|
150
|
+
const month = this.viewDate().getMonth();
|
151
|
+
const year = this.viewDate().getFullYear();
|
152
|
+
|
153
|
+
let weeks = [],
|
154
|
+
firstDate = new Date(year, month, 1),
|
155
|
+
lastDate = new Date(year, month + 1, 0),
|
156
|
+
numDays = lastDate.getDate();
|
157
|
+
|
158
|
+
let start = 1;
|
159
|
+
let end;
|
160
|
+
if (firstDate.getDay() === 1) {
|
161
|
+
end = 7;
|
162
|
+
} else if (firstDate.getDay() === 0) {
|
163
|
+
let preMonthEndDay = new Date(year, month, 0);
|
164
|
+
start = preMonthEndDay.getDate() - 6 + 1;
|
165
|
+
end = 1;
|
166
|
+
} else {
|
167
|
+
let preMonthEndDay = new Date(year, month, 0);
|
168
|
+
start = preMonthEndDay.getDate() + 1 - firstDate.getDay() + 1;
|
169
|
+
end = 7 - firstDate.getDay() + 1;
|
170
|
+
weeks.push({
|
171
|
+
start: start,
|
172
|
+
end: end,
|
173
|
+
});
|
174
|
+
start = end + 1;
|
175
|
+
end = end + 7;
|
176
|
+
}
|
177
|
+
while (start <= numDays) {
|
178
|
+
weeks.push({
|
179
|
+
start: start,
|
180
|
+
end: end,
|
181
|
+
});
|
182
|
+
start = end + 1;
|
183
|
+
end = end + 7;
|
184
|
+
end = start === 1 && end === 8 ? 1 : end;
|
185
|
+
if (end > numDays && start <= numDays) {
|
186
|
+
end = end - numDays;
|
187
|
+
weeks.push({
|
188
|
+
start: start,
|
189
|
+
end: end,
|
190
|
+
});
|
191
|
+
break;
|
192
|
+
}
|
193
|
+
}
|
194
|
+
// *** the magic starts here
|
195
|
+
return weeks.map(({ start, end }, index) => {
|
196
|
+
const sub = +(start > end && index === 0);
|
197
|
+
return Array.from({ length: 7 }, (_, index) => {
|
198
|
+
const date = new Date(year, month - sub, start + index);
|
199
|
+
return date;
|
200
|
+
});
|
201
|
+
});
|
202
|
+
}
|
203
|
+
|
204
|
+
formatDate(date) {
|
205
|
+
const format = this.formatValue;
|
206
|
+
const day = date.getDate();
|
207
|
+
const month = date.getMonth() + 1;
|
208
|
+
const year = date.getFullYear();
|
209
|
+
const hours = date.getHours();
|
210
|
+
const minutes = date.getMinutes();
|
211
|
+
const seconds = date.getSeconds();
|
212
|
+
const dayOfWeek = date.toLocaleString("en-US", { weekday: "long" });
|
213
|
+
const monthName = date.toLocaleString("en-US", { month: "long" });
|
214
|
+
const daySuffix = this.getDaySuffix(day);
|
215
|
+
|
216
|
+
const map = {
|
217
|
+
yyyy: year,
|
218
|
+
MM: ("0" + month).slice(-2),
|
219
|
+
dd: ("0" + day).slice(-2),
|
220
|
+
HH: ("0" + hours).slice(-2),
|
221
|
+
mm: ("0" + minutes).slice(-2),
|
222
|
+
ss: ("0" + seconds).slice(-2),
|
223
|
+
EEEE: dayOfWeek,
|
224
|
+
MMMM: monthName,
|
225
|
+
do: day + daySuffix,
|
226
|
+
PPPP: `${dayOfWeek}, ${monthName} ${day}${daySuffix}, ${year}`,
|
227
|
+
};
|
228
|
+
|
229
|
+
const formattedDate = format.replace(
|
230
|
+
/yyyy|MM|dd|HH|mm|ss|EEEE|MMMM|do|PPPP/g,
|
231
|
+
(matched) => map[matched],
|
232
|
+
);
|
233
|
+
return formattedDate;
|
234
|
+
}
|
235
|
+
|
236
|
+
getDaySuffix(day) {
|
237
|
+
if (day > 3 && day < 21) return "th";
|
238
|
+
switch (day % 10) {
|
239
|
+
case 1:
|
240
|
+
return "st";
|
241
|
+
case 2:
|
242
|
+
return "nd";
|
243
|
+
case 3:
|
244
|
+
return "rd";
|
245
|
+
default:
|
246
|
+
return "th";
|
247
|
+
}
|
248
|
+
}
|
249
|
+
}
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyUI
|
4
|
+
class Carousel < Base
|
5
|
+
def initialize(orientation: :horizontal, options: {}, **user_attrs)
|
6
|
+
@orientation = orientation
|
7
|
+
@options = options
|
8
|
+
|
9
|
+
super(**user_attrs)
|
10
|
+
end
|
11
|
+
|
12
|
+
def view_template(&)
|
13
|
+
div(**attrs, &)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def default_attrs
|
19
|
+
{
|
20
|
+
class: ["relative group", orientation_classes],
|
21
|
+
role: "region",
|
22
|
+
aria_roledescription: "carousel",
|
23
|
+
data: {
|
24
|
+
controller: "ruby-ui--carousel",
|
25
|
+
ruby_ui__carousel_options_value: default_options.merge(@options).to_json,
|
26
|
+
action: %w[
|
27
|
+
keydown.right->ruby-ui--carousel#scrollNext:prevent
|
28
|
+
keydown.left->ruby-ui--carousel#scrollPrev:prevent
|
29
|
+
]
|
30
|
+
}
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def default_options
|
35
|
+
{
|
36
|
+
axis: (@orientation == :horizontal) ? "x" : "y"
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def orientation_classes
|
41
|
+
(@orientation == :horizontal) ? "is-horizontal" : "is-vertical"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyUI
|
4
|
+
class CarouselContent < Base
|
5
|
+
def view_template(&)
|
6
|
+
div(class: "overflow-hidden", data: {ruby_ui__carousel_target: "viewport"}) do
|
7
|
+
div(**attrs, &)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def default_attrs
|
14
|
+
{
|
15
|
+
class: [
|
16
|
+
"flex",
|
17
|
+
"group-[.is-horizontal]:-ml-4",
|
18
|
+
"group-[.is-vertical]:-mt-4 group-[.is-vertical]:flex-col"
|
19
|
+
]
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
import EmblaCarousel from 'embla-carousel'
|
3
|
+
|
4
|
+
const DEFAULT_OPTIONS = {
|
5
|
+
loop: true
|
6
|
+
}
|
7
|
+
|
8
|
+
export default class extends Controller {
|
9
|
+
static values = {
|
10
|
+
options: {
|
11
|
+
type: Object,
|
12
|
+
default: {},
|
13
|
+
}
|
14
|
+
}
|
15
|
+
static targets = ["viewport", "nextButton", "prevButton"]
|
16
|
+
|
17
|
+
connect() {
|
18
|
+
this.initCarousel(this.#mergedOptions)
|
19
|
+
}
|
20
|
+
|
21
|
+
disconnect() {
|
22
|
+
this.destroyCarousel()
|
23
|
+
}
|
24
|
+
|
25
|
+
initCarousel(options, plugins = []) {
|
26
|
+
this.carousel = EmblaCarousel(this.viewportTarget, options, plugins)
|
27
|
+
|
28
|
+
this.carousel.on("init", this.#updateControls.bind(this))
|
29
|
+
this.carousel.on("reInit", this.#updateControls.bind(this))
|
30
|
+
this.carousel.on("select", this.#updateControls.bind(this))
|
31
|
+
}
|
32
|
+
|
33
|
+
destroyCarousel() {
|
34
|
+
this.carousel.destroy()
|
35
|
+
}
|
36
|
+
|
37
|
+
scrollNext() {
|
38
|
+
this.carousel.scrollNext()
|
39
|
+
}
|
40
|
+
|
41
|
+
scrollPrev() {
|
42
|
+
this.carousel.scrollPrev()
|
43
|
+
}
|
44
|
+
|
45
|
+
#updateControls() {
|
46
|
+
this.#toggleButtonsDisabledState(this.nextButtonTargets, !this.carousel.canScrollNext())
|
47
|
+
this.#toggleButtonsDisabledState(this.prevButtonTargets, !this.carousel.canScrollPrev())
|
48
|
+
}
|
49
|
+
|
50
|
+
#toggleButtonsDisabledState(buttons, isDisabled) {
|
51
|
+
buttons.forEach((button) => button.disabled = isDisabled)
|
52
|
+
}
|
53
|
+
|
54
|
+
get #mergedOptions() {
|
55
|
+
return {
|
56
|
+
...DEFAULT_OPTIONS,
|
57
|
+
...this.optionsValue
|
58
|
+
}
|
59
|
+
}
|
60
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyUI
|
4
|
+
class CarouselItem < Base
|
5
|
+
def view_template(&)
|
6
|
+
div(**attrs, &)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def default_attrs
|
12
|
+
{
|
13
|
+
role: "group",
|
14
|
+
aria_roledescription: "slide",
|
15
|
+
class: [
|
16
|
+
"min-w-0 shrink-0 grow-0 basis-full",
|
17
|
+
"group-[.is-horizontal]:pl-4",
|
18
|
+
"group-[.is-vertical]:pt-4"
|
19
|
+
]
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyUI
|
4
|
+
class CarouselNext < Base
|
5
|
+
def view_template(&)
|
6
|
+
Button(**attrs) do
|
7
|
+
icon
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def default_attrs
|
14
|
+
{
|
15
|
+
variant: :outline,
|
16
|
+
icon: true,
|
17
|
+
class: [
|
18
|
+
"absolute h-8 w-8 rounded-full",
|
19
|
+
"group-[.is-horizontal]:-right-12 group-[.is-horizontal]:top-1/2 group-[.is-horizontal]:-translate-y-1/2",
|
20
|
+
"group-[.is-vertical]:-bottom-12 group-[.is-vertical]:left-1/2 group-[.is-vertical]:-translate-x-1/2 group-[.is-vertical]:rotate-90"
|
21
|
+
],
|
22
|
+
disabled: true,
|
23
|
+
data: {
|
24
|
+
action: "click->ruby-ui--carousel#scrollNext",
|
25
|
+
ruby_ui__carousel_target: "nextButton"
|
26
|
+
}
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def icon
|
31
|
+
svg(
|
32
|
+
width: "24",
|
33
|
+
height: "24",
|
34
|
+
viewBox: "0 0 24 24",
|
35
|
+
fill: "none",
|
36
|
+
stroke: "currentColor",
|
37
|
+
stroke_width: "2",
|
38
|
+
stroke_linecap: "round",
|
39
|
+
stroke_linejoin: "round",
|
40
|
+
xmlns: "http://www.w3.org/2000/svg",
|
41
|
+
class: "w-4 h-4"
|
42
|
+
) do |s|
|
43
|
+
s.path(d: "M5 12h14")
|
44
|
+
s.path(d: "m12 5 7 7-7 7")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|