phlex_kit 0.10.0 → 0.12.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/phlex_kit.css +1 -0
- data/app/components/phlex_kit/accordion/accordion.css +7 -0
- data/app/components/phlex_kit/alert_dialog/alert_dialog.css +3 -1
- data/app/components/phlex_kit/alert_dialog/alert_dialog_content.rb +13 -2
- data/app/components/phlex_kit/alert_dialog/alert_dialog_controller.js +38 -7
- data/app/components/phlex_kit/attachment/attachment.css +15 -0
- data/app/components/phlex_kit/attachment/attachment_group.rb +6 -1
- data/app/components/phlex_kit/attachment/attachment_trigger.rb +8 -0
- 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 +1 -0
- data/app/components/phlex_kit/bubble/bubble.rb +4 -6
- data/app/components/phlex_kit/bubble/bubble_reactions.rb +4 -6
- data/app/components/phlex_kit/calendar/calendar_controller.js +56 -15
- data/app/components/phlex_kit/carousel/carousel.rb +3 -2
- data/app/components/phlex_kit/carousel/carousel_controller.js +59 -4
- data/app/components/phlex_kit/chart/chart.rb +10 -2
- data/app/components/phlex_kit/chart/chart_controller.js +37 -8
- data/app/components/phlex_kit/clipboard/clipboard_controller.js +9 -2
- data/app/components/phlex_kit/clipboard/clipboard_trigger.rb +1 -1
- data/app/components/phlex_kit/codeblock/codeblock.rb +11 -1
- data/app/components/phlex_kit/collapsible/collapsible.rb +4 -1
- data/app/components/phlex_kit/combobox/combobox.css +1 -1
- data/app/components/phlex_kit/combobox/combobox_badge_trigger.rb +6 -2
- data/app/components/phlex_kit/combobox/combobox_controller.js +72 -3
- 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_toggle_all_checkbox.rb +4 -0
- data/app/components/phlex_kit/command/command_controller.js +23 -2
- data/app/components/phlex_kit/command/command_input.rb +3 -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.css +3 -0
- data/app/components/phlex_kit/context_menu/context_menu.rb +9 -1
- data/app/components/phlex_kit/context_menu/context_menu_controller.js +74 -12
- 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 +1 -11
- 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/data_table/data_table_pagination.rb +4 -1
- data/app/components/phlex_kit/data_table/data_table_per_page_select.rb +8 -3
- data/app/components/phlex_kit/data_table/data_table_search.rb +7 -2
- data/app/components/phlex_kit/date_picker/date_picker.rb +19 -2
- data/app/components/phlex_kit/date_picker/date_picker_controller.js +28 -0
- data/app/components/phlex_kit/dialog/dialog.css +3 -1
- data/app/components/phlex_kit/dialog/dialog_content.rb +1 -1
- data/app/components/phlex_kit/dialog/dialog_controller.js +16 -2
- data/app/components/phlex_kit/drawer/drawer.rb +1 -1
- data/app/components/phlex_kit/drawer/drawer_content.rb +10 -3
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu.css +9 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu.rb +3 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_controller.js +81 -13
- 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/field/field_separator.rb +1 -1
- data/app/components/phlex_kit/form_field/form_field_controller.js +54 -25
- data/app/components/phlex_kit/form_field/form_field_hint.rb +6 -2
- data/app/components/phlex_kit/hover_card/hover_card.css +10 -2
- data/app/components/phlex_kit/hover_card/hover_card_controller.js +24 -0
- 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/input.css +0 -1
- data/app/components/phlex_kit/input_group/input_group.css +0 -1
- data/app/components/phlex_kit/input_otp/input_otp.rb +6 -3
- data/app/components/phlex_kit/input_otp/input_otp_controller.js +24 -2
- data/app/components/phlex_kit/item/item.css +2 -1
- data/app/components/phlex_kit/kbd/kbd.css +2 -1
- data/app/components/phlex_kit/label/label.css +1 -1
- data/app/components/phlex_kit/link/link.rb +1 -0
- data/app/components/phlex_kit/marker/marker.css +2 -1
- data/app/components/phlex_kit/marker/marker.rb +9 -2
- data/app/components/phlex_kit/masked_input/masked_input_controller.js +20 -14
- data/app/components/phlex_kit/menubar/menubar.css +3 -0
- data/app/components/phlex_kit/menubar/menubar_controller.js +46 -16
- data/app/components/phlex_kit/message/message.rb +5 -0
- data/app/components/phlex_kit/message_scroller/message_scroller_controller.js +3 -0
- data/app/components/phlex_kit/native_select/native_select.css +14 -0
- data/app/components/phlex_kit/native_select/native_select.rb +12 -3
- data/app/components/phlex_kit/navigation_menu/navigation_menu.rb +4 -1
- data/app/components/phlex_kit/navigation_menu/navigation_menu_link.rb +8 -1
- data/app/components/phlex_kit/pagination/pagination.rb +10 -2
- data/app/components/phlex_kit/pagination/pagination_next.rb +3 -1
- data/app/components/phlex_kit/pagination/pagination_previous.rb +3 -1
- data/app/components/phlex_kit/popover/popover_controller.js +42 -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 +3 -0
- data/app/components/phlex_kit/scroll_area/scroll_area.rb +5 -15
- data/app/components/phlex_kit/select/select.css +5 -2
- data/app/components/phlex_kit/select/select_controller.js +49 -4
- data/app/components/phlex_kit/select/select_trigger.rb +4 -1
- data/app/components/phlex_kit/separator/separator.rb +5 -1
- data/app/components/phlex_kit/sheet/sheet.css +4 -2
- data/app/components/phlex_kit/sheet/sheet_content.rb +13 -3
- data/app/components/phlex_kit/sheet/sheet_content_controller.js +40 -0
- data/app/components/phlex_kit/sheet/sheet_controller.js +31 -1
- 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/spinner/spinner.rb +6 -3
- data/app/components/phlex_kit/switch/switch.css +5 -0
- data/app/components/phlex_kit/switch/switch.rb +6 -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_content.rb +4 -2
- data/app/components/phlex_kit/tabs/tabs_controller.js +8 -2
- data/app/components/phlex_kit/tabs/tabs_list.rb +7 -2
- data/app/components/phlex_kit/tabs/tabs_trigger.rb +11 -2
- data/app/components/phlex_kit/theme_toggle/theme_toggle_controller.js +14 -0
- data/app/components/phlex_kit/toast/toast.css +2 -1
- data/app/components/phlex_kit/toast/toast_action.rb +4 -1
- data/app/components/phlex_kit/toast/toast_close.rb +6 -3
- data/app/components/phlex_kit/toast/toast_controller.js +40 -21
- data/app/components/phlex_kit/toast/toast_item.rb +11 -7
- data/app/components/phlex_kit/toast/toast_region.rb +29 -10
- data/app/components/phlex_kit/toast/toaster_controller.js +17 -7
- data/app/components/phlex_kit/toggle_group/toggle_group.rb +17 -2
- data/app/components/phlex_kit/toggle_group/toggle_group_controller.js +16 -9
- data/app/components/phlex_kit/toggle_group/toggle_group_item.rb +20 -2
- data/app/components/phlex_kit/tooltip/tooltip.css +5 -1
- data/app/components/phlex_kit/tooltip/tooltip_controller.js +7 -1
- data/app/components/phlex_kit/typography/heading.rb +7 -1
- data/app/components/phlex_kit/typography/list.rb +22 -0
- data/app/components/phlex_kit/typography/text.rb +7 -1
- data/app/javascript/phlex_kit/controllers/index.js +2 -0
- data/lib/phlex_kit/base_component.rb +23 -0
- data/lib/phlex_kit/version.rb +1 -1
- data/lib/phlex_kit.rb +1 -0
- metadata +4 -1
|
@@ -2,14 +2,19 @@ module PhlexKit
|
|
|
2
2
|
class TabsList < BaseComponent
|
|
3
3
|
VARIANTS = { default: nil, line: "line" }.freeze
|
|
4
4
|
|
|
5
|
-
def initialize(variant: :default, **attrs)
|
|
5
|
+
def initialize(variant: :default, orientation: :horizontal, **attrs)
|
|
6
6
|
@variant = variant.to_sym
|
|
7
|
+
@orientation = orientation.to_sym
|
|
7
8
|
@attrs = attrs
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
def view_template(&)
|
|
11
12
|
classes = [ "pk-tabs-list", fetch_option(VARIANTS, @variant, :variant) ].compact.join(" ")
|
|
12
|
-
|
|
13
|
+
base = { class: classes, role: "tablist", data: { phlex_kit__tabs_target: "list", action: "keydown->phlex-kit--tabs#keydown" } }
|
|
14
|
+
# Server-rendered so AT hears the right axis pre-JS / with JS off; the
|
|
15
|
+
# controller re-stamps it on connect from the root's vertical class.
|
|
16
|
+
base[:aria] = { orientation: "vertical" } if @orientation == :vertical
|
|
17
|
+
div(**mix(base, @attrs), &)
|
|
13
18
|
end
|
|
14
19
|
end
|
|
15
20
|
end
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
class TabsTrigger < BaseComponent
|
|
3
|
-
|
|
3
|
+
# id: is a named kwarg (not left in **attrs) because `mix` would *merge* a
|
|
4
|
+
# caller id with the deterministic one into an invalid two-token id,
|
|
5
|
+
# breaking the aria pairing pre-JS (and scopeIds' caller-id detection).
|
|
6
|
+
AS_TAGS = %i[button a].freeze
|
|
7
|
+
|
|
8
|
+
def initialize(value:, as: :button, active: false, id: nil, **attrs)
|
|
4
9
|
@value = value
|
|
5
10
|
@as = as.to_sym
|
|
11
|
+
unless AS_TAGS.include?(@as)
|
|
12
|
+
raise ArgumentError, "unknown TabsTrigger as: #{@as.inspect} (use one of #{AS_TAGS.join(", ")})"
|
|
13
|
+
end
|
|
6
14
|
@active = active
|
|
15
|
+
@id = id || "pk-tabs-trigger-#{value}"
|
|
7
16
|
@attrs = attrs
|
|
8
17
|
end
|
|
9
18
|
def view_template(&)
|
|
@@ -13,7 +22,7 @@ module PhlexKit
|
|
|
13
22
|
# aria-selected and the roving tabindex are kept in sync by the
|
|
14
23
|
# controller's activeValueChanged.
|
|
15
24
|
base = {
|
|
16
|
-
class: "pk-tabs-trigger", role: "tab", id:
|
|
25
|
+
class: "pk-tabs-trigger", role: "tab", id: @id,
|
|
17
26
|
aria_selected: @active ? "true" : "false",
|
|
18
27
|
aria_controls: "pk-tabs-panel-#{@value}",
|
|
19
28
|
tabindex: @active ? "0" : "-1",
|
|
@@ -18,11 +18,25 @@ export default class extends Controller {
|
|
|
18
18
|
if (!this.storedTheme()) this.applyTheme("system")
|
|
19
19
|
}
|
|
20
20
|
this._media.addEventListener("change", this._onMediaChange)
|
|
21
|
+
// Multiple toggles on one page (header + mobile menu): each instance
|
|
22
|
+
// observes :root[data-theme] so a change applied by ANY of them (or by
|
|
23
|
+
// host code) re-syncs every toggle's pressed state — without this the
|
|
24
|
+
// stale one showed the wrong state and its first click was eaten by the
|
|
25
|
+
// echo guard in apply().
|
|
26
|
+
this._rootObserver = new MutationObserver(() => this.syncPressed())
|
|
27
|
+
this._rootObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] })
|
|
21
28
|
this.applyTheme(this.storedTheme() || "system")
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
disconnect() {
|
|
25
32
|
this._media?.removeEventListener("change", this._onMediaChange)
|
|
33
|
+
this._rootObserver?.disconnect()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
syncPressed() {
|
|
37
|
+
const theme = document.documentElement.getAttribute("data-theme") || "system"
|
|
38
|
+
const dark = theme === "system" ? this._media.matches : theme === "dark"
|
|
39
|
+
this.element.setAttribute("data-phlex-kit--toggle-pressed-value", dark ? "true" : "false")
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
apply(e) {
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
.pk-toast-region {
|
|
8
8
|
position: fixed;
|
|
9
9
|
z-index: 100;
|
|
10
|
-
|
|
10
|
+
/* --pk-toast-offset is stamped inline by ToastRegion(offset:). */
|
|
11
|
+
padding: var(--pk-toast-offset, 1.5rem);
|
|
11
12
|
pointer-events: none;
|
|
12
13
|
}
|
|
13
14
|
.pk-toast-region[data-position="top-left"] { top: 0; left: 0; }
|
|
@@ -12,7 +12,10 @@ module PhlexKit
|
|
|
12
12
|
|
|
13
13
|
def view_template
|
|
14
14
|
data = { slot: "action" }
|
|
15
|
-
|
|
15
|
+
# Caller action first, then dismiss — client-spawned actions dismiss
|
|
16
|
+
# their toast after onClick (Sonner parity), so server-rendered ones
|
|
17
|
+
# must too.
|
|
18
|
+
data[:action] = [ @on, "click->phlex-kit--toast#dismiss" ].compact.join(" ")
|
|
16
19
|
button(**mix({ type: :button, class: "pk-toast-action", data: data }, @attrs)) { @label }
|
|
17
20
|
end
|
|
18
21
|
end
|
|
@@ -7,12 +7,15 @@ module PhlexKit
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def view_template
|
|
10
|
-
|
|
10
|
+
base = {
|
|
11
11
|
type: :button,
|
|
12
12
|
class: "pk-toast-close",
|
|
13
|
-
aria: { label: "Close toast" },
|
|
14
13
|
data: { slot: "close", action: "click->phlex-kit--toast#dismiss" }
|
|
15
|
-
}
|
|
14
|
+
}
|
|
15
|
+
# Default label only when the caller didn't supply their own — `mix`
|
|
16
|
+
# would fuse aria-label into a two-token value instead of overriding.
|
|
17
|
+
base[:aria] = { label: "Close toast" } unless aria_labelled?
|
|
18
|
+
button(**mix(base, @attrs)) do
|
|
16
19
|
render Icon.new(:x, size: 14)
|
|
17
20
|
span(class: "pk-sr-only") { "Close" }
|
|
18
21
|
end
|
|
@@ -20,23 +20,28 @@ export default class extends Controller {
|
|
|
20
20
|
this._unmountTimer = null
|
|
21
21
|
this._startedAt = 0
|
|
22
22
|
this._remaining = this.durationValue
|
|
23
|
-
|
|
23
|
+
// Independent pause sources (hover / focus / region) — resume only once
|
|
24
|
+
// ALL are clear. A single shared boolean let any one source's resume
|
|
25
|
+
// restart the timer while another source still held it paused (e.g.
|
|
26
|
+
// pointer leaving a toast into the gap while the region is still
|
|
27
|
+
// hover-paused).
|
|
28
|
+
this._pauseSources = new Set()
|
|
24
29
|
this._swipe = { active: false, x: 0, y: 0, startedAt: 0 }
|
|
25
30
|
|
|
26
31
|
this._onPointerDown = this._onPointerDown.bind(this)
|
|
27
32
|
this._onPointerMove = this._onPointerMove.bind(this)
|
|
28
33
|
this._onPointerUp = this._onPointerUp.bind(this)
|
|
29
|
-
this._onPointerEnter = () => this._pause()
|
|
30
|
-
this._onPointerLeave = () => { if (!this._swipe.active) this._resume() }
|
|
34
|
+
this._onPointerEnter = () => this._pause("hover")
|
|
35
|
+
this._onPointerLeave = () => { if (!this._swipe.active) this._resume("hover") }
|
|
31
36
|
// Keyboard parity with hover: tabbing into the toast (e.g. to reach its
|
|
32
37
|
// action button) must pause auto-dismiss just like pointerenter does.
|
|
33
|
-
this._onFocusIn = () => this._pause()
|
|
34
|
-
this._onFocusOut = (e) => { if (!this.element.contains(e.relatedTarget)) this._resume() }
|
|
38
|
+
this._onFocusIn = () => this._pause("focus")
|
|
39
|
+
this._onFocusOut = (e) => { if (!this.element.contains(e.relatedTarget)) this._resume("focus") }
|
|
35
40
|
this._onKeyDown = this._onKeyDown.bind(this)
|
|
36
41
|
this._onForceDismiss = (e) => { e.stopPropagation(); this._close() }
|
|
37
42
|
this._onRestart = () => this._restart()
|
|
38
|
-
this._onRegionPause = () => this._pause()
|
|
39
|
-
this._onRegionResume = () => this._resume()
|
|
43
|
+
this._onRegionPause = () => this._pause("region")
|
|
44
|
+
this._onRegionResume = () => this._resume("region")
|
|
40
45
|
|
|
41
46
|
this.element.addEventListener("pointerdown", this._onPointerDown)
|
|
42
47
|
this.element.addEventListener("pointerenter", this._onPointerEnter)
|
|
@@ -46,10 +51,14 @@ export default class extends Controller {
|
|
|
46
51
|
this.element.addEventListener("keydown", this._onKeyDown)
|
|
47
52
|
this.element.addEventListener("phlex-kit:toast:force-dismiss", this._onForceDismiss)
|
|
48
53
|
this.element.addEventListener("phlex-kit:toast:restart", this._onRestart)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
// Region-scoped: the toaster dispatches pause/resume on its own list, so
|
|
55
|
+
// another region's hover can't freeze (or resume) this toast's timer.
|
|
56
|
+
this._regionList = this.element.closest(".pk-toast-list") || document
|
|
57
|
+
this._regionList.addEventListener("phlex-kit:toast:pause", this._onRegionPause)
|
|
58
|
+
this._regionList.addEventListener("phlex-kit:toast:resume", this._onRegionResume)
|
|
59
|
+
|
|
60
|
+
this._connectFrame = requestAnimationFrame(() => {
|
|
61
|
+
this._connectFrame = null
|
|
53
62
|
this.element.dataset.state = "open"
|
|
54
63
|
// Spawned under an already-hovered stack (the toaster stamps
|
|
55
64
|
// data-pk-toasts-paused while dispatching pause): arm as paused so the
|
|
@@ -57,7 +66,7 @@ export default class extends Controller {
|
|
|
57
66
|
// under the user's cursor.
|
|
58
67
|
if (Number.isFinite(this.durationValue) && this.durationValue > 0 &&
|
|
59
68
|
this.element.closest("[data-pk-toasts-paused]")) {
|
|
60
|
-
this.
|
|
69
|
+
this._pauseSources.add("region")
|
|
61
70
|
this._remaining = this.durationValue
|
|
62
71
|
} else {
|
|
63
72
|
this._start()
|
|
@@ -67,6 +76,8 @@ export default class extends Controller {
|
|
|
67
76
|
|
|
68
77
|
disconnect() {
|
|
69
78
|
this._clearTimer()
|
|
79
|
+
if (this._connectFrame) cancelAnimationFrame(this._connectFrame)
|
|
80
|
+
this._connectFrame = null
|
|
70
81
|
// Don't let a pending 200ms unmount fire after teardown (e.g. into a
|
|
71
82
|
// Turbo-cached copy of the page).
|
|
72
83
|
if (this._unmountTimer) clearTimeout(this._unmountTimer)
|
|
@@ -79,8 +90,8 @@ export default class extends Controller {
|
|
|
79
90
|
this.element.removeEventListener("keydown", this._onKeyDown)
|
|
80
91
|
this.element.removeEventListener("phlex-kit:toast:force-dismiss", this._onForceDismiss)
|
|
81
92
|
this.element.removeEventListener("phlex-kit:toast:restart", this._onRestart)
|
|
82
|
-
|
|
83
|
-
|
|
93
|
+
this._regionList?.removeEventListener("phlex-kit:toast:pause", this._onRegionPause)
|
|
94
|
+
this._regionList?.removeEventListener("phlex-kit:toast:resume", this._onRegionResume)
|
|
84
95
|
}
|
|
85
96
|
|
|
86
97
|
dismiss(e) {
|
|
@@ -112,22 +123,30 @@ export default class extends Controller {
|
|
|
112
123
|
this._timer = setTimeout(() => this._close("auto"), this._remaining)
|
|
113
124
|
}
|
|
114
125
|
|
|
126
|
+
// Resets the timer duration but must respect any pause source still
|
|
127
|
+
// active (e.g. a promise resolving into a fresh duration while the toast
|
|
128
|
+
// is hovered) — arm paused instead of ticking away under the cursor, and
|
|
129
|
+
// leave the source set intact so the matching resume can still fire.
|
|
115
130
|
_restart() {
|
|
116
131
|
this._clearTimer()
|
|
132
|
+
this._remaining = this.durationValue
|
|
133
|
+
if (this._pauseSources.size > 0) return
|
|
117
134
|
this._start()
|
|
118
135
|
}
|
|
119
136
|
|
|
120
|
-
_pause() {
|
|
121
|
-
|
|
122
|
-
this.
|
|
137
|
+
_pause(source) {
|
|
138
|
+
this._pauseSources.add(source)
|
|
139
|
+
if (!this._timer) return
|
|
123
140
|
clearTimeout(this._timer)
|
|
124
141
|
this._timer = null
|
|
125
142
|
this._remaining -= performance.now() - this._startedAt
|
|
126
143
|
}
|
|
127
144
|
|
|
128
|
-
_resume() {
|
|
129
|
-
|
|
130
|
-
this.
|
|
145
|
+
_resume(source) {
|
|
146
|
+
this._pauseSources.delete(source)
|
|
147
|
+
if (this._pauseSources.size > 0) return
|
|
148
|
+
if (this._timer) return
|
|
149
|
+
if (!Number.isFinite(this.durationValue) || this.durationValue <= 0) return
|
|
131
150
|
if (this._remaining <= 0) return this._close("auto")
|
|
132
151
|
this._startedAt = performance.now()
|
|
133
152
|
this._timer = setTimeout(() => this._close("auto"), this._remaining)
|
|
@@ -189,7 +208,7 @@ export default class extends Controller {
|
|
|
189
208
|
this.element.dataset.swipe = "cancel"
|
|
190
209
|
this.element.style.removeProperty("--pk-toast-swipe-x")
|
|
191
210
|
this.element.style.removeProperty("--pk-toast-swipe-y")
|
|
192
|
-
this._resume()
|
|
211
|
+
this._resume("hover")
|
|
193
212
|
}
|
|
194
213
|
}
|
|
195
214
|
}
|
|
@@ -56,13 +56,17 @@ module PhlexKit
|
|
|
56
56
|
data[:phlex_kit__toast_on_dismiss_value] = @on_dismiss if @on_dismiss
|
|
57
57
|
data[:phlex_kit__toast_on_auto_close_value] = @on_auto_close if @on_auto_close
|
|
58
58
|
|
|
59
|
-
attrs = {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
attrs = { class: "pk-toast", data: data }
|
|
60
|
+
# Defaults only when the caller didn't supply their own — `mix` would
|
|
61
|
+
# fuse role="status log" / tabindex="0 -1" / aria-atomic="true false"
|
|
62
|
+
# instead of overriding.
|
|
63
|
+
unless @attrs.key?(:role) || @attrs.key?("role")
|
|
64
|
+
attrs[:role] = ALERT_VARIANTS.include?(@variant) ? "alert" : "status"
|
|
65
|
+
end
|
|
66
|
+
attrs[:tabindex] = "0" unless @attrs.key?(:tabindex) || @attrs.key?("tabindex")
|
|
67
|
+
unless aria_key_set?(:atomic)
|
|
68
|
+
attrs[:aria] = { atomic: "true" }
|
|
69
|
+
end
|
|
66
70
|
attrs[:id] = @id if @id
|
|
67
71
|
attrs
|
|
68
72
|
end
|
|
@@ -10,6 +10,12 @@ module PhlexKit
|
|
|
10
10
|
class ToastRegion < BaseComponent
|
|
11
11
|
SKELETON_VARIANTS = %i[default success error warning info loading].freeze
|
|
12
12
|
|
|
13
|
+
# Sentinel default for dir: — distinguishes "caller didn't pass dir:" (omit
|
|
14
|
+
# the attribute, inherit from the page) from an explicit dir: :ltr (must
|
|
15
|
+
# render dir="ltr"), which a plain `:ltr` default can't tell apart.
|
|
16
|
+
DIR_UNSET = Object.new.freeze
|
|
17
|
+
private_constant :DIR_UNSET
|
|
18
|
+
|
|
13
19
|
POSITIONS = {
|
|
14
20
|
top_left: "top-left",
|
|
15
21
|
top_center: "top-center",
|
|
@@ -30,7 +36,7 @@ module PhlexKit
|
|
|
30
36
|
rich_colors: false,
|
|
31
37
|
close_button: false,
|
|
32
38
|
hotkey: %w[alt t],
|
|
33
|
-
dir:
|
|
39
|
+
dir: DIR_UNSET,
|
|
34
40
|
flash: nil,
|
|
35
41
|
id: "pk-toaster",
|
|
36
42
|
**attrs
|
|
@@ -41,11 +47,23 @@ module PhlexKit
|
|
|
41
47
|
@duration = duration
|
|
42
48
|
@gap = gap
|
|
43
49
|
@offset = offset
|
|
50
|
+
# theme:/rich_colors: are Sonner API surface the kit deliberately does
|
|
51
|
+
# not implement (tokens are :root-scoped; there is no status palette) —
|
|
52
|
+
# fail loud rather than silently no-op (audit round 6).
|
|
44
53
|
@theme = theme.to_sym
|
|
45
|
-
@
|
|
54
|
+
unless @theme == :system
|
|
55
|
+
raise ArgumentError, "ToastRegion theme: only :system is supported — toasts follow the page theme (:root[data-theme])"
|
|
56
|
+
end
|
|
57
|
+
if rich_colors
|
|
58
|
+
raise ArgumentError, "ToastRegion rich_colors: is not supported — the kit has no status color palette (toasts follow --pk-* tokens)"
|
|
59
|
+
end
|
|
46
60
|
@close_button = close_button
|
|
47
61
|
@hotkey = hotkey
|
|
48
|
-
@
|
|
62
|
+
@dir_explicit = !dir.equal?(DIR_UNSET)
|
|
63
|
+
@dir = (@dir_explicit ? dir : :ltr).to_sym
|
|
64
|
+
unless %i[ltr rtl auto].include?(@dir)
|
|
65
|
+
raise ArgumentError, "ToastRegion dir: must be :ltr, :rtl or :auto"
|
|
66
|
+
end
|
|
49
67
|
@flash = flash
|
|
50
68
|
# Base id for the <ol> stack; the wrapper takes "#{id}-region". Defaults
|
|
51
69
|
# keep the historical "pk-toaster"/"pk-toaster-region" pair — pass a
|
|
@@ -105,10 +123,12 @@ module PhlexKit
|
|
|
105
123
|
end
|
|
106
124
|
|
|
107
125
|
def region_attrs
|
|
108
|
-
{
|
|
126
|
+
base = {
|
|
109
127
|
id: "#{@id}-region",
|
|
110
128
|
role: "region",
|
|
111
129
|
class: "pk-toast-region",
|
|
130
|
+
# offset: viewport-edge padding, consumed by toast.css.
|
|
131
|
+
style: "--pk-toast-offset: #{@offset.is_a?(Numeric) ? "#{@offset}px" : @offset};",
|
|
112
132
|
# No aria-live here: each toast item is its own live region
|
|
113
133
|
# (role="status"/"alert"); a live wrapper would double-announce.
|
|
114
134
|
aria: { label: "Notifications" },
|
|
@@ -122,14 +142,13 @@ module PhlexKit
|
|
|
122
142
|
phlex_kit__toaster_max_value: @max.to_s,
|
|
123
143
|
phlex_kit__toaster_duration_value: @duration.to_s,
|
|
124
144
|
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
|
|
145
|
+
phlex_kit__toaster_hotkey_value: Array(@hotkey).join("+")
|
|
131
146
|
}
|
|
132
147
|
}
|
|
148
|
+
# Only an explicit dir: renders the attribute — the true default (no
|
|
149
|
+
# dir: passed) omits it so the toast inherits the page's direction.
|
|
150
|
+
base[:dir] = @dir.to_s if @dir_explicit
|
|
151
|
+
base
|
|
133
152
|
end
|
|
134
153
|
end
|
|
135
154
|
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
|
|
@@ -174,6 +172,7 @@ export default class extends Controller {
|
|
|
174
172
|
)
|
|
175
173
|
return
|
|
176
174
|
}
|
|
175
|
+
if (!this._listEl) return
|
|
177
176
|
const el = this._listEl.querySelector(`#${CSS.escape(id)}`)
|
|
178
177
|
if (el) el.dispatchEvent(new CustomEvent("phlex-kit:toast:force-dismiss", { bubbles: true }))
|
|
179
178
|
}
|
|
@@ -192,7 +191,10 @@ export default class extends Controller {
|
|
|
192
191
|
// Mirror the pause state onto the DOM so toasts spawned while the stack
|
|
193
192
|
// is already hovered can arm paused (the pause event predates them).
|
|
194
193
|
this._listEl?.toggleAttribute("data-pk-toasts-paused", value)
|
|
195
|
-
|
|
194
|
+
// Region-scoped, not document-global: on multi-region pages one region's
|
|
195
|
+
// hover must not pause (or worse, RESUME under the pointer) another's
|
|
196
|
+
// toasts. Toasts listen on their own list element.
|
|
197
|
+
this._listEl.dispatchEvent(new CustomEvent(value ? "phlex-kit:toast:pause" : "phlex-kit:toast:resume"))
|
|
196
198
|
}
|
|
197
199
|
|
|
198
200
|
_setExpanded(value) {
|
|
@@ -277,7 +279,14 @@ export default class extends Controller {
|
|
|
277
279
|
const wantCtrl = parts.includes("ctrl")
|
|
278
280
|
const wantMeta = parts.includes("meta")
|
|
279
281
|
const wantShift = parts.includes("shift")
|
|
280
|
-
|
|
282
|
+
// Match e.code as well as e.key: with Option held, macOS reports the
|
|
283
|
+
// COMPOSED character (Option+T → "†"), so alt+ hotkeys never match on
|
|
284
|
+
// e.key alone (Sonner matches event.code for the same reason).
|
|
285
|
+
const code = (e.code || "").toLowerCase()
|
|
286
|
+
const keyMatches = e.key.toLowerCase() === key.toLowerCase()
|
|
287
|
+
|| code === `key${key.toLowerCase()}`
|
|
288
|
+
|| code === `digit${key.toLowerCase()}`
|
|
289
|
+
if (!keyMatches) return
|
|
281
290
|
if (wantAlt !== e.altKey) return
|
|
282
291
|
if (wantCtrl !== e.ctrlKey) return
|
|
283
292
|
if (wantMeta !== e.metaKey) return
|
|
@@ -321,6 +330,7 @@ export default class extends Controller {
|
|
|
321
330
|
}
|
|
322
331
|
|
|
323
332
|
_mutate(id, variant, text) {
|
|
333
|
+
if (!this._listEl) return
|
|
324
334
|
const el = this._listEl.querySelector(`#${CSS.escape(id)}`)
|
|
325
335
|
if (!el) return
|
|
326
336
|
el.dataset.variant = variant
|
|
@@ -21,20 +21,35 @@ 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
|
|
28
28
|
|
|
29
29
|
def item_context
|
|
30
30
|
{ type: @type, variant: @variant, size: @size, disabled: @disabled,
|
|
31
|
-
selected_values: selected_values, spacing: @spacing, orientation: @orientation
|
|
31
|
+
selected_values: selected_values, spacing: @spacing, orientation: @orientation,
|
|
32
|
+
group: self }
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
def ToggleGroupItem(**kwargs, &block)
|
|
35
36
|
render PhlexKit::ToggleGroupItem.new(group_context: item_context, **kwargs), &block
|
|
36
37
|
end
|
|
37
38
|
|
|
39
|
+
# Roving-tabindex initial stop (single-type, nothing selected): the FIRST
|
|
40
|
+
# enabled item claims tabindex="0" server-side, mirroring what
|
|
41
|
+
# toggle_group_controller.js's reconcile() computes at runtime
|
|
42
|
+
# (`pressed || enabledItems()[0]`). Items call this once each, in render
|
|
43
|
+
# order, as they're constructed — a single-threaded sequential claim, not
|
|
44
|
+
# a lookahead over siblings not yet rendered.
|
|
45
|
+
def claim_tab_stop(item_disabled)
|
|
46
|
+
return false unless selected_values.empty?
|
|
47
|
+
return false if item_disabled
|
|
48
|
+
return false if @tab_stop_claimed
|
|
49
|
+
@tab_stop_claimed = true
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
|
|
38
53
|
private
|
|
39
54
|
|
|
40
55
|
def selected_values
|
|
@@ -33,23 +33,30 @@ export default class extends Controller {
|
|
|
33
33
|
if (items.length === 0) return
|
|
34
34
|
const currentIndex = items.indexOf(event.currentTarget)
|
|
35
35
|
let nextIndex = currentIndex
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
// In RTL the horizontal arrows mirror (physical LEFT = next); Up/Down are
|
|
37
|
+
// unaffected. Runtime dir check is reliable after a dynamic flip.
|
|
38
|
+
const rtl = getComputedStyle(this.element).direction === "rtl"
|
|
39
|
+
const fwd = rtl ? "ArrowLeft" : "ArrowRight"
|
|
40
|
+
const back = rtl ? "ArrowRight" : "ArrowLeft"
|
|
41
|
+
if (event.key === fwd || event.key === "ArrowDown") nextIndex = (currentIndex + 1) % items.length
|
|
42
|
+
else if (event.key === back || event.key === "ArrowUp") nextIndex = (currentIndex - 1 + items.length) % items.length
|
|
43
|
+
else if (event.key === "Home") nextIndex = 0
|
|
44
|
+
else if (event.key === "End") nextIndex = items.length - 1
|
|
45
|
+
else if (event.key === " " || event.key === "Enter") { event.preventDefault(); event.currentTarget.click(); return }
|
|
46
|
+
else return
|
|
44
47
|
event.preventDefault()
|
|
45
48
|
const next = items[nextIndex]
|
|
46
49
|
this.updateRovingTabindex(next)
|
|
47
50
|
next.focus()
|
|
51
|
+
// APG radio-group keyboard model: with role="radio", arrows move focus
|
|
52
|
+
// AND check the landed-on item (Home/End included).
|
|
53
|
+
if (next !== event.currentTarget) next.click()
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
reconcile() {
|
|
51
57
|
if (this.typeValue === "single") {
|
|
52
|
-
|
|
58
|
+
// A disabled button can't take focus — never hand it the tab stop.
|
|
59
|
+
const pressed = this.itemTargets.find((el) => this.isPressed(el) && !el.disabled)
|
|
53
60
|
const first = pressed || this.enabledItems()[0]
|
|
54
61
|
this.itemTargets.forEach((el) => el.setAttribute("tabindex", el === first ? "0" : "-1"))
|
|
55
62
|
} else {
|
|
@@ -1,13 +1,27 @@
|
|
|
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, or that the group
|
|
5
|
+
# derives itself (pressed: comes from the group's selected_values) —
|
|
6
|
+
# inheriting them silently discarded the caller's input.
|
|
7
|
+
unsupported = attrs.keys & %i[wrapper name unpressed_value pressed]
|
|
8
|
+
if unsupported.any?
|
|
9
|
+
raise ArgumentError, "ToggleGroupItem does not support #{unsupported.join(", ")} (group items render a bare button)"
|
|
10
|
+
end
|
|
4
11
|
@item_value = value.to_s
|
|
5
12
|
@group_context = group_context
|
|
6
13
|
pressed = group_context[:selected_values].include?(@item_value)
|
|
14
|
+
item_disabled = attrs.key?(:disabled) ? attrs.delete(:disabled) : group_context[:disabled]
|
|
15
|
+
# Roving tabindex initial stop: when nothing is selected yet, the first
|
|
16
|
+
# enabled item claims tabindex="0" (see ToggleGroup#claim_tab_stop).
|
|
17
|
+
# group_context[:group] is nil for a hand-built context (bypassing
|
|
18
|
+
# ToggleGroup#item_context) — treat that as "no tab-stop claim" rather
|
|
19
|
+
# than raising, since standalone tests construct contexts by hand.
|
|
20
|
+
@is_first_tab_stop = group_context[:group]&.claim_tab_stop(item_disabled) || false
|
|
7
21
|
super(pressed: pressed, name: nil, value: @item_value,
|
|
8
22
|
variant: variant || group_context[:variant],
|
|
9
23
|
size: size || group_context[:size],
|
|
10
|
-
disabled:
|
|
24
|
+
disabled: item_disabled, **attrs)
|
|
11
25
|
end
|
|
12
26
|
|
|
13
27
|
def view_template(&block)
|
|
@@ -26,7 +40,11 @@ module PhlexKit
|
|
|
26
40
|
if @group_context[:type] == :single
|
|
27
41
|
a[:role] = "radio"
|
|
28
42
|
a[:aria] = { checked: @pressed.to_s }
|
|
29
|
-
|
|
43
|
+
# A disabled button can't take focus — giving the tab stop to a
|
|
44
|
+
# pressed-but-disabled item made the whole group Tab-unreachable.
|
|
45
|
+
# When nothing is selected, the first enabled item claims the initial
|
|
46
|
+
# roving-tabindex stop instead of every item defaulting to -1.
|
|
47
|
+
a[:tabindex] = (@pressed && !@disabled) || @is_first_tab_stop ? "0" : "-1"
|
|
30
48
|
else
|
|
31
49
|
a[:aria] = { pressed: @pressed.to_s }
|
|
32
50
|
a[:tabindex] = "0"
|
|
@@ -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,6 +8,9 @@ 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)`
|
|
13
16
|
# (a nil default used to render h1 with the level-6 size — self-disagreeing).
|
|
@@ -18,7 +21,10 @@ module PhlexKit
|
|
|
18
21
|
raise ArgumentError, "Invalid heading level: #{level.inspect} — valid: 1–6"
|
|
19
22
|
end
|
|
20
23
|
@level = level
|
|
21
|
-
@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
|
|
22
28
|
@size = size
|
|
23
29
|
@attrs = attrs
|
|
24
30
|
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
|