phlex_kit 0.9.1 → 0.10.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 +8 -0
- data/app/components/phlex_kit/alert/alert.rb +1 -1
- data/app/components/phlex_kit/alert_dialog/alert_dialog_controller.js +12 -0
- data/app/components/phlex_kit/attachment/attachment.css +6 -3
- data/app/components/phlex_kit/attachment/scroll_fade_controller.js +7 -2
- data/app/components/phlex_kit/avatar/avatar.css +2 -2
- data/app/components/phlex_kit/breadcrumb/breadcrumb.css +0 -4
- data/app/components/phlex_kit/bubble/bubble.css +1 -1
- data/app/components/phlex_kit/bubble/bubble_content.rb +4 -3
- data/app/components/phlex_kit/button_group/button_group.css +16 -16
- data/app/components/phlex_kit/calendar/calendar.css +10 -2
- data/app/components/phlex_kit/carousel/carousel.css +4 -4
- data/app/components/phlex_kit/clipboard/clipboard_controller.js +18 -2
- data/app/components/phlex_kit/collapsible/collapsible_controller.js +15 -6
- data/app/components/phlex_kit/collapsible/collapsible_trigger.rb +5 -2
- data/app/components/phlex_kit/combobox/combobox.css +3 -3
- data/app/components/phlex_kit/combobox/combobox.rb +3 -3
- data/app/components/phlex_kit/combobox/combobox_controller.js +17 -9
- data/app/components/phlex_kit/combobox/combobox_search_input.rb +3 -1
- data/app/components/phlex_kit/combobox/combobox_toggle_all_checkbox.rb +3 -1
- data/app/components/phlex_kit/command/command.css +2 -2
- data/app/components/phlex_kit/command/command_controller.js +57 -34
- data/app/components/phlex_kit/command/command_dialog_controller.js +30 -1
- data/app/components/phlex_kit/command/command_group.rb +5 -2
- data/app/components/phlex_kit/dialog/dialog.css +2 -2
- data/app/components/phlex_kit/dialog/dialog_close.rb +1 -1
- data/app/components/phlex_kit/dialog/dialog_controller.js +9 -0
- data/app/components/phlex_kit/drawer/drawer.css +4 -4
- data/app/components/phlex_kit/empty/empty.css +1 -1
- data/app/components/phlex_kit/field/field.css +2 -1
- data/app/components/phlex_kit/form_field/form_field_controller.js +7 -0
- data/app/components/phlex_kit/input_otp/input_otp.css +4 -4
- data/app/components/phlex_kit/marker/marker.rb +6 -0
- data/app/components/phlex_kit/masked_input/masked_input_controller.js +17 -3
- data/app/components/phlex_kit/menubar/menubar.rb +3 -1
- data/app/components/phlex_kit/menubar/menubar_controller.js +22 -1
- data/app/components/phlex_kit/menubar/menubar_item.rb +4 -2
- data/app/components/phlex_kit/menubar/menubar_radio_group.rb +4 -2
- data/app/components/phlex_kit/message_scroller/message_scroller_controller.js +9 -5
- data/app/components/phlex_kit/native_select/native_select.css +4 -2
- data/app/components/phlex_kit/navigation_menu/navigation_menu.rb +4 -1
- data/app/components/phlex_kit/pagination/pagination_next.rb +9 -2
- data/app/components/phlex_kit/pagination/pagination_previous.rb +9 -2
- data/app/components/phlex_kit/popover/popover_controller.js +9 -1
- data/app/components/phlex_kit/resizable/resizable_controller.js +49 -5
- data/app/components/phlex_kit/resizable/resizable_panel.rb +13 -4
- data/app/components/phlex_kit/scroll_area/scroll_area.rb +20 -4
- data/app/components/phlex_kit/select/select.css +0 -1
- data/app/components/phlex_kit/select/select_content.rb +9 -3
- data/app/components/phlex_kit/select/select_controller.js +32 -2
- data/app/components/phlex_kit/select/select_item.rb +5 -1
- data/app/components/phlex_kit/separator/separator.rb +7 -1
- data/app/components/phlex_kit/sheet/sheet_close.rb +1 -1
- data/app/components/phlex_kit/sidebar/sidebar.css +1 -1
- data/app/components/phlex_kit/sidebar/sidebar_controller.js +10 -0
- data/app/components/phlex_kit/tabs/tabs.css +2 -2
- data/app/components/phlex_kit/tabs/tabs_controller.js +4 -1
- data/app/components/phlex_kit/tabs/tabs_trigger.rb +4 -1
- data/app/components/phlex_kit/toast/toast.css +1 -1
- data/app/components/phlex_kit/toast/toast_controller.js +6 -1
- data/app/components/phlex_kit/toast/toaster_controller.js +19 -5
- data/app/components/phlex_kit/toggle_group/toggle_group.css +7 -7
- data/app/components/phlex_kit/toggle_group/toggle_group.rb +6 -1
- data/app/components/phlex_kit/toggle_group/toggle_group_controller.js +8 -0
- data/app/components/phlex_kit/tooltip/tooltip.css +2 -2
- data/app/components/phlex_kit/typography/heading.rb +6 -4
- data/lib/phlex_kit/version.rb +1 -1
- metadata +1 -1
|
@@ -33,7 +33,15 @@ export default class extends Controller {
|
|
|
33
33
|
this.contentTarget.togglePopover()
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// One stable handler added/removed symmetrically — an anonymous listener
|
|
37
|
+
// here would pile up a duplicate on every target reconnect.
|
|
38
|
+
syncState = (e) => { e.target.dataset.state = e.newState === "open" ? "open" : "closed" }
|
|
39
|
+
|
|
36
40
|
contentTargetConnected(el) {
|
|
37
|
-
el.addEventListener("toggle",
|
|
41
|
+
el.addEventListener("toggle", this.syncState)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
contentTargetDisconnected(el) {
|
|
45
|
+
el.removeEventListener("toggle", this.syncState)
|
|
38
46
|
}
|
|
39
47
|
}
|
|
@@ -47,15 +47,21 @@ export default class extends Controller {
|
|
|
47
47
|
// constant: normalizing to 2 rescaled the pair against untouched
|
|
48
48
|
// siblings, so in a 25/25/50 group the 50 panel ballooned on first drag.
|
|
49
49
|
pairGrow: this.growOf(prev) + this.growOf(next) || 2,
|
|
50
|
+
// Snapshot the group's total grow: min/max-size percentages are shares
|
|
51
|
+
// of the WHOLE group, and untouched siblings keep their grow mid-drag.
|
|
52
|
+
groupGrow: this.groupGrow(),
|
|
50
53
|
}
|
|
51
54
|
try { handle.setPointerCapture(e.pointerId) } catch {}
|
|
52
55
|
|
|
53
56
|
const onMove = (ev) => {
|
|
57
|
+
// LTR assumption: clientX grows toward the trailing panel; RTL drag
|
|
58
|
+
// inversion is a documented limitation.
|
|
54
59
|
const delta = (horizontal ? ev.clientX : ev.clientY) - drag.startPos
|
|
55
60
|
const total = drag.prevSize + drag.nextSize
|
|
56
61
|
const prevSize = Math.min(Math.max(drag.prevSize + delta, 0), total)
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
const prevGrow = this.clampPrevGrow(prev, next, (prevSize / total) * drag.pairGrow, drag.pairGrow, drag.groupGrow)
|
|
63
|
+
prev.style.flexGrow = prevGrow
|
|
64
|
+
next.style.flexGrow = drag.pairGrow - prevGrow
|
|
59
65
|
this.syncValuenow(handle)
|
|
60
66
|
}
|
|
61
67
|
const onUp = () => {
|
|
@@ -71,7 +77,8 @@ export default class extends Controller {
|
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
// 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
|
|
80
|
+
// Home/End collapse the leading panel to its min/max (clamped to any
|
|
81
|
+
// per-panel min-size/max-size bounds, same as drags).
|
|
75
82
|
keydown(e) {
|
|
76
83
|
const handle = e.currentTarget
|
|
77
84
|
const prev = handle.previousElementSibling
|
|
@@ -96,8 +103,9 @@ export default class extends Controller {
|
|
|
96
103
|
}
|
|
97
104
|
e.preventDefault()
|
|
98
105
|
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
const prevGrow = this.clampPrevGrow(prev, next, share * pairGrow, pairGrow, this.groupGrow())
|
|
107
|
+
prev.style.flexGrow = prevGrow
|
|
108
|
+
next.style.flexGrow = pairGrow - prevGrow
|
|
101
109
|
this.syncValuenow(handle)
|
|
102
110
|
}
|
|
103
111
|
|
|
@@ -106,6 +114,42 @@ export default class extends Controller {
|
|
|
106
114
|
return Number.isFinite(g) ? g : 1
|
|
107
115
|
}
|
|
108
116
|
|
|
117
|
+
// Clamp the leading panel's proposed flex-grow to both panels' optional
|
|
118
|
+
// min-size/max-size percentages. Bounds are shares of the whole group
|
|
119
|
+
// (grow / groupGrow), and the pair's combined grow is fixed, so a bound on
|
|
120
|
+
// one panel implies the complementary bound on its neighbour. Without any
|
|
121
|
+
// bounds this collapses to the historical [0, pairGrow] clamp.
|
|
122
|
+
clampPrevGrow(prev, next, prevGrow, pairGrow, groupGrow) {
|
|
123
|
+
const pb = this.boundsOf(prev)
|
|
124
|
+
const nb = this.boundsOf(next)
|
|
125
|
+
let lo = 0
|
|
126
|
+
let hi = pairGrow
|
|
127
|
+
if (pb.min !== null) lo = Math.max(lo, pb.min * groupGrow)
|
|
128
|
+
if (nb.max !== null) lo = Math.max(lo, pairGrow - nb.max * groupGrow)
|
|
129
|
+
if (pb.max !== null) hi = Math.min(hi, pb.max * groupGrow)
|
|
130
|
+
if (nb.min !== null) hi = Math.min(hi, pairGrow - nb.min * groupGrow)
|
|
131
|
+
if (lo > hi) return Math.min(Math.max(prevGrow, 0), pairGrow) // unsatisfiable bounds: ignore them
|
|
132
|
+
return Math.min(Math.max(prevGrow, lo), hi)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// A panel's declared size bounds as fractions of the group, or nulls.
|
|
136
|
+
boundsOf(el) {
|
|
137
|
+
const min = parseFloat(el.getAttribute("data-phlex-kit--resizable-min-size"))
|
|
138
|
+
const max = parseFloat(el.getAttribute("data-phlex-kit--resizable-max-size"))
|
|
139
|
+
return {
|
|
140
|
+
min: Number.isFinite(min) ? min / 100 : null,
|
|
141
|
+
max: Number.isFinite(max) ? max / 100 : null,
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Combined flex-grow of this group's own panels (direct children only —
|
|
146
|
+
// panels of a nested group belong to that group's own controller).
|
|
147
|
+
groupGrow() {
|
|
148
|
+
return [...this.element.children]
|
|
149
|
+
.filter((el) => el.matches('[data-phlex-kit--resizable-target~="panel"]'))
|
|
150
|
+
.reduce((sum, el) => sum + this.growOf(el), 0) || 2
|
|
151
|
+
}
|
|
152
|
+
|
|
109
153
|
syncValuenow(handle) {
|
|
110
154
|
const prev = handle.previousElementSibling
|
|
111
155
|
const next = handle.nextElementSibling
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
|
-
# One pane of a ResizablePanelGroup. `default_size:`
|
|
2
|
+
# One pane of a ResizablePanelGroup. `default_size:`, `min_size:` and
|
|
3
|
+
# `max_size:` are percentages of the group (shadcn's defaultSize/minSize/
|
|
4
|
+
# maxSize); the bounds ride along as data attributes the resizable
|
|
5
|
+
# controller clamps drags and keyboard resizes against.
|
|
3
6
|
# See resizable_panel_group.rb.
|
|
4
7
|
class ResizablePanel < BaseComponent
|
|
5
|
-
def initialize(default_size: nil, **attrs)
|
|
6
|
-
# Numeric coercion: the
|
|
8
|
+
def initialize(default_size: nil, min_size: nil, max_size: nil, **attrs)
|
|
9
|
+
# Numeric coercion: the values are interpolated into attributes, and
|
|
7
10
|
# hosts plausibly round-trip saved panel sizes from client state.
|
|
8
11
|
@default_size = default_size.nil? ? nil : Float(default_size)
|
|
12
|
+
@min_size = min_size.nil? ? nil : Float(min_size)
|
|
13
|
+
@max_size = max_size.nil? ? nil : Float(max_size)
|
|
9
14
|
@attrs = attrs
|
|
10
15
|
end
|
|
11
16
|
|
|
@@ -16,7 +21,11 @@ module PhlexKit
|
|
|
16
21
|
div(**mix({
|
|
17
22
|
class: "pk-resizable-panel",
|
|
18
23
|
style: style,
|
|
19
|
-
data: {
|
|
24
|
+
data: {
|
|
25
|
+
phlex_kit__resizable_target: "panel",
|
|
26
|
+
phlex_kit__resizable_min_size: @min_size && Kernel.format("%g", @min_size),
|
|
27
|
+
phlex_kit__resizable_max_size: @max_size && Kernel.format("%g", @max_size)
|
|
28
|
+
}.compact
|
|
20
29
|
}.compact, @attrs), &)
|
|
21
30
|
end
|
|
22
31
|
end
|
|
@@ -2,16 +2,32 @@ 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
4
|
# Constrain it with a height/width from the caller. tabindex=0 makes the
|
|
5
|
-
# scrollable region keyboard-focusable (WCAG
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# scrollable region keyboard-focusable (WCAG scrollable-region-focusable).
|
|
6
|
+
# Pass `aria: { label: ... }` (or aria-labelledby) to name it — only then
|
|
7
|
+
# does it render role=region; a nameless region is noise for AT, so the
|
|
8
|
+
# role is omitted when unlabelled. `.pk-scroll-area` (scroll_area.css).
|
|
8
9
|
class ScrollArea < BaseComponent
|
|
9
10
|
def initialize(**attrs)
|
|
10
11
|
@attrs = attrs
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def view_template(&)
|
|
14
|
-
|
|
15
|
+
base = { class: "pk-scroll-area", tabindex: "0" }
|
|
16
|
+
base[:role] = "region" if labelled?
|
|
17
|
+
div(**mix(base, @attrs), &)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# True when the caller supplies an accessible name, via the aria: hash
|
|
23
|
+
# (aria: { label:/labelledby: }) or flat aria_label/aria-labelledby attrs.
|
|
24
|
+
def labelled?
|
|
25
|
+
aria = @attrs[:aria] || @attrs["aria"]
|
|
26
|
+
if aria.is_a?(Hash)
|
|
27
|
+
return true if %w[label labelledby].any? { |k| aria[k.to_sym] || aria[k] }
|
|
28
|
+
end
|
|
29
|
+
[ :aria_label, "aria_label", "aria-label",
|
|
30
|
+
:aria_labelledby, "aria_labelledby", "aria-labelledby" ].any? { |k| @attrs[k] }
|
|
15
31
|
end
|
|
16
32
|
end
|
|
17
33
|
end
|
|
@@ -5,8 +5,11 @@ module PhlexKit
|
|
|
5
5
|
# inner `.pk-select-viewport` is the bordered, scrollable box.
|
|
6
6
|
# Holds SelectGroup / SelectLabel / SelectItem children. See select.rb.
|
|
7
7
|
class SelectContent < BaseComponent
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
# id: is a named kwarg (not left in **attrs) because `mix` would *merge* a
|
|
9
|
+
# caller id with the generated one into an invalid two-token id, breaking
|
|
10
|
+
# aria-controls and the generated item ids.
|
|
11
|
+
def initialize(id: nil, **attrs)
|
|
12
|
+
@id = id || "pk-select-content-#{SecureRandom.hex(4)}"
|
|
10
13
|
@attrs = attrs
|
|
11
14
|
end
|
|
12
15
|
|
|
@@ -17,7 +20,10 @@ module PhlexKit
|
|
|
17
20
|
tabindex: "-1",
|
|
18
21
|
class: "pk-select-content",
|
|
19
22
|
popover: "manual",
|
|
20
|
-
data: {
|
|
23
|
+
data: {
|
|
24
|
+
phlex_kit__select_target: "content",
|
|
25
|
+
action: "keydown->phlex-kit--select#typeahead"
|
|
26
|
+
}
|
|
21
27
|
}, @attrs)) do
|
|
22
28
|
div(class: "pk-select-viewport", &block)
|
|
23
29
|
end
|
|
@@ -21,6 +21,9 @@ export default class extends Controller {
|
|
|
21
21
|
// document-scoped outlet: outlets match `.pk-select-item` page-wide and
|
|
22
22
|
// clobbered every other Select's aria-selected.
|
|
23
23
|
const item = event.currentTarget;
|
|
24
|
+
// A malformed (hand-written) item without data-value must never submit
|
|
25
|
+
// the literal string "undefined". SelectItem itself fails loud on nil.
|
|
26
|
+
if (item.dataset.value === undefined) return;
|
|
24
27
|
this.itemTargets.forEach((el) => {
|
|
25
28
|
el.setAttribute("aria-selected", el === item ? "true" : "false");
|
|
26
29
|
});
|
|
@@ -36,7 +39,7 @@ export default class extends Controller {
|
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
onClick() {
|
|
39
|
-
this.
|
|
42
|
+
this.toggleContent();
|
|
40
43
|
|
|
41
44
|
if (this.openValue) {
|
|
42
45
|
this.setFocusAndCurrent();
|
|
@@ -127,10 +130,37 @@ export default class extends Controller {
|
|
|
127
130
|
// Open/close derive from the live :popover-open state, never a stored
|
|
128
131
|
// flag — a stale flag is how a close on an already-closed panel becomes
|
|
129
132
|
// an open (bit the popover's keyboard toggle).
|
|
130
|
-
|
|
133
|
+
toggleContent() {
|
|
131
134
|
this.contentTarget.matches(":popover-open") ? this.#hide() : this.#show();
|
|
132
135
|
}
|
|
133
136
|
|
|
137
|
+
// Listbox typeahead: printable keys accumulate into a short-lived buffer
|
|
138
|
+
// and move the highlight to the first option whose text starts with it.
|
|
139
|
+
typeahead(event) {
|
|
140
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
141
|
+
if (event.key.length !== 1 || event.key === " ") return; // Space selects
|
|
142
|
+
clearTimeout(this.typeaheadTimer);
|
|
143
|
+
this.typeaheadBuffer = (this.typeaheadBuffer || "") + event.key.toLowerCase();
|
|
144
|
+
this.typeaheadTimer = setTimeout(() => { this.typeaheadBuffer = ""; }, 500);
|
|
145
|
+
|
|
146
|
+
let index = this.itemTargets.findIndex((item) =>
|
|
147
|
+
item.innerText.trim().toLowerCase().startsWith(this.typeaheadBuffer));
|
|
148
|
+
if (index < 0) {
|
|
149
|
+
// Buffer stopped matching — restart it from this keystroke (APG
|
|
150
|
+
// typeahead: a failed prefix shouldn't dead-end until the timer clears).
|
|
151
|
+
this.typeaheadBuffer = event.key.toLowerCase();
|
|
152
|
+
index = this.itemTargets.findIndex((item) =>
|
|
153
|
+
item.innerText.trim().toLowerCase().startsWith(this.typeaheadBuffer));
|
|
154
|
+
}
|
|
155
|
+
if (index < 0) return;
|
|
156
|
+
this.resetCurrent();
|
|
157
|
+
this.setAriaCurrentAndActiveDescendant(index);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
disconnect() {
|
|
161
|
+
clearTimeout(this.typeaheadTimer);
|
|
162
|
+
}
|
|
163
|
+
|
|
134
164
|
#show() {
|
|
135
165
|
this.openValue = true;
|
|
136
166
|
this.contentTarget.showPopover();
|
|
@@ -7,7 +7,10 @@ module PhlexKit
|
|
|
7
7
|
# repeated attribute rather than override it.
|
|
8
8
|
# See select.rb.
|
|
9
9
|
class SelectItem < BaseComponent
|
|
10
|
-
def initialize(value
|
|
10
|
+
def initialize(value:, selected: false, **attrs)
|
|
11
|
+
# nil would render no data-value at all and the controller would submit
|
|
12
|
+
# the literal string "undefined" — fail loud instead.
|
|
13
|
+
raise ArgumentError, "SelectItem requires a value:" if value.nil?
|
|
11
14
|
@value = value
|
|
12
15
|
@selected = selected
|
|
13
16
|
@attrs = attrs
|
|
@@ -25,6 +28,7 @@ module PhlexKit
|
|
|
25
28
|
data: {
|
|
26
29
|
value: @value,
|
|
27
30
|
action: "click->phlex-kit--select#selectItem keydown.enter->phlex-kit--select#selectItem " \
|
|
31
|
+
"keydown.space->phlex-kit--select#selectItem " \
|
|
28
32
|
"keydown.down->phlex-kit--select#handleKeyDown keydown.up->phlex-kit--select#handleKeyUp " \
|
|
29
33
|
"keydown.home->phlex-kit--select#handleHome keydown.end->phlex-kit--select#handleEnd " \
|
|
30
34
|
"keydown.esc->phlex-kit--select#handleEsc",
|
|
@@ -6,9 +6,15 @@ module PhlexKit
|
|
|
6
6
|
# the kit's fail-loud selectors.
|
|
7
7
|
class Separator < BaseComponent
|
|
8
8
|
ORIENTATIONS = %i[horizontal vertical].freeze
|
|
9
|
+
# `as:` is dispatched dynamically (send) — whitelist the documented
|
|
10
|
+
# elements so it can't reach arbitrary (including private) methods.
|
|
11
|
+
AS_TAGS = %i[div hr].freeze
|
|
9
12
|
|
|
10
13
|
def initialize(as: :div, orientation: :horizontal, decorative: true, **attrs)
|
|
11
|
-
@as = as
|
|
14
|
+
@as = as.to_sym
|
|
15
|
+
unless AS_TAGS.include?(@as)
|
|
16
|
+
raise ArgumentError, "unknown Separator as: #{@as.inspect} (use one of #{AS_TAGS.join(", ")})"
|
|
17
|
+
end
|
|
12
18
|
@orientation = orientation.to_sym
|
|
13
19
|
unless ORIENTATIONS.include?(@orientation)
|
|
14
20
|
raise ArgumentError, "Invalid orientation: #{orientation.inspect}"
|
|
@@ -4,7 +4,7 @@ module PhlexKit
|
|
|
4
4
|
class SheetClose < BaseComponent
|
|
5
5
|
def initialize(**attrs) = (@attrs = attrs)
|
|
6
6
|
def view_template(&)
|
|
7
|
-
span(**mix({ style: "display: contents", data: { action: "click->phlex-kit--sheet-content#close" } }, @attrs), &)
|
|
7
|
+
span(**mix({ style: "display: contents;", data: { action: "click->phlex-kit--sheet-content#close" } }, @attrs), &)
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -25,11 +25,21 @@ export default class extends Controller {
|
|
|
25
25
|
static COOKIE = "pk_sidebar_state"
|
|
26
26
|
|
|
27
27
|
connect() {
|
|
28
|
+
// Crossing to desktop while the mobile drawer is open would strand the
|
|
29
|
+
// page inert: the scrim/drawer CSS stops applying at >=768px but the
|
|
30
|
+
// inert marks (and data-open) would survive. Close on breakpoint change.
|
|
31
|
+
this.mql = window.matchMedia(this.constructor.MOBILE)
|
|
32
|
+
this.onBreakpoint = () => {
|
|
33
|
+
if (!this.mql.matches) this.closeMobile()
|
|
34
|
+
this.#syncExpanded()
|
|
35
|
+
}
|
|
36
|
+
this.mql.addEventListener("change", this.onBreakpoint)
|
|
28
37
|
this.#wireAriaControls()
|
|
29
38
|
this.#syncExpanded()
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
disconnect() {
|
|
42
|
+
this.mql.removeEventListener("change", this.onBreakpoint)
|
|
33
43
|
this.#restoreInert()
|
|
34
44
|
}
|
|
35
45
|
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
border-color: transparent;
|
|
45
45
|
box-shadow: none;
|
|
46
46
|
}
|
|
47
|
-
.pk-tabs-trigger::after {
|
|
47
|
+
.pk-tabs-list.line .pk-tabs-trigger::after {
|
|
48
48
|
content: "";
|
|
49
49
|
position: absolute;
|
|
50
50
|
inset-inline: 0;
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
opacity: 0;
|
|
55
55
|
transition: opacity .15s ease;
|
|
56
56
|
}
|
|
57
|
-
.pk-tabs.vertical .pk-tabs-trigger::after {
|
|
57
|
+
.pk-tabs.vertical .pk-tabs-list.line .pk-tabs-trigger::after {
|
|
58
58
|
inset-inline: auto;
|
|
59
59
|
inset-block: 0;
|
|
60
60
|
inset-inline-end: -.25rem;
|
|
@@ -13,7 +13,10 @@ export default class extends Controller {
|
|
|
13
13
|
this.listTarget.setAttribute("aria-orientation", "vertical")
|
|
14
14
|
}
|
|
15
15
|
if (!this.hasActiveValue && this.triggerTargets.length > 0) {
|
|
16
|
-
|
|
16
|
+
// Honor a server-marked TabsTrigger(active: true) before falling back
|
|
17
|
+
// to the first trigger — hydration must not contradict the markup.
|
|
18
|
+
const marked = this.triggerTargets.find((el) => el.dataset.state === "active")
|
|
19
|
+
this.activeValue = (marked || this.triggerTargets[0]).dataset.value
|
|
17
20
|
} else {
|
|
18
21
|
this.sync()
|
|
19
22
|
}
|
|
@@ -17,7 +17,10 @@ module PhlexKit
|
|
|
17
17
|
aria_selected: @active ? "true" : "false",
|
|
18
18
|
aria_controls: "pk-tabs-panel-#{@value}",
|
|
19
19
|
tabindex: @active ? "0" : "-1",
|
|
20
|
-
data
|
|
20
|
+
# data-state is the only hook tabs.css styles the active trigger by —
|
|
21
|
+
# it must be server-rendered or the active tab looks inactive pre-JS.
|
|
22
|
+
data: { phlex_kit__tabs_target: "trigger", action: "click->phlex-kit--tabs#show", value: @value,
|
|
23
|
+
state: @active ? "active" : "inactive" }
|
|
21
24
|
}
|
|
22
25
|
if @as == :a
|
|
23
26
|
a(**mix(base, @attrs), &)
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
width: 356px;
|
|
41
41
|
max-width: 100%;
|
|
42
42
|
border: 1px solid var(--pk-border);
|
|
43
|
-
border-radius: 1rem
|
|
43
|
+
border-radius: calc(var(--pk-radius) + 6px); /* 1rem at the default 0.625rem token */
|
|
44
44
|
background: var(--pk-surface);
|
|
45
45
|
color: var(--pk-text);
|
|
46
46
|
padding: 1rem;
|
|
@@ -17,6 +17,7 @@ export default class extends Controller {
|
|
|
17
17
|
|
|
18
18
|
connect() {
|
|
19
19
|
this._timer = null
|
|
20
|
+
this._unmountTimer = null
|
|
20
21
|
this._startedAt = 0
|
|
21
22
|
this._remaining = this.durationValue
|
|
22
23
|
this._paused = false
|
|
@@ -66,6 +67,10 @@ export default class extends Controller {
|
|
|
66
67
|
|
|
67
68
|
disconnect() {
|
|
68
69
|
this._clearTimer()
|
|
70
|
+
// Don't let a pending 200ms unmount fire after teardown (e.g. into a
|
|
71
|
+
// Turbo-cached copy of the page).
|
|
72
|
+
if (this._unmountTimer) clearTimeout(this._unmountTimer)
|
|
73
|
+
this._unmountTimer = null
|
|
69
74
|
this.element.removeEventListener("pointerdown", this._onPointerDown)
|
|
70
75
|
this.element.removeEventListener("pointerenter", this._onPointerEnter)
|
|
71
76
|
this.element.removeEventListener("pointerleave", this._onPointerLeave)
|
|
@@ -89,7 +94,7 @@ export default class extends Controller {
|
|
|
89
94
|
this.element.dataset.state = "closing"
|
|
90
95
|
this.element.dispatchEvent(new CustomEvent(reason === "auto" ? "phlex-kit:toast:auto-close" : "phlex-kit:toast:dismiss", { bubbles: true, detail: { id: this.element.id } }))
|
|
91
96
|
this._invokeCallback(reason === "auto" ? this.onAutoCloseValue : this.onDismissValue)
|
|
92
|
-
setTimeout(() => this.element.remove(), TIME_BEFORE_UNMOUNT)
|
|
97
|
+
this._unmountTimer = setTimeout(() => this.element.remove(), TIME_BEFORE_UNMOUNT)
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
// on_dismiss:/on_auto_close: name a global function (Sonner-style callback
|
|
@@ -50,6 +50,7 @@ export default class extends Controller {
|
|
|
50
50
|
this._heights = new Map()
|
|
51
51
|
this._resizeObservers = new WeakMap()
|
|
52
52
|
this._expanded = false
|
|
53
|
+
this._paused = false
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
connect() {
|
|
@@ -59,8 +60,11 @@ export default class extends Controller {
|
|
|
59
60
|
registerStreamAction()
|
|
60
61
|
if (!this._listEl) return
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
// Pause/resume tracks the POINTER, not the expand transition: an
|
|
64
|
+
// expand:true region is born expanded, and tying the pause dispatch to
|
|
65
|
+
// _setExpanded's state change left auto-dismiss running under the cursor.
|
|
66
|
+
this._onPointerEnter = () => { this._setPaused(true); this._setExpanded(true) }
|
|
67
|
+
this._onPointerLeave = () => { this._setPaused(false); if (!this.expandValue) this._setExpanded(false) }
|
|
64
68
|
this._onWindowToast = (e) => this._spawn(e.detail || {})
|
|
65
69
|
this._onWindowDismissAll = () => this._dismissById(null)
|
|
66
70
|
this._onKey = this._onKey.bind(this)
|
|
@@ -82,6 +86,10 @@ export default class extends Controller {
|
|
|
82
86
|
this._listEl?.removeEventListener("pointerenter", this._onPointerEnter)
|
|
83
87
|
this._listEl?.removeEventListener("pointerleave", this._onPointerLeave)
|
|
84
88
|
document.removeEventListener("keydown", this._onKey)
|
|
89
|
+
// Drop the global API if it still closes over this controller — after
|
|
90
|
+
// region teardown it would touch a stale _listEl. A replacement region
|
|
91
|
+
// re-registers its own on connect.
|
|
92
|
+
if (window.PhlexKit?.toast === this._api) delete window.PhlexKit.toast
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
toastTargetConnected(el) {
|
|
@@ -178,13 +186,18 @@ export default class extends Controller {
|
|
|
178
186
|
return tpl.content.firstElementChild.cloneNode(true)
|
|
179
187
|
}
|
|
180
188
|
|
|
181
|
-
|
|
182
|
-
if (this.
|
|
183
|
-
this.
|
|
189
|
+
_setPaused(value) {
|
|
190
|
+
if (this._paused === value) return
|
|
191
|
+
this._paused = value
|
|
184
192
|
// Mirror the pause state onto the DOM so toasts spawned while the stack
|
|
185
193
|
// is already hovered can arm paused (the pause event predates them).
|
|
186
194
|
this._listEl?.toggleAttribute("data-pk-toasts-paused", value)
|
|
187
195
|
document.dispatchEvent(new CustomEvent(value ? "phlex-kit:toast:pause" : "phlex-kit:toast:resume"))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
_setExpanded(value) {
|
|
199
|
+
if (this._expanded === value) return
|
|
200
|
+
this._expanded = value
|
|
188
201
|
this._reflow()
|
|
189
202
|
}
|
|
190
203
|
|
|
@@ -303,6 +316,7 @@ export default class extends Controller {
|
|
|
303
316
|
}
|
|
304
317
|
|
|
305
318
|
window.PhlexKit = window.PhlexKit || {}
|
|
319
|
+
this._api = api
|
|
306
320
|
window.PhlexKit.toast = api
|
|
307
321
|
}
|
|
308
322
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/* Co-located with toggle_group.rb. Theme tokens from the global stylesheet. */
|
|
2
2
|
.pk-toggle-group { display: inline-flex; width: fit-content; align-items: center; border-radius: var(--pk-radius); }
|
|
3
3
|
.pk-toggle-group.vertical { flex-direction: column; align-items: stretch; }
|
|
4
|
-
|
|
4
|
+
/* spacing: N stamps --pk-toggle-group-gap inline (N × .25rem). */
|
|
5
|
+
.pk-toggle-group.spaced { gap: var(--pk-toggle-group-gap, .25rem); }
|
|
5
6
|
/* Joined (default, unspaced): square the inner corners, tighten padding. */
|
|
6
|
-
.pk-toggle-group:not(.spaced) .pk-toggle-group-item { padding-inline: .5rem; }
|
|
7
|
-
.pk-toggle-group:not(.spaced) .pk-toggle-group-item { border-radius:
|
|
8
|
-
.pk-toggle-group:not(.spaced):not(.vertical) .pk-toggle-group-item:
|
|
9
|
-
.pk-toggle-group:not(.spaced)
|
|
10
|
-
.pk-toggle-group.vertical:not(.spaced) .pk-toggle-group-item:
|
|
11
|
-
.pk-toggle-group.vertical:not(.spaced) .pk-toggle-group-item:last-of-type { border-bottom-left-radius: var(--pk-radius); border-bottom-right-radius: var(--pk-radius); }
|
|
7
|
+
.pk-toggle-group:not(.spaced) .pk-toggle-group-item { padding-inline: .5rem; border-radius: 0; }
|
|
8
|
+
.pk-toggle-group:not(.spaced):not(.vertical) .pk-toggle-group-item:first-of-type { border-start-start-radius: var(--pk-radius); border-end-start-radius: var(--pk-radius); }
|
|
9
|
+
.pk-toggle-group:not(.spaced):not(.vertical) .pk-toggle-group-item:last-of-type { border-start-end-radius: var(--pk-radius); border-end-end-radius: var(--pk-radius); }
|
|
10
|
+
.pk-toggle-group.vertical:not(.spaced) .pk-toggle-group-item:first-of-type { border-start-start-radius: var(--pk-radius); border-start-end-radius: var(--pk-radius); }
|
|
11
|
+
.pk-toggle-group.vertical:not(.spaced) .pk-toggle-group-item:last-of-type { border-end-start-radius: var(--pk-radius); border-end-end-radius: var(--pk-radius); }
|
|
@@ -54,12 +54,17 @@ module PhlexKit
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def group_default_attrs
|
|
57
|
-
{ role: (@type == :single) ? "radiogroup" : "group",
|
|
57
|
+
attrs = { role: (@type == :single) ? "radiogroup" : "group",
|
|
58
58
|
class: [ "pk-toggle-group", (@orientation == :vertical ? "vertical" : nil), (@spacing > 0 ? "spaced" : nil) ].compact.join(" "),
|
|
59
59
|
data: { controller: "phlex-kit--toggle-group",
|
|
60
60
|
phlex_kit__toggle_group_type_value: @type.to_s,
|
|
61
61
|
phlex_kit__toggle_group_name_value: @name.to_s,
|
|
62
62
|
orientation: @orientation.to_s, spacing: @spacing.to_s } }
|
|
63
|
+
# spacing: N gaps the items by N × .25rem (Tailwind-style scale) via the
|
|
64
|
+
# custom property toggle_group.css reads. Trailing ";" so a caller
|
|
65
|
+
# style: merges into a second valid declaration (Phlex mix gotcha).
|
|
66
|
+
attrs[:style] = "--pk-toggle-group-gap: #{@spacing * 0.25}rem;" if @spacing > 0
|
|
67
|
+
attrs
|
|
63
68
|
end
|
|
64
69
|
end
|
|
65
70
|
end
|
|
@@ -17,6 +17,14 @@ export default class extends Controller {
|
|
|
17
17
|
}
|
|
18
18
|
this.rebuildInputs()
|
|
19
19
|
this.updateRovingTabindex(item)
|
|
20
|
+
// Parity with Toggle: announce the selection as
|
|
21
|
+
// "phlex-kit--toggle-group:change" — single carries the one selected
|
|
22
|
+
// value (radiogroup semantics: no deselect), multiple the pressed array.
|
|
23
|
+
const values = this.itemTargets.filter((el) => this.isPressed(el)).map((el) => el.dataset.value)
|
|
24
|
+
this.dispatch("change", {
|
|
25
|
+
detail: this.typeValue === "single" ? { value: values[0] ?? null } : { value: values },
|
|
26
|
+
bubbles: true
|
|
27
|
+
})
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
navigate(event) {
|
|
@@ -64,11 +64,11 @@
|
|
|
64
64
|
}
|
|
65
65
|
.pk-tooltip-content.bottom { position-area: block-end; margin: 6px 0 0; }
|
|
66
66
|
.pk-tooltip-content.left {
|
|
67
|
-
position-area: inline-start; margin: 0 6px
|
|
67
|
+
position-area: inline-start; margin: 0; margin-inline-end: 6px;
|
|
68
68
|
position-try-fallbacks: flip-inline;
|
|
69
69
|
}
|
|
70
70
|
.pk-tooltip-content.right {
|
|
71
|
-
position-area: inline-end; margin: 0
|
|
71
|
+
position-area: inline-end; margin: 0; margin-inline-start: 6px;
|
|
72
72
|
position-try-fallbacks: flip-inline;
|
|
73
73
|
}
|
|
74
74
|
/* Hover reveal only on devices that actually hover (a touch tap must not
|
|
@@ -9,10 +9,12 @@ module PhlexKit
|
|
|
9
9
|
# extend the scale downward — HTML has h1–h6, so all six must map.
|
|
10
10
|
LEVEL_SIZE = { "1" => "7", "2" => "6", "3" => "5", "4" => "4", "5" => "3", "6" => "2" }.freeze
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
# level: defaults to 1 so `Heading.new` is identical to `Heading.new(level: 1)`
|
|
13
|
+
# (a nil default used to render h1 with the level-6 size — self-disagreeing).
|
|
14
|
+
def initialize(level: 1, as: nil, size: nil, **attrs)
|
|
13
15
|
# Fail loud at construction: level 7 previously exploded at render time
|
|
14
16
|
# with NoMethodError (no h7 element), and 5/6 fell through the size map.
|
|
15
|
-
unless
|
|
17
|
+
unless LEVEL_SIZE.key?(level.to_s)
|
|
16
18
|
raise ArgumentError, "Invalid heading level: #{level.inspect} — valid: 1–6"
|
|
17
19
|
end
|
|
18
20
|
@level = level
|
|
@@ -28,11 +30,11 @@ module PhlexKit
|
|
|
28
30
|
private
|
|
29
31
|
|
|
30
32
|
def element
|
|
31
|
-
(@as ||
|
|
33
|
+
(@as || "h#{@level}").to_sym
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
def size_token
|
|
35
|
-
token = (@size || LEVEL_SIZE
|
|
37
|
+
token = (@size || LEVEL_SIZE.fetch(@level.to_s)).to_s
|
|
36
38
|
raise ArgumentError, "Invalid heading size: #{token.inspect}" unless SIZES.include?(token)
|
|
37
39
|
token
|
|
38
40
|
end
|
data/lib/phlex_kit/version.rb
CHANGED