phlex_kit 0.4.0 → 0.5.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 +16 -0
- data/app/assets/stylesheets/phlex_kit/phlex_kit.css +1 -0
- data/app/components/phlex_kit/alert/alert.css +60 -22
- data/app/components/phlex_kit/alert/alert.rb +11 -7
- data/app/components/phlex_kit/alert/alert_action.rb +14 -0
- data/app/components/phlex_kit/alert/alert_title.rb +3 -2
- data/app/components/phlex_kit/alert_dialog/alert_dialog.css +5 -2
- data/app/components/phlex_kit/avatar/avatar.css +80 -9
- data/app/components/phlex_kit/avatar/avatar.rb +6 -4
- data/app/components/phlex_kit/avatar/avatar_badge.rb +15 -0
- data/app/components/phlex_kit/avatar/avatar_group_count.rb +14 -0
- data/app/components/phlex_kit/badge/badge.css +61 -39
- data/app/components/phlex_kit/badge/badge.rb +24 -7
- data/app/components/phlex_kit/breadcrumb/breadcrumb.css +2 -2
- data/app/components/phlex_kit/bubble/bubble.css +49 -8
- data/app/components/phlex_kit/button/button.css +71 -26
- data/app/components/phlex_kit/button/button.rb +20 -13
- data/app/components/phlex_kit/button_group/button_group.css +59 -8
- data/app/components/phlex_kit/button_group/button_group.rb +11 -5
- data/app/components/phlex_kit/button_group/button_group_separator.rb +14 -0
- data/app/components/phlex_kit/button_group/button_group_text.rb +14 -0
- data/app/components/phlex_kit/calendar/calendar.css +66 -15
- data/app/components/phlex_kit/calendar/calendar.rb +87 -19
- data/app/components/phlex_kit/calendar/calendar_controller.js +226 -75
- data/app/components/phlex_kit/calendar/calendar_days.rb +7 -6
- data/app/components/phlex_kit/calendar/calendar_weekdays.rb +3 -1
- data/app/components/phlex_kit/checkbox/checkbox.css +52 -7
- data/app/components/phlex_kit/collapsible/collapsible.css +10 -1
- data/app/components/phlex_kit/collapsible/collapsible_controller.js +9 -2
- data/app/components/phlex_kit/combobox/combobox.css +37 -3
- data/app/components/phlex_kit/combobox/combobox.rb +3 -1
- data/app/components/phlex_kit/combobox/combobox_controller.js +9 -1
- data/app/components/phlex_kit/combobox/combobox_input_trigger.rb +7 -2
- data/app/components/phlex_kit/command/command.css +58 -22
- data/app/components/phlex_kit/command/command_controller.js +3 -1
- data/app/components/phlex_kit/command/command_input.rb +29 -25
- data/app/components/phlex_kit/command/command_separator.rb +18 -0
- data/app/components/phlex_kit/command/command_shortcut.rb +13 -0
- data/app/components/phlex_kit/context_menu/context_menu.css +78 -6
- data/app/components/phlex_kit/context_menu/context_menu_checkbox_item.rb +27 -0
- data/app/components/phlex_kit/context_menu/context_menu_group.rb +10 -0
- data/app/components/phlex_kit/context_menu/context_menu_item.rb +13 -3
- data/app/components/phlex_kit/context_menu/context_menu_radio_group.rb +10 -0
- data/app/components/phlex_kit/context_menu/context_menu_radio_item.rb +30 -0
- data/app/components/phlex_kit/context_menu/context_menu_shortcut.rb +11 -0
- data/app/components/phlex_kit/context_menu/context_menu_sub.rb +11 -0
- data/app/components/phlex_kit/context_menu/context_menu_sub_content.rb +12 -0
- data/app/components/phlex_kit/context_menu/context_menu_sub_trigger.rb +17 -0
- data/app/components/phlex_kit/dialog/dialog.css +34 -16
- data/app/components/phlex_kit/dialog/dialog_close.rb +13 -0
- data/app/components/phlex_kit/dialog/dialog_content.rb +13 -5
- data/app/components/phlex_kit/drawer/drawer.css +56 -17
- data/app/components/phlex_kit/drawer/drawer_content.rb +8 -4
- data/app/components/phlex_kit/empty/empty.css +13 -8
- data/app/components/phlex_kit/field/field.css +152 -0
- data/app/components/phlex_kit/field/field.rb +35 -0
- data/app/components/phlex_kit/field/field_content.rb +11 -0
- data/app/components/phlex_kit/field/field_description.rb +10 -0
- data/app/components/phlex_kit/field/field_error.rb +29 -0
- data/app/components/phlex_kit/field/field_group.rb +12 -0
- data/app/components/phlex_kit/field/field_label.rb +12 -0
- data/app/components/phlex_kit/field/field_legend.rb +17 -0
- data/app/components/phlex_kit/field/field_separator.rb +15 -0
- data/app/components/phlex_kit/field/field_set.rb +10 -0
- data/app/components/phlex_kit/field/field_title.rb +11 -0
- data/app/components/phlex_kit/hover_card/hover_card.css +12 -4
- data/app/components/phlex_kit/hover_card/hover_card_content.rb +11 -2
- data/app/components/phlex_kit/input/input.css +45 -11
- data/app/components/phlex_kit/input_group/input_group.css +88 -12
- data/app/components/phlex_kit/input_group/input_group_addon.rb +11 -4
- data/app/components/phlex_kit/input_group/input_group_button.rb +18 -0
- data/app/components/phlex_kit/input_otp/input_otp.css +29 -5
- data/app/components/phlex_kit/item/item.css +56 -8
- data/app/components/phlex_kit/item/item.rb +13 -3
- data/app/components/phlex_kit/item/item_footer.rb +8 -0
- data/app/components/phlex_kit/item/item_header.rb +8 -0
- data/app/components/phlex_kit/item/item_media.rb +13 -3
- data/app/components/phlex_kit/item/item_separator.rb +10 -0
- data/app/components/phlex_kit/kbd/kbd.css +10 -0
- data/app/components/phlex_kit/marker/marker.css +29 -9
- data/app/components/phlex_kit/marker/marker.rb +13 -3
- data/app/components/phlex_kit/menubar/menubar.css +88 -17
- data/app/components/phlex_kit/menubar/menubar_checkbox_item.rb +26 -0
- data/app/components/phlex_kit/menubar/menubar_group.rb +10 -0
- data/app/components/phlex_kit/menubar/menubar_item.rb +9 -4
- data/app/components/phlex_kit/menubar/menubar_label.rb +10 -0
- data/app/components/phlex_kit/menubar/menubar_radio_group.rb +10 -0
- data/app/components/phlex_kit/menubar/menubar_radio_item.rb +29 -0
- data/app/components/phlex_kit/menubar/menubar_shortcut.rb +10 -0
- data/app/components/phlex_kit/menubar/menubar_sub.rb +10 -0
- data/app/components/phlex_kit/menubar/menubar_sub_content.rb +11 -0
- data/app/components/phlex_kit/menubar/menubar_sub_trigger.rb +17 -0
- data/app/components/phlex_kit/message/message.css +8 -1
- data/app/components/phlex_kit/message_scroller/message_scroller.css +18 -0
- data/app/components/phlex_kit/native_select/native_select.css +25 -5
- data/app/components/phlex_kit/navigation_menu/navigation_menu.css +9 -6
- data/app/components/phlex_kit/pagination/pagination.css +10 -3
- data/app/components/phlex_kit/pagination/pagination_link.rb +21 -0
- data/app/components/phlex_kit/pagination/pagination_next.rb +20 -0
- data/app/components/phlex_kit/pagination/pagination_previous.rb +21 -0
- data/app/components/phlex_kit/popover/popover.css +15 -4
- data/app/components/phlex_kit/popover/popover_content.rb +11 -2
- data/app/components/phlex_kit/popover/popover_description.rb +10 -0
- data/app/components/phlex_kit/popover/popover_header.rb +10 -0
- data/app/components/phlex_kit/popover/popover_title.rb +10 -0
- data/app/components/phlex_kit/progress/progress.css +5 -5
- data/app/components/phlex_kit/radio_button/radio_button.css +46 -2
- data/app/components/phlex_kit/radio_group/radio_group.css +2 -2
- data/app/components/phlex_kit/resizable/resizable.css +17 -0
- data/app/components/phlex_kit/resizable/resizable_handle.rb +7 -3
- data/app/components/phlex_kit/select/select.css +46 -10
- data/app/components/phlex_kit/select/select_separator.rb +10 -0
- data/app/components/phlex_kit/select/select_trigger.rb +5 -2
- data/app/components/phlex_kit/sheet/sheet.css +18 -9
- data/app/components/phlex_kit/sheet/sheet_close.rb +10 -0
- data/app/components/phlex_kit/sheet/sheet_content.rb +8 -4
- data/app/components/phlex_kit/sidebar/sidebar.css +76 -9
- data/app/components/phlex_kit/sidebar/sidebar_controller.js +22 -7
- data/app/components/phlex_kit/sidebar/sidebar_menu_button.rb +8 -3
- data/app/components/phlex_kit/sidebar/sidebar_rail.rb +21 -0
- data/app/components/phlex_kit/sidebar/sidebar_wrapper.rb +25 -8
- data/app/components/phlex_kit/skeleton/skeleton.css +1 -1
- data/app/components/phlex_kit/slider/slider.css +33 -12
- data/app/components/phlex_kit/switch/switch.css +50 -13
- data/app/components/phlex_kit/switch/switch.rb +5 -2
- data/app/components/phlex_kit/table/table.css +8 -4
- data/app/components/phlex_kit/tabs/tabs.css +72 -8
- data/app/components/phlex_kit/tabs/tabs.rb +7 -2
- data/app/components/phlex_kit/tabs/tabs_list.rb +11 -2
- data/app/components/phlex_kit/textarea/textarea.css +33 -13
- data/app/components/phlex_kit/toast/toast.css +1 -1
- data/app/components/phlex_kit/toggle/toggle.css +25 -10
- data/app/components/phlex_kit/toggle_group/toggle_group.css +7 -6
- data/app/components/phlex_kit/tooltip/tooltip.css +37 -10
- data/app/components/phlex_kit/tooltip/tooltip_content.rb +9 -4
- data/lib/phlex_kit/version.rb +1 -1
- metadata +50 -1
|
@@ -1,20 +1,65 @@
|
|
|
1
|
-
/* Co-located with checkbox.rb —
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/* Co-located with checkbox.rb — PhlexKit::Checkbox. Matched to shadcn/ui's
|
|
2
|
+
current checkbox: appearance:none custom box (size-4, 4px radius,
|
|
3
|
+
input-border, translucent input fill in dark), solid brand fill when
|
|
4
|
+
checked with a masked check glyph (currentColor-free — the mask carries
|
|
5
|
+
the shape, the fill is brand-ink), an :indeterminate minus, an extended
|
|
6
|
+
touch target (::before inset bleed, their after:-inset-x-3/-inset-y-2),
|
|
7
|
+
focus + invalid rings. Native input semantics kept — no JS. */
|
|
6
8
|
.pk-checkbox {
|
|
9
|
+
appearance: none;
|
|
10
|
+
-webkit-appearance: none;
|
|
11
|
+
position: relative;
|
|
12
|
+
display: grid;
|
|
13
|
+
place-content: center;
|
|
14
|
+
flex-shrink: 0;
|
|
7
15
|
width: 1rem;
|
|
8
16
|
height: 1rem;
|
|
9
|
-
|
|
10
|
-
|
|
17
|
+
margin: 0;
|
|
18
|
+
border: 1px solid var(--pk-input);
|
|
19
|
+
border-radius: 4px;
|
|
20
|
+
background: color-mix(in oklab, var(--pk-input) 30%, transparent);
|
|
11
21
|
cursor: pointer;
|
|
22
|
+
transition: border-color .15s ease, background-color .15s ease, box-shadow .15s ease;
|
|
23
|
+
}
|
|
24
|
+
/* Extended touch target. */
|
|
25
|
+
.pk-checkbox::before { content: ""; position: absolute; inset: -.5rem -.75rem; }
|
|
26
|
+
.pk-checkbox:checked, .pk-checkbox:indeterminate {
|
|
27
|
+
background: var(--pk-brand);
|
|
28
|
+
border-color: var(--pk-brand);
|
|
29
|
+
}
|
|
30
|
+
.pk-checkbox:checked::after, .pk-checkbox:indeterminate::after {
|
|
31
|
+
content: "";
|
|
32
|
+
width: .875rem;
|
|
33
|
+
height: .875rem;
|
|
34
|
+
background: var(--pk-brand-ink);
|
|
35
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / contain no-repeat;
|
|
36
|
+
}
|
|
37
|
+
.pk-checkbox:indeterminate::after {
|
|
38
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3C/svg%3E") center / contain no-repeat;
|
|
12
39
|
}
|
|
13
40
|
.pk-checkbox:focus-visible {
|
|
14
41
|
outline: none;
|
|
42
|
+
border-color: var(--pk-ring);
|
|
15
43
|
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
16
44
|
}
|
|
45
|
+
.pk-checkbox[aria-invalid="true"] {
|
|
46
|
+
border-color: color-mix(in oklab, var(--pk-red) 50%, transparent);
|
|
47
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 40%, transparent);
|
|
48
|
+
}
|
|
49
|
+
.pk-checkbox[aria-invalid="true"]:checked { border-color: var(--pk-brand); }
|
|
17
50
|
.pk-checkbox:disabled { cursor: not-allowed; opacity: .5; }
|
|
51
|
+
:root[data-theme="light"] .pk-checkbox:not(:checked):not(:indeterminate) { background: transparent; }
|
|
52
|
+
:root[data-theme="light"] .pk-checkbox[aria-invalid="true"] {
|
|
53
|
+
border-color: var(--pk-red);
|
|
54
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent);
|
|
55
|
+
}
|
|
56
|
+
@media (prefers-color-scheme: light) {
|
|
57
|
+
:root[data-theme="system"] .pk-checkbox:not(:checked):not(:indeterminate) { background: transparent; }
|
|
58
|
+
:root[data-theme="system"] .pk-checkbox[aria-invalid="true"] {
|
|
59
|
+
border-color: var(--pk-red);
|
|
60
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
18
63
|
|
|
19
64
|
/* A checkbox paired inline with its label (ruby_ui's `flex items-center
|
|
20
65
|
space-x-3` row). Used for checkbox lists like store-assignment; vertical
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
-
/* Co-located with collapsible.rb. Theme tokens from the global stylesheet.
|
|
1
|
+
/* Co-located with collapsible.rb. Theme tokens from the global stylesheet.
|
|
2
|
+
The controller stamps data-state="open|closed" on the root (shadcn's
|
|
3
|
+
styling hook). Give a trigger glyph .pk-collapsible-chevron to rotate it
|
|
4
|
+
when open — 180deg by default, override with --pk-collapsible-rotate
|
|
5
|
+
(file trees use 90deg on a chevron-right). Scoped to the nearest root so
|
|
6
|
+
nested collapsibles rotate independently. */
|
|
2
7
|
.pk-collapsible-trigger { cursor: pointer; }
|
|
3
8
|
.pk-collapsible-content { overflow-y: hidden; }
|
|
9
|
+
.pk-collapsible-chevron { transition: transform .15s ease; }
|
|
10
|
+
.pk-collapsible[data-state="open"] > .pk-collapsible-trigger .pk-collapsible-chevron {
|
|
11
|
+
transform: rotate(var(--pk-collapsible-rotate, 180deg));
|
|
12
|
+
}
|
|
@@ -9,6 +9,13 @@ export default class extends Controller {
|
|
|
9
9
|
toggle() { this.openValue = !this.openValue }
|
|
10
10
|
openValueChanged(isOpen) { isOpen ? this.open() : this.close() }
|
|
11
11
|
|
|
12
|
-
open() {
|
|
13
|
-
|
|
12
|
+
open() {
|
|
13
|
+
if (this.hasContentTarget) { this.contentTarget.classList.remove("pk-hidden"); this.openValue = true }
|
|
14
|
+
this.element.dataset.state = "open";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
close() {
|
|
18
|
+
if (this.hasContentTarget) { this.contentTarget.classList.add("pk-hidden"); this.openValue = false }
|
|
19
|
+
this.element.dataset.state = "closed";
|
|
20
|
+
}
|
|
14
21
|
}
|
|
@@ -24,10 +24,15 @@
|
|
|
24
24
|
cursor: pointer;
|
|
25
25
|
transition: background-color .15s ease, border-color .15s ease, box-shadow .15s ease;
|
|
26
26
|
}
|
|
27
|
-
.pk-combobox-trigger
|
|
27
|
+
.pk-combobox-trigger { background: color-mix(in oklab, var(--pk-input) 30%, transparent); }
|
|
28
|
+
.pk-combobox-trigger:hover { background: color-mix(in oklab, var(--pk-input) 50%, transparent); }
|
|
29
|
+
.pk-combobox-trigger[aria-invalid="true"] {
|
|
30
|
+
border-color: color-mix(in oklab, var(--pk-red) 50%, transparent);
|
|
31
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 40%, transparent);
|
|
32
|
+
}
|
|
28
33
|
.pk-combobox-trigger:focus-visible {
|
|
29
34
|
outline: none;
|
|
30
|
-
border-color: var(--pk-
|
|
35
|
+
border-color: var(--pk-ring);
|
|
31
36
|
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
32
37
|
}
|
|
33
38
|
.pk-combobox-trigger:disabled,
|
|
@@ -177,8 +182,18 @@
|
|
|
177
182
|
cursor: text;
|
|
178
183
|
transition: border-color .15s ease, box-shadow .15s ease;
|
|
179
184
|
}
|
|
185
|
+
.pk-combobox-input-trigger { background: color-mix(in oklab, var(--pk-input) 30%, transparent); }
|
|
186
|
+
.pk-combobox-input-trigger[aria-invalid="true"] {
|
|
187
|
+
border-color: color-mix(in oklab, var(--pk-red) 50%, transparent);
|
|
188
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 40%, transparent);
|
|
189
|
+
}
|
|
190
|
+
.pk-combobox-input-trigger:has(.pk-combobox-input-trigger-field:disabled) {
|
|
191
|
+
pointer-events: none;
|
|
192
|
+
opacity: .5;
|
|
193
|
+
background: color-mix(in oklab, var(--pk-input) 80%, transparent);
|
|
194
|
+
}
|
|
180
195
|
.pk-combobox-input-trigger:focus-within {
|
|
181
|
-
border-color: var(--pk-
|
|
196
|
+
border-color: var(--pk-ring);
|
|
182
197
|
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
183
198
|
}
|
|
184
199
|
.pk-combobox-input-trigger-field {
|
|
@@ -308,3 +323,22 @@
|
|
|
308
323
|
box-shadow: 0 0 0 2px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
309
324
|
}
|
|
310
325
|
.pk-combobox-clear-button svg { width: .875rem; height: .875rem; }
|
|
326
|
+
|
|
327
|
+
:root[data-theme="light"] .pk-combobox-trigger,
|
|
328
|
+
:root[data-theme="light"] .pk-combobox-input-trigger { background: transparent; }
|
|
329
|
+
:root[data-theme="light"] .pk-combobox-trigger:hover { background: transparent; }
|
|
330
|
+
:root[data-theme="light"] .pk-combobox-trigger[aria-invalid="true"],
|
|
331
|
+
:root[data-theme="light"] .pk-combobox-input-trigger[aria-invalid="true"] {
|
|
332
|
+
border-color: var(--pk-red);
|
|
333
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent);
|
|
334
|
+
}
|
|
335
|
+
@media (prefers-color-scheme: light) {
|
|
336
|
+
:root[data-theme="system"] .pk-combobox-trigger,
|
|
337
|
+
:root[data-theme="system"] .pk-combobox-input-trigger { background: transparent; }
|
|
338
|
+
:root[data-theme="system"] .pk-combobox-trigger:hover { background: transparent; }
|
|
339
|
+
:root[data-theme="system"] .pk-combobox-trigger[aria-invalid="true"],
|
|
340
|
+
:root[data-theme="system"] .pk-combobox-input-trigger[aria-invalid="true"] {
|
|
341
|
+
border-color: var(--pk-red);
|
|
342
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
@@ -13,8 +13,9 @@ module PhlexKit
|
|
|
13
13
|
# latter two shipped unfinished in ruby_ui (no controller support) and are
|
|
14
14
|
# completed here. Tailwind → vanilla `.pk-combobox*` (combobox.css).
|
|
15
15
|
class Combobox < BaseComponent
|
|
16
|
-
def initialize(term: nil, **attrs)
|
|
16
|
+
def initialize(term: nil, auto_highlight: false, **attrs)
|
|
17
17
|
@term = term
|
|
18
|
+
@auto_highlight = auto_highlight
|
|
18
19
|
@attrs = attrs
|
|
19
20
|
end
|
|
20
21
|
|
|
@@ -25,6 +26,7 @@ module PhlexKit
|
|
|
25
26
|
data: {
|
|
26
27
|
controller: "phlex-kit--combobox",
|
|
27
28
|
phlex_kit__combobox_term_value: @term,
|
|
29
|
+
phlex_kit__combobox_auto_highlight_value: (@auto_highlight ? "true" : nil),
|
|
28
30
|
action: "turbo:morph@window->phlex-kit--combobox#updateTriggerContent click@window->phlex-kit--combobox#onClickOutside"
|
|
29
31
|
}
|
|
30
32
|
}, @attrs), &)
|
|
@@ -14,7 +14,8 @@ import { Controller } from "@hotwired/stimulus";
|
|
|
14
14
|
export default class extends Controller {
|
|
15
15
|
static values = {
|
|
16
16
|
term: String,
|
|
17
|
-
minPopoverWidth: { type: Number, default: 240 }
|
|
17
|
+
minPopoverWidth: { type: Number, default: 240 },
|
|
18
|
+
autoHighlight: { type: Boolean, default: false }
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
static targets = [
|
|
@@ -241,6 +242,13 @@ export default class extends Controller {
|
|
|
241
242
|
if (this.hasEmptyStateTarget) {
|
|
242
243
|
this.emptyStateTarget.classList.toggle("pk-hidden", resultCount !== 0)
|
|
243
244
|
}
|
|
245
|
+
|
|
246
|
+
// autoHighlight: keep the first visible option marked while typing so
|
|
247
|
+
// Enter picks it immediately (their autoHighlight prop).
|
|
248
|
+
if (this.autoHighlightValue && filterTerm && resultCount > 0) {
|
|
249
|
+
this.selectedItemIndex = 0
|
|
250
|
+
this.focusSelectedInput()
|
|
251
|
+
}
|
|
244
252
|
}
|
|
245
253
|
|
|
246
254
|
keyDownPressed(e) {
|
|
@@ -9,15 +9,19 @@ module PhlexKit
|
|
|
9
9
|
# popover's own bindings only fire for the button+search layout.
|
|
10
10
|
# See combobox.rb.
|
|
11
11
|
class ComboboxInputTrigger < BaseComponent
|
|
12
|
-
def initialize(placeholder: "", **attrs)
|
|
12
|
+
def initialize(placeholder: "", disabled: false, invalid: false, **attrs)
|
|
13
13
|
@placeholder = placeholder
|
|
14
|
+
@disabled = disabled
|
|
15
|
+
@invalid = invalid
|
|
14
16
|
@attrs = attrs
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
def view_template
|
|
20
|
+
wrapper_aria = { haspopup: "listbox", expanded: "false" }
|
|
21
|
+
wrapper_aria[:invalid] = "true" if @invalid
|
|
18
22
|
div(**mix({
|
|
19
23
|
class: "pk-combobox-input-trigger",
|
|
20
|
-
aria:
|
|
24
|
+
aria: wrapper_aria,
|
|
21
25
|
data: {
|
|
22
26
|
placeholder: @placeholder,
|
|
23
27
|
phlex_kit__combobox_target: "trigger",
|
|
@@ -34,6 +38,7 @@ module PhlexKit
|
|
|
34
38
|
input(
|
|
35
39
|
type: :text,
|
|
36
40
|
placeholder: @placeholder,
|
|
41
|
+
disabled: @disabled,
|
|
37
42
|
autocomplete: "off",
|
|
38
43
|
autocorrect: "off",
|
|
39
44
|
spellcheck: "false",
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
/* Co-located with command.rb —
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
/* Co-located with command.rb — PhlexKit::Command (+ CommandDialog overlay).
|
|
2
|
+
Matched to shadcn/ui's current command (nova): the palette is a padded
|
|
3
|
+
rounded-xl popover surface; the search field is a bordered PILL inside it
|
|
4
|
+
(input-wrapper > input-group) rather than a full-width divider row; groups
|
|
5
|
+
are no longer divide-y — an explicit CommandSeparator (hidden while
|
|
6
|
+
filtering) draws the line; selected rows fill with the muted surface.
|
|
7
|
+
fuse.js stays replaced by the controller's substring scorer. */
|
|
8
|
+
.pk-command {
|
|
9
|
+
background: var(--pk-surface);
|
|
10
|
+
color: var(--pk-text);
|
|
11
|
+
border-radius: .75rem;
|
|
12
|
+
padding: .25rem;
|
|
13
|
+
}
|
|
7
14
|
.pk-command-dialog-trigger { display: inline-block; }
|
|
8
15
|
|
|
9
16
|
/* Backdrop of the cloned overlay. */
|
|
@@ -12,8 +19,8 @@
|
|
|
12
19
|
inset: 0;
|
|
13
20
|
z-index: 50;
|
|
14
21
|
pointer-events: auto;
|
|
15
|
-
background: rgb(0 0 0 / .
|
|
16
|
-
backdrop-filter: blur(
|
|
22
|
+
background: rgb(0 0 0 / .1);
|
|
23
|
+
backdrop-filter: blur(4px);
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
/* Centered palette panel. */
|
|
@@ -24,12 +31,10 @@
|
|
|
24
31
|
transform: translate(-50%, -50%);
|
|
25
32
|
z-index: 50;
|
|
26
33
|
pointer-events: auto;
|
|
27
|
-
display: grid;
|
|
28
|
-
gap: 1rem;
|
|
29
34
|
width: 100%;
|
|
30
35
|
max-width: 32rem;
|
|
31
36
|
border: 1px solid var(--pk-border);
|
|
32
|
-
border-radius:
|
|
37
|
+
border-radius: .75rem;
|
|
33
38
|
background: var(--pk-surface);
|
|
34
39
|
color: var(--pk-text);
|
|
35
40
|
box-shadow: 0 10px 30px rgb(0 0 0 / .35);
|
|
@@ -40,18 +45,22 @@
|
|
|
40
45
|
.pk-command-dialog.xl { max-width: 56rem; }
|
|
41
46
|
.pk-command-dialog.full { max-width: 100%; }
|
|
42
47
|
|
|
43
|
-
/* Search
|
|
44
|
-
.pk-command-input-
|
|
48
|
+
/* Search pill (their input-wrapper > input-group). */
|
|
49
|
+
.pk-command-input-wrapper { padding: .25rem .25rem 0; }
|
|
50
|
+
.pk-command-input-pill {
|
|
45
51
|
display: flex;
|
|
46
52
|
align-items: center;
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
gap: .5rem;
|
|
54
|
+
height: 2rem;
|
|
55
|
+
padding: 0 .5rem;
|
|
56
|
+
border: 1px solid color-mix(in oklab, var(--pk-input) 30%, transparent);
|
|
57
|
+
border-radius: var(--pk-radius);
|
|
58
|
+
background: color-mix(in oklab, var(--pk-input) 30%, transparent);
|
|
49
59
|
}
|
|
50
|
-
.pk-command-input-icon { flex: none; width: 1rem; height: 1rem;
|
|
60
|
+
.pk-command-input-icon { flex: none; width: 1rem; height: 1rem; opacity: .5; }
|
|
51
61
|
.pk-command-input {
|
|
52
62
|
width: 100%;
|
|
53
|
-
height:
|
|
54
|
-
padding: .75rem 0;
|
|
63
|
+
height: 100%;
|
|
55
64
|
border: none;
|
|
56
65
|
outline: none;
|
|
57
66
|
background: transparent;
|
|
@@ -62,8 +71,16 @@
|
|
|
62
71
|
.pk-command-input::placeholder { color: var(--pk-muted); }
|
|
63
72
|
.pk-command-input:disabled { cursor: not-allowed; opacity: .5; }
|
|
64
73
|
|
|
65
|
-
/*
|
|
66
|
-
.pk-command-list
|
|
74
|
+
/* Scrollable results (their max-h-72, hidden scrollbar). */
|
|
75
|
+
.pk-command-list {
|
|
76
|
+
max-height: 18rem;
|
|
77
|
+
overflow-y: auto;
|
|
78
|
+
scroll-padding-block: .25rem;
|
|
79
|
+
outline: none;
|
|
80
|
+
scrollbar-width: none;
|
|
81
|
+
}
|
|
82
|
+
.pk-command-list::-webkit-scrollbar { display: none; }
|
|
83
|
+
|
|
67
84
|
.pk-command-group {
|
|
68
85
|
overflow: hidden;
|
|
69
86
|
padding: .25rem;
|
|
@@ -76,6 +93,12 @@
|
|
|
76
93
|
color: var(--pk-muted);
|
|
77
94
|
}
|
|
78
95
|
|
|
96
|
+
.pk-command-separator {
|
|
97
|
+
height: 1px;
|
|
98
|
+
margin: 0 -.25rem;
|
|
99
|
+
background: var(--pk-border);
|
|
100
|
+
}
|
|
101
|
+
|
|
79
102
|
/* One result row. */
|
|
80
103
|
.pk-command-item {
|
|
81
104
|
position: relative;
|
|
@@ -87,18 +110,31 @@
|
|
|
87
110
|
font-size: .875rem;
|
|
88
111
|
color: var(--pk-text);
|
|
89
112
|
text-decoration: none;
|
|
90
|
-
cursor:
|
|
113
|
+
cursor: default;
|
|
91
114
|
user-select: none;
|
|
92
115
|
outline: none;
|
|
93
116
|
transition: background-color .12s ease;
|
|
94
117
|
}
|
|
118
|
+
.pk-command-item svg:not([width]) { width: 1rem; height: 1rem; }
|
|
95
119
|
.pk-command-item:hover,
|
|
96
|
-
.pk-command-item[aria-selected="true"] { background: var(--pk-
|
|
120
|
+
.pk-command-item[aria-selected="true"] { background: var(--pk-surface-2); color: var(--pk-text); }
|
|
97
121
|
.pk-command-item[data-disabled] { pointer-events: none; opacity: .5; }
|
|
98
122
|
|
|
123
|
+
.pk-command-shortcut {
|
|
124
|
+
margin-left: auto;
|
|
125
|
+
font-size: .75rem;
|
|
126
|
+
letter-spacing: .1em;
|
|
127
|
+
color: var(--pk-muted);
|
|
128
|
+
}
|
|
129
|
+
.pk-command-item:hover .pk-command-shortcut,
|
|
130
|
+
.pk-command-item[aria-selected="true"] .pk-command-shortcut { color: var(--pk-text); }
|
|
131
|
+
|
|
99
132
|
/* Filter empty state (hidden/shown by the controller). */
|
|
100
133
|
.pk-command-empty {
|
|
101
134
|
padding: 1.5rem 0;
|
|
102
135
|
text-align: center;
|
|
103
136
|
font-size: .875rem;
|
|
104
137
|
}
|
|
138
|
+
@supports not (backdrop-filter: blur(1px)) {
|
|
139
|
+
.pk-command-overlay { background: rgb(0 0 0 / .5); }
|
|
140
|
+
}
|
|
@@ -33,7 +33,7 @@ function fuzzyScore(query, value) {
|
|
|
33
33
|
// nav, dismiss) is unchanged.
|
|
34
34
|
// Connects to data-controller="phlex-kit--command"
|
|
35
35
|
export default class extends Controller {
|
|
36
|
-
static targets = ["input", "group", "item", "empty"];
|
|
36
|
+
static targets = ["input", "group", "item", "empty", "separator"];
|
|
37
37
|
|
|
38
38
|
connect() {
|
|
39
39
|
this.selectedIndex = -1;
|
|
@@ -76,6 +76,7 @@ export default class extends Controller {
|
|
|
76
76
|
);
|
|
77
77
|
|
|
78
78
|
this.toggleVisibility(this.emptyTargets, results.length === 0);
|
|
79
|
+
this.toggleVisibility(this.separatorTargets, false);
|
|
79
80
|
this.updateGroupVisibility();
|
|
80
81
|
}
|
|
81
82
|
|
|
@@ -96,6 +97,7 @@ export default class extends Controller {
|
|
|
96
97
|
resetVisibility() {
|
|
97
98
|
this.toggleVisibility(this.itemTargets, true);
|
|
98
99
|
this.toggleVisibility(this.groupTargets, true);
|
|
100
|
+
this.toggleVisibility(this.separatorTargets, true);
|
|
99
101
|
this.toggleVisibility(this.emptyTargets, false);
|
|
100
102
|
}
|
|
101
103
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
# Search field at the top of a PhlexKit::Command — filters the items as you
|
|
3
|
-
# type, arrows/enter drive selection, esc dismisses.
|
|
3
|
+
# type, arrows/enter drive selection, esc dismisses. Renders as shadcn's
|
|
4
|
+
# current bordered pill inside the padded palette (input-wrapper >
|
|
5
|
+
# input-group). See command.rb.
|
|
4
6
|
class CommandInput < BaseComponent
|
|
5
7
|
def initialize(placeholder: "Type a command or search...", **attrs)
|
|
6
8
|
@placeholder = placeholder
|
|
@@ -8,30 +10,32 @@ module PhlexKit
|
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
def view_template
|
|
11
|
-
div(class: "pk-command-input-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
13
|
+
div(class: "pk-command-input-wrapper") do
|
|
14
|
+
div(class: "pk-command-input-pill") do
|
|
15
|
+
search_icon
|
|
16
|
+
input(**mix({
|
|
17
|
+
type: :text,
|
|
18
|
+
class: "pk-command-input",
|
|
19
|
+
placeholder: @placeholder,
|
|
20
|
+
autocomplete: "off",
|
|
21
|
+
autocorrect: "off",
|
|
22
|
+
spellcheck: "false",
|
|
23
|
+
autofocus: true,
|
|
24
|
+
role: "combobox",
|
|
25
|
+
value: "",
|
|
26
|
+
aria: { autocomplete: "list", expanded: "true" },
|
|
27
|
+
data: {
|
|
28
|
+
phlex_kit__command_target: "input",
|
|
29
|
+
action: [
|
|
30
|
+
"input->phlex-kit--command#filter",
|
|
31
|
+
"keydown.down->phlex-kit--command#handleKeydown",
|
|
32
|
+
"keydown.up->phlex-kit--command#handleKeydown",
|
|
33
|
+
"keydown.enter->phlex-kit--command#handleKeydown",
|
|
34
|
+
"keydown.esc->phlex-kit--command#dismiss"
|
|
35
|
+
].join(" ")
|
|
36
|
+
}
|
|
37
|
+
}, @attrs))
|
|
38
|
+
end
|
|
35
39
|
end
|
|
36
40
|
end
|
|
37
41
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module PhlexKit
|
|
2
|
+
# Hairline between CommandGroups, ported from shadcn/ui's CommandSeparator.
|
|
3
|
+
# Hidden while a filter query is active (like cmdk). See command.rb.
|
|
4
|
+
class CommandSeparator < BaseComponent
|
|
5
|
+
def initialize(**attrs)
|
|
6
|
+
@attrs = attrs
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def view_template
|
|
10
|
+
div(**mix({
|
|
11
|
+
class: "pk-command-separator",
|
|
12
|
+
role: "separator",
|
|
13
|
+
aria: { hidden: true },
|
|
14
|
+
data: { phlex_kit__command_target: "separator" }
|
|
15
|
+
}, @attrs))
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module PhlexKit
|
|
2
|
+
# Right-aligned keyboard hint on a CommandItem, ported from shadcn/ui's
|
|
3
|
+
# CommandShortcut. See command.rb.
|
|
4
|
+
class CommandShortcut < BaseComponent
|
|
5
|
+
def initialize(**attrs)
|
|
6
|
+
@attrs = attrs
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def view_template(&block)
|
|
10
|
+
span(**mix({ class: "pk-command-shortcut" }, @attrs), &block)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -1,19 +1,91 @@
|
|
|
1
|
-
/* Co-located with context_menu.rb. CSS-positioned at the cursor.
|
|
1
|
+
/* Co-located with context_menu.rb. CSS-positioned at the cursor. Matches the
|
|
2
|
+
dropdown-menu grammar (shadcn's context menu shares the same part set):
|
|
3
|
+
gap-based rows, destructive variant, hidden-input checkbox/radio rows with
|
|
4
|
+
:has() indicators, pure-CSS hover submenus. Tokens global. */
|
|
2
5
|
.pk-context-menu { position: relative; }
|
|
3
6
|
.pk-context-menu-content {
|
|
4
|
-
position: absolute; z-index: 50; min-width: 8rem; overflow:
|
|
7
|
+
position: absolute; z-index: 50; min-width: 8rem; overflow: visible;
|
|
5
8
|
border: 1px solid var(--pk-border); border-radius: calc(var(--pk-radius) - 2px); background: var(--pk-surface);
|
|
6
9
|
color: var(--pk-text); padding: .25rem; box-shadow: 0 8px 24px rgb(0 0 0 / .3); outline: none;
|
|
7
10
|
}
|
|
8
11
|
.pk-context-menu-item {
|
|
9
|
-
position: relative; display: flex;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
position: relative; display: flex; align-items: center; gap: .5rem;
|
|
13
|
+
width: 100%; cursor: pointer; user-select: none;
|
|
14
|
+
border-radius: calc(var(--pk-radius) - 4px); padding: .375rem .5rem; font-size: .875rem;
|
|
15
|
+
color: var(--pk-text); text-decoration: none; outline: none;
|
|
12
16
|
}
|
|
17
|
+
.pk-context-menu-item > svg { flex: none; width: 1rem; height: 1rem; color: var(--pk-muted); }
|
|
13
18
|
.pk-context-menu-item:hover, .pk-context-menu-item:focus { background: var(--pk-accent); color: var(--pk-text); }
|
|
14
19
|
.pk-context-menu-item[data-disabled="true"] { pointer-events: none; opacity: .5; }
|
|
15
|
-
.pk-context-menu-
|
|
20
|
+
.pk-context-menu-item.inset { padding-left: 2rem; }
|
|
21
|
+
.pk-context-menu-check {
|
|
22
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
23
|
+
flex: none; width: 1rem; height: 1rem;
|
|
24
|
+
}
|
|
25
|
+
.pk-context-menu-check svg { width: .875rem; height: .875rem; }
|
|
16
26
|
.pk-context-menu-shortcut { margin-left: auto; font-size: .75rem; letter-spacing: .1em; color: var(--pk-muted); }
|
|
17
27
|
.pk-context-menu-label { padding: .375rem .5rem; font-size: .875rem; font-weight: 600; color: var(--pk-text); }
|
|
18
28
|
.pk-context-menu-label.inset { padding-left: 2rem; }
|
|
19
29
|
.pk-context-menu-separator { height: 1px; margin: .25rem -.25rem; background: var(--pk-border); }
|
|
30
|
+
.pk-context-menu-group { display: contents; }
|
|
31
|
+
|
|
32
|
+
/* Destructive rows (irreversible actions). */
|
|
33
|
+
.pk-context-menu-item.destructive { color: var(--pk-red); }
|
|
34
|
+
.pk-context-menu-item.destructive > svg { color: var(--pk-red); }
|
|
35
|
+
.pk-context-menu-item.destructive:hover,
|
|
36
|
+
.pk-context-menu-item.destructive:focus {
|
|
37
|
+
background: color-mix(in oklab, var(--pk-red) 12%, transparent);
|
|
38
|
+
color: var(--pk-red);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Checkbox / radio rows: real (hidden) inputs; indicator shows via :has. */
|
|
42
|
+
.pk-context-menu-item-input {
|
|
43
|
+
position: absolute;
|
|
44
|
+
width: 1px;
|
|
45
|
+
height: 1px;
|
|
46
|
+
opacity: 0;
|
|
47
|
+
pointer-events: none;
|
|
48
|
+
}
|
|
49
|
+
.pk-context-menu-checkbox-item,
|
|
50
|
+
.pk-context-menu-radio-item { position: relative; }
|
|
51
|
+
.pk-context-menu-item-indicator {
|
|
52
|
+
display: inline-flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
justify-content: center;
|
|
55
|
+
flex: none;
|
|
56
|
+
width: 1rem;
|
|
57
|
+
height: 1rem;
|
|
58
|
+
visibility: hidden;
|
|
59
|
+
}
|
|
60
|
+
.pk-context-menu-item-indicator svg { width: .875rem; height: .875rem; }
|
|
61
|
+
.pk-context-menu-radio-item .pk-context-menu-item-indicator svg { width: .5rem; height: .5rem; }
|
|
62
|
+
.pk-context-menu-item:has(.pk-context-menu-item-input:checked) .pk-context-menu-item-indicator { visibility: visible; }
|
|
63
|
+
.pk-context-menu-item:has(.pk-context-menu-item-input:focus-visible) {
|
|
64
|
+
box-shadow: 0 0 0 2px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* Submenu: trigger row + side panel, revealed on hover/focus — pure CSS. */
|
|
68
|
+
.pk-context-menu-sub { position: relative; }
|
|
69
|
+
.pk-context-menu-sub-chevron { margin-left: auto; width: 1rem; height: 1rem; }
|
|
70
|
+
.pk-context-menu-sub-content {
|
|
71
|
+
position: absolute;
|
|
72
|
+
top: -0.25rem;
|
|
73
|
+
left: 100%;
|
|
74
|
+
z-index: 60;
|
|
75
|
+
min-width: 10rem;
|
|
76
|
+
padding-left: .25rem; /* hover bridge over the gap */
|
|
77
|
+
display: none;
|
|
78
|
+
}
|
|
79
|
+
.pk-context-menu-sub-viewport {
|
|
80
|
+
min-width: 10rem;
|
|
81
|
+
border-radius: calc(var(--pk-radius) - 2px);
|
|
82
|
+
border: 1px solid var(--pk-border);
|
|
83
|
+
background: var(--pk-surface);
|
|
84
|
+
color: var(--pk-text);
|
|
85
|
+
padding: .25rem;
|
|
86
|
+
box-shadow: 0 8px 24px rgb(0 0 0 / .3);
|
|
87
|
+
}
|
|
88
|
+
.pk-context-menu-sub:hover > .pk-context-menu-sub-content,
|
|
89
|
+
.pk-context-menu-sub:focus-within > .pk-context-menu-sub-content { display: block; }
|
|
90
|
+
.pk-context-menu-sub:hover > .pk-context-menu-sub-trigger,
|
|
91
|
+
.pk-context-menu-sub:focus-within > .pk-context-menu-sub-trigger { background: var(--pk-accent); }
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module PhlexKit
|
|
2
|
+
# Toggleable row, mirroring shadcn/ui's ContextMenuCheckboxItem: a <label>
|
|
3
|
+
# around a real (hidden) checkbox — clicking toggles it natively and the ✓
|
|
4
|
+
# shows via CSS, without closing the menu (matching Radix). Pass `name:`/
|
|
5
|
+
# `value:` to submit the state with a form. See context_menu.rb.
|
|
6
|
+
class ContextMenuCheckboxItem < BaseComponent
|
|
7
|
+
def initialize(checked: false, name: nil, value: "1", **attrs)
|
|
8
|
+
@checked = checked
|
|
9
|
+
@name = name
|
|
10
|
+
@value = value
|
|
11
|
+
@attrs = attrs
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def view_template(&block)
|
|
15
|
+
label(**mix({
|
|
16
|
+
class: "pk-context-menu-item pk-context-menu-checkbox-item",
|
|
17
|
+
role: "menuitemcheckbox"
|
|
18
|
+
}, @attrs)) do
|
|
19
|
+
input(type: :checkbox, class: "pk-context-menu-item-input", name: @name, value: @value, checked: @checked)
|
|
20
|
+
span(class: "pk-context-menu-item-indicator", aria: { hidden: "true" }) do
|
|
21
|
+
render Icon.new(:check, size: nil)
|
|
22
|
+
end
|
|
23
|
+
yield if block
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module PhlexKit
|
|
2
|
+
# Grouping wrapper for related rows (with an optional leading
|
|
3
|
+
# ContextMenuLabel). Mirrors shadcn/ui's ContextMenuGroup. See context_menu.rb.
|
|
4
|
+
class ContextMenuGroup < BaseComponent
|
|
5
|
+
def initialize(**attrs) = (@attrs = attrs)
|
|
6
|
+
def view_template(&)
|
|
7
|
+
div(**mix({ class: "pk-context-menu-group", role: "group" }, @attrs), &)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|