phlex_kit 0.13.0 → 0.14.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/components/phlex_kit/accordion/accordion_content.rb +3 -1
- data/app/components/phlex_kit/accordion/accordion_default_trigger.rb +1 -1
- data/app/components/phlex_kit/accordion/accordion_trigger.rb +1 -1
- data/app/components/phlex_kit/alert/alert.rb +5 -1
- data/app/components/phlex_kit/alert_dialog/alert_dialog_controller.js +16 -6
- data/app/components/phlex_kit/alert_dialog/alert_dialog_media.rb +4 -1
- data/app/components/phlex_kit/aspect_ratio/aspect_ratio.rb +14 -4
- data/app/components/phlex_kit/attachment/attachment.css +7 -1
- data/app/components/phlex_kit/attachment/scroll_fade_controller.js +4 -3
- data/app/components/phlex_kit/avatar/avatar.css +4 -0
- data/app/components/phlex_kit/breadcrumb/breadcrumb.css +4 -0
- data/app/components/phlex_kit/breadcrumb/breadcrumb.rb +8 -1
- data/app/components/phlex_kit/breadcrumb/breadcrumb_page.rb +1 -1
- data/app/components/phlex_kit/breadcrumb/breadcrumb_separator.rb +1 -1
- data/app/components/phlex_kit/bubble/bubble_content.rb +5 -1
- data/app/components/phlex_kit/button_group/button_group_separator.rb +1 -1
- data/app/components/phlex_kit/calendar/calendar.css +5 -0
- data/app/components/phlex_kit/calendar/calendar_controller.js +26 -8
- data/app/components/phlex_kit/calendar/calendar_next.rb +5 -3
- data/app/components/phlex_kit/calendar/calendar_prev.rb +5 -3
- data/app/components/phlex_kit/carousel/carousel.css +7 -0
- data/app/components/phlex_kit/carousel/carousel.rb +8 -4
- data/app/components/phlex_kit/carousel/carousel_controller.js +64 -20
- data/app/components/phlex_kit/carousel/carousel_item.rb +5 -5
- data/app/components/phlex_kit/chart/chart_controller.js +5 -1
- data/app/components/phlex_kit/checkbox/checkbox.css +16 -0
- data/app/components/phlex_kit/checkbox/checkbox.rb +5 -1
- data/app/components/phlex_kit/clipboard/clipboard.rb +7 -0
- data/app/components/phlex_kit/clipboard/clipboard_controller.js +12 -1
- data/app/components/phlex_kit/clipboard/clipboard_popover.rb +5 -3
- data/app/components/phlex_kit/combobox/combobox.rb +1 -1
- data/app/components/phlex_kit/combobox/combobox_clear_button.rb +5 -3
- data/app/components/phlex_kit/combobox/combobox_controller.js +56 -5
- data/app/components/phlex_kit/combobox/combobox_item.rb +7 -4
- data/app/components/phlex_kit/command/command_controller.js +9 -1
- data/app/components/phlex_kit/command/command_dialog_content.rb +10 -6
- data/app/components/phlex_kit/command/command_empty.rb +5 -3
- data/app/components/phlex_kit/command/command_separator.rb +1 -1
- data/app/components/phlex_kit/context_menu/context_menu_checkbox_item.rb +7 -5
- data/app/components/phlex_kit/context_menu/context_menu_content.rb +7 -1
- data/app/components/phlex_kit/context_menu/context_menu_controller.js +17 -2
- data/app/components/phlex_kit/context_menu/context_menu_group.rb +4 -1
- data/app/components/phlex_kit/context_menu/context_menu_item.rb +5 -2
- data/app/components/phlex_kit/context_menu/context_menu_radio_group.rb +4 -1
- data/app/components/phlex_kit/context_menu/context_menu_radio_item.rb +7 -5
- data/app/components/phlex_kit/context_menu/context_menu_separator.rb +6 -1
- data/app/components/phlex_kit/context_menu/context_menu_sub_content.rb +4 -1
- data/app/components/phlex_kit/context_menu/context_menu_sub_trigger.rb +12 -9
- data/app/components/phlex_kit/data_table/data_table.css +4 -0
- data/app/components/phlex_kit/data_table/data_table_search_controller.js +15 -1
- data/app/components/phlex_kit/data_table/data_table_select_all_checkbox.rb +8 -4
- data/app/components/phlex_kit/drawer/drawer.css +4 -0
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_checkbox_item.rb +7 -5
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_content.rb +4 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_controller.js +10 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_group.rb +4 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_item.rb +3 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_radio_group.rb +4 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_radio_item.rb +7 -5
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_separator.rb +6 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub_content.rb +4 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub_trigger.rb +12 -9
- data/app/components/phlex_kit/empty/empty.rb +1 -0
- data/app/components/phlex_kit/field/field.rb +4 -1
- data/app/components/phlex_kit/field/field_error.rb +4 -1
- data/app/components/phlex_kit/form_field/form_field_controller.js +22 -5
- data/app/components/phlex_kit/icon/icon.rb +5 -1
- data/app/components/phlex_kit/input_group/input_group.css +6 -3
- data/app/components/phlex_kit/input_otp/input_otp_controller.js +14 -6
- data/app/components/phlex_kit/input_otp/input_otp_slot.rb +7 -6
- data/app/components/phlex_kit/item/item.css +9 -1
- data/app/components/phlex_kit/item/item_separator.rb +1 -1
- data/app/components/phlex_kit/label/label.css +6 -1
- data/app/components/phlex_kit/menubar/menubar.rb +5 -3
- data/app/components/phlex_kit/menubar/menubar_checkbox_item.rb +6 -6
- data/app/components/phlex_kit/menubar/menubar_content.rb +5 -1
- data/app/components/phlex_kit/menubar/menubar_controller.js +11 -1
- data/app/components/phlex_kit/menubar/menubar_group.rb +4 -1
- data/app/components/phlex_kit/menubar/menubar_item.rb +4 -3
- data/app/components/phlex_kit/menubar/menubar_menu.rb +4 -1
- data/app/components/phlex_kit/menubar/menubar_radio_group.rb +4 -1
- data/app/components/phlex_kit/menubar/menubar_radio_item.rb +6 -6
- data/app/components/phlex_kit/menubar/menubar_separator.rb +6 -1
- data/app/components/phlex_kit/menubar/menubar_sub_content.rb +4 -1
- data/app/components/phlex_kit/menubar/menubar_sub_trigger.rb +12 -11
- data/app/components/phlex_kit/menubar/menubar_trigger.rb +7 -4
- data/app/components/phlex_kit/message_scroller/message_scroller_controller.js +5 -0
- data/app/components/phlex_kit/pagination/pagination.css +4 -0
- data/app/components/phlex_kit/radio_button/radio_button.rb +1 -1
- data/app/components/phlex_kit/resizable/resizable_controller.js +17 -4
- data/app/components/phlex_kit/resizable/resizable_handle.rb +9 -7
- data/app/components/phlex_kit/select/select.rb +1 -1
- data/app/components/phlex_kit/select/select_content.rb +7 -5
- data/app/components/phlex_kit/select/select_controller.js +10 -0
- data/app/components/phlex_kit/select/select_separator.rb +1 -1
- data/app/components/phlex_kit/sheet/sheet_content_controller.js +12 -5
- data/app/components/phlex_kit/sidebar/sidebar_group_action.rb +1 -1
- data/app/components/phlex_kit/sidebar/sidebar_menu_action.rb +1 -1
- data/app/components/phlex_kit/sidebar/sidebar_rail.rb +3 -2
- data/app/components/phlex_kit/sidebar/sidebar_trigger.rb +2 -1
- data/app/components/phlex_kit/switch/switch.rb +4 -1
- data/app/components/phlex_kit/tabs/tabs_content.rb +6 -1
- data/app/components/phlex_kit/tabs/tabs_trigger.rb +3 -1
- data/app/components/phlex_kit/theme_toggle/theme_toggle_controller.js +24 -6
- data/app/components/phlex_kit/toast/toast_controller.js +46 -1
- data/app/components/phlex_kit/toast/toaster_controller.js +29 -1
- data/app/components/phlex_kit/toggle/toggle.rb +1 -1
- data/app/components/phlex_kit/toggle_group/toggle_group.rb +18 -4
- data/app/components/phlex_kit/toggle_group/toggle_group_item.rb +2 -2
- data/app/components/phlex_kit/tooltip/tooltip_controller.js +5 -0
- data/app/components/phlex_kit/typography/typography.css +5 -2
- data/lib/phlex_kit/version.rb +1 -1
- metadata +1 -1
|
@@ -5,7 +5,10 @@ module PhlexKit
|
|
|
5
5
|
class DropdownMenuGroup < BaseComponent
|
|
6
6
|
def initialize(**attrs) = (@attrs = attrs)
|
|
7
7
|
def view_template(&)
|
|
8
|
-
|
|
8
|
+
base = { class: "pk-dropdown-menu-group" }
|
|
9
|
+
# Default only when the caller didn't supply their own — `mix` fuses.
|
|
10
|
+
base[:role] = "group" unless attr_set?(:role)
|
|
11
|
+
div(**mix(base, @attrs), &)
|
|
9
12
|
end
|
|
10
13
|
end
|
|
11
14
|
end
|
|
@@ -13,11 +13,12 @@ module PhlexKit
|
|
|
13
13
|
|
|
14
14
|
def view_template(&block)
|
|
15
15
|
base = {
|
|
16
|
-
role: "menuitem",
|
|
17
|
-
tabindex: "-1",
|
|
18
16
|
class: [ "pk-dropdown-menu-item", fetch_option(VARIANTS, @variant, :variant) ].compact.join(" "),
|
|
19
17
|
data: { phlex_kit__dropdown_menu_target: "menuItem", action: "click->phlex-kit--dropdown-menu#close" }
|
|
20
18
|
}
|
|
19
|
+
# Defaults only when the caller didn't supply their own — `mix` fuses.
|
|
20
|
+
base[:role] = "menuitem" unless attr_set?(:role)
|
|
21
|
+
base[:tabindex] = "-1" unless attr_set?(:tabindex)
|
|
21
22
|
base[:href] = @href unless @as == :div
|
|
22
23
|
send(@as, **mix(base, @attrs), &block)
|
|
23
24
|
end
|
|
@@ -4,7 +4,10 @@ module PhlexKit
|
|
|
4
4
|
class DropdownMenuRadioGroup < BaseComponent
|
|
5
5
|
def initialize(**attrs) = (@attrs = attrs)
|
|
6
6
|
def view_template(&)
|
|
7
|
-
|
|
7
|
+
base = { class: "pk-dropdown-menu-group" }
|
|
8
|
+
# Default only when the caller didn't supply their own — `mix` fuses.
|
|
9
|
+
base[:role] = "radiogroup" unless attr_set?(:role)
|
|
10
|
+
div(**mix(base, @attrs), &)
|
|
8
11
|
end
|
|
9
12
|
end
|
|
10
13
|
end
|
|
@@ -11,13 +11,15 @@ module PhlexKit
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def view_template(&block)
|
|
14
|
-
|
|
14
|
+
base = {
|
|
15
15
|
class: "pk-dropdown-menu-item pk-dropdown-menu-radio-item",
|
|
16
|
-
role: "menuitemradio",
|
|
17
|
-
tabindex: "-1",
|
|
18
|
-
aria: { checked: @checked ? "true" : "false" },
|
|
19
16
|
data: { phlex_kit__dropdown_menu_target: "menuItem" }
|
|
20
|
-
}
|
|
17
|
+
}
|
|
18
|
+
# Defaults only when the caller didn't supply their own — `mix` fuses.
|
|
19
|
+
base[:role] = "menuitemradio" unless attr_set?(:role)
|
|
20
|
+
base[:tabindex] = "-1" unless attr_set?(:tabindex)
|
|
21
|
+
base[:"aria-checked"] = (@checked ? "true" : "false") unless aria_key_set?(:checked)
|
|
22
|
+
label(**mix(base, @attrs)) do
|
|
21
23
|
input(type: :radio, class: "pk-dropdown-menu-item-input", name: @name, value: @value, checked: @checked,
|
|
22
24
|
tabindex: "-1", data: { action: "change->phlex-kit--dropdown-menu#syncChecked" })
|
|
23
25
|
span(class: "pk-dropdown-menu-item-indicator", aria: { hidden: "true" }) do
|
|
@@ -6,7 +6,12 @@ module PhlexKit
|
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
def view_template
|
|
9
|
-
|
|
9
|
+
base = { class: "pk-dropdown-menu-separator" }
|
|
10
|
+
# Defaults only when the caller didn't supply their own — `mix` fuses,
|
|
11
|
+
# and aria_key_set? covers the aria: hash spelling the flat key missed.
|
|
12
|
+
base[:role] = "separator" unless attr_set?(:role)
|
|
13
|
+
base[:"aria-orientation"] = "horizontal" unless aria_key_set?(:orientation)
|
|
14
|
+
div(**mix(base, @attrs))
|
|
10
15
|
end
|
|
11
16
|
end
|
|
12
17
|
end
|
|
@@ -4,7 +4,10 @@ module PhlexKit
|
|
|
4
4
|
class DropdownMenuSubContent < BaseComponent
|
|
5
5
|
def initialize(**attrs) = (@attrs = attrs)
|
|
6
6
|
def view_template(&)
|
|
7
|
-
|
|
7
|
+
base = { class: "pk-dropdown-menu-sub-content" }
|
|
8
|
+
# Default only when the caller didn't supply their own — `mix` fuses.
|
|
9
|
+
base[:role] = "menu" unless attr_set?(:role)
|
|
10
|
+
div(**mix(base, @attrs)) do
|
|
8
11
|
div(class: "pk-dropdown-menu-viewport", &)
|
|
9
12
|
end
|
|
10
13
|
end
|
|
@@ -4,17 +4,20 @@ module PhlexKit
|
|
|
4
4
|
class DropdownMenuSubTrigger < BaseComponent
|
|
5
5
|
def initialize(**attrs) = (@attrs = attrs)
|
|
6
6
|
def view_template(&block)
|
|
7
|
-
|
|
7
|
+
base = {
|
|
8
8
|
class: "pk-dropdown-menu-item pk-dropdown-menu-sub-trigger",
|
|
9
|
-
role: "menuitem",
|
|
10
|
-
# -1: roving focus reaches it via arrows (focus-within opens the sub);
|
|
11
|
-
# tabindex 0 made it a stray tab stop inside the menu.
|
|
12
|
-
tabindex: "-1",
|
|
13
|
-
# expanded starts false and is mirrored live by the controller's
|
|
14
|
-
# syncSub (the reveal itself is pure CSS :hover/:focus-within).
|
|
15
|
-
aria: { haspopup: "menu", expanded: "false" },
|
|
16
9
|
data: { phlex_kit__dropdown_menu_target: "menuItem" }
|
|
17
|
-
}
|
|
10
|
+
}
|
|
11
|
+
# Defaults only when the caller didn't supply their own — `mix` fuses.
|
|
12
|
+
base[:role] = "menuitem" unless attr_set?(:role)
|
|
13
|
+
# -1: roving focus reaches it via arrows (focus-within opens the sub);
|
|
14
|
+
# tabindex 0 made it a stray tab stop inside the menu.
|
|
15
|
+
base[:tabindex] = "-1" unless attr_set?(:tabindex)
|
|
16
|
+
base[:"aria-haspopup"] = "menu" unless aria_key_set?(:haspopup)
|
|
17
|
+
# expanded starts false and is mirrored live by the controller's
|
|
18
|
+
# syncSub (the reveal itself is pure CSS :hover/:focus-within).
|
|
19
|
+
base[:"aria-expanded"] = "false" unless aria_key_set?(:expanded)
|
|
20
|
+
div(**mix(base, @attrs)) do
|
|
18
21
|
block&.call
|
|
19
22
|
render Icon.new(:chevron_right, size: nil, class: "pk-dropdown-menu-sub-chevron")
|
|
20
23
|
end
|
|
@@ -29,7 +29,10 @@ module PhlexKit
|
|
|
29
29
|
data = { slot: "field", orientation: fetch_option(ORIENTATIONS, @orientation, :orientation) }
|
|
30
30
|
data[:invalid] = "true" if @invalid
|
|
31
31
|
data[:disabled] = "true" if @disabled
|
|
32
|
-
|
|
32
|
+
base = { class: "pk-field", data: data }
|
|
33
|
+
# Default only when the caller didn't supply their own — `mix` fuses.
|
|
34
|
+
base[:role] = "group" unless attr_set?(:role)
|
|
35
|
+
div(**mix(base, @attrs), &)
|
|
33
36
|
end
|
|
34
37
|
end
|
|
35
38
|
end
|
|
@@ -13,7 +13,10 @@ module PhlexKit
|
|
|
13
13
|
def view_template(&block)
|
|
14
14
|
return if @errors.empty? && !block
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
base = { class: "pk-field-error", data: { slot: "field-error" } }
|
|
17
|
+
# Default only when the caller didn't supply their own — `mix` fuses.
|
|
18
|
+
base[:role] = "alert" unless attr_set?(:role)
|
|
19
|
+
div(**mix(base, @attrs)) do
|
|
17
20
|
if @errors.length == 1
|
|
18
21
|
plain @errors.first
|
|
19
22
|
elsif @errors.length > 1
|
|
@@ -49,11 +49,28 @@ export default class extends Controller {
|
|
|
49
49
|
error.preventDefault();
|
|
50
50
|
|
|
51
51
|
// preventDefault also cancels the browser's focus-first-invalid — restore
|
|
52
|
-
// it for the
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
// it for the FIRST invalid control of the submit pass. Coordinate across
|
|
53
|
+
// sibling FormField controllers via a flag on the shared form: invalid
|
|
54
|
+
// events fire in document order, so the first control to claim wins. A
|
|
55
|
+
// per-controller flag alone let each FormField focus its own control, so
|
|
56
|
+
// the LAST invalid field ended up focused, inverting native behavior. No
|
|
57
|
+
// shared JS util (kit rule) — the form element itself is the coordination
|
|
58
|
+
// point. The reset runs on a macrotask, NOT queueMicrotask: a microtask
|
|
59
|
+
// checkpoint can drain between two invalid events (the JS stack empties
|
|
60
|
+
// after each listener), clearing the flag mid-pass so the next field
|
|
61
|
+
// re-claims. setTimeout survives the whole synchronous validation pass and
|
|
62
|
+
// resets before the next submit. A control with no form (rare) falls back
|
|
63
|
+
// to the per-controller flag.
|
|
64
|
+
const form = error.target.form;
|
|
65
|
+
const alreadyClaimed = form ? form.dataset.pkInvalidFocused : this._focusedInvalid;
|
|
66
|
+
if (!alreadyClaimed) {
|
|
67
|
+
if (form) {
|
|
68
|
+
form.dataset.pkInvalidFocused = "1";
|
|
69
|
+
setTimeout(() => { delete form.dataset.pkInvalidFocused; }, 0);
|
|
70
|
+
} else {
|
|
71
|
+
this._focusedInvalid = true;
|
|
72
|
+
setTimeout(() => { this._focusedInvalid = false; }, 0);
|
|
73
|
+
}
|
|
57
74
|
error.target.focus({ preventScroll: true });
|
|
58
75
|
error.target.scrollIntoView({ block: "nearest" });
|
|
59
76
|
}
|
|
@@ -23,9 +23,13 @@ module PhlexKit
|
|
|
23
23
|
base = base_attrs(icon)
|
|
24
24
|
# `mix` merges duplicate string attrs ("16 24") — drop a generated attr
|
|
25
25
|
# whenever the caller supplies their own copy, so theirs wins.
|
|
26
|
-
%i[width height viewbox
|
|
26
|
+
%i[width height viewbox].each do |key|
|
|
27
27
|
base.delete(key) if attr_set?(key)
|
|
28
28
|
end
|
|
29
|
+
# aria-hidden needs the aria-aware guard: the aria: hash and underscore
|
|
30
|
+
# spellings must also suppress the default (a caller-supplied
|
|
31
|
+
# accessible name must not be hidden from AT by our default).
|
|
32
|
+
base.delete(:"aria-hidden") if aria_key_set?(:hidden) || aria_labelled?
|
|
29
33
|
svg(**mix(base, @attrs)) do |s|
|
|
30
34
|
icon[:elements].each { |tag, attrs| s.public_send(tag, **attrs) }
|
|
31
35
|
end
|
|
@@ -34,7 +34,10 @@
|
|
|
34
34
|
border-color: color-mix(in oklab, var(--pk-red) 50%, transparent);
|
|
35
35
|
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 40%, transparent);
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
/* Group-disabled presentation follows the CONTROL only (the direct-child
|
|
38
|
+
input/textarea/select) — a disabled addon button must not dim a group whose
|
|
39
|
+
input is still editable. */
|
|
40
|
+
.pk-input-group:has(> input:disabled, > textarea:disabled, > select:disabled) {
|
|
38
41
|
opacity: .5;
|
|
39
42
|
background: color-mix(in oklab, var(--pk-input) 80%, transparent);
|
|
40
43
|
}
|
|
@@ -97,14 +100,14 @@
|
|
|
97
100
|
.pk-input-group-text svg { pointer-events: none; }
|
|
98
101
|
|
|
99
102
|
:root[data-theme="light"] .pk-input-group { background: transparent; }
|
|
100
|
-
:root[data-theme="light"] .pk-input-group:has(:disabled) { background: color-mix(in oklab, var(--pk-input) 50%, transparent); }
|
|
103
|
+
:root[data-theme="light"] .pk-input-group:has(> input:disabled, > textarea:disabled, > select:disabled) { background: color-mix(in oklab, var(--pk-input) 50%, transparent); }
|
|
101
104
|
:root[data-theme="light"] .pk-input-group:has([aria-invalid="true"]) {
|
|
102
105
|
border-color: var(--pk-red);
|
|
103
106
|
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent);
|
|
104
107
|
}
|
|
105
108
|
@media (prefers-color-scheme: light) {
|
|
106
109
|
:root[data-theme="system"] .pk-input-group { background: transparent; }
|
|
107
|
-
:root[data-theme="system"] .pk-input-group:has(:disabled) { background: color-mix(in oklab, var(--pk-input) 50%, transparent); }
|
|
110
|
+
:root[data-theme="system"] .pk-input-group:has(> input:disabled, > textarea:disabled, > select:disabled) { background: color-mix(in oklab, var(--pk-input) 50%, transparent); }
|
|
108
111
|
:root[data-theme="system"] .pk-input-group:has([aria-invalid="true"]) {
|
|
109
112
|
border-color: var(--pk-red);
|
|
110
113
|
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent);
|
|
@@ -90,12 +90,15 @@ export default class extends Controller {
|
|
|
90
90
|
prev.dataset.otpValue = ""
|
|
91
91
|
prev.focus()
|
|
92
92
|
this.syncValue()
|
|
93
|
-
} else if (e.key === "ArrowLeft"
|
|
93
|
+
} else if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
|
|
94
|
+
// The slot row mirrors in RTL (all logical CSS), so the physical
|
|
95
|
+
// arrows flip: ArrowLeft moves toward the NEXT slot there. Runtime
|
|
96
|
+
// dir check — reliable after a dynamic flip, unlike :dir().
|
|
97
|
+
const rtl = getComputedStyle(this.element).direction === "rtl"
|
|
98
|
+
const delta = (e.key === "ArrowLeft") !== rtl ? -1 : 1
|
|
99
|
+
if (delta === -1 && index === 0) return
|
|
94
100
|
e.preventDefault()
|
|
95
|
-
this.focusSlot(index
|
|
96
|
-
} else if (e.key === "ArrowRight") {
|
|
97
|
-
e.preventDefault()
|
|
98
|
-
this.focusSlot(index + 1)
|
|
101
|
+
this.focusSlot(index + delta)
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
104
|
|
|
@@ -113,7 +116,12 @@ export default class extends Controller {
|
|
|
113
116
|
// (shared by paste and autofill), then park focus after the last filled one.
|
|
114
117
|
fillFrom(slot, text) {
|
|
115
118
|
const chars = text.split("")
|
|
116
|
-
|
|
119
|
+
// A full-length (or longer) code is distributed from the FIRST slot no
|
|
120
|
+
// matter which one is focused — pasting a complete code into a middle
|
|
121
|
+
// slot otherwise silently truncated it (only the slots from focus onward
|
|
122
|
+
// got filled) and merged the run with whatever digits sat before it.
|
|
123
|
+
// A shorter (partial) paste still fills from the focused slot.
|
|
124
|
+
const start = chars.length >= this.slotTargets.length ? 0 : this.slotTargets.indexOf(slot)
|
|
117
125
|
this.slotTargets.slice(start).forEach((s, i) => { if (chars[i] != null) { s.value = chars[i]; s.dataset.otpValue = chars[i] } })
|
|
118
126
|
this.focusSlot(Math.min(start + chars.length, this.slotTargets.length - 1))
|
|
119
127
|
this.syncValue()
|
|
@@ -6,12 +6,13 @@ module PhlexKit
|
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
def view_template
|
|
9
|
-
input
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
base = { type: :text, class: "pk-input-otp-slot" }
|
|
10
|
+
# Defaults only when the caller didn't supply their own — `mix` fuses
|
|
11
|
+
# (e.g. inputmode: "text" for alphanumeric codes).
|
|
12
|
+
base[:inputmode] = "numeric" unless attr_set?(:inputmode)
|
|
13
|
+
base[:autocomplete] = "one-time-code" unless attr_set?(:autocomplete)
|
|
14
|
+
base[:maxlength] = "1" unless attr_set?(:maxlength)
|
|
15
|
+
input(**mix(base, {
|
|
15
16
|
data: {
|
|
16
17
|
phlex_kit__input_otp_target: "slot",
|
|
17
18
|
action: [
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
text-decoration: none;
|
|
17
17
|
}
|
|
18
18
|
a.pk-item:hover { background: var(--pk-surface-2); }
|
|
19
|
+
a.pk-item:focus-visible {
|
|
20
|
+
outline: none;
|
|
21
|
+
border-color: var(--pk-ring);
|
|
22
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
23
|
+
}
|
|
19
24
|
.pk-item.outline { border-color: var(--pk-border); }
|
|
20
25
|
.pk-item.muted { background: color-mix(in oklab, var(--pk-surface-2) 50%, transparent); }
|
|
21
26
|
.pk-item.xs { gap: .5rem; padding: .5rem .625rem; }
|
|
@@ -62,8 +67,11 @@ a.pk-item:hover { background: var(--pk-surface-2); }
|
|
|
62
67
|
font-size: .875rem;
|
|
63
68
|
line-height: 1.5;
|
|
64
69
|
text-align: start;
|
|
70
|
+
/* shadcn's ItemDescription line-clamps to 2 lines (line-clamp-2). */
|
|
71
|
+
display: -webkit-box;
|
|
72
|
+
-webkit-box-orient: vertical;
|
|
73
|
+
-webkit-line-clamp: 2;
|
|
65
74
|
overflow: hidden;
|
|
66
|
-
text-overflow: ellipsis;
|
|
67
75
|
}
|
|
68
76
|
.pk-item.xs .pk-item-description { font-size: .75rem; }
|
|
69
77
|
.pk-item-actions { display: flex; align-items: center; gap: .5rem; flex: none; }
|
|
@@ -6,7 +6,7 @@ module PhlexKit
|
|
|
6
6
|
def view_template
|
|
7
7
|
# Purely decorative — aria-hidden only (role="separator" would
|
|
8
8
|
# contradict aria-hidden and confuse AT).
|
|
9
|
-
div(**mix({ class: "pk-item-separator", aria: { hidden: true } }, @attrs))
|
|
9
|
+
div(**mix({ class: "pk-item-separator", aria: { hidden: "true" } }, @attrs))
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
12
|
end
|
|
@@ -9,4 +9,9 @@
|
|
|
9
9
|
line-height: 1;
|
|
10
10
|
user-select: none;
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
/* The wrapper arms cover controls whose root is a <label> around a nested
|
|
13
|
+
input (e.g. Switch's <label class="pk-switch">) — scoped to label so an
|
|
14
|
+
unrelated container with a disabled control somewhere inside can't dim a
|
|
15
|
+
neighboring Label. */
|
|
16
|
+
.pk-label:has(+ :disabled), .pk-label:has(:disabled), :disabled + .pk-label,
|
|
17
|
+
.pk-label:has(+ label:has(:disabled)), label:has(:disabled) + .pk-label { cursor: not-allowed; opacity: .5; }
|
|
@@ -11,9 +11,8 @@ module PhlexKit
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def view_template(&)
|
|
14
|
-
|
|
14
|
+
base = {
|
|
15
15
|
class: "pk-menubar",
|
|
16
|
-
role: "menubar",
|
|
17
16
|
data: {
|
|
18
17
|
controller: "phlex-kit--menubar",
|
|
19
18
|
# focusout: tabbing out of the bar closes the open [popover=manual]
|
|
@@ -22,7 +21,10 @@ module PhlexKit
|
|
|
22
21
|
# NOT bound by navigation_menu.rb — nav menus are non-modal).
|
|
23
22
|
action: "mousedown@window->phlex-kit--menubar#onMousedownOutside click@window->phlex-kit--menubar#onClickOutside keydown->phlex-kit--menubar#onKeydown focusout->phlex-kit--menubar#onFocusout"
|
|
24
23
|
}
|
|
25
|
-
}
|
|
24
|
+
}
|
|
25
|
+
# Default only when the caller didn't supply their own — `mix` fuses.
|
|
26
|
+
base[:role] = "menubar" unless attr_set?(:role)
|
|
27
|
+
div(**mix(base, @attrs), &)
|
|
26
28
|
end
|
|
27
29
|
end
|
|
28
30
|
end
|
|
@@ -11,12 +11,12 @@ module PhlexKit
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def view_template(&block)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
base = { class: "pk-menubar-item pk-menubar-checkbox-item" }
|
|
15
|
+
# Defaults only when the caller didn't supply their own — `mix` fuses.
|
|
16
|
+
base[:role] = "menuitemcheckbox" unless attr_set?(:role)
|
|
17
|
+
base[:tabindex] = "-1" unless attr_set?(:tabindex)
|
|
18
|
+
base[:"aria-checked"] = (@checked ? "true" : "false") unless aria_key_set?(:checked)
|
|
19
|
+
label(**mix(base, @attrs)) do
|
|
20
20
|
input(type: :checkbox, class: "pk-menubar-item-input", name: @name, value: @value, checked: @checked,
|
|
21
21
|
tabindex: "-1", data: { action: "change->phlex-kit--menubar#syncChecked" })
|
|
22
22
|
span(class: "pk-menubar-item-indicator", aria: { hidden: "true" }) do
|
|
@@ -9,7 +9,11 @@ module PhlexKit
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def view_template(&)
|
|
12
|
-
|
|
12
|
+
base = { id: @id, class: "pk-menubar-content" }
|
|
13
|
+
# Defaults only when the caller didn't supply their own — `mix` fuses.
|
|
14
|
+
base[:popover] = "manual" unless attr_set?(:popover)
|
|
15
|
+
base[:role] = "menu" unless attr_set?(:role)
|
|
16
|
+
div(**mix(base, @attrs), &)
|
|
13
17
|
end
|
|
14
18
|
end
|
|
15
19
|
end
|
|
@@ -19,7 +19,17 @@ let uid = 0
|
|
|
19
19
|
// dedupes re-arming with the same fn. If the gesture's click never fires
|
|
20
20
|
// (e.g. suppressed after a selection drag) the armed swallow eats the next
|
|
21
21
|
// one — rare enough to accept.
|
|
22
|
-
const swallowClick = (ev) =>
|
|
22
|
+
const swallowClick = (ev) => {
|
|
23
|
+
// preventDefault alone suppresses only native default actions; the
|
|
24
|
+
// dismissing outside click would still fire addEventListener handlers on
|
|
25
|
+
// whatever sits under the pointer. stopPropagation (this listener is
|
|
26
|
+
// capture-phase on window, the outermost node, so it runs before any deeper
|
|
27
|
+
// handler) starves those too — the modal-menu contract is that an outside
|
|
28
|
+
// click ONLY dismisses. Only menubar arms this; navigation_menu (non-modal)
|
|
29
|
+
// never does, so nav-menu outside clicks still pass through.
|
|
30
|
+
ev.preventDefault()
|
|
31
|
+
ev.stopPropagation()
|
|
32
|
+
}
|
|
23
33
|
function armSwallowClick() {
|
|
24
34
|
window.addEventListener("click", swallowClick, { once: true, capture: true })
|
|
25
35
|
}
|
|
@@ -4,7 +4,10 @@ module PhlexKit
|
|
|
4
4
|
class MenubarGroup < BaseComponent
|
|
5
5
|
def initialize(**attrs) = (@attrs = attrs)
|
|
6
6
|
def view_template(&)
|
|
7
|
-
|
|
7
|
+
base = { class: "pk-menubar-group" }
|
|
8
|
+
# Default only when the caller didn't supply their own — `mix` fuses.
|
|
9
|
+
base[:role] = "group" unless attr_set?(:role)
|
|
10
|
+
div(**mix(base, @attrs), &)
|
|
8
11
|
end
|
|
9
12
|
end
|
|
10
13
|
end
|
|
@@ -18,12 +18,13 @@ module PhlexKit
|
|
|
18
18
|
|
|
19
19
|
def view_template(&block)
|
|
20
20
|
base = {
|
|
21
|
-
role: "menuitem",
|
|
22
|
-
tabindex: "-1",
|
|
23
21
|
class: [ "pk-menubar-item", fetch_option(VARIANTS, @variant, :variant), ("inset" if @inset) ].compact.join(" "),
|
|
24
22
|
data: @disabled ? { disabled: "true" } : { action: "click->phlex-kit--menubar#close" }
|
|
25
23
|
}
|
|
26
|
-
|
|
24
|
+
# Defaults only when the caller didn't supply their own — `mix` fuses.
|
|
25
|
+
base[:role] = "menuitem" unless attr_set?(:role)
|
|
26
|
+
base[:tabindex] = "-1" unless attr_set?(:tabindex)
|
|
27
|
+
base[:"aria-disabled"] = "true" if @disabled && !aria_key_set?(:disabled)
|
|
27
28
|
# No default href: "#" would make Enter/click navigate (hash change +
|
|
28
29
|
# scroll-to-top). tabindex="-1" keeps the row programmatically focusable.
|
|
29
30
|
base[:href] = @href unless @href.nil? || @as == :div || @disabled
|
|
@@ -3,10 +3,13 @@ module PhlexKit
|
|
|
3
3
|
class MenubarMenu < BaseComponent
|
|
4
4
|
def initialize(**attrs) = (@attrs = attrs)
|
|
5
5
|
def view_template(&)
|
|
6
|
+
base = { class: "pk-menubar-menu", data: { phlex_kit__menubar_target: "menu" } }
|
|
6
7
|
# role="none": role="menubar" only allows menuitem children in the
|
|
7
8
|
# accessibility tree — this layout wrapper must be presentational so
|
|
8
9
|
# the tree flattens to menubar > menuitem (axe: required-children).
|
|
9
|
-
|
|
10
|
+
# Default only when the caller didn't supply their own — `mix` fuses.
|
|
11
|
+
base[:role] = "none" unless attr_set?(:role)
|
|
12
|
+
div(**mix(base, @attrs), &)
|
|
10
13
|
end
|
|
11
14
|
end
|
|
12
15
|
end
|
|
@@ -6,7 +6,10 @@ module PhlexKit
|
|
|
6
6
|
class MenubarRadioGroup < BaseComponent
|
|
7
7
|
def initialize(**attrs) = (@attrs = attrs)
|
|
8
8
|
def view_template(&)
|
|
9
|
-
|
|
9
|
+
base = { class: "pk-menubar-group" }
|
|
10
|
+
# Default only when the caller didn't supply their own — `mix` fuses.
|
|
11
|
+
base[:role] = "group" unless attr_set?(:role)
|
|
12
|
+
div(**mix(base, @attrs), &)
|
|
10
13
|
end
|
|
11
14
|
end
|
|
12
15
|
end
|
|
@@ -11,12 +11,12 @@ module PhlexKit
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def view_template(&block)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
base = { class: "pk-menubar-item pk-menubar-radio-item" }
|
|
15
|
+
# Defaults only when the caller didn't supply their own — `mix` fuses.
|
|
16
|
+
base[:role] = "menuitemradio" unless attr_set?(:role)
|
|
17
|
+
base[:tabindex] = "-1" unless attr_set?(:tabindex)
|
|
18
|
+
base[:"aria-checked"] = (@checked ? "true" : "false") unless aria_key_set?(:checked)
|
|
19
|
+
label(**mix(base, @attrs)) do
|
|
20
20
|
input(type: :radio, class: "pk-menubar-item-input", name: @name, value: @value, checked: @checked,
|
|
21
21
|
tabindex: "-1", data: { action: "change->phlex-kit--menubar#syncChecked" })
|
|
22
22
|
span(class: "pk-menubar-item-indicator", aria: { hidden: "true" }) do
|
|
@@ -2,6 +2,11 @@ module PhlexKit
|
|
|
2
2
|
# See menubar.rb.
|
|
3
3
|
class MenubarSeparator < BaseComponent
|
|
4
4
|
def initialize(**attrs) = (@attrs = attrs)
|
|
5
|
-
def view_template
|
|
5
|
+
def view_template
|
|
6
|
+
base = { class: "pk-menubar-separator" }
|
|
7
|
+
# Default only when the caller didn't supply their own — `mix` fuses.
|
|
8
|
+
base[:role] = "separator" unless attr_set?(:role)
|
|
9
|
+
div(**mix(base, @attrs))
|
|
10
|
+
end
|
|
6
11
|
end
|
|
7
12
|
end
|
|
@@ -3,7 +3,10 @@ module PhlexKit
|
|
|
3
3
|
class MenubarSubContent < BaseComponent
|
|
4
4
|
def initialize(**attrs) = (@attrs = attrs)
|
|
5
5
|
def view_template(&)
|
|
6
|
-
|
|
6
|
+
base = { class: "pk-menubar-sub-content" }
|
|
7
|
+
# Default only when the caller didn't supply their own — `mix` fuses.
|
|
8
|
+
base[:role] = "menu" unless attr_set?(:role)
|
|
9
|
+
div(**mix(base, @attrs)) do
|
|
7
10
|
div(class: "pk-menubar-sub-viewport", &)
|
|
8
11
|
end
|
|
9
12
|
end
|
|
@@ -3,17 +3,18 @@ module PhlexKit
|
|
|
3
3
|
class MenubarSubTrigger < BaseComponent
|
|
4
4
|
def initialize(**attrs) = (@attrs = attrs)
|
|
5
5
|
def view_template(&block)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
6
|
+
base = { class: "pk-menubar-item pk-menubar-sub-trigger" }
|
|
7
|
+
# Defaults only when the caller didn't supply their own — `mix` fuses.
|
|
8
|
+
base[:role] = "menuitem" unless attr_set?(:role)
|
|
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
|
+
base[:tabindex] = "-1" unless attr_set?(:tabindex)
|
|
12
|
+
base[:"aria-haspopup"] = "menu" unless aria_key_set?(:haspopup)
|
|
13
|
+
# expanded starts false; the sub is revealed by CSS (:hover /
|
|
14
|
+
# :focus-within), so MenubarSub's syncSub actions mirror that state
|
|
15
|
+
# onto this attribute.
|
|
16
|
+
base[:"aria-expanded"] = "false" unless aria_key_set?(:expanded)
|
|
17
|
+
div(**mix(base, @attrs)) do
|
|
17
18
|
block&.call
|
|
18
19
|
render Icon.new(:chevron_right, size: nil, class: "pk-menubar-sub-chevron")
|
|
19
20
|
end
|
|
@@ -3,13 +3,16 @@ module PhlexKit
|
|
|
3
3
|
class MenubarTrigger < BaseComponent
|
|
4
4
|
def initialize(**attrs) = (@attrs = attrs)
|
|
5
5
|
def view_template(&)
|
|
6
|
-
|
|
6
|
+
base = {
|
|
7
7
|
type: :button,
|
|
8
8
|
class: "pk-menubar-trigger",
|
|
9
|
-
role: "menuitem",
|
|
10
|
-
aria: { haspopup: "menu", expanded: "false" },
|
|
11
9
|
data: { action: "click->phlex-kit--menubar#toggle mouseenter->phlex-kit--menubar#switch" }
|
|
12
|
-
}
|
|
10
|
+
}
|
|
11
|
+
# Defaults only when the caller didn't supply their own — `mix` fuses.
|
|
12
|
+
base[:role] = "menuitem" unless attr_set?(:role)
|
|
13
|
+
base[:"aria-haspopup"] = "menu" unless aria_key_set?(:haspopup)
|
|
14
|
+
base[:"aria-expanded"] = "false" unless aria_key_set?(:expanded)
|
|
15
|
+
button(**mix(base, @attrs), &)
|
|
13
16
|
end
|
|
14
17
|
end
|
|
15
18
|
end
|
|
@@ -30,6 +30,11 @@ export default class extends Controller {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
connect() {
|
|
33
|
+
// connect() re-derives reflected state: a Turbo snapshot serializes a
|
|
34
|
+
// mid-flight data-autoscrolling marker; no programmatic scroll is in
|
|
35
|
+
// flight on a fresh connect.
|
|
36
|
+
this.element.removeAttribute("data-autoscrolling");
|
|
37
|
+
if (this.hasViewportTarget) this.viewportTarget.removeAttribute("data-autoscrolling");
|
|
33
38
|
// Reader is considered "following" the live edge until they move away.
|
|
34
39
|
this.following = true;
|
|
35
40
|
// True only while a programmatic scroll is in flight, so reader-intent
|
|
@@ -10,3 +10,7 @@
|
|
|
10
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; } }
|
|
13
|
+
|
|
14
|
+
/* RTL: Previous/Next chevrons follow the reading direction. */
|
|
15
|
+
[dir="rtl"] .pk-pagination-previous svg,
|
|
16
|
+
[dir="rtl"] .pk-pagination-next svg { transform: scaleX(-1); }
|