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
|
@@ -2,18 +2,33 @@ import { Controller } from "@hotwired/stimulus";
|
|
|
2
2
|
|
|
3
3
|
// Ported from ruby_ui's command-dialog controller (Stimulus-only upstream),
|
|
4
4
|
// identifiers renamed ruby-ui → phlex-kit and the Tailwind body scroll-lock
|
|
5
|
-
// class swapped for inline overflow (matching the shipped dialog).
|
|
5
|
+
// class swapped for inline overflow (matching the shipped dialog). Unlike
|
|
6
|
+
// upstream's document-scoped outlet (which let one dialog adopt another's
|
|
7
|
+
// open palette), each instance tracks its own clone — sheet_controller's
|
|
8
|
+
// pattern — and clears it on turbo:before-cache so a cached page never
|
|
9
|
+
// restores a stale overlay + scroll lock.
|
|
6
10
|
// Connects to data-controller="phlex-kit--command-dialog"
|
|
7
11
|
export default class extends Controller {
|
|
8
12
|
static targets = ["content"];
|
|
9
|
-
static outlets = ["phlex-kit--command"];
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
this.
|
|
14
|
+
initialize() {
|
|
15
|
+
this.overlay = null;
|
|
16
|
+
this.clearOverlay = () => {
|
|
17
|
+
if (this.overlay?.isConnected) {
|
|
18
|
+
this.overlay.remove();
|
|
19
|
+
document.body.style.removeProperty("overflow");
|
|
20
|
+
}
|
|
21
|
+
this.overlay = null;
|
|
22
|
+
};
|
|
13
23
|
}
|
|
14
24
|
|
|
15
|
-
|
|
16
|
-
this.
|
|
25
|
+
connect() {
|
|
26
|
+
document.addEventListener("turbo:before-cache", this.clearOverlay);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
disconnect() {
|
|
30
|
+
document.removeEventListener("turbo:before-cache", this.clearOverlay);
|
|
31
|
+
this.clearOverlay();
|
|
17
32
|
}
|
|
18
33
|
|
|
19
34
|
open(e) {
|
|
@@ -25,12 +40,15 @@ export default class extends Controller {
|
|
|
25
40
|
return;
|
|
26
41
|
}
|
|
27
42
|
|
|
28
|
-
|
|
29
|
-
|
|
43
|
+
// Already open (the command controller's dismiss may have removed the
|
|
44
|
+
// clone behind our back — isConnected catches that): just refocus.
|
|
45
|
+
if (this.overlay?.isConnected) {
|
|
46
|
+
this.overlay.querySelector("[data-phlex-kit--command-target='input']")?.focus();
|
|
30
47
|
return;
|
|
31
48
|
}
|
|
32
49
|
|
|
33
50
|
document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML);
|
|
51
|
+
this.overlay = document.body.lastElementChild;
|
|
34
52
|
// prevent scroll on body
|
|
35
53
|
document.body.style.overflow = "hidden";
|
|
36
54
|
}
|
|
@@ -8,9 +8,13 @@ module PhlexKit
|
|
|
8
8
|
# stays "true" because the list is always visible (filtering hides items,
|
|
9
9
|
# never the listbox itself).
|
|
10
10
|
class CommandInput < BaseComponent
|
|
11
|
-
|
|
11
|
+
# `autofocus:` defaults off so an inline palette doesn't steal page focus
|
|
12
|
+
# on load; the dialog clone needs no autofocus either — the command
|
|
13
|
+
# controller's connect() focuses the input when the clone is inserted.
|
|
14
|
+
def initialize(placeholder: "Type a command or search...", list_id: nil, autofocus: false, **attrs)
|
|
12
15
|
@placeholder = placeholder
|
|
13
16
|
@list_id = list_id
|
|
17
|
+
@autofocus = autofocus
|
|
14
18
|
@attrs = attrs
|
|
15
19
|
end
|
|
16
20
|
|
|
@@ -25,7 +29,7 @@ module PhlexKit
|
|
|
25
29
|
autocomplete: "off",
|
|
26
30
|
autocorrect: "off",
|
|
27
31
|
spellcheck: "false",
|
|
28
|
-
autofocus:
|
|
32
|
+
autofocus: @autofocus,
|
|
29
33
|
role: "combobox",
|
|
30
34
|
value: "",
|
|
31
35
|
aria: { autocomplete: "list", expanded: "true", controls: @list_id },
|
|
@@ -23,7 +23,10 @@ module PhlexKit
|
|
|
23
23
|
phlex_kit__command_target: "item",
|
|
24
24
|
value: @value,
|
|
25
25
|
text: @text,
|
|
26
|
-
disabled: (@disabled ? "true" : nil)
|
|
26
|
+
disabled: (@disabled ? "true" : nil),
|
|
27
|
+
# Guards the default href="#": without it a mouse click (or the
|
|
28
|
+
# Enter-synthesized click()) scrolls to top and appends # to the URL.
|
|
29
|
+
action: "click->phlex-kit--command#onItemClick"
|
|
27
30
|
}
|
|
28
31
|
}, @attrs), &)
|
|
29
32
|
end
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
.pk-context-menu-content {
|
|
10
10
|
position: fixed; inset: auto; margin: 0; min-width: 8rem; overflow: visible;
|
|
11
11
|
border: 1px solid var(--pk-border); border-radius: calc(var(--pk-radius) - 2px); background: var(--pk-surface);
|
|
12
|
-
color: var(--pk-text); padding: .25rem; box-shadow: 0 8px 24px
|
|
12
|
+
color: var(--pk-text); padding: .25rem; box-shadow: 0 8px 24px color-mix(in srgb, var(--pk-shadow-color) 30%, transparent); outline: none;
|
|
13
13
|
}
|
|
14
14
|
.pk-context-menu-item {
|
|
15
15
|
position: relative; display: flex; align-items: center; gap: .5rem;
|
|
@@ -20,15 +20,15 @@
|
|
|
20
20
|
.pk-context-menu-item > svg { flex: none; width: 1rem; height: 1rem; color: var(--pk-muted); }
|
|
21
21
|
.pk-context-menu-item:hover, .pk-context-menu-item:focus { background: var(--pk-accent); color: var(--pk-text); }
|
|
22
22
|
.pk-context-menu-item[data-disabled="true"] { pointer-events: none; opacity: .5; }
|
|
23
|
-
.pk-context-menu-item.inset { padding-
|
|
23
|
+
.pk-context-menu-item.inset { padding-inline-start: 2rem; }
|
|
24
24
|
.pk-context-menu-check {
|
|
25
25
|
display: inline-flex; align-items: center; justify-content: center;
|
|
26
26
|
flex: none; width: 1rem; height: 1rem;
|
|
27
27
|
}
|
|
28
28
|
.pk-context-menu-check svg { width: .875rem; height: .875rem; }
|
|
29
|
-
.pk-context-menu-shortcut { margin-
|
|
29
|
+
.pk-context-menu-shortcut { margin-inline-start: auto; font-size: .75rem; letter-spacing: .1em; color: var(--pk-muted); }
|
|
30
30
|
.pk-context-menu-label { padding: .375rem .5rem; font-size: .875rem; font-weight: 600; color: var(--pk-text); }
|
|
31
|
-
.pk-context-menu-label.inset { padding-
|
|
31
|
+
.pk-context-menu-label.inset { padding-inline-start: 2rem; }
|
|
32
32
|
.pk-context-menu-separator { height: 1px; margin: .25rem -.25rem; background: var(--pk-border); }
|
|
33
33
|
.pk-context-menu-group { display: contents; }
|
|
34
34
|
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
The row is its own scoped anchor; the panel flips to the other side at
|
|
72
72
|
the viewport right edge. */
|
|
73
73
|
.pk-context-menu-sub { anchor-name: --pk-context-menu-sub; anchor-scope: --pk-context-menu-sub; }
|
|
74
|
-
.pk-context-menu-sub-chevron { margin-
|
|
74
|
+
.pk-context-menu-sub-chevron { margin-inline-start: auto; width: 1rem; height: 1rem; }
|
|
75
75
|
.pk-context-menu-sub-content {
|
|
76
76
|
position: fixed;
|
|
77
77
|
position-anchor: --pk-context-menu-sub;
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
background: var(--pk-surface);
|
|
93
93
|
color: var(--pk-text);
|
|
94
94
|
padding: .25rem;
|
|
95
|
-
box-shadow: 0 8px 24px
|
|
95
|
+
box-shadow: 0 8px 24px color-mix(in srgb, var(--pk-shadow-color) 30%, transparent);
|
|
96
96
|
}
|
|
97
97
|
.pk-context-menu-sub:hover > .pk-context-menu-sub-content,
|
|
98
98
|
.pk-context-menu-sub:focus-within > .pk-context-menu-sub-content { display: block; }
|
|
@@ -14,9 +14,13 @@ module PhlexKit
|
|
|
14
14
|
def view_template(&block)
|
|
15
15
|
label(**mix({
|
|
16
16
|
class: "pk-context-menu-item pk-context-menu-checkbox-item",
|
|
17
|
-
role: "menuitemcheckbox"
|
|
17
|
+
role: "menuitemcheckbox",
|
|
18
|
+
tabindex: "-1",
|
|
19
|
+
aria: { checked: @checked ? "true" : "false" },
|
|
20
|
+
data: { phlex_kit__context_menu_target: "menuItem" }
|
|
18
21
|
}, @attrs)) do
|
|
19
|
-
input(type: :checkbox, class: "pk-context-menu-item-input", name: @name, value: @value, checked: @checked
|
|
22
|
+
input(type: :checkbox, class: "pk-context-menu-item-input", name: @name, value: @value, checked: @checked,
|
|
23
|
+
tabindex: "-1", data: { action: "change->phlex-kit--context-menu#syncChecked" })
|
|
20
24
|
span(class: "pk-context-menu-item-indicator", aria: { hidden: "true" }) do
|
|
21
25
|
render Icon.new(:check, size: nil)
|
|
22
26
|
end
|
|
@@ -12,12 +12,22 @@ export default class extends Controller {
|
|
|
12
12
|
static targets = ["content", "trigger", "menuItem"]
|
|
13
13
|
|
|
14
14
|
connect() {
|
|
15
|
-
this.onDoc = (e) => {
|
|
15
|
+
this.onDoc = (e) => {
|
|
16
|
+
if (this.contentTarget.contains(e.target)) return
|
|
17
|
+
// The opening right-click bubbles to document AFTER open() adds this
|
|
18
|
+
// listener — a contextmenu inside our own element must not close what
|
|
19
|
+
// it just opened (it repositions instead). A contextmenu anywhere
|
|
20
|
+
// else closes us, which also covers opening a second kit context
|
|
21
|
+
// menu: its trigger's contextmenu is "outside" for this instance.
|
|
22
|
+
if (e.type === "contextmenu" && this.element.contains(e.target)) return
|
|
23
|
+
this.close()
|
|
24
|
+
}
|
|
16
25
|
this.onKey = (e) => this.keydown(e)
|
|
17
26
|
}
|
|
18
27
|
|
|
19
28
|
disconnect() {
|
|
20
29
|
document.removeEventListener("click", this.onDoc)
|
|
30
|
+
document.removeEventListener("contextmenu", this.onDoc)
|
|
21
31
|
document.removeEventListener("keydown", this.onKey)
|
|
22
32
|
}
|
|
23
33
|
|
|
@@ -38,14 +48,21 @@ export default class extends Controller {
|
|
|
38
48
|
c.style.top = `${y}px`
|
|
39
49
|
c.dataset.state = "open"
|
|
40
50
|
document.addEventListener("click", this.onDoc)
|
|
51
|
+
document.addEventListener("contextmenu", this.onDoc)
|
|
41
52
|
document.addEventListener("keydown", this.onKey)
|
|
42
53
|
this.items()[0]?.focus()
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
close(opts = {}) {
|
|
57
|
+
// Menu rows default to href="#"; without this an Enter/click on a
|
|
58
|
+
// non-link row scrolls to top and appends # to the URL. (`opts` is the
|
|
59
|
+
// click event when close runs as the item's data-action; Enter/Space
|
|
60
|
+
// arrive here too, via keydown()'s synthesized click.)
|
|
61
|
+
if (opts?.target?.closest?.('a[href="#"]')) opts.preventDefault()
|
|
46
62
|
if (this.contentTarget.matches(":popover-open")) this.contentTarget.hidePopover()
|
|
47
63
|
this.contentTarget.dataset.state = "closed"
|
|
48
64
|
document.removeEventListener("click", this.onDoc)
|
|
65
|
+
document.removeEventListener("contextmenu", this.onDoc)
|
|
49
66
|
document.removeEventListener("keydown", this.onKey)
|
|
50
67
|
if (opts.refocus === true) this.anchor().focus()
|
|
51
68
|
}
|
|
@@ -74,14 +91,64 @@ export default class extends Controller {
|
|
|
74
91
|
e.preventDefault()
|
|
75
92
|
items[items.length - 1]?.focus()
|
|
76
93
|
break
|
|
94
|
+
case "Enter":
|
|
95
|
+
case " ":
|
|
96
|
+
// Explicit click so label rows (checkbox/radio) activate — labels
|
|
97
|
+
// have no native keyboard activation (and this gives links Space).
|
|
98
|
+
if (index >= 0) {
|
|
99
|
+
e.preventDefault()
|
|
100
|
+
items[index].click()
|
|
101
|
+
}
|
|
102
|
+
break
|
|
103
|
+
case "ArrowRight":
|
|
104
|
+
// On a sub trigger, enter the submenu (focus reveals it via
|
|
105
|
+
// :focus-within, making its rows visible to the roving nav).
|
|
106
|
+
if (document.activeElement?.matches(".pk-context-menu-sub-trigger")) {
|
|
107
|
+
e.preventDefault()
|
|
108
|
+
this.enterSub(document.activeElement)
|
|
109
|
+
}
|
|
110
|
+
break
|
|
111
|
+
case "ArrowLeft": {
|
|
112
|
+
// Inside a submenu, step back to its trigger (closes it on focus-out).
|
|
113
|
+
const sub = document.activeElement?.closest(".pk-context-menu-sub-content")
|
|
114
|
+
if (sub) {
|
|
115
|
+
e.preventDefault()
|
|
116
|
+
sub.closest(".pk-context-menu-sub")?.querySelector(".pk-context-menu-sub-trigger")?.focus()
|
|
117
|
+
}
|
|
118
|
+
break
|
|
119
|
+
}
|
|
77
120
|
}
|
|
78
121
|
}
|
|
79
122
|
|
|
123
|
+
enterSub(trigger) {
|
|
124
|
+
trigger.focus()
|
|
125
|
+
const panel = trigger.closest(".pk-context-menu-sub")?.querySelector(".pk-context-menu-sub-content")
|
|
126
|
+
if (!panel) return
|
|
127
|
+
const first = [...panel.querySelectorAll("[role^=\"menuitem\"]")]
|
|
128
|
+
.find((el) => !el.closest("[data-disabled]") && el.getClientRects().length > 0)
|
|
129
|
+
first?.focus()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Mirrors a checkbox/radio row's native input state onto the row's
|
|
133
|
+
// aria-checked (radios also reset their group's siblings).
|
|
134
|
+
syncChecked(e) {
|
|
135
|
+
const input = e.target
|
|
136
|
+
const group = input.name
|
|
137
|
+
? this.element.querySelectorAll(`input[name="${CSS.escape(input.name)}"]`)
|
|
138
|
+
: [input]
|
|
139
|
+
group.forEach((i) => i.closest("[role^=\"menuitem\"]")?.setAttribute("aria-checked", i.checked))
|
|
140
|
+
}
|
|
141
|
+
|
|
80
142
|
anchor() {
|
|
81
143
|
return this.hasTriggerTarget ? this.triggerTarget : this.element
|
|
82
144
|
}
|
|
83
145
|
|
|
84
146
|
items() {
|
|
85
|
-
|
|
147
|
+
// getClientRects also skips rows inside a CLOSED sub panel (its
|
|
148
|
+
// display:none comes from the hover/focus-within CSS, not .pk-hidden) —
|
|
149
|
+
// focus() on those silently fails and the roving nav jams there.
|
|
150
|
+
return this.menuItemTargets.filter(
|
|
151
|
+
(el) => !el.closest("[data-disabled]") && el.getClientRects().length > 0
|
|
152
|
+
)
|
|
86
153
|
}
|
|
87
154
|
}
|
|
@@ -13,9 +13,13 @@ module PhlexKit
|
|
|
13
13
|
def view_template(&block)
|
|
14
14
|
label(**mix({
|
|
15
15
|
class: "pk-context-menu-item pk-context-menu-radio-item",
|
|
16
|
-
role: "menuitemradio"
|
|
16
|
+
role: "menuitemradio",
|
|
17
|
+
tabindex: "-1",
|
|
18
|
+
aria: { checked: @checked ? "true" : "false" },
|
|
19
|
+
data: { phlex_kit__context_menu_target: "menuItem" }
|
|
17
20
|
}, @attrs)) do
|
|
18
|
-
input(type: :radio, class: "pk-context-menu-item-input", name: @name, value: @value, checked: @checked
|
|
21
|
+
input(type: :radio, class: "pk-context-menu-item-input", name: @name, value: @value, checked: @checked,
|
|
22
|
+
tabindex: "-1", data: { action: "change->phlex-kit--context-menu#syncChecked" })
|
|
19
23
|
span(class: "pk-context-menu-item-indicator", aria: { hidden: "true" }) do
|
|
20
24
|
# A filled selection dot is geometry, not icon-library vocabulary —
|
|
21
25
|
# identical across icon_library settings (matches the dropdown).
|
|
@@ -6,8 +6,11 @@ module PhlexKit
|
|
|
6
6
|
div(**mix({
|
|
7
7
|
class: "pk-context-menu-item pk-context-menu-sub-trigger",
|
|
8
8
|
role: "menuitem",
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
# -1: roving focus reaches it via arrows (focus-within opens the sub);
|
|
10
|
+
# tabindex 0 made it a stray tab stop inside the menu.
|
|
11
|
+
tabindex: "-1",
|
|
12
|
+
aria: { haspopup: "menu" },
|
|
13
|
+
data: { phlex_kit__context_menu_target: "menuItem" }
|
|
11
14
|
}, @attrs)) do
|
|
12
15
|
block&.call
|
|
13
16
|
render Icon.new(:chevron_right, size: nil, class: "pk-context-menu-sub-chevron")
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
/* "Columns" dropdown rows. */
|
|
24
24
|
.pk-data-table-column-toggle { position: relative; }
|
|
25
|
-
.pk-data-table-column-toggle-icon { width: 1rem; height: 1rem; margin-
|
|
25
|
+
.pk-data-table-column-toggle-icon { width: 1rem; height: 1rem; margin-inline-start: .25rem; }
|
|
26
26
|
.pk-data-table-column-option {
|
|
27
27
|
display: flex;
|
|
28
28
|
align-items: center;
|
|
@@ -22,18 +22,17 @@ module PhlexKit
|
|
|
22
22
|
end
|
|
23
23
|
render DropdownMenuContent.new do
|
|
24
24
|
@columns.each do |col|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
end
|
|
25
|
+
# A real menu row (role="menuitemcheckbox" + aria-checked, arrow-
|
|
26
|
+
# navigable) — plain <label>s directly inside role="menu" are
|
|
27
|
+
# invalid ARIA. The change event bubbles from the hidden checkbox
|
|
28
|
+
# to this row, where our toggle action picks it up.
|
|
29
|
+
render DropdownMenuCheckboxItem.new(
|
|
30
|
+
checked: true,
|
|
31
|
+
data: {
|
|
32
|
+
column_key: col[:key].to_s,
|
|
33
|
+
action: "change->phlex-kit--data-table-column-visibility#toggle"
|
|
34
|
+
}
|
|
35
|
+
) { plain col[:label] }
|
|
37
36
|
end
|
|
38
37
|
end
|
|
39
38
|
end
|
|
@@ -7,8 +7,11 @@ import { Controller } from "@hotwired/stimulus";
|
|
|
7
7
|
// Connects to data-controller="phlex-kit--data-table-column-visibility"
|
|
8
8
|
export default class extends Controller {
|
|
9
9
|
toggle(event) {
|
|
10
|
-
|
|
10
|
+
// The change event originates on the checkbox, but data-column-key lives
|
|
11
|
+
// on the menu row (DropdownMenuCheckboxItem) — look up through ancestors.
|
|
12
|
+
const key = event.target.closest("[data-column-key]")?.dataset.columnKey;
|
|
11
13
|
const visible = event.target.checked;
|
|
14
|
+
if (!key) return;
|
|
12
15
|
const root = this.element.closest('[data-controller~="phlex-kit--data-table"]');
|
|
13
16
|
if (!root) return;
|
|
14
17
|
root
|
|
@@ -1,28 +1,42 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
# Per-page NativeSelect in a GET form that self-submits on change (targeting
|
|
3
|
-
# the table's turbo-frame when `frame_id:` is given).
|
|
3
|
+
# the table's turbo-frame when `frame_id:` is given). Extra filter params
|
|
4
|
+
# (search/sort/direction/...) survive via `preserved_params:` hidden inputs,
|
|
5
|
+
# mirroring DataTableSearch. See data_table.rb.
|
|
4
6
|
class DataTablePerPageSelect < BaseComponent
|
|
5
|
-
def initialize(path:, name: "per_page", value: nil, frame_id: nil, options: [ 5, 10, 25, 50 ], **attrs)
|
|
7
|
+
def initialize(path:, name: "per_page", value: nil, frame_id: nil, options: [ 5, 10, 25, 50 ], preserved_params: {}, **attrs)
|
|
6
8
|
@path = path
|
|
7
9
|
@name = name
|
|
8
10
|
@value = value
|
|
9
11
|
@frame_id = frame_id
|
|
10
12
|
@options = options
|
|
13
|
+
@preserved_params = preserved_params
|
|
11
14
|
@attrs = attrs
|
|
12
15
|
end
|
|
13
16
|
|
|
14
17
|
def view_template
|
|
15
|
-
form_attrs = {
|
|
16
|
-
|
|
18
|
+
form_attrs = {
|
|
19
|
+
class: "pk-data-table-per-page",
|
|
20
|
+
action: @path,
|
|
21
|
+
method: "get",
|
|
22
|
+
# Reuse the search controller for its CSP-safe #submitNow action.
|
|
23
|
+
data: { controller: "phlex-kit--data-table-search" }
|
|
24
|
+
}
|
|
25
|
+
form_attrs[:data][:turbo_frame] = @frame_id if @frame_id
|
|
17
26
|
|
|
18
27
|
form(**mix(form_attrs, @attrs)) do
|
|
19
|
-
render NativeSelect.new(name: @name,
|
|
28
|
+
render NativeSelect.new(name: @name, data: { action: "change->phlex-kit--data-table-search#submitNow" }) do
|
|
20
29
|
@options.each do |opt|
|
|
21
30
|
option_attrs = { value: opt.to_s }
|
|
22
31
|
option_attrs[:selected] = true if opt.to_s == @value.to_s
|
|
23
32
|
option(**option_attrs) { plain opt.to_s }
|
|
24
33
|
end
|
|
25
34
|
end
|
|
35
|
+
@preserved_params.each do |k, v|
|
|
36
|
+
next if v.nil? || (v.respond_to?(:empty?) && v.empty?)
|
|
37
|
+
next if k.to_s == @name
|
|
38
|
+
input(type: :hidden, name: k.to_s, value: v.to_s)
|
|
39
|
+
end
|
|
26
40
|
end
|
|
27
41
|
end
|
|
28
42
|
end
|
|
@@ -32,6 +32,13 @@ export default class extends Controller {
|
|
|
32
32
|
this.timer = setTimeout(() => this.element.requestSubmit(), this.delayValue);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// Immediate, undebounced submit — used by change-driven controls like the
|
|
36
|
+
// per-page select (CSP-safe replacement for an inline onchange handler).
|
|
37
|
+
submitNow() {
|
|
38
|
+
clearTimeout(this.timer);
|
|
39
|
+
this.element.requestSubmit();
|
|
40
|
+
}
|
|
41
|
+
|
|
35
42
|
captureBeforeRender() {
|
|
36
43
|
const input = this.input();
|
|
37
44
|
if (!input || document.activeElement !== input) return;
|
|
@@ -62,6 +69,11 @@ export default class extends Controller {
|
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
key() {
|
|
65
|
-
|
|
72
|
+
// The key must survive the Turbo Frame swap (the form element is replaced),
|
|
73
|
+
// so it's derived from stable markup: the form id when the caller set one,
|
|
74
|
+
// else action URL + search-input name — two same-path forms (e.g. search
|
|
75
|
+
// alongside the per-page select) must not share a focus-restore slot.
|
|
76
|
+
if (this.element.id) return `#${this.element.id}`;
|
|
77
|
+
return `${this.element.action || "_"}::${this.input()?.name ?? ""}`;
|
|
66
78
|
}
|
|
67
79
|
}
|
|
@@ -24,10 +24,13 @@ module PhlexKit
|
|
|
24
24
|
@id = id || "date-picker-#{SecureRandom.hex(4)}"
|
|
25
25
|
@name = name
|
|
26
26
|
@label = label
|
|
27
|
-
@value = value || selected_date&.to_s
|
|
28
|
-
@placeholder = placeholder
|
|
29
27
|
@selected_date = selected_date
|
|
30
28
|
@date_format = date_format
|
|
29
|
+
# Seed the input with the same date_format the calendar controller
|
|
30
|
+
# writes — a bare `selected_date.to_s` (ISO) would mismatch the format
|
|
31
|
+
# until the first interaction.
|
|
32
|
+
@value = value || format_selected_date
|
|
33
|
+
@placeholder = placeholder
|
|
31
34
|
@input_attrs = input_attrs
|
|
32
35
|
@calendar_attrs = calendar_attrs
|
|
33
36
|
@trigger_attrs = trigger_attrs
|
|
@@ -45,7 +48,7 @@ module PhlexKit
|
|
|
45
48
|
end
|
|
46
49
|
end
|
|
47
50
|
render PopoverContent.new(**@content_attrs) do
|
|
48
|
-
render Calendar.new(input_id:
|
|
51
|
+
render Calendar.new(input_id: input_selector, selected_date: @selected_date, date_format: @date_format, **@calendar_attrs)
|
|
49
52
|
end
|
|
50
53
|
end
|
|
51
54
|
end
|
|
@@ -53,6 +56,50 @@ module PhlexKit
|
|
|
53
56
|
|
|
54
57
|
private
|
|
55
58
|
|
|
59
|
+
# The outlet takes a CSS selector. "#id" is only valid for CSS-identifier
|
|
60
|
+
# ids — anything else (Rails-style "user:due.date" etc) must go through a
|
|
61
|
+
# quoted attribute selector or querySelector throws.
|
|
62
|
+
def input_selector
|
|
63
|
+
return "##{@id}" if @id.match?(/\A[A-Za-z][\w-]*\z/)
|
|
64
|
+
|
|
65
|
+
%([id="#{@id.gsub(/[\\"]/) { |c| "\\#{c}" }}"])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Mirrors the calendar controller's formatDate tokens (calendar_controller.js)
|
|
69
|
+
# so the server-rendered input matches what JS writes after a pick.
|
|
70
|
+
FORMAT_TOKENS = /yyyy|MMMM|MM|dd|HH|mm|ss|EEEE|PPPP|do/
|
|
71
|
+
|
|
72
|
+
def format_selected_date
|
|
73
|
+
return nil unless @selected_date
|
|
74
|
+
|
|
75
|
+
date = @selected_date.respond_to?(:strftime) ? @selected_date : Date.parse(@selected_date.to_s)
|
|
76
|
+
day = date.day
|
|
77
|
+
suffix = day_suffix(day)
|
|
78
|
+
day_name = Date::DAYNAMES[date.wday]
|
|
79
|
+
month_name = Date::MONTHNAMES[date.month]
|
|
80
|
+
map = {
|
|
81
|
+
"yyyy" => date.year.to_s,
|
|
82
|
+
"MMMM" => month_name,
|
|
83
|
+
"MM" => Kernel.format("%02d", date.month),
|
|
84
|
+
"dd" => Kernel.format("%02d", day),
|
|
85
|
+
"HH" => Kernel.format("%02d", date.respond_to?(:hour) ? date.hour : 0),
|
|
86
|
+
"mm" => Kernel.format("%02d", date.respond_to?(:min) ? date.min : 0),
|
|
87
|
+
"ss" => Kernel.format("%02d", date.respond_to?(:sec) ? date.sec : 0),
|
|
88
|
+
"EEEE" => day_name,
|
|
89
|
+
"do" => "#{day}#{suffix}",
|
|
90
|
+
"PPPP" => "#{day_name}, #{month_name} #{day}#{suffix}, #{date.year}"
|
|
91
|
+
}
|
|
92
|
+
@date_format.gsub(FORMAT_TOKENS) { |token| map[token] }
|
|
93
|
+
rescue Date::Error
|
|
94
|
+
@selected_date.to_s
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def day_suffix(day)
|
|
98
|
+
return "th" if day > 3 && day < 21
|
|
99
|
+
|
|
100
|
+
{ 1 => "st", 2 => "nd", 3 => "rd" }.fetch(day % 10, "th")
|
|
101
|
+
end
|
|
102
|
+
|
|
56
103
|
def trigger_attrs
|
|
57
104
|
mix({ class: "pk-date-picker-trigger" }, @trigger_attrs)
|
|
58
105
|
end
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
border-radius: .75rem; background: var(--pk-surface); color: var(--pk-text);
|
|
18
18
|
padding: var(--pk-dialog-spacing);
|
|
19
19
|
font-size: .875rem;
|
|
20
|
-
box-shadow: 0 10px 30px
|
|
20
|
+
box-shadow: 0 10px 30px color-mix(in srgb, var(--pk-shadow-color) 35%, transparent);
|
|
21
21
|
}
|
|
22
22
|
.pk-dialog[open] { display: flex; flex-direction: column; }
|
|
23
|
-
.pk-dialog::backdrop { background:
|
|
23
|
+
.pk-dialog::backdrop { background: color-mix(in srgb, var(--pk-overlay) 10%, transparent); backdrop-filter: blur(4px); }
|
|
24
24
|
@supports not (backdrop-filter: blur(1px)) {
|
|
25
|
-
.pk-dialog::backdrop { background:
|
|
25
|
+
.pk-dialog::backdrop { background: color-mix(in srgb, var(--pk-overlay) 50%, transparent); }
|
|
26
26
|
}
|
|
27
27
|
.pk-dialog.xs { max-width: min(16rem, calc(100% - 2rem)); }
|
|
28
28
|
.pk-dialog.sm { max-width: min(20rem, calc(100% - 2rem)); }
|
|
@@ -51,6 +51,6 @@
|
|
|
51
51
|
}
|
|
52
52
|
.pk-overlay-close:hover { opacity: 1; }
|
|
53
53
|
@media (min-width: 640px) {
|
|
54
|
-
.pk-dialog-header { text-align:
|
|
54
|
+
.pk-dialog-header { text-align: start; }
|
|
55
55
|
.pk-dialog-footer { flex-direction: row; justify-content: flex-end; }
|
|
56
56
|
}
|
|
@@ -24,10 +24,26 @@ export default class extends Controller {
|
|
|
24
24
|
}
|
|
25
25
|
disconnect() {
|
|
26
26
|
this.dialogTarget.removeEventListener("close", this.handleClose)
|
|
27
|
-
|
|
27
|
+
if (this.dialogTarget.open) this.handleClose()
|
|
28
|
+
}
|
|
29
|
+
open(e) {
|
|
30
|
+
e?.preventDefault()
|
|
31
|
+
this.dialogTarget.showModal()
|
|
32
|
+
// Save/restore rather than removeProperty: a host page (or another
|
|
33
|
+
// component) may have set body overflow itself.
|
|
34
|
+
this.previousOverflow = document.body.style.overflow
|
|
35
|
+
document.body.style.overflow = "hidden"
|
|
28
36
|
}
|
|
29
|
-
open(e) { e?.preventDefault(); this.dialogTarget.showModal(); document.body.style.overflow = "hidden" }
|
|
30
37
|
dismiss() { this.dialogTarget.close() }
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
// Only a click on the ::backdrop dismisses. e.target is the <dialog> for
|
|
39
|
+
// backdrop clicks, but ALSO for clicks on the dialog's own padding/flex
|
|
40
|
+
// gaps — disambiguate by testing the click point against the dialog's box.
|
|
41
|
+
backdropClick(e) {
|
|
42
|
+
if (e.target !== this.dialogTarget) return
|
|
43
|
+
if (e.detail === 0) return // synthetic/keyboard click — no coordinates
|
|
44
|
+
const r = this.dialogTarget.getBoundingClientRect()
|
|
45
|
+
const inside = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom
|
|
46
|
+
if (!inside) this.dismiss()
|
|
47
|
+
}
|
|
48
|
+
handleClose = () => { document.body.style.overflow = this.previousOverflow ?? "" }
|
|
33
49
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
position: fixed;
|
|
10
10
|
inset: 0;
|
|
11
11
|
z-index: 50;
|
|
12
|
-
background:
|
|
12
|
+
background: color-mix(in srgb, var(--pk-overlay) 10%, transparent);
|
|
13
13
|
backdrop-filter: blur(4px);
|
|
14
14
|
}
|
|
15
15
|
.pk-drawer {
|
|
@@ -87,8 +87,8 @@
|
|
|
87
87
|
padding: 1rem;
|
|
88
88
|
}
|
|
89
89
|
@media (min-width: 768px) {
|
|
90
|
-
.pk-drawer-header { text-align:
|
|
90
|
+
.pk-drawer-header { text-align: start; }
|
|
91
91
|
}
|
|
92
92
|
@supports not (backdrop-filter: blur(1px)) {
|
|
93
|
-
.pk-drawer-overlay { background:
|
|
93
|
+
.pk-drawer-overlay { background: color-mix(in srgb, var(--pk-overlay) 50%, transparent); }
|
|
94
94
|
}
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
background: var(--pk-surface);
|
|
38
38
|
color: var(--pk-text);
|
|
39
39
|
padding: .25rem;
|
|
40
|
-
box-shadow: 0 4px 16px
|
|
40
|
+
box-shadow: 0 4px 16px color-mix(in srgb, var(--pk-shadow-color) 40%, transparent);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
.pk-dropdown-menu-item {
|
|
@@ -54,9 +54,10 @@
|
|
|
54
54
|
text-decoration: none; /* href items render as <a> — no UA underline at rest */
|
|
55
55
|
outline: none;
|
|
56
56
|
}
|
|
57
|
+
/* Keyboard nav moves real focus — :focus is the highlight (no aria-selected:
|
|
58
|
+
that attribute is unsupported on menuitems and AT ignores it). */
|
|
57
59
|
.pk-dropdown-menu-item:hover,
|
|
58
|
-
.pk-dropdown-menu-item:focus
|
|
59
|
-
.pk-dropdown-menu-item[aria-selected="true"] { background: var(--pk-accent); text-decoration: none; }
|
|
60
|
+
.pk-dropdown-menu-item:focus { background: var(--pk-accent); text-decoration: none; }
|
|
60
61
|
|
|
61
62
|
.pk-dropdown-menu-label { margin: 0; padding: .375rem .5rem; font-size: .875rem; font-weight: 600; }
|
|
62
63
|
.pk-dropdown-menu-separator { height: 1px; margin: .25rem -.25rem; background: var(--pk-border); }
|
|
@@ -75,7 +76,7 @@
|
|
|
75
76
|
|
|
76
77
|
/* Keyboard hint, pushed to the row's end. */
|
|
77
78
|
.pk-dropdown-menu-shortcut {
|
|
78
|
-
margin-
|
|
79
|
+
margin-inline-start: auto;
|
|
79
80
|
font-size: .75rem;
|
|
80
81
|
letter-spacing: .1em;
|
|
81
82
|
color: var(--pk-muted);
|
|
@@ -111,7 +112,7 @@
|
|
|
111
112
|
The row is its own scoped anchor; the panel flips to the other side at
|
|
112
113
|
the viewport right edge. */
|
|
113
114
|
.pk-dropdown-menu-sub { anchor-name: --pk-dropdown-menu-sub; anchor-scope: --pk-dropdown-menu-sub; }
|
|
114
|
-
.pk-dropdown-menu-sub-chevron { margin-
|
|
115
|
+
.pk-dropdown-menu-sub-chevron { margin-inline-start: auto; width: 1rem; height: 1rem; }
|
|
115
116
|
.pk-dropdown-menu-sub-content {
|
|
116
117
|
position: fixed;
|
|
117
118
|
position-anchor: --pk-dropdown-menu-sub;
|