phlex_kit 0.8.0 → 0.9.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/_tokens.css +11 -0
- data/app/components/phlex_kit/accordion/accordion.css +2 -2
- data/app/components/phlex_kit/accordion/accordion_content.rb +11 -2
- data/app/components/phlex_kit/accordion/accordion_controller.js +23 -4
- 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 +11 -4
- data/app/components/phlex_kit/alert_dialog/alert_dialog_controller.js +123 -6
- data/app/components/phlex_kit/aspect_ratio/aspect_ratio.rb +10 -4
- 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 +12 -1
- data/app/components/phlex_kit/calendar/calendar_controller.js +151 -12
- 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 +37 -10
- 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/clipboard/clipboard_popover.rb +3 -1
- 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 +67 -11
- 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_checkbox_item.rb +6 -2
- data/app/components/phlex_kit/context_menu/context_menu_controller.js +69 -2
- data/app/components/phlex_kit/context_menu/context_menu_radio_item.rb +6 -2
- data/app/components/phlex_kit/context_menu/context_menu_sub_trigger.rb +5 -2
- 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 +6 -5
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu.rb +5 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_checkbox_item.rb +6 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_content.rb +3 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_controller.js +99 -45
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_label.rb +3 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_radio_item.rb +6 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub_trigger.rb +5 -2
- 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 +44 -12
- data/app/components/phlex_kit/form_field/form_field_error.rb +8 -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 +43 -4
- 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.rb +1 -1
- data/app/components/phlex_kit/masked_input/masked_input_controller.js +34 -9
- 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 +119 -12
- 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 +7 -2
- 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 +11 -3
- data/app/components/phlex_kit/navigation_menu/navigation_menu.rb +6 -1
- data/app/components/phlex_kit/navigation_menu/navigation_menu_trigger.rb +1 -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 +78 -3
- data/app/components/phlex_kit/resizable/resizable_handle.rb +3 -1
- 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 +11 -7
- data/app/components/phlex_kit/select/select_controller.js +35 -4
- data/app/components/phlex_kit/select/select_item.rb +5 -2
- 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 +27 -15
- 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 +3 -3
- 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 +11 -0
- data/app/components/phlex_kit/theme_toggle/theme_toggle_controller.js +49 -7
- 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 +19 -5
- data/app/components/phlex_kit/toast/toaster_controller.js +20 -6
- data/app/components/phlex_kit/toggle/toggle.css +4 -4
- data/app/components/phlex_kit/toggle/toggle.rb +9 -3
- 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/app/javascript/phlex_kit/controllers/index.js +0 -2
- data/lib/phlex_kit/version.rb +1 -1
- metadata +1 -2
- data/app/components/phlex_kit/select/select_item_controller.js +0 -14
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
|
-
# Divider between OTP groups. See input_otp.rb.
|
|
2
|
+
# Divider between OTP groups. See input_otp.rb. Purely decorative — the dash
|
|
3
|
+
# is aria-hidden (no separator role) so AT doesn't announce a stray "-"
|
|
4
|
+
# between the digit fields.
|
|
3
5
|
class InputOtpSeparator < BaseComponent
|
|
4
6
|
def initialize(**attrs) = (@attrs = attrs)
|
|
5
|
-
def view_template = div(**mix({ class: "pk-input-otp-separator",
|
|
7
|
+
def view_template = div(**mix({ class: "pk-input-otp-separator", aria_hidden: "true" }, @attrs)) { "-" }
|
|
6
8
|
end
|
|
7
9
|
end
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
|
-
# Stack of Items. See item.rb.
|
|
2
|
+
# Stack of Items. See item.rb. No role="list": Items carry no
|
|
3
|
+
# role="listitem" (an href Item is an <a> that must keep its link role), so
|
|
4
|
+
# claiming a list here made AT announce an empty list.
|
|
3
5
|
class ItemGroup < BaseComponent
|
|
4
6
|
def initialize(**attrs) = (@attrs = attrs)
|
|
5
|
-
def view_template(&) = div(**mix({ class: "pk-item-group"
|
|
7
|
+
def view_template(&) = div(**mix({ class: "pk-item-group" }, @attrs), &)
|
|
6
8
|
end
|
|
7
9
|
end
|
|
@@ -4,7 +4,9 @@ module PhlexKit
|
|
|
4
4
|
class ItemSeparator < BaseComponent
|
|
5
5
|
def initialize(**attrs) = (@attrs = attrs)
|
|
6
6
|
def view_template
|
|
7
|
-
|
|
7
|
+
# Purely decorative — aria-hidden only (role="separator" would
|
|
8
|
+
# contradict aria-hidden and confuse AT).
|
|
9
|
+
div(**mix({ class: "pk-item-separator", aria: { hidden: true } }, @attrs))
|
|
8
10
|
end
|
|
9
11
|
end
|
|
10
12
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
# Text input with an inline mask. Ported from ruby_ui's RubyUI::MaskedInput —
|
|
3
3
|
# ruby_ui uses the `maska` JS lib; PhlexKit ships a small dependency-free mask
|
|
4
|
-
# controller (#=digit, A=letter, *=any) driven by a data-mask attribute. Swap in
|
|
4
|
+
# controller (#=digit, A=letter, *=any alphanumeric) driven by a data-mask attribute. Swap in
|
|
5
5
|
# maska by replacing masked_input_controller.js if you need its full feature set.
|
|
6
6
|
class MaskedInput < BaseComponent
|
|
7
7
|
def initialize(**attrs) = (@attrs = attrs)
|
|
@@ -1,26 +1,51 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
|
|
3
3
|
// Connects to data-controller="phlex-kit--masked-input". Lightweight, dependency-
|
|
4
|
-
// free mask driven by a data-mask pattern (# = digit, A = letter, * = any
|
|
5
|
-
//
|
|
4
|
+
// free mask driven by a data-mask pattern (# = digit, A = letter, * = any
|
|
5
|
+
// alphanumeric — formatting characters are stripped before matching, so *
|
|
6
|
+
// cannot match punctuation; swap in `maska` (ruby_ui's choice) if you need a
|
|
7
|
+
// fuller mask engine).
|
|
6
8
|
export default class extends Controller {
|
|
7
9
|
connect() {
|
|
8
10
|
this.mask = this.element.getAttribute("data-mask") || ""
|
|
9
11
|
if (!this.mask) return
|
|
10
|
-
|
|
12
|
+
// Skip IME composition updates — masking mid-composition mangles the text.
|
|
13
|
+
this.onInput = (e) => { if (!e.isComposing) this.apply() }
|
|
11
14
|
this.element.addEventListener("input", this.onInput)
|
|
15
|
+
// A server-prefilled value renders unmasked — format it once up front.
|
|
16
|
+
if (this.element.value) this.apply()
|
|
12
17
|
}
|
|
13
18
|
disconnect() { if (this.onInput) this.element.removeEventListener("input", this.onInput) }
|
|
14
19
|
apply() {
|
|
15
|
-
const
|
|
20
|
+
const el = this.element
|
|
21
|
+
// The whole value is rewritten below, which throws the caret to the end —
|
|
22
|
+
// remember how many maskable chars sit before it and re-seat it after
|
|
23
|
+
// the same count in the masked output (mid-field edits keep their place).
|
|
24
|
+
const caret = el.selectionStart ?? el.value.length
|
|
25
|
+
const rawBefore = el.value.slice(0, caret).replace(/[^0-9A-Za-z]/g, "").length
|
|
26
|
+
|
|
27
|
+
const raw = el.value.replace(/[^0-9A-Za-z]/g, "")
|
|
16
28
|
let out = "", i = 0
|
|
17
29
|
for (const t of this.mask) {
|
|
18
30
|
if (i >= raw.length) break
|
|
19
|
-
if (t === "#"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
if (t === "#" || t === "A" || t === "*") {
|
|
32
|
+
// Skip wrong-class characters instead of aborting the fill —
|
|
33
|
+
// "12a34" against ##/## must yield "12/34", not "12".
|
|
34
|
+
const re = t === "#" ? /\d/ : t === "A" ? /[A-Za-z]/ : /[0-9A-Za-z]/
|
|
35
|
+
while (i < raw.length && !re.test(raw[i])) i++
|
|
36
|
+
if (i >= raw.length) break
|
|
37
|
+
out += raw[i++]
|
|
38
|
+
} else { out += t; if (raw[i] === t) i++ }
|
|
39
|
+
}
|
|
40
|
+
el.value = out
|
|
41
|
+
|
|
42
|
+
if (document.activeElement === el) {
|
|
43
|
+
let pos = 0, seen = 0
|
|
44
|
+
while (pos < out.length && seen < rawBefore) {
|
|
45
|
+
if (/[0-9A-Za-z]/.test(out[pos])) seen++
|
|
46
|
+
pos++
|
|
47
|
+
}
|
|
48
|
+
el.setSelectionRange(pos, pos)
|
|
23
49
|
}
|
|
24
|
-
this.element.value = out
|
|
25
50
|
}
|
|
26
51
|
}
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
}
|
|
31
31
|
.pk-menubar-trigger:hover,
|
|
32
32
|
.pk-menubar-trigger[aria-expanded="true"] { background: var(--pk-surface-2); }
|
|
33
|
+
.pk-menubar-trigger:focus-visible { box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent); }
|
|
33
34
|
.pk-menubar-content {
|
|
34
35
|
position: fixed;
|
|
35
36
|
position-anchor: --pk-menubar-menu;
|
|
@@ -46,7 +47,7 @@
|
|
|
46
47
|
background: var(--pk-surface);
|
|
47
48
|
color: var(--pk-text);
|
|
48
49
|
padding: .25rem;
|
|
49
|
-
box-shadow: 0 8px 24px
|
|
50
|
+
box-shadow: 0 8px 24px color-mix(in srgb, var(--pk-shadow-color) 30%, transparent);
|
|
50
51
|
}
|
|
51
52
|
/* Hover bridge over the trigger→panel gap — both sides, since a flip
|
|
52
53
|
fallback puts the gap below the panel instead of above. */
|
|
@@ -82,7 +83,7 @@
|
|
|
82
83
|
.pk-menubar-item svg:not([class*="size-"]) { width: 1rem; height: 1rem; }
|
|
83
84
|
.pk-menubar-item:hover, .pk-menubar-item:focus { background: var(--pk-accent); }
|
|
84
85
|
.pk-menubar-item[data-disabled="true"] { pointer-events: none; opacity: .5; }
|
|
85
|
-
.pk-menubar-item.inset { padding-
|
|
86
|
+
.pk-menubar-item.inset { padding-inline-start: 1.75rem; }
|
|
86
87
|
.pk-menubar-item.destructive { color: var(--pk-red); }
|
|
87
88
|
.pk-menubar-item.destructive > svg { color: var(--pk-red); }
|
|
88
89
|
.pk-menubar-item.destructive:hover,
|
|
@@ -90,9 +91,9 @@
|
|
|
90
91
|
background: color-mix(in oklab, var(--pk-red) 12%, transparent);
|
|
91
92
|
color: var(--pk-red);
|
|
92
93
|
}
|
|
93
|
-
.pk-menubar-shortcut { margin-
|
|
94
|
+
.pk-menubar-shortcut { margin-inline-start: auto; font-size: .75rem; color: var(--pk-muted); letter-spacing: .1em; }
|
|
94
95
|
.pk-menubar-label { padding: .25rem .375rem; font-size: .875rem; font-weight: 500; }
|
|
95
|
-
.pk-menubar-label.inset { padding-
|
|
96
|
+
.pk-menubar-label.inset { padding-inline-start: 1.75rem; }
|
|
96
97
|
.pk-menubar-separator { height: 1px; margin: .25rem -0.25rem; background: var(--pk-border); }
|
|
97
98
|
.pk-menubar-group { display: contents; }
|
|
98
99
|
|
|
@@ -105,10 +106,10 @@
|
|
|
105
106
|
pointer-events: none;
|
|
106
107
|
}
|
|
107
108
|
.pk-menubar-checkbox-item,
|
|
108
|
-
.pk-menubar-radio-item { position: relative; padding-
|
|
109
|
+
.pk-menubar-radio-item { position: relative; padding-inline-start: 1.75rem; }
|
|
109
110
|
.pk-menubar-item-indicator {
|
|
110
111
|
position: absolute;
|
|
111
|
-
|
|
112
|
+
inset-inline-start: .375rem;
|
|
112
113
|
display: inline-flex;
|
|
113
114
|
align-items: center;
|
|
114
115
|
justify-content: center;
|
|
@@ -120,14 +121,14 @@
|
|
|
120
121
|
.pk-menubar-radio-item .pk-menubar-item-indicator svg { width: .5rem; height: .5rem; }
|
|
121
122
|
.pk-menubar-item:has(.pk-menubar-item-input:checked) .pk-menubar-item-indicator { visibility: visible; }
|
|
122
123
|
.pk-menubar-item:has(.pk-menubar-item-input:focus-visible) {
|
|
123
|
-
box-shadow: 0 0 0
|
|
124
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
/* Submenu: trigger row + side panel, revealed on hover/focus — pure CSS.
|
|
127
128
|
The row is its own scoped anchor; the panel flips to the other side at
|
|
128
129
|
the viewport right edge. */
|
|
129
130
|
.pk-menubar-sub { anchor-name: --pk-menubar-sub; anchor-scope: --pk-menubar-sub; }
|
|
130
|
-
.pk-menubar-sub-chevron { margin-
|
|
131
|
+
.pk-menubar-sub-chevron { margin-inline-start: auto; width: 1rem; height: 1rem; }
|
|
131
132
|
.pk-menubar-sub-content {
|
|
132
133
|
position: fixed;
|
|
133
134
|
position-anchor: --pk-menubar-sub;
|
|
@@ -148,7 +149,7 @@
|
|
|
148
149
|
background: var(--pk-surface);
|
|
149
150
|
color: var(--pk-text);
|
|
150
151
|
padding: .25rem;
|
|
151
|
-
box-shadow: 0 8px 24px
|
|
152
|
+
box-shadow: 0 8px 24px color-mix(in srgb, var(--pk-shadow-color) 30%, transparent);
|
|
152
153
|
}
|
|
153
154
|
.pk-menubar-sub:hover > .pk-menubar-sub-content,
|
|
154
155
|
.pk-menubar-sub:focus-within > .pk-menubar-sub-content { display: block; }
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
# Dropdown panel of a MenubarMenu. See menubar.rb.
|
|
3
3
|
class MenubarContent < BaseComponent
|
|
4
|
-
def initialize(**attrs)
|
|
4
|
+
def initialize(**attrs)
|
|
5
|
+
# Default id so the controller can wire the trigger's aria-controls to
|
|
6
|
+
# this panel in menuTargetConnected (same pattern as accordion).
|
|
7
|
+
@id = attrs.delete(:id) || "pk-menubar-content-#{SecureRandom.hex(4)}"
|
|
8
|
+
@attrs = attrs
|
|
9
|
+
end
|
|
10
|
+
|
|
5
11
|
def view_template(&)
|
|
6
|
-
div(**mix({ class: "pk-menubar-content", popover: "manual", role: "menu" }, @attrs), &)
|
|
12
|
+
div(**mix({ id: @id, class: "pk-menubar-content", popover: "manual", role: "menu" }, @attrs), &)
|
|
7
13
|
end
|
|
8
14
|
end
|
|
9
15
|
end
|
|
@@ -11,6 +11,8 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
11
11
|
// (skipping [data-disabled]); ArrowLeft/ArrowRight jump to the sibling menus;
|
|
12
12
|
// Escape closes and returns focus to the trigger.
|
|
13
13
|
// Connects to data-controller="phlex-kit--menubar"
|
|
14
|
+
let uid = 0
|
|
15
|
+
|
|
14
16
|
export default class extends Controller {
|
|
15
17
|
static targets = ["menu"]
|
|
16
18
|
|
|
@@ -21,13 +23,38 @@ export default class extends Controller {
|
|
|
21
23
|
return this.menuTargets.find((menu) => this.panel(menu)?.matches(":popover-open")) ?? null
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
initialize() {
|
|
27
|
+
// menuTargetConnected fires BEFORE connect() — state it reads must be
|
|
28
|
+
// set here. Roving tabindex applies only in menubar mode: a nav's
|
|
29
|
+
// triggers and links stay natural tab stops.
|
|
30
|
+
this.roving = this.element.dataset.hoverOpen === undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Pair each trigger with its panel for AT (aria-controls), and fold the
|
|
34
|
+
// trigger into the roving tabindex (first trigger 0, rest -1).
|
|
35
|
+
menuTargetConnected(menu) {
|
|
36
|
+
const trigger = this.trigger(menu)
|
|
37
|
+
const panel = this.panel(menu)
|
|
38
|
+
if (trigger && panel) {
|
|
39
|
+
if (!panel.id) panel.id = `pk-menu-panel-${++uid}`
|
|
40
|
+
trigger.setAttribute("aria-controls", panel.id)
|
|
41
|
+
}
|
|
42
|
+
if (this.roving) this.applyRoving()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
menuTargetDisconnected() {
|
|
46
|
+
if (this.roving) this.applyRoving()
|
|
47
|
+
}
|
|
48
|
+
|
|
24
49
|
disconnect() {
|
|
25
50
|
clearTimeout(this.graceTimer)
|
|
26
51
|
}
|
|
27
52
|
|
|
28
53
|
toggle(e) {
|
|
29
54
|
const menu = e.currentTarget.closest("[data-phlex-kit--menubar-target=\"menu\"]")
|
|
30
|
-
|
|
55
|
+
// detail 0 = keyboard-activated click (Enter/Space): only then does the
|
|
56
|
+
// first item get force-focused — a mouse open leaves focus on the trigger.
|
|
57
|
+
this.openMenu === menu ? this.close() : this.show(menu, e.detail === 0)
|
|
31
58
|
}
|
|
32
59
|
|
|
33
60
|
// Hover: switches between menus while one is open (menubar), or opens
|
|
@@ -58,10 +85,15 @@ export default class extends Controller {
|
|
|
58
85
|
show(menu, focus = false) {
|
|
59
86
|
if (this.openMenu !== menu) {
|
|
60
87
|
this.close()
|
|
61
|
-
this.panel(menu)
|
|
62
|
-
|
|
88
|
+
if (this.panel(menu)) {
|
|
89
|
+
this.panel(menu).showPopover()
|
|
90
|
+
menu.querySelector("[aria-expanded]")?.setAttribute("aria-expanded", "true")
|
|
91
|
+
}
|
|
63
92
|
}
|
|
64
|
-
if (
|
|
93
|
+
if (this.roving) this.applyRoving(this.trigger(menu))
|
|
94
|
+
// No panel items (e.g. a panel-less nav link item reached via arrows):
|
|
95
|
+
// land on the trigger itself instead of stranding focus.
|
|
96
|
+
if (focus) (this.items(menu)[0] ?? this.trigger(menu))?.focus()
|
|
65
97
|
}
|
|
66
98
|
|
|
67
99
|
close(opts = {}) {
|
|
@@ -84,11 +116,20 @@ export default class extends Controller {
|
|
|
84
116
|
// cleanup): roving focus while a menu is open, ArrowDown-opens while closed.
|
|
85
117
|
onKeydown(e) {
|
|
86
118
|
if (!this.openMenu) {
|
|
87
|
-
if (e.key !== "ArrowDown") return
|
|
88
119
|
const menu = e.target.closest("[data-phlex-kit--menubar-target=\"menu\"]")
|
|
89
120
|
if (!menu) return
|
|
90
|
-
e.
|
|
91
|
-
|
|
121
|
+
if (e.key === "ArrowDown") {
|
|
122
|
+
e.preventDefault()
|
|
123
|
+
this.show(menu, true)
|
|
124
|
+
} else if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
|
|
125
|
+
// Closed bar: left/right move focus between the triggers (APG).
|
|
126
|
+
e.preventDefault()
|
|
127
|
+
const menus = this.menuTargets
|
|
128
|
+
const next = menus[(menus.indexOf(menu) + (e.key === "ArrowRight" ? 1 : -1) + menus.length) % menus.length]
|
|
129
|
+
const target = next ? this.trigger(next) : null
|
|
130
|
+
target?.focus()
|
|
131
|
+
if (this.roving && target) this.applyRoving(target)
|
|
132
|
+
}
|
|
92
133
|
return
|
|
93
134
|
}
|
|
94
135
|
const items = this.items(this.openMenu)
|
|
@@ -114,17 +155,51 @@ export default class extends Controller {
|
|
|
114
155
|
e.preventDefault()
|
|
115
156
|
items[items.length - 1]?.focus()
|
|
116
157
|
break
|
|
158
|
+
case "Enter":
|
|
159
|
+
case " ":
|
|
160
|
+
// Explicit click so label rows (checkbox/radio) activate — labels
|
|
161
|
+
// have no native keyboard activation (and this gives links Space).
|
|
162
|
+
if (index >= 0) {
|
|
163
|
+
e.preventDefault()
|
|
164
|
+
items[index].click()
|
|
165
|
+
}
|
|
166
|
+
break
|
|
117
167
|
case "ArrowRight":
|
|
118
168
|
e.preventDefault()
|
|
119
|
-
|
|
169
|
+
// On a sub trigger, enter the submenu (focus reveals it via
|
|
170
|
+
// :focus-within) instead of jumping to the next top-level menu.
|
|
171
|
+
if (document.activeElement?.matches(".pk-menubar-sub-trigger")) {
|
|
172
|
+
this.enterSub(document.activeElement)
|
|
173
|
+
} else {
|
|
174
|
+
this.shift(1)
|
|
175
|
+
}
|
|
120
176
|
break
|
|
121
|
-
case "ArrowLeft":
|
|
177
|
+
case "ArrowLeft": {
|
|
122
178
|
e.preventDefault()
|
|
123
|
-
|
|
179
|
+
// Inside a submenu, step back to its trigger instead of switching menus.
|
|
180
|
+
const sub = document.activeElement?.closest(".pk-menubar-sub-content")
|
|
181
|
+
if (sub) {
|
|
182
|
+
sub.closest(".pk-menubar-sub")?.querySelector(".pk-menubar-sub-trigger")?.focus()
|
|
183
|
+
} else {
|
|
184
|
+
this.shift(-1)
|
|
185
|
+
}
|
|
124
186
|
break
|
|
187
|
+
}
|
|
125
188
|
}
|
|
126
189
|
}
|
|
127
190
|
|
|
191
|
+
// Focus the first row of a sub trigger's panel. The panel opens on
|
|
192
|
+
// :focus-within, so focus the trigger first, then hop into the revealed
|
|
193
|
+
// panel's first visible item.
|
|
194
|
+
enterSub(trigger) {
|
|
195
|
+
trigger.focus()
|
|
196
|
+
const panel = trigger.closest(".pk-menubar-sub")?.querySelector(".pk-menubar-sub-content")
|
|
197
|
+
if (!panel) return
|
|
198
|
+
const first = [...panel.querySelectorAll("[role^=\"menuitem\"]")]
|
|
199
|
+
.find((el) => !el.closest("[data-disabled]") && el.getClientRects().length > 0)
|
|
200
|
+
first?.focus()
|
|
201
|
+
}
|
|
202
|
+
|
|
128
203
|
// Mirrors a checkbox/radio item's native input state onto the item's
|
|
129
204
|
// aria-checked (radios also reset their group's siblings).
|
|
130
205
|
syncChecked(e) {
|
|
@@ -135,12 +210,38 @@ export default class extends Controller {
|
|
|
135
210
|
group.forEach((i) => i.closest("[role^=\"menuitem\"]")?.setAttribute("aria-checked", i.checked))
|
|
136
211
|
}
|
|
137
212
|
|
|
213
|
+
// Mirrors the CSS-driven submenu state (:hover / :focus-within) onto the
|
|
214
|
+
// sub trigger's aria-expanded. rAF: on mouseleave/focusout the pseudo-class
|
|
215
|
+
// state isn't settled until the event finishes dispatching.
|
|
216
|
+
syncSub(e) {
|
|
217
|
+
const sub = e.currentTarget
|
|
218
|
+
requestAnimationFrame(() => {
|
|
219
|
+
sub.querySelector(":scope > [aria-haspopup]")
|
|
220
|
+
?.setAttribute("aria-expanded", sub.matches(":hover, :focus-within"))
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
138
224
|
shift(dir) {
|
|
139
225
|
const menus = this.menuTargets
|
|
140
226
|
const index = menus.indexOf(this.openMenu)
|
|
141
227
|
this.show(menus[(index + dir + menus.length) % menus.length], true)
|
|
142
228
|
}
|
|
143
229
|
|
|
230
|
+
// The bar's single tab stop (APG menubar): `current` keeps tabindex 0,
|
|
231
|
+
// every other trigger gets -1.
|
|
232
|
+
applyRoving(current = null) {
|
|
233
|
+
const triggers = this.menuTargets.map((menu) => this.trigger(menu)).filter(Boolean)
|
|
234
|
+
if (triggers.length === 0) return
|
|
235
|
+
if (!current || !triggers.includes(current)) {
|
|
236
|
+
current = triggers.find((t) => t.getAttribute("tabindex") === "0") ?? triggers[0]
|
|
237
|
+
}
|
|
238
|
+
triggers.forEach((t) => t.setAttribute("tabindex", t === current ? "0" : "-1"))
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
trigger(menu) {
|
|
242
|
+
return menu?.querySelector("[aria-expanded], a, button, [tabindex]")
|
|
243
|
+
}
|
|
244
|
+
|
|
144
245
|
panel(menu) {
|
|
145
246
|
return menu.querySelector("[role=\"menu\"], .pk-menubar-content, .pk-navigation-menu-content")
|
|
146
247
|
}
|
|
@@ -148,7 +249,13 @@ export default class extends Controller {
|
|
|
148
249
|
items(menu) {
|
|
149
250
|
const panel = this.panel(menu)
|
|
150
251
|
if (!panel) return []
|
|
151
|
-
|
|
152
|
-
|
|
252
|
+
// getClientRects also skips rows inside a CLOSED sub panel (its
|
|
253
|
+
// display:none comes from the hover/focus-within CSS, not .pk-hidden) —
|
|
254
|
+
// focus() on those silently fails and the roving nav jams there.
|
|
255
|
+
// Nav panels hold plain links (no menu roles, matching Radix/shadcn) —
|
|
256
|
+
// fall back to them so arrow keys navigate there too.
|
|
257
|
+
const rows = [...panel.querySelectorAll("[role^=\"menuitem\"]")]
|
|
258
|
+
const list = rows.length ? rows : [...panel.querySelectorAll("a[href], button")]
|
|
259
|
+
return list.filter((el) => !el.closest("[data-disabled]") && el.getClientRects().length > 0)
|
|
153
260
|
}
|
|
154
261
|
}
|
|
@@ -3,7 +3,10 @@ module PhlexKit
|
|
|
3
3
|
class MenubarMenu < BaseComponent
|
|
4
4
|
def initialize(**attrs) = (@attrs = attrs)
|
|
5
5
|
def view_template(&)
|
|
6
|
-
|
|
6
|
+
# role="none": role="menubar" only allows menuitem children in the
|
|
7
|
+
# accessibility tree — this layout wrapper must be presentational so
|
|
8
|
+
# the tree flattens to menubar > menuitem (axe: required-children).
|
|
9
|
+
div(**mix({ class: "pk-menubar-menu", role: "none", data: { phlex_kit__menubar_target: "menu" } }, @attrs), &)
|
|
7
10
|
end
|
|
8
11
|
end
|
|
9
12
|
end
|
|
@@ -4,7 +4,12 @@ module PhlexKit
|
|
|
4
4
|
class MenubarSub < BaseComponent
|
|
5
5
|
def initialize(**attrs) = (@attrs = attrs)
|
|
6
6
|
def view_template(&)
|
|
7
|
-
|
|
7
|
+
# syncSub mirrors the CSS-driven open state (:hover / :focus-within)
|
|
8
|
+
# onto the sub trigger's aria-expanded.
|
|
9
|
+
div(**mix({
|
|
10
|
+
class: "pk-menubar-sub",
|
|
11
|
+
data: { action: "mouseenter->phlex-kit--menubar#syncSub mouseleave->phlex-kit--menubar#syncSub focusin->phlex-kit--menubar#syncSub focusout->phlex-kit--menubar#syncSub" }
|
|
12
|
+
}, @attrs), &)
|
|
8
13
|
end
|
|
9
14
|
end
|
|
10
15
|
end
|
|
@@ -6,8 +6,13 @@ module PhlexKit
|
|
|
6
6
|
div(**mix({
|
|
7
7
|
class: "pk-menubar-item pk-menubar-sub-trigger",
|
|
8
8
|
role: "menuitem",
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
# -1: the roving focus reaches it via arrows (focus-within opens the
|
|
10
|
+
# sub); tabindex 0 made it a stray tab stop inside the open panel.
|
|
11
|
+
tabindex: "-1",
|
|
12
|
+
# expanded starts false; the sub is revealed by CSS (:hover /
|
|
13
|
+
# :focus-within), so MenubarSub's syncSub actions mirror that state
|
|
14
|
+
# onto this attribute.
|
|
15
|
+
aria: { haspopup: "menu", expanded: "false" }
|
|
11
16
|
}, @attrs)) do
|
|
12
17
|
block&.call
|
|
13
18
|
render Icon.new(:chevron_right, size: nil, class: "pk-menubar-sub-chevron")
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
opacity: 0;
|
|
12
12
|
pointer-events: none;
|
|
13
13
|
transition: opacity .15s ease, transform .15s ease;
|
|
14
|
-
box-shadow: 0 2px 8px
|
|
14
|
+
box-shadow: 0 2px 8px color-mix(in srgb, var(--pk-shadow-color) 25%, transparent);
|
|
15
15
|
}
|
|
16
16
|
.pk-message-scroller-jump[data-active="true"] {
|
|
17
17
|
opacity: 1;
|
|
@@ -65,8 +65,19 @@ export default class extends Controller {
|
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// The MutationObserver only sees DOM changes — late image loads and
|
|
69
|
+
// viewport resizes also move the live edge, so keep the pin through both.
|
|
70
|
+
this.onContentGrowth = this.onContentGrowth.bind(this);
|
|
71
|
+
this.resizeObserver = new ResizeObserver(this.onContentGrowth);
|
|
72
|
+
if (this.hasContentTarget) this.resizeObserver.observe(this.contentTarget);
|
|
73
|
+
if (this.hasViewportTarget) {
|
|
74
|
+
this.resizeObserver.observe(this.viewportTarget);
|
|
75
|
+
// load doesn't bubble — capture catches images/media inside messages.
|
|
76
|
+
this.viewportTarget.addEventListener("load", this.onContentGrowth, true);
|
|
77
|
+
}
|
|
78
|
+
|
|
68
79
|
// Apply the opening position after layout settles.
|
|
69
|
-
requestAnimationFrame(() => {
|
|
80
|
+
this.initialFrame = requestAnimationFrame(() => {
|
|
70
81
|
this.applyDefaultPosition();
|
|
71
82
|
this.updateButton();
|
|
72
83
|
});
|
|
@@ -78,9 +89,22 @@ export default class extends Controller {
|
|
|
78
89
|
this.viewportTarget.removeEventListener("wheel", this.onWheel);
|
|
79
90
|
this.viewportTarget.removeEventListener("touchstart", this.onTouchStart);
|
|
80
91
|
this.viewportTarget.removeEventListener("keydown", this.onKeydown);
|
|
92
|
+
this.viewportTarget.removeEventListener("load", this.onContentGrowth, true);
|
|
81
93
|
}
|
|
82
94
|
this.observer?.disconnect();
|
|
95
|
+
this.resizeObserver?.disconnect();
|
|
83
96
|
if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
|
|
97
|
+
if (this.initialFrame) cancelAnimationFrame(this.initialFrame);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Content/viewport geometry changed (image decoded, panel resized, …):
|
|
101
|
+
// re-pin to the live edge when following, otherwise just refresh the button.
|
|
102
|
+
onContentGrowth() {
|
|
103
|
+
if (this.programmatic) return;
|
|
104
|
+
if (this.autoScrollValue && this.following && !this.isAtEnd()) {
|
|
105
|
+
this.scrollToEnd("auto");
|
|
106
|
+
}
|
|
107
|
+
this.updateButton();
|
|
84
108
|
}
|
|
85
109
|
|
|
86
110
|
// --- Reader intent -------------------------------------------------------
|
|
@@ -91,17 +115,37 @@ export default class extends Controller {
|
|
|
91
115
|
this.updateButton();
|
|
92
116
|
}
|
|
93
117
|
|
|
94
|
-
// Any upward wheel is a deliberate move away from the live edge.
|
|
118
|
+
// Any upward wheel is a deliberate move away from the live edge. Any wheel
|
|
119
|
+
// during a programmatic scroll hands control straight back to the reader.
|
|
95
120
|
onWheel(event) {
|
|
121
|
+
if (this.programmatic) {
|
|
122
|
+
this.interrupt();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
96
125
|
if (event.deltaY < 0) this.release();
|
|
97
126
|
}
|
|
98
127
|
|
|
99
128
|
onTouchStart() {
|
|
100
129
|
// A touch that turns into an upward drag surfaces through onScroll; this
|
|
101
130
|
// just makes the release feel immediate when the reader grabs the list.
|
|
131
|
+
if (this.programmatic) {
|
|
132
|
+
this.interrupt();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
102
135
|
if (!this.isAtEnd()) this.release();
|
|
103
136
|
}
|
|
104
137
|
|
|
138
|
+
// Cancel an in-flight programmatic scroll and release the pin — the
|
|
139
|
+
// reader's wheel/touch always wins over our 300ms animation.
|
|
140
|
+
interrupt() {
|
|
141
|
+
if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
|
|
142
|
+
this.programmatic = false;
|
|
143
|
+
this.element.removeAttribute("data-autoscrolling");
|
|
144
|
+
this.viewportTarget?.removeAttribute("data-autoscrolling");
|
|
145
|
+
this.following = false;
|
|
146
|
+
this.updateButton();
|
|
147
|
+
}
|
|
148
|
+
|
|
105
149
|
onKeydown(event) {
|
|
106
150
|
const navKeys = ["ArrowUp", "PageUp", "Home", "ArrowDown", "PageDown", "End", " "];
|
|
107
151
|
if (navKeys.includes(event.key)) this.release();
|
|
@@ -39,7 +39,10 @@
|
|
|
39
39
|
position-anchor: --pk-nav-item;
|
|
40
40
|
inset: auto;
|
|
41
41
|
position-area: block-end span-inline-end;
|
|
42
|
-
|
|
42
|
+
/* flip-inline too: a 16rem panel on an item near the right viewport edge
|
|
43
|
+
otherwise clips (same bug class popover fixed in #38). The hover
|
|
44
|
+
bridges span the block-axis gap, which flip-inline doesn't change. */
|
|
45
|
+
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
|
|
43
46
|
/* the [popover] UA style sets overflow:auto, which would clip the
|
|
44
47
|
negative-offset ::before bridge below */
|
|
45
48
|
overflow: visible;
|
|
@@ -54,7 +57,7 @@
|
|
|
54
57
|
background: var(--pk-surface);
|
|
55
58
|
color: var(--pk-text);
|
|
56
59
|
padding: .25rem;
|
|
57
|
-
box-shadow: 0 8px 24px
|
|
60
|
+
box-shadow: 0 8px 24px color-mix(in srgb, var(--pk-shadow-color) 30%, transparent);
|
|
58
61
|
}
|
|
59
62
|
.pk-navigation-menu-content .pk-navigation-menu-link {
|
|
60
63
|
display: flex;
|
|
@@ -65,7 +68,12 @@
|
|
|
65
68
|
align-items: flex-start;
|
|
66
69
|
gap: .25rem;
|
|
67
70
|
}
|
|
68
|
-
/* Gap bridges on both sides — a flip fallback puts the gap below.
|
|
71
|
+
/* Gap bridges on both sides — a flip fallback puts the gap below. They only
|
|
72
|
+
exist while the panel is open (the closed panel is display:none) and are
|
|
73
|
+
already sized to the actual gap; the ~7px hit-area spill on the non-gap
|
|
74
|
+
side is the accepted cost: CSS has no selector for which position-try
|
|
75
|
+
fallback applied, so we can't scope the bridge to the active side, and
|
|
76
|
+
pointer-events:none would defeat the mouseleave continuity they exist for. */
|
|
69
77
|
.pk-navigation-menu-content::before {
|
|
70
78
|
content: "";
|
|
71
79
|
position: absolute;
|
|
@@ -15,7 +15,12 @@ module PhlexKit
|
|
|
15
15
|
data: {
|
|
16
16
|
controller: "phlex-kit--menubar",
|
|
17
17
|
hover_open: true,
|
|
18
|
-
|
|
18
|
+
# onKeydown (not keydown.esc->close): the shared handler drives
|
|
19
|
+
# arrow-key navigation over the panels AND closes on Escape with
|
|
20
|
+
# focus returned to the trigger (bare #close as an action received
|
|
21
|
+
# the event as `opts`, so refocus never happened — focus fell to
|
|
22
|
+
# <body> when the panel hid).
|
|
23
|
+
action: "click@window->phlex-kit--menubar#onClickOutside keydown->phlex-kit--menubar#onKeydown mouseleave->phlex-kit--menubar#closeSoon"
|
|
19
24
|
}
|
|
20
25
|
}, @attrs), &)
|
|
21
26
|
end
|
|
@@ -6,7 +6,7 @@ module PhlexKit
|
|
|
6
6
|
button(**mix({
|
|
7
7
|
type: :button,
|
|
8
8
|
class: "pk-navigation-menu-trigger",
|
|
9
|
-
aria: {
|
|
9
|
+
aria: { expanded: "false" }, # no haspopup="menu": the panel deliberately has no menu role (matching Radix/shadcn)
|
|
10
10
|
data: { action: "click->phlex-kit--menubar#toggle mouseenter->phlex-kit--menubar#switch" }
|
|
11
11
|
}, @attrs)) do
|
|
12
12
|
block&.call
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
.pk-pagination-content { display: flex; flex-direction: row; align-items: center; gap: .125rem; list-style: none; margin: 0; padding: 0; }
|
|
7
7
|
.pk-pagination-ellipsis { display: flex; height: 2rem; width: 2rem; align-items: center; justify-content: center; }
|
|
8
8
|
.pk-pagination-ellipsis > svg { width: 1rem; height: 1rem; }
|
|
9
|
-
.pk-button.pk-pagination-previous { padding-
|
|
10
|
-
.pk-button.pk-pagination-next { padding-
|
|
9
|
+
.pk-button.pk-pagination-previous { padding-inline-start: .375rem; }
|
|
10
|
+
.pk-button.pk-pagination-next { padding-inline-end: .375rem; }
|
|
11
11
|
.pk-pagination-label { display: none; }
|
|
12
12
|
@media (min-width: 640px) { .pk-pagination-label { display: block; } }
|