phlex_kit 0.8.0 → 0.9.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 +11 -0
- data/app/components/phlex_kit/accordion/accordion.css +2 -2
- data/app/components/phlex_kit/accordion/accordion_content.rb +11 -2
- data/app/components/phlex_kit/accordion/accordion_controller.js +23 -4
- 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 +11 -4
- data/app/components/phlex_kit/alert_dialog/alert_dialog_controller.js +123 -6
- data/app/components/phlex_kit/aspect_ratio/aspect_ratio.rb +10 -4
- 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 +12 -1
- data/app/components/phlex_kit/calendar/calendar_controller.js +151 -12
- 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 +37 -10
- 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/clipboard/clipboard_popover.rb +3 -1
- 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 +67 -11
- 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_checkbox_item.rb +6 -2
- data/app/components/phlex_kit/context_menu/context_menu_controller.js +69 -2
- data/app/components/phlex_kit/context_menu/context_menu_radio_item.rb +6 -2
- data/app/components/phlex_kit/context_menu/context_menu_sub_trigger.rb +5 -2
- data/app/components/phlex_kit/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 +6 -5
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu.rb +5 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_checkbox_item.rb +6 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_content.rb +3 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_controller.js +99 -45
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_label.rb +3 -1
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_radio_item.rb +6 -2
- data/app/components/phlex_kit/dropdown_menu/dropdown_menu_sub_trigger.rb +5 -2
- data/app/components/phlex_kit/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 +44 -12
- data/app/components/phlex_kit/form_field/form_field_error.rb +8 -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 +43 -4
- 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.rb +1 -1
- data/app/components/phlex_kit/masked_input/masked_input_controller.js +34 -9
- 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 +119 -12
- 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 +7 -2
- 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 +11 -3
- data/app/components/phlex_kit/navigation_menu/navigation_menu.rb +6 -1
- data/app/components/phlex_kit/navigation_menu/navigation_menu_trigger.rb +1 -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 +78 -3
- data/app/components/phlex_kit/resizable/resizable_handle.rb +3 -1
- 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 +11 -7
- data/app/components/phlex_kit/select/select_controller.js +35 -4
- data/app/components/phlex_kit/select/select_item.rb +5 -2
- 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 +27 -15
- 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 +3 -3
- 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 +11 -0
- data/app/components/phlex_kit/theme_toggle/theme_toggle_controller.js +49 -7
- 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 +19 -5
- data/app/components/phlex_kit/toast/toaster_controller.js +20 -6
- data/app/components/phlex_kit/toggle/toggle.css +4 -4
- data/app/components/phlex_kit/toggle/toggle.rb +9 -3
- 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/app/javascript/phlex_kit/controllers/index.js +0 -2
- data/lib/phlex_kit/version.rb +1 -1
- metadata +1 -2
- data/app/components/phlex_kit/select/select_item_controller.js +0 -14
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f52a630c21854a2d2823951ef321a7749ff01adf9d5f60b0c3483aa635407aaa
|
|
4
|
+
data.tar.gz: c0a594263a95f57bf56b20955054369cc251d58fb6ecbc2bd22b45aa3b4dd278
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5c1b054a1ce8c3c1f01e4530dccdda7e23b013e2e0b3ccd966959d9419b784c462a2da725c9eefbb78b2d71675edc2d553de42745675c768c5621b31c0a41685
|
|
7
|
+
data.tar.gz: ee5a88c2c3615582fd51ca3b75d9574aba7dfedadca8e1fe2879370e052b311f2904a6ca3469ceba6fd256392becec583d3ae14c0ccb8df6c6196ab2b7cddd17
|
|
@@ -140,6 +140,13 @@
|
|
|
140
140
|
--pk-chart-3: #2563eb;
|
|
141
141
|
--pk-chart-4: #1d4ed8;
|
|
142
142
|
--pk-chart-5: #1e40af;
|
|
143
|
+
/* Shadow/overlay base colors — bare colors; each rule applies its own alpha
|
|
144
|
+
via color-mix(in srgb, var(--pk-*) N%, transparent). shadcn/ui keeps
|
|
145
|
+
shadows black in both modes (tailwind's default shadow color), so these
|
|
146
|
+
are identical in every theme block; restated in the light blocks per the
|
|
147
|
+
cascade-trap convention anyway. */
|
|
148
|
+
--pk-shadow-color: #000000;
|
|
149
|
+
--pk-overlay: #000000;
|
|
143
150
|
--pk-radius: 0.625rem;
|
|
144
151
|
--pk-font-sans: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
145
152
|
--pk-font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
|
@@ -163,6 +170,8 @@
|
|
|
163
170
|
--pk-green: #16a34a;
|
|
164
171
|
--pk-amber: #d97706;
|
|
165
172
|
--pk-red: #e40014;
|
|
173
|
+
--pk-shadow-color: #000000;
|
|
174
|
+
--pk-overlay: #000000;
|
|
166
175
|
color-scheme: light;
|
|
167
176
|
}
|
|
168
177
|
|
|
@@ -183,6 +192,8 @@
|
|
|
183
192
|
--pk-green: #16a34a;
|
|
184
193
|
--pk-amber: #d97706;
|
|
185
194
|
--pk-red: #e40014;
|
|
195
|
+
--pk-shadow-color: #000000;
|
|
196
|
+
--pk-overlay: #000000;
|
|
186
197
|
color-scheme: light;
|
|
187
198
|
}
|
|
188
199
|
}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
.pk-accordion-heading { margin: 0; display: flex; font-size: inherit; font-weight: inherit; line-height: inherit; }
|
|
6
6
|
.pk-accordion-trigger {
|
|
7
7
|
width: 100%; display: flex; flex: 1; align-items: center; justify-content: space-between;
|
|
8
|
-
padding-block: 1rem; font-size: .875rem; font-weight: 500; text-align:
|
|
9
|
-
background: none; border: 0; cursor: pointer; color: inherit; transition:
|
|
8
|
+
padding-block: 1rem; font-size: .875rem; font-weight: 500; text-align: start;
|
|
9
|
+
background: none; border: 0; cursor: pointer; color: inherit; transition: color .15s ease, background-color .15s ease, text-decoration-color .15s ease;
|
|
10
10
|
}
|
|
11
11
|
.pk-accordion-trigger:hover { text-decoration: underline; }
|
|
12
12
|
.pk-accordion-trigger-label { flex: 1; }
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
class AccordionContent < BaseComponent
|
|
3
|
-
|
|
3
|
+
# Pass `open: true` alongside AccordionItem(open: true) so the content is
|
|
4
|
+
# readable without JS — the closed-state hidden + height:0 otherwise makes
|
|
5
|
+
# a server-rendered open item unreachable pre-hydration.
|
|
6
|
+
def initialize(open: false, **attrs)
|
|
7
|
+
@open = open
|
|
8
|
+
@attrs = attrs
|
|
9
|
+
end
|
|
10
|
+
|
|
4
11
|
def view_template(&)
|
|
5
12
|
# role="region" + aria-labelledby → trigger id (wired by the controller
|
|
6
13
|
# in connect(), which also assigns the ids), per the APG accordion pattern.
|
|
7
|
-
|
|
14
|
+
base = { class: "pk-accordion-content", role: "region", data: { phlex_kit__accordion_target: "content", state: @open ? "open" : "closed" } }
|
|
15
|
+
base.merge!(style: "height: 0px;", hidden: true) unless @open
|
|
16
|
+
div(**mix(base, @attrs), &)
|
|
8
17
|
end
|
|
9
18
|
end
|
|
10
19
|
end
|
|
@@ -15,8 +15,11 @@ export default class extends Controller {
|
|
|
15
15
|
|
|
16
16
|
connect() {
|
|
17
17
|
this.wireAria()
|
|
18
|
+
// Apply the initial state without animating (the initial valueChanged
|
|
19
|
+
// callback is skipped — it fires before connect()).
|
|
18
20
|
const d = this.animationDurationValue
|
|
19
21
|
this.animationDurationValue = 0
|
|
22
|
+
if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", this.openValue ? "true" : "false")
|
|
20
23
|
this.openValue ? this.open() : this.close()
|
|
21
24
|
this.animationDurationValue = d
|
|
22
25
|
}
|
|
@@ -34,11 +37,18 @@ export default class extends Controller {
|
|
|
34
37
|
if (!root || root.dataset.type !== "single") return
|
|
35
38
|
root.querySelectorAll('[data-controller~="phlex-kit--accordion"]').forEach((el) => {
|
|
36
39
|
if (el === this.element) return
|
|
40
|
+
// Skip items of a NESTED accordion — querySelectorAll descends into
|
|
41
|
+
// item content, and opening an outer item collapsed inner ones too.
|
|
42
|
+
if (el.closest(".pk-accordion") !== root) return
|
|
37
43
|
const ctrl = this.application.getControllerForElementAndIdentifier(el, "phlex-kit--accordion")
|
|
38
44
|
if (ctrl && ctrl.openValue) ctrl.openValue = false
|
|
39
45
|
})
|
|
40
46
|
}
|
|
41
|
-
openValueChanged(isOpen) {
|
|
47
|
+
openValueChanged(isOpen, wasOpen) {
|
|
48
|
+
// Stimulus fires the initial value callback BEFORE connect() — before
|
|
49
|
+
// connect zeroes the duration — so a server-rendered open item visibly
|
|
50
|
+
// animated open on every page load. connect() applies the initial state.
|
|
51
|
+
if (wasOpen === undefined) return
|
|
42
52
|
if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", isOpen ? "true" : "false")
|
|
43
53
|
isOpen ? this.open() : this.close()
|
|
44
54
|
}
|
|
@@ -73,14 +83,23 @@ export default class extends Controller {
|
|
|
73
83
|
if (this.hasIconTarget) this.rotate()
|
|
74
84
|
}
|
|
75
85
|
|
|
86
|
+
// Honor prefers-reduced-motion: collapse the grow/shrink to a jump-cut
|
|
87
|
+
// (duration 0 keeps the finished-promise bookkeeping intact).
|
|
88
|
+
get effectiveDuration() {
|
|
89
|
+
return matchMedia("(prefers-reduced-motion: reduce)").matches ? 0 : this.animationDurationValue
|
|
90
|
+
}
|
|
91
|
+
|
|
76
92
|
reveal() {
|
|
77
93
|
const c = this.contentTarget
|
|
78
94
|
c.removeAttribute("hidden")
|
|
79
95
|
c.dataset.state = "open"
|
|
80
96
|
const h = c.scrollHeight
|
|
81
|
-
c.animate([{ height: c.style.height || "0px" }, { height: `${h}px` }],
|
|
82
|
-
{ duration: this.
|
|
97
|
+
const a = c.animate([{ height: c.style.height || "0px" }, { height: `${h}px` }],
|
|
98
|
+
{ duration: this.effectiveDuration, easing: "ease-in-out" })
|
|
83
99
|
c.style.height = `${h}px`
|
|
100
|
+
// Settle at auto, not the frozen pixel height — with overflow hidden a
|
|
101
|
+
// later content growth (or a resize that rewraps text) silently clipped.
|
|
102
|
+
a.finished.then(() => { if (c.dataset.state === "open") c.style.height = "auto" }).catch(() => {})
|
|
84
103
|
}
|
|
85
104
|
|
|
86
105
|
hide() {
|
|
@@ -88,7 +107,7 @@ export default class extends Controller {
|
|
|
88
107
|
c.dataset.state = "closed"
|
|
89
108
|
const from = c.scrollHeight
|
|
90
109
|
const a = c.animate([{ height: `${from}px` }, { height: "0px" }],
|
|
91
|
-
{ duration: this.
|
|
110
|
+
{ duration: this.effectiveDuration, easing: "ease-in-out" })
|
|
92
111
|
c.style.height = "0px"
|
|
93
112
|
a.finished.then(() => { if (c.dataset.state === "closed") c.setAttribute("hidden", "") }).catch(() => {})
|
|
94
113
|
}
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
padding: .5rem .625rem;
|
|
15
15
|
font-size: .875rem;
|
|
16
16
|
line-height: 1.25rem;
|
|
17
|
-
text-align:
|
|
17
|
+
text-align: start;
|
|
18
18
|
background: var(--pk-surface);
|
|
19
19
|
color: var(--pk-text);
|
|
20
20
|
}
|
|
21
21
|
.pk-alert:has(> .pk-alert-action) {
|
|
22
22
|
position: relative;
|
|
23
|
-
padding-
|
|
23
|
+
padding-inline-end: 4.5rem;
|
|
24
24
|
}
|
|
25
25
|
.pk-alert:has(> svg) {
|
|
26
26
|
grid-template-columns: auto 1fr;
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
.pk-alert-action {
|
|
67
67
|
position: absolute;
|
|
68
68
|
top: .5rem;
|
|
69
|
-
|
|
69
|
+
inset-inline-end: .5rem;
|
|
70
70
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
position: fixed;
|
|
9
9
|
inset: 0;
|
|
10
10
|
z-index: 50;
|
|
11
|
-
background:
|
|
11
|
+
background: color-mix(in srgb, var(--pk-overlay) 10%, transparent);
|
|
12
12
|
backdrop-filter: blur(4px);
|
|
13
13
|
}
|
|
14
14
|
.pk-alert-dialog-panel {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
color: var(--pk-text);
|
|
31
31
|
--pk-dialog-spacing: 1.5rem;
|
|
32
32
|
padding: var(--pk-dialog-spacing);
|
|
33
|
-
box-shadow: 0 10px 40px
|
|
33
|
+
box-shadow: 0 10px 40px color-mix(in srgb, var(--pk-shadow-color) 50%, transparent);
|
|
34
34
|
}
|
|
35
35
|
.pk-alert-dialog-panel.sm { max-width: 24rem; --pk-dialog-spacing: 1rem; gap: .75rem; }
|
|
36
36
|
.pk-alert-dialog-panel.sm .pk-alert-dialog-title { font-size: 1rem; }
|
|
@@ -77,5 +77,5 @@
|
|
|
77
77
|
background: color-mix(in oklab, var(--pk-surface-2) 50%, transparent);
|
|
78
78
|
}
|
|
79
79
|
@supports not (backdrop-filter: blur(1px)) {
|
|
80
|
-
.pk-alert-dialog-overlay { background:
|
|
80
|
+
.pk-alert-dialog-overlay { background: color-mix(in srgb, var(--pk-overlay) 50%, transparent); }
|
|
81
81
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
# The modal body, held in a <template> and cloned into <body> on open. The
|
|
3
3
|
# cloned div carries its own `phlex-kit--alert-dialog` controller so Cancel
|
|
4
|
-
# (#dismiss) can remove it. Holds Header + Footer.
|
|
4
|
+
# (#dismiss) can remove it. Holds Header + Footer. Caller **attrs land on the
|
|
5
|
+
# visible panel (the role="alertdialog" div), not the inert <template>.
|
|
6
|
+
# See alert_dialog.rb.
|
|
5
7
|
class AlertDialogContent < BaseComponent
|
|
6
8
|
# size => panel modifier; :sm is shadcn's compact variant.
|
|
7
9
|
SIZES = { default: nil, sm: "sm" }.freeze
|
|
@@ -12,10 +14,15 @@ module PhlexKit
|
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def view_template(&block)
|
|
15
|
-
template(
|
|
17
|
+
template(data: { phlex_kit__alert_dialog_target: "content" }) do
|
|
18
|
+
# The clone's controller listens for keydown on `document` while open
|
|
19
|
+
# (an element-scoped listener dies the moment focus escapes to <body>);
|
|
20
|
+
# mousedown on the overlay is prevented so a stray click can't move
|
|
21
|
+
# focus out of the trap. tabindex="-1" on the panel is the focus
|
|
22
|
+
# fallback when the dialog has no focusable children.
|
|
16
23
|
div(data: { controller: "phlex-kit--alert-dialog" }) do
|
|
17
|
-
div(class: "pk-alert-dialog-overlay", "aria-hidden": "true")
|
|
18
|
-
div(role: "alertdialog", "aria-modal": "true", class: [ "pk-alert-dialog-panel", fetch_option(SIZES, @size, :size) ].compact.join(" "), &block)
|
|
24
|
+
div(class: "pk-alert-dialog-overlay", "aria-hidden": "true", data: { action: "mousedown->phlex-kit--alert-dialog#overlayMousedown" })
|
|
25
|
+
div(**mix({ role: "alertdialog", "aria-modal": "true", tabindex: "-1", class: [ "pk-alert-dialog-panel", fetch_option(SIZES, @size, :size) ].compact.join(" ") }, @attrs), &block)
|
|
19
26
|
end
|
|
20
27
|
end
|
|
21
28
|
end
|
|
@@ -1,24 +1,141 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus";
|
|
2
2
|
|
|
3
3
|
// Ported from ruby_ui's phlex-kit--alert-dialog controller. open() clones the
|
|
4
|
-
// <template> content into <body> as a modal
|
|
5
|
-
// of this controller
|
|
6
|
-
//
|
|
4
|
+
// <template> content into <body> as a modal; the clone carries its own
|
|
5
|
+
// instance of this controller, which owns the full modal contract for the
|
|
6
|
+
// clone's lifetime (mirroring sheet_content_controller): scroll lock, initial
|
|
7
|
+
// focus (the Cancel action when present — APG wants the least destructive
|
|
8
|
+
// control), Tab focus trap, Escape-to-dismiss, focus restore to the opener,
|
|
9
|
+
// background inert, and aria-labelledby/-describedby wiring to
|
|
10
|
+
// Title/Description. Escape/Tab are handled by a document-level keydown
|
|
11
|
+
// listener (an element-scoped one dies the moment focus escapes to <body>);
|
|
12
|
+
// the overlay's mousedown guard keeps focus from leaving the trap. connect and
|
|
13
|
+
// disconnect exactly bracket the clone, so removal by any path (Cancel,
|
|
14
|
+
// Escape, turbo:before-cache) restores state.
|
|
15
|
+
const FOCUSABLE =
|
|
16
|
+
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
17
|
+
|
|
7
18
|
export default class extends Controller {
|
|
8
19
|
static targets = ["content"];
|
|
9
20
|
static values = { open: { type: Boolean, default: false } };
|
|
10
21
|
|
|
11
22
|
connect() {
|
|
12
|
-
|
|
23
|
+
// The source element holds the <template>; the clone in <body> does not.
|
|
24
|
+
if (this.hasContentTarget) {
|
|
25
|
+
if (this.openValue) this.open();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
this.opener = document.activeElement;
|
|
29
|
+
this.previousOverflow = document.body.style.overflow;
|
|
30
|
+
document.body.style.overflow = "hidden";
|
|
31
|
+
this.onKeydown = (event) => this.keydown(event);
|
|
32
|
+
this.beforeCache = () => this.element.remove();
|
|
33
|
+
document.addEventListener("keydown", this.onKeydown);
|
|
34
|
+
document.addEventListener("turbo:before-cache", this.beforeCache);
|
|
35
|
+
this.#inertOthers();
|
|
36
|
+
this.#wireAria();
|
|
37
|
+
const focusables = this.#focusables();
|
|
38
|
+
const cancel = focusables.find((el) => (el.dataset.action || "").includes("#dismiss"));
|
|
39
|
+
(cancel || focusables[0] || this.#panel())?.focus();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
disconnect() {
|
|
43
|
+
if (this.hasContentTarget) return;
|
|
44
|
+
document.removeEventListener("keydown", this.onKeydown);
|
|
45
|
+
document.removeEventListener("turbo:before-cache", this.beforeCache);
|
|
46
|
+
this.#restoreInert();
|
|
47
|
+
document.body.style.overflow = this.previousOverflow;
|
|
48
|
+
if (this.opener?.isConnected) this.opener.focus();
|
|
13
49
|
}
|
|
14
50
|
|
|
15
51
|
open() {
|
|
52
|
+
// Double-open guard: Enter+click (or a double-click) on the trigger must
|
|
53
|
+
// not stack a second clone over the first.
|
|
54
|
+
if (this.clone?.isConnected) return;
|
|
16
55
|
document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML);
|
|
17
|
-
document.body.
|
|
56
|
+
this.clone = document.body.lastElementChild;
|
|
18
57
|
}
|
|
19
58
|
|
|
20
59
|
dismiss() {
|
|
21
|
-
document.body.style.overflow = "";
|
|
22
60
|
this.element.remove();
|
|
23
61
|
}
|
|
62
|
+
|
|
63
|
+
// Overlay mousedown would move focus to <body>, killing an element-scoped
|
|
64
|
+
// trap; prevented here so focus stays inside the panel (the overlay is
|
|
65
|
+
// deliberately NOT a dismiss surface on an alert dialog).
|
|
66
|
+
overlayMousedown(event) {
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
keydown(event) {
|
|
71
|
+
if (event.key === "Escape") {
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
this.dismiss();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (event.key !== "Tab") return;
|
|
77
|
+
const focusables = this.#focusables();
|
|
78
|
+
if (focusables.length === 0) {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
this.#panel()?.focus();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Focus somehow left the dialog (e.g. programmatically): pull it back in.
|
|
84
|
+
if (!this.element.contains(document.activeElement)) {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
focusables[0].focus();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const first = focusables[0];
|
|
90
|
+
const last = focusables[focusables.length - 1];
|
|
91
|
+
if (event.shiftKey && (document.activeElement === first || document.activeElement === this.#panel())) {
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
last.focus();
|
|
94
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
first.focus();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#panel() {
|
|
101
|
+
return this.element.querySelector('[role="alertdialog"]');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#focusables() {
|
|
105
|
+
const panel = this.#panel();
|
|
106
|
+
if (!panel) return [];
|
|
107
|
+
return [...panel.querySelectorAll(FOCUSABLE)].filter((el) => el.getClientRects().length > 0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Make everything behind the modal inert (the clone is <body>'s last child,
|
|
111
|
+
// so its siblings are the whole page). Prior inert state is saved per
|
|
112
|
+
// element and restored on disconnect.
|
|
113
|
+
#inertOthers() {
|
|
114
|
+
this.inerted = [];
|
|
115
|
+
for (const el of document.body.children) {
|
|
116
|
+
if (el === this.element) continue;
|
|
117
|
+
if (["SCRIPT", "STYLE", "LINK", "TEMPLATE"].includes(el.tagName)) continue;
|
|
118
|
+
this.inerted.push([el, el.inert]);
|
|
119
|
+
el.inert = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#restoreInert() {
|
|
124
|
+
for (const [el, wasInert] of this.inerted || []) el.inert = wasInert;
|
|
125
|
+
this.inerted = null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#wireAria() {
|
|
129
|
+
const panel = this.#panel();
|
|
130
|
+
if (!panel) return;
|
|
131
|
+
const wire = (attr, selector) => {
|
|
132
|
+
if (panel.getAttribute(attr)) return;
|
|
133
|
+
const el = this.element.querySelector(selector);
|
|
134
|
+
if (!el) return;
|
|
135
|
+
if (!el.id) el.id = `pk-alert-dialog-${Math.random().toString(36).slice(2, 10)}`;
|
|
136
|
+
panel.setAttribute(attr, el.id);
|
|
137
|
+
};
|
|
138
|
+
wire("aria-labelledby", ".pk-alert-dialog-title");
|
|
139
|
+
wire("aria-describedby", ".pk-alert-dialog-description");
|
|
140
|
+
}
|
|
24
141
|
}
|
|
@@ -5,16 +5,22 @@ module PhlexKit
|
|
|
5
5
|
# `.pk-aspect-ratio*` (aspect_ratio.css).
|
|
6
6
|
class AspectRatio < BaseComponent
|
|
7
7
|
def initialize(aspect_ratio: "16/9", **attrs)
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
# Both terms must be positive integers — "16/0" yields Infinity% and
|
|
9
|
+
# "a/b" NaN% padding, silently breaking the layout.
|
|
10
|
+
unless aspect_ratio.is_a?(String) && aspect_ratio.match?(%r{\A[1-9]\d*/[1-9]\d*\z})
|
|
11
|
+
raise ArgumentError, %(aspect_ratio must be a "w/h" string of positive integers (e.g. "16/9"))
|
|
10
12
|
end
|
|
11
13
|
@aspect_ratio = aspect_ratio
|
|
12
14
|
@attrs = attrs
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def view_template(&block)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
# **mix on the ROOT (kit rule) — caller attrs previously landed on the
|
|
19
|
+
# inner div and could never reach the actual root element. The trailing
|
|
20
|
+
# ";" matters: mix joins duplicate string attrs with a space, so a
|
|
21
|
+
# caller style: would otherwise fuse into one invalid declaration.
|
|
22
|
+
div(**mix({ class: "pk-aspect-ratio", style: "padding-bottom: #{padding_bottom}%;" }, @attrs)) do
|
|
23
|
+
div(class: "pk-aspect-ratio-inner", &block)
|
|
18
24
|
end
|
|
19
25
|
end
|
|
20
26
|
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
.pk-attachment.vertical .pk-attachment-actions {
|
|
93
93
|
position: absolute;
|
|
94
94
|
top: .75rem;
|
|
95
|
-
|
|
95
|
+
inset-inline-end: .75rem;
|
|
96
96
|
}
|
|
97
97
|
.pk-attachment.vertical .pk-attachment-action {
|
|
98
98
|
background: color-mix(in oklab, var(--pk-bg) 60%, transparent);
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
|
-
# Small ghost icon button in an Attachment (remove/download).
|
|
3
|
-
#
|
|
2
|
+
# Small ghost icon button in an Attachment (remove/download). Block content
|
|
3
|
+
# is the icon (defaults to ×). `label:` renders a pk-sr-only span so the
|
|
4
|
+
# icon-only button always has an accessible name (default "Remove") —
|
|
5
|
+
# override it per action, or pass `label: nil` when supplying your own
|
|
6
|
+
# aria-label. See attachment.rb.
|
|
4
7
|
class AttachmentAction < BaseComponent
|
|
5
|
-
def initialize(**attrs)
|
|
8
|
+
def initialize(label: "Remove", **attrs)
|
|
9
|
+
@label = label
|
|
6
10
|
@attrs = attrs
|
|
7
11
|
end
|
|
8
12
|
|
|
@@ -13,6 +17,7 @@ module PhlexKit
|
|
|
13
17
|
else
|
|
14
18
|
render Icon.new(:x, size: nil)
|
|
15
19
|
end
|
|
20
|
+
span(class: "pk-sr-only") { @label } if @label
|
|
16
21
|
end
|
|
17
22
|
end
|
|
18
23
|
end
|
|
@@ -10,12 +10,12 @@ module PhlexKit
|
|
|
10
10
|
@attrs = attrs
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
def view_template
|
|
13
|
+
def view_template(&block)
|
|
14
14
|
base = { class: "pk-attachment-trigger" }
|
|
15
15
|
if @as == :a
|
|
16
|
-
a(**mix(base.merge(href: @href), @attrs))
|
|
16
|
+
a(**mix(base.merge(href: @href), @attrs), &block)
|
|
17
17
|
else
|
|
18
|
-
button(**mix(base.merge(type: :button), @attrs))
|
|
18
|
+
button(**mix(base.merge(type: :button), @attrs), &block)
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
end
|
|
@@ -11,11 +11,19 @@ export default class extends Controller {
|
|
|
11
11
|
connect() {
|
|
12
12
|
this._onScroll = () => this.update()
|
|
13
13
|
this.element.addEventListener("scroll", this._onScroll, { passive: true })
|
|
14
|
+
// Content changes (items added/removed, images sizing in) and container
|
|
15
|
+
// resizes shift the overflow edges — without these the masks go stale.
|
|
16
|
+
this._resizeObserver = new ResizeObserver(() => this.update())
|
|
17
|
+
this._resizeObserver.observe(this.element)
|
|
18
|
+
this._mutationObserver = new MutationObserver(() => this.update())
|
|
19
|
+
this._mutationObserver.observe(this.element, { childList: true, subtree: true })
|
|
14
20
|
this.update()
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
disconnect() {
|
|
18
24
|
this.element.removeEventListener("scroll", this._onScroll)
|
|
25
|
+
this._resizeObserver?.disconnect()
|
|
26
|
+
this._mutationObserver?.disconnect()
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
update() {
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
Group / GroupCount). Matched to shadcn/ui's current avatar (nova): the root
|
|
3
3
|
is NOT overflow-clipped (a Badge hangs over the edge) — image and fallback
|
|
4
4
|
round themselves, and a ::after overlay draws the hairline border, blended
|
|
5
|
-
darken (light mode) / lighten (dark mode) so it hugs any photo. The
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
darken (light mode) / lighten (dark mode) so it hugs any photo. The image
|
|
6
|
+
is absolutely stacked over the in-flow fallback (both can coexist without
|
|
7
|
+
squashing pre-hydration / no-JS); the controller then toggles `.pk-hidden`
|
|
8
|
+
between them as progressive enhancement. shadcn ships sm/default/lg;
|
|
9
|
+
xs/xl are kit extras. */
|
|
8
10
|
.pk-avatar {
|
|
9
11
|
position: relative;
|
|
10
12
|
display: inline-flex;
|
|
@@ -33,7 +35,14 @@
|
|
|
33
35
|
.pk-avatar.lg { height: 2.5rem; width: 2.5rem; font-size: 1rem; }
|
|
34
36
|
.pk-avatar.xl { height: 5rem; width: 5rem; font-size: 1.875rem; }
|
|
35
37
|
|
|
38
|
+
/* Stacked over the fallback: pre-hydration both render, and as flex siblings
|
|
39
|
+
they squashed into half-circles. Absolutely positioning the image lets a
|
|
40
|
+
loaded image simply cover the fallback with no JS at all; on 404 the
|
|
41
|
+
fallback stays visible underneath (and the controller removes the broken
|
|
42
|
+
image once it runs). */
|
|
36
43
|
.pk-avatar-image {
|
|
44
|
+
position: absolute;
|
|
45
|
+
inset: 0;
|
|
37
46
|
aspect-ratio: 1 / 1;
|
|
38
47
|
height: 100%;
|
|
39
48
|
width: 100%;
|
|
@@ -50,7 +59,9 @@
|
|
|
50
59
|
background: var(--pk-surface-2);
|
|
51
60
|
color: var(--pk-muted);
|
|
52
61
|
}
|
|
53
|
-
|
|
62
|
+
/* Local restatement of the kit-wide .pk-hidden utility (_tokens.css) so the
|
|
63
|
+
controller's toggle also holds if avatar.css is consumed standalone. */
|
|
64
|
+
.pk-avatar-image.pk-hidden, .pk-avatar-fallback.pk-hidden { display: none; }
|
|
54
65
|
|
|
55
66
|
/* Status dot pinned to the bottom-right corner; sized by the avatar's size
|
|
56
67
|
modifier, glyph hidden when there's no room (shadcn's AvatarBadge). */
|
|
@@ -84,7 +95,7 @@
|
|
|
84
95
|
.pk-avatar-group { display: flex; }
|
|
85
96
|
.pk-avatar-group .pk-avatar,
|
|
86
97
|
.pk-avatar-group .pk-avatar-group-count { box-shadow: 0 0 0 2px var(--pk-bg); }
|
|
87
|
-
.pk-avatar-group > :not(:first-child) { margin-
|
|
98
|
+
.pk-avatar-group > :not(:first-child) { margin-inline-start: -.5rem; }
|
|
88
99
|
|
|
89
100
|
.pk-avatar-group-count {
|
|
90
101
|
position: relative;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus";
|
|
2
2
|
|
|
3
|
-
// Ported
|
|
4
|
-
//
|
|
5
|
-
// connect() no-ops and the fallback stays visible.
|
|
3
|
+
// Ported from ruby_ui's phlex-kit--avatar controller. Shows the image once it
|
|
4
|
+
// has loaded (else the fallback). With no image target (fallback-only avatar)
|
|
5
|
+
// connect() no-ops and the fallback stays visible. Purely progressive
|
|
6
|
+
// enhancement: the CSS stacks the image over the fallback, so with no JS a
|
|
7
|
+
// loaded image covers the fallback and a failed one leaves it showing. Uses
|
|
8
|
+
// the kit's .pk-hidden utility (not a bare .hidden).
|
|
6
9
|
export default class extends Controller {
|
|
7
10
|
static targets = ["image", "fallback"];
|
|
8
11
|
|
|
@@ -19,12 +22,12 @@ export default class extends Controller {
|
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
showImage() {
|
|
22
|
-
this.imageTargets.forEach((image) => image.classList.remove("hidden"));
|
|
23
|
-
this.fallbackTargets.forEach((fallback) => fallback.classList.add("hidden"));
|
|
25
|
+
this.imageTargets.forEach((image) => image.classList.remove("pk-hidden"));
|
|
26
|
+
this.fallbackTargets.forEach((fallback) => fallback.classList.add("pk-hidden"));
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
showFallback() {
|
|
27
|
-
this.imageTargets.forEach((image) => image.classList.add("hidden"));
|
|
28
|
-
this.fallbackTargets.forEach((fallback) => fallback.classList.remove("hidden"));
|
|
30
|
+
this.imageTargets.forEach((image) => image.classList.add("pk-hidden"));
|
|
31
|
+
this.fallbackTargets.forEach((fallback) => fallback.classList.remove("pk-hidden"));
|
|
29
32
|
}
|
|
30
33
|
}
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
}
|
|
27
27
|
/* shadcn forces glyphs to size-3 with `!` — override Spinner's own sizing. */
|
|
28
28
|
.pk-badge > svg { width: .75rem !important; height: .75rem !important; pointer-events: none; }
|
|
29
|
-
.pk-badge:has(> [data-icon="inline-start"]) { padding-
|
|
30
|
-
.pk-badge:has(> [data-icon="inline-end"]) { padding-
|
|
29
|
+
.pk-badge:has(> [data-icon="inline-start"]) { padding-inline-start: .375rem; }
|
|
30
|
+
.pk-badge:has(> [data-icon="inline-end"]) { padding-inline-end: .375rem; }
|
|
31
31
|
.pk-badge:focus-visible {
|
|
32
32
|
outline: none;
|
|
33
33
|
border-color: var(--pk-ring);
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
|
|
37
37
|
/* Sizes (md is the base above) — kit extras. */
|
|
38
38
|
.pk-badge.sm { height: 1rem; padding: 0 .375rem; font-size: .7rem; }
|
|
39
|
-
.pk-badge.sm:has(> [data-icon="inline-start"]) { padding-
|
|
40
|
-
.pk-badge.sm:has(> [data-icon="inline-end"]) { padding-
|
|
39
|
+
.pk-badge.sm:has(> [data-icon="inline-start"]) { padding-inline-start: .25rem; }
|
|
40
|
+
.pk-badge.sm:has(> [data-icon="inline-end"]) { padding-inline-end: .25rem; }
|
|
41
41
|
.pk-badge.lg { height: 1.5rem; padding: .125rem .75rem; font-size: .875rem; }
|
|
42
42
|
|
|
43
43
|
.pk-badge.primary { background: var(--pk-brand); color: var(--pk-brand-ink); }
|
|
@@ -2,7 +2,9 @@ module PhlexKit
|
|
|
2
2
|
class BreadcrumbEllipsis < BaseComponent
|
|
3
3
|
def initialize(**attrs) = (@attrs = attrs)
|
|
4
4
|
def view_template
|
|
5
|
-
|
|
5
|
+
# No aria-hidden on the wrapper — it would swallow the sr-only text.
|
|
6
|
+
# The Icon renders its own aria-hidden="true"; only the glyph is hidden.
|
|
7
|
+
span(**mix({ class: "pk-breadcrumb-ellipsis", role: "presentation" }, @attrs)) do
|
|
6
8
|
render Icon.new(:ellipsis, size: nil)
|
|
7
9
|
span(class: "pk-sr-only") { "More" }
|
|
8
10
|
end
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
padding: .5rem .75rem; font-size: .875rem; line-height: 1.625;
|
|
19
19
|
}
|
|
20
20
|
.pk-bubble[data-align="end"] > .pk-bubble-content { align-self: flex-end; }
|
|
21
|
-
button.pk-bubble-content { text-align:
|
|
21
|
+
button.pk-bubble-content { text-align: start; font: inherit; }
|
|
22
22
|
a.pk-bubble-content { color: inherit; text-decoration: none; }
|
|
23
23
|
button.pk-bubble-content, a.pk-bubble-content {
|
|
24
24
|
cursor: pointer; outline: none;
|
|
@@ -69,5 +69,5 @@ button.pk-bubble-content, a.pk-bubble-content {
|
|
|
69
69
|
.pk-bubble-reactions:has(button) { padding: 0; }
|
|
70
70
|
.pk-bubble-reactions[data-side="bottom"] { bottom: 0; transform: translateY(75%); }
|
|
71
71
|
.pk-bubble-reactions[data-side="top"] { top: 0; transform: translateY(-75%); }
|
|
72
|
-
.pk-bubble-reactions[data-align="end"] {
|
|
73
|
-
.pk-bubble-reactions[data-align="start"] {
|
|
72
|
+
.pk-bubble-reactions[data-align="end"] { inset-inline-end: .75rem; }
|
|
73
|
+
.pk-bubble-reactions[data-align="start"] { inset-inline-start: .75rem; }
|
|
@@ -4,10 +4,12 @@ module PhlexKit
|
|
|
4
4
|
# trailing edge. Compose Bubble > BubbleContent (+ BubbleReactions).
|
|
5
5
|
class Bubble < BaseComponent
|
|
6
6
|
VARIANTS = %i[default secondary muted tinted outline ghost destructive].freeze
|
|
7
|
+
ALIGNS = %i[start end].freeze
|
|
7
8
|
def initialize(variant: :default, align: :start, **attrs)
|
|
8
9
|
@variant = variant.to_sym
|
|
9
10
|
raise KeyError, "unknown Bubble variant #{@variant}" unless VARIANTS.include?(@variant)
|
|
10
11
|
@align = align.to_sym
|
|
12
|
+
raise KeyError, "unknown Bubble align #{@align}" unless ALIGNS.include?(@align)
|
|
11
13
|
@attrs = attrs
|
|
12
14
|
end
|
|
13
15
|
def view_template(&)
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
class BubbleContent < BaseComponent
|
|
3
|
+
# `as:` is dispatched dynamically — whitelist the sensible text containers
|
|
4
|
+
# so it can't reach arbitrary (including private) methods.
|
|
5
|
+
AS_TAGS = %i[div p blockquote].freeze
|
|
3
6
|
def initialize(as: :div, **attrs)
|
|
4
|
-
@as = as
|
|
7
|
+
@as = as.to_sym
|
|
8
|
+
raise ArgumentError, "unknown BubbleContent as: #{@as.inspect} (use one of #{AS_TAGS.join(", ")})" unless AS_TAGS.include?(@as)
|
|
5
9
|
@attrs = attrs
|
|
6
10
|
end
|
|
7
11
|
def view_template(&)
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
class BubbleReactions < BaseComponent
|
|
3
|
+
SIDES = %i[top bottom].freeze
|
|
4
|
+
ALIGNS = %i[start end].freeze
|
|
3
5
|
def initialize(side: :bottom, align: :end, **attrs)
|
|
4
6
|
@side = side.to_sym
|
|
7
|
+
raise KeyError, "unknown BubbleReactions side #{@side}" unless SIDES.include?(@side)
|
|
5
8
|
@align = align.to_sym
|
|
9
|
+
raise KeyError, "unknown BubbleReactions align #{@align}" unless ALIGNS.include?(@align)
|
|
6
10
|
@attrs = attrs
|
|
7
11
|
end
|
|
8
12
|
def view_template(&)
|