phlex_kit 0.10.0 → 0.11.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/app/assets/stylesheets/phlex_kit/phlex_kit.css +1 -0
- data/app/components/phlex_kit/accordion/accordion.css +7 -0
- data/app/components/phlex_kit/alert_dialog/alert_dialog_controller.js +3 -0
- data/app/components/phlex_kit/attachment/attachment_group.rb +6 -1
- data/app/components/phlex_kit/attachment/attachment_trigger.rb +5 -0
- data/app/components/phlex_kit/avatar/avatar_badge.rb +9 -2
- data/app/components/phlex_kit/badge/badge.css +1 -1
- data/app/components/phlex_kit/bubble/bubble.rb +4 -6
- data/app/components/phlex_kit/bubble/bubble_reactions.rb +4 -6
- data/app/components/phlex_kit/calendar/calendar_controller.js +33 -9
- data/app/components/phlex_kit/carousel/carousel.rb +3 -2
- data/app/components/phlex_kit/carousel/carousel_controller.js +23 -2
- data/app/components/phlex_kit/chart/chart.rb +8 -2
- data/app/components/phlex_kit/clipboard/clipboard_controller.js +9 -2
- data/app/components/phlex_kit/clipboard/clipboard_trigger.rb +1 -1
- data/app/components/phlex_kit/codeblock/codeblock.rb +7 -1
- data/app/components/phlex_kit/collapsible/collapsible.rb +4 -1
- data/app/components/phlex_kit/combobox/combobox_badge_trigger.rb +6 -2
- data/app/components/phlex_kit/combobox/combobox_controller.js +15 -1
- data/app/components/phlex_kit/combobox/combobox_input_trigger.rb +4 -1
- data/app/components/phlex_kit/combobox/combobox_list.rb +6 -2
- data/app/components/phlex_kit/combobox/combobox_toggle_all_checkbox.rb +4 -0
- data/app/components/phlex_kit/command/command_controller.js +6 -1
- data/app/components/phlex_kit/command/command_item.rb +4 -0
- data/app/components/phlex_kit/command/command_list.rb +6 -2
- data/app/components/phlex_kit/context_menu/context_menu_controller.js +25 -0
- data/app/components/phlex_kit/context_menu/context_menu_sub.rb +3 -1
- data/app/components/phlex_kit/context_menu/context_menu_sub_trigger.rb +3 -1
- data/app/components/phlex_kit/data_table/data_table.css +0 -10
- data/app/components/phlex_kit/data_table/data_table_column_toggle.rb +4 -1
- data/app/components/phlex_kit/data_table/data_table_controller.js +4 -0
- data/app/components/phlex_kit/date_picker/date_picker.rb +6 -0
- data/app/components/phlex_kit/dialog/dialog.css +3 -1
- data/app/components/phlex_kit/dialog/dialog_content.rb +1 -1
- data/app/components/phlex_kit/dialog/dialog_controller.js +13 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu.css +5 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_controller.js +27 -0
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub.rb +3 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub_trigger.rb +3 -1
- data/app/components/phlex_kit/field/field_separator.rb +1 -1
- data/app/components/phlex_kit/form_field/form_field_controller.js +22 -1
- data/app/components/phlex_kit/form_field/form_field_hint.rb +6 -2
- data/app/components/phlex_kit/hover_card/hover_card.css +4 -2
- data/app/components/phlex_kit/icon/icon.css +9 -0
- data/app/components/phlex_kit/icon/icon.rb +7 -1
- data/app/components/phlex_kit/input_group/input_group.css +0 -1
- data/app/components/phlex_kit/marker/marker.rb +9 -2
- data/app/components/phlex_kit/masked_input/masked_input_controller.js +20 -14
- data/app/components/phlex_kit/menubar/menubar_controller.js +6 -0
- data/app/components/phlex_kit/message/message.rb +5 -0
- data/app/components/phlex_kit/message_scroller/message_scroller_controller.js +3 -0
- data/app/components/phlex_kit/native_select/native_select.rb +12 -3
- data/app/components/phlex_kit/navigation_menu/navigation_menu.rb +4 -1
- data/app/components/phlex_kit/pagination/pagination.rb +10 -2
- data/app/components/phlex_kit/pagination/pagination_next.rb +3 -1
- data/app/components/phlex_kit/pagination/pagination_previous.rb +3 -1
- data/app/components/phlex_kit/popover/popover_controller.js +23 -1
- data/app/components/phlex_kit/progress/progress.css +8 -0
- data/app/components/phlex_kit/progress/progress.rb +14 -5
- data/app/components/phlex_kit/resizable/resizable_controller.js +3 -0
- data/app/components/phlex_kit/scroll_area/scroll_area.rb +5 -2
- data/app/components/phlex_kit/select/select.css +5 -2
- data/app/components/phlex_kit/select/select_controller.js +27 -4
- data/app/components/phlex_kit/separator/separator.rb +5 -1
- data/app/components/phlex_kit/sheet/sheet.css +4 -2
- data/app/components/phlex_kit/sheet/sheet_content_controller.js +4 -0
- data/app/components/phlex_kit/sheet/sheet_controller.js +15 -0
- data/app/components/phlex_kit/sidebar/sidebar_menu_button.rb +9 -1
- data/app/components/phlex_kit/sidebar/sidebar_menu_sub_button.rb +8 -1
- data/app/components/phlex_kit/slider/slider.css +15 -0
- data/app/components/phlex_kit/switch/switch.css +5 -0
- data/app/components/phlex_kit/table/table.css +2 -1
- data/app/components/phlex_kit/table/table.rb +5 -2
- data/app/components/phlex_kit/tabs/tabs_content.rb +4 -2
- data/app/components/phlex_kit/tabs/tabs_list.rb +7 -2
- data/app/components/phlex_kit/tabs/tabs_trigger.rb +11 -2
- data/app/components/phlex_kit/theme_toggle/theme_toggle_controller.js +14 -0
- data/app/components/phlex_kit/toast/toast.css +2 -1
- data/app/components/phlex_kit/toast/toast_action.rb +4 -1
- data/app/components/phlex_kit/toast/toast_controller.js +7 -4
- data/app/components/phlex_kit/toast/toast_region.rb +19 -9
- data/app/components/phlex_kit/toast/toaster_controller.js +15 -7
- data/app/components/phlex_kit/toggle_group/toggle_group.rb +1 -1
- data/app/components/phlex_kit/toggle_group/toggle_group_controller.js +5 -1
- data/app/components/phlex_kit/toggle_group/toggle_group_item.rb +9 -1
- data/app/components/phlex_kit/tooltip/tooltip.css +5 -1
- data/app/components/phlex_kit/tooltip/tooltip_controller.js +7 -1
- data/app/components/phlex_kit/typography/heading.rb +7 -1
- data/app/components/phlex_kit/typography/list.rb +22 -0
- data/app/components/phlex_kit/typography/text.rb +7 -1
- data/lib/phlex_kit/base_component.rb +13 -0
- data/lib/phlex_kit/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: caadd4e868a63939f22013699b89f9b1557dfa9569022301602c0f1cb07020f0
|
|
4
|
+
data.tar.gz: fa3ba42e9566f171e1fdee7ddcbfc475397bf404152031adbca2fd0cacbbda61
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 76304ba104026fe236758b3fa93014b516ec84e65e6baefd307853da30bb7dc9d38d1f6fff0d2c7639ede3ee9116642beed8a615c24963ea3e0462c0ca89656a
|
|
7
|
+
data.tar.gz: 0c8d0dfd56d4e4b873b35149dd7c362010c97a02a4642de0b72d80606301be27dea505849753aa8754e081c38e49484b3358524267d3df497a12b8c5d9e9ccd0
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
@import url("form/form.css");
|
|
45
45
|
@import url("form_field/form_field.css");
|
|
46
46
|
@import url("hover_card/hover_card.css");
|
|
47
|
+
@import url("icon/icon.css");
|
|
47
48
|
@import url("input/input.css");
|
|
48
49
|
@import url("input_group/input_group.css");
|
|
49
50
|
@import url("input_otp/input_otp.css");
|
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
background: none; border: 0; cursor: pointer; color: inherit; transition: color .15s ease, background-color .15s ease, text-decoration-color .15s ease;
|
|
10
10
|
}
|
|
11
11
|
.pk-accordion-trigger:hover { text-decoration: underline; }
|
|
12
|
+
/* background/border are stripped above — reinstate the kit focus ring or
|
|
13
|
+
keyboard focus falls back to the UA outline. */
|
|
14
|
+
.pk-accordion-trigger:focus-visible {
|
|
15
|
+
outline: none;
|
|
16
|
+
border-radius: calc(var(--pk-radius) - 2px);
|
|
17
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
18
|
+
}
|
|
12
19
|
.pk-accordion-trigger-label { flex: 1; }
|
|
13
20
|
.pk-accordion-icon { display: inline-flex; opacity: .5; transition: transform .2s ease; }
|
|
14
21
|
.pk-accordion-icon svg { width: 1rem; height: 1rem; }
|
|
@@ -52,6 +52,9 @@ export default class extends Controller {
|
|
|
52
52
|
// Double-open guard: Enter+click (or a double-click) on the trigger must
|
|
53
53
|
// not stack a second clone over the first.
|
|
54
54
|
if (this.clone?.isConnected) return;
|
|
55
|
+
// open: is one-shot — the reflected value sits in the Turbo snapshot, so
|
|
56
|
+
// a cache-restored reconnect would re-open a dialog the user dismissed.
|
|
57
|
+
this.openValue = false;
|
|
55
58
|
document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML);
|
|
56
59
|
this.clone = document.body.lastElementChild;
|
|
57
60
|
}
|
|
@@ -11,9 +11,14 @@ module PhlexKit
|
|
|
11
11
|
div(**mix({
|
|
12
12
|
class: "pk-attachment-group",
|
|
13
13
|
role: "group",
|
|
14
|
+
# at-start + at-end = no edge fades. Server-rendered so a group that
|
|
15
|
+
# FITS shows no false "more content" fade pre-hydration / with JS off;
|
|
16
|
+
# the controller's first update() removes them when there's overflow.
|
|
14
17
|
data: {
|
|
15
18
|
controller: "phlex-kit--scroll-fade",
|
|
16
|
-
phlex_kit__scroll_fade_axis_value: "x"
|
|
19
|
+
phlex_kit__scroll_fade_axis_value: "x",
|
|
20
|
+
at_start: "",
|
|
21
|
+
at_end: ""
|
|
17
22
|
}
|
|
18
23
|
}, @attrs), &)
|
|
19
24
|
end
|
|
@@ -4,8 +4,13 @@ module PhlexKit
|
|
|
4
4
|
# stay independently clickable. Give it an aria-label; pass `as: :a` +
|
|
5
5
|
# `href:` for a link. See attachment.rb.
|
|
6
6
|
class AttachmentTrigger < BaseComponent
|
|
7
|
+
AS_TAGS = %i[button a].freeze
|
|
8
|
+
|
|
7
9
|
def initialize(as: :button, href: nil, **attrs)
|
|
8
10
|
@as = as.to_sym
|
|
11
|
+
unless AS_TAGS.include?(@as)
|
|
12
|
+
raise ArgumentError, "unknown AttachmentTrigger as: #{@as.inspect} (use one of #{AS_TAGS.join(", ")})"
|
|
13
|
+
end
|
|
9
14
|
@href = href
|
|
10
15
|
@attrs = attrs
|
|
11
16
|
end
|
|
@@ -4,12 +4,19 @@ module PhlexKit
|
|
|
4
4
|
# svg glyph (hidden automatically on xs/sm avatars). Recolor via style/class
|
|
5
5
|
# — defaults to the brand fill. See avatar.rb.
|
|
6
6
|
class AvatarBadge < BaseComponent
|
|
7
|
-
|
|
7
|
+
# label: gives the color-only presence dot an accessible alternative
|
|
8
|
+
# (a .pk-sr-only span, like AttachmentAction's). No default — a purely
|
|
9
|
+
# decorative dot stays silent.
|
|
10
|
+
def initialize(label: nil, **attrs)
|
|
11
|
+
@label = label
|
|
8
12
|
@attrs = attrs
|
|
9
13
|
end
|
|
10
14
|
|
|
11
15
|
def view_template(&block)
|
|
12
|
-
span(**mix({ class: "pk-avatar-badge" }, @attrs)
|
|
16
|
+
span(**mix({ class: "pk-avatar-badge" }, @attrs)) do
|
|
17
|
+
span(class: "pk-sr-only") { @label } if @label
|
|
18
|
+
block&.call
|
|
19
|
+
end
|
|
13
20
|
end
|
|
14
21
|
end
|
|
15
22
|
end
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
overflow: hidden;
|
|
16
16
|
white-space: nowrap;
|
|
17
17
|
height: 1.25rem;
|
|
18
|
-
border-radius:
|
|
18
|
+
border-radius: 999px; /* fully-round pill (matches avatar/progress), not a --pk-radius derivative */
|
|
19
19
|
border: 1px solid transparent;
|
|
20
20
|
padding: .125rem .5rem;
|
|
21
21
|
font-size: .75rem;
|
|
@@ -3,13 +3,11 @@ module PhlexKit
|
|
|
3
3
|
# the child BubbleContent via a data-slot selector; align: :end flips it to the
|
|
4
4
|
# trailing edge. Compose Bubble > BubbleContent (+ BubbleReactions).
|
|
5
5
|
class Bubble < BaseComponent
|
|
6
|
-
VARIANTS = %i[default secondary muted tinted outline ghost destructive].freeze
|
|
7
|
-
ALIGNS = %i[start end].freeze
|
|
6
|
+
VARIANTS = %i[default secondary muted tinted outline ghost destructive].to_h { |v| [ v, v ] }.freeze
|
|
7
|
+
ALIGNS = %i[start end].to_h { |v| [ v, v ] }.freeze
|
|
8
8
|
def initialize(variant: :default, align: :start, **attrs)
|
|
9
|
-
@variant = variant.to_sym
|
|
10
|
-
|
|
11
|
-
@align = align.to_sym
|
|
12
|
-
raise KeyError, "unknown Bubble align #{@align}" unless ALIGNS.include?(@align)
|
|
9
|
+
@variant = fetch_option(VARIANTS, variant.to_sym, :variant)
|
|
10
|
+
@align = fetch_option(ALIGNS, align.to_sym, :align)
|
|
13
11
|
@attrs = attrs
|
|
14
12
|
end
|
|
15
13
|
def view_template(&)
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
class BubbleReactions < BaseComponent
|
|
3
|
-
SIDES = %i[top bottom].freeze
|
|
4
|
-
ALIGNS = %i[start end].freeze
|
|
3
|
+
SIDES = %i[top bottom].to_h { |v| [ v, v ] }.freeze
|
|
4
|
+
ALIGNS = %i[start end].to_h { |v| [ v, v ] }.freeze
|
|
5
5
|
def initialize(side: :bottom, align: :end, **attrs)
|
|
6
|
-
@side = side.to_sym
|
|
7
|
-
|
|
8
|
-
@align = align.to_sym
|
|
9
|
-
raise KeyError, "unknown BubbleReactions align #{@align}" unless ALIGNS.include?(@align)
|
|
6
|
+
@side = fetch_option(SIDES, side.to_sym, :side)
|
|
7
|
+
@align = fetch_option(ALIGNS, align.to_sym, :align)
|
|
10
8
|
@attrs = attrs
|
|
11
9
|
end
|
|
12
10
|
def view_template(&)
|
|
@@ -159,7 +159,9 @@ export default class extends Controller {
|
|
|
159
159
|
|
|
160
160
|
toggleMultipleDay(iso) {
|
|
161
161
|
const day = this.isoDate(this.parseDate(iso));
|
|
162
|
-
|
|
162
|
+
// Normalized copy: server-supplied entries may be datetime strings
|
|
163
|
+
// (Time#to_s) — indexOf against the bare isoDate must still match.
|
|
164
|
+
const current = this.normalizeDates(this.selectedDatesValue);
|
|
163
165
|
const index = current.indexOf(day);
|
|
164
166
|
if (index >= 0) {
|
|
165
167
|
current.splice(index, 1);
|
|
@@ -303,8 +305,18 @@ export default class extends Controller {
|
|
|
303
305
|
}
|
|
304
306
|
e.preventDefault();
|
|
305
307
|
|
|
306
|
-
|
|
307
|
-
if (this.isDateDisabled(iso))
|
|
308
|
+
let iso = this.isoDate(target);
|
|
309
|
+
if (this.isDateDisabled(iso)) {
|
|
310
|
+
// A min/max boundary holds focus in place; an isolated booked date is
|
|
311
|
+
// SKIPPED in the travel direction (APG date-grid) — arrows only, the
|
|
312
|
+
// month/row jumps (PageUp/Home/End) still stay put.
|
|
313
|
+
if (!(e.key in STEPS) || this.outOfRange(target)) return;
|
|
314
|
+
do {
|
|
315
|
+
target.setDate(target.getDate() + STEPS[e.key]);
|
|
316
|
+
if (this.outOfRange(target)) return;
|
|
317
|
+
} while (this.isBooked(target));
|
|
318
|
+
iso = this.isoDate(target);
|
|
319
|
+
}
|
|
308
320
|
|
|
309
321
|
if (target.getMonth() !== this.viewDate().getMonth() || target.getFullYear() !== this.viewDate().getFullYear()) {
|
|
310
322
|
this.viewDateValue = iso; // re-renders the grid on the target month
|
|
@@ -412,7 +424,7 @@ export default class extends Controller {
|
|
|
412
424
|
);
|
|
413
425
|
}
|
|
414
426
|
if (this.modeValue === "multiple") {
|
|
415
|
-
return this.selectedDatesValue.includes(this.isoDate(day));
|
|
427
|
+
return this.normalizeDates(this.selectedDatesValue).includes(this.isoDate(day));
|
|
416
428
|
}
|
|
417
429
|
const selectedDate = this.selectedDate();
|
|
418
430
|
return selectedDate && day.toDateString() === selectedDate.toDateString();
|
|
@@ -428,7 +440,17 @@ export default class extends Controller {
|
|
|
428
440
|
}
|
|
429
441
|
|
|
430
442
|
isBooked(day) {
|
|
431
|
-
return this.disabledDatesValue.includes(this.isoDate(day));
|
|
443
|
+
return this.normalizeDates(this.disabledDatesValue).includes(this.isoDate(day));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// disabled_dates / selected_dates entries may be Time#to_s or datetime ISO
|
|
447
|
+
// strings — every other date input tolerates those via parseDate, so the
|
|
448
|
+
// membership arrays must be normalized the same way (exact string equality
|
|
449
|
+
// silently no-oped for anything but bare yyyy-MM-dd).
|
|
450
|
+
normalizeDates(values) {
|
|
451
|
+
return values
|
|
452
|
+
.map((d) => { const parsed = this.parseDate(d); return parsed ? this.isoDate(parsed) : null; })
|
|
453
|
+
.filter(Boolean);
|
|
432
454
|
}
|
|
433
455
|
|
|
434
456
|
monthAndYear() {
|
|
@@ -575,14 +597,16 @@ export default class extends Controller {
|
|
|
575
597
|
isDateDisabled(date) {
|
|
576
598
|
const candidate = this.parseDate(date);
|
|
577
599
|
if (!candidate) return false;
|
|
600
|
+
return this.outOfRange(candidate) || this.isBooked(candidate);
|
|
601
|
+
}
|
|
578
602
|
|
|
603
|
+
// min/max bounds only (not booked entries) — the keyboard skip needs to
|
|
604
|
+
// distinguish a hard boundary from a hole in the middle of the grid.
|
|
605
|
+
outOfRange(candidate) {
|
|
579
606
|
const minDate = this.minDate();
|
|
580
607
|
if (minDate && this.startOfDay(candidate) < this.startOfDay(minDate)) return true;
|
|
581
|
-
|
|
582
608
|
const maxDate = this.maxDate();
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
return this.disabledDatesValue.includes(this.isoDate(candidate));
|
|
609
|
+
return !!(maxDate && this.startOfDay(candidate) > this.startOfDay(maxDate));
|
|
586
610
|
}
|
|
587
611
|
|
|
588
612
|
parseDate(value) {
|
|
@@ -35,12 +35,13 @@ module PhlexKit
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# Arrow keys follow the scroll axis: left/right for horizontal, up/down
|
|
38
|
-
# for vertical.
|
|
38
|
+
# for vertical. Horizontal arrows route through keyNext/keyPrev, which
|
|
39
|
+
# flip in RTL (the next slide sits to the physical left there).
|
|
39
40
|
def keyboard_action
|
|
40
41
|
if @orientation == :vertical
|
|
41
42
|
"keydown.down->phlex-kit--carousel#scrollNext:prevent keydown.up->phlex-kit--carousel#scrollPrev:prevent"
|
|
42
43
|
else
|
|
43
|
-
"keydown.right->phlex-kit--carousel#
|
|
44
|
+
"keydown.right->phlex-kit--carousel#keyNext:prevent keydown.left->phlex-kit--carousel#keyPrev:prevent"
|
|
44
45
|
end
|
|
45
46
|
end
|
|
46
47
|
end
|
|
@@ -73,6 +73,16 @@ export default class extends Controller {
|
|
|
73
73
|
this._goTo(this.index - 1);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// Physical arrow keys (horizontal axis only): in RTL the next slide sits
|
|
77
|
+
// to the physical LEFT, so the right arrow must travel toward "previous".
|
|
78
|
+
keyNext() {
|
|
79
|
+
this._rtl() ? this.scrollPrev() : this.scrollNext();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
keyPrev() {
|
|
83
|
+
this._rtl() ? this.scrollNext() : this.scrollPrev();
|
|
84
|
+
}
|
|
85
|
+
|
|
76
86
|
get slides() {
|
|
77
87
|
return this.track ? Array.from(this.track.children) : [];
|
|
78
88
|
}
|
|
@@ -129,6 +139,8 @@ export default class extends Controller {
|
|
|
129
139
|
const drag = this.drag;
|
|
130
140
|
if (!drag) return;
|
|
131
141
|
drag.delta = this._pointerPos(e) - drag.start;
|
|
142
|
+
// Normalize to scroll-axis space: in RTL a rightward drag reveals next.
|
|
143
|
+
if (this._rtl()) drag.delta = -drag.delta;
|
|
132
144
|
if (!drag.moved && Math.abs(drag.delta) > 5) {
|
|
133
145
|
drag.moved = true;
|
|
134
146
|
this.track.style.transition = "none";
|
|
@@ -183,13 +195,22 @@ export default class extends Controller {
|
|
|
183
195
|
return this.options.axis === "y" ? e.clientY : e.clientX;
|
|
184
196
|
}
|
|
185
197
|
|
|
198
|
+
// In RTL the horizontal track lays slides right-to-left: later slides have
|
|
199
|
+
// SMALLER offsetLeft and the track must translate in +x to reveal them.
|
|
200
|
+
// All geometry below works in a direction-neutral "offset along the scroll
|
|
201
|
+
// axis" space; _rtl() flips the signs at the physical boundaries only.
|
|
202
|
+
_rtl() {
|
|
203
|
+
return this.options.axis !== "y" && getComputedStyle(this.element).direction === "rtl";
|
|
204
|
+
}
|
|
205
|
+
|
|
186
206
|
_offsetOf(index) {
|
|
187
207
|
const slide = this.slides[index];
|
|
188
208
|
const first = this.slides[0];
|
|
189
209
|
if (!slide || !first) return 0;
|
|
190
|
-
|
|
210
|
+
let raw = this.options.axis === "y"
|
|
191
211
|
? slide.offsetTop - first.offsetTop
|
|
192
212
|
: slide.offsetLeft - first.offsetLeft;
|
|
213
|
+
if (this._rtl()) raw = -raw;
|
|
193
214
|
// Never scroll past the content edge: with multi-up layouts the last
|
|
194
215
|
// slides' offsets overshoot the max scrollable offset (track − viewport).
|
|
195
216
|
return Math.min(Math.max(0, raw), this._maxOffset());
|
|
@@ -211,6 +232,6 @@ export default class extends Controller {
|
|
|
211
232
|
_translateTo(offset) {
|
|
212
233
|
this.track.style.transform = this.options.axis === "y"
|
|
213
234
|
? `translate3d(0, ${-offset}px, 0)`
|
|
214
|
-
: `translate3d(${-offset}px, 0, 0)`;
|
|
235
|
+
: `translate3d(${this._rtl() ? offset : -offset}px, 0, 0)`;
|
|
215
236
|
}
|
|
216
237
|
}
|
|
@@ -19,13 +19,19 @@ module PhlexKit
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def view_template(&)
|
|
22
|
-
|
|
22
|
+
base = {
|
|
23
23
|
class: "pk-chart",
|
|
24
|
+
# Canvas content is invisible to AT — announce it as an image with at
|
|
25
|
+
# least a generic name; pass aria: { label: "…" } (or block fallback
|
|
26
|
+
# content) to describe the actual data.
|
|
27
|
+
role: "img",
|
|
24
28
|
data: {
|
|
25
29
|
controller: "phlex-kit--chart",
|
|
26
30
|
phlex_kit__chart_options_value: JSON.generate(@options)
|
|
27
31
|
}
|
|
28
|
-
}
|
|
32
|
+
}
|
|
33
|
+
base[:aria] = { label: "Chart" } unless aria_labelled?
|
|
34
|
+
canvas(**mix(base, @attrs), &)
|
|
29
35
|
end
|
|
30
36
|
end
|
|
31
37
|
end
|
|
@@ -3,7 +3,8 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
3
3
|
// Connects to data-controller="phlex-kit--clipboard". Copies the source text and
|
|
4
4
|
// flashes a success/error popover (CSS-positioned, no @floating-ui). Ported from ruby_ui.
|
|
5
5
|
export default class extends Controller {
|
|
6
|
-
|
|
6
|
+
// No "trigger" target: the click->#copy action alone wires the trigger.
|
|
7
|
+
static targets = ["source", "successPopover", "errorPopover"]
|
|
7
8
|
|
|
8
9
|
connect() {
|
|
9
10
|
// Turbo snapshots at turbo:before-cache, BEFORE disconnect — hide the
|
|
@@ -28,10 +29,16 @@ export default class extends Controller {
|
|
|
28
29
|
// A source usually wraps one element (input/code/…), but bare text is
|
|
29
30
|
// legal too — fall back to the source's own text instead of erroring.
|
|
30
31
|
const el = this.sourceTarget.children[0]
|
|
32
|
+
// TEXTAREA reads .value like INPUT — innerText would return the initial
|
|
33
|
+
// content, not the user-edited value.
|
|
31
34
|
const text = el
|
|
32
|
-
? (el.tagName
|
|
35
|
+
? (["INPUT", "TEXTAREA"].includes(el.tagName) ? el.value : el.innerText)
|
|
33
36
|
: this.sourceTarget.textContent.trim()
|
|
34
37
|
if (!text) { this.show(this.errorPopoverTarget); return }
|
|
38
|
+
// navigator.clipboard is undefined in insecure contexts (plain-HTTP
|
|
39
|
+
// hosts, some webviews) — that's the error popover's job, not an
|
|
40
|
+
// unhandled TypeError's.
|
|
41
|
+
if (!navigator.clipboard) { this.show(this.errorPopoverTarget); return }
|
|
35
42
|
navigator.clipboard.writeText(text)
|
|
36
43
|
.then(() => this.show(this.successPopoverTarget))
|
|
37
44
|
.catch(() => this.show(this.errorPopoverTarget))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
class ClipboardTrigger < BaseComponent
|
|
3
3
|
def initialize(**attrs) = (@attrs = attrs)
|
|
4
|
-
def view_template(&) = div(**mix({ class: "pk-clipboard-trigger", data: {
|
|
4
|
+
def view_template(&) = div(**mix({ class: "pk-clipboard-trigger", data: { action: "click->phlex-kit--clipboard#copy" } }, @attrs), &)
|
|
5
5
|
end
|
|
6
6
|
end
|
|
@@ -12,7 +12,13 @@ module PhlexKit
|
|
|
12
12
|
# tabindex=0 + role=region: the block scrolls (overflow:auto), so it must
|
|
13
13
|
# be keyboard-focusable (WCAG 2.1.1 scrollable-region-focusable). Pass
|
|
14
14
|
# `aria: { label: ... }` to name the region for AT.
|
|
15
|
-
|
|
15
|
+
base = { class: "pk-codeblock", tabindex: "0", role: "region", data: { syntax: @syntax } }
|
|
16
|
+
# A region landmark without a name is an axe violation — default one
|
|
17
|
+
# from the syntax; a caller aria: { label: } replaces it (never fused).
|
|
18
|
+
unless aria_labelled?
|
|
19
|
+
base[:aria] = { label: @syntax ? "#{@syntax} code" : "Code" }
|
|
20
|
+
end
|
|
21
|
+
div(**mix(base, @attrs)) do
|
|
16
22
|
pre do
|
|
17
23
|
if block
|
|
18
24
|
code(&block)
|
|
@@ -7,7 +7,10 @@ module PhlexKit
|
|
|
7
7
|
@attrs = attrs
|
|
8
8
|
end
|
|
9
9
|
def view_template(&)
|
|
10
|
-
|
|
10
|
+
# data-state is the CSS styling hook (chevron rotation) — it must be
|
|
11
|
+
# server-rendered or an open: true collapsible renders a closed-pointing
|
|
12
|
+
# chevron until Stimulus connects (and always, with JS off).
|
|
13
|
+
div(**mix({ class: "pk-collapsible", data: { controller: "phlex-kit--collapsible", phlex_kit__collapsible_open_value: @open, state: @open ? "open" : "closed" } }, @attrs), &)
|
|
11
14
|
end
|
|
12
15
|
end
|
|
13
16
|
end
|
|
@@ -30,7 +30,8 @@ module PhlexKit
|
|
|
30
30
|
"keydown.down->phlex-kit--combobox#keyDownPressed",
|
|
31
31
|
"keydown.up->phlex-kit--combobox#keyUpPressed",
|
|
32
32
|
"keydown.enter->phlex-kit--combobox#keyEnterPressed",
|
|
33
|
-
|
|
33
|
+
# no :prevent — closePopover only swallows Escape while open.
|
|
34
|
+
"keydown.esc->phlex-kit--combobox#closePopover"
|
|
34
35
|
].join(" ")
|
|
35
36
|
}
|
|
36
37
|
}, @attrs)) do
|
|
@@ -49,7 +50,10 @@ module PhlexKit
|
|
|
49
50
|
action: [
|
|
50
51
|
"keyup->phlex-kit--combobox#filterItems",
|
|
51
52
|
"input->phlex-kit--combobox#filterItems",
|
|
52
|
-
"
|
|
53
|
+
# plain keydown — "backspace" is not a Stimulus key filter
|
|
54
|
+
# (using it throws "unknown key filter" on every keystroke and
|
|
55
|
+
# the handler never ran); the method guards on e.key itself.
|
|
56
|
+
"keydown->phlex-kit--combobox#handleBadgeInputBackspace"
|
|
53
57
|
].join(" ")
|
|
54
58
|
}
|
|
55
59
|
)
|
|
@@ -45,6 +45,11 @@ export default class extends Controller {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
connect() {
|
|
48
|
+
// A Turbo snapshot serializes aria-expanded / aria-activedescendant even
|
|
49
|
+
// though :popover-open does not survive the restore — normalize here so
|
|
50
|
+
// a restored page doesn't announce an open listbox over a closed combobox.
|
|
51
|
+
this.setExpanded(false)
|
|
52
|
+
this.clearActiveDescendant()
|
|
48
53
|
this.generateItemIds()
|
|
49
54
|
this.updateTriggerContent()
|
|
50
55
|
}
|
|
@@ -217,6 +222,8 @@ export default class extends Controller {
|
|
|
217
222
|
}
|
|
218
223
|
|
|
219
224
|
handleBadgeInputBackspace(e) {
|
|
225
|
+
// Bound as a bare keydown: "backspace" is not a Stimulus key filter.
|
|
226
|
+
if (e.key !== "Backspace") return
|
|
220
227
|
if (this.badgeInputTarget.value !== "") return
|
|
221
228
|
|
|
222
229
|
const checked = this.checkedInputs()
|
|
@@ -278,7 +285,14 @@ export default class extends Controller {
|
|
|
278
285
|
}
|
|
279
286
|
}
|
|
280
287
|
|
|
281
|
-
closePopover() {
|
|
288
|
+
closePopover(e) {
|
|
289
|
+
// Escape must keep its browser default (e.g. cancelling an enclosing
|
|
290
|
+
// <dialog>) when the popover is already closed — only swallow the key
|
|
291
|
+
// when this close actually consumes it.
|
|
292
|
+
if (e?.type === "keydown") {
|
|
293
|
+
if (!this.isOpen()) return
|
|
294
|
+
e.preventDefault()
|
|
295
|
+
}
|
|
282
296
|
// Hiding the popover while focus is inside it (search input, option) drops
|
|
283
297
|
// focus to <body>; hand it back to the trigger. Checked BEFORE hiding, and
|
|
284
298
|
// only when focus would actually be orphaned — a click elsewhere on the
|
|
@@ -36,7 +36,10 @@ module PhlexKit
|
|
|
36
36
|
"keydown.down->phlex-kit--combobox#keyDownPressed",
|
|
37
37
|
"keydown.up->phlex-kit--combobox#keyUpPressed",
|
|
38
38
|
"keydown.enter->phlex-kit--combobox#keyEnterPressed",
|
|
39
|
-
|
|
39
|
+
# no :prevent — closePopover only swallows Escape while the
|
|
40
|
+
# popover is open, so a closed field keeps the key's default
|
|
41
|
+
# (e.g. cancelling an enclosing <dialog>).
|
|
42
|
+
"keydown.esc->phlex-kit--combobox#closePopover"
|
|
40
43
|
].join(" ")
|
|
41
44
|
}
|
|
42
45
|
}, @attrs)) do
|
|
@@ -3,13 +3,17 @@ module PhlexKit
|
|
|
3
3
|
# role and an id the controller points the combobox element's aria-controls
|
|
4
4
|
# at (and derives option ids from). See combobox.rb.
|
|
5
5
|
class ComboboxList < BaseComponent
|
|
6
|
-
|
|
6
|
+
# id: is a named kwarg (not left in **attrs) because `mix` would *merge* a
|
|
7
|
+
# caller id with the generated one into an invalid two-token id, breaking
|
|
8
|
+
# aria-controls (the documented list_id: wiring) and derived option ids.
|
|
9
|
+
def initialize(id: nil, **attrs)
|
|
10
|
+
@id = id || "pk-combobox-list-#{SecureRandom.hex(4)}"
|
|
7
11
|
@attrs = attrs
|
|
8
12
|
end
|
|
9
13
|
|
|
10
14
|
def view_template(&)
|
|
11
15
|
div(**mix({
|
|
12
|
-
id:
|
|
16
|
+
id: @id,
|
|
13
17
|
class: "pk-combobox-list",
|
|
14
18
|
role: "listbox",
|
|
15
19
|
data: { phlex_kit__combobox_target: "list" }
|
|
@@ -13,6 +13,10 @@ module PhlexKit
|
|
|
13
13
|
input(**mix({
|
|
14
14
|
type: :checkbox,
|
|
15
15
|
class: "pk-combobox-checkbox",
|
|
16
|
+
# The option label is the interactive surface (ARIA option); the real
|
|
17
|
+
# input must not be a second tab stop inside it — same as
|
|
18
|
+
# ComboboxCheckbox/ComboboxRadio.
|
|
19
|
+
tabindex: "-1",
|
|
16
20
|
data: {
|
|
17
21
|
phlex_kit__combobox_target: "toggleAll",
|
|
18
22
|
action: "change->phlex-kit--combobox#toggleAllItems"
|
|
@@ -98,6 +98,9 @@ export default class extends Controller {
|
|
|
98
98
|
this.searchEntries = this.searchEntries.filter(
|
|
99
99
|
(entry) => entry.element !== item,
|
|
100
100
|
);
|
|
101
|
+
// The highlight index is positional — after a removal it points at the
|
|
102
|
+
// wrong (or a missing) row, so Enter would activate the wrong item.
|
|
103
|
+
this.selectedIndex = -1;
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
dismiss() {
|
|
@@ -266,7 +269,9 @@ export default class extends Controller {
|
|
|
266
269
|
this.updateSelectedItem(visibleItems, -1);
|
|
267
270
|
} else if (e.key === "Enter" && this.selectedIndex !== -1) {
|
|
268
271
|
e.preventDefault();
|
|
269
|
-
|
|
272
|
+
// Optional-chained: a stale selectedIndex after dynamic item removal
|
|
273
|
+
// (turbo stream re-render) must not throw.
|
|
274
|
+
visibleItems[this.selectedIndex]?.click();
|
|
270
275
|
}
|
|
271
276
|
}
|
|
272
277
|
|
|
@@ -18,6 +18,10 @@ module PhlexKit
|
|
|
18
18
|
class: "pk-command-item",
|
|
19
19
|
href: @href,
|
|
20
20
|
role: "option",
|
|
21
|
+
# Options in an aria-activedescendant listbox are not tab stops —
|
|
22
|
+
# keyboard access is arrows/Enter from the input (cmdk parity); a
|
|
23
|
+
# tabbable anchor per row also made disabled items focusable.
|
|
24
|
+
tabindex: "-1",
|
|
21
25
|
aria: { disabled: (@disabled ? "true" : nil) },
|
|
22
26
|
data: {
|
|
23
27
|
phlex_kit__command_target: "item",
|
|
@@ -3,13 +3,17 @@ module PhlexKit
|
|
|
3
3
|
# listbox role and an id the controller points the input's aria-controls at
|
|
4
4
|
# (and derives result ids from, for aria-activedescendant). See command.rb.
|
|
5
5
|
class CommandList < BaseComponent
|
|
6
|
-
|
|
6
|
+
# id: is a named kwarg (not left in **attrs) because `mix` would *merge* a
|
|
7
|
+
# caller id with the generated one into an invalid two-token id, breaking
|
|
8
|
+
# aria-controls (the documented list_id: wiring) and derived result ids.
|
|
9
|
+
def initialize(id: nil, **attrs)
|
|
10
|
+
@id = id || "pk-command-list-#{SecureRandom.hex(4)}"
|
|
7
11
|
@attrs = attrs
|
|
8
12
|
end
|
|
9
13
|
|
|
10
14
|
def view_template(&)
|
|
11
15
|
div(**mix({
|
|
12
|
-
id:
|
|
16
|
+
id: @id,
|
|
13
17
|
class: "pk-command-list",
|
|
14
18
|
role: "listbox",
|
|
15
19
|
data: { phlex_kit__command_target: "list" }
|
|
@@ -23,12 +23,37 @@ export default class extends Controller {
|
|
|
23
23
|
this.close()
|
|
24
24
|
}
|
|
25
25
|
this.onKey = (e) => this.keydown(e)
|
|
26
|
+
// Turbo snapshots BEFORE disconnect — close now or the snapshot keeps a
|
|
27
|
+
// stale data-state="open" (the panel itself can't resurrect, but host
|
|
28
|
+
// CSS keyed on the state hook would misfire after a restore).
|
|
29
|
+
this.onBeforeCache = () => {
|
|
30
|
+
if (this.contentTarget.matches(":popover-open")) this.close()
|
|
31
|
+
}
|
|
32
|
+
document.addEventListener("turbo:before-cache", this.onBeforeCache)
|
|
33
|
+
// Outstanding syncSub rAF handles — cancelled on disconnect (menubar's
|
|
34
|
+
// pattern).
|
|
35
|
+
this.subFrames = new Set()
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
disconnect() {
|
|
29
39
|
document.removeEventListener("click", this.onDoc)
|
|
30
40
|
document.removeEventListener("contextmenu", this.onDoc)
|
|
31
41
|
document.removeEventListener("keydown", this.onKey)
|
|
42
|
+
document.removeEventListener("turbo:before-cache", this.onBeforeCache)
|
|
43
|
+
this.subFrames.forEach((id) => cancelAnimationFrame(id))
|
|
44
|
+
this.subFrames.clear()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Mirrors the CSS-driven submenu state (:hover / :focus-within) onto the
|
|
48
|
+
// sub trigger's aria-expanded — same wiring as dropdown_menu/menubar.
|
|
49
|
+
syncSub(e) {
|
|
50
|
+
const sub = e.currentTarget
|
|
51
|
+
const id = requestAnimationFrame(() => {
|
|
52
|
+
this.subFrames.delete(id)
|
|
53
|
+
sub.querySelector(":scope > [aria-haspopup]")
|
|
54
|
+
?.setAttribute("aria-expanded", sub.matches(":hover, :focus-within"))
|
|
55
|
+
})
|
|
56
|
+
this.subFrames.add(id)
|
|
32
57
|
}
|
|
33
58
|
|
|
34
59
|
open(e) {
|
|
@@ -5,7 +5,9 @@ module PhlexKit
|
|
|
5
5
|
class ContextMenuSub < BaseComponent
|
|
6
6
|
def initialize(**attrs) = (@attrs = attrs)
|
|
7
7
|
def view_template(&)
|
|
8
|
-
|
|
8
|
+
# syncSub mirrors the CSS-driven reveal state onto the sub trigger's
|
|
9
|
+
# aria-expanded (same wiring as DropdownMenuSub / MenubarSub).
|
|
10
|
+
div(**mix({ class: "pk-context-menu-sub", data: { action: "mouseenter->phlex-kit--context-menu#syncSub mouseleave->phlex-kit--context-menu#syncSub focusin->phlex-kit--context-menu#syncSub focusout->phlex-kit--context-menu#syncSub" } }, @attrs), &)
|
|
9
11
|
end
|
|
10
12
|
end
|
|
11
13
|
end
|
|
@@ -9,7 +9,9 @@ module PhlexKit
|
|
|
9
9
|
# -1: roving focus reaches it via arrows (focus-within opens the sub);
|
|
10
10
|
# tabindex 0 made it a stray tab stop inside the menu.
|
|
11
11
|
tabindex: "-1",
|
|
12
|
-
|
|
12
|
+
# expanded starts false and is mirrored live by the controller's
|
|
13
|
+
# syncSub (the reveal itself is pure CSS :hover/:focus-within).
|
|
14
|
+
aria: { haspopup: "menu", expanded: "false" },
|
|
13
15
|
data: { phlex_kit__context_menu_target: "menuItem" }
|
|
14
16
|
}, @attrs)) do
|
|
15
17
|
block&.call
|
|
@@ -23,16 +23,6 @@
|
|
|
23
23
|
/* "Columns" dropdown rows. */
|
|
24
24
|
.pk-data-table-column-toggle { position: relative; }
|
|
25
25
|
.pk-data-table-column-toggle-icon { width: 1rem; height: 1rem; margin-inline-start: .25rem; }
|
|
26
|
-
.pk-data-table-column-option {
|
|
27
|
-
display: flex;
|
|
28
|
-
align-items: center;
|
|
29
|
-
gap: .5rem;
|
|
30
|
-
border-radius: calc(var(--pk-radius) - 4px);
|
|
31
|
-
padding: .375rem .5rem;
|
|
32
|
-
font-size: .875rem;
|
|
33
|
-
cursor: pointer;
|
|
34
|
-
}
|
|
35
|
-
.pk-data-table-column-option:hover { background: var(--pk-accent); }
|
|
36
26
|
|
|
37
27
|
/* Sortable header link. */
|
|
38
28
|
.pk-data-table-sort-head { color: var(--pk-text); white-space: nowrap; }
|