phlex_kit 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/phlex_kit/_tokens.css +8 -0
- data/app/assets/stylesheets/phlex_kit/phlex_kit.css +1 -0
- data/app/components/phlex_kit/accordion/accordion.css +7 -0
- data/app/components/phlex_kit/alert/alert.rb +1 -1
- data/app/components/phlex_kit/alert_dialog/alert_dialog_controller.js +15 -0
- data/app/components/phlex_kit/attachment/attachment.css +6 -3
- data/app/components/phlex_kit/attachment/attachment_group.rb +6 -1
- data/app/components/phlex_kit/attachment/attachment_trigger.rb +5 -0
- data/app/components/phlex_kit/attachment/scroll_fade_controller.js +7 -2
- data/app/components/phlex_kit/avatar/avatar.css +2 -2
- data/app/components/phlex_kit/avatar/avatar_badge.rb +9 -2
- data/app/components/phlex_kit/badge/badge.css +1 -1
- data/app/components/phlex_kit/breadcrumb/breadcrumb.css +0 -4
- data/app/components/phlex_kit/bubble/bubble.css +1 -1
- data/app/components/phlex_kit/bubble/bubble.rb +4 -6
- data/app/components/phlex_kit/bubble/bubble_content.rb +4 -3
- data/app/components/phlex_kit/bubble/bubble_reactions.rb +4 -6
- data/app/components/phlex_kit/button_group/button_group.css +16 -16
- data/app/components/phlex_kit/calendar/calendar.css +10 -2
- data/app/components/phlex_kit/calendar/calendar_controller.js +33 -9
- data/app/components/phlex_kit/carousel/carousel.css +4 -4
- data/app/components/phlex_kit/carousel/carousel.rb +3 -2
- data/app/components/phlex_kit/carousel/carousel_controller.js +23 -2
- data/app/components/phlex_kit/chart/chart.rb +8 -2
- data/app/components/phlex_kit/clipboard/clipboard_controller.js +26 -3
- data/app/components/phlex_kit/clipboard/clipboard_trigger.rb +1 -1
- data/app/components/phlex_kit/codeblock/codeblock.rb +7 -1
- data/app/components/phlex_kit/collapsible/collapsible.rb +4 -1
- data/app/components/phlex_kit/collapsible/collapsible_controller.js +15 -6
- data/app/components/phlex_kit/collapsible/collapsible_trigger.rb +5 -2
- data/app/components/phlex_kit/combobox/combobox.css +3 -3
- data/app/components/phlex_kit/combobox/combobox.rb +3 -3
- data/app/components/phlex_kit/combobox/combobox_badge_trigger.rb +6 -2
- data/app/components/phlex_kit/combobox/combobox_controller.js +32 -10
- data/app/components/phlex_kit/combobox/combobox_input_trigger.rb +4 -1
- data/app/components/phlex_kit/combobox/combobox_list.rb +6 -2
- data/app/components/phlex_kit/combobox/combobox_search_input.rb +3 -1
- data/app/components/phlex_kit/combobox/combobox_toggle_all_checkbox.rb +7 -1
- data/app/components/phlex_kit/command/command.css +2 -2
- data/app/components/phlex_kit/command/command_controller.js +63 -35
- data/app/components/phlex_kit/command/command_dialog_controller.js +30 -1
- data/app/components/phlex_kit/command/command_group.rb +5 -2
- data/app/components/phlex_kit/command/command_item.rb +4 -0
- data/app/components/phlex_kit/command/command_list.rb +6 -2
- data/app/components/phlex_kit/context_menu/context_menu_controller.js +25 -0
- data/app/components/phlex_kit/context_menu/context_menu_sub.rb +3 -1
- data/app/components/phlex_kit/context_menu/context_menu_sub_trigger.rb +3 -1
- data/app/components/phlex_kit/data_table/data_table.css +0 -10
- data/app/components/phlex_kit/data_table/data_table_column_toggle.rb +4 -1
- data/app/components/phlex_kit/data_table/data_table_controller.js +4 -0
- data/app/components/phlex_kit/date_picker/date_picker.rb +6 -0
- data/app/components/phlex_kit/dialog/dialog.css +5 -3
- data/app/components/phlex_kit/dialog/dialog_close.rb +1 -1
- data/app/components/phlex_kit/dialog/dialog_content.rb +1 -1
- data/app/components/phlex_kit/dialog/dialog_controller.js +22 -2
- data/app/components/phlex_kit/drawer/drawer.css +4 -4
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu.css +5 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_controller.js +27 -0
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub.rb +3 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub_trigger.rb +3 -1
- data/app/components/phlex_kit/empty/empty.css +1 -1
- data/app/components/phlex_kit/field/field.css +2 -1
- data/app/components/phlex_kit/field/field_separator.rb +1 -1
- data/app/components/phlex_kit/form_field/form_field_controller.js +29 -1
- data/app/components/phlex_kit/form_field/form_field_hint.rb +6 -2
- data/app/components/phlex_kit/hover_card/hover_card.css +4 -2
- data/app/components/phlex_kit/icon/icon.css +9 -0
- data/app/components/phlex_kit/icon/icon.rb +7 -1
- data/app/components/phlex_kit/input_group/input_group.css +0 -1
- data/app/components/phlex_kit/input_otp/input_otp.css +4 -4
- data/app/components/phlex_kit/marker/marker.rb +15 -2
- data/app/components/phlex_kit/masked_input/masked_input_controller.js +35 -15
- data/app/components/phlex_kit/menubar/menubar.rb +3 -1
- data/app/components/phlex_kit/menubar/menubar_controller.js +28 -1
- data/app/components/phlex_kit/menubar/menubar_item.rb +4 -2
- data/app/components/phlex_kit/menubar/menubar_radio_group.rb +4 -2
- data/app/components/phlex_kit/message/message.rb +5 -0
- data/app/components/phlex_kit/message_scroller/message_scroller_controller.js +12 -5
- data/app/components/phlex_kit/native_select/native_select.css +4 -2
- data/app/components/phlex_kit/native_select/native_select.rb +12 -3
- data/app/components/phlex_kit/navigation_menu/navigation_menu.rb +7 -1
- data/app/components/phlex_kit/pagination/pagination.rb +10 -2
- data/app/components/phlex_kit/pagination/pagination_next.rb +11 -2
- data/app/components/phlex_kit/pagination/pagination_previous.rb +11 -2
- data/app/components/phlex_kit/popover/popover_controller.js +31 -1
- data/app/components/phlex_kit/progress/progress.css +8 -0
- data/app/components/phlex_kit/progress/progress.rb +14 -5
- data/app/components/phlex_kit/resizable/resizable_controller.js +52 -5
- data/app/components/phlex_kit/resizable/resizable_panel.rb +13 -4
- data/app/components/phlex_kit/scroll_area/scroll_area.rb +23 -4
- data/app/components/phlex_kit/select/select.css +5 -3
- data/app/components/phlex_kit/select/select_content.rb +9 -3
- data/app/components/phlex_kit/select/select_controller.js +59 -6
- data/app/components/phlex_kit/select/select_item.rb +5 -1
- data/app/components/phlex_kit/separator/separator.rb +12 -2
- data/app/components/phlex_kit/sheet/sheet.css +4 -2
- data/app/components/phlex_kit/sheet/sheet_close.rb +1 -1
- data/app/components/phlex_kit/sheet/sheet_content_controller.js +4 -0
- data/app/components/phlex_kit/sheet/sheet_controller.js +15 -0
- data/app/components/phlex_kit/sidebar/sidebar.css +1 -1
- data/app/components/phlex_kit/sidebar/sidebar_controller.js +10 -0
- data/app/components/phlex_kit/sidebar/sidebar_menu_button.rb +9 -1
- data/app/components/phlex_kit/sidebar/sidebar_menu_sub_button.rb +8 -1
- data/app/components/phlex_kit/slider/slider.css +15 -0
- data/app/components/phlex_kit/switch/switch.css +5 -0
- data/app/components/phlex_kit/table/table.css +2 -1
- data/app/components/phlex_kit/table/table.rb +5 -2
- data/app/components/phlex_kit/tabs/tabs.css +2 -2
- data/app/components/phlex_kit/tabs/tabs_content.rb +4 -2
- data/app/components/phlex_kit/tabs/tabs_controller.js +4 -1
- data/app/components/phlex_kit/tabs/tabs_list.rb +7 -2
- data/app/components/phlex_kit/tabs/tabs_trigger.rb +15 -3
- data/app/components/phlex_kit/theme_toggle/theme_toggle_controller.js +14 -0
- data/app/components/phlex_kit/toast/toast.css +3 -2
- data/app/components/phlex_kit/toast/toast_action.rb +4 -1
- data/app/components/phlex_kit/toast/toast_controller.js +13 -5
- data/app/components/phlex_kit/toast/toast_region.rb +19 -9
- data/app/components/phlex_kit/toast/toaster_controller.js +34 -12
- data/app/components/phlex_kit/toggle_group/toggle_group.css +7 -7
- data/app/components/phlex_kit/toggle_group/toggle_group.rb +7 -2
- data/app/components/phlex_kit/toggle_group/toggle_group_controller.js +13 -1
- data/app/components/phlex_kit/toggle_group/toggle_group_item.rb +9 -1
- data/app/components/phlex_kit/tooltip/tooltip.css +7 -3
- data/app/components/phlex_kit/tooltip/tooltip_controller.js +7 -1
- data/app/components/phlex_kit/typography/heading.rb +13 -5
- data/app/components/phlex_kit/typography/list.rb +22 -0
- data/app/components/phlex_kit/typography/text.rb +7 -1
- data/lib/phlex_kit/base_component.rb +13 -0
- data/lib/phlex_kit/version.rb +1 -1
- metadata +3 -1
|
@@ -41,11 +41,22 @@ module PhlexKit
|
|
|
41
41
|
@duration = duration
|
|
42
42
|
@gap = gap
|
|
43
43
|
@offset = offset
|
|
44
|
+
# theme:/rich_colors: are Sonner API surface the kit deliberately does
|
|
45
|
+
# not implement (tokens are :root-scoped; there is no status palette) —
|
|
46
|
+
# fail loud rather than silently no-op (audit round 6).
|
|
44
47
|
@theme = theme.to_sym
|
|
45
|
-
@
|
|
48
|
+
unless @theme == :system
|
|
49
|
+
raise ArgumentError, "ToastRegion theme: only :system is supported — toasts follow the page theme (:root[data-theme])"
|
|
50
|
+
end
|
|
51
|
+
if rich_colors
|
|
52
|
+
raise ArgumentError, "ToastRegion rich_colors: is not supported — the kit has no status color palette (toasts follow --pk-* tokens)"
|
|
53
|
+
end
|
|
46
54
|
@close_button = close_button
|
|
47
55
|
@hotkey = hotkey
|
|
48
|
-
@dir = dir
|
|
56
|
+
@dir = dir.to_sym
|
|
57
|
+
unless %i[ltr rtl auto].include?(@dir)
|
|
58
|
+
raise ArgumentError, "ToastRegion dir: must be :ltr, :rtl or :auto"
|
|
59
|
+
end
|
|
49
60
|
@flash = flash
|
|
50
61
|
# Base id for the <ol> stack; the wrapper takes "#{id}-region". Defaults
|
|
51
62
|
# keep the historical "pk-toaster"/"pk-toaster-region" pair — pass a
|
|
@@ -105,10 +116,12 @@ module PhlexKit
|
|
|
105
116
|
end
|
|
106
117
|
|
|
107
118
|
def region_attrs
|
|
108
|
-
{
|
|
119
|
+
base = {
|
|
109
120
|
id: "#{@id}-region",
|
|
110
121
|
role: "region",
|
|
111
122
|
class: "pk-toast-region",
|
|
123
|
+
# offset: viewport-edge padding, consumed by toast.css.
|
|
124
|
+
style: "--pk-toast-offset: #{@offset.is_a?(Numeric) ? "#{@offset}px" : @offset};",
|
|
112
125
|
# No aria-live here: each toast item is its own live region
|
|
113
126
|
# (role="status"/"alert"); a live wrapper would double-announce.
|
|
114
127
|
aria: { label: "Notifications" },
|
|
@@ -122,14 +135,11 @@ module PhlexKit
|
|
|
122
135
|
phlex_kit__toaster_max_value: @max.to_s,
|
|
123
136
|
phlex_kit__toaster_duration_value: @duration.to_s,
|
|
124
137
|
phlex_kit__toaster_gap_value: @gap.to_s,
|
|
125
|
-
|
|
126
|
-
phlex_kit__toaster_theme_value: @theme.to_s,
|
|
127
|
-
phlex_kit__toaster_rich_colors_value: @rich_colors.to_s,
|
|
128
|
-
phlex_kit__toaster_close_button_value: @close_button.to_s,
|
|
129
|
-
phlex_kit__toaster_hotkey_value: Array(@hotkey).join("+"),
|
|
130
|
-
phlex_kit__toaster_dir_value: @dir.to_s
|
|
138
|
+
phlex_kit__toaster_hotkey_value: Array(@hotkey).join("+")
|
|
131
139
|
}
|
|
132
140
|
}
|
|
141
|
+
base[:dir] = @dir.to_s unless @dir == :ltr
|
|
142
|
+
base
|
|
133
143
|
end
|
|
134
144
|
end
|
|
135
145
|
end
|
|
@@ -34,12 +34,10 @@ export default class extends Controller {
|
|
|
34
34
|
max: { type: Number, default: 3 },
|
|
35
35
|
duration: { type: Number, default: 4000 },
|
|
36
36
|
gap: { type: Number, default: 14 },
|
|
37
|
-
offset
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
closeButton: { type: Boolean, default: false },
|
|
37
|
+
// offset is a CSS custom property (--pk-toast-offset) and dir a real
|
|
38
|
+
// dir attribute, both stamped by ToastRegion; theme/richColors are
|
|
39
|
+
// unsupported (region raises) — none of them are controller state.
|
|
41
40
|
hotkey: { type: String, default: "alt+t" },
|
|
42
|
-
dir: { type: String, default: "ltr" },
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
// State the target callbacks touch lives here, NOT in connect(): Stimulus
|
|
@@ -50,6 +48,7 @@ export default class extends Controller {
|
|
|
50
48
|
this._heights = new Map()
|
|
51
49
|
this._resizeObservers = new WeakMap()
|
|
52
50
|
this._expanded = false
|
|
51
|
+
this._paused = false
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
connect() {
|
|
@@ -59,8 +58,11 @@ export default class extends Controller {
|
|
|
59
58
|
registerStreamAction()
|
|
60
59
|
if (!this._listEl) return
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
// Pause/resume tracks the POINTER, not the expand transition: an
|
|
62
|
+
// expand:true region is born expanded, and tying the pause dispatch to
|
|
63
|
+
// _setExpanded's state change left auto-dismiss running under the cursor.
|
|
64
|
+
this._onPointerEnter = () => { this._setPaused(true); this._setExpanded(true) }
|
|
65
|
+
this._onPointerLeave = () => { this._setPaused(false); if (!this.expandValue) this._setExpanded(false) }
|
|
64
66
|
this._onWindowToast = (e) => this._spawn(e.detail || {})
|
|
65
67
|
this._onWindowDismissAll = () => this._dismissById(null)
|
|
66
68
|
this._onKey = this._onKey.bind(this)
|
|
@@ -82,6 +84,10 @@ export default class extends Controller {
|
|
|
82
84
|
this._listEl?.removeEventListener("pointerenter", this._onPointerEnter)
|
|
83
85
|
this._listEl?.removeEventListener("pointerleave", this._onPointerLeave)
|
|
84
86
|
document.removeEventListener("keydown", this._onKey)
|
|
87
|
+
// Drop the global API if it still closes over this controller — after
|
|
88
|
+
// region teardown it would touch a stale _listEl. A replacement region
|
|
89
|
+
// re-registers its own on connect.
|
|
90
|
+
if (window.PhlexKit?.toast === this._api) delete window.PhlexKit.toast
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
toastTargetConnected(el) {
|
|
@@ -178,13 +184,21 @@ export default class extends Controller {
|
|
|
178
184
|
return tpl.content.firstElementChild.cloneNode(true)
|
|
179
185
|
}
|
|
180
186
|
|
|
181
|
-
|
|
182
|
-
if (this.
|
|
183
|
-
this.
|
|
187
|
+
_setPaused(value) {
|
|
188
|
+
if (this._paused === value) return
|
|
189
|
+
this._paused = value
|
|
184
190
|
// Mirror the pause state onto the DOM so toasts spawned while the stack
|
|
185
191
|
// is already hovered can arm paused (the pause event predates them).
|
|
186
192
|
this._listEl?.toggleAttribute("data-pk-toasts-paused", value)
|
|
187
|
-
|
|
193
|
+
// Region-scoped, not document-global: on multi-region pages one region's
|
|
194
|
+
// hover must not pause (or worse, RESUME under the pointer) another's
|
|
195
|
+
// toasts. Toasts listen on their own list element.
|
|
196
|
+
this._listEl.dispatchEvent(new CustomEvent(value ? "phlex-kit:toast:pause" : "phlex-kit:toast:resume"))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
_setExpanded(value) {
|
|
200
|
+
if (this._expanded === value) return
|
|
201
|
+
this._expanded = value
|
|
188
202
|
this._reflow()
|
|
189
203
|
}
|
|
190
204
|
|
|
@@ -264,7 +278,14 @@ export default class extends Controller {
|
|
|
264
278
|
const wantCtrl = parts.includes("ctrl")
|
|
265
279
|
const wantMeta = parts.includes("meta")
|
|
266
280
|
const wantShift = parts.includes("shift")
|
|
267
|
-
|
|
281
|
+
// Match e.code as well as e.key: with Option held, macOS reports the
|
|
282
|
+
// COMPOSED character (Option+T → "†"), so alt+ hotkeys never match on
|
|
283
|
+
// e.key alone (Sonner matches event.code for the same reason).
|
|
284
|
+
const code = (e.code || "").toLowerCase()
|
|
285
|
+
const keyMatches = e.key.toLowerCase() === key.toLowerCase()
|
|
286
|
+
|| code === `key${key.toLowerCase()}`
|
|
287
|
+
|| code === `digit${key.toLowerCase()}`
|
|
288
|
+
if (!keyMatches) return
|
|
268
289
|
if (wantAlt !== e.altKey) return
|
|
269
290
|
if (wantCtrl !== e.ctrlKey) return
|
|
270
291
|
if (wantMeta !== e.metaKey) return
|
|
@@ -303,6 +324,7 @@ export default class extends Controller {
|
|
|
303
324
|
}
|
|
304
325
|
|
|
305
326
|
window.PhlexKit = window.PhlexKit || {}
|
|
327
|
+
this._api = api
|
|
306
328
|
window.PhlexKit.toast = api
|
|
307
329
|
}
|
|
308
330
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/* Co-located with toggle_group.rb. Theme tokens from the global stylesheet. */
|
|
2
2
|
.pk-toggle-group { display: inline-flex; width: fit-content; align-items: center; border-radius: var(--pk-radius); }
|
|
3
3
|
.pk-toggle-group.vertical { flex-direction: column; align-items: stretch; }
|
|
4
|
-
|
|
4
|
+
/* spacing: N stamps --pk-toggle-group-gap inline (N × .25rem). */
|
|
5
|
+
.pk-toggle-group.spaced { gap: var(--pk-toggle-group-gap, .25rem); }
|
|
5
6
|
/* Joined (default, unspaced): square the inner corners, tighten padding. */
|
|
6
|
-
.pk-toggle-group:not(.spaced) .pk-toggle-group-item { padding-inline: .5rem; }
|
|
7
|
-
.pk-toggle-group:not(.spaced) .pk-toggle-group-item { border-radius:
|
|
8
|
-
.pk-toggle-group:not(.spaced):not(.vertical) .pk-toggle-group-item:
|
|
9
|
-
.pk-toggle-group:not(.spaced)
|
|
10
|
-
.pk-toggle-group.vertical:not(.spaced) .pk-toggle-group-item:
|
|
11
|
-
.pk-toggle-group.vertical:not(.spaced) .pk-toggle-group-item:last-of-type { border-bottom-left-radius: var(--pk-radius); border-bottom-right-radius: var(--pk-radius); }
|
|
7
|
+
.pk-toggle-group:not(.spaced) .pk-toggle-group-item { padding-inline: .5rem; border-radius: 0; }
|
|
8
|
+
.pk-toggle-group:not(.spaced):not(.vertical) .pk-toggle-group-item:first-of-type { border-start-start-radius: var(--pk-radius); border-end-start-radius: var(--pk-radius); }
|
|
9
|
+
.pk-toggle-group:not(.spaced):not(.vertical) .pk-toggle-group-item:last-of-type { border-start-end-radius: var(--pk-radius); border-end-end-radius: var(--pk-radius); }
|
|
10
|
+
.pk-toggle-group.vertical:not(.spaced) .pk-toggle-group-item:first-of-type { border-start-start-radius: var(--pk-radius); border-start-end-radius: var(--pk-radius); }
|
|
11
|
+
.pk-toggle-group.vertical:not(.spaced) .pk-toggle-group-item:last-of-type { border-end-start-radius: var(--pk-radius); border-end-end-radius: var(--pk-radius); }
|
|
@@ -21,7 +21,7 @@ module PhlexKit
|
|
|
21
21
|
|
|
22
22
|
def view_template(&block)
|
|
23
23
|
div(**mix(group_default_attrs, @attrs)) do
|
|
24
|
-
yield(self)
|
|
24
|
+
yield(self) if block
|
|
25
25
|
render_hidden_inputs
|
|
26
26
|
end
|
|
27
27
|
end
|
|
@@ -54,12 +54,17 @@ module PhlexKit
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def group_default_attrs
|
|
57
|
-
{ role: (@type == :single) ? "radiogroup" : "group",
|
|
57
|
+
attrs = { role: (@type == :single) ? "radiogroup" : "group",
|
|
58
58
|
class: [ "pk-toggle-group", (@orientation == :vertical ? "vertical" : nil), (@spacing > 0 ? "spaced" : nil) ].compact.join(" "),
|
|
59
59
|
data: { controller: "phlex-kit--toggle-group",
|
|
60
60
|
phlex_kit__toggle_group_type_value: @type.to_s,
|
|
61
61
|
phlex_kit__toggle_group_name_value: @name.to_s,
|
|
62
62
|
orientation: @orientation.to_s, spacing: @spacing.to_s } }
|
|
63
|
+
# spacing: N gaps the items by N × .25rem (Tailwind-style scale) via the
|
|
64
|
+
# custom property toggle_group.css reads. Trailing ";" so a caller
|
|
65
|
+
# style: merges into a second valid declaration (Phlex mix gotcha).
|
|
66
|
+
attrs[:style] = "--pk-toggle-group-gap: #{@spacing * 0.25}rem;" if @spacing > 0
|
|
67
|
+
attrs
|
|
63
68
|
end
|
|
64
69
|
end
|
|
65
70
|
end
|
|
@@ -17,6 +17,14 @@ export default class extends Controller {
|
|
|
17
17
|
}
|
|
18
18
|
this.rebuildInputs()
|
|
19
19
|
this.updateRovingTabindex(item)
|
|
20
|
+
// Parity with Toggle: announce the selection as
|
|
21
|
+
// "phlex-kit--toggle-group:change" — single carries the one selected
|
|
22
|
+
// value (radiogroup semantics: no deselect), multiple the pressed array.
|
|
23
|
+
const values = this.itemTargets.filter((el) => this.isPressed(el)).map((el) => el.dataset.value)
|
|
24
|
+
this.dispatch("change", {
|
|
25
|
+
detail: this.typeValue === "single" ? { value: values[0] ?? null } : { value: values },
|
|
26
|
+
bubbles: true
|
|
27
|
+
})
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
navigate(event) {
|
|
@@ -37,11 +45,15 @@ export default class extends Controller {
|
|
|
37
45
|
const next = items[nextIndex]
|
|
38
46
|
this.updateRovingTabindex(next)
|
|
39
47
|
next.focus()
|
|
48
|
+
// APG radio-group keyboard model: with role="radio", arrows move focus
|
|
49
|
+
// AND check the landed-on item (Home/End included).
|
|
50
|
+
if (next !== event.currentTarget) next.click()
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
reconcile() {
|
|
43
54
|
if (this.typeValue === "single") {
|
|
44
|
-
|
|
55
|
+
// A disabled button can't take focus — never hand it the tab stop.
|
|
56
|
+
const pressed = this.itemTargets.find((el) => this.isPressed(el) && !el.disabled)
|
|
45
57
|
const first = pressed || this.enabledItems()[0]
|
|
46
58
|
this.itemTargets.forEach((el) => el.setAttribute("tabindex", el === first ? "0" : "-1"))
|
|
47
59
|
} else {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
class ToggleGroupItem < Toggle
|
|
3
3
|
def initialize(value:, group_context:, variant: nil, size: nil, **attrs)
|
|
4
|
+
# Toggle kwargs this item renders no markup for — inheriting them
|
|
5
|
+
# silently discarded the caller's input.
|
|
6
|
+
unsupported = attrs.keys & %i[wrapper name unpressed_value]
|
|
7
|
+
if unsupported.any?
|
|
8
|
+
raise ArgumentError, "ToggleGroupItem does not support #{unsupported.join(", ")} (group items render a bare button)"
|
|
9
|
+
end
|
|
4
10
|
@item_value = value.to_s
|
|
5
11
|
@group_context = group_context
|
|
6
12
|
pressed = group_context[:selected_values].include?(@item_value)
|
|
@@ -26,7 +32,9 @@ module PhlexKit
|
|
|
26
32
|
if @group_context[:type] == :single
|
|
27
33
|
a[:role] = "radio"
|
|
28
34
|
a[:aria] = { checked: @pressed.to_s }
|
|
29
|
-
|
|
35
|
+
# A disabled button can't take focus — giving the tab stop to a
|
|
36
|
+
# pressed-but-disabled item made the whole group Tab-unreachable.
|
|
37
|
+
a[:tabindex] = (@pressed && !@disabled) ? "0" : "-1"
|
|
30
38
|
else
|
|
31
39
|
a[:aria] = { pressed: @pressed.to_s }
|
|
32
40
|
a[:tabindex] = "0"
|
|
@@ -64,11 +64,11 @@
|
|
|
64
64
|
}
|
|
65
65
|
.pk-tooltip-content.bottom { position-area: block-end; margin: 6px 0 0; }
|
|
66
66
|
.pk-tooltip-content.left {
|
|
67
|
-
position-area: inline-start; margin: 0 6px
|
|
67
|
+
position-area: inline-start; margin: 0; margin-inline-end: 6px;
|
|
68
68
|
position-try-fallbacks: flip-inline;
|
|
69
69
|
}
|
|
70
70
|
.pk-tooltip-content.right {
|
|
71
|
-
position-area: inline-end; margin: 0
|
|
71
|
+
position-area: inline-end; margin: 0; margin-inline-start: 6px;
|
|
72
72
|
position-try-fallbacks: flip-inline;
|
|
73
73
|
}
|
|
74
74
|
/* Hover reveal only on devices that actually hover (a touch tap must not
|
|
@@ -79,6 +79,9 @@
|
|
|
79
79
|
.pk-tooltip:hover .pk-tooltip-content {
|
|
80
80
|
opacity: 1;
|
|
81
81
|
visibility: visible;
|
|
82
|
+
/* WCAG 1.4.13 Hoverable: the revealed bubble must accept the pointer —
|
|
83
|
+
it's a DOM child of .pk-tooltip, so hovering it keeps the reveal. */
|
|
84
|
+
pointer-events: auto;
|
|
82
85
|
transition-delay: .4s, .4s;
|
|
83
86
|
}
|
|
84
87
|
}
|
|
@@ -87,9 +90,10 @@
|
|
|
87
90
|
.pk-tooltip:has(:focus-visible) .pk-tooltip-content {
|
|
88
91
|
opacity: 1;
|
|
89
92
|
visibility: visible;
|
|
93
|
+
pointer-events: auto;
|
|
90
94
|
transition-delay: 0s, 0s;
|
|
91
95
|
}
|
|
92
96
|
/* Escape-dismissed (WCAG 1.4.13): suppressed until pointer/focus leaves.
|
|
93
97
|
transition: none so the hide is immediate regardless of hover-in delays. */
|
|
94
98
|
.pk-tooltip[data-pk-dismissed]:hover .pk-tooltip-content,
|
|
95
|
-
.pk-tooltip[data-pk-dismissed]:focus-within .pk-tooltip-content { opacity: 0; visibility: hidden; transition: none; }
|
|
99
|
+
.pk-tooltip[data-pk-dismissed]:focus-within .pk-tooltip-content { opacity: 0; visibility: hidden; pointer-events: none; transition: none; }
|
|
@@ -11,7 +11,13 @@ export default class extends Controller {
|
|
|
11
11
|
const trigger = this.element.querySelector(".pk-tooltip-trigger")
|
|
12
12
|
if (content && trigger) {
|
|
13
13
|
if (!content.id) content.id = `pk-tooltip-${Math.random().toString(36).slice(2, 8)}`
|
|
14
|
-
|
|
14
|
+
// Describe the element that actually takes focus: with focusable: false
|
|
15
|
+
// the wrapper span never receives focus, so a describedby left only on
|
|
16
|
+
// it is never announced — wire the focusable child too.
|
|
17
|
+
const focusable = trigger.querySelector("button, a[href], input, select, textarea, [tabindex]")
|
|
18
|
+
;[trigger, focusable].forEach((el) => {
|
|
19
|
+
if (el && !el.hasAttribute("aria-describedby")) el.setAttribute("aria-describedby", content.id)
|
|
20
|
+
})
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
this._onKeydown = (e) => {
|
|
@@ -8,15 +8,23 @@ module PhlexKit
|
|
|
8
8
|
# ruby_ui's level → default size mapping (h1 is big, h4 smaller); 5/6
|
|
9
9
|
# extend the scale downward — HTML has h1–h6, so all six must map.
|
|
10
10
|
LEVEL_SIZE = { "1" => "7", "2" => "6", "3" => "5", "4" => "4", "5" => "3", "6" => "2" }.freeze
|
|
11
|
+
# `as:` is dispatched dynamically (send) — whitelist so it can't reach
|
|
12
|
+
# arbitrary (including private) methods. Mirrors Separator/Marker.
|
|
13
|
+
AS_TAGS = %i[h1 h2 h3 h4 h5 h6 p span div].freeze
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
# level: defaults to 1 so `Heading.new` is identical to `Heading.new(level: 1)`
|
|
16
|
+
# (a nil default used to render h1 with the level-6 size — self-disagreeing).
|
|
17
|
+
def initialize(level: 1, as: nil, size: nil, **attrs)
|
|
13
18
|
# Fail loud at construction: level 7 previously exploded at render time
|
|
14
19
|
# with NoMethodError (no h7 element), and 5/6 fell through the size map.
|
|
15
|
-
unless
|
|
20
|
+
unless LEVEL_SIZE.key?(level.to_s)
|
|
16
21
|
raise ArgumentError, "Invalid heading level: #{level.inspect} — valid: 1–6"
|
|
17
22
|
end
|
|
18
23
|
@level = level
|
|
19
|
-
@as = as
|
|
24
|
+
@as = as&.to_sym
|
|
25
|
+
if @as && !AS_TAGS.include?(@as)
|
|
26
|
+
raise ArgumentError, "unknown Heading as: #{@as.inspect} (use one of #{AS_TAGS.join(", ")})"
|
|
27
|
+
end
|
|
20
28
|
@size = size
|
|
21
29
|
@attrs = attrs
|
|
22
30
|
end
|
|
@@ -28,11 +36,11 @@ module PhlexKit
|
|
|
28
36
|
private
|
|
29
37
|
|
|
30
38
|
def element
|
|
31
|
-
(@as ||
|
|
39
|
+
(@as || "h#{@level}").to_sym
|
|
32
40
|
end
|
|
33
41
|
|
|
34
42
|
def size_token
|
|
35
|
-
token = (@size || LEVEL_SIZE
|
|
43
|
+
token = (@size || LEVEL_SIZE.fetch(@level.to_s)).to_s
|
|
36
44
|
raise ArgumentError, "Invalid heading size: #{token.inspect}" unless SIZES.include?(token)
|
|
37
45
|
token
|
|
38
46
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module PhlexKit
|
|
2
|
+
# Prose list, ported from shadcn/ui's typography "list" style (ruby_ui's
|
|
3
|
+
# TypographyList). Renders a ul (or ol via as:) with the .pk-list styling
|
|
4
|
+
# that previously shipped as an orphaned utility class. Children are plain
|
|
5
|
+
# <li>s.
|
|
6
|
+
class List < BaseComponent
|
|
7
|
+
# `as:` is dispatched dynamically (send) — whitelist like Separator.
|
|
8
|
+
AS_TAGS = %i[ul ol].freeze
|
|
9
|
+
|
|
10
|
+
def initialize(as: :ul, **attrs)
|
|
11
|
+
@as = as.to_sym
|
|
12
|
+
unless AS_TAGS.include?(@as)
|
|
13
|
+
raise ArgumentError, "unknown List as: #{@as.inspect} (use one of #{AS_TAGS.join(", ")})"
|
|
14
|
+
end
|
|
15
|
+
@attrs = attrs
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def view_template(&)
|
|
19
|
+
send(@as, **mix({ class: "pk-list" }, @attrs), &)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -27,9 +27,15 @@ module PhlexKit
|
|
|
27
27
|
bold: "pk-text-bold",
|
|
28
28
|
muted: "pk-text-muted"
|
|
29
29
|
}.freeze
|
|
30
|
+
# `as:` is dispatched dynamically (send) — whitelist so it can't reach
|
|
31
|
+
# arbitrary (including private) methods. Mirrors Separator/Marker.
|
|
32
|
+
AS_TAGS = %i[p span div label small strong em].freeze
|
|
30
33
|
|
|
31
34
|
def initialize(as: :p, size: "3", weight: :regular, **attrs)
|
|
32
|
-
@as = as
|
|
35
|
+
@as = as.to_sym
|
|
36
|
+
unless AS_TAGS.include?(@as)
|
|
37
|
+
raise ArgumentError, "unknown Text as: #{@as.inspect} (use one of #{AS_TAGS.join(", ")})"
|
|
38
|
+
end
|
|
33
39
|
@size = size.to_s
|
|
34
40
|
@weight = weight.to_sym
|
|
35
41
|
@attrs = attrs
|
|
@@ -30,5 +30,18 @@ module PhlexKit
|
|
|
30
30
|
"valid: #{map.keys.map(&:inspect).join(", ")}"
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
|
+
|
|
34
|
+
# True when the caller supplied an accessible name in @attrs — via the
|
|
35
|
+
# aria: hash or flat aria_label/aria-label(ledby) keys. Components with a
|
|
36
|
+
# generated default label must skip it then: `mix` merges duplicate string
|
|
37
|
+
# attrs ("pagination Résultats") instead of overriding.
|
|
38
|
+
def aria_labelled?
|
|
39
|
+
aria = @attrs[:aria] || @attrs["aria"]
|
|
40
|
+
if aria.is_a?(Hash)
|
|
41
|
+
return true if %i[label labelledby].any? { |k| aria[k] || aria[k.to_s] }
|
|
42
|
+
end
|
|
43
|
+
[ :aria_label, "aria_label", "aria-label",
|
|
44
|
+
:aria_labelledby, "aria_labelledby", "aria-labelledby" ].any? { |k| @attrs[k] }
|
|
45
|
+
end
|
|
33
46
|
end
|
|
34
47
|
end
|
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.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matt Kennedy
|
|
@@ -373,6 +373,7 @@ files:
|
|
|
373
373
|
- app/components/phlex_kit/hover_card/hover_card_content.rb
|
|
374
374
|
- app/components/phlex_kit/hover_card/hover_card_controller.js
|
|
375
375
|
- app/components/phlex_kit/hover_card/hover_card_trigger.rb
|
|
376
|
+
- app/components/phlex_kit/icon/icon.css
|
|
376
377
|
- app/components/phlex_kit/icon/icon.rb
|
|
377
378
|
- app/components/phlex_kit/input/input.css
|
|
378
379
|
- app/components/phlex_kit/input/input.rb
|
|
@@ -587,6 +588,7 @@ files:
|
|
|
587
588
|
- app/components/phlex_kit/typography/heading.rb
|
|
588
589
|
- app/components/phlex_kit/typography/inline_code.rb
|
|
589
590
|
- app/components/phlex_kit/typography/inline_link.rb
|
|
591
|
+
- app/components/phlex_kit/typography/list.rb
|
|
590
592
|
- app/components/phlex_kit/typography/text.rb
|
|
591
593
|
- app/components/phlex_kit/typography/typography.css
|
|
592
594
|
- app/javascript/phlex_kit/controllers/index.js
|