phlex_kit 0.8.1 → 0.9.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 +11 -0
- data/app/components/phlex_kit/accordion/accordion.css +2 -2
- data/app/components/phlex_kit/accordion/accordion_controller.js +8 -2
- data/app/components/phlex_kit/alert/alert.css +3 -3
- data/app/components/phlex_kit/alert_dialog/alert_dialog.css +3 -3
- data/app/components/phlex_kit/alert_dialog/alert_dialog_content.rb +12 -8
- data/app/components/phlex_kit/alert_dialog/alert_dialog_controller.js +46 -3
- data/app/components/phlex_kit/aspect_ratio/aspect_ratio.rb +4 -2
- data/app/components/phlex_kit/attachment/attachment.css +1 -1
- data/app/components/phlex_kit/attachment/attachment_action.rb +8 -3
- data/app/components/phlex_kit/attachment/attachment_trigger.rb +3 -3
- data/app/components/phlex_kit/attachment/scroll_fade_controller.js +8 -0
- data/app/components/phlex_kit/avatar/avatar.css +16 -5
- data/app/components/phlex_kit/avatar/avatar_controller.js +10 -7
- data/app/components/phlex_kit/badge/badge.css +4 -4
- data/app/components/phlex_kit/breadcrumb/breadcrumb_ellipsis.rb +3 -1
- data/app/components/phlex_kit/bubble/bubble.css +3 -3
- data/app/components/phlex_kit/bubble/bubble.rb +2 -0
- data/app/components/phlex_kit/bubble/bubble_content.rb +5 -1
- data/app/components/phlex_kit/bubble/bubble_reactions.rb +4 -0
- data/app/components/phlex_kit/button/button.css +6 -6
- data/app/components/phlex_kit/button_group/button_group.css +2 -2
- data/app/components/phlex_kit/button_group/button_group_separator.rb +3 -1
- data/app/components/phlex_kit/calendar/calendar.css +2 -2
- data/app/components/phlex_kit/calendar/calendar.rb +10 -3
- data/app/components/phlex_kit/calendar/calendar_body.rb +3 -0
- data/app/components/phlex_kit/calendar/calendar_controller.js +91 -14
- data/app/components/phlex_kit/calendar/calendar_days.rb +17 -15
- data/app/components/phlex_kit/calendar/calendar_input_controller.js +6 -0
- data/app/components/phlex_kit/calendar/calendar_next.rb +5 -1
- data/app/components/phlex_kit/calendar/calendar_prev.rb +5 -1
- data/app/components/phlex_kit/calendar/calendar_weekdays.rb +1 -1
- data/app/components/phlex_kit/carousel/carousel.rb +11 -1
- data/app/components/phlex_kit/carousel/carousel_controller.js +28 -1
- data/app/components/phlex_kit/chart/chart_controller.js +32 -9
- data/app/components/phlex_kit/checkbox/checkbox.css +2 -1
- data/app/components/phlex_kit/clipboard/clipboard.css +1 -1
- data/app/components/phlex_kit/clipboard/clipboard_controller.js +7 -0
- data/app/components/phlex_kit/codeblock/codeblock.rb +4 -1
- data/app/components/phlex_kit/collapsible/collapsible_content.rb +15 -2
- data/app/components/phlex_kit/collapsible/collapsible_controller.js +11 -2
- data/app/components/phlex_kit/collapsible/collapsible_trigger.rb +17 -2
- data/app/components/phlex_kit/combobox/combobox.css +10 -8
- data/app/components/phlex_kit/combobox/combobox_controller.js +52 -9
- data/app/components/phlex_kit/combobox/combobox_list_group.rb +13 -5
- data/app/components/phlex_kit/command/command.css +4 -4
- data/app/components/phlex_kit/command/command_controller.js +51 -10
- data/app/components/phlex_kit/command/command_dialog.rb +5 -6
- data/app/components/phlex_kit/command/command_dialog_controller.js +26 -8
- data/app/components/phlex_kit/command/command_input.rb +6 -2
- data/app/components/phlex_kit/command/command_item.rb +4 -1
- data/app/components/phlex_kit/context_menu/context_menu.css +6 -6
- data/app/components/phlex_kit/context_menu/context_menu_controller.js +18 -1
- data/app/components/phlex_kit/data_table/data_table.css +1 -1
- data/app/components/phlex_kit/data_table/data_table_column_toggle.rb +11 -12
- data/app/components/phlex_kit/data_table/data_table_column_visibility_controller.js +4 -1
- data/app/components/phlex_kit/data_table/data_table_pagy_adapter.rb +2 -1
- data/app/components/phlex_kit/data_table/data_table_per_page_select.rb +19 -5
- data/app/components/phlex_kit/data_table/data_table_search_controller.js +13 -1
- data/app/components/phlex_kit/date_picker/date_picker.rb +50 -3
- data/app/components/phlex_kit/dialog/dialog.css +4 -4
- data/app/components/phlex_kit/dialog/dialog_controller.js +20 -4
- data/app/components/phlex_kit/drawer/drawer.css +3 -3
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu.css +3 -3
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu.rb +5 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_controller.js +33 -0
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_label.rb +3 -1
- data/app/components/phlex_kit/field/field.css +1 -1
- data/app/components/phlex_kit/field/field_error.rb +5 -6
- data/app/components/phlex_kit/form_field/form_field_controller.js +35 -16
- data/app/components/phlex_kit/form_field/form_field_error.rb +5 -1
- data/app/components/phlex_kit/hover_card/hover_card.css +1 -1
- data/app/components/phlex_kit/hover_card/hover_card_controller.js +29 -5
- data/app/components/phlex_kit/input/input.css +1 -1
- data/app/components/phlex_kit/input_group/input_group.css +9 -7
- data/app/components/phlex_kit/input_group/input_group_addon.rb +5 -1
- data/app/components/phlex_kit/input_otp/input_otp.css +1 -1
- data/app/components/phlex_kit/input_otp/input_otp_controller.js +13 -0
- data/app/components/phlex_kit/input_otp/input_otp_separator.rb +4 -2
- data/app/components/phlex_kit/item/item.css +1 -1
- data/app/components/phlex_kit/item/item_group.rb +4 -2
- data/app/components/phlex_kit/item/item_separator.rb +3 -1
- data/app/components/phlex_kit/marker/marker.css +1 -1
- data/app/components/phlex_kit/masked_input/masked_input_controller.js +10 -4
- data/app/components/phlex_kit/menubar/menubar.css +10 -9
- data/app/components/phlex_kit/menubar/menubar_content.rb +8 -2
- data/app/components/phlex_kit/menubar/menubar_controller.js +70 -7
- data/app/components/phlex_kit/menubar/menubar_menu.rb +4 -1
- data/app/components/phlex_kit/menubar/menubar_sub.rb +6 -1
- data/app/components/phlex_kit/menubar/menubar_sub_trigger.rb +4 -1
- data/app/components/phlex_kit/message_scroller/message_scroller.css +1 -1
- data/app/components/phlex_kit/message_scroller/message_scroller_controller.js +46 -2
- data/app/components/phlex_kit/native_select/native_select.css +1 -1
- data/app/components/phlex_kit/navigation_menu/navigation_menu.css +7 -2
- data/app/components/phlex_kit/navigation_menu/navigation_menu.rb +6 -1
- data/app/components/phlex_kit/pagination/pagination.css +2 -2
- data/app/components/phlex_kit/pagination/pagination_ellipsis.rb +3 -1
- data/app/components/phlex_kit/popover/popover.css +1 -1
- data/app/components/phlex_kit/resizable/resizable.css +3 -1
- data/app/components/phlex_kit/resizable/resizable_controller.js +13 -0
- data/app/components/phlex_kit/resizable/resizable_panel.rb +3 -1
- data/app/components/phlex_kit/scroll_area/scroll_area.rb +4 -2
- data/app/components/phlex_kit/select/select.css +3 -3
- data/app/components/phlex_kit/select/select.rb +4 -1
- data/app/components/phlex_kit/select/select_controller.js +25 -0
- data/app/components/phlex_kit/select/select_item.rb +5 -1
- data/app/components/phlex_kit/sheet/sheet.css +3 -3
- data/app/components/phlex_kit/sheet/sheet_content_controller.js +36 -12
- data/app/components/phlex_kit/sidebar/sidebar.css +8 -8
- data/app/components/phlex_kit/sidebar/sidebar_controller.js +98 -1
- data/app/components/phlex_kit/sidebar/sidebar_menu_button.rb +24 -5
- data/app/components/phlex_kit/sidebar/sidebar_rail.rb +13 -4
- data/app/components/phlex_kit/sidebar/sidebar_trigger.rb +14 -4
- data/app/components/phlex_kit/slider/slider.css +1 -1
- data/app/components/phlex_kit/slider/slider.rb +3 -1
- data/app/components/phlex_kit/switch/switch.rb +6 -1
- data/app/components/phlex_kit/table/table.css +5 -3
- data/app/components/phlex_kit/tabs/tabs.css +4 -4
- data/app/components/phlex_kit/tabs/tabs_content.rb +6 -3
- data/app/components/phlex_kit/tabs/tabs_controller.js +3 -1
- data/app/components/phlex_kit/theme_toggle/theme_toggle.rb +2 -2
- data/app/components/phlex_kit/theme_toggle/theme_toggle_controller.js +45 -9
- data/app/components/phlex_kit/toast/toast.css +32 -14
- data/app/components/phlex_kit/toast/toast_controller.js +24 -4
- data/app/components/phlex_kit/toast/toast_region.rb +13 -3
- data/app/components/phlex_kit/toast/toaster_controller.js +13 -3
- data/app/components/phlex_kit/toggle/toggle.css +4 -4
- data/app/components/phlex_kit/toggle/toggle.rb +6 -2
- data/app/components/phlex_kit/tooltip/tooltip.css +28 -6
- data/app/components/phlex_kit/typography/heading.rb +9 -3
- data/app/components/phlex_kit/typography/typography.css +4 -3
- data/lib/phlex_kit/version.rb +1 -1
- metadata +1 -1
|
@@ -3,26 +3,62 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
3
3
|
// Connects to data-controller="phlex-kit--theme-toggle". Sits on the same wrapper
|
|
4
4
|
// as phlex-kit--toggle; pressed = dark. Flips :root[data-theme] to match the
|
|
5
5
|
// PhlexKit token system (not .dark/.light classes). Adapted from ruby_ui.
|
|
6
|
+
//
|
|
7
|
+
// With no stored choice the root is stamped data-theme="system" — _tokens.css
|
|
8
|
+
// resolves that via prefers-color-scheme, and the matchMedia listener below
|
|
9
|
+
// keeps the toggle's pressed state live when the OS preference flips. Only an
|
|
10
|
+
// explicit user toggle persists "dark"/"light". localStorage access is
|
|
11
|
+
// try/catch-wrapped (storage-blocked contexts): the toggle then still works,
|
|
12
|
+
// just non-persistently, via the in-memory fallback.
|
|
6
13
|
export default class extends Controller {
|
|
7
|
-
connect() {
|
|
14
|
+
connect() {
|
|
15
|
+
this._media = window.matchMedia("(prefers-color-scheme: dark)")
|
|
16
|
+
this._onMediaChange = () => {
|
|
17
|
+
// Only re-apply while the user has no explicit choice.
|
|
18
|
+
if (!this.storedTheme()) this.applyTheme("system")
|
|
19
|
+
}
|
|
20
|
+
this._media.addEventListener("change", this._onMediaChange)
|
|
21
|
+
this.applyTheme(this.storedTheme() || "system")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
disconnect() {
|
|
25
|
+
this._media?.removeEventListener("change", this._onMediaChange)
|
|
26
|
+
}
|
|
27
|
+
|
|
8
28
|
apply(e) {
|
|
9
29
|
const theme = e.detail?.pressed ? "dark" : "light"
|
|
10
30
|
// connect()'s own pressed-state sync re-enters here via the toggle's
|
|
11
31
|
// change event (Stimulus value observation is async, so a flag can't
|
|
12
|
-
// gate it). If the incoming theme already matches the
|
|
32
|
+
// gate it). If the incoming theme already matches the resolved one this
|
|
13
33
|
// is that echo, not a user toggle — persisting it would silently pin
|
|
14
34
|
// the visitor's OS preference in localStorage on first visit.
|
|
15
|
-
if (theme ===
|
|
16
|
-
|
|
35
|
+
if (theme === this.resolvedTheme()) return
|
|
36
|
+
this.storeTheme(theme)
|
|
17
37
|
this.applyTheme(theme)
|
|
18
38
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
39
|
+
|
|
40
|
+
// The user's explicit choice, if any ("dark"/"light"), else null.
|
|
41
|
+
storedTheme() {
|
|
42
|
+
try {
|
|
43
|
+
const t = localStorage.theme
|
|
44
|
+
if (t === "dark" || t === "light") return t
|
|
45
|
+
} catch {}
|
|
46
|
+
return this._fallbackTheme || null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
storeTheme(theme) {
|
|
50
|
+
this._fallbackTheme = theme
|
|
51
|
+
try { localStorage.theme = theme } catch {}
|
|
23
52
|
}
|
|
53
|
+
|
|
54
|
+
// The theme actually in effect right now: explicit choice, else OS.
|
|
55
|
+
resolvedTheme() {
|
|
56
|
+
return this.storedTheme() || (this._media.matches ? "dark" : "light")
|
|
57
|
+
}
|
|
58
|
+
|
|
24
59
|
applyTheme(theme) {
|
|
25
60
|
document.documentElement.setAttribute("data-theme", theme)
|
|
26
|
-
|
|
61
|
+
const dark = theme === "system" ? this._media.matches : theme === "dark"
|
|
62
|
+
this.element.setAttribute("data-phlex-kit--toggle-pressed-value", dark ? "true" : "false")
|
|
27
63
|
}
|
|
28
64
|
}
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
color: var(--pk-text);
|
|
46
46
|
padding: 1rem;
|
|
47
47
|
font-size: 13px;
|
|
48
|
-
box-shadow: 0 4px 12px
|
|
48
|
+
box-shadow: 0 4px 12px color-mix(in srgb, var(--pk-shadow-color) 10%, transparent);
|
|
49
49
|
transition: transform .3s ease-out, opacity .3s ease-out;
|
|
50
50
|
will-change: transform;
|
|
51
51
|
opacity: var(--opacity, 1);
|
|
@@ -53,9 +53,27 @@
|
|
|
53
53
|
.pk-toast[data-state="pending"], .pk-toast[data-state="closing"] { opacity: 0; }
|
|
54
54
|
.pk-toast[data-swipe="move"] { transition: none; }
|
|
55
55
|
|
|
56
|
+
/* Swipe-out: fly along the direction the controller stamps into
|
|
57
|
+
--swipe-end-x/y (implicit `from` = the inline transform, which still
|
|
58
|
+
carries the drag offset via --pk-toast-swipe-x/y). forwards keeps the
|
|
59
|
+
end pose until the 200ms unmount removes the node. */
|
|
60
|
+
.pk-toast[data-swipe="end"] {
|
|
61
|
+
animation: pk-toast-swipe-out .2s ease-out forwards;
|
|
62
|
+
}
|
|
63
|
+
@keyframes pk-toast-swipe-out {
|
|
64
|
+
to {
|
|
65
|
+
transform: translate3d(var(--swipe-end-x, 0px), calc(var(--y-offset, 0px) + var(--swipe-end-y, 0px)), 0);
|
|
66
|
+
opacity: 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
@media (prefers-reduced-motion: reduce) {
|
|
70
|
+
/* No fly-out; [data-state="closing"] still fades it. */
|
|
71
|
+
.pk-toast[data-swipe="end"] { animation: none; }
|
|
72
|
+
}
|
|
73
|
+
|
|
56
74
|
/* Room for the corner close button (region-wide, or per-spawned-toast). */
|
|
57
75
|
.pk-toast-region[data-close-button="true"] .pk-toast,
|
|
58
|
-
.pk-toast.pk-toast-with-close { padding-
|
|
76
|
+
.pk-toast.pk-toast-with-close { padding-inline-end: 2.5rem; }
|
|
59
77
|
|
|
60
78
|
.pk-toast-icon {
|
|
61
79
|
position: relative;
|
|
@@ -65,11 +83,11 @@
|
|
|
65
83
|
justify-content: flex-start;
|
|
66
84
|
width: 1rem;
|
|
67
85
|
height: 1rem;
|
|
68
|
-
margin-
|
|
69
|
-
margin-
|
|
86
|
+
margin-inline-start: -3px;
|
|
87
|
+
margin-inline-end: .25rem;
|
|
70
88
|
color: var(--pk-text);
|
|
71
89
|
}
|
|
72
|
-
.pk-toast-icon-svg { width: 1rem; height: 1rem; margin-
|
|
90
|
+
.pk-toast-icon-svg { width: 1rem; height: 1rem; margin-inline-start: -1px; }
|
|
73
91
|
.pk-toast-icon-svg.spin { animation: pk-toast-spin 1s linear infinite; }
|
|
74
92
|
@keyframes pk-toast-spin { to { transform: rotate(360deg); } }
|
|
75
93
|
|
|
@@ -93,7 +111,7 @@
|
|
|
93
111
|
justify-content: center;
|
|
94
112
|
flex: none;
|
|
95
113
|
height: 1.5rem;
|
|
96
|
-
margin-
|
|
114
|
+
margin-inline-start: auto;
|
|
97
115
|
border: 0;
|
|
98
116
|
border-radius: calc(var(--pk-radius) - 4px);
|
|
99
117
|
padding: 0 .5rem;
|
|
@@ -106,9 +124,9 @@
|
|
|
106
124
|
transition: opacity .15s ease;
|
|
107
125
|
}
|
|
108
126
|
.pk-toast-action:hover { opacity: .9; }
|
|
109
|
-
.pk-toast-action:focus {
|
|
127
|
+
.pk-toast-action:focus-visible {
|
|
110
128
|
outline: none;
|
|
111
|
-
box-shadow: 0 0 0
|
|
129
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
112
130
|
}
|
|
113
131
|
.pk-toast-action:disabled { pointer-events: none; opacity: .5; }
|
|
114
132
|
|
|
@@ -119,7 +137,7 @@
|
|
|
119
137
|
justify-content: center;
|
|
120
138
|
flex: none;
|
|
121
139
|
height: 1.5rem;
|
|
122
|
-
margin-
|
|
140
|
+
margin-inline-start: auto;
|
|
123
141
|
border: 0;
|
|
124
142
|
border-radius: calc(var(--pk-radius) - 4px);
|
|
125
143
|
padding: 0 .5rem;
|
|
@@ -132,15 +150,15 @@
|
|
|
132
150
|
transition: background-color .15s ease;
|
|
133
151
|
}
|
|
134
152
|
.pk-toast-cancel:hover { background: color-mix(in oklab, var(--pk-text) 15%, transparent); }
|
|
135
|
-
.pk-toast-cancel:focus {
|
|
153
|
+
.pk-toast-cancel:focus-visible {
|
|
136
154
|
outline: none;
|
|
137
|
-
box-shadow: 0 0 0
|
|
155
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
138
156
|
}
|
|
139
157
|
|
|
140
158
|
/* Corner × close. */
|
|
141
159
|
.pk-toast-close {
|
|
142
160
|
position: absolute;
|
|
143
|
-
|
|
161
|
+
inset-inline-end: .5rem;
|
|
144
162
|
top: .5rem;
|
|
145
163
|
display: flex;
|
|
146
164
|
align-items: center;
|
|
@@ -156,8 +174,8 @@
|
|
|
156
174
|
transition: background-color .15s ease, color .15s ease;
|
|
157
175
|
}
|
|
158
176
|
.pk-toast-close:hover { background: var(--pk-accent); color: var(--pk-text); }
|
|
159
|
-
.pk-toast-close:focus {
|
|
177
|
+
.pk-toast-close:focus-visible {
|
|
160
178
|
outline: none;
|
|
161
|
-
box-shadow: 0 0 0
|
|
179
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
162
180
|
}
|
|
163
181
|
.pk-toast-close svg { width: .875rem; height: .875rem; }
|
|
@@ -50,7 +50,17 @@ export default class extends Controller {
|
|
|
50
50
|
|
|
51
51
|
requestAnimationFrame(() => {
|
|
52
52
|
this.element.dataset.state = "open"
|
|
53
|
-
|
|
53
|
+
// Spawned under an already-hovered stack (the toaster stamps
|
|
54
|
+
// data-pk-toasts-paused while dispatching pause): arm as paused so the
|
|
55
|
+
// region's resume event starts the timer, instead of ticking away
|
|
56
|
+
// under the user's cursor.
|
|
57
|
+
if (Number.isFinite(this.durationValue) && this.durationValue > 0 &&
|
|
58
|
+
this.element.closest("[data-pk-toasts-paused]")) {
|
|
59
|
+
this._paused = true
|
|
60
|
+
this._remaining = this.durationValue
|
|
61
|
+
} else {
|
|
62
|
+
this._start()
|
|
63
|
+
}
|
|
54
64
|
})
|
|
55
65
|
}
|
|
56
66
|
|
|
@@ -142,7 +152,11 @@ export default class extends Controller {
|
|
|
142
152
|
const dx = e.clientX - this._swipe.x
|
|
143
153
|
const dy = e.clientY - this._swipe.y
|
|
144
154
|
this.element.dataset.swipe = "move"
|
|
145
|
-
|
|
155
|
+
// Never write style.transform here: the toaster owns it (stacking
|
|
156
|
+
// translate3d + scale, written to compose these vars). Writing it
|
|
157
|
+
// directly snapped non-front toasts out of position.
|
|
158
|
+
this.element.style.setProperty("--pk-toast-swipe-x", `${dx}px`)
|
|
159
|
+
this.element.style.setProperty("--pk-toast-swipe-y", `${dy}px`)
|
|
146
160
|
}
|
|
147
161
|
|
|
148
162
|
_onPointerUp(e) {
|
|
@@ -156,14 +170,20 @@ export default class extends Controller {
|
|
|
156
170
|
this.element.removeEventListener("pointercancel", this._onPointerUp)
|
|
157
171
|
this._swipe.active = false
|
|
158
172
|
if (dist > SWIPE_THRESHOLD || velocity > 0.5) {
|
|
173
|
+
// Keep the swipe vars in place: they are the animation's start point.
|
|
174
|
+
// toast.css's [data-swipe="end"] keyframes fly the toast out along
|
|
175
|
+
// --swipe-end-x/y while _close fades + removes it.
|
|
159
176
|
this.element.style.setProperty("--swipe-end-x", `${Math.sign(dx) * 500}px`)
|
|
160
177
|
this.element.style.setProperty("--swipe-end-y", `${Math.sign(dy) * 500}px`)
|
|
161
178
|
this.element.dataset.swipe = "end"
|
|
162
|
-
this.element.style.transform = ""
|
|
163
179
|
this._close("dismiss")
|
|
164
180
|
} else {
|
|
181
|
+
// Clearing the vars composes the toast back into the toaster's stacking
|
|
182
|
+
// transform; the transition (re-enabled once data-swipe leaves "move")
|
|
183
|
+
// animates it home.
|
|
165
184
|
this.element.dataset.swipe = "cancel"
|
|
166
|
-
this.element.style.
|
|
185
|
+
this.element.style.removeProperty("--pk-toast-swipe-x")
|
|
186
|
+
this.element.style.removeProperty("--pk-toast-swipe-y")
|
|
167
187
|
this._resume()
|
|
168
188
|
}
|
|
169
189
|
}
|
|
@@ -32,6 +32,7 @@ module PhlexKit
|
|
|
32
32
|
hotkey: %w[alt t],
|
|
33
33
|
dir: :ltr,
|
|
34
34
|
flash: nil,
|
|
35
|
+
id: "pk-toaster",
|
|
35
36
|
**attrs
|
|
36
37
|
)
|
|
37
38
|
@position = POSITIONS.fetch(position.to_sym)
|
|
@@ -46,12 +47,16 @@ module PhlexKit
|
|
|
46
47
|
@hotkey = hotkey
|
|
47
48
|
@dir = dir
|
|
48
49
|
@flash = flash
|
|
50
|
+
# Base id for the <ol> stack; the wrapper takes "#{id}-region". Defaults
|
|
51
|
+
# keep the historical "pk-toaster"/"pk-toaster-region" pair — pass a
|
|
52
|
+
# distinct id when rendering more than one region on a page.
|
|
53
|
+
@id = id
|
|
49
54
|
@attrs = attrs
|
|
50
55
|
end
|
|
51
56
|
|
|
52
57
|
def view_template(&block)
|
|
53
58
|
div(**mix(region_attrs, @attrs)) do
|
|
54
|
-
ol(id:
|
|
59
|
+
ol(id: @id, class: "pk-toast-list") do
|
|
55
60
|
render_flash if @flash
|
|
56
61
|
yield(self) if block
|
|
57
62
|
end
|
|
@@ -73,6 +78,9 @@ module PhlexKit
|
|
|
73
78
|
render ToastItem.new(variant: variant, id: "flash-#{key}", duration: @duration) do
|
|
74
79
|
render ToastIcon.new(variant: variant)
|
|
75
80
|
render ToastTitle.new { message.to_s }
|
|
81
|
+
# The region-wide close button must reach flash toasts too — the CSS
|
|
82
|
+
# already reserves their right padding when close_button is on.
|
|
83
|
+
render ToastClose.new if @close_button
|
|
76
84
|
end
|
|
77
85
|
end
|
|
78
86
|
end
|
|
@@ -98,10 +106,12 @@ module PhlexKit
|
|
|
98
106
|
|
|
99
107
|
def region_attrs
|
|
100
108
|
{
|
|
101
|
-
id: "
|
|
109
|
+
id: "#{@id}-region",
|
|
102
110
|
role: "region",
|
|
103
111
|
class: "pk-toast-region",
|
|
104
|
-
aria:
|
|
112
|
+
# No aria-live here: each toast item is its own live region
|
|
113
|
+
# (role="status"/"alert"); a live wrapper would double-announce.
|
|
114
|
+
aria: { label: "Notifications" },
|
|
105
115
|
data: {
|
|
106
116
|
controller: "phlex-kit--toaster",
|
|
107
117
|
turbo_permanent: "",
|
|
@@ -181,6 +181,9 @@ export default class extends Controller {
|
|
|
181
181
|
_setExpanded(value) {
|
|
182
182
|
if (this._expanded === value) return
|
|
183
183
|
this._expanded = value
|
|
184
|
+
// Mirror the pause state onto the DOM so toasts spawned while the stack
|
|
185
|
+
// is already hovered can arm paused (the pause event predates them).
|
|
186
|
+
this._listEl?.toggleAttribute("data-pk-toasts-paused", value)
|
|
184
187
|
document.dispatchEvent(new CustomEvent(value ? "phlex-kit:toast:pause" : "phlex-kit:toast:resume"))
|
|
185
188
|
this._reflow()
|
|
186
189
|
}
|
|
@@ -227,7 +230,10 @@ export default class extends Controller {
|
|
|
227
230
|
el.style.transformOrigin = isBottom ? "center bottom" : "center top"
|
|
228
231
|
el.style.top = isBottom ? "auto" : "0"
|
|
229
232
|
el.style.bottom = isBottom ? "0" : "auto"
|
|
230
|
-
|
|
233
|
+
// Compose the toast controller's swipe offset (custom props it sets
|
|
234
|
+
// during pointer-drag) into the stacking transform — the swipe must
|
|
235
|
+
// never overwrite this transform or non-front toasts snap out of place.
|
|
236
|
+
el.style.transform = `translate3d(var(--pk-toast-swipe-x, 0px), calc(${ty}px + var(--pk-toast-swipe-y, 0px)), 0) scale(${scale})`
|
|
231
237
|
el.style.zIndex = String(1000 - i)
|
|
232
238
|
el.style.pointerEvents = visible ? "auto" : "none"
|
|
233
239
|
el.tabIndex = visible ? 0 : -1
|
|
@@ -257,13 +263,17 @@ export default class extends Controller {
|
|
|
257
263
|
const wantAlt = parts.includes("alt")
|
|
258
264
|
const wantCtrl = parts.includes("ctrl")
|
|
259
265
|
const wantMeta = parts.includes("meta")
|
|
266
|
+
const wantShift = parts.includes("shift")
|
|
260
267
|
if (e.key.toLowerCase() !== key.toLowerCase()) return
|
|
261
268
|
if (wantAlt !== e.altKey) return
|
|
262
269
|
if (wantCtrl !== e.ctrlKey) return
|
|
263
270
|
if (wantMeta !== e.metaKey) return
|
|
271
|
+
if (wantShift !== e.shiftKey) return
|
|
264
272
|
e.preventDefault()
|
|
265
|
-
|
|
266
|
-
first
|
|
273
|
+
// Newest toast is the LAST child (append order) and fronts the stack —
|
|
274
|
+
// the first child is the oldest, faded, possibly pointer-events:none one.
|
|
275
|
+
const newest = this._listEl.lastElementChild
|
|
276
|
+
newest?.focus()
|
|
267
277
|
}
|
|
268
278
|
|
|
269
279
|
_registerGlobalApi() {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
.pk-toggle svg { pointer-events: none; flex-shrink: 0; }
|
|
26
26
|
.pk-toggle svg:not([class*="size-"]) { width: 1rem; height: 1rem; }
|
|
27
27
|
.pk-toggle.sm svg:not([class*="size-"]) { width: .875rem; height: .875rem; }
|
|
28
|
-
.pk-toggle:has(> [data-icon="inline-start"]) { padding-
|
|
29
|
-
.pk-toggle:has(> [data-icon="inline-end"]) { padding-
|
|
30
|
-
.pk-toggle.sm:has(> [data-icon="inline-start"]) { padding-
|
|
31
|
-
.pk-toggle.sm:has(> [data-icon="inline-end"]) { padding-
|
|
28
|
+
.pk-toggle:has(> [data-icon="inline-start"]) { padding-inline-start: .5rem; }
|
|
29
|
+
.pk-toggle:has(> [data-icon="inline-end"]) { padding-inline-end: .5rem; }
|
|
30
|
+
.pk-toggle.sm:has(> [data-icon="inline-start"]) { padding-inline-start: .375rem; }
|
|
31
|
+
.pk-toggle.sm:has(> [data-icon="inline-end"]) { padding-inline-end: .375rem; }
|
|
@@ -54,8 +54,12 @@ module PhlexKit
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def render_hidden_input
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
# Disabled in lockstep with the button — a disabled toggle must not
|
|
58
|
+
# submit its value (matches native disabled-control form semantics).
|
|
59
|
+
a = { type: "hidden", name: @name, value: @pressed ? @value : @unpressed_value.to_s,
|
|
60
|
+
data: { phlex_kit__toggle_target: "input" } }
|
|
61
|
+
a[:disabled] = true if @disabled
|
|
62
|
+
input(**a)
|
|
59
63
|
end
|
|
60
64
|
end
|
|
61
65
|
end
|
|
@@ -34,9 +34,14 @@
|
|
|
34
34
|
opacity: 0;
|
|
35
35
|
visibility: hidden;
|
|
36
36
|
pointer-events: none;
|
|
37
|
-
|
|
37
|
+
/* Zero delay here = hover-out hides immediately; the hover-in rule below
|
|
38
|
+
re-declares the delays for the open direction only. */
|
|
39
|
+
transition: opacity .12s ease 0s, visibility 0s linear 0s;
|
|
38
40
|
}
|
|
39
|
-
|
|
41
|
+
@media (prefers-reduced-motion: reduce) {
|
|
42
|
+
.pk-tooltip-content { transition-duration: 0s, 0s; }
|
|
43
|
+
}
|
|
44
|
+
.pk-tooltip-content:has(kbd) { padding-inline-end: .375rem; }
|
|
40
45
|
/* The notch. The arrow is position:fixed so both anchors are acceptable
|
|
41
46
|
(an absolute pseudo's containing block is the bubble, which doesn't
|
|
42
47
|
contain the trigger — anchor() wouldn't resolve). Its center chases the
|
|
@@ -66,8 +71,25 @@
|
|
|
66
71
|
position-area: inline-end; margin: 0 0 0 6px;
|
|
67
72
|
position-try-fallbacks: flip-inline;
|
|
68
73
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
/* Hover reveal only on devices that actually hover (a touch tap must not
|
|
75
|
+
stick the bubble open), with an open delay so tooltips don't flash while
|
|
76
|
+
the cursor sweeps across dense UI. Visibility gets the same delay so the
|
|
77
|
+
bubble doesn't occupy hit-testing space during the wait. */
|
|
78
|
+
@media (hover: hover) {
|
|
79
|
+
.pk-tooltip:hover .pk-tooltip-content {
|
|
80
|
+
opacity: 1;
|
|
81
|
+
visibility: visible;
|
|
82
|
+
transition-delay: .4s, .4s;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/* Keyboard focus reveals instantly (:focus-visible, so pointer/touch clicks
|
|
86
|
+
that focus the trigger don't stick the bubble open). */
|
|
87
|
+
.pk-tooltip:has(:focus-visible) .pk-tooltip-content {
|
|
88
|
+
opacity: 1;
|
|
89
|
+
visibility: visible;
|
|
90
|
+
transition-delay: 0s, 0s;
|
|
91
|
+
}
|
|
92
|
+
/* Escape-dismissed (WCAG 1.4.13): suppressed until pointer/focus leaves.
|
|
93
|
+
transition: none so the hide is immediate regardless of hover-in delays. */
|
|
72
94
|
.pk-tooltip[data-pk-dismissed]:hover .pk-tooltip-content,
|
|
73
|
-
.pk-tooltip[data-pk-dismissed]:focus-within .pk-tooltip-content { opacity: 0; visibility: hidden; }
|
|
95
|
+
.pk-tooltip[data-pk-dismissed]:focus-within .pk-tooltip-content { opacity: 0; visibility: hidden; transition: none; }
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
|
-
# Heading, ported from ruby_ui's RubyUI::Heading. `level:` (1–
|
|
2
|
+
# Heading, ported from ruby_ui's RubyUI::Heading. `level:` (1–6) picks the
|
|
3
3
|
# element AND a default size; `as:` overrides the element; `size:` (1–9)
|
|
4
4
|
# overrides the size on ruby_ui's scale (text-xs … text-5xl). Presentational;
|
|
5
5
|
# attrs pass through via mix. Tailwind → vanilla `.pk-heading*` (typography.css).
|
|
6
6
|
class Heading < BaseComponent
|
|
7
7
|
SIZES = %w[1 2 3 4 5 6 7 8 9].freeze
|
|
8
|
-
# ruby_ui's level → default size mapping (h1 is big, h4 smaller)
|
|
9
|
-
|
|
8
|
+
# ruby_ui's level → default size mapping (h1 is big, h4 smaller); 5/6
|
|
9
|
+
# extend the scale downward — HTML has h1–h6, so all six must map.
|
|
10
|
+
LEVEL_SIZE = { "1" => "7", "2" => "6", "3" => "5", "4" => "4", "5" => "3", "6" => "2" }.freeze
|
|
10
11
|
|
|
11
12
|
def initialize(level: nil, as: nil, size: nil, **attrs)
|
|
13
|
+
# Fail loud at construction: level 7 previously exploded at render time
|
|
14
|
+
# with NoMethodError (no h7 element), and 5/6 fell through the size map.
|
|
15
|
+
unless level.nil? || LEVEL_SIZE.key?(level.to_s)
|
|
16
|
+
raise ArgumentError, "Invalid heading level: #{level.inspect} — valid: 1–6"
|
|
17
|
+
end
|
|
12
18
|
@level = level
|
|
13
19
|
@as = as
|
|
14
20
|
@size = size
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
font-size: .875rem;
|
|
39
39
|
background: var(--pk-bg);
|
|
40
40
|
border: 1px solid var(--pk-border);
|
|
41
|
-
|
|
41
|
+
/* Tracks the radius token — 6px at the default --pk-radius (0.625rem). */
|
|
42
|
+
border-radius: calc(var(--pk-radius) - 4px);
|
|
42
43
|
padding: .15rem .4rem;
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -47,8 +48,8 @@
|
|
|
47
48
|
.pk-inline-link:hover { text-decoration: underline; }
|
|
48
49
|
|
|
49
50
|
/* Blockquote */
|
|
50
|
-
.pk-blockquote { margin: 1.5rem 0; border-
|
|
51
|
+
.pk-blockquote { margin: 1.5rem 0; border-inline-start: 2px solid var(--pk-border); padding-inline-start: 1.5rem; font-style: italic; }
|
|
51
52
|
|
|
52
53
|
/* Prose list (shadcn typography's list style). */
|
|
53
|
-
.pk-list { margin: 1.5rem 0; padding-
|
|
54
|
+
.pk-list { margin: 1.5rem 0; padding-inline-start: 1.5rem; list-style: disc; }
|
|
54
55
|
.pk-list > li { margin-top: .5rem; }
|
data/lib/phlex_kit/version.rb
CHANGED