phlex_kit 0.5.1 → 0.6.1
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/README.md +21 -8
- data/app/components/phlex_kit/accordion/accordion.css +2 -0
- data/app/components/phlex_kit/accordion/accordion.rb +1 -1
- data/app/components/phlex_kit/accordion/accordion_content.rb +3 -1
- data/app/components/phlex_kit/accordion/accordion_controller.js +26 -2
- data/app/components/phlex_kit/accordion/accordion_default_trigger.rb +20 -4
- data/app/components/phlex_kit/accordion/accordion_trigger.rb +18 -2
- data/app/components/phlex_kit/alert/alert.rb +1 -1
- data/app/components/phlex_kit/alert_dialog/alert_dialog_cancel.rb +1 -1
- data/app/components/phlex_kit/alert_dialog/alert_dialog_content.rb +1 -1
- data/app/components/phlex_kit/attachment/attachment.rb +2 -2
- data/app/components/phlex_kit/attachment/attachment_media.rb +1 -1
- data/app/components/phlex_kit/avatar/avatar.rb +1 -1
- data/app/components/phlex_kit/badge/badge.rb +1 -1
- data/app/components/phlex_kit/button/button.rb +1 -1
- data/app/components/phlex_kit/button_group/button_group.rb +1 -1
- data/app/components/phlex_kit/calendar/calendar_controller.js +12 -8
- data/app/components/phlex_kit/card/card.rb +1 -1
- data/app/components/phlex_kit/carousel/carousel.rb +1 -1
- data/app/components/phlex_kit/clipboard/clipboard_popover.rb +1 -1
- data/app/components/phlex_kit/combobox/combobox.css +21 -12
- data/app/components/phlex_kit/combobox/combobox.rb +19 -3
- data/app/components/phlex_kit/combobox/combobox_badge_trigger.rb +7 -2
- data/app/components/phlex_kit/combobox/combobox_checkbox.rb +3 -0
- data/app/components/phlex_kit/combobox/combobox_controller.js +85 -20
- data/app/components/phlex_kit/combobox/combobox_input_trigger.rb +11 -2
- data/app/components/phlex_kit/combobox/combobox_item.rb +4 -1
- data/app/components/phlex_kit/combobox/combobox_list.rb +9 -2
- data/app/components/phlex_kit/combobox/combobox_popover.rb +8 -7
- data/app/components/phlex_kit/combobox/combobox_radio.rb +3 -0
- data/app/components/phlex_kit/combobox/combobox_search_input.rb +8 -3
- data/app/components/phlex_kit/command/command.rb +17 -2
- data/app/components/phlex_kit/command/command_controller.js +93 -3
- data/app/components/phlex_kit/command/command_dialog_content.rb +16 -4
- data/app/components/phlex_kit/command/command_input.rb +7 -2
- data/app/components/phlex_kit/command/command_item.rb +12 -3
- data/app/components/phlex_kit/command/command_list.rb +9 -2
- data/app/components/phlex_kit/context_menu/context_menu.css +25 -13
- data/app/components/phlex_kit/context_menu/context_menu_content.rb +1 -1
- data/app/components/phlex_kit/context_menu/context_menu_controller.js +71 -12
- data/app/components/phlex_kit/context_menu/context_menu_item.rb +7 -3
- data/app/components/phlex_kit/context_menu/context_menu_label.rb +1 -1
- data/app/components/phlex_kit/context_menu/context_menu_trigger.rb +3 -1
- data/app/components/phlex_kit/data_table/data_table_column_visibility_controller.js +1 -1
- data/app/components/phlex_kit/data_table/data_table_controller.js +12 -1
- data/app/components/phlex_kit/data_table/data_table_pagination.rb +12 -10
- data/app/components/phlex_kit/data_table/data_table_selection_summary.rb +1 -0
- data/app/components/phlex_kit/data_table/data_table_sort_head.rb +5 -1
- data/app/components/phlex_kit/dialog/dialog_content.rb +12 -4
- data/app/components/phlex_kit/dialog/dialog_controller.js +13 -0
- data/app/components/phlex_kit/drawer/drawer_content.rb +2 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu.css +36 -15
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_content.rb +4 -3
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_controller.js +8 -11
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_item.rb +1 -1
- data/app/components/phlex_kit/empty/empty_media.rb +1 -1
- data/app/components/phlex_kit/field/field.rb +1 -1
- data/app/components/phlex_kit/field/field_legend.rb +1 -1
- data/app/components/phlex_kit/hover_card/hover_card.css +27 -8
- data/app/components/phlex_kit/hover_card/hover_card.rb +2 -1
- data/app/components/phlex_kit/hover_card/hover_card_content.rb +2 -2
- data/app/components/phlex_kit/hover_card/hover_card_controller.js +19 -5
- data/app/components/phlex_kit/input_group/input_group_addon.rb +1 -1
- data/app/components/phlex_kit/input_otp/input_otp.rb +4 -1
- data/app/components/phlex_kit/input_otp/input_otp_controller.js +12 -1
- data/app/components/phlex_kit/item/item.rb +1 -1
- data/app/components/phlex_kit/item/item_media.rb +1 -1
- data/app/components/phlex_kit/link/link.rb +1 -1
- data/app/components/phlex_kit/marker/marker.rb +1 -1
- data/app/components/phlex_kit/menubar/menubar.css +36 -14
- data/app/components/phlex_kit/menubar/menubar.rb +1 -1
- data/app/components/phlex_kit/menubar/menubar_checkbox_item.rb +5 -2
- data/app/components/phlex_kit/menubar/menubar_content.rb +1 -1
- data/app/components/phlex_kit/menubar/menubar_controller.js +100 -12
- data/app/components/phlex_kit/menubar/menubar_item.rb +8 -5
- data/app/components/phlex_kit/menubar/menubar_radio_item.rb +5 -2
- data/app/components/phlex_kit/menubar/menubar_trigger.rb +1 -0
- data/app/components/phlex_kit/native_select/native_select.rb +1 -1
- data/app/components/phlex_kit/navigation_menu/navigation_menu.css +24 -7
- data/app/components/phlex_kit/navigation_menu/navigation_menu_content.rb +3 -1
- data/app/components/phlex_kit/popover/popover.css +24 -9
- data/app/components/phlex_kit/popover/popover.rb +2 -1
- data/app/components/phlex_kit/popover/popover_content.rb +6 -4
- data/app/components/phlex_kit/popover/popover_controller.js +12 -8
- data/app/components/phlex_kit/popover/popover_trigger.rb +1 -1
- data/app/components/phlex_kit/resizable/resizable_panel.rb +4 -2
- data/app/components/phlex_kit/select/select.css +24 -12
- data/app/components/phlex_kit/select/select_content.rb +6 -4
- data/app/components/phlex_kit/select/select_controller.js +21 -17
- data/app/components/phlex_kit/select/select_trigger.rb +1 -1
- data/app/components/phlex_kit/sheet/sheet_content.rb +2 -2
- data/app/components/phlex_kit/sheet/sheet_content_controller.js +68 -2
- data/app/components/phlex_kit/sheet/sheet_controller.js +28 -3
- data/app/components/phlex_kit/sidebar/sidebar.rb +1 -1
- data/app/components/phlex_kit/sidebar/sidebar_wrapper.rb +1 -1
- data/app/components/phlex_kit/spinner/spinner.rb +1 -1
- data/app/components/phlex_kit/switch/switch.rb +7 -4
- data/app/components/phlex_kit/tabs/tabs.rb +1 -1
- data/app/components/phlex_kit/tabs/tabs_content.rb +4 -1
- data/app/components/phlex_kit/tabs/tabs_controller.js +60 -7
- data/app/components/phlex_kit/tabs/tabs_list.rb +2 -2
- data/app/components/phlex_kit/tabs/tabs_trigger.rb +14 -2
- data/app/components/phlex_kit/theme_toggle/theme_toggle.rb +11 -3
- data/app/components/phlex_kit/toast/toast_controller.js +17 -0
- data/app/components/phlex_kit/toast/toast_icon.rb +1 -1
- data/app/components/phlex_kit/toast/toast_item.rb +1 -1
- data/app/components/phlex_kit/toggle/toggle.rb +2 -2
- data/app/components/phlex_kit/toggle_group/toggle_group.rb +2 -2
- data/app/components/phlex_kit/toggle_group/toggle_group_item.rb +1 -1
- data/app/components/phlex_kit/tooltip/tooltip.css +40 -22
- data/app/components/phlex_kit/tooltip/tooltip.rb +3 -1
- data/app/components/phlex_kit/tooltip/tooltip_content.rb +1 -1
- data/app/components/phlex_kit/tooltip/tooltip_controller.js +32 -0
- data/app/components/phlex_kit/tooltip/tooltip_trigger.rb +8 -3
- data/app/components/phlex_kit/typography/text.rb +1 -1
- data/app/javascript/phlex_kit/controllers/index.js +2 -0
- data/lib/generators/phlex_kit/component/component_generator.rb +28 -0
- data/lib/generators/phlex_kit/install/templates/phlex_kit.rb +3 -0
- data/lib/phlex_kit/base_component.rb +12 -0
- data/lib/phlex_kit/engine.rb +11 -1
- data/lib/phlex_kit/icons/lucide.rb +1 -1
- data/lib/phlex_kit/icons/phosphor.rb +1 -1
- data/lib/phlex_kit/icons/remix.rb +1 -1
- data/lib/phlex_kit/icons/tabler.rb +1 -1
- data/lib/phlex_kit/version.rb +1 -1
- data/lib/phlex_kit.rb +5 -0
- metadata +2 -1
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
/* Co-located with context_menu.rb.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
.
|
|
1
|
+
/* Co-located with context_menu.rb. The panel is a native [popover=manual] in
|
|
2
|
+
the top layer, placed at the cursor in viewport coordinates and clamped by
|
|
3
|
+
the controller (a cursor point isn't an element, so it can't use anchor
|
|
4
|
+
positioning). Submenus DO anchor to their row and flip at the viewport
|
|
5
|
+
right edge. Matches the dropdown-menu grammar (shadcn's context menu
|
|
6
|
+
shares the same part set): gap-based rows, destructive variant,
|
|
7
|
+
hidden-input checkbox/radio rows with :has() indicators, pure-CSS hover
|
|
8
|
+
submenus. Tokens global. */
|
|
6
9
|
.pk-context-menu-content {
|
|
7
|
-
position:
|
|
10
|
+
position: fixed; inset: auto; margin: 0; min-width: 8rem; overflow: visible;
|
|
8
11
|
border: 1px solid var(--pk-border); border-radius: calc(var(--pk-radius) - 2px); background: var(--pk-surface);
|
|
9
12
|
color: var(--pk-text); padding: .25rem; box-shadow: 0 8px 24px rgb(0 0 0 / .3); outline: none;
|
|
10
13
|
}
|
|
@@ -64,16 +67,22 @@
|
|
|
64
67
|
box-shadow: 0 0 0 2px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
65
68
|
}
|
|
66
69
|
|
|
67
|
-
/* Submenu: trigger row + side panel, revealed on hover/focus — pure CSS.
|
|
68
|
-
|
|
70
|
+
/* Submenu: trigger row + side panel, revealed on hover/focus — pure CSS.
|
|
71
|
+
The row is its own scoped anchor; the panel flips to the other side at
|
|
72
|
+
the viewport right edge. */
|
|
73
|
+
.pk-context-menu-sub { anchor-name: --pk-context-menu-sub; anchor-scope: --pk-context-menu-sub; }
|
|
69
74
|
.pk-context-menu-sub-chevron { margin-left: auto; width: 1rem; height: 1rem; }
|
|
70
75
|
.pk-context-menu-sub-content {
|
|
71
|
-
position:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
position: fixed;
|
|
77
|
+
position-anchor: --pk-context-menu-sub;
|
|
78
|
+
inset: auto;
|
|
79
|
+
position-area: inline-end span-block-end;
|
|
80
|
+
margin-top: -0.25rem;
|
|
81
|
+
position-try-fallbacks: flip-inline;
|
|
75
82
|
min-width: 10rem;
|
|
76
|
-
|
|
83
|
+
/* hover bridge over the gap — symmetric because flip tactics flip
|
|
84
|
+
margins/insets but not padding, so the flipped side needs one too */
|
|
85
|
+
padding: 0 .25rem;
|
|
77
86
|
display: none;
|
|
78
87
|
}
|
|
79
88
|
.pk-context-menu-sub-viewport {
|
|
@@ -89,3 +98,6 @@
|
|
|
89
98
|
.pk-context-menu-sub:focus-within > .pk-context-menu-sub-content { display: block; }
|
|
90
99
|
.pk-context-menu-sub:hover > .pk-context-menu-sub-trigger,
|
|
91
100
|
.pk-context-menu-sub:focus-within > .pk-context-menu-sub-trigger { background: var(--pk-accent); }
|
|
101
|
+
/* Hiding must survive author display values (a host class like
|
|
102
|
+
display:grid would beat the UA [popover] { display: none }). */
|
|
103
|
+
.pk-context-menu-content:not(:popover-open) { display: none; }
|
|
@@ -2,7 +2,7 @@ module PhlexKit
|
|
|
2
2
|
class ContextMenuContent < BaseComponent
|
|
3
3
|
def initialize(**attrs) = (@attrs = attrs)
|
|
4
4
|
def view_template(&)
|
|
5
|
-
div(**mix({ class: "pk-context-menu-content
|
|
5
|
+
div(**mix({ class: "pk-context-menu-content", popover: "manual", role: "menu", tabindex: "-1", aria: { orientation: "vertical" }, data: { phlex_kit__context_menu_target: "content", state: "closed" } }, @attrs), &)
|
|
6
6
|
end
|
|
7
7
|
end
|
|
8
8
|
end
|
|
@@ -1,28 +1,87 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
|
|
3
|
-
// Connects to data-controller="phlex-kit--context-menu". Opens at the cursor
|
|
4
|
-
// contextmenu
|
|
3
|
+
// Connects to data-controller="phlex-kit--context-menu". Opens at the cursor
|
|
4
|
+
// on contextmenu (at the trigger's rect for the keyboard Menu key / Shift+F10,
|
|
5
|
+
// which fire with clientX/Y of 0) and focuses the first item; ArrowDown/
|
|
6
|
+
// ArrowUp/Home/End rove over the items (skipping [data-disabled]); closes on
|
|
7
|
+
// outside click or Escape, returning focus to the trigger. The panel is a
|
|
8
|
+
// native [popover=manual] in the top layer (context_menu.css); it is placed
|
|
9
|
+
// in viewport coordinates after showPopover() — a hidden popover has no
|
|
10
|
+
// size — and clamped so it never overflows the viewport (no floating-ui).
|
|
5
11
|
export default class extends Controller {
|
|
6
|
-
static targets = ["content"]
|
|
12
|
+
static targets = ["content", "trigger", "menuItem"]
|
|
13
|
+
|
|
7
14
|
connect() {
|
|
8
15
|
this.onDoc = (e) => { if (!this.contentTarget.contains(e.target)) this.close() }
|
|
9
|
-
this.onKey = (e) =>
|
|
16
|
+
this.onKey = (e) => this.keydown(e)
|
|
10
17
|
}
|
|
11
|
-
|
|
18
|
+
|
|
19
|
+
disconnect() {
|
|
20
|
+
document.removeEventListener("click", this.onDoc)
|
|
21
|
+
document.removeEventListener("keydown", this.onKey)
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
open(e) {
|
|
13
25
|
e.preventDefault()
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
let x = e.clientX
|
|
27
|
+
let y = e.clientY
|
|
28
|
+
if (x === 0 && y === 0) {
|
|
29
|
+
const t = this.anchor().getBoundingClientRect()
|
|
30
|
+
x = t.left
|
|
31
|
+
y = t.bottom
|
|
32
|
+
}
|
|
33
|
+
const c = this.contentTarget
|
|
34
|
+
if (!c.matches(":popover-open")) c.showPopover()
|
|
35
|
+
x = Math.max(4, Math.min(x, window.innerWidth - c.offsetWidth - 4))
|
|
36
|
+
y = Math.max(4, Math.min(y, window.innerHeight - c.offsetHeight - 4))
|
|
37
|
+
c.style.left = `${x}px`
|
|
38
|
+
c.style.top = `${y}px`
|
|
39
|
+
c.dataset.state = "open"
|
|
19
40
|
document.addEventListener("click", this.onDoc)
|
|
20
41
|
document.addEventListener("keydown", this.onKey)
|
|
42
|
+
this.items()[0]?.focus()
|
|
21
43
|
}
|
|
22
|
-
|
|
23
|
-
|
|
44
|
+
|
|
45
|
+
close(opts = {}) {
|
|
46
|
+
if (this.contentTarget.matches(":popover-open")) this.contentTarget.hidePopover()
|
|
24
47
|
this.contentTarget.dataset.state = "closed"
|
|
25
48
|
document.removeEventListener("click", this.onDoc)
|
|
26
49
|
document.removeEventListener("keydown", this.onKey)
|
|
50
|
+
if (opts.refocus === true) this.anchor().focus()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
keydown(e) {
|
|
54
|
+
const items = this.items()
|
|
55
|
+
const index = items.indexOf(document.activeElement)
|
|
56
|
+
switch (e.key) {
|
|
57
|
+
case "Escape":
|
|
58
|
+
e.preventDefault()
|
|
59
|
+
this.close({ refocus: true })
|
|
60
|
+
break
|
|
61
|
+
case "ArrowDown":
|
|
62
|
+
e.preventDefault()
|
|
63
|
+
items[(index + 1) % items.length]?.focus()
|
|
64
|
+
break
|
|
65
|
+
case "ArrowUp":
|
|
66
|
+
e.preventDefault()
|
|
67
|
+
items[index < 0 ? items.length - 1 : (index - 1 + items.length) % items.length]?.focus()
|
|
68
|
+
break
|
|
69
|
+
case "Home":
|
|
70
|
+
e.preventDefault()
|
|
71
|
+
items[0]?.focus()
|
|
72
|
+
break
|
|
73
|
+
case "End":
|
|
74
|
+
e.preventDefault()
|
|
75
|
+
items[items.length - 1]?.focus()
|
|
76
|
+
break
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
anchor() {
|
|
81
|
+
return this.hasTriggerTarget ? this.triggerTarget : this.element
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
items() {
|
|
85
|
+
return this.menuItemTargets.filter((el) => !el.closest("[data-disabled]") && !el.closest(".pk-hidden"))
|
|
27
86
|
}
|
|
28
87
|
}
|
|
@@ -17,9 +17,13 @@ module PhlexKit
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def view_template(&block)
|
|
20
|
-
classes = [ "pk-context-menu-item", VARIANTS
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
classes = [ "pk-context-menu-item", fetch_option(VARIANTS, @variant, :variant) ].compact.join(" ")
|
|
21
|
+
base = { href: @href, role: "menuitem", tabindex: "-1", class: classes,
|
|
22
|
+
data: { action: ("click->phlex-kit--context-menu#close" unless @disabled),
|
|
23
|
+
phlex_kit__context_menu_target: "menuItem",
|
|
24
|
+
disabled: (@disabled ? "true" : nil) }.compact }
|
|
25
|
+
base[:aria] = { disabled: "true" } if @disabled
|
|
26
|
+
a(**mix(base, @attrs)) do
|
|
23
27
|
if @checked
|
|
24
28
|
span(class: "pk-context-menu-check") do
|
|
25
29
|
render Icon.new(:check, size: nil)
|
|
@@ -5,7 +5,7 @@ module PhlexKit
|
|
|
5
5
|
@attrs = attrs
|
|
6
6
|
end
|
|
7
7
|
def view_template(&)
|
|
8
|
-
div(**mix({ class: ["pk-context-menu-label", (@inset ? "inset" : nil)].compact.join(" ") }, @attrs), &)
|
|
8
|
+
div(**mix({ class: [ "pk-context-menu-label", (@inset ? "inset" : nil) ].compact.join(" ") }, @attrs), &)
|
|
9
9
|
end
|
|
10
10
|
end
|
|
11
11
|
end
|
|
@@ -2,7 +2,9 @@ module PhlexKit
|
|
|
2
2
|
class ContextMenuTrigger < BaseComponent
|
|
3
3
|
def initialize(**attrs) = (@attrs = attrs)
|
|
4
4
|
def view_template(&)
|
|
5
|
-
|
|
5
|
+
# tabindex 0 so the keyboard context-menu key (Menu / Shift+F10) can
|
|
6
|
+
# reach it, and so Escape has somewhere to return focus to.
|
|
7
|
+
div(**mix({ class: "pk-context-menu-trigger", tabindex: "0", data: { phlex_kit__context_menu_target: "trigger", action: "contextmenu->phlex-kit--context-menu#open" } }, @attrs), &)
|
|
6
8
|
end
|
|
7
9
|
end
|
|
8
10
|
end
|
|
@@ -12,7 +12,7 @@ export default class extends Controller {
|
|
|
12
12
|
const root = this.element.closest('[data-controller~="phlex-kit--data-table"]');
|
|
13
13
|
if (!root) return;
|
|
14
14
|
root
|
|
15
|
-
.querySelectorAll(`[data-column="${key}"]`)
|
|
15
|
+
.querySelectorAll(`[data-column="${CSS.escape(key)}"]`)
|
|
16
16
|
.forEach((el) => el.classList.toggle("pk-hidden", !visible));
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -10,7 +10,6 @@ export default class extends Controller {
|
|
|
10
10
|
"selectAll",
|
|
11
11
|
"rowCheckbox",
|
|
12
12
|
"selectionSummary",
|
|
13
|
-
"selectionBar",
|
|
14
13
|
"bulkActions",
|
|
15
14
|
];
|
|
16
15
|
|
|
@@ -45,6 +44,18 @@ export default class extends Controller {
|
|
|
45
44
|
const total = this.rowCheckboxTargets.length;
|
|
46
45
|
const selected = this.rowCheckboxTargets.filter((cb) => cb.checked).length;
|
|
47
46
|
|
|
47
|
+
// table.css styles .pk-table-row[data-state="selected"] — set it so
|
|
48
|
+
// selected rows actually get the highlight.
|
|
49
|
+
this.rowCheckboxTargets.forEach((cb) => {
|
|
50
|
+
const row = cb.closest(".pk-table-row");
|
|
51
|
+
if (!row) return;
|
|
52
|
+
if (cb.checked) {
|
|
53
|
+
row.setAttribute("data-state", "selected");
|
|
54
|
+
} else {
|
|
55
|
+
row.removeAttribute("data-state");
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
48
59
|
if (this.hasSelectAllTarget) {
|
|
49
60
|
this.selectAllTarget.checked = total > 0 && selected === total;
|
|
50
61
|
this.selectAllTarget.indeterminate = selected > 0 && selected < total;
|
|
@@ -56,22 +56,24 @@ module PhlexKit
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def prev_item
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
span(class: "pk-data-table-page-disabled") { @prev_label }
|
|
62
|
-
end
|
|
63
|
-
else
|
|
64
|
-
render PaginationItem.new(href: page_href(current - 1)) { @prev_label }
|
|
65
|
-
end
|
|
59
|
+
edge_item(disabled: current <= 1, href: page_href(current - 1),
|
|
60
|
+
label: @prev_label, name: "Go to previous page")
|
|
66
61
|
end
|
|
67
62
|
|
|
68
63
|
def next_item
|
|
69
|
-
|
|
64
|
+
edge_item(disabled: current >= total, href: page_href(current + 1),
|
|
65
|
+
label: @next_label, name: "Go to next page")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The default labels are bare glyphs ("<"/">") — give AT a real name, and
|
|
69
|
+
# announce the disabled edge instead of rendering an anonymous span.
|
|
70
|
+
def edge_item(disabled:, href:, label:, name:)
|
|
71
|
+
if disabled
|
|
70
72
|
li do
|
|
71
|
-
span(class: "pk-data-table-page-disabled") {
|
|
73
|
+
span(class: "pk-data-table-page-disabled", aria: { disabled: "true", label: name }) { label }
|
|
72
74
|
end
|
|
73
75
|
else
|
|
74
|
-
render PaginationItem.new(href:
|
|
76
|
+
render PaginationItem.new(href: href, aria: { label: name }) { label }
|
|
75
77
|
end
|
|
76
78
|
end
|
|
77
79
|
|
|
@@ -10,6 +10,7 @@ module PhlexKit
|
|
|
10
10
|
def view_template
|
|
11
11
|
div(**mix({
|
|
12
12
|
class: "pk-data-table-selection-summary",
|
|
13
|
+
role: "status", # live region — selection changes are announced to AT
|
|
13
14
|
data: { phlex_kit__data_table_target: "selectionSummary" }
|
|
14
15
|
}, @attrs)) do
|
|
15
16
|
plain "0 of #{@total_on_page} row(s) selected."
|
|
@@ -15,8 +15,12 @@ module PhlexKit
|
|
|
15
15
|
@attrs = attrs
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
ARIA_SORT = { "asc" => "ascending", "desc" => "descending" }.freeze
|
|
19
|
+
|
|
18
20
|
def view_template
|
|
19
|
-
|
|
21
|
+
defaults = { class: "pk-data-table-sort-head" }
|
|
22
|
+
defaults[:aria] = { sort: ARIA_SORT.fetch(current_direction) } if current_direction
|
|
23
|
+
render TableHead.new(**mix(defaults, @attrs)) do
|
|
20
24
|
a(href: sort_href, class: "pk-data-table-sort-link") do
|
|
21
25
|
plain @label
|
|
22
26
|
sort_icon
|
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
# The <dialog> box. `size:` picks the width tier (md = shadcn's default
|
|
3
3
|
# max-w-sm); `show_close_button: false` drops the top-right ✕ (their
|
|
4
|
-
# showCloseButton).
|
|
4
|
+
# showCloseButton). `labelledby:`/`describedby:` render aria-labelledby /
|
|
5
|
+
# aria-describedby for callers who assign their own ids to DialogTitle /
|
|
6
|
+
# DialogDescription; when absent, the phlex-kit--dialog controller auto-wires
|
|
7
|
+
# them in connect(). See dialog.rb.
|
|
5
8
|
class DialogContent < BaseComponent
|
|
6
9
|
SIZES = { xs: "xs", sm: "sm", md: nil, lg: "lg", xl: "xl", full: "full" }.freeze
|
|
7
10
|
|
|
8
|
-
def initialize(size: :md, show_close_button: true, **attrs)
|
|
11
|
+
def initialize(size: :md, show_close_button: true, labelledby: nil, describedby: nil, **attrs)
|
|
9
12
|
@size = size.to_sym
|
|
10
13
|
@show_close_button = show_close_button
|
|
14
|
+
@labelledby = labelledby
|
|
15
|
+
@describedby = describedby
|
|
11
16
|
@attrs = attrs
|
|
12
17
|
end
|
|
13
18
|
|
|
14
19
|
def view_template(&block)
|
|
15
|
-
cls = [ "pk-dialog", SIZES
|
|
16
|
-
|
|
20
|
+
cls = [ "pk-dialog", fetch_option(SIZES, @size, :size) ].compact.join(" ")
|
|
21
|
+
aria = {}
|
|
22
|
+
aria[:labelledby] = @labelledby if @labelledby
|
|
23
|
+
aria[:describedby] = @describedby if @describedby
|
|
24
|
+
dialog(**mix({ class: cls, aria: aria, data: { phlex_kit__dialog_target: "dialog", action: "click->phlex-kit--dialog#backdropClick" } }, @attrs)) do
|
|
17
25
|
yield
|
|
18
26
|
if @show_close_button
|
|
19
27
|
button(type: "button", class: "pk-overlay-close", data: { action: "click->phlex-kit--dialog#dismiss" }) do
|
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
|
|
3
3
|
// Connects to data-controller="phlex-kit--dialog". Ported from ruby_ui.
|
|
4
|
+
// connect() auto-wires the dialog's accessible name: when the caller didn't
|
|
5
|
+
// pass labelledby:/describedby: to DialogContent, the title/description parts
|
|
6
|
+
// get generated ids and the <dialog> points at them.
|
|
4
7
|
export default class extends Controller {
|
|
5
8
|
static targets = ["dialog"]
|
|
6
9
|
static values = { open: { type: Boolean, default: false } }
|
|
7
10
|
|
|
8
11
|
connect() {
|
|
9
12
|
this.dialogTarget.addEventListener("close", this.handleClose)
|
|
13
|
+
this.#wireAria("aria-labelledby", ".pk-dialog-title")
|
|
14
|
+
this.#wireAria("aria-describedby", ".pk-dialog-description")
|
|
10
15
|
if (this.openValue) this.open()
|
|
11
16
|
}
|
|
17
|
+
|
|
18
|
+
#wireAria(attr, selector) {
|
|
19
|
+
if (this.dialogTarget.getAttribute(attr)) return
|
|
20
|
+
const el = this.dialogTarget.querySelector(selector)
|
|
21
|
+
if (!el) return
|
|
22
|
+
if (!el.id) el.id = `pk-dialog-${attr.slice(5)}-${Math.random().toString(36).slice(2, 10)}`
|
|
23
|
+
this.dialogTarget.setAttribute(attr, el.id)
|
|
24
|
+
}
|
|
12
25
|
disconnect() {
|
|
13
26
|
this.dialogTarget.removeEventListener("close", this.handleClose)
|
|
14
27
|
document.body.style.removeProperty("overflow")
|
|
@@ -12,9 +12,9 @@ module PhlexKit
|
|
|
12
12
|
|
|
13
13
|
def view_template(&block)
|
|
14
14
|
template(data: { phlex_kit__sheet_target: "content" }) do
|
|
15
|
-
div(data: { controller: "phlex-kit--sheet-content" }) do
|
|
15
|
+
div(data: { controller: "phlex-kit--sheet-content", action: "keydown->phlex-kit--sheet-content#keydown" }) do
|
|
16
16
|
div(class: "pk-drawer-overlay", data: { action: "click->phlex-kit--sheet-content#close" })
|
|
17
|
-
div(**mix({ class: "pk-drawer #{SIDES
|
|
17
|
+
div(**mix({ class: "pk-drawer #{fetch_option(SIDES, @side, :side)}", role: "dialog", aria: { modal: "true" }, tabindex: "-1", data: { phlex_kit__sheet_content_target: "panel" } }, @attrs)) do
|
|
18
18
|
div(class: "pk-drawer-handle", aria: { hidden: "true" })
|
|
19
19
|
yield if block
|
|
20
20
|
end
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
/* Co-located with dropdown_menu.rb — UI::DropdownMenu (+ Trigger / Content / Item
|
|
2
|
-
/ Label / Separator). ruby_ui's structure + Stimulus kept; @floating-ui dropped
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
.
|
|
2
|
+
/ Label / Separator). ruby_ui's structure + Stimulus kept; @floating-ui dropped.
|
|
3
|
+
The panel is a native [popover=manual] in the top layer, anchor-positioned to
|
|
4
|
+
the trigger (Baseline 2026); position-try-fallbacks flips it above at the
|
|
5
|
+
viewport bottom (`side: :top` starts above and flips down). Submenus anchor
|
|
6
|
+
to their row the same way and flip at the right edge. Tailwind → vanilla.
|
|
7
|
+
Theme tokens come from the global stylesheet. */
|
|
8
|
+
.pk-dropdown-menu { display: inline-block; anchor-scope: --pk-dropdown-menu; }
|
|
9
|
+
.pk-dropdown-menu-trigger { display: inline-block; anchor-name: --pk-dropdown-menu; }
|
|
7
10
|
|
|
8
|
-
.pk-dropdown-menu-content {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
.pk-dropdown-menu-content {
|
|
12
|
+
position: fixed;
|
|
13
|
+
position-anchor: --pk-dropdown-menu;
|
|
14
|
+
inset: auto;
|
|
15
|
+
/* the bordered box is the inner viewport — strip the [popover] UA chrome
|
|
16
|
+
(border: solid, padding, Canvas background) from this wrapper */
|
|
17
|
+
border: 0; padding: 0; background: transparent; color: inherit; overflow: visible;
|
|
18
|
+
position-area: block-end span-inline-end;
|
|
19
|
+
margin: 4px 0 0;
|
|
20
|
+
position-try-fallbacks: flip-block;
|
|
21
|
+
}
|
|
22
|
+
.pk-dropdown-menu-content-up { position-area: block-start span-inline-end; margin: 0 0 4px; }
|
|
11
23
|
.pk-dropdown-menu-viewport {
|
|
12
24
|
min-width: 10rem;
|
|
13
25
|
border-radius: calc(var(--pk-radius) - 2px);
|
|
@@ -85,19 +97,28 @@
|
|
|
85
97
|
box-shadow: 0 0 0 2px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
86
98
|
}
|
|
87
99
|
|
|
88
|
-
/* Submenu: trigger row + side panel, revealed on hover/focus — pure CSS.
|
|
89
|
-
|
|
100
|
+
/* Submenu: trigger row + side panel, revealed on hover/focus — pure CSS.
|
|
101
|
+
The row is its own scoped anchor; the panel flips to the other side at
|
|
102
|
+
the viewport right edge. */
|
|
103
|
+
.pk-dropdown-menu-sub { anchor-name: --pk-dropdown-menu-sub; anchor-scope: --pk-dropdown-menu-sub; }
|
|
90
104
|
.pk-dropdown-menu-sub-chevron { margin-left: auto; width: 1rem; height: 1rem; }
|
|
91
105
|
.pk-dropdown-menu-sub-content {
|
|
92
|
-
position:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
106
|
+
position: fixed;
|
|
107
|
+
position-anchor: --pk-dropdown-menu-sub;
|
|
108
|
+
inset: auto;
|
|
109
|
+
position-area: inline-end span-block-end;
|
|
110
|
+
margin-top: -0.25rem;
|
|
111
|
+
position-try-fallbacks: flip-inline;
|
|
96
112
|
min-width: 10rem;
|
|
97
|
-
|
|
113
|
+
/* hover bridge over the gap — symmetric because flip tactics flip
|
|
114
|
+
margins/insets but not padding, so the flipped side needs one too */
|
|
115
|
+
padding: 0 .25rem;
|
|
98
116
|
display: none;
|
|
99
117
|
}
|
|
100
118
|
.pk-dropdown-menu-sub:hover > .pk-dropdown-menu-sub-content,
|
|
101
119
|
.pk-dropdown-menu-sub:focus-within > .pk-dropdown-menu-sub-content { display: block; }
|
|
102
120
|
.pk-dropdown-menu-sub:hover > .pk-dropdown-menu-sub-trigger,
|
|
103
121
|
.pk-dropdown-menu-sub:focus-within > .pk-dropdown-menu-sub-trigger { background: var(--pk-accent); }
|
|
122
|
+
/* Hiding must survive author display values (a host class like
|
|
123
|
+
display:grid would beat the UA [popover] { display: none }). */
|
|
124
|
+
.pk-dropdown-menu-content:not(:popover-open) { display: none; }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
|
-
# The dropdown panel
|
|
2
|
+
# The dropdown panel — a native [popover=manual] the trigger toggles,
|
|
3
|
+
# anchor-positioned with viewport-edge flipping. `side:` is :bottom
|
|
3
4
|
# (default, opens below) or :top (opens above — for triggers near the bottom of
|
|
4
5
|
# the viewport, e.g. the sidebar footer). See dropdown_menu.rb.
|
|
5
6
|
class DropdownMenuContent < BaseComponent
|
|
@@ -9,7 +10,7 @@ module PhlexKit
|
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
def view_template(&block)
|
|
12
|
-
div(class: wrapper_classes, data: { phlex_kit__dropdown_menu_target: "content" }) do
|
|
13
|
+
div(class: wrapper_classes, popover: "manual", data: { phlex_kit__dropdown_menu_target: "content" }) do
|
|
13
14
|
div(**mix({ class: "pk-dropdown-menu-viewport" }, @attrs), &block)
|
|
14
15
|
end
|
|
15
16
|
end
|
|
@@ -17,7 +18,7 @@ module PhlexKit
|
|
|
17
18
|
private
|
|
18
19
|
|
|
19
20
|
def wrapper_classes
|
|
20
|
-
[ "pk-dropdown-menu-content", ("pk-dropdown-menu-content-up" if @side == :top)
|
|
21
|
+
[ "pk-dropdown-menu-content", ("pk-dropdown-menu-content-up" if @side == :top) ].compact.join(" ")
|
|
21
22
|
end
|
|
22
23
|
end
|
|
23
24
|
end
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus";
|
|
2
2
|
|
|
3
|
-
// Ported from ruby_ui's phlex-kit--dropdown-menu controller
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// Ported from ruby_ui's phlex-kit--dropdown-menu controller, minus the
|
|
4
|
+
// @floating-ui/dom dependency: the panel is a native [popover=manual] in the
|
|
5
|
+
// top layer, anchor-positioned with viewport-edge flipping by
|
|
6
|
+
// dropdown_menu.css (manual so this controller keeps owning click-outside,
|
|
7
|
+
// Escape, and arrow/enter keyboard nav — all unchanged from upstream).
|
|
8
8
|
export default class extends Controller {
|
|
9
9
|
static targets = ["trigger", "content", "menuItem"];
|
|
10
10
|
static values = { open: { type: Boolean, default: false } };
|
|
@@ -27,23 +27,20 @@ export default class extends Controller {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
toggle() {
|
|
30
|
-
this.contentTarget.
|
|
30
|
+
this.contentTarget.matches(":popover-open") ? this.close() : this.#open();
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
#open() {
|
|
34
34
|
this.openValue = true;
|
|
35
35
|
this.#deselectAll();
|
|
36
36
|
this.#addEventListeners();
|
|
37
|
-
|
|
38
|
-
this.element.style.zIndex = "50";
|
|
39
|
-
this.contentTarget.classList.remove("hidden");
|
|
37
|
+
this.contentTarget.showPopover();
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
close() {
|
|
43
41
|
this.openValue = false;
|
|
44
42
|
this.#removeEventListeners();
|
|
45
|
-
this.
|
|
46
|
-
this.contentTarget.classList.add("hidden");
|
|
43
|
+
if (this.contentTarget.matches(":popover-open")) this.contentTarget.hidePopover();
|
|
47
44
|
}
|
|
48
45
|
|
|
49
46
|
#handleKeydown(e) {
|
|
@@ -15,7 +15,7 @@ module PhlexKit
|
|
|
15
15
|
base = {
|
|
16
16
|
role: "menuitem",
|
|
17
17
|
tabindex: "-1",
|
|
18
|
-
class: [ "pk-dropdown-menu-item", VARIANTS
|
|
18
|
+
class: [ "pk-dropdown-menu-item", fetch_option(VARIANTS, @variant, :variant) ].compact.join(" "),
|
|
19
19
|
data: { phlex_kit__dropdown_menu_target: "menuItem", action: "click->phlex-kit--dropdown-menu#close" }
|
|
20
20
|
}
|
|
21
21
|
base[:href] = @href unless @as == :div
|
|
@@ -8,7 +8,7 @@ module PhlexKit
|
|
|
8
8
|
@attrs = attrs
|
|
9
9
|
end
|
|
10
10
|
def view_template(&)
|
|
11
|
-
div(**mix({ class: ["pk-empty-media", VARIANTS
|
|
11
|
+
div(**mix({ class: [ "pk-empty-media", fetch_option(VARIANTS, @variant, :variant) ].compact.join(" ") }, @attrs), &)
|
|
12
12
|
end
|
|
13
13
|
end
|
|
14
14
|
end
|
|
@@ -26,7 +26,7 @@ module PhlexKit
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def view_template(&)
|
|
29
|
-
data = { slot: "field", orientation: ORIENTATIONS
|
|
29
|
+
data = { slot: "field", orientation: fetch_option(ORIENTATIONS, @orientation, :orientation) }
|
|
30
30
|
data[:invalid] = "true" if @invalid
|
|
31
31
|
data[:disabled] = "true" if @disabled
|
|
32
32
|
div(**mix({ class: "pk-field", role: "group", data: data }, @attrs), &)
|
|
@@ -11,7 +11,7 @@ module PhlexKit
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def view_template(&)
|
|
14
|
-
legend(**mix({ class: "pk-field-legend #{VARIANTS
|
|
14
|
+
legend(**mix({ class: "pk-field-legend #{fetch_option(VARIANTS, @variant, :variant)}" }, @attrs), &)
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
end
|
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
/* Co-located with hover_card.rb. Matched to shadcn/ui's current hover-card
|
|
2
2
|
(nova): 16rem popover panel, rounded-lg, p-2.5, text-sm, foreground-tint
|
|
3
|
-
ring.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
.
|
|
3
|
+
ring. Tokens from the global stylesheet.
|
|
4
|
+
The panel is a native [popover=manual] in the top layer, anchor-positioned
|
|
5
|
+
to the trigger (Baseline 2026); position-try-fallbacks flips it to the
|
|
6
|
+
opposite side at the viewport edge. side: top/left/right pick the
|
|
7
|
+
position-area (bottom default). anchor-scope keeps sibling hover cards
|
|
8
|
+
from resolving to each other's anchors. */
|
|
9
|
+
.pk-hover-card { display: inline-block; anchor-scope: --pk-hover-card; }
|
|
10
|
+
.pk-hover-card-trigger { display: inline-block; anchor-name: --pk-hover-card; }
|
|
7
11
|
.pk-hover-card-content {
|
|
8
|
-
position:
|
|
12
|
+
position: fixed;
|
|
13
|
+
position-anchor: --pk-hover-card;
|
|
14
|
+
inset: auto;
|
|
15
|
+
position-area: block-end span-inline-end;
|
|
16
|
+
margin: .25rem 0 0;
|
|
17
|
+
position-try-fallbacks: flip-block;
|
|
18
|
+
width: 16rem;
|
|
9
19
|
border: 1px solid color-mix(in oklab, var(--pk-text) 10%, transparent);
|
|
10
20
|
border-radius: var(--pk-radius); background: var(--pk-surface);
|
|
11
21
|
color: var(--pk-text); padding: .625rem; font-size: .875rem;
|
|
12
22
|
box-shadow: 0 8px 24px rgb(0 0 0 / .3); outline: none;
|
|
13
23
|
}
|
|
14
|
-
.pk-hover-card-content.top {
|
|
15
|
-
.pk-hover-card-content.left {
|
|
16
|
-
|
|
24
|
+
.pk-hover-card-content.top { position-area: block-start span-inline-end; margin: 0 0 .25rem; }
|
|
25
|
+
.pk-hover-card-content.left {
|
|
26
|
+
position-area: inline-start; margin: 0 .25rem 0 0;
|
|
27
|
+
position-try-fallbacks: flip-inline;
|
|
28
|
+
}
|
|
29
|
+
.pk-hover-card-content.right {
|
|
30
|
+
position-area: inline-end; margin: 0 0 0 .25rem;
|
|
31
|
+
position-try-fallbacks: flip-inline;
|
|
32
|
+
}
|
|
33
|
+
/* Hiding must survive author display values (a host class like
|
|
34
|
+
display:grid would beat the UA [popover] { display: none }). */
|
|
35
|
+
.pk-hover-card-content:not(:popover-open) { display: none; }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
|
-
# Hover/focus-triggered floating card
|
|
2
|
+
# Hover/focus-triggered floating card — native [popover] + CSS anchor
|
|
3
|
+
# positioning with viewport-edge flipping. Ported from ruby_ui's
|
|
3
4
|
# RubyUI::HoverCard. Compose HoverCard > (HoverCardTrigger + HoverCardContent).
|
|
4
5
|
class HoverCard < BaseComponent
|
|
5
6
|
def initialize(**attrs) = (@attrs = attrs)
|
|
@@ -10,8 +10,8 @@ module PhlexKit
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def view_template(&)
|
|
13
|
-
classes = [ "pk-hover-card-content",
|
|
14
|
-
div(**mix({ class: classes, data: { phlex_kit__hover_card_target: "content", state: "closed" } }, @attrs), &)
|
|
13
|
+
classes = [ "pk-hover-card-content", fetch_option(SIDES, @side, :side) ].compact.join(" ")
|
|
14
|
+
div(**mix({ class: classes, popover: "manual", data: { phlex_kit__hover_card_target: "content", state: "closed" } }, @attrs), &)
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
end
|