phlex_kit 0.8.1 → 0.9.1
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/app/assets/stylesheets/phlex_kit/_tokens.css +11 -0
- data/app/components/phlex_kit/accordion/accordion.css +2 -2
- data/app/components/phlex_kit/accordion/accordion_controller.js +8 -2
- data/app/components/phlex_kit/alert/alert.css +3 -3
- data/app/components/phlex_kit/alert_dialog/alert_dialog.css +3 -3
- data/app/components/phlex_kit/alert_dialog/alert_dialog_content.rb +12 -8
- data/app/components/phlex_kit/alert_dialog/alert_dialog_controller.js +46 -3
- data/app/components/phlex_kit/aspect_ratio/aspect_ratio.rb +4 -2
- data/app/components/phlex_kit/attachment/attachment.css +1 -1
- data/app/components/phlex_kit/attachment/attachment_action.rb +8 -3
- data/app/components/phlex_kit/attachment/attachment_trigger.rb +3 -3
- data/app/components/phlex_kit/attachment/scroll_fade_controller.js +8 -0
- data/app/components/phlex_kit/avatar/avatar.css +16 -5
- data/app/components/phlex_kit/avatar/avatar_controller.js +10 -7
- data/app/components/phlex_kit/badge/badge.css +4 -4
- data/app/components/phlex_kit/breadcrumb/breadcrumb_ellipsis.rb +3 -1
- data/app/components/phlex_kit/bubble/bubble.css +3 -3
- data/app/components/phlex_kit/bubble/bubble.rb +2 -0
- data/app/components/phlex_kit/bubble/bubble_content.rb +5 -1
- data/app/components/phlex_kit/bubble/bubble_reactions.rb +4 -0
- data/app/components/phlex_kit/button/button.css +6 -6
- data/app/components/phlex_kit/button_group/button_group.css +2 -2
- data/app/components/phlex_kit/button_group/button_group_separator.rb +3 -1
- data/app/components/phlex_kit/calendar/calendar.css +2 -2
- data/app/components/phlex_kit/calendar/calendar.rb +10 -3
- data/app/components/phlex_kit/calendar/calendar_body.rb +3 -0
- data/app/components/phlex_kit/calendar/calendar_controller.js +91 -14
- data/app/components/phlex_kit/calendar/calendar_days.rb +17 -15
- data/app/components/phlex_kit/calendar/calendar_input_controller.js +6 -0
- data/app/components/phlex_kit/calendar/calendar_next.rb +5 -1
- data/app/components/phlex_kit/calendar/calendar_prev.rb +5 -1
- data/app/components/phlex_kit/calendar/calendar_weekdays.rb +1 -1
- data/app/components/phlex_kit/carousel/carousel.rb +11 -1
- data/app/components/phlex_kit/carousel/carousel_controller.js +28 -1
- data/app/components/phlex_kit/chart/chart_controller.js +32 -9
- data/app/components/phlex_kit/checkbox/checkbox.css +2 -1
- data/app/components/phlex_kit/clipboard/clipboard.css +1 -1
- data/app/components/phlex_kit/clipboard/clipboard_controller.js +7 -0
- data/app/components/phlex_kit/codeblock/codeblock.rb +4 -1
- data/app/components/phlex_kit/collapsible/collapsible_content.rb +15 -2
- data/app/components/phlex_kit/collapsible/collapsible_controller.js +11 -2
- data/app/components/phlex_kit/collapsible/collapsible_trigger.rb +17 -2
- data/app/components/phlex_kit/combobox/combobox.css +10 -8
- data/app/components/phlex_kit/combobox/combobox_controller.js +52 -9
- data/app/components/phlex_kit/combobox/combobox_list_group.rb +13 -5
- data/app/components/phlex_kit/command/command.css +4 -4
- data/app/components/phlex_kit/command/command_controller.js +51 -10
- data/app/components/phlex_kit/command/command_dialog.rb +5 -6
- data/app/components/phlex_kit/command/command_dialog_controller.js +26 -8
- data/app/components/phlex_kit/command/command_input.rb +6 -2
- data/app/components/phlex_kit/command/command_item.rb +4 -1
- data/app/components/phlex_kit/context_menu/context_menu.css +6 -6
- data/app/components/phlex_kit/context_menu/context_menu_controller.js +18 -1
- data/app/components/phlex_kit/data_table/data_table.css +1 -1
- data/app/components/phlex_kit/data_table/data_table_column_toggle.rb +11 -12
- data/app/components/phlex_kit/data_table/data_table_column_visibility_controller.js +4 -1
- data/app/components/phlex_kit/data_table/data_table_pagy_adapter.rb +2 -1
- data/app/components/phlex_kit/data_table/data_table_per_page_select.rb +19 -5
- data/app/components/phlex_kit/data_table/data_table_search_controller.js +13 -1
- data/app/components/phlex_kit/date_picker/date_picker.rb +50 -3
- data/app/components/phlex_kit/dialog/dialog.css +4 -4
- data/app/components/phlex_kit/dialog/dialog_controller.js +20 -4
- data/app/components/phlex_kit/drawer/drawer.css +3 -3
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu.css +3 -3
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu.rb +5 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_controller.js +33 -0
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_label.rb +3 -1
- data/app/components/phlex_kit/field/field.css +1 -1
- data/app/components/phlex_kit/field/field_error.rb +5 -6
- data/app/components/phlex_kit/form_field/form_field_controller.js +35 -16
- data/app/components/phlex_kit/form_field/form_field_error.rb +5 -1
- data/app/components/phlex_kit/hover_card/hover_card.css +1 -1
- data/app/components/phlex_kit/hover_card/hover_card_controller.js +29 -5
- data/app/components/phlex_kit/input/input.css +1 -1
- data/app/components/phlex_kit/input_group/input_group.css +9 -7
- data/app/components/phlex_kit/input_group/input_group_addon.rb +5 -1
- data/app/components/phlex_kit/input_otp/input_otp.css +1 -1
- data/app/components/phlex_kit/input_otp/input_otp_controller.js +13 -0
- data/app/components/phlex_kit/input_otp/input_otp_separator.rb +4 -2
- data/app/components/phlex_kit/item/item.css +1 -1
- data/app/components/phlex_kit/item/item_group.rb +4 -2
- data/app/components/phlex_kit/item/item_separator.rb +3 -1
- data/app/components/phlex_kit/marker/marker.css +1 -1
- data/app/components/phlex_kit/masked_input/masked_input_controller.js +10 -4
- data/app/components/phlex_kit/menubar/menubar.css +10 -9
- data/app/components/phlex_kit/menubar/menubar_content.rb +8 -2
- data/app/components/phlex_kit/menubar/menubar_controller.js +70 -7
- data/app/components/phlex_kit/menubar/menubar_menu.rb +4 -1
- data/app/components/phlex_kit/menubar/menubar_sub.rb +6 -1
- data/app/components/phlex_kit/menubar/menubar_sub_trigger.rb +4 -1
- data/app/components/phlex_kit/message_scroller/message_scroller.css +1 -1
- data/app/components/phlex_kit/message_scroller/message_scroller_controller.js +46 -2
- data/app/components/phlex_kit/native_select/native_select.css +1 -1
- data/app/components/phlex_kit/navigation_menu/navigation_menu.css +7 -2
- data/app/components/phlex_kit/navigation_menu/navigation_menu.rb +6 -1
- data/app/components/phlex_kit/pagination/pagination.css +2 -2
- data/app/components/phlex_kit/pagination/pagination_ellipsis.rb +3 -1
- data/app/components/phlex_kit/popover/popover.css +1 -1
- data/app/components/phlex_kit/resizable/resizable.css +3 -1
- data/app/components/phlex_kit/resizable/resizable_controller.js +13 -0
- data/app/components/phlex_kit/resizable/resizable_panel.rb +3 -1
- data/app/components/phlex_kit/scroll_area/scroll_area.rb +4 -2
- data/app/components/phlex_kit/select/select.css +3 -3
- data/app/components/phlex_kit/select/select.rb +4 -1
- data/app/components/phlex_kit/select/select_controller.js +25 -0
- data/app/components/phlex_kit/select/select_item.rb +5 -1
- data/app/components/phlex_kit/sheet/sheet.css +3 -3
- data/app/components/phlex_kit/sheet/sheet_content_controller.js +36 -12
- data/app/components/phlex_kit/sidebar/sidebar.css +8 -8
- data/app/components/phlex_kit/sidebar/sidebar_controller.js +98 -1
- data/app/components/phlex_kit/sidebar/sidebar_menu_button.rb +24 -5
- data/app/components/phlex_kit/sidebar/sidebar_rail.rb +13 -4
- data/app/components/phlex_kit/sidebar/sidebar_trigger.rb +14 -4
- data/app/components/phlex_kit/slider/slider.css +1 -1
- data/app/components/phlex_kit/slider/slider.rb +3 -1
- data/app/components/phlex_kit/switch/switch.rb +6 -1
- data/app/components/phlex_kit/table/table.css +5 -3
- data/app/components/phlex_kit/tabs/tabs.css +4 -4
- data/app/components/phlex_kit/tabs/tabs_content.rb +6 -3
- data/app/components/phlex_kit/tabs/tabs_controller.js +3 -1
- data/app/components/phlex_kit/theme_toggle/theme_toggle.rb +2 -2
- data/app/components/phlex_kit/theme_toggle/theme_toggle_controller.js +45 -9
- data/app/components/phlex_kit/toast/toast.css +32 -14
- data/app/components/phlex_kit/toast/toast_controller.js +24 -4
- data/app/components/phlex_kit/toast/toast_region.rb +13 -3
- data/app/components/phlex_kit/toast/toaster_controller.js +13 -3
- data/app/components/phlex_kit/toggle/toggle.css +4 -4
- data/app/components/phlex_kit/toggle/toggle.rb +6 -2
- data/app/components/phlex_kit/tooltip/tooltip.css +28 -6
- data/app/components/phlex_kit/typography/heading.rb +9 -3
- data/app/components/phlex_kit/typography/typography.css +4 -3
- data/lib/phlex_kit/version.rb +1 -1
- metadata +1 -1
|
@@ -9,6 +9,9 @@ module PhlexKit
|
|
|
9
9
|
def view_template
|
|
10
10
|
table(**mix({
|
|
11
11
|
class: "pk-calendar-body",
|
|
12
|
+
# The day grid is a real ARIA grid: this table is role="grid", the
|
|
13
|
+
# controller-rendered rows are role="row", the <td>s gridcells.
|
|
14
|
+
role: "grid",
|
|
12
15
|
data: {
|
|
13
16
|
phlex_kit__calendar_target: "calendar",
|
|
14
17
|
# Delegated arrow-key/Home/End navigation for the day grid (the
|
|
@@ -17,6 +17,8 @@ export default class extends Controller {
|
|
|
17
17
|
"title",
|
|
18
18
|
"monthSelect",
|
|
19
19
|
"yearSelect",
|
|
20
|
+
"prevButton",
|
|
21
|
+
"nextButton",
|
|
20
22
|
"weekdaysTemplate",
|
|
21
23
|
"disabledDateTemplate",
|
|
22
24
|
"selectedDateTemplate",
|
|
@@ -42,18 +44,18 @@ export default class extends Controller {
|
|
|
42
44
|
};
|
|
43
45
|
static outlets = ["phlex-kit--calendar-input"];
|
|
44
46
|
|
|
45
|
-
initialize()
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
// No initialize() render: Stimulus fires every value's `…ValueChanged`
|
|
48
|
+
// callback once during value initialization (before connect()), so
|
|
49
|
+
// viewDateValueChanged already performs the initial updateCalendar().
|
|
48
50
|
|
|
49
51
|
nextMonth(e) {
|
|
50
52
|
e.preventDefault();
|
|
51
|
-
this.viewDateValue = this.adjustMonth(1);
|
|
53
|
+
this.viewDateValue = this.clampViewIso(this.adjustMonth(1));
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
prevMonth(e) {
|
|
55
57
|
e.preventDefault();
|
|
56
|
-
this.viewDateValue = this.adjustMonth(-1);
|
|
58
|
+
this.viewDateValue = this.clampViewIso(this.adjustMonth(-1));
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
// month/year dropdown caption (native selects)
|
|
@@ -61,14 +63,62 @@ export default class extends Controller {
|
|
|
61
63
|
const date = this.viewDate();
|
|
62
64
|
date.setDate(2);
|
|
63
65
|
date.setMonth(Number(e.target.value));
|
|
64
|
-
this.viewDateValue = this.isoDate(date);
|
|
66
|
+
this.viewDateValue = this.clampViewIso(this.isoDate(date));
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
setYear(e) {
|
|
68
70
|
const date = this.viewDate();
|
|
69
71
|
date.setDate(2);
|
|
70
72
|
date.setFullYear(Number(e.target.value));
|
|
71
|
-
this.viewDateValue = this.isoDate(date);
|
|
73
|
+
this.viewDateValue = this.clampViewIso(this.isoDate(date));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// --- month-navigation bounds (min/max dates + the year dropdown's range) ---
|
|
77
|
+
|
|
78
|
+
// Earliest/latest navigable months. min/max dates bound directly; a year
|
|
79
|
+
// dropdown additionally bounds the view to its option range (from_year /
|
|
80
|
+
// to_year) so navigation can't drive the select out of range (blank).
|
|
81
|
+
monthBounds() {
|
|
82
|
+
let min = this.minDate();
|
|
83
|
+
let max = this.maxDate();
|
|
84
|
+
if (this.hasYearSelectTarget) {
|
|
85
|
+
const years = [...this.yearSelectTarget.options].map((o) => Number(o.value)).filter(Number.isFinite);
|
|
86
|
+
if (years.length) {
|
|
87
|
+
const yearMin = new Date(Math.min(...years), 0, 1);
|
|
88
|
+
const yearMax = new Date(Math.max(...years), 11, 31);
|
|
89
|
+
if (!min || yearMin > min) min = yearMin;
|
|
90
|
+
if (!max || yearMax < max) max = yearMax;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { min, max };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
monthIndexOf(date) {
|
|
97
|
+
return date.getFullYear() * 12 + date.getMonth();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
clampViewIso(iso) {
|
|
101
|
+
const date = this.parseDate(iso);
|
|
102
|
+
if (!date) return iso;
|
|
103
|
+
const { min, max } = this.monthBounds();
|
|
104
|
+
if (min && this.monthIndexOf(date) < this.monthIndexOf(min)) {
|
|
105
|
+
return this.isoDate(new Date(min.getFullYear(), min.getMonth(), 2));
|
|
106
|
+
}
|
|
107
|
+
if (max && this.monthIndexOf(date) > this.monthIndexOf(max)) {
|
|
108
|
+
return this.isoDate(new Date(max.getFullYear(), max.getMonth(), 2));
|
|
109
|
+
}
|
|
110
|
+
return iso;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
syncNavButtons() {
|
|
114
|
+
const { min, max } = this.monthBounds();
|
|
115
|
+
const view = this.monthIndexOf(this.viewDate());
|
|
116
|
+
if (this.hasPrevButtonTarget) {
|
|
117
|
+
this.prevButtonTarget.disabled = Boolean(min && view <= this.monthIndexOf(min));
|
|
118
|
+
}
|
|
119
|
+
if (this.hasNextButtonTarget) {
|
|
120
|
+
this.nextButtonTarget.disabled = Boolean(max && view >= this.monthIndexOf(max));
|
|
121
|
+
}
|
|
72
122
|
}
|
|
73
123
|
|
|
74
124
|
selectDay(e) {
|
|
@@ -162,6 +212,9 @@ export default class extends Controller {
|
|
|
162
212
|
return end ? `${this.formatDate(start)} – ${this.formatDate(end)}` : this.formatDate(start);
|
|
163
213
|
}
|
|
164
214
|
if (this.modeValue === "multiple") {
|
|
215
|
+
// Deselecting the last date must clear the bound input: return ""
|
|
216
|
+
// (pushed to outlets), not null (skipped by pushToOutlets' guard).
|
|
217
|
+
if (!this.selectedDatesValue.length) return "";
|
|
165
218
|
return this.selectedDatesValue.map((d) => this.formatDate(this.parseDate(d))).join(", ");
|
|
166
219
|
}
|
|
167
220
|
const selected = this.selectedDate();
|
|
@@ -193,6 +246,7 @@ export default class extends Controller {
|
|
|
193
246
|
this.titleTarget.textContent = this.monthAndYear();
|
|
194
247
|
}
|
|
195
248
|
this.syncDropdowns();
|
|
249
|
+
this.syncNavButtons();
|
|
196
250
|
// innerHTML replacement drops focus — remember the focused day so
|
|
197
251
|
// keyboard selection/month hops don't strand focus on <body>.
|
|
198
252
|
const focusedDay = this.calendarTarget.contains(document.activeElement)
|
|
@@ -221,9 +275,10 @@ export default class extends Controller {
|
|
|
221
275
|
if (focused) stop.focus({ preventScroll: true });
|
|
222
276
|
}
|
|
223
277
|
|
|
224
|
-
// Arrow keys move by day/week,
|
|
225
|
-
//
|
|
226
|
-
//
|
|
278
|
+
// Arrow keys move by day/week, PageUp/PageDown by month (Shift: by year,
|
|
279
|
+
// per APG), Home/End jump within the rendered week row; crossing a month
|
|
280
|
+
// boundary re-renders the grid on the target month. Enter/Space activate
|
|
281
|
+
// natively (the days are real <button>s).
|
|
227
282
|
onKeydown(e) {
|
|
228
283
|
const STEPS = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 };
|
|
229
284
|
const day = e.target.closest?.("[data-day]");
|
|
@@ -233,6 +288,13 @@ export default class extends Controller {
|
|
|
233
288
|
if (e.key in STEPS) {
|
|
234
289
|
target = this.parseDate(day.dataset.day);
|
|
235
290
|
target.setDate(target.getDate() + STEPS[e.key]);
|
|
291
|
+
} else if (e.key === "PageUp" || e.key === "PageDown") {
|
|
292
|
+
const base = this.parseDate(day.dataset.day);
|
|
293
|
+
const months = (e.key === "PageUp" ? -1 : 1) * (e.shiftKey ? 12 : 1);
|
|
294
|
+
target = new Date(base.getFullYear(), base.getMonth() + months, 1);
|
|
295
|
+
// land on the same day-of-month, clamped to the target month's length
|
|
296
|
+
const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
|
|
297
|
+
target.setDate(Math.min(base.getDate(), lastDay));
|
|
236
298
|
} else if (e.key === "Home" || e.key === "End") {
|
|
237
299
|
const rowDays = [...day.closest("tr").querySelectorAll("[data-day]")];
|
|
238
300
|
target = this.parseDate(rowDays[e.key === "Home" ? 0 : rowDays.length - 1].dataset.day);
|
|
@@ -260,7 +322,15 @@ export default class extends Controller {
|
|
|
260
322
|
this.monthSelectTarget.value = String(this.viewDate().getMonth());
|
|
261
323
|
}
|
|
262
324
|
if (this.hasYearSelectTarget) {
|
|
263
|
-
|
|
325
|
+
const select = this.yearSelectTarget;
|
|
326
|
+
const year = this.viewDate().getFullYear();
|
|
327
|
+
select.value = String(year);
|
|
328
|
+
if (select.selectedIndex === -1 && select.options.length) {
|
|
329
|
+
// view year outside the option range — clamp to the nearest end
|
|
330
|
+
// instead of leaving the select blank
|
|
331
|
+
const years = [...select.options].map((o) => Number(o.value));
|
|
332
|
+
select.value = String(year < Math.min(...years) ? Math.min(...years) : Math.max(...years));
|
|
333
|
+
}
|
|
264
334
|
}
|
|
265
335
|
}
|
|
266
336
|
|
|
@@ -278,15 +348,22 @@ export default class extends Controller {
|
|
|
278
348
|
let cells = week.map((day) => this.renderDay(day)).join("");
|
|
279
349
|
if (this.weekNumbersValue) {
|
|
280
350
|
const weekNumber = this.isoWeekNumber(week[0]);
|
|
281
|
-
cells = `<td class="pk-calendar-weeknumber" role="
|
|
351
|
+
cells = `<td class="pk-calendar-weeknumber" role="rowheader">${weekNumber}</td>` + cells;
|
|
282
352
|
}
|
|
283
|
-
return `<tr class="pk-calendar-week">${cells}</tr>`;
|
|
353
|
+
return `<tr class="pk-calendar-week" role="row">${cells}</tr>`;
|
|
284
354
|
}
|
|
285
355
|
|
|
286
356
|
renderDay(day) {
|
|
287
357
|
const today = new Date();
|
|
288
358
|
const iso = this.isoDate(day);
|
|
289
|
-
const data = {
|
|
359
|
+
const data = {
|
|
360
|
+
day: iso,
|
|
361
|
+
dayDate: day.getDate(),
|
|
362
|
+
// full human-readable date for the button's accessible name — a bare
|
|
363
|
+
// day number ("14") is meaningless to a screen reader
|
|
364
|
+
dayLabel: day.toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric", year: "numeric" }),
|
|
365
|
+
state: this.dayState(day),
|
|
366
|
+
};
|
|
290
367
|
|
|
291
368
|
if (this.isDateDisabled(day)) {
|
|
292
369
|
return renderTemplate(this.disabledDateTemplateTarget.innerHTML, data);
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
# <template>s for the five day states (disabled / selected / today / current
|
|
3
3
|
# month / other month). The controller picks one per day and interpolates
|
|
4
|
-
# {{day}} / {{dayDate}} / {{state}} — state carries the extra
|
|
5
|
-
# classes (range-start / range-end / in-range / booked)
|
|
4
|
+
# {{day}} / {{dayDate}} / {{dayLabel}} / {{state}} — state carries the extra
|
|
5
|
+
# range/booked classes (range-start / range-end / in-range / booked), and
|
|
6
|
+
# dayLabel is the full human-readable date used as the button's accessible
|
|
7
|
+
# name. Per the APG date-grid pattern the <td> is the gridcell (and carries
|
|
8
|
+
# aria-selected); the <button> inside is a plain button. See calendar.rb.
|
|
6
9
|
class CalendarDays < BaseComponent
|
|
7
10
|
def initialize(**attrs)
|
|
8
11
|
@attrs = attrs
|
|
@@ -25,25 +28,24 @@ module PhlexKit
|
|
|
25
28
|
name: "day",
|
|
26
29
|
class: "pk-calendar-day disabled {{state}}",
|
|
27
30
|
disabled: true,
|
|
28
|
-
role: "gridcell",
|
|
29
31
|
tabindex: "-1",
|
|
30
32
|
type: "button",
|
|
31
|
-
aria_disabled: "true"
|
|
33
|
+
aria_disabled: "true",
|
|
34
|
+
aria_label: "{{dayLabel}}"
|
|
32
35
|
) { "{{dayDate}}" }
|
|
33
36
|
end
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
def render_selected_date_template
|
|
37
|
-
date_template("selectedDateTemplate") do
|
|
40
|
+
date_template("selectedDateTemplate", aria_selected: "true") do
|
|
38
41
|
button(
|
|
39
42
|
data_day: "{{day}}",
|
|
40
43
|
data_action: "click->phlex-kit--calendar#selectDay",
|
|
41
44
|
name: "day",
|
|
42
45
|
class: "pk-calendar-day selected {{state}}",
|
|
43
|
-
role: "gridcell",
|
|
44
46
|
tabindex: "0",
|
|
45
47
|
type: "button",
|
|
46
|
-
|
|
48
|
+
aria_label: "{{dayLabel}}"
|
|
47
49
|
) { "{{dayDate}}" }
|
|
48
50
|
end
|
|
49
51
|
end
|
|
@@ -55,9 +57,9 @@ module PhlexKit
|
|
|
55
57
|
data_action: "click->phlex-kit--calendar#selectDay",
|
|
56
58
|
name: "day",
|
|
57
59
|
class: "pk-calendar-day today {{state}}",
|
|
58
|
-
role: "gridcell",
|
|
59
60
|
tabindex: "-1",
|
|
60
|
-
type: "button"
|
|
61
|
+
type: "button",
|
|
62
|
+
aria_label: "{{dayLabel}}"
|
|
61
63
|
) { "{{dayDate}}" }
|
|
62
64
|
end
|
|
63
65
|
end
|
|
@@ -69,9 +71,9 @@ module PhlexKit
|
|
|
69
71
|
data_action: "click->phlex-kit--calendar#selectDay",
|
|
70
72
|
name: "day",
|
|
71
73
|
class: "pk-calendar-day {{state}}",
|
|
72
|
-
role: "gridcell",
|
|
73
74
|
tabindex: "-1",
|
|
74
|
-
type: "button"
|
|
75
|
+
type: "button",
|
|
76
|
+
aria_label: "{{dayLabel}}"
|
|
75
77
|
) { "{{dayDate}}" }
|
|
76
78
|
end
|
|
77
79
|
end
|
|
@@ -83,16 +85,16 @@ module PhlexKit
|
|
|
83
85
|
data_action: "click->phlex-kit--calendar#selectDay",
|
|
84
86
|
name: "day",
|
|
85
87
|
class: "pk-calendar-day other {{state}}",
|
|
86
|
-
role: "gridcell",
|
|
87
88
|
tabindex: "-1",
|
|
88
|
-
type: "button"
|
|
89
|
+
type: "button",
|
|
90
|
+
aria_label: "{{dayLabel}}"
|
|
89
91
|
) { "{{dayDate}}" }
|
|
90
92
|
end
|
|
91
93
|
end
|
|
92
94
|
|
|
93
|
-
def date_template(target, &block)
|
|
95
|
+
def date_template(target, **td_attrs, &block)
|
|
94
96
|
template(data: { phlex_kit__calendar_target: target }) do
|
|
95
|
-
td(class: "pk-calendar-cell", role: "
|
|
97
|
+
td(class: "pk-calendar-cell", role: "gridcell", **td_attrs, &block)
|
|
96
98
|
end
|
|
97
99
|
end
|
|
98
100
|
end
|
|
@@ -5,6 +5,12 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
5
5
|
// Connects to data-controller="phlex-kit--calendar-input"
|
|
6
6
|
export default class extends Controller {
|
|
7
7
|
setValue(value) {
|
|
8
|
+
if (this.element.value === value) return
|
|
8
9
|
this.element.value = value
|
|
10
|
+
// Programmatic .value assignment fires nothing — dispatch input/change so
|
|
11
|
+
// form frameworks, validators and Stimulus actions bound to the field see
|
|
12
|
+
// the calendar's write.
|
|
13
|
+
this.element.dispatchEvent(new Event("input", { bubbles: true }))
|
|
14
|
+
this.element.dispatchEvent(new Event("change", { bubbles: true }))
|
|
9
15
|
}
|
|
10
16
|
}
|
|
@@ -11,7 +11,11 @@ module PhlexKit
|
|
|
11
11
|
name: "next-month",
|
|
12
12
|
class: "pk-calendar-next",
|
|
13
13
|
aria: { label: "Go to next month" },
|
|
14
|
-
data: {
|
|
14
|
+
data: {
|
|
15
|
+
action: "click->phlex-kit--calendar#nextMonth",
|
|
16
|
+
# target so the controller can disable it at the max-date/to-year bound
|
|
17
|
+
phlex_kit__calendar_target: "nextButton"
|
|
18
|
+
}
|
|
15
19
|
}, @attrs)) do
|
|
16
20
|
icon
|
|
17
21
|
end
|
|
@@ -11,7 +11,11 @@ module PhlexKit
|
|
|
11
11
|
name: "previous-month",
|
|
12
12
|
class: "pk-calendar-prev",
|
|
13
13
|
aria: { label: "Go to previous month" },
|
|
14
|
-
data: {
|
|
14
|
+
data: {
|
|
15
|
+
action: "click->phlex-kit--calendar#prevMonth",
|
|
16
|
+
# target so the controller can disable it at the min-date/from-year bound
|
|
17
|
+
phlex_kit__calendar_target: "prevButton"
|
|
18
|
+
}
|
|
15
19
|
}, @attrs)) do
|
|
16
20
|
icon
|
|
17
21
|
end
|
|
@@ -12,7 +12,7 @@ module PhlexKit
|
|
|
12
12
|
def view_template
|
|
13
13
|
template(data: { phlex_kit__calendar_target: "weekdaysTemplate" }) do
|
|
14
14
|
thead(**@attrs) do
|
|
15
|
-
tr(class: "pk-calendar-weekdays-row") do
|
|
15
|
+
tr(class: "pk-calendar-weekdays-row", role: "row") do
|
|
16
16
|
th(scope: "col", class: "pk-calendar-weekday pk-calendar-weeknumber-head", aria: { label: "Week number" }) { "#" } if @week_numbers
|
|
17
17
|
DAYS.each { |day| render_day(day) }
|
|
18
18
|
end
|
|
@@ -23,7 +23,7 @@ module PhlexKit
|
|
|
23
23
|
data: {
|
|
24
24
|
controller: "phlex-kit--carousel",
|
|
25
25
|
phlex_kit__carousel_options_value: JSON.generate(default_options.merge(@options)),
|
|
26
|
-
action:
|
|
26
|
+
action: keyboard_action
|
|
27
27
|
}
|
|
28
28
|
}, @attrs), &)
|
|
29
29
|
end
|
|
@@ -33,5 +33,15 @@ module PhlexKit
|
|
|
33
33
|
def default_options
|
|
34
34
|
{ axis: (@orientation == :horizontal) ? "x" : "y" }
|
|
35
35
|
end
|
|
36
|
+
|
|
37
|
+
# Arrow keys follow the scroll axis: left/right for horizontal, up/down
|
|
38
|
+
# for vertical.
|
|
39
|
+
def keyboard_action
|
|
40
|
+
if @orientation == :vertical
|
|
41
|
+
"keydown.down->phlex-kit--carousel#scrollNext:prevent keydown.up->phlex-kit--carousel#scrollPrev:prevent"
|
|
42
|
+
else
|
|
43
|
+
"keydown.right->phlex-kit--carousel#scrollNext:prevent keydown.left->phlex-kit--carousel#scrollPrev:prevent"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
36
46
|
end
|
|
37
47
|
end
|
|
@@ -37,6 +37,12 @@ export default class extends Controller {
|
|
|
37
37
|
this._onPointerUp = this._onPointerUp.bind(this);
|
|
38
38
|
this._onClickCapture = this._onClickCapture.bind(this);
|
|
39
39
|
window.addEventListener("resize", this._onResize);
|
|
40
|
+
// window resize misses container-only resizes (flex/grid reflow,
|
|
41
|
+
// sidebar toggles) — observe the viewport itself.
|
|
42
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
43
|
+
this._resizeObserver = new ResizeObserver(() => this._applyTransform());
|
|
44
|
+
this._resizeObserver.observe(this.viewportTarget);
|
|
45
|
+
}
|
|
40
46
|
this.viewportTarget.addEventListener("pointerdown", this._onPointerDown);
|
|
41
47
|
this.viewportTarget.addEventListener("click", this._onClickCapture, true);
|
|
42
48
|
this._update();
|
|
@@ -44,8 +50,19 @@ export default class extends Controller {
|
|
|
44
50
|
|
|
45
51
|
disconnect() {
|
|
46
52
|
window.removeEventListener("resize", this._onResize);
|
|
53
|
+
this._resizeObserver?.disconnect();
|
|
54
|
+
this._resizeObserver = null;
|
|
47
55
|
this.viewportTarget.removeEventListener("pointerdown", this._onPointerDown);
|
|
48
56
|
this.viewportTarget.removeEventListener("click", this._onClickCapture, true);
|
|
57
|
+
// a disconnect mid-drag must drop the move/up listeners and drag state
|
|
58
|
+
this.viewportTarget.removeEventListener("pointermove", this._onPointerMove);
|
|
59
|
+
this.viewportTarget.removeEventListener("pointerup", this._onPointerUp);
|
|
60
|
+
this.viewportTarget.removeEventListener("pointercancel", this._onPointerUp);
|
|
61
|
+
if (this.drag) {
|
|
62
|
+
this.drag = null;
|
|
63
|
+
if (this.track) this.track.style.transition = "";
|
|
64
|
+
this.viewportTarget.classList.remove("dragging");
|
|
65
|
+
}
|
|
49
66
|
}
|
|
50
67
|
|
|
51
68
|
scrollNext() {
|
|
@@ -170,9 +187,19 @@ export default class extends Controller {
|
|
|
170
187
|
const slide = this.slides[index];
|
|
171
188
|
const first = this.slides[0];
|
|
172
189
|
if (!slide || !first) return 0;
|
|
173
|
-
|
|
190
|
+
const raw = this.options.axis === "y"
|
|
174
191
|
? slide.offsetTop - first.offsetTop
|
|
175
192
|
: slide.offsetLeft - first.offsetLeft;
|
|
193
|
+
// Never scroll past the content edge: with multi-up layouts the last
|
|
194
|
+
// slides' offsets overshoot the max scrollable offset (track − viewport).
|
|
195
|
+
return Math.min(Math.max(0, raw), this._maxOffset());
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
_maxOffset() {
|
|
199
|
+
const vertical = this.options.axis === "y";
|
|
200
|
+
const trackSize = vertical ? this.track.scrollHeight : this.track.scrollWidth;
|
|
201
|
+
const viewportSize = vertical ? this.viewportTarget.clientHeight : this.viewportTarget.clientWidth;
|
|
202
|
+
return Math.max(0, trackSize - viewportSize);
|
|
176
203
|
}
|
|
177
204
|
|
|
178
205
|
_slideSize() {
|
|
@@ -80,15 +80,22 @@ export default class extends Controller {
|
|
|
80
80
|
|
|
81
81
|
setDefaultColorsForChart() {
|
|
82
82
|
const Chart = this.chartLibrary()
|
|
83
|
-
|
|
84
|
-
Chart.defaults
|
|
85
|
-
|
|
83
|
+
// One computed-style resolve for all nine tokens (this runs on every
|
|
84
|
+
// connect; assigning identical values to Chart.defaults is free — charts
|
|
85
|
+
// read defaults at construction — so the style resolves are the only cost
|
|
86
|
+
// worth trimming).
|
|
87
|
+
const styles = getComputedStyle(document.documentElement)
|
|
88
|
+
const token = (name) => styles.getPropertyValue(`--${name}`).trim()
|
|
89
|
+
|
|
90
|
+
Chart.defaults.color = token("pk-muted") // font color
|
|
91
|
+
Chart.defaults.borderColor = token("pk-border") // border color
|
|
92
|
+
Chart.defaults.backgroundColor = token("pk-bg") // background color
|
|
86
93
|
|
|
87
94
|
// tooltip colors
|
|
88
|
-
Chart.defaults.plugins.tooltip.backgroundColor =
|
|
89
|
-
Chart.defaults.plugins.tooltip.borderColor =
|
|
90
|
-
Chart.defaults.plugins.tooltip.titleColor =
|
|
91
|
-
Chart.defaults.plugins.tooltip.bodyColor =
|
|
95
|
+
Chart.defaults.plugins.tooltip.backgroundColor = token("pk-surface")
|
|
96
|
+
Chart.defaults.plugins.tooltip.borderColor = token("pk-border")
|
|
97
|
+
Chart.defaults.plugins.tooltip.titleColor = token("pk-text")
|
|
98
|
+
Chart.defaults.plugins.tooltip.bodyColor = token("pk-muted")
|
|
92
99
|
Chart.defaults.plugins.tooltip.borderWidth = 1
|
|
93
100
|
|
|
94
101
|
// legend
|
|
@@ -99,7 +106,7 @@ export default class extends Controller {
|
|
|
99
106
|
Chart.defaults.plugins.legend.labels.borderRadius = 2
|
|
100
107
|
|
|
101
108
|
// shadcn look: hairline grid, no axis borders, muted ticks
|
|
102
|
-
Chart.defaults.scale.grid = { ...Chart.defaults.scale.grid, color:
|
|
109
|
+
Chart.defaults.scale.grid = { ...Chart.defaults.scale.grid, color: token("pk-border"), drawTicks: false }
|
|
103
110
|
Chart.defaults.scale.border = { ...Chart.defaults.scale.border, display: false }
|
|
104
111
|
}
|
|
105
112
|
|
|
@@ -108,14 +115,30 @@ export default class extends Controller {
|
|
|
108
115
|
this.initChart()
|
|
109
116
|
}
|
|
110
117
|
|
|
111
|
-
// The kit themes via <html data-theme="...">, not a class —
|
|
118
|
+
// The kit themes via <html data-theme="...">, not a class — but a class can
|
|
119
|
+
// re-theme too (.pk-dark, custom theme classes), so watch both. ANY root
|
|
120
|
+
// class mutation fires the observer though, so only rebuild when the
|
|
121
|
+
// resolved token values actually changed — rebuilding every chart on an
|
|
122
|
+
// unrelated class flip is the expensive path.
|
|
112
123
|
initThemeObserver() {
|
|
124
|
+
this._themeKey = this.currentThemeKey()
|
|
113
125
|
this.themeObserver = new MutationObserver(() => {
|
|
126
|
+
const key = this.currentThemeKey()
|
|
127
|
+
if (key === this._themeKey) return
|
|
128
|
+
this._themeKey = key
|
|
114
129
|
this.refreshChart()
|
|
115
130
|
})
|
|
116
131
|
this.themeObserver.observe(document.documentElement, { attributeFilter: ["data-theme", "class"] })
|
|
117
132
|
}
|
|
118
133
|
|
|
134
|
+
// Cheap fingerprint of the resolved theme: two tokens that differ between
|
|
135
|
+
// every light/dark pair. One style resolve per mutation vs a full chart
|
|
136
|
+
// rebuild.
|
|
137
|
+
currentThemeKey() {
|
|
138
|
+
const styles = getComputedStyle(document.documentElement)
|
|
139
|
+
return `${styles.getPropertyValue("--pk-bg")}|${styles.getPropertyValue("--pk-text")}`
|
|
140
|
+
}
|
|
141
|
+
|
|
119
142
|
mergeOptionsWithDefaults() {
|
|
120
143
|
return {
|
|
121
144
|
...this.optionsValue,
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
height: 1rem;
|
|
17
17
|
margin: 0;
|
|
18
18
|
border: 1px solid var(--pk-input);
|
|
19
|
-
|
|
19
|
+
/* Tracks the radius token — 4px at the default --pk-radius (0.625rem). */
|
|
20
|
+
border-radius: calc(var(--pk-radius) - 6px);
|
|
20
21
|
background: color-mix(in oklab, var(--pk-input) 30%, transparent);
|
|
21
22
|
cursor: pointer;
|
|
22
23
|
transition: border-color .15s ease, background-color .15s ease, box-shadow .15s ease;
|
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
.pk-clipboard-popover { position: absolute; bottom: calc(100% + .5rem); left: 50%; transform: translateX(-50%); z-index: 50; width: max-content; }
|
|
5
5
|
.pk-clipboard-popover-inner {
|
|
6
6
|
border: 1px solid var(--pk-border); border-radius: calc(var(--pk-radius) - 2px); background: var(--pk-surface);
|
|
7
|
-
color: var(--pk-text); padding: .125rem .5rem; font-size: .875rem; box-shadow: 0 8px 24px
|
|
7
|
+
color: var(--pk-text); padding: .125rem .5rem; font-size: .875rem; box-shadow: 0 8px 24px color-mix(in srgb, var(--pk-shadow-color) 30%, transparent);
|
|
8
8
|
}
|
|
@@ -5,6 +5,13 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
5
5
|
export default class extends Controller {
|
|
6
6
|
static targets = ["trigger", "source", "successPopover", "errorPopover"]
|
|
7
7
|
|
|
8
|
+
disconnect() {
|
|
9
|
+
// Don't leave the 1.5s hide timer running into a dead/recycled element
|
|
10
|
+
// (Turbo can detach and re-attach this subtree mid-flash).
|
|
11
|
+
clearTimeout(this.timer)
|
|
12
|
+
this.hideAll()
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
copy() {
|
|
9
16
|
const el = this.sourceTarget.children[0]
|
|
10
17
|
if (!el) { this.show(this.errorPopoverTarget); return }
|
|
@@ -9,7 +9,10 @@ module PhlexKit
|
|
|
9
9
|
@attrs = attrs
|
|
10
10
|
end
|
|
11
11
|
def view_template(&block)
|
|
12
|
-
|
|
12
|
+
# tabindex=0 + role=region: the block scrolls (overflow:auto), so it must
|
|
13
|
+
# be keyboard-focusable (WCAG 2.1.1 scrollable-region-focusable). Pass
|
|
14
|
+
# `aria: { label: ... }` to name the region for AT.
|
|
15
|
+
div(**mix({ class: "pk-codeblock", tabindex: "0", role: "region", data: { syntax: @syntax } }, @attrs)) do
|
|
13
16
|
pre do
|
|
14
17
|
if block
|
|
15
18
|
code(&block)
|
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
|
+
# Collapsed region of a Collapsible. Pass `open:` matching the parent
|
|
3
|
+
# Collapsible's so a closed region is hidden before JS (no flash-open, and
|
|
4
|
+
# no stuck-open region without JS); the controller toggles `.pk-hidden`
|
|
5
|
+
# from there. The default id lets the trigger's aria-controls point here.
|
|
2
6
|
class CollapsibleContent < BaseComponent
|
|
3
|
-
def initialize(
|
|
7
|
+
def initialize(open: false, id: nil, **attrs)
|
|
8
|
+
@open = open
|
|
9
|
+
@id = id || "pk-collapsible-content-#{SecureRandom.hex(4)}"
|
|
10
|
+
@attrs = attrs
|
|
11
|
+
end
|
|
12
|
+
|
|
4
13
|
def view_template(&)
|
|
5
|
-
div(**mix({
|
|
14
|
+
div(**mix({
|
|
15
|
+
class: @open ? "pk-collapsible-content" : "pk-collapsible-content pk-hidden",
|
|
16
|
+
id: @id,
|
|
17
|
+
data: { phlex_kit__collapsible_target: "content" }
|
|
18
|
+
}, @attrs), &)
|
|
6
19
|
end
|
|
7
20
|
end
|
|
8
21
|
end
|
|
@@ -2,20 +2,29 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
2
2
|
|
|
3
3
|
// Connects to data-controller="phlex-kit--collapsible"
|
|
4
4
|
export default class extends Controller {
|
|
5
|
-
static targets = ["content"]
|
|
5
|
+
static targets = ["trigger", "content"]
|
|
6
6
|
static values = { open: { type: Boolean, default: false } }
|
|
7
7
|
|
|
8
|
-
connect() {
|
|
8
|
+
connect() {
|
|
9
|
+
// The trigger renders aria-expanded from its own `open:` kwarg but can't
|
|
10
|
+
// know the content's id at render time — wire aria-controls here.
|
|
11
|
+
if (this.hasTriggerTarget && this.hasContentTarget && this.contentTarget.id) {
|
|
12
|
+
this.triggerTarget.setAttribute("aria-controls", this.contentTarget.id)
|
|
13
|
+
}
|
|
14
|
+
this.openValue ? this.open() : this.close()
|
|
15
|
+
}
|
|
9
16
|
toggle() { this.openValue = !this.openValue }
|
|
10
17
|
openValueChanged(isOpen) { isOpen ? this.open() : this.close() }
|
|
11
18
|
|
|
12
19
|
open() {
|
|
13
20
|
if (this.hasContentTarget) { this.contentTarget.classList.remove("pk-hidden"); this.openValue = true }
|
|
21
|
+
if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", "true")
|
|
14
22
|
this.element.dataset.state = "open";
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
close() {
|
|
18
26
|
if (this.hasContentTarget) { this.contentTarget.classList.add("pk-hidden"); this.openValue = false }
|
|
27
|
+
if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", "false")
|
|
19
28
|
this.element.dataset.state = "closed";
|
|
20
29
|
}
|
|
21
30
|
}
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
|
+
# Toggle for a Collapsible. Renders a plain wrapper carrying the click
|
|
3
|
+
# action — it deliberately has no role of its own, so nest a real focusable
|
|
4
|
+
# control (a <button>, e.g. PhlexKit::Button) inside it for keyboard
|
|
5
|
+
# operability and semantics, the same composition pattern as
|
|
6
|
+
# AttachmentTrigger. Pass `open:` matching the parent Collapsible's so
|
|
7
|
+
# aria-expanded is correct before JS; the controller keeps it in sync and
|
|
8
|
+
# wires aria-controls to the content's id in connect().
|
|
2
9
|
class CollapsibleTrigger < BaseComponent
|
|
3
|
-
def initialize(
|
|
10
|
+
def initialize(open: false, **attrs)
|
|
11
|
+
@open = open
|
|
12
|
+
@attrs = attrs
|
|
13
|
+
end
|
|
14
|
+
|
|
4
15
|
def view_template(&)
|
|
5
|
-
div(**mix({
|
|
16
|
+
div(**mix({
|
|
17
|
+
class: "pk-collapsible-trigger",
|
|
18
|
+
aria_expanded: @open ? "true" : "false",
|
|
19
|
+
data: { phlex_kit__collapsible_target: "trigger", action: "click->phlex-kit--collapsible#toggle" }
|
|
20
|
+
}, @attrs), &)
|
|
6
21
|
end
|
|
7
22
|
end
|
|
8
23
|
end
|