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
|
@@ -3,7 +3,9 @@ module PhlexKit
|
|
|
3
3
|
def initialize(**attrs) = (@attrs = attrs)
|
|
4
4
|
def view_template
|
|
5
5
|
li do
|
|
6
|
-
|
|
6
|
+
# No aria-hidden on the wrapper — it would swallow the sr-only text.
|
|
7
|
+
# The Icon renders its own aria-hidden="true"; only the glyph is hidden.
|
|
8
|
+
span(**mix({ class: "pk-pagination-ellipsis" }, @attrs)) do
|
|
7
9
|
render Icon.new(:ellipsis, size: nil)
|
|
8
10
|
span(class: "pk-sr-only") { "More pages" }
|
|
9
11
|
end
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
border-radius: var(--pk-radius);
|
|
22
22
|
background: var(--pk-surface); color: var(--pk-text); padding: .625rem;
|
|
23
23
|
font-size: .875rem;
|
|
24
|
-
box-shadow: 0 8px 24px
|
|
24
|
+
box-shadow: 0 8px 24px color-mix(in srgb, var(--pk-shadow-color) 30%, transparent); outline: none;
|
|
25
25
|
}
|
|
26
26
|
/* display must be gated on :popover-open — an unconditional author
|
|
27
27
|
`display: flex` would beat the UA's [popover] { display: none }. */
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
position: relative;
|
|
8
8
|
flex: none;
|
|
9
9
|
background: var(--pk-border);
|
|
10
|
+
/* without this, touch drags scroll the page instead of resizing */
|
|
11
|
+
touch-action: none;
|
|
10
12
|
}
|
|
11
13
|
.pk-resizable-group.horizontal > .pk-resizable-handle { width: 1px; cursor: col-resize; }
|
|
12
14
|
.pk-resizable-group.vertical > .pk-resizable-handle { height: 1px; cursor: row-resize; }
|
|
@@ -19,7 +21,7 @@
|
|
|
19
21
|
}
|
|
20
22
|
.pk-resizable-handle:focus-visible {
|
|
21
23
|
outline: none;
|
|
22
|
-
box-shadow: 0 0 0
|
|
24
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/* Visible grip pill (their withHandle / .cn-resizable-handle-icon). */
|
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
|
|
3
3
|
// PhlexKit's stand-in for react-resizable-panels behind shadcn's Resizable:
|
|
4
|
-
// dragging a handle rebalances the flex-grow of the two
|
|
4
|
+
// dragging (or arrow-keying) a handle rebalances the flex-grow of the two
|
|
5
|
+
// panels around it. The handle is a focusable role="separator", so it also
|
|
6
|
+
// carries the ARIA value contract: aria-valuenow tracks the leading panel's
|
|
7
|
+
// share of the pair, and aria-orientation follows the group direction (a
|
|
8
|
+
// handle in a horizontal group is a vertical separator).
|
|
5
9
|
// Connects to data-controller="phlex-kit--resizable"
|
|
6
10
|
export default class extends Controller {
|
|
7
11
|
static targets = ["panel", "handle"]
|
|
8
12
|
static values = { direction: { type: String, default: "horizontal" } }
|
|
9
13
|
|
|
14
|
+
disconnect() {
|
|
15
|
+
// a disconnect mid-drag must drop the handle's move/up listeners
|
|
16
|
+
if (this._activeDrag) {
|
|
17
|
+
const { handle, onMove, onUp } = this._activeDrag
|
|
18
|
+
handle.removeEventListener("pointermove", onMove)
|
|
19
|
+
handle.removeEventListener("pointerup", onUp)
|
|
20
|
+
handle.removeEventListener("pointercancel", onUp)
|
|
21
|
+
this._activeDrag = null
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
handleTargetConnected(handle) {
|
|
26
|
+
handle.setAttribute("aria-orientation", this.directionValue === "horizontal" ? "vertical" : "horizontal")
|
|
27
|
+
handle.setAttribute("aria-valuemin", "0")
|
|
28
|
+
handle.setAttribute("aria-valuemax", "100")
|
|
29
|
+
this.syncValuenow(handle)
|
|
30
|
+
}
|
|
31
|
+
|
|
10
32
|
start(e) {
|
|
33
|
+
if (e.button !== 0 || !e.isPrimary) return
|
|
11
34
|
const handle = e.currentTarget
|
|
12
35
|
const prev = handle.previousElementSibling
|
|
13
36
|
const next = handle.nextElementSibling
|
|
@@ -20,6 +43,10 @@ export default class extends Controller {
|
|
|
20
43
|
startPos: horizontal ? e.clientX : e.clientY,
|
|
21
44
|
prevSize: sizeOf(prev),
|
|
22
45
|
nextSize: sizeOf(next),
|
|
46
|
+
// The drag redistributes the PAIR's combined flex-grow, not a fixed
|
|
47
|
+
// constant: normalizing to 2 rescaled the pair against untouched
|
|
48
|
+
// siblings, so in a 25/25/50 group the 50 panel ballooned on first drag.
|
|
49
|
+
pairGrow: this.growOf(prev) + this.growOf(next) || 2,
|
|
23
50
|
}
|
|
24
51
|
try { handle.setPointerCapture(e.pointerId) } catch {}
|
|
25
52
|
|
|
@@ -27,16 +54,64 @@ export default class extends Controller {
|
|
|
27
54
|
const delta = (horizontal ? ev.clientX : ev.clientY) - drag.startPos
|
|
28
55
|
const total = drag.prevSize + drag.nextSize
|
|
29
56
|
const prevSize = Math.min(Math.max(drag.prevSize + delta, 0), total)
|
|
30
|
-
prev.style.flexGrow = (prevSize / total) *
|
|
31
|
-
next.style.flexGrow = ((total - prevSize) / total) *
|
|
57
|
+
prev.style.flexGrow = (prevSize / total) * drag.pairGrow
|
|
58
|
+
next.style.flexGrow = ((total - prevSize) / total) * drag.pairGrow
|
|
59
|
+
this.syncValuenow(handle)
|
|
32
60
|
}
|
|
33
61
|
const onUp = () => {
|
|
34
62
|
handle.removeEventListener("pointermove", onMove)
|
|
35
63
|
handle.removeEventListener("pointerup", onUp)
|
|
36
64
|
handle.removeEventListener("pointercancel", onUp)
|
|
65
|
+
this._activeDrag = null
|
|
37
66
|
}
|
|
38
67
|
handle.addEventListener("pointermove", onMove)
|
|
39
68
|
handle.addEventListener("pointerup", onUp)
|
|
40
69
|
handle.addEventListener("pointercancel", onUp)
|
|
70
|
+
this._activeDrag = { handle, onMove, onUp } // for disconnect() cleanup
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Arrow keys resize by 5% of the pair per press (matching the drag axis);
|
|
74
|
+
// Home/End collapse the leading panel to its min/max.
|
|
75
|
+
keydown(e) {
|
|
76
|
+
const handle = e.currentTarget
|
|
77
|
+
const prev = handle.previousElementSibling
|
|
78
|
+
const next = handle.nextElementSibling
|
|
79
|
+
if (!prev || !next) return
|
|
80
|
+
|
|
81
|
+
const horizontal = this.directionValue === "horizontal"
|
|
82
|
+
const steps = horizontal
|
|
83
|
+
? { ArrowLeft: -0.05, ArrowRight: 0.05 }
|
|
84
|
+
: { ArrowUp: -0.05, ArrowDown: 0.05 }
|
|
85
|
+
|
|
86
|
+
const pairGrow = this.growOf(prev) + this.growOf(next) || 2
|
|
87
|
+
let share = this.growOf(prev) / (pairGrow || 1)
|
|
88
|
+
if (e.key in steps) {
|
|
89
|
+
share = Math.min(1, Math.max(0, share + steps[e.key]))
|
|
90
|
+
} else if (e.key === "Home") {
|
|
91
|
+
share = 0
|
|
92
|
+
} else if (e.key === "End") {
|
|
93
|
+
share = 1
|
|
94
|
+
} else {
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
e.preventDefault()
|
|
98
|
+
|
|
99
|
+
prev.style.flexGrow = share * pairGrow
|
|
100
|
+
next.style.flexGrow = (1 - share) * pairGrow
|
|
101
|
+
this.syncValuenow(handle)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
growOf(el) {
|
|
105
|
+
const g = parseFloat(getComputedStyle(el).flexGrow)
|
|
106
|
+
return Number.isFinite(g) ? g : 1
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
syncValuenow(handle) {
|
|
110
|
+
const prev = handle.previousElementSibling
|
|
111
|
+
const next = handle.nextElementSibling
|
|
112
|
+
if (!prev || !next) return
|
|
113
|
+
const pair = this.growOf(prev) + this.growOf(next)
|
|
114
|
+
const share = pair > 0 ? this.growOf(prev) / pair : 0.5
|
|
115
|
+
handle.setAttribute("aria-valuenow", String(Math.round(share * 100)))
|
|
41
116
|
}
|
|
42
117
|
}
|
|
@@ -12,10 +12,12 @@ module PhlexKit
|
|
|
12
12
|
class: "pk-resizable-handle",
|
|
13
13
|
role: "separator",
|
|
14
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.
|
|
15
17
|
aria: { orientation: "vertical" },
|
|
16
18
|
data: {
|
|
17
19
|
phlex_kit__resizable_target: "handle",
|
|
18
|
-
action: "pointerdown->phlex-kit--resizable#start"
|
|
20
|
+
action: "pointerdown->phlex-kit--resizable#start keydown->phlex-kit--resizable#keydown"
|
|
19
21
|
}
|
|
20
22
|
}, @attrs)) do
|
|
21
23
|
span(class: "pk-resizable-handle-grip", aria: { hidden: "true" }) if @with_handle
|
|
@@ -10,7 +10,9 @@ module PhlexKit
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def view_template(&)
|
|
13
|
-
|
|
13
|
+
# Trailing ";" matters: Phlex's mix joins string attrs with a space, so
|
|
14
|
+
# a caller style: would otherwise fuse into one invalid declaration.
|
|
15
|
+
style = @default_size ? "flex-grow: #{Kernel.format("%g", @default_size)};" : nil
|
|
14
16
|
div(**mix({
|
|
15
17
|
class: "pk-resizable-panel",
|
|
16
18
|
style: style,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
# Styled scroll container, ported from shadcn/ui's ScrollArea. Radix's custom
|
|
3
3
|
# scrollbars are replaced with native thin scrollbars themed via CSS — no JS.
|
|
4
|
-
# Constrain it with a height/width from the caller.
|
|
4
|
+
# Constrain it with a height/width from the caller. tabindex=0 makes the
|
|
5
|
+
# scrollable region keyboard-focusable (WCAG); role=region gives that focus
|
|
6
|
+
# stop a role — pass `aria: { label: ... }` to name it. `.pk-scroll-area`
|
|
5
7
|
# (scroll_area.css).
|
|
6
8
|
class ScrollArea < BaseComponent
|
|
7
9
|
def initialize(**attrs)
|
|
@@ -9,7 +11,7 @@ module PhlexKit
|
|
|
9
11
|
end
|
|
10
12
|
|
|
11
13
|
def view_template(&)
|
|
12
|
-
div(**mix({ class: "pk-scroll-area", tabindex: "0" }, @attrs), &)
|
|
14
|
+
div(**mix({ class: "pk-scroll-area", tabindex: "0", role: "region" }, @attrs), &)
|
|
13
15
|
end
|
|
14
16
|
end
|
|
15
17
|
end
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
45
45
|
}
|
|
46
46
|
.pk-select-trigger:disabled { cursor: not-allowed; opacity: .5; }
|
|
47
|
-
.pk-select-trigger-icon { flex: none; width: 1rem; height: 1rem; margin-
|
|
47
|
+
.pk-select-trigger-icon { flex: none; width: 1rem; height: 1rem; margin-inline-start: .5rem; opacity: .5; }
|
|
48
48
|
.pk-select-value { overflow: hidden; text-overflow: ellipsis; pointer-events: none; }
|
|
49
49
|
|
|
50
50
|
/* Dropdown panel — anchored directly below the trigger, flips above at the
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
background: var(--pk-surface);
|
|
76
76
|
color: var(--pk-text);
|
|
77
77
|
padding: .25rem;
|
|
78
|
-
box-shadow: 0 4px 16px
|
|
78
|
+
box-shadow: 0 4px 16px color-mix(in srgb, var(--pk-shadow-color) 40%, transparent);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
.pk-select-label { margin: 0; padding: .25rem .375rem; font-size: .75rem; font-weight: 400; color: var(--pk-muted); }
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
.pk-select-item-check {
|
|
103
103
|
visibility: hidden;
|
|
104
104
|
position: absolute;
|
|
105
|
-
|
|
105
|
+
inset-inline-end: .5rem;
|
|
106
106
|
display: flex;
|
|
107
107
|
align-items: center;
|
|
108
108
|
justify-content: center;
|
|
@@ -2,11 +2,13 @@ module PhlexKit
|
|
|
2
2
|
# Custom dropdown select, ported from ruby_ui's RubyUI::Select — the styled
|
|
3
3
|
# popover (Image #2), NOT the native <select> (that's PhlexKit::NativeSelect). Unlike
|
|
4
4
|
# the rest of the kit this component IS JS-driven: it keeps ruby_ui's Stimulus
|
|
5
|
-
# wiring (`phlex-kit--select` /
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# the
|
|
9
|
-
#
|
|
5
|
+
# wiring (`phlex-kit--select`), since the open/close, selection, and keyboard
|
|
6
|
+
# nav are the point. Two changes from upstream: the controller drops the
|
|
7
|
+
# `@floating-ui/dom` dependency and positions the panel with plain CSS (opens
|
|
8
|
+
# directly below the trigger), and selection flips aria-selected via the
|
|
9
|
+
# instance-scoped itemTargets rather than upstream's document-scoped
|
|
10
|
+
# `.pk-select-item` outlet (which clobbered every other Select on the page).
|
|
11
|
+
# Tailwind → vanilla `.pk-select-*` (select.css).
|
|
10
12
|
#
|
|
11
13
|
# Multi-part. The hidden SelectInput carries the form value/name; SelectTrigger +
|
|
12
14
|
# SelectValue are the closed-state button; SelectContent > SelectGroup >
|
|
@@ -34,8 +36,10 @@ module PhlexKit
|
|
|
34
36
|
data: {
|
|
35
37
|
controller: "phlex-kit--select",
|
|
36
38
|
phlex_kit__select_open_value: "false",
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
# keydown.esc rides on the root (not only the items) so Escape closes
|
|
40
|
+
# the [popover=manual] panel with focus on the trigger too; handleEsc
|
|
41
|
+
# no-ops while closed.
|
|
42
|
+
action: "click@window->phlex-kit--select#clickOutside keydown.esc->phlex-kit--select#handleEsc"
|
|
39
43
|
}
|
|
40
44
|
}, @attrs), &block)
|
|
41
45
|
end
|
|
@@ -8,7 +8,6 @@ import { Controller } from "@hotwired/stimulus";
|
|
|
8
8
|
export default class extends Controller {
|
|
9
9
|
static targets = ["trigger", "content", "input", "value", "item"];
|
|
10
10
|
static values = { open: Boolean };
|
|
11
|
-
static outlets = ["phlex-kit--select-item"];
|
|
12
11
|
|
|
13
12
|
connect() {
|
|
14
13
|
this.generateItemsIds();
|
|
@@ -17,13 +16,20 @@ export default class extends Controller {
|
|
|
17
16
|
selectItem(event) {
|
|
18
17
|
event.preventDefault();
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
// currentTarget, not target: the click may land on a child of the item
|
|
20
|
+
// (target.dataset.value would be undefined). itemTargets, not a
|
|
21
|
+
// document-scoped outlet: outlets match `.pk-select-item` page-wide and
|
|
22
|
+
// clobbered every other Select's aria-selected.
|
|
23
|
+
const item = event.currentTarget;
|
|
24
|
+
this.itemTargets.forEach((el) => {
|
|
25
|
+
el.setAttribute("aria-selected", el === item ? "true" : "false");
|
|
26
|
+
});
|
|
21
27
|
|
|
22
28
|
const oldValue = this.inputTarget.value;
|
|
23
|
-
const newValue =
|
|
29
|
+
const newValue = item.dataset.value;
|
|
24
30
|
|
|
25
31
|
this.inputTarget.value = newValue;
|
|
26
|
-
this.valueTarget.innerText =
|
|
32
|
+
this.valueTarget.innerText = item.innerText;
|
|
27
33
|
|
|
28
34
|
this.dispatchOnChange(oldValue, newValue);
|
|
29
35
|
this.closeContent();
|
|
@@ -66,7 +72,25 @@ export default class extends Controller {
|
|
|
66
72
|
}
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
handleHome(event) {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
if (this.itemTargets.length === 0) return;
|
|
78
|
+
this.resetCurrent();
|
|
79
|
+
this.setAriaCurrentAndActiveDescendant(0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
handleEnd(event) {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
if (this.itemTargets.length === 0) return;
|
|
85
|
+
this.resetCurrent();
|
|
86
|
+
this.setAriaCurrentAndActiveDescendant(this.itemTargets.length - 1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Bound on the root as well as the items, so Escape also closes with focus
|
|
90
|
+
// on the trigger; no-op while closed (a closed-state Escape must not steal
|
|
91
|
+
// focus back to the trigger).
|
|
69
92
|
handleEsc(event) {
|
|
93
|
+
if (!this.contentTarget.matches(":popover-open")) return;
|
|
70
94
|
event.preventDefault();
|
|
71
95
|
this.closeContent();
|
|
72
96
|
}
|
|
@@ -79,7 +103,11 @@ export default class extends Controller {
|
|
|
79
103
|
const focusItem = selectedItem || (this.hasItemTarget ? this.itemTarget : null);
|
|
80
104
|
if (!focusItem) return; // empty select — nothing to focus
|
|
81
105
|
|
|
106
|
+
// preventScroll stops the page jumping when the top-layer panel opens;
|
|
107
|
+
// scrollIntoView(nearest) then scrolls only the panel's own viewport so
|
|
108
|
+
// the focused option is actually visible (max-height panel).
|
|
82
109
|
focusItem.focus({ preventScroll: true });
|
|
110
|
+
focusItem.scrollIntoView({ block: "nearest" });
|
|
83
111
|
focusItem.setAttribute("aria-current", "true");
|
|
84
112
|
this.triggerTarget.setAttribute("aria-activedescendant", focusItem.getAttribute("id"));
|
|
85
113
|
}
|
|
@@ -127,6 +155,9 @@ export default class extends Controller {
|
|
|
127
155
|
setAriaCurrentAndActiveDescendant(currentIndex) {
|
|
128
156
|
const currentItem = this.itemTargets[currentIndex];
|
|
129
157
|
currentItem.focus({ preventScroll: true });
|
|
158
|
+
// Keep the highlighted option visible inside the scrollable viewport —
|
|
159
|
+
// preventScroll (page-jump guard) otherwise lets it scroll out of view.
|
|
160
|
+
currentItem.scrollIntoView({ block: "nearest" });
|
|
130
161
|
currentItem.setAttribute("aria-current", "true");
|
|
131
162
|
this.triggerTarget.setAttribute("aria-activedescendant", currentItem.getAttribute("id"));
|
|
132
163
|
}
|
|
@@ -16,14 +16,17 @@ module PhlexKit
|
|
|
16
16
|
def view_template(&block)
|
|
17
17
|
div(**mix({
|
|
18
18
|
role: "option",
|
|
19
|
-
|
|
19
|
+
# Roving listbox pattern: options are focusable programmatically only
|
|
20
|
+
# (focus() works on tabindex=-1), so Tab exits the widget instead of
|
|
21
|
+
# walking every option.
|
|
22
|
+
tabindex: "-1",
|
|
20
23
|
class: "pk-select-item",
|
|
21
24
|
"aria-selected": (@selected ? "true" : "false"),
|
|
22
25
|
data: {
|
|
23
26
|
value: @value,
|
|
24
|
-
controller: "phlex-kit--select-item",
|
|
25
27
|
action: "click->phlex-kit--select#selectItem keydown.enter->phlex-kit--select#selectItem " \
|
|
26
28
|
"keydown.down->phlex-kit--select#handleKeyDown keydown.up->phlex-kit--select#handleKeyUp " \
|
|
29
|
+
"keydown.home->phlex-kit--select#handleHome keydown.end->phlex-kit--select#handleEnd " \
|
|
27
30
|
"keydown.esc->phlex-kit--select#handleEsc",
|
|
28
31
|
phlex_kit__select_target: "item"
|
|
29
32
|
}
|
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
75%-wide capped at 24rem. Overlay at the kit-unified black/10 + blur-xs
|
|
5
5
|
(see dialog.css). Theme tokens from the global stylesheet. */
|
|
6
6
|
.pk-sheet-trigger { display: inline-block; }
|
|
7
|
-
.pk-sheet-backdrop { position: fixed; inset: 0; z-index: 50; background:
|
|
7
|
+
.pk-sheet-backdrop { position: fixed; inset: 0; z-index: 50; background: color-mix(in srgb, var(--pk-overlay) 10%, transparent); backdrop-filter: blur(4px); }
|
|
8
8
|
@supports not (backdrop-filter: blur(1px)) {
|
|
9
|
-
.pk-sheet-backdrop { background:
|
|
9
|
+
.pk-sheet-backdrop { background: color-mix(in srgb, var(--pk-overlay) 50%, transparent); }
|
|
10
10
|
}
|
|
11
11
|
.pk-sheet-content {
|
|
12
12
|
position: fixed; z-index: 50; display: flex; flex-direction: column; gap: 1rem;
|
|
13
13
|
background: var(--pk-surface); color: var(--pk-text); padding: 0;
|
|
14
14
|
font-size: .875rem;
|
|
15
|
-
box-shadow: 0 10px 30px
|
|
15
|
+
box-shadow: 0 10px 30px color-mix(in srgb, var(--pk-shadow-color) 35%, transparent); overflow: auto;
|
|
16
16
|
}
|
|
17
17
|
.pk-sheet-content.right { inset-block: 0; right: 0; height: 100%; width: 75%; max-width: 24rem; border-left: 1px solid var(--pk-border); }
|
|
18
18
|
.pk-sheet-content.left { inset-block: 0; left: 0; height: 100%; width: 75%; max-width: 24rem; border-right: 1px solid var(--pk-border); }
|
|
@@ -2,12 +2,14 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
2
2
|
|
|
3
3
|
// Connects to data-controller="phlex-kit--sheet-content" (on the cloned node —
|
|
4
4
|
// drawers ride the same machinery). Owns the modal contract for the open panel:
|
|
5
|
-
// scroll lock, initial focus, Tab focus trap, Escape-to-close,
|
|
6
|
-
// the opener, and aria-labelledby wiring to the
|
|
7
|
-
// and disconnect exactly bracket the
|
|
8
|
-
// (close button, backdrop, Escape,
|
|
5
|
+
// scroll lock, background inert, initial focus, Tab focus trap, Escape-to-close,
|
|
6
|
+
// focus restore to the opener, and aria-labelledby/-describedby wiring to the
|
|
7
|
+
// Sheet/Drawer Title + Description. connect and disconnect exactly bracket the
|
|
8
|
+
// clone's lifetime, so removal by any path (close button, backdrop, Escape,
|
|
9
|
+
// turbo:before-cache — the phlex-kit--sheet source controller removes the clone
|
|
10
|
+
// there) restores state.
|
|
9
11
|
const FOCUSABLE =
|
|
10
|
-
'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
12
|
+
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
11
13
|
|
|
12
14
|
export default class extends Controller {
|
|
13
15
|
static targets = ["panel"]
|
|
@@ -16,12 +18,15 @@ export default class extends Controller {
|
|
|
16
18
|
this.opener = document.activeElement
|
|
17
19
|
this.previousOverflow = document.body.style.overflow
|
|
18
20
|
document.body.style.overflow = "hidden"
|
|
19
|
-
this.#
|
|
21
|
+
this.#inertOthers()
|
|
22
|
+
this.#wireAria("aria-labelledby", ".pk-sheet-title, .pk-drawer-title")
|
|
23
|
+
this.#wireAria("aria-describedby", ".pk-sheet-description, .pk-drawer-description")
|
|
20
24
|
const focusables = this.#focusables()
|
|
21
25
|
;(focusables[0] || this.panel).focus()
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
disconnect() {
|
|
29
|
+
this.#restoreInert()
|
|
25
30
|
document.body.style.overflow = this.previousOverflow
|
|
26
31
|
if (this.opener?.isConnected) this.opener.focus()
|
|
27
32
|
}
|
|
@@ -62,11 +67,30 @@ export default class extends Controller {
|
|
|
62
67
|
return [...this.panel.querySelectorAll(FOCUSABLE)].filter((el) => el.getClientRects().length > 0)
|
|
63
68
|
}
|
|
64
69
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this.
|
|
70
|
+
// Make everything behind the panel inert (the clone is <body>'s last child,
|
|
71
|
+
// so its siblings are the whole page). Prior inert state is saved per
|
|
72
|
+
// element and restored on disconnect — which fires on every removal path,
|
|
73
|
+
// including the source controller's turbo:before-cache cleanup.
|
|
74
|
+
#inertOthers() {
|
|
75
|
+
this.inerted = []
|
|
76
|
+
for (const el of document.body.children) {
|
|
77
|
+
if (el === this.element) continue
|
|
78
|
+
if (["SCRIPT", "STYLE", "LINK", "TEMPLATE"].includes(el.tagName)) continue
|
|
79
|
+
this.inerted.push([el, el.inert])
|
|
80
|
+
el.inert = true
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#restoreInert() {
|
|
85
|
+
for (const [el, wasInert] of this.inerted || []) el.inert = wasInert
|
|
86
|
+
this.inerted = null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#wireAria(attr, selector) {
|
|
90
|
+
if (this.panel.getAttribute(attr)) return
|
|
91
|
+
const el = this.element.querySelector(selector)
|
|
92
|
+
if (!el) return
|
|
93
|
+
if (!el.id) el.id = `pk-modal-${attr.slice(5)}-${Math.random().toString(36).slice(2, 10)}`
|
|
94
|
+
this.panel.setAttribute(attr, el.id)
|
|
71
95
|
}
|
|
72
96
|
}
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
.pk-sidebar-group-action {
|
|
43
43
|
position: absolute;
|
|
44
44
|
top: .375rem;
|
|
45
|
-
|
|
45
|
+
inset-inline-end: .25rem;
|
|
46
46
|
display: flex;
|
|
47
47
|
align-items: center;
|
|
48
48
|
justify-content: center;
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
color: var(--pk-sidebar-text, var(--pk-text));
|
|
72
72
|
font: inherit;
|
|
73
73
|
font-size: .9rem;
|
|
74
|
-
text-align:
|
|
74
|
+
text-align: start;
|
|
75
75
|
text-decoration: none;
|
|
76
76
|
cursor: pointer;
|
|
77
77
|
}
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
.pk-sidebar-menu-action {
|
|
87
87
|
position: absolute;
|
|
88
88
|
top: .5rem;
|
|
89
|
-
|
|
89
|
+
inset-inline-end: .25rem;
|
|
90
90
|
display: flex;
|
|
91
91
|
align-items: center;
|
|
92
92
|
justify-content: center;
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
user-select: none;
|
|
106
106
|
position: absolute;
|
|
107
107
|
top: .5rem;
|
|
108
|
-
|
|
108
|
+
inset-inline-end: .25rem;
|
|
109
109
|
display: flex;
|
|
110
110
|
align-items: center;
|
|
111
111
|
justify-content: center;
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
min-width: 0;
|
|
131
131
|
margin: 0 .875rem;
|
|
132
132
|
padding: .125rem .625rem;
|
|
133
|
-
border-
|
|
133
|
+
border-inline-start: 1px solid var(--pk-sidebar-border, var(--pk-border));
|
|
134
134
|
}
|
|
135
135
|
.pk-sidebar-menu-sub-item { position: relative; }
|
|
136
136
|
.pk-sidebar-menu-sub-button {
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
color: var(--pk-sidebar-text, var(--pk-text));
|
|
149
149
|
font: inherit;
|
|
150
150
|
font-size: .875rem;
|
|
151
|
-
text-align:
|
|
151
|
+
text-align: start;
|
|
152
152
|
text-decoration: none;
|
|
153
153
|
cursor: pointer;
|
|
154
154
|
}
|
|
@@ -169,14 +169,23 @@
|
|
|
169
169
|
.pk-sidebar-wrapper.collapsible-offcanvas .pk-sidebar,
|
|
170
170
|
.pk-sidebar-wrapper.collapsible-icon .pk-sidebar {
|
|
171
171
|
position: relative;
|
|
172
|
-
transition:
|
|
172
|
+
/* visibility rides the transition: it stays visible while sliding out and
|
|
173
|
+
flips hidden at the end — hidden also drops the off-screen links from
|
|
174
|
+
the tab order (they stayed focusable while invisible before). */
|
|
175
|
+
transition: margin-left .2s ease, transform .2s ease, width .2s ease, visibility .2s;
|
|
173
176
|
}
|
|
174
|
-
.pk-sidebar-wrapper.collapsible-offcanvas[data-collapsed] .pk-sidebar { margin-left: -16rem; }
|
|
177
|
+
.pk-sidebar-wrapper.collapsible-offcanvas[data-collapsed] .pk-sidebar { margin-left: -16rem; visibility: hidden; }
|
|
175
178
|
|
|
176
179
|
/* Icon mode: the rail shrinks to a 3rem strip — labels and text hide, menu
|
|
177
180
|
buttons become centered icon squares, hover shows a data-tooltip bubble. */
|
|
178
181
|
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar { width: 3rem; }
|
|
179
|
-
|
|
182
|
+
/* Desktop-only: at mobile widths icon mode becomes the full drawer, and the
|
|
183
|
+
labels/subs must keep their AUTHOR display values (flex). The old approach
|
|
184
|
+
un-hid them with `display: revert`, but revert rolls back to the USER-AGENT
|
|
185
|
+
origin — group labels lost their flex centering, sub lists their gap. */
|
|
186
|
+
@media (min-width: 768px) {
|
|
187
|
+
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] :is(.pk-sidebar-group-label, .pk-sidebar-group-action, .pk-sidebar-menu-badge, .pk-sidebar-menu-action, .pk-sidebar-menu-sub, .pk-sidebar-input, .pk-sidebar-separator) { display: none; }
|
|
188
|
+
}
|
|
180
189
|
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] :is(.pk-sidebar-header, .pk-sidebar-content, .pk-sidebar-footer) {
|
|
181
190
|
padding-inline: .5rem;
|
|
182
191
|
align-items: center;
|
|
@@ -187,7 +196,9 @@
|
|
|
187
196
|
padding: .5rem;
|
|
188
197
|
justify-content: center;
|
|
189
198
|
}
|
|
190
|
-
|
|
199
|
+
@media (min-width: 768px) {
|
|
200
|
+
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button > :not(svg) { display: none; }
|
|
201
|
+
}
|
|
191
202
|
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button[data-tooltip] { position: relative; }
|
|
192
203
|
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button[data-tooltip]:hover::after {
|
|
193
204
|
content: attr(data-tooltip);
|
|
@@ -252,9 +263,8 @@
|
|
|
252
263
|
@media (max-width: 767px) {
|
|
253
264
|
/* Below the cutoff both collapsible modes become the overlay drawer. */
|
|
254
265
|
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar { width: 16rem; }
|
|
255
|
-
|
|
266
|
+
/* No un-hiding needed: the icon-mode hide rules are scoped to >=768px. */
|
|
256
267
|
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button { width: 100%; height: auto; padding: .5rem; justify-content: flex-start; }
|
|
257
|
-
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button > :not(svg) { display: revert; }
|
|
258
268
|
.pk-sidebar-wrapper.collapsible-icon .pk-sidebar-rail { display: none; }
|
|
259
269
|
.pk-sidebar-wrapper:is(.collapsible-offcanvas, .collapsible-icon) .pk-sidebar {
|
|
260
270
|
position: fixed;
|
|
@@ -264,15 +274,17 @@
|
|
|
264
274
|
height: 100dvh;
|
|
265
275
|
margin-left: 0;
|
|
266
276
|
transform: translateX(-100%);
|
|
267
|
-
|
|
277
|
+
/* closed drawer: invisible AND out of the tab order */
|
|
278
|
+
visibility: hidden;
|
|
279
|
+
box-shadow: 0 8px 30px color-mix(in srgb, var(--pk-shadow-color) 35%, transparent);
|
|
268
280
|
}
|
|
269
|
-
.pk-sidebar-wrapper:is(.collapsible-offcanvas, .collapsible-icon)[data-open] .pk-sidebar { transform: none; }
|
|
281
|
+
.pk-sidebar-wrapper:is(.collapsible-offcanvas, .collapsible-icon)[data-open] .pk-sidebar { transform: none; visibility: visible; }
|
|
270
282
|
.pk-sidebar-wrapper:is(.collapsible-offcanvas, .collapsible-icon)[data-open] .pk-sidebar-scrim {
|
|
271
283
|
display: block;
|
|
272
284
|
position: fixed;
|
|
273
285
|
inset: 0;
|
|
274
286
|
z-index: 49;
|
|
275
|
-
background:
|
|
287
|
+
background: color-mix(in srgb, var(--pk-overlay) 50%, transparent);
|
|
276
288
|
border: 0;
|
|
277
289
|
padding: 0;
|
|
278
290
|
cursor: default;
|