phlex_kit 0.12.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_content.rb +2 -2
- 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 +38 -9
- 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.rb +1 -1
- 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 +19 -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/codeblock/codeblock.rb +2 -2
- 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 +60 -5
- data/app/components/phlex_kit/combobox/combobox_item.rb +7 -4
- data/app/components/phlex_kit/command/command_controller.js +69 -2
- data/app/components/phlex_kit/command/command_dialog_content.rb +15 -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 +42 -1
- 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_controller.js +7 -1
- 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/data_table/data_table_selection_summary.rb +12 -3
- data/app/components/phlex_kit/date_picker/date_picker_controller.js +3 -5
- data/app/components/phlex_kit/drawer/drawer.css +4 -0
- data/app/components/phlex_kit/drawer/drawer_content.rb +2 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu.rb +3 -1
- 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 +33 -5
- 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 +6 -2
- 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/link/link.rb +10 -1
- data/app/components/phlex_kit/menubar/menubar.rb +8 -4
- 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 +36 -0
- 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/progress/progress.css +19 -0
- data/app/components/phlex_kit/progress/progress.rb +29 -13
- 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/scroll_area/scroll_area.rb +1 -1
- 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.rb +2 -2
- 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 +5 -2
- 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 +52 -2
- data/app/components/phlex_kit/toast/toast_item.rb +2 -2
- data/app/components/phlex_kit/toast/toaster_controller.js +63 -7
- 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/base_component.rb +9 -0
- data/lib/phlex_kit/version.rb +1 -1
- metadata +1 -1
|
@@ -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; }
|
|
@@ -36,11 +36,20 @@ module PhlexKit
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def view_template(&block)
|
|
39
|
-
|
|
39
|
+
base = { href: @href, class: classes }
|
|
40
|
+
# target="_blank" hands the new page a window.opener reference in older
|
|
41
|
+
# browsers (modern ones imply noopener) — default the safe rel unless
|
|
42
|
+
# the caller supplied their own (`mix` would fuse "noopener external").
|
|
43
|
+
base[:rel] = "noopener" if blank_target? && !attr_set?(:rel)
|
|
44
|
+
a(**mix(base, @attrs), &block)
|
|
40
45
|
end
|
|
41
46
|
|
|
42
47
|
private
|
|
43
48
|
|
|
49
|
+
def blank_target?
|
|
50
|
+
(@attrs[:target] || @attrs["target"]).to_s == "_blank"
|
|
51
|
+
end
|
|
52
|
+
|
|
44
53
|
def classes
|
|
45
54
|
[ "pk-button", fetch_option(VARIANTS, @variant, :variant), fetch_option(SIZES, @size, :size), ("icon" if @icon) ].compact.join(" ")
|
|
46
55
|
end
|
|
@@ -11,16 +11,20 @@ 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]
|
|
20
19
|
# panel (which no focus trap holds open).
|
|
21
|
-
|
|
20
|
+
# mousedown@window arms the modal-menu click swallow (deliberately
|
|
21
|
+
# NOT bound by navigation_menu.rb — nav menus are non-modal).
|
|
22
|
+
action: "mousedown@window->phlex-kit--menubar#onMousedownOutside click@window->phlex-kit--menubar#onClickOutside keydown->phlex-kit--menubar#onKeydown focusout->phlex-kit--menubar#onFocusout"
|
|
22
23
|
}
|
|
23
|
-
}
|
|
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), &)
|
|
24
28
|
end
|
|
25
29
|
end
|
|
26
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
|
|
@@ -13,6 +13,27 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
13
13
|
// Connects to data-controller="phlex-kit--menubar"
|
|
14
14
|
let uid = 0
|
|
15
15
|
|
|
16
|
+
// One-shot capture listener swallowing the click an outside mousedown is
|
|
17
|
+
// about to produce (see onMousedownOutside; same helper in dropdown/context
|
|
18
|
+
// menu — duplicated per controller by design). {once} self-removes; the DOM
|
|
19
|
+
// dedupes re-arming with the same fn. If the gesture's click never fires
|
|
20
|
+
// (e.g. suppressed after a selection drag) the armed swallow eats the next
|
|
21
|
+
// one — rare enough to accept.
|
|
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
|
+
}
|
|
33
|
+
function armSwallowClick() {
|
|
34
|
+
window.addEventListener("click", swallowClick, { once: true, capture: true })
|
|
35
|
+
}
|
|
36
|
+
|
|
16
37
|
export default class extends Controller {
|
|
17
38
|
static targets = ["menu"]
|
|
18
39
|
|
|
@@ -131,6 +152,21 @@ export default class extends Controller {
|
|
|
131
152
|
if (opts.refocus === true) trigger?.focus()
|
|
132
153
|
}
|
|
133
154
|
|
|
155
|
+
// Kit-wide dismiss contract (Radix parity): MENU overlays (dropdown,
|
|
156
|
+
// context, menubar, select) are modal — the dismissing outside click is
|
|
157
|
+
// swallowed rather than also acting on what was under the pointer. The
|
|
158
|
+
// decision happens at MOUSEDOWN: the same gesture's focusout closes the
|
|
159
|
+
// panel before the click event fires, so a click-time openMenu check
|
|
160
|
+
// would already be null (that race made the old swallow silently skip
|
|
161
|
+
// focusable click targets). Only menubar.rb binds this action — the
|
|
162
|
+
// navigation menu (hover-open mode) is non-modal like Radix's and its
|
|
163
|
+
// dismissing click clicks through.
|
|
164
|
+
onMousedownOutside(e) {
|
|
165
|
+
if (this.element.contains(e.target)) return
|
|
166
|
+
if (!this.openMenu) return
|
|
167
|
+
armSwallowClick()
|
|
168
|
+
}
|
|
169
|
+
|
|
134
170
|
onClickOutside(e) {
|
|
135
171
|
if (this.element.contains(e.target)) return
|
|
136
172
|
this.close()
|
|
@@ -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); }
|
|
@@ -23,3 +23,22 @@
|
|
|
23
23
|
.pk-progress:dir(rtl) .pk-progress-indicator {
|
|
24
24
|
transform: translateX(calc((100 - var(--pk-progress-value, 0)) * 1%));
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
/* Indeterminate (value: nil): a 40%-wide segment sweeps the track.
|
|
28
|
+
translateX % is relative to the segment's own width: -100% parks it just
|
|
29
|
+
off the track's start edge, +250% (track = 250% of segment) just past the
|
|
30
|
+
end. Direction-agnostic — the sweep reads fine mirrored, no RTL arm. */
|
|
31
|
+
.pk-progress.indeterminate .pk-progress-indicator {
|
|
32
|
+
width: 40%;
|
|
33
|
+
transition: none;
|
|
34
|
+
animation: pk-progress-indeterminate 1.5s ease-in-out infinite;
|
|
35
|
+
}
|
|
36
|
+
@keyframes pk-progress-indeterminate {
|
|
37
|
+
from { transform: translateX(-100%); }
|
|
38
|
+
to { transform: translateX(250%); }
|
|
39
|
+
}
|
|
40
|
+
@media (prefers-reduced-motion: reduce) {
|
|
41
|
+
.pk-progress.indeterminate .pk-progress-indicator {
|
|
42
|
+
animation-duration: 3s;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -6,29 +6,45 @@ module PhlexKit
|
|
|
6
6
|
class Progress < BaseComponent
|
|
7
7
|
# value_text: is a named kwarg (not read from **attrs) because `mix`
|
|
8
8
|
# would *fuse* a caller "aria-valuetext" with the generated default.
|
|
9
|
+
# `value: nil` renders the INDETERMINATE state (Radix parity): a sliding
|
|
10
|
+
# animation, data-state="indeterminate", and no aria-valuenow — which is
|
|
11
|
+
# exactly how ARIA marks a progressbar of unknown value.
|
|
9
12
|
def initialize(value: 0, value_text: nil, **attrs)
|
|
10
|
-
@value = value.to_f.clamp(0.0, 100.0)
|
|
13
|
+
@value = value.nil? ? nil : value.to_f.clamp(0.0, 100.0)
|
|
11
14
|
@value_text = value_text
|
|
12
15
|
@attrs = attrs
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
def view_template
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
div(**mix(root_attrs, @attrs)) do
|
|
20
|
+
div(class: "pk-progress-indicator")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def root_attrs
|
|
27
|
+
base = {
|
|
22
28
|
role: "progressbar",
|
|
23
|
-
"aria-valuenow": v,
|
|
24
29
|
"aria-valuemin": 0,
|
|
25
30
|
"aria-valuemax": 100,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
class: "pk-progress"
|
|
32
|
+
}
|
|
33
|
+
if @value.nil?
|
|
34
|
+
base[:class] = "pk-progress indeterminate"
|
|
35
|
+
base[:data] = { state: "indeterminate" }
|
|
36
|
+
base[:"aria-valuetext"] = @value_text if @value_text
|
|
37
|
+
else
|
|
38
|
+
# %g drops float artifacts ("40.0" would be announced as "forty point
|
|
39
|
+
# zero percent"). The value travels as a custom property so the fill
|
|
40
|
+
# transform lives in progress.css, where a :dir(rtl) arm can flip it —
|
|
41
|
+
# an inline translateX always filled from the physical left.
|
|
42
|
+
v = Kernel.format("%g", @value)
|
|
43
|
+
base[:"aria-valuenow"] = v
|
|
44
|
+
base[:"aria-valuetext"] = @value_text || "#{v}%"
|
|
45
|
+
base[:style] = "--pk-progress-value: #{v};"
|
|
31
46
|
end
|
|
47
|
+
base
|
|
32
48
|
end
|
|
33
49
|
end
|
|
34
50
|
end
|
|
@@ -57,6 +57,9 @@ export default class extends Controller {
|
|
|
57
57
|
try { handle.setPointerCapture(e.pointerId) } catch {}
|
|
58
58
|
|
|
59
59
|
const onMove = (ev) => {
|
|
60
|
+
// Only the initiating pointer drives the drag — a second touch point
|
|
61
|
+
// over the handle must not hijack it.
|
|
62
|
+
if (ev.pointerId !== e.pointerId) return
|
|
60
63
|
// LTR assumption: clientX grows toward the trailing panel; RTL drag
|
|
61
64
|
// inversion is a documented limitation.
|
|
62
65
|
const delta = (horizontal ? ev.clientX : ev.clientY) - drag.startPos
|
|
@@ -65,9 +68,12 @@ export default class extends Controller {
|
|
|
65
68
|
const prevGrow = this.clampPrevGrow(prev, next, (prevSize / total) * drag.pairGrow, drag.pairGrow, drag.groupGrow)
|
|
66
69
|
prev.style.flexGrow = prevGrow
|
|
67
70
|
next.style.flexGrow = drag.pairGrow - prevGrow
|
|
68
|
-
this
|
|
71
|
+
// Resizing this pair moves every share: 3+ panel groups have sibling
|
|
72
|
+
// handles whose aria-valuenow would otherwise go stale.
|
|
73
|
+
this.handleTargets.forEach((h) => this.syncValuenow(h))
|
|
69
74
|
}
|
|
70
|
-
const onUp = () => {
|
|
75
|
+
const onUp = (ev) => {
|
|
76
|
+
if (ev.pointerId !== e.pointerId) return
|
|
71
77
|
handle.removeEventListener("pointermove", onMove)
|
|
72
78
|
handle.removeEventListener("pointerup", onUp)
|
|
73
79
|
handle.removeEventListener("pointercancel", onUp)
|
|
@@ -89,8 +95,14 @@ export default class extends Controller {
|
|
|
89
95
|
if (!prev || !next) return
|
|
90
96
|
|
|
91
97
|
const horizontal = this.directionValue === "horizontal"
|
|
98
|
+
// Horizontal arrows follow visual direction (round-7 kit convention, same
|
|
99
|
+
// as tabs/toggle_group): in RTL the DOM-first panel is on the RIGHT, so
|
|
100
|
+
// ArrowLeft must GROW it (move the divider visually left). Only the
|
|
101
|
+
// pointer-drag clientX math keeps its documented LTR exemption; Up/Down
|
|
102
|
+
// never flip. Runtime dir check — reliable after a dynamic flip.
|
|
103
|
+
const rtl = horizontal && getComputedStyle(this.element).direction === "rtl"
|
|
92
104
|
const steps = horizontal
|
|
93
|
-
? { ArrowLeft: -0.05, ArrowRight: 0.05 }
|
|
105
|
+
? (rtl ? { ArrowLeft: 0.05, ArrowRight: -0.05 } : { ArrowLeft: -0.05, ArrowRight: 0.05 })
|
|
94
106
|
: { ArrowUp: -0.05, ArrowDown: 0.05 }
|
|
95
107
|
|
|
96
108
|
const pairGrow = this.growOf(prev) + this.growOf(next) || 2
|
|
@@ -109,7 +121,8 @@ export default class extends Controller {
|
|
|
109
121
|
const prevGrow = this.clampPrevGrow(prev, next, share * pairGrow, pairGrow, this.groupGrow())
|
|
110
122
|
prev.style.flexGrow = prevGrow
|
|
111
123
|
next.style.flexGrow = pairGrow - prevGrow
|
|
112
|
-
|
|
124
|
+
// All handles: sibling dividers share panels in 3+ panel groups.
|
|
125
|
+
this.handleTargets.forEach((h) => this.syncValuenow(h))
|
|
113
126
|
}
|
|
114
127
|
|
|
115
128
|
growOf(el) {
|
|
@@ -8,18 +8,20 @@ module PhlexKit
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def view_template
|
|
11
|
-
|
|
11
|
+
base = {
|
|
12
12
|
class: "pk-resizable-handle",
|
|
13
|
-
role: "separator",
|
|
14
|
-
tabindex: "0",
|
|
15
|
-
# Correct for the default horizontal group; the controller re-stamps
|
|
16
|
-
# orientation (and aria-valuenow/min/max) per group direction on connect.
|
|
17
|
-
aria: { orientation: "vertical" },
|
|
18
13
|
data: {
|
|
19
14
|
phlex_kit__resizable_target: "handle",
|
|
20
15
|
action: "pointerdown->phlex-kit--resizable#start keydown->phlex-kit--resizable#keydown"
|
|
21
16
|
}
|
|
22
|
-
}
|
|
17
|
+
}
|
|
18
|
+
# Defaults only when the caller didn't supply their own — `mix` fuses.
|
|
19
|
+
base[:role] = "separator" unless attr_set?(:role)
|
|
20
|
+
base[:tabindex] = "0" unless attr_set?(:tabindex)
|
|
21
|
+
# Correct for the default horizontal group; the controller re-stamps
|
|
22
|
+
# orientation (and aria-valuenow/min/max) per group direction on connect.
|
|
23
|
+
base[:aria] = { orientation: "vertical" } unless aria_key_set?(:orientation)
|
|
24
|
+
div(**mix(base, @attrs)) do
|
|
23
25
|
span(class: "pk-resizable-handle-grip", aria: { hidden: "true" }) if @with_handle
|
|
24
26
|
end
|
|
25
27
|
end
|
|
@@ -15,7 +15,7 @@ module PhlexKit
|
|
|
15
15
|
# Defaults only when the caller didn't supply their own — `mix` would
|
|
16
16
|
# fuse role="region log" / tabindex="0 -1" instead of overriding.
|
|
17
17
|
base = { class: "pk-scroll-area" }
|
|
18
|
-
base[:tabindex] = "0" unless
|
|
18
|
+
base[:tabindex] = "0" unless attr_set?(:tabindex)
|
|
19
19
|
base[:role] = "region" if aria_labelled? && !@attrs.key?(:role) && !@attrs.key?("role")
|
|
20
20
|
div(**mix(base, @attrs), &)
|
|
21
21
|
end
|
|
@@ -39,7 +39,7 @@ module PhlexKit
|
|
|
39
39
|
# keydown.esc rides on the root (not only the items) so Escape closes
|
|
40
40
|
# the [popover=manual] panel with focus on the trigger too; handleEsc
|
|
41
41
|
# no-ops while closed.
|
|
42
|
-
action: "click@window->phlex-kit--select#clickOutside keydown.esc->phlex-kit--select#handleEsc"
|
|
42
|
+
action: "click@window->phlex-kit--select#clickOutside keydown.esc->phlex-kit--select#handleEsc focusout->phlex-kit--select#onFocusout"
|
|
43
43
|
}
|
|
44
44
|
}, @attrs), &block)
|
|
45
45
|
end
|