phlex_kit 0.9.1 → 0.11.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/assets/stylesheets/phlex_kit/phlex_kit.css +1 -0
- data/app/components/phlex_kit/accordion/accordion.css +7 -0
- data/app/components/phlex_kit/alert/alert.rb +1 -1
- data/app/components/phlex_kit/alert_dialog/alert_dialog_controller.js +15 -0
- data/app/components/phlex_kit/attachment/attachment.css +6 -3
- data/app/components/phlex_kit/attachment/attachment_group.rb +6 -1
- data/app/components/phlex_kit/attachment/attachment_trigger.rb +5 -0
- 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/avatar/avatar_badge.rb +9 -2
- data/app/components/phlex_kit/badge/badge.css +1 -1
- 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.rb +4 -6
- data/app/components/phlex_kit/bubble/bubble_content.rb +4 -3
- data/app/components/phlex_kit/bubble/bubble_reactions.rb +4 -6
- 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/calendar/calendar_controller.js +33 -9
- data/app/components/phlex_kit/carousel/carousel.css +4 -4
- data/app/components/phlex_kit/carousel/carousel.rb +3 -2
- data/app/components/phlex_kit/carousel/carousel_controller.js +23 -2
- data/app/components/phlex_kit/chart/chart.rb +8 -2
- data/app/components/phlex_kit/clipboard/clipboard_controller.js +26 -3
- data/app/components/phlex_kit/clipboard/clipboard_trigger.rb +1 -1
- data/app/components/phlex_kit/codeblock/codeblock.rb +7 -1
- data/app/components/phlex_kit/collapsible/collapsible.rb +4 -1
- 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_badge_trigger.rb +6 -2
- data/app/components/phlex_kit/combobox/combobox_controller.js +32 -10
- data/app/components/phlex_kit/combobox/combobox_input_trigger.rb +4 -1
- data/app/components/phlex_kit/combobox/combobox_list.rb +6 -2
- data/app/components/phlex_kit/combobox/combobox_search_input.rb +3 -1
- data/app/components/phlex_kit/combobox/combobox_toggle_all_checkbox.rb +7 -1
- data/app/components/phlex_kit/command/command.css +2 -2
- data/app/components/phlex_kit/command/command_controller.js +63 -35
- 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/command/command_item.rb +4 -0
- data/app/components/phlex_kit/command/command_list.rb +6 -2
- data/app/components/phlex_kit/context_menu/context_menu_controller.js +25 -0
- data/app/components/phlex_kit/context_menu/context_menu_sub.rb +3 -1
- data/app/components/phlex_kit/context_menu/context_menu_sub_trigger.rb +3 -1
- data/app/components/phlex_kit/data_table/data_table.css +0 -10
- data/app/components/phlex_kit/data_table/data_table_column_toggle.rb +4 -1
- data/app/components/phlex_kit/data_table/data_table_controller.js +4 -0
- data/app/components/phlex_kit/date_picker/date_picker.rb +6 -0
- data/app/components/phlex_kit/dialog/dialog.css +5 -3
- data/app/components/phlex_kit/dialog/dialog_close.rb +1 -1
- data/app/components/phlex_kit/dialog/dialog_content.rb +1 -1
- data/app/components/phlex_kit/dialog/dialog_controller.js +22 -2
- data/app/components/phlex_kit/drawer/drawer.css +4 -4
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu.css +5 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_controller.js +27 -0
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub.rb +3 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub_trigger.rb +3 -1
- 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/field/field_separator.rb +1 -1
- data/app/components/phlex_kit/form_field/form_field_controller.js +29 -1
- data/app/components/phlex_kit/form_field/form_field_hint.rb +6 -2
- data/app/components/phlex_kit/hover_card/hover_card.css +4 -2
- data/app/components/phlex_kit/icon/icon.css +9 -0
- data/app/components/phlex_kit/icon/icon.rb +7 -1
- data/app/components/phlex_kit/input_group/input_group.css +0 -1
- data/app/components/phlex_kit/input_otp/input_otp.css +4 -4
- data/app/components/phlex_kit/marker/marker.rb +15 -2
- data/app/components/phlex_kit/masked_input/masked_input_controller.js +35 -15
- data/app/components/phlex_kit/menubar/menubar.rb +3 -1
- data/app/components/phlex_kit/menubar/menubar_controller.js +28 -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/message.rb +5 -0
- data/app/components/phlex_kit/message_scroller/message_scroller_controller.js +12 -5
- data/app/components/phlex_kit/native_select/native_select.css +4 -2
- data/app/components/phlex_kit/native_select/native_select.rb +12 -3
- data/app/components/phlex_kit/navigation_menu/navigation_menu.rb +7 -1
- data/app/components/phlex_kit/pagination/pagination.rb +10 -2
- data/app/components/phlex_kit/pagination/pagination_next.rb +11 -2
- data/app/components/phlex_kit/pagination/pagination_previous.rb +11 -2
- data/app/components/phlex_kit/popover/popover_controller.js +31 -1
- data/app/components/phlex_kit/progress/progress.css +8 -0
- data/app/components/phlex_kit/progress/progress.rb +14 -5
- data/app/components/phlex_kit/resizable/resizable_controller.js +52 -5
- data/app/components/phlex_kit/resizable/resizable_panel.rb +13 -4
- data/app/components/phlex_kit/scroll_area/scroll_area.rb +23 -4
- data/app/components/phlex_kit/select/select.css +5 -3
- data/app/components/phlex_kit/select/select_content.rb +9 -3
- data/app/components/phlex_kit/select/select_controller.js +59 -6
- data/app/components/phlex_kit/select/select_item.rb +5 -1
- data/app/components/phlex_kit/separator/separator.rb +12 -2
- data/app/components/phlex_kit/sheet/sheet.css +4 -2
- data/app/components/phlex_kit/sheet/sheet_close.rb +1 -1
- data/app/components/phlex_kit/sheet/sheet_content_controller.js +4 -0
- data/app/components/phlex_kit/sheet/sheet_controller.js +15 -0
- 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/sidebar/sidebar_menu_button.rb +9 -1
- data/app/components/phlex_kit/sidebar/sidebar_menu_sub_button.rb +8 -1
- data/app/components/phlex_kit/slider/slider.css +15 -0
- data/app/components/phlex_kit/switch/switch.css +5 -0
- data/app/components/phlex_kit/table/table.css +2 -1
- data/app/components/phlex_kit/table/table.rb +5 -2
- data/app/components/phlex_kit/tabs/tabs.css +2 -2
- data/app/components/phlex_kit/tabs/tabs_content.rb +4 -2
- data/app/components/phlex_kit/tabs/tabs_controller.js +4 -1
- data/app/components/phlex_kit/tabs/tabs_list.rb +7 -2
- data/app/components/phlex_kit/tabs/tabs_trigger.rb +15 -3
- data/app/components/phlex_kit/theme_toggle/theme_toggle_controller.js +14 -0
- data/app/components/phlex_kit/toast/toast.css +3 -2
- data/app/components/phlex_kit/toast/toast_action.rb +4 -1
- data/app/components/phlex_kit/toast/toast_controller.js +13 -5
- data/app/components/phlex_kit/toast/toast_region.rb +19 -9
- data/app/components/phlex_kit/toast/toaster_controller.js +34 -12
- data/app/components/phlex_kit/toggle_group/toggle_group.css +7 -7
- data/app/components/phlex_kit/toggle_group/toggle_group.rb +7 -2
- data/app/components/phlex_kit/toggle_group/toggle_group_controller.js +13 -1
- data/app/components/phlex_kit/toggle_group/toggle_group_item.rb +9 -1
- data/app/components/phlex_kit/tooltip/tooltip.css +7 -3
- data/app/components/phlex_kit/tooltip/tooltip_controller.js +7 -1
- data/app/components/phlex_kit/typography/heading.rb +13 -5
- data/app/components/phlex_kit/typography/list.rb +22 -0
- data/app/components/phlex_kit/typography/text.rb +7 -1
- data/lib/phlex_kit/base_component.rb +13 -0
- data/lib/phlex_kit/version.rb +1 -1
- metadata +3 -1
|
@@ -9,7 +9,28 @@ export default class extends Controller {
|
|
|
9
9
|
static targets = ["trigger", "content", "input", "value", "item"];
|
|
10
10
|
static values = { open: Boolean };
|
|
11
11
|
|
|
12
|
+
initialize() {
|
|
13
|
+
// Stimulus fires [target]Connected before connect(): state used by
|
|
14
|
+
// itemTargetConnected must be initialized here.
|
|
15
|
+
this.itemIdCounter = 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Per-item, not a connect()-time sweep: options injected later (Turbo
|
|
19
|
+
// stream/frame) must still get ids or aria-activedescendant would point
|
|
20
|
+
// at nothing — and a caller-provided id is left alone, not clobbered.
|
|
21
|
+
itemTargetConnected(item) {
|
|
22
|
+
if (!item.id) item.id = `${this.contentTarget.id}-${this.itemIdCounter++}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
connect() {
|
|
26
|
+
// A Turbo snapshot serializes aria-expanded / aria-activedescendant /
|
|
27
|
+
// aria-current even though :popover-open does not survive the restore —
|
|
28
|
+
// normalize here so a restored page doesn't announce an open listbox
|
|
29
|
+
// over a closed select (same mitigation as dropdown_menu's connect).
|
|
30
|
+
this.openValue = false;
|
|
31
|
+
this.triggerTarget.setAttribute("aria-expanded", "false");
|
|
32
|
+
this.triggerTarget.removeAttribute("aria-activedescendant");
|
|
33
|
+
this.itemTargets.forEach((item) => item.removeAttribute("aria-current"));
|
|
13
34
|
this.generateItemsIds();
|
|
14
35
|
}
|
|
15
36
|
|
|
@@ -21,6 +42,9 @@ export default class extends Controller {
|
|
|
21
42
|
// document-scoped outlet: outlets match `.pk-select-item` page-wide and
|
|
22
43
|
// clobbered every other Select's aria-selected.
|
|
23
44
|
const item = event.currentTarget;
|
|
45
|
+
// A malformed (hand-written) item without data-value must never submit
|
|
46
|
+
// the literal string "undefined". SelectItem itself fails loud on nil.
|
|
47
|
+
if (item.dataset.value === undefined) return;
|
|
24
48
|
this.itemTargets.forEach((el) => {
|
|
25
49
|
el.setAttribute("aria-selected", el === item ? "true" : "false");
|
|
26
50
|
});
|
|
@@ -36,7 +60,7 @@ export default class extends Controller {
|
|
|
36
60
|
}
|
|
37
61
|
|
|
38
62
|
onClick() {
|
|
39
|
-
this.
|
|
63
|
+
this.toggleContent();
|
|
40
64
|
|
|
41
65
|
if (this.openValue) {
|
|
42
66
|
this.setFocusAndCurrent();
|
|
@@ -127,10 +151,37 @@ export default class extends Controller {
|
|
|
127
151
|
// Open/close derive from the live :popover-open state, never a stored
|
|
128
152
|
// flag — a stale flag is how a close on an already-closed panel becomes
|
|
129
153
|
// an open (bit the popover's keyboard toggle).
|
|
130
|
-
|
|
154
|
+
toggleContent() {
|
|
131
155
|
this.contentTarget.matches(":popover-open") ? this.#hide() : this.#show();
|
|
132
156
|
}
|
|
133
157
|
|
|
158
|
+
// Listbox typeahead: printable keys accumulate into a short-lived buffer
|
|
159
|
+
// and move the highlight to the first option whose text starts with it.
|
|
160
|
+
typeahead(event) {
|
|
161
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
162
|
+
if (event.key.length !== 1 || event.key === " ") return; // Space selects
|
|
163
|
+
clearTimeout(this.typeaheadTimer);
|
|
164
|
+
this.typeaheadBuffer = (this.typeaheadBuffer || "") + event.key.toLowerCase();
|
|
165
|
+
this.typeaheadTimer = setTimeout(() => { this.typeaheadBuffer = ""; }, 500);
|
|
166
|
+
|
|
167
|
+
let index = this.itemTargets.findIndex((item) =>
|
|
168
|
+
item.innerText.trim().toLowerCase().startsWith(this.typeaheadBuffer));
|
|
169
|
+
if (index < 0) {
|
|
170
|
+
// Buffer stopped matching — restart it from this keystroke (APG
|
|
171
|
+
// typeahead: a failed prefix shouldn't dead-end until the timer clears).
|
|
172
|
+
this.typeaheadBuffer = event.key.toLowerCase();
|
|
173
|
+
index = this.itemTargets.findIndex((item) =>
|
|
174
|
+
item.innerText.trim().toLowerCase().startsWith(this.typeaheadBuffer));
|
|
175
|
+
}
|
|
176
|
+
if (index < 0) return;
|
|
177
|
+
this.resetCurrent();
|
|
178
|
+
this.setAriaCurrentAndActiveDescendant(index);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
disconnect() {
|
|
182
|
+
clearTimeout(this.typeaheadTimer);
|
|
183
|
+
}
|
|
184
|
+
|
|
134
185
|
#show() {
|
|
135
186
|
this.openValue = true;
|
|
136
187
|
this.contentTarget.showPopover();
|
|
@@ -146,10 +197,12 @@ export default class extends Controller {
|
|
|
146
197
|
generateItemsIds() {
|
|
147
198
|
const contentId = this.contentTarget.getAttribute("id");
|
|
148
199
|
this.triggerTarget.setAttribute("aria-controls", contentId);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
200
|
+
// Name the listbox from the trigger (Radix wires it the same way) —
|
|
201
|
+
// an unnamed role=listbox is announced as an anonymous list.
|
|
202
|
+
if (!this.triggerTarget.id) this.triggerTarget.id = `${contentId}-trigger`;
|
|
203
|
+
if (!this.contentTarget.hasAttribute("aria-labelledby")) {
|
|
204
|
+
this.contentTarget.setAttribute("aria-labelledby", this.triggerTarget.id);
|
|
205
|
+
}
|
|
153
206
|
}
|
|
154
207
|
|
|
155
208
|
setAriaCurrentAndActiveDescendant(currentIndex) {
|
|
@@ -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}"
|
|
@@ -18,7 +24,11 @@ module PhlexKit
|
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
def view_template(&block)
|
|
21
|
-
|
|
27
|
+
base = { role: (@decorative ? "none" : "separator"), class: classes }
|
|
28
|
+
# role="separator" is implicitly horizontal — a semantic vertical one
|
|
29
|
+
# must say so or AT announces the wrong axis.
|
|
30
|
+
base[:"aria-orientation"] = "vertical" if !@decorative && @orientation == :vertical
|
|
31
|
+
send(@as, **mix(base, @attrs), &block)
|
|
22
32
|
end
|
|
23
33
|
|
|
24
34
|
private
|
|
@@ -16,8 +16,10 @@
|
|
|
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); }
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
/* max-height: without it a tall top/bottom panel grows past the viewport and
|
|
20
|
+
overflow:auto never engages (footer/close unreachable). */
|
|
21
|
+
.pk-sheet-content.top { inset-inline: 0; top: 0; max-height: 100dvh; border-bottom: 1px solid var(--pk-border); }
|
|
22
|
+
.pk-sheet-content.bottom { inset-inline: 0; bottom: 0; max-height: 100dvh; border-top: 1px solid var(--pk-border); }
|
|
21
23
|
.pk-sheet-header { display: flex; flex-direction: column; gap: .125rem; padding: 1rem; }
|
|
22
24
|
.pk-sheet-footer { display: flex; flex-direction: column-reverse; gap: .5rem; padding: 1rem; margin-top: auto; }
|
|
23
25
|
.pk-sheet-title { font-size: 1rem; font-weight: 500; line-height: 1; color: var(--pk-text); }
|
|
@@ -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
|
|
@@ -29,6 +29,10 @@ export default class extends Controller {
|
|
|
29
29
|
this.#restoreInert()
|
|
30
30
|
document.body.style.overflow = this.previousOverflow
|
|
31
31
|
if (this.opener?.isConnected) this.opener.focus()
|
|
32
|
+
// Tell the source phlex-kit--sheet controller the clone is gone so it
|
|
33
|
+
// can flip the trigger's aria-expanded back (removal happens by many
|
|
34
|
+
// paths: close button, Escape, backdrop, before-cache).
|
|
35
|
+
document.dispatchEvent(new CustomEvent("phlex-kit:sheet-content:closed"))
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
close() {
|
|
@@ -19,17 +19,32 @@ export default class extends Controller {
|
|
|
19
19
|
|
|
20
20
|
connect() {
|
|
21
21
|
document.addEventListener("turbo:before-cache", this.clearOverlay)
|
|
22
|
+
// Popup semantics for AT: the trigger's real control announces that it
|
|
23
|
+
// opens a dialog, and expanded state tracks the clone's lifetime (the
|
|
24
|
+
// content controller signals removal via phlex-kit:sheet-content:closed).
|
|
25
|
+
this.invoker = this.element.querySelector(".pk-sheet-trigger button, .pk-sheet-trigger a, .pk-sheet-trigger [tabindex]")
|
|
26
|
+
this.invoker?.setAttribute("aria-haspopup", "dialog")
|
|
27
|
+
this.invoker?.setAttribute("aria-expanded", "false")
|
|
28
|
+
this.onOverlayClosed = () => {
|
|
29
|
+
if (!this.overlay?.isConnected) this.invoker?.setAttribute("aria-expanded", "false")
|
|
30
|
+
}
|
|
31
|
+
document.addEventListener("phlex-kit:sheet-content:closed", this.onOverlayClosed)
|
|
22
32
|
if (this.openValue) this.open()
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
disconnect() {
|
|
26
36
|
document.removeEventListener("turbo:before-cache", this.clearOverlay)
|
|
37
|
+
document.removeEventListener("phlex-kit:sheet-content:closed", this.onOverlayClosed)
|
|
27
38
|
this.clearOverlay()
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
open() {
|
|
31
42
|
if (this.overlay?.isConnected) return
|
|
43
|
+
// open: is one-shot — the reflected value sits in the Turbo snapshot, so
|
|
44
|
+
// a cache-restored reconnect would re-open a dismissed sheet.
|
|
45
|
+
this.openValue = false
|
|
32
46
|
document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML)
|
|
33
47
|
this.overlay = document.body.lastElementChild
|
|
48
|
+
this.invoker?.setAttribute("aria-expanded", "true")
|
|
34
49
|
}
|
|
35
50
|
}
|
|
@@ -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
|
|
|
@@ -11,8 +11,16 @@ module PhlexKit
|
|
|
11
11
|
# untouched — keep the label in a real element there. Attrs pass through via
|
|
12
12
|
# mix. See sidebar.rb.
|
|
13
13
|
class SidebarMenuButton < BaseComponent
|
|
14
|
+
# `as:` is dispatched dynamically (send) — whitelist so it can't reach
|
|
15
|
+
# arbitrary (including private) methods; to_sym so a String "a" still
|
|
16
|
+
# earns aria-current when active.
|
|
17
|
+
AS_TAGS = %i[button a].freeze
|
|
18
|
+
|
|
14
19
|
def initialize(as: :button, active: false, tooltip: nil, **attrs)
|
|
15
|
-
@as = as
|
|
20
|
+
@as = as.to_sym
|
|
21
|
+
unless AS_TAGS.include?(@as)
|
|
22
|
+
raise ArgumentError, "unknown SidebarMenuButton as: #{@as.inspect} (use one of #{AS_TAGS.join(", ")})"
|
|
23
|
+
end
|
|
16
24
|
@active = active
|
|
17
25
|
@tooltip = tooltip
|
|
18
26
|
@attrs = attrs
|
|
@@ -5,8 +5,15 @@ module PhlexKit
|
|
|
5
5
|
# primary). Kwarg rather than attr for the same mix-merge reason as
|
|
6
6
|
# SidebarMenuButton. See sidebar.rb.
|
|
7
7
|
class SidebarMenuSubButton < BaseComponent
|
|
8
|
+
# `as:` is dispatched dynamically (send) — whitelist, matching
|
|
9
|
+
# SidebarMenuButton.
|
|
10
|
+
AS_TAGS = %i[a button].freeze
|
|
11
|
+
|
|
8
12
|
def initialize(as: :a, active: false, **attrs)
|
|
9
|
-
@as = as
|
|
13
|
+
@as = as.to_sym
|
|
14
|
+
unless AS_TAGS.include?(@as)
|
|
15
|
+
raise ArgumentError, "unknown SidebarMenuSubButton as: #{@as.inspect} (use one of #{AS_TAGS.join(", ")})"
|
|
16
|
+
end
|
|
10
17
|
@active = active
|
|
11
18
|
@attrs = attrs
|
|
12
19
|
end
|
|
@@ -18,6 +18,15 @@
|
|
|
18
18
|
var(--pk-brand) var(--pk-slider-progress, 50%),
|
|
19
19
|
var(--pk-surface-2) var(--pk-slider-progress, 50%));
|
|
20
20
|
}
|
|
21
|
+
/* Chrome mirrors the native range input in RTL but the gradient stays
|
|
22
|
+
physical — repaint the fill from the right. (:dir() reads document
|
|
23
|
+
directionality, so .vertical's internal `direction: rtl` flip below
|
|
24
|
+
doesn't match; its own `to top` gradient also comes later and wins.) */
|
|
25
|
+
.pk-slider:dir(rtl)::-webkit-slider-runnable-track {
|
|
26
|
+
background: linear-gradient(to left,
|
|
27
|
+
var(--pk-brand) var(--pk-slider-progress, 50%),
|
|
28
|
+
var(--pk-surface-2) var(--pk-slider-progress, 50%));
|
|
29
|
+
}
|
|
21
30
|
.pk-slider::-webkit-slider-thumb {
|
|
22
31
|
-webkit-appearance: none;
|
|
23
32
|
width: .75rem;
|
|
@@ -57,6 +66,12 @@
|
|
|
57
66
|
.pk-slider:active::-moz-range-thumb {
|
|
58
67
|
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
59
68
|
}
|
|
69
|
+
/* line 13 strips the native outline in every browser — without this arm the
|
|
70
|
+
replacement ring only existed on the -webkit thumb and Firefox keyboard
|
|
71
|
+
focus was invisible. */
|
|
72
|
+
.pk-slider:focus-visible::-moz-range-thumb {
|
|
73
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
74
|
+
}
|
|
60
75
|
.pk-slider:disabled { opacity: .5; cursor: not-allowed; }
|
|
61
76
|
/* Vertical: modern writing-mode flip (min-h-40 like theirs). */
|
|
62
77
|
.pk-slider.vertical {
|
|
@@ -49,6 +49,11 @@
|
|
|
49
49
|
transform: translateX(calc(100% - 2px));
|
|
50
50
|
background: var(--pk-brand-ink);
|
|
51
51
|
}
|
|
52
|
+
/* RTL: the resting thumb sits at the physical right — checked travel must
|
|
53
|
+
slide it left or it leaves the track. */
|
|
54
|
+
.pk-switch:dir(rtl):has(.pk-switch-input:checked) .pk-switch-thumb {
|
|
55
|
+
transform: translateX(calc(-100% + 2px));
|
|
56
|
+
}
|
|
52
57
|
:root[data-theme="light"] .pk-switch { background: var(--pk-input); }
|
|
53
58
|
:root[data-theme="light"] .pk-switch:has(.pk-switch-input:checked) { background: var(--pk-brand); }
|
|
54
59
|
:root[data-theme="light"] .pk-switch-thumb { background: var(--pk-bg) !important; }
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
.pk-table-wrapper { position: relative; width: 100%; overflow: auto; }
|
|
6
6
|
.pk-table { width: 100%; border-collapse: collapse; caption-side: bottom; font-size: .875rem; }
|
|
7
7
|
|
|
8
|
-
.pk-table-header .pk-table-row { border-bottom: 1px solid var(--pk-border); }
|
|
9
8
|
.pk-table-body .pk-table-row:last-child { border-bottom: 0; }
|
|
10
9
|
.pk-table-footer { border-top: 1px solid var(--pk-border); font-weight: 500; background: color-mix(in oklab, var(--pk-surface-2) 50%, transparent); }
|
|
10
|
+
/* shadcn parity: the totals row doesn't draw a stray line under the table. */
|
|
11
|
+
.pk-table-footer .pk-table-row:last-child { border-bottom: 0; }
|
|
11
12
|
|
|
12
13
|
.pk-table-row { border-bottom: 1px solid var(--pk-border); transition: background-color .12s ease; }
|
|
13
14
|
.pk-table-row:hover { background: color-mix(in oklab, var(--pk-surface-2) 50%, transparent); }
|
|
@@ -22,12 +22,15 @@ module PhlexKit
|
|
|
22
22
|
#
|
|
23
23
|
# Tailwind → vanilla `.pk-table-*` (table.css), palette from the global tokens.
|
|
24
24
|
class Table < BaseComponent
|
|
25
|
-
def initialize(**attrs)
|
|
25
|
+
def initialize(wrapper: {}, **attrs)
|
|
26
|
+
@wrapper = wrapper
|
|
26
27
|
@attrs = attrs
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
def view_template(&block)
|
|
30
|
-
|
|
31
|
+
# Caller attrs land on the <table>; wrapper: reaches the scroll
|
|
32
|
+
# container (e.g. to cap its height) — same escape hatch as Switch.
|
|
33
|
+
div(**mix({ class: "pk-table-wrapper" }, @wrapper)) do
|
|
31
34
|
table(**mix({ class: "pk-table" }, @attrs), &block)
|
|
32
35
|
end
|
|
33
36
|
end
|
|
@@ -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;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
class TabsContent < BaseComponent
|
|
3
|
-
|
|
3
|
+
# id: is a named kwarg (not left in **attrs) — see TabsTrigger.
|
|
4
|
+
def initialize(value:, active: false, id: nil, **attrs)
|
|
4
5
|
@value = value
|
|
5
6
|
@active = active
|
|
7
|
+
@id = id || "pk-tabs-panel-#{value}"
|
|
6
8
|
@attrs = attrs
|
|
7
9
|
end
|
|
8
10
|
def view_template(&)
|
|
@@ -11,7 +13,7 @@ module PhlexKit
|
|
|
11
13
|
# focusable, per the APG tabs pattern. `active:` (mirroring TabsTrigger's)
|
|
12
14
|
# renders the panel visible pre-JS/no-JS; the controller's sync()
|
|
13
15
|
# reconciles visibility from the active value after connect.
|
|
14
|
-
div(**mix({ class: @active ? "pk-tabs-content" : "pk-tabs-content pk-hidden", role: "tabpanel", id:
|
|
16
|
+
div(**mix({ class: @active ? "pk-tabs-content" : "pk-tabs-content pk-hidden", role: "tabpanel", id: @id, aria_labelledby: "pk-tabs-trigger-#{@value}", tabindex: "0", data: { phlex_kit__tabs_target: "content", value: @value } }, @attrs), &)
|
|
15
17
|
end
|
|
16
18
|
end
|
|
17
19
|
end
|
|
@@ -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
|
}
|
|
@@ -2,14 +2,19 @@ module PhlexKit
|
|
|
2
2
|
class TabsList < BaseComponent
|
|
3
3
|
VARIANTS = { default: nil, line: "line" }.freeze
|
|
4
4
|
|
|
5
|
-
def initialize(variant: :default, **attrs)
|
|
5
|
+
def initialize(variant: :default, orientation: :horizontal, **attrs)
|
|
6
6
|
@variant = variant.to_sym
|
|
7
|
+
@orientation = orientation.to_sym
|
|
7
8
|
@attrs = attrs
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
def view_template(&)
|
|
11
12
|
classes = [ "pk-tabs-list", fetch_option(VARIANTS, @variant, :variant) ].compact.join(" ")
|
|
12
|
-
|
|
13
|
+
base = { class: classes, role: "tablist", data: { phlex_kit__tabs_target: "list", action: "keydown->phlex-kit--tabs#keydown" } }
|
|
14
|
+
# Server-rendered so AT hears the right axis pre-JS / with JS off; the
|
|
15
|
+
# controller re-stamps it on connect from the root's vertical class.
|
|
16
|
+
base[:aria] = { orientation: "vertical" } if @orientation == :vertical
|
|
17
|
+
div(**mix(base, @attrs), &)
|
|
13
18
|
end
|
|
14
19
|
end
|
|
15
20
|
end
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
class TabsTrigger < BaseComponent
|
|
3
|
-
|
|
3
|
+
# id: is a named kwarg (not left in **attrs) because `mix` would *merge* a
|
|
4
|
+
# caller id with the deterministic one into an invalid two-token id,
|
|
5
|
+
# breaking the aria pairing pre-JS (and scopeIds' caller-id detection).
|
|
6
|
+
AS_TAGS = %i[button a].freeze
|
|
7
|
+
|
|
8
|
+
def initialize(value:, as: :button, active: false, id: nil, **attrs)
|
|
4
9
|
@value = value
|
|
5
10
|
@as = as.to_sym
|
|
11
|
+
unless AS_TAGS.include?(@as)
|
|
12
|
+
raise ArgumentError, "unknown TabsTrigger as: #{@as.inspect} (use one of #{AS_TAGS.join(", ")})"
|
|
13
|
+
end
|
|
6
14
|
@active = active
|
|
15
|
+
@id = id || "pk-tabs-trigger-#{value}"
|
|
7
16
|
@attrs = attrs
|
|
8
17
|
end
|
|
9
18
|
def view_template(&)
|
|
@@ -13,11 +22,14 @@ module PhlexKit
|
|
|
13
22
|
# aria-selected and the roving tabindex are kept in sync by the
|
|
14
23
|
# controller's activeValueChanged.
|
|
15
24
|
base = {
|
|
16
|
-
class: "pk-tabs-trigger", role: "tab", id:
|
|
25
|
+
class: "pk-tabs-trigger", role: "tab", id: @id,
|
|
17
26
|
aria_selected: @active ? "true" : "false",
|
|
18
27
|
aria_controls: "pk-tabs-panel-#{@value}",
|
|
19
28
|
tabindex: @active ? "0" : "-1",
|
|
20
|
-
data
|
|
29
|
+
# data-state is the only hook tabs.css styles the active trigger by —
|
|
30
|
+
# it must be server-rendered or the active tab looks inactive pre-JS.
|
|
31
|
+
data: { phlex_kit__tabs_target: "trigger", action: "click->phlex-kit--tabs#show", value: @value,
|
|
32
|
+
state: @active ? "active" : "inactive" }
|
|
21
33
|
}
|
|
22
34
|
if @as == :a
|
|
23
35
|
a(**mix(base, @attrs), &)
|
|
@@ -18,11 +18,25 @@ export default class extends Controller {
|
|
|
18
18
|
if (!this.storedTheme()) this.applyTheme("system")
|
|
19
19
|
}
|
|
20
20
|
this._media.addEventListener("change", this._onMediaChange)
|
|
21
|
+
// Multiple toggles on one page (header + mobile menu): each instance
|
|
22
|
+
// observes :root[data-theme] so a change applied by ANY of them (or by
|
|
23
|
+
// host code) re-syncs every toggle's pressed state — without this the
|
|
24
|
+
// stale one showed the wrong state and its first click was eaten by the
|
|
25
|
+
// echo guard in apply().
|
|
26
|
+
this._rootObserver = new MutationObserver(() => this.syncPressed())
|
|
27
|
+
this._rootObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] })
|
|
21
28
|
this.applyTheme(this.storedTheme() || "system")
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
disconnect() {
|
|
25
32
|
this._media?.removeEventListener("change", this._onMediaChange)
|
|
33
|
+
this._rootObserver?.disconnect()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
syncPressed() {
|
|
37
|
+
const theme = document.documentElement.getAttribute("data-theme") || "system"
|
|
38
|
+
const dark = theme === "system" ? this._media.matches : theme === "dark"
|
|
39
|
+
this.element.setAttribute("data-phlex-kit--toggle-pressed-value", dark ? "true" : "false")
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
apply(e) {
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
.pk-toast-region {
|
|
8
8
|
position: fixed;
|
|
9
9
|
z-index: 100;
|
|
10
|
-
|
|
10
|
+
/* --pk-toast-offset is stamped inline by ToastRegion(offset:). */
|
|
11
|
+
padding: var(--pk-toast-offset, 1.5rem);
|
|
11
12
|
pointer-events: none;
|
|
12
13
|
}
|
|
13
14
|
.pk-toast-region[data-position="top-left"] { top: 0; left: 0; }
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
width: 356px;
|
|
41
42
|
max-width: 100%;
|
|
42
43
|
border: 1px solid var(--pk-border);
|
|
43
|
-
border-radius: 1rem
|
|
44
|
+
border-radius: calc(var(--pk-radius) + 6px); /* 1rem at the default 0.625rem token */
|
|
44
45
|
background: var(--pk-surface);
|
|
45
46
|
color: var(--pk-text);
|
|
46
47
|
padding: 1rem;
|
|
@@ -12,7 +12,10 @@ module PhlexKit
|
|
|
12
12
|
|
|
13
13
|
def view_template
|
|
14
14
|
data = { slot: "action" }
|
|
15
|
-
|
|
15
|
+
# Caller action first, then dismiss — client-spawned actions dismiss
|
|
16
|
+
# their toast after onClick (Sonner parity), so server-rendered ones
|
|
17
|
+
# must too.
|
|
18
|
+
data[:action] = [ @on, "click->phlex-kit--toast#dismiss" ].compact.join(" ") if @on
|
|
16
19
|
button(**mix({ type: :button, class: "pk-toast-action", data: data }, @attrs)) { @label }
|
|
17
20
|
end
|
|
18
21
|
end
|
|
@@ -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
|
|
@@ -45,8 +46,11 @@ export default class extends Controller {
|
|
|
45
46
|
this.element.addEventListener("keydown", this._onKeyDown)
|
|
46
47
|
this.element.addEventListener("phlex-kit:toast:force-dismiss", this._onForceDismiss)
|
|
47
48
|
this.element.addEventListener("phlex-kit:toast:restart", this._onRestart)
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
// Region-scoped: the toaster dispatches pause/resume on its own list, so
|
|
50
|
+
// another region's hover can't freeze (or resume) this toast's timer.
|
|
51
|
+
this._regionList = this.element.closest(".pk-toast-list") || document
|
|
52
|
+
this._regionList.addEventListener("phlex-kit:toast:pause", this._onRegionPause)
|
|
53
|
+
this._regionList.addEventListener("phlex-kit:toast:resume", this._onRegionResume)
|
|
50
54
|
|
|
51
55
|
requestAnimationFrame(() => {
|
|
52
56
|
this.element.dataset.state = "open"
|
|
@@ -66,6 +70,10 @@ export default class extends Controller {
|
|
|
66
70
|
|
|
67
71
|
disconnect() {
|
|
68
72
|
this._clearTimer()
|
|
73
|
+
// Don't let a pending 200ms unmount fire after teardown (e.g. into a
|
|
74
|
+
// Turbo-cached copy of the page).
|
|
75
|
+
if (this._unmountTimer) clearTimeout(this._unmountTimer)
|
|
76
|
+
this._unmountTimer = null
|
|
69
77
|
this.element.removeEventListener("pointerdown", this._onPointerDown)
|
|
70
78
|
this.element.removeEventListener("pointerenter", this._onPointerEnter)
|
|
71
79
|
this.element.removeEventListener("pointerleave", this._onPointerLeave)
|
|
@@ -74,8 +82,8 @@ export default class extends Controller {
|
|
|
74
82
|
this.element.removeEventListener("keydown", this._onKeyDown)
|
|
75
83
|
this.element.removeEventListener("phlex-kit:toast:force-dismiss", this._onForceDismiss)
|
|
76
84
|
this.element.removeEventListener("phlex-kit:toast:restart", this._onRestart)
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
this._regionList?.removeEventListener("phlex-kit:toast:pause", this._onRegionPause)
|
|
86
|
+
this._regionList?.removeEventListener("phlex-kit:toast:resume", this._onRegionResume)
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
dismiss(e) {
|
|
@@ -89,7 +97,7 @@ export default class extends Controller {
|
|
|
89
97
|
this.element.dataset.state = "closing"
|
|
90
98
|
this.element.dispatchEvent(new CustomEvent(reason === "auto" ? "phlex-kit:toast:auto-close" : "phlex-kit:toast:dismiss", { bubbles: true, detail: { id: this.element.id } }))
|
|
91
99
|
this._invokeCallback(reason === "auto" ? this.onAutoCloseValue : this.onDismissValue)
|
|
92
|
-
setTimeout(() => this.element.remove(), TIME_BEFORE_UNMOUNT)
|
|
100
|
+
this._unmountTimer = setTimeout(() => this.element.remove(), TIME_BEFORE_UNMOUNT)
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
// on_dismiss:/on_auto_close: name a global function (Sonner-style callback
|