phlex_kit 0.7.0 → 0.8.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/app/assets/stylesheets/phlex_kit/_tokens.css +10 -2
- data/app/assets/stylesheets/phlex_kit/themes/claude.css +1 -1
- data/app/assets/stylesheets/phlex_kit/themes/neutral.css +1 -1
- data/app/assets/stylesheets/phlex_kit/themes/zinc.css +1 -1
- data/app/components/phlex_kit/accordion/accordion_content.rb +11 -2
- data/app/components/phlex_kit/accordion/accordion_controller.js +15 -2
- data/app/components/phlex_kit/alert_dialog/alert_dialog_content.rb +5 -2
- data/app/components/phlex_kit/alert_dialog/alert_dialog_controller.js +80 -6
- data/app/components/phlex_kit/aspect_ratio/aspect_ratio.rb +8 -4
- data/app/components/phlex_kit/calendar/calendar_body.rb +9 -1
- data/app/components/phlex_kit/calendar/calendar_controller.js +63 -1
- data/app/components/phlex_kit/chart/chart_controller.js +5 -1
- data/app/components/phlex_kit/clipboard/clipboard_popover.rb +3 -1
- data/app/components/phlex_kit/combobox/combobox_controller.js +15 -2
- data/app/components/phlex_kit/context_menu/context_menu_checkbox_item.rb +6 -2
- data/app/components/phlex_kit/context_menu/context_menu_controller.js +51 -1
- data/app/components/phlex_kit/context_menu/context_menu_radio_item.rb +6 -2
- data/app/components/phlex_kit/context_menu/context_menu_sub_trigger.rb +5 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu.css +3 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_checkbox_item.rb +6 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_content.rb +3 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_controller.js +71 -48
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_radio_item.rb +6 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub_trigger.rb +5 -2
- data/app/components/phlex_kit/form_field/form_field_controller.js +14 -1
- data/app/components/phlex_kit/form_field/form_field_error.rb +3 -0
- data/app/components/phlex_kit/input_otp/input_otp_controller.js +30 -4
- data/app/components/phlex_kit/masked_input/masked_input.rb +1 -1
- data/app/components/phlex_kit/masked_input/masked_input_controller.js +24 -5
- data/app/components/phlex_kit/menubar/menubar_controller.js +56 -11
- data/app/components/phlex_kit/menubar/menubar_sub_trigger.rb +3 -1
- data/app/components/phlex_kit/navigation_menu/navigation_menu.css +4 -1
- data/app/components/phlex_kit/navigation_menu/navigation_menu_trigger.rb +1 -1
- data/app/components/phlex_kit/popover/popover.css +3 -3
- data/app/components/phlex_kit/popover/popover_controller.js +29 -6
- data/app/components/phlex_kit/popover/popover_trigger.rb +4 -1
- data/app/components/phlex_kit/resizable/resizable_controller.js +65 -3
- data/app/components/phlex_kit/resizable/resizable_handle.rb +3 -1
- data/app/components/phlex_kit/select/select.rb +8 -7
- data/app/components/phlex_kit/select/select_controller.js +29 -14
- data/app/components/phlex_kit/select/select_item.rb +0 -1
- data/app/components/phlex_kit/sidebar/sidebar.css +19 -7
- data/app/components/phlex_kit/slider/slider.css +2 -2
- data/app/components/phlex_kit/theme_toggle/theme_toggle.rb +11 -0
- data/app/components/phlex_kit/theme_toggle/theme_toggle_controller.js +6 -0
- data/app/components/phlex_kit/toast/toast_region.rb +6 -2
- data/app/components/phlex_kit/toast/toaster_controller.js +7 -3
- data/app/components/phlex_kit/toggle/toggle.rb +3 -1
- data/app/javascript/phlex_kit/controllers/index.js +0 -2
- data/lib/phlex_kit/version.rb +1 -1
- metadata +1 -2
- data/app/components/phlex_kit/select/select_item_controller.js +0 -14
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
|
|
3
3
|
// PhlexKit's stand-in for react-resizable-panels behind shadcn's Resizable:
|
|
4
|
-
// dragging a handle rebalances the flex-grow of the two
|
|
4
|
+
// dragging (or arrow-keying) a handle rebalances the flex-grow of the two
|
|
5
|
+
// panels around it. The handle is a focusable role="separator", so it also
|
|
6
|
+
// carries the ARIA value contract: aria-valuenow tracks the leading panel's
|
|
7
|
+
// share of the pair, and aria-orientation follows the group direction (a
|
|
8
|
+
// handle in a horizontal group is a vertical separator).
|
|
5
9
|
// Connects to data-controller="phlex-kit--resizable"
|
|
6
10
|
export default class extends Controller {
|
|
7
11
|
static targets = ["panel", "handle"]
|
|
8
12
|
static values = { direction: { type: String, default: "horizontal" } }
|
|
9
13
|
|
|
14
|
+
handleTargetConnected(handle) {
|
|
15
|
+
handle.setAttribute("aria-orientation", this.directionValue === "horizontal" ? "vertical" : "horizontal")
|
|
16
|
+
handle.setAttribute("aria-valuemin", "0")
|
|
17
|
+
handle.setAttribute("aria-valuemax", "100")
|
|
18
|
+
this.syncValuenow(handle)
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
start(e) {
|
|
22
|
+
if (e.button !== 0 || !e.isPrimary) return
|
|
11
23
|
const handle = e.currentTarget
|
|
12
24
|
const prev = handle.previousElementSibling
|
|
13
25
|
const next = handle.nextElementSibling
|
|
@@ -20,6 +32,10 @@ export default class extends Controller {
|
|
|
20
32
|
startPos: horizontal ? e.clientX : e.clientY,
|
|
21
33
|
prevSize: sizeOf(prev),
|
|
22
34
|
nextSize: sizeOf(next),
|
|
35
|
+
// The drag redistributes the PAIR's combined flex-grow, not a fixed
|
|
36
|
+
// constant: normalizing to 2 rescaled the pair against untouched
|
|
37
|
+
// siblings, so in a 25/25/50 group the 50 panel ballooned on first drag.
|
|
38
|
+
pairGrow: this.growOf(prev) + this.growOf(next) || 2,
|
|
23
39
|
}
|
|
24
40
|
try { handle.setPointerCapture(e.pointerId) } catch {}
|
|
25
41
|
|
|
@@ -27,8 +43,9 @@ export default class extends Controller {
|
|
|
27
43
|
const delta = (horizontal ? ev.clientX : ev.clientY) - drag.startPos
|
|
28
44
|
const total = drag.prevSize + drag.nextSize
|
|
29
45
|
const prevSize = Math.min(Math.max(drag.prevSize + delta, 0), total)
|
|
30
|
-
prev.style.flexGrow = (prevSize / total) *
|
|
31
|
-
next.style.flexGrow = ((total - prevSize) / total) *
|
|
46
|
+
prev.style.flexGrow = (prevSize / total) * drag.pairGrow
|
|
47
|
+
next.style.flexGrow = ((total - prevSize) / total) * drag.pairGrow
|
|
48
|
+
this.syncValuenow(handle)
|
|
32
49
|
}
|
|
33
50
|
const onUp = () => {
|
|
34
51
|
handle.removeEventListener("pointermove", onMove)
|
|
@@ -39,4 +56,49 @@ export default class extends Controller {
|
|
|
39
56
|
handle.addEventListener("pointerup", onUp)
|
|
40
57
|
handle.addEventListener("pointercancel", onUp)
|
|
41
58
|
}
|
|
59
|
+
|
|
60
|
+
// Arrow keys resize by 5% of the pair per press (matching the drag axis);
|
|
61
|
+
// Home/End collapse the leading panel to its min/max.
|
|
62
|
+
keydown(e) {
|
|
63
|
+
const handle = e.currentTarget
|
|
64
|
+
const prev = handle.previousElementSibling
|
|
65
|
+
const next = handle.nextElementSibling
|
|
66
|
+
if (!prev || !next) return
|
|
67
|
+
|
|
68
|
+
const horizontal = this.directionValue === "horizontal"
|
|
69
|
+
const steps = horizontal
|
|
70
|
+
? { ArrowLeft: -0.05, ArrowRight: 0.05 }
|
|
71
|
+
: { ArrowUp: -0.05, ArrowDown: 0.05 }
|
|
72
|
+
|
|
73
|
+
const pairGrow = this.growOf(prev) + this.growOf(next) || 2
|
|
74
|
+
let share = this.growOf(prev) / (pairGrow || 1)
|
|
75
|
+
if (e.key in steps) {
|
|
76
|
+
share = Math.min(1, Math.max(0, share + steps[e.key]))
|
|
77
|
+
} else if (e.key === "Home") {
|
|
78
|
+
share = 0
|
|
79
|
+
} else if (e.key === "End") {
|
|
80
|
+
share = 1
|
|
81
|
+
} else {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
e.preventDefault()
|
|
85
|
+
|
|
86
|
+
prev.style.flexGrow = share * pairGrow
|
|
87
|
+
next.style.flexGrow = (1 - share) * pairGrow
|
|
88
|
+
this.syncValuenow(handle)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
growOf(el) {
|
|
92
|
+
const g = parseFloat(getComputedStyle(el).flexGrow)
|
|
93
|
+
return Number.isFinite(g) ? g : 1
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
syncValuenow(handle) {
|
|
97
|
+
const prev = handle.previousElementSibling
|
|
98
|
+
const next = handle.nextElementSibling
|
|
99
|
+
if (!prev || !next) return
|
|
100
|
+
const pair = this.growOf(prev) + this.growOf(next)
|
|
101
|
+
const share = pair > 0 ? this.growOf(prev) / pair : 0.5
|
|
102
|
+
handle.setAttribute("aria-valuenow", String(Math.round(share * 100)))
|
|
103
|
+
}
|
|
42
104
|
}
|
|
@@ -12,10 +12,12 @@ module PhlexKit
|
|
|
12
12
|
class: "pk-resizable-handle",
|
|
13
13
|
role: "separator",
|
|
14
14
|
tabindex: "0",
|
|
15
|
+
# Correct for the default horizontal group; the controller re-stamps
|
|
16
|
+
# orientation (and aria-valuenow/min/max) per group direction on connect.
|
|
15
17
|
aria: { orientation: "vertical" },
|
|
16
18
|
data: {
|
|
17
19
|
phlex_kit__resizable_target: "handle",
|
|
18
|
-
action: "pointerdown->phlex-kit--resizable#start"
|
|
20
|
+
action: "pointerdown->phlex-kit--resizable#start keydown->phlex-kit--resizable#keydown"
|
|
19
21
|
}
|
|
20
22
|
}, @attrs)) do
|
|
21
23
|
span(class: "pk-resizable-handle-grip", aria: { hidden: "true" }) if @with_handle
|
|
@@ -2,11 +2,13 @@ module PhlexKit
|
|
|
2
2
|
# Custom dropdown select, ported from ruby_ui's RubyUI::Select — the styled
|
|
3
3
|
# popover (Image #2), NOT the native <select> (that's PhlexKit::NativeSelect). Unlike
|
|
4
4
|
# the rest of the kit this component IS JS-driven: it keeps ruby_ui's Stimulus
|
|
5
|
-
# wiring (`phlex-kit--select` /
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# the
|
|
9
|
-
#
|
|
5
|
+
# wiring (`phlex-kit--select`), since the open/close, selection, and keyboard
|
|
6
|
+
# nav are the point. Two changes from upstream: the controller drops the
|
|
7
|
+
# `@floating-ui/dom` dependency and positions the panel with plain CSS (opens
|
|
8
|
+
# directly below the trigger), and selection flips aria-selected via the
|
|
9
|
+
# instance-scoped itemTargets rather than upstream's document-scoped
|
|
10
|
+
# `.pk-select-item` outlet (which clobbered every other Select on the page).
|
|
11
|
+
# Tailwind → vanilla `.pk-select-*` (select.css).
|
|
10
12
|
#
|
|
11
13
|
# Multi-part. The hidden SelectInput carries the form value/name; SelectTrigger +
|
|
12
14
|
# SelectValue are the closed-state button; SelectContent > SelectGroup >
|
|
@@ -34,8 +36,7 @@ module PhlexKit
|
|
|
34
36
|
data: {
|
|
35
37
|
controller: "phlex-kit--select",
|
|
36
38
|
phlex_kit__select_open_value: "false",
|
|
37
|
-
action: "click@window->phlex-kit--select#clickOutside"
|
|
38
|
-
phlex_kit__select_phlex_kit__select_item_outlet: ".pk-select-item"
|
|
39
|
+
action: "click@window->phlex-kit--select#clickOutside"
|
|
39
40
|
}
|
|
40
41
|
}, @attrs), &block)
|
|
41
42
|
end
|
|
@@ -8,7 +8,6 @@ import { Controller } from "@hotwired/stimulus";
|
|
|
8
8
|
export default class extends Controller {
|
|
9
9
|
static targets = ["trigger", "content", "input", "value", "item"];
|
|
10
10
|
static values = { open: Boolean };
|
|
11
|
-
static outlets = ["phlex-kit--select-item"];
|
|
12
11
|
|
|
13
12
|
connect() {
|
|
14
13
|
this.generateItemsIds();
|
|
@@ -17,13 +16,20 @@ export default class extends Controller {
|
|
|
17
16
|
selectItem(event) {
|
|
18
17
|
event.preventDefault();
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
// currentTarget, not target: the click may land on a child of the item
|
|
20
|
+
// (target.dataset.value would be undefined). itemTargets, not a
|
|
21
|
+
// document-scoped outlet: outlets match `.pk-select-item` page-wide and
|
|
22
|
+
// clobbered every other Select's aria-selected.
|
|
23
|
+
const item = event.currentTarget;
|
|
24
|
+
this.itemTargets.forEach((el) => {
|
|
25
|
+
el.setAttribute("aria-selected", el === item ? "true" : "false");
|
|
26
|
+
});
|
|
21
27
|
|
|
22
28
|
const oldValue = this.inputTarget.value;
|
|
23
|
-
const newValue =
|
|
29
|
+
const newValue = item.dataset.value;
|
|
24
30
|
|
|
25
31
|
this.inputTarget.value = newValue;
|
|
26
|
-
this.valueTarget.innerText =
|
|
32
|
+
this.valueTarget.innerText = item.innerText;
|
|
27
33
|
|
|
28
34
|
this.dispatchOnChange(oldValue, newValue);
|
|
29
35
|
this.closeContent();
|
|
@@ -89,21 +95,30 @@ export default class extends Controller {
|
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
clickOutside(event) {
|
|
92
|
-
if (!this.
|
|
98
|
+
if (!this.contentTarget.matches(":popover-open")) return;
|
|
93
99
|
if (this.element.contains(event.target)) return;
|
|
94
100
|
|
|
95
101
|
event.preventDefault();
|
|
96
|
-
this
|
|
102
|
+
this.#hide();
|
|
97
103
|
}
|
|
98
104
|
|
|
105
|
+
// Open/close derive from the live :popover-open state, never a stored
|
|
106
|
+
// flag — a stale flag is how a close on an already-closed panel becomes
|
|
107
|
+
// an open (bit the popover's keyboard toggle).
|
|
99
108
|
toogleContent() {
|
|
100
|
-
this.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
this.triggerTarget.setAttribute("aria-expanded",
|
|
109
|
+
this.contentTarget.matches(":popover-open") ? this.#hide() : this.#show();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#show() {
|
|
113
|
+
this.openValue = true;
|
|
114
|
+
this.contentTarget.showPopover();
|
|
115
|
+
this.triggerTarget.setAttribute("aria-expanded", "true");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#hide() {
|
|
119
|
+
this.openValue = false;
|
|
120
|
+
if (this.contentTarget.matches(":popover-open")) this.contentTarget.hidePopover();
|
|
121
|
+
this.triggerTarget.setAttribute("aria-expanded", "false");
|
|
107
122
|
}
|
|
108
123
|
|
|
109
124
|
generateItemsIds() {
|
|
@@ -123,7 +138,7 @@ export default class extends Controller {
|
|
|
123
138
|
}
|
|
124
139
|
|
|
125
140
|
closeContent() {
|
|
126
|
-
this
|
|
141
|
+
this.#hide();
|
|
127
142
|
this.resetCurrent();
|
|
128
143
|
|
|
129
144
|
// aria-activedescendant holds an element id; on close it must be removed,
|
|
@@ -21,7 +21,6 @@ module PhlexKit
|
|
|
21
21
|
"aria-selected": (@selected ? "true" : "false"),
|
|
22
22
|
data: {
|
|
23
23
|
value: @value,
|
|
24
|
-
controller: "phlex-kit--select-item",
|
|
25
24
|
action: "click->phlex-kit--select#selectItem keydown.enter->phlex-kit--select#selectItem " \
|
|
26
25
|
"keydown.down->phlex-kit--select#handleKeyDown keydown.up->phlex-kit--select#handleKeyUp " \
|
|
27
26
|
"keydown.esc->phlex-kit--select#handleEsc",
|
|
@@ -169,14 +169,23 @@
|
|
|
169
169
|
.pk-sidebar-wrapper.collapsible-offcanvas .pk-sidebar,
|
|
170
170
|
.pk-sidebar-wrapper.collapsible-icon .pk-sidebar {
|
|
171
171
|
position: relative;
|
|
172
|
-
transition:
|
|
172
|
+
/* visibility rides the transition: it stays visible while sliding out and
|
|
173
|
+
flips hidden at the end — hidden also drops the off-screen links from
|
|
174
|
+
the tab order (they stayed focusable while invisible before). */
|
|
175
|
+
transition: margin-left .2s ease, transform .2s ease, width .2s ease, visibility .2s;
|
|
173
176
|
}
|
|
174
|
-
.pk-sidebar-wrapper.collapsible-offcanvas[data-collapsed] .pk-sidebar { margin-left: -16rem; }
|
|
177
|
+
.pk-sidebar-wrapper.collapsible-offcanvas[data-collapsed] .pk-sidebar { margin-left: -16rem; visibility: hidden; }
|
|
175
178
|
|
|
176
179
|
/* Icon mode: the rail shrinks to a 3rem strip — labels and text hide, menu
|
|
177
180
|
buttons become centered icon squares, hover shows a data-tooltip bubble. */
|
|
178
181
|
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar { width: 3rem; }
|
|
179
|
-
|
|
182
|
+
/* Desktop-only: at mobile widths icon mode becomes the full drawer, and the
|
|
183
|
+
labels/subs must keep their AUTHOR display values (flex). The old approach
|
|
184
|
+
un-hid them with `display: revert`, but revert rolls back to the USER-AGENT
|
|
185
|
+
origin — group labels lost their flex centering, sub lists their gap. */
|
|
186
|
+
@media (min-width: 768px) {
|
|
187
|
+
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] :is(.pk-sidebar-group-label, .pk-sidebar-group-action, .pk-sidebar-menu-badge, .pk-sidebar-menu-action, .pk-sidebar-menu-sub, .pk-sidebar-input, .pk-sidebar-separator) { display: none; }
|
|
188
|
+
}
|
|
180
189
|
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] :is(.pk-sidebar-header, .pk-sidebar-content, .pk-sidebar-footer) {
|
|
181
190
|
padding-inline: .5rem;
|
|
182
191
|
align-items: center;
|
|
@@ -187,7 +196,9 @@
|
|
|
187
196
|
padding: .5rem;
|
|
188
197
|
justify-content: center;
|
|
189
198
|
}
|
|
190
|
-
|
|
199
|
+
@media (min-width: 768px) {
|
|
200
|
+
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button > :not(svg) { display: none; }
|
|
201
|
+
}
|
|
191
202
|
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button[data-tooltip] { position: relative; }
|
|
192
203
|
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button[data-tooltip]:hover::after {
|
|
193
204
|
content: attr(data-tooltip);
|
|
@@ -252,9 +263,8 @@
|
|
|
252
263
|
@media (max-width: 767px) {
|
|
253
264
|
/* Below the cutoff both collapsible modes become the overlay drawer. */
|
|
254
265
|
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar { width: 16rem; }
|
|
255
|
-
|
|
266
|
+
/* No un-hiding needed: the icon-mode hide rules are scoped to >=768px. */
|
|
256
267
|
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button { width: 100%; height: auto; padding: .5rem; justify-content: flex-start; }
|
|
257
|
-
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button > :not(svg) { display: revert; }
|
|
258
268
|
.pk-sidebar-wrapper.collapsible-icon .pk-sidebar-rail { display: none; }
|
|
259
269
|
.pk-sidebar-wrapper:is(.collapsible-offcanvas, .collapsible-icon) .pk-sidebar {
|
|
260
270
|
position: fixed;
|
|
@@ -264,9 +274,11 @@
|
|
|
264
274
|
height: 100dvh;
|
|
265
275
|
margin-left: 0;
|
|
266
276
|
transform: translateX(-100%);
|
|
277
|
+
/* closed drawer: invisible AND out of the tab order */
|
|
278
|
+
visibility: hidden;
|
|
267
279
|
box-shadow: 0 8px 30px rgb(0 0 0 / .35);
|
|
268
280
|
}
|
|
269
|
-
.pk-sidebar-wrapper:is(.collapsible-offcanvas, .collapsible-icon)[data-open] .pk-sidebar { transform: none; }
|
|
281
|
+
.pk-sidebar-wrapper:is(.collapsible-offcanvas, .collapsible-icon)[data-open] .pk-sidebar { transform: none; visibility: visible; }
|
|
270
282
|
.pk-sidebar-wrapper:is(.collapsible-offcanvas, .collapsible-icon)[data-open] .pk-sidebar-scrim {
|
|
271
283
|
display: block;
|
|
272
284
|
position: fixed;
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
margin-top: -0.25rem;
|
|
26
26
|
border-radius: 9999px;
|
|
27
27
|
border: 1px solid var(--pk-ring);
|
|
28
|
-
background:
|
|
28
|
+
background: var(--pk-bg);
|
|
29
29
|
box-shadow: 0 1px 2px rgb(0 0 0 / .25);
|
|
30
30
|
transition: box-shadow .15s ease;
|
|
31
31
|
}
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
height: .75rem;
|
|
52
52
|
border-radius: 9999px;
|
|
53
53
|
border: 1px solid var(--pk-ring);
|
|
54
|
-
background:
|
|
54
|
+
background: var(--pk-bg);
|
|
55
55
|
}
|
|
56
56
|
.pk-slider:hover::-moz-range-thumb,
|
|
57
57
|
.pk-slider:active::-moz-range-thumb {
|
|
@@ -2,6 +2,17 @@ module PhlexKit
|
|
|
2
2
|
# Light/dark theme switch built on Toggle. Ported from ruby_ui's RubyUI::
|
|
3
3
|
# ThemeToggle. The sibling phlex-kit--theme-toggle controller listens for the
|
|
4
4
|
# toggle's change event and flips :root[data-theme] (matching the token system).
|
|
5
|
+
# Only an actual user toggle is persisted to localStorage — with no stored
|
|
6
|
+
# choice the OS preference is followed live on every load.
|
|
7
|
+
#
|
|
8
|
+
# The controller applies the stored theme at Stimulus connect, which is late
|
|
9
|
+
# enough to flash light-mode for returning dark-theme users. To avoid the
|
|
10
|
+
# FOUC, inline this in <head> before any stylesheets:
|
|
11
|
+
#
|
|
12
|
+
# <script>
|
|
13
|
+
# const t = localStorage.theme;
|
|
14
|
+
# if (t === "dark" || t === "light") document.documentElement.dataset.theme = t;
|
|
15
|
+
# </script>
|
|
5
16
|
class ThemeToggle < BaseComponent
|
|
6
17
|
def initialize(wrapper: {}, aria: {}, **attrs)
|
|
7
18
|
@wrapper = wrapper
|
|
@@ -7,6 +7,12 @@ export default class extends Controller {
|
|
|
7
7
|
connect() { this.applyTheme(this.currentTheme()) }
|
|
8
8
|
apply(e) {
|
|
9
9
|
const theme = e.detail?.pressed ? "dark" : "light"
|
|
10
|
+
// connect()'s own pressed-state sync re-enters here via the toggle's
|
|
11
|
+
// change event (Stimulus value observation is async, so a flag can't
|
|
12
|
+
// gate it). If the incoming theme already matches the applied one this
|
|
13
|
+
// is that echo, not a user toggle — persisting it would silently pin
|
|
14
|
+
// the visitor's OS preference in localStorage on first visit.
|
|
15
|
+
if (theme === document.documentElement.getAttribute("data-theme")) return
|
|
10
16
|
localStorage.theme = theme
|
|
11
17
|
this.applyTheme(theme)
|
|
12
18
|
}
|
|
@@ -68,7 +68,9 @@ module PhlexKit
|
|
|
68
68
|
@flash.each do |key, message|
|
|
69
69
|
next if message.nil? || message.to_s.empty?
|
|
70
70
|
variant = Toast.flash_variant(key)
|
|
71
|
-
|
|
71
|
+
# Stamp the region's duration — without it the toast controller's
|
|
72
|
+
# 4000ms default wins and ToastRegion(duration:) is dead config.
|
|
73
|
+
render ToastItem.new(variant: variant, id: "flash-#{key}", duration: @duration) do
|
|
72
74
|
render ToastIcon.new(variant: variant)
|
|
73
75
|
render ToastTitle.new { message.to_s }
|
|
74
76
|
end
|
|
@@ -77,7 +79,9 @@ module PhlexKit
|
|
|
77
79
|
|
|
78
80
|
def skeleton(variant)
|
|
79
81
|
template(data: { phlex_kit__toaster_target: "skeleton", variant: variant.to_s }) do
|
|
80
|
-
|
|
82
|
+
# Region duration baked into the skeleton; _spawn overrides it only
|
|
83
|
+
# when the caller passes a per-toast duration.
|
|
84
|
+
render ToastItem.new(variant: variant, duration: @duration) do
|
|
81
85
|
render ToastIcon.new(variant: variant)
|
|
82
86
|
div(class: "pk-toast-body") do
|
|
83
87
|
render ToastTitle.new
|
|
@@ -189,7 +189,10 @@ export default class extends Controller {
|
|
|
189
189
|
if (!this._listEl) return
|
|
190
190
|
const isBottom = this.positionValue.startsWith("bottom")
|
|
191
191
|
const items = this.toastTargets
|
|
192
|
-
|
|
192
|
+
// Newest toast fronts the stack at BOTH edges (index 0 = front); only
|
|
193
|
+
// the offset direction flips with the position. Using DOM order at
|
|
194
|
+
// top-* put the oldest toast in front and stacked new ones behind it.
|
|
195
|
+
const order = items.slice().reverse()
|
|
193
196
|
const heights = order.map(el => this._heights.get(el) || el.offsetHeight || 64)
|
|
194
197
|
const gap = this.gapValue
|
|
195
198
|
const peekOffset = 16
|
|
@@ -237,9 +240,10 @@ export default class extends Controller {
|
|
|
237
240
|
|
|
238
241
|
_enforceMax(items) {
|
|
239
242
|
if (items.length <= this.maxValue) return
|
|
240
|
-
const isBottom = this.positionValue.startsWith("bottom")
|
|
241
243
|
const dropping = items.length - this.maxValue
|
|
242
|
-
|
|
244
|
+
// Evict the OLDEST toasts (DOM order = append order) regardless of edge;
|
|
245
|
+
// slicing from the tail at top-* dropped the newest ones instead.
|
|
246
|
+
const candidates = items.slice(0, dropping)
|
|
243
247
|
candidates.forEach(el => {
|
|
244
248
|
if (el.dataset.state !== "closing") {
|
|
245
249
|
el.dispatchEvent(new CustomEvent("phlex-kit:toast:force-dismiss", { bubbles: true }))
|
|
@@ -7,7 +7,9 @@ module PhlexKit
|
|
|
7
7
|
SIZES = { sm: "sm", default: nil, lg: "lg" }.freeze
|
|
8
8
|
|
|
9
9
|
def self.modifier_classes(variant:, size:)
|
|
10
|
-
|
|
10
|
+
# Plain fetch — an unknown variant/size raises KeyError (kit-wide
|
|
11
|
+
# fail-loud rule); the nil-default form silently rendered unstyled.
|
|
12
|
+
[ VARIANTS.fetch(variant), SIZES.fetch(size) ].compact
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def initialize(pressed: false, name: nil, value: "1", unpressed_value: nil,
|
|
@@ -36,7 +36,6 @@ import PopoverController from "phlex_kit/controllers/popover_controller"
|
|
|
36
36
|
import ResizableController from "phlex_kit/controllers/resizable_controller"
|
|
37
37
|
import ScrollFadeController from "phlex_kit/controllers/scroll_fade_controller"
|
|
38
38
|
import SelectController from "phlex_kit/controllers/select_controller"
|
|
39
|
-
import SelectItemController from "phlex_kit/controllers/select_item_controller"
|
|
40
39
|
import SheetController from "phlex_kit/controllers/sheet_controller"
|
|
41
40
|
import SheetContentController from "phlex_kit/controllers/sheet_content_controller"
|
|
42
41
|
import SidebarController from "phlex_kit/controllers/sidebar_controller"
|
|
@@ -78,7 +77,6 @@ export function registerPhlexKitControllers(application) {
|
|
|
78
77
|
application.register("phlex-kit--resizable", ResizableController)
|
|
79
78
|
application.register("phlex-kit--scroll-fade", ScrollFadeController)
|
|
80
79
|
application.register("phlex-kit--select", SelectController)
|
|
81
|
-
application.register("phlex-kit--select-item", SelectItemController)
|
|
82
80
|
application.register("phlex-kit--sheet", SheetController)
|
|
83
81
|
application.register("phlex-kit--sheet-content", SheetContentController)
|
|
84
82
|
application.register("phlex-kit--sidebar", SidebarController)
|
data/lib/phlex_kit/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: phlex_kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matt Kennedy
|
|
@@ -486,7 +486,6 @@ files:
|
|
|
486
486
|
- app/components/phlex_kit/select/select_group.rb
|
|
487
487
|
- app/components/phlex_kit/select/select_input.rb
|
|
488
488
|
- app/components/phlex_kit/select/select_item.rb
|
|
489
|
-
- app/components/phlex_kit/select/select_item_controller.js
|
|
490
489
|
- app/components/phlex_kit/select/select_label.rb
|
|
491
490
|
- app/components/phlex_kit/select/select_separator.rb
|
|
492
491
|
- app/components/phlex_kit/select/select_trigger.rb
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { Controller } from "@hotwired/stimulus";
|
|
2
|
-
|
|
3
|
-
// Ported verbatim from ruby_ui's phlex-kit--select-item controller. Flips its own
|
|
4
|
-
// element's aria-selected when an item is chosen (the select controller invokes
|
|
5
|
-
// this on every item via the outlet), which drives the checkmark in select.css.
|
|
6
|
-
export default class extends Controller {
|
|
7
|
-
handleSelectItem({ target }) {
|
|
8
|
-
if (this.element.dataset.value == target.dataset.value) {
|
|
9
|
-
this.element.setAttribute("aria-selected", true);
|
|
10
|
-
} else {
|
|
11
|
-
this.element.removeAttribute("aria-selected");
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
}
|