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,9 +1,20 @@
|
|
|
1
|
-
/* Co-located with popover.rb. CSS-positioned panel
|
|
1
|
+
/* Co-located with popover.rb. CSS-positioned panel matched to nova: w-72,
|
|
2
|
+
rounded-lg, p-2.5 with a gap-2.5 column, foreground-tint ring, text-sm;
|
|
3
|
+
Header (title + muted description) stacks at gap-0.5. `align: :end`
|
|
4
|
+
flips the panel to the trigger's end edge. Tokens global. */
|
|
2
5
|
.pk-popover { position: relative; display: inline-block; }
|
|
3
6
|
.pk-popover-trigger { display: inline-block; }
|
|
4
7
|
.pk-popover-content {
|
|
5
|
-
position: absolute; z-index: 50; top: calc(100% + .
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
position: absolute; z-index: 50; top: calc(100% + .25rem); left: 0;
|
|
9
|
+
display: flex; flex-direction: column; gap: .625rem;
|
|
10
|
+
width: 18rem;
|
|
11
|
+
border: 1px solid color-mix(in oklab, var(--pk-text) 10%, transparent);
|
|
12
|
+
border-radius: var(--pk-radius);
|
|
13
|
+
background: var(--pk-surface); color: var(--pk-text); padding: .625rem;
|
|
14
|
+
font-size: .875rem;
|
|
8
15
|
box-shadow: 0 8px 24px rgb(0 0 0 / .3); outline: none;
|
|
9
16
|
}
|
|
17
|
+
.pk-popover-content.end { left: auto; right: 0; }
|
|
18
|
+
.pk-popover-header { display: flex; flex-direction: column; gap: .125rem; font-size: .875rem; }
|
|
19
|
+
.pk-popover-title { margin: 0; font-size: .875rem; font-weight: 500; }
|
|
20
|
+
.pk-popover-description { margin: 0; color: var(--pk-muted); }
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
|
+
# The floating panel of a PhlexKit::Popover. `align: :end` flips it to the
|
|
3
|
+
# trigger's end edge (their align prop). See popover.rb.
|
|
2
4
|
class PopoverContent < BaseComponent
|
|
3
|
-
|
|
5
|
+
ALIGNS = { start: nil, end: "end" }.freeze
|
|
6
|
+
|
|
7
|
+
def initialize(align: :start, **attrs)
|
|
8
|
+
@align = align.to_sym
|
|
9
|
+
@attrs = attrs
|
|
10
|
+
end
|
|
11
|
+
|
|
4
12
|
def view_template(&)
|
|
5
|
-
|
|
13
|
+
classes = [ "pk-popover-content", "pk-hidden", ALIGNS.fetch(@align) ].compact.join(" ")
|
|
14
|
+
div(**mix({ class: classes, data: { phlex_kit__popover_target: "content", state: "closed" } }, @attrs), &)
|
|
6
15
|
end
|
|
7
16
|
end
|
|
8
17
|
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module PhlexKit
|
|
2
|
+
# Muted body copy of a PopoverHeader, ported from shadcn/ui's
|
|
3
|
+
# PopoverDescription. See popover.rb.
|
|
4
|
+
class PopoverDescription < BaseComponent
|
|
5
|
+
def initialize(**attrs) = (@attrs = attrs)
|
|
6
|
+
def view_template(&)
|
|
7
|
+
p(**mix({ class: "pk-popover-description" }, @attrs), &)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module PhlexKit
|
|
2
|
+
# Title + description block at the top of a PopoverContent, ported from
|
|
3
|
+
# shadcn/ui's PopoverHeader. See popover.rb.
|
|
4
|
+
class PopoverHeader < BaseComponent
|
|
5
|
+
def initialize(**attrs) = (@attrs = attrs)
|
|
6
|
+
def view_template(&)
|
|
7
|
+
div(**mix({ class: "pk-popover-header" }, @attrs), &)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module PhlexKit
|
|
2
|
+
# Heading of a PopoverHeader, ported from shadcn/ui's PopoverTitle.
|
|
3
|
+
# See popover.rb.
|
|
4
|
+
class PopoverTitle < BaseComponent
|
|
5
|
+
def initialize(**attrs) = (@attrs = attrs)
|
|
6
|
+
def view_template(&)
|
|
7
|
+
h2(**mix({ class: "pk-popover-title" }, @attrs), &)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
/* Co-located with progress.rb —
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/* Co-located with progress.rb — PhlexKit::Progress. Matched to nova: an h-1
|
|
2
|
+
muted track with the brand indicator translated by value. Theme tokens
|
|
3
|
+
from the global stylesheet. */
|
|
4
4
|
.pk-progress {
|
|
5
5
|
position: relative;
|
|
6
|
-
height: .
|
|
6
|
+
height: .25rem;
|
|
7
7
|
width: 100%;
|
|
8
8
|
overflow: hidden;
|
|
9
9
|
border-radius: 999px;
|
|
10
|
-
background:
|
|
10
|
+
background: var(--pk-surface-2);
|
|
11
11
|
}
|
|
12
12
|
.pk-progress-indicator {
|
|
13
13
|
height: 100%;
|
|
@@ -1,9 +1,53 @@
|
|
|
1
|
-
/* Co-located with radio_button.rb
|
|
1
|
+
/* Co-located with radio_button.rb — PhlexKit::RadioButton. Matched to
|
|
2
|
+
shadcn/ui's current radio-group item (same recipe as the checkbox):
|
|
3
|
+
appearance:none circle with the input border + translucent dark fill,
|
|
4
|
+
brand fill with a primary-foreground dot when checked, extended touch
|
|
5
|
+
target, focus + aria-invalid rings. Native input semantics kept — no JS. */
|
|
2
6
|
.pk-radio {
|
|
7
|
+
appearance: none;
|
|
8
|
+
-webkit-appearance: none;
|
|
9
|
+
position: relative;
|
|
10
|
+
display: inline-grid;
|
|
11
|
+
place-content: center;
|
|
3
12
|
height: 1rem;
|
|
4
13
|
width: 1rem;
|
|
5
14
|
flex: none;
|
|
15
|
+
margin: 0;
|
|
6
16
|
border-radius: 9999px;
|
|
7
|
-
|
|
17
|
+
border: 1px solid var(--pk-input);
|
|
18
|
+
background: color-mix(in oklab, var(--pk-input) 30%, transparent);
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
transition: border-color .15s ease, background-color .15s ease, box-shadow .15s ease;
|
|
8
21
|
}
|
|
22
|
+
.pk-radio::before { content: ""; position: absolute; inset: -.5rem; }
|
|
23
|
+
.pk-radio:checked { background: var(--pk-brand); border-color: var(--pk-brand); }
|
|
24
|
+
.pk-radio:checked::after {
|
|
25
|
+
content: "";
|
|
26
|
+
width: .5rem;
|
|
27
|
+
height: .5rem;
|
|
28
|
+
border-radius: 9999px;
|
|
29
|
+
background: var(--pk-brand-ink);
|
|
30
|
+
}
|
|
31
|
+
.pk-radio:focus-visible {
|
|
32
|
+
outline: none;
|
|
33
|
+
border-color: var(--pk-ring);
|
|
34
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
35
|
+
}
|
|
36
|
+
.pk-radio[aria-invalid="true"] {
|
|
37
|
+
border-color: color-mix(in oklab, var(--pk-red) 50%, transparent);
|
|
38
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 40%, transparent);
|
|
39
|
+
}
|
|
40
|
+
.pk-radio[aria-invalid="true"]:checked { border-color: var(--pk-brand); }
|
|
9
41
|
.pk-radio:disabled { cursor: not-allowed; opacity: .5; }
|
|
42
|
+
:root[data-theme="light"] .pk-radio:not(:checked) { background: transparent; }
|
|
43
|
+
:root[data-theme="light"] .pk-radio[aria-invalid="true"] {
|
|
44
|
+
border-color: var(--pk-red);
|
|
45
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent);
|
|
46
|
+
}
|
|
47
|
+
@media (prefers-color-scheme: light) {
|
|
48
|
+
:root[data-theme="system"] .pk-radio:not(:checked) { background: transparent; }
|
|
49
|
+
:root[data-theme="system"] .pk-radio[aria-invalid="true"] {
|
|
50
|
+
border-color: var(--pk-red);
|
|
51
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
/* Co-located with radio_group.rb — UI::RadioGroup, shadcn parity (grid gap-
|
|
2
|
-
.pk-radio-group { display: grid; gap: .
|
|
1
|
+
/* Co-located with radio_group.rb — UI::RadioGroup, shadcn parity (grid gap-2). */
|
|
2
|
+
.pk-radio-group { display: grid; gap: .5rem; }
|
|
3
3
|
.pk-radio-group .pk-label { gap: .75rem; }
|
|
@@ -21,3 +21,20 @@
|
|
|
21
21
|
outline: none;
|
|
22
22
|
box-shadow: 0 0 0 2px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
/* Visible grip pill (their withHandle / .cn-resizable-handle-icon). */
|
|
26
|
+
.pk-resizable-handle-grip {
|
|
27
|
+
position: absolute;
|
|
28
|
+
top: 50%;
|
|
29
|
+
left: 50%;
|
|
30
|
+
z-index: 1;
|
|
31
|
+
width: .25rem;
|
|
32
|
+
height: 1.5rem;
|
|
33
|
+
border-radius: var(--pk-radius);
|
|
34
|
+
background: var(--pk-border);
|
|
35
|
+
transform: translate(-50%, -50%);
|
|
36
|
+
}
|
|
37
|
+
.pk-resizable-group.vertical > .pk-resizable-handle .pk-resizable-handle-grip {
|
|
38
|
+
width: 1.5rem;
|
|
39
|
+
height: .25rem;
|
|
40
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
|
-
# Drag handle between two ResizablePanels.
|
|
2
|
+
# Drag handle between two ResizablePanels. `with_handle: true` shows the
|
|
3
|
+
# visible grip pill (their withHandle). See resizable_panel_group.rb.
|
|
3
4
|
class ResizableHandle < BaseComponent
|
|
4
|
-
def initialize(**attrs)
|
|
5
|
+
def initialize(with_handle: false, **attrs)
|
|
6
|
+
@with_handle = with_handle
|
|
5
7
|
@attrs = attrs
|
|
6
8
|
end
|
|
7
9
|
|
|
@@ -15,7 +17,9 @@ module PhlexKit
|
|
|
15
17
|
phlex_kit__resizable_target: "handle",
|
|
16
18
|
action: "pointerdown->phlex-kit--resizable#start"
|
|
17
19
|
}
|
|
18
|
-
}, @attrs))
|
|
20
|
+
}, @attrs)) do
|
|
21
|
+
span(class: "pk-resizable-handle-grip", aria: { hidden: "true" }) if @with_handle
|
|
22
|
+
end
|
|
19
23
|
end
|
|
20
24
|
end
|
|
21
25
|
end
|
|
@@ -21,15 +21,23 @@
|
|
|
21
21
|
border: 1px solid var(--pk-input);
|
|
22
22
|
background: transparent;
|
|
23
23
|
color: var(--pk-text);
|
|
24
|
-
padding: .25rem .
|
|
24
|
+
padding: .25rem .5rem .25rem .625rem;
|
|
25
|
+
gap: .375rem;
|
|
25
26
|
font: inherit;
|
|
26
27
|
font-size: .875rem;
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
background: color-mix(in oklab, var(--pk-input) 30%, transparent);
|
|
30
|
+
transition: border-color .15s ease, box-shadow .15s ease, background-color .15s ease;
|
|
31
|
+
}
|
|
32
|
+
.pk-select-trigger:hover { background: color-mix(in oklab, var(--pk-input) 50%, transparent); }
|
|
33
|
+
.pk-select-trigger.sm { height: 1.75rem; border-radius: min(calc(var(--pk-radius) - 2px), 10px); }
|
|
34
|
+
.pk-select-trigger[aria-invalid="true"] {
|
|
35
|
+
border-color: color-mix(in oklab, var(--pk-red) 50%, transparent);
|
|
36
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 40%, transparent);
|
|
29
37
|
}
|
|
30
38
|
.pk-select-trigger:focus-visible {
|
|
31
39
|
outline: none;
|
|
32
|
-
border-color: var(--pk-
|
|
40
|
+
border-color: var(--pk-ring);
|
|
33
41
|
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-ring) 50%, transparent);
|
|
34
42
|
}
|
|
35
43
|
.pk-select-trigger:disabled { cursor: not-allowed; opacity: .5; }
|
|
@@ -49,15 +57,15 @@
|
|
|
49
57
|
.pk-select-viewport {
|
|
50
58
|
max-height: 24rem;
|
|
51
59
|
overflow: auto;
|
|
52
|
-
border-radius:
|
|
53
|
-
border: 1px solid var(--pk-
|
|
60
|
+
border-radius: var(--pk-radius);
|
|
61
|
+
border: 1px solid color-mix(in oklab, var(--pk-text) 10%, transparent);
|
|
54
62
|
background: var(--pk-surface);
|
|
55
63
|
color: var(--pk-text);
|
|
56
64
|
padding: .25rem;
|
|
57
65
|
box-shadow: 0 4px 16px rgba(0, 0, 0, .4);
|
|
58
66
|
}
|
|
59
67
|
|
|
60
|
-
.pk-select-label { margin: 0; padding: .
|
|
68
|
+
.pk-select-label { margin: 0; padding: .25rem .375rem; font-size: .75rem; font-weight: 400; color: var(--pk-muted); }
|
|
61
69
|
|
|
62
70
|
.pk-select-item {
|
|
63
71
|
position: relative;
|
|
@@ -65,16 +73,44 @@
|
|
|
65
73
|
align-items: center;
|
|
66
74
|
cursor: pointer;
|
|
67
75
|
user-select: none;
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
gap: .375rem;
|
|
77
|
+
border-radius: calc(var(--pk-radius) - 2px);
|
|
78
|
+
padding: .25rem 2rem .25rem .375rem;
|
|
70
79
|
font-size: .875rem;
|
|
71
80
|
color: var(--pk-text);
|
|
72
81
|
outline: none;
|
|
73
82
|
transition: background-color .12s ease, color .12s ease;
|
|
74
83
|
}
|
|
84
|
+
.pk-select-item svg:not([class*="size-"]) { width: 1rem; height: 1rem; }
|
|
75
85
|
.pk-select-item:hover,
|
|
76
86
|
.pk-select-item:focus,
|
|
77
87
|
.pk-select-item[aria-current="true"],
|
|
78
88
|
.pk-select-item[aria-selected="true"] { background: var(--pk-accent); }
|
|
79
|
-
.pk-select-item-check {
|
|
89
|
+
.pk-select-item-check {
|
|
90
|
+
visibility: hidden;
|
|
91
|
+
position: absolute;
|
|
92
|
+
right: .5rem;
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
justify-content: center;
|
|
96
|
+
width: 1rem;
|
|
97
|
+
height: 1rem;
|
|
98
|
+
pointer-events: none;
|
|
99
|
+
}
|
|
80
100
|
.pk-select-item[aria-selected="true"] .pk-select-item-check { visibility: visible; }
|
|
101
|
+
|
|
102
|
+
.pk-select-separator { height: 1px; margin: .25rem -.25rem; background: var(--pk-border); }
|
|
103
|
+
:root[data-theme="light"] .pk-select-trigger { background: transparent; }
|
|
104
|
+
:root[data-theme="light"] .pk-select-trigger:hover { background: transparent; }
|
|
105
|
+
:root[data-theme="light"] .pk-select-trigger[aria-invalid="true"] {
|
|
106
|
+
border-color: var(--pk-red);
|
|
107
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent);
|
|
108
|
+
}
|
|
109
|
+
@media (prefers-color-scheme: light) {
|
|
110
|
+
:root[data-theme="system"] .pk-select-trigger { background: transparent; }
|
|
111
|
+
:root[data-theme="system"] .pk-select-trigger:hover { background: transparent; }
|
|
112
|
+
:root[data-theme="system"] .pk-select-trigger[aria-invalid="true"] {
|
|
113
|
+
border-color: var(--pk-red);
|
|
114
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pk-red) 20%, transparent);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module PhlexKit
|
|
2
|
+
# Hairline between SelectGroups, ported from shadcn/ui's SelectSeparator.
|
|
3
|
+
# See select.rb.
|
|
4
|
+
class SelectSeparator < BaseComponent
|
|
5
|
+
def initialize(**attrs) = (@attrs = attrs)
|
|
6
|
+
def view_template
|
|
7
|
+
div(**mix({ class: "pk-select-separator", role: "separator", aria: { hidden: true } }, @attrs))
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -3,7 +3,10 @@ module PhlexKit
|
|
|
3
3
|
# up/down chevron, and opens the panel on click (`phlex-kit--select#onClick`).
|
|
4
4
|
# See select.rb.
|
|
5
5
|
class SelectTrigger < BaseComponent
|
|
6
|
-
|
|
6
|
+
SIZES = { md: nil, sm: "sm" }.freeze
|
|
7
|
+
|
|
8
|
+
def initialize(size: :md, **attrs)
|
|
9
|
+
@size = size.to_sym
|
|
7
10
|
@attrs = attrs
|
|
8
11
|
end
|
|
9
12
|
|
|
@@ -11,7 +14,7 @@ module PhlexKit
|
|
|
11
14
|
button(**mix({
|
|
12
15
|
type: :button,
|
|
13
16
|
role: "combobox",
|
|
14
|
-
class: "pk-select-trigger",
|
|
17
|
+
class: [ "pk-select-trigger", SIZES.fetch(@size) ].compact.join(" "),
|
|
15
18
|
aria: { expanded: "false", haspopup: "listbox", autocomplete: "none" },
|
|
16
19
|
data: { action: "phlex-kit--select#onClick", phlex_kit__select_target: "trigger" }
|
|
17
20
|
}, @attrs)) do
|
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
/* Co-located with sheet.rb.
|
|
1
|
+
/* Co-located with sheet.rb. Matched to nova: the panel carries NO padding
|
|
2
|
+
(header/footer bring their own p-4; body content adds px-4), gap-4 column,
|
|
3
|
+
text-sm, title text-base/medium, close pinned top-3/right-3, side panels
|
|
4
|
+
75%-wide capped at 24rem. Overlay at the kit-unified black/10 + blur-xs
|
|
5
|
+
(see dialog.css). Theme tokens from the global stylesheet. */
|
|
2
6
|
.pk-sheet-trigger { display: inline-block; }
|
|
3
|
-
.pk-sheet-backdrop { position: fixed; inset: 0; z-index: 50; background: rgb(0 0 0 / .
|
|
7
|
+
.pk-sheet-backdrop { position: fixed; inset: 0; z-index: 50; background: rgb(0 0 0 / .1); backdrop-filter: blur(4px); }
|
|
8
|
+
@supports not (backdrop-filter: blur(1px)) {
|
|
9
|
+
.pk-sheet-backdrop { background: rgb(0 0 0 / .5); }
|
|
10
|
+
}
|
|
4
11
|
.pk-sheet-content {
|
|
5
12
|
position: fixed; z-index: 50; display: flex; flex-direction: column; gap: 1rem;
|
|
6
|
-
background: var(--pk-surface); color: var(--pk-text); padding:
|
|
13
|
+
background: var(--pk-surface); color: var(--pk-text); padding: 0;
|
|
14
|
+
font-size: .875rem;
|
|
7
15
|
box-shadow: 0 10px 30px rgb(0 0 0 / .35); overflow: auto;
|
|
8
16
|
}
|
|
9
|
-
.pk-sheet-content.right { inset-block: 0; right: 0; height: 100%; width:
|
|
10
|
-
.pk-sheet-content.left { inset-block: 0; left: 0; height: 100%; width:
|
|
17
|
+
.pk-sheet-content.right { inset-block: 0; right: 0; height: 100%; width: 75%; max-width: 24rem; border-left: 1px solid var(--pk-border); }
|
|
18
|
+
.pk-sheet-content.left { inset-block: 0; left: 0; height: 100%; width: 75%; max-width: 24rem; border-right: 1px solid var(--pk-border); }
|
|
11
19
|
.pk-sheet-content.top { inset-inline: 0; top: 0; border-bottom: 1px solid var(--pk-border); }
|
|
12
20
|
.pk-sheet-content.bottom { inset-inline: 0; bottom: 0; border-top: 1px solid var(--pk-border); }
|
|
13
|
-
.pk-sheet-header { display: flex; flex-direction: column; gap: .
|
|
14
|
-
.pk-sheet-footer { display: flex; flex-direction: column-reverse; gap: .5rem; }
|
|
15
|
-
.pk-sheet-title { font-size:
|
|
21
|
+
.pk-sheet-header { display: flex; flex-direction: column; gap: .125rem; padding: 1rem; }
|
|
22
|
+
.pk-sheet-footer { display: flex; flex-direction: column-reverse; gap: .5rem; padding: 1rem; margin-top: auto; }
|
|
23
|
+
.pk-sheet-title { font-size: 1rem; font-weight: 500; line-height: 1; color: var(--pk-text); }
|
|
16
24
|
.pk-sheet-description { font-size: .875rem; color: var(--pk-muted); }
|
|
17
|
-
.pk-sheet-middle { padding-
|
|
25
|
+
.pk-sheet-middle { flex: 1; padding-inline: 1rem; }
|
|
26
|
+
.pk-sheet-content > .pk-overlay-close { inset-inline-end: .75rem; top: .75rem; }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module PhlexKit
|
|
2
|
+
# Wraps any element (typically a Button) so clicking it closes the enclosing
|
|
3
|
+
# sheet, mirroring shadcn/ui's SheetClose. See sheet.rb.
|
|
4
|
+
class SheetClose < BaseComponent
|
|
5
|
+
def initialize(**attrs) = (@attrs = attrs)
|
|
6
|
+
def view_template(&)
|
|
7
|
+
span(**mix({ style: "display: contents", data: { action: "click->phlex-kit--sheet-content#close" } }, @attrs), &)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
class SheetContent < BaseComponent
|
|
3
3
|
SIDES = { top: "top", right: "right", bottom: "bottom", left: "left" }.freeze
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
def initialize(side: :right, show_close_button: true, **attrs)
|
|
5
6
|
@side = side.to_sym
|
|
7
|
+
@show_close_button = show_close_button
|
|
6
8
|
@attrs = attrs
|
|
7
9
|
end
|
|
8
10
|
def view_template(&block)
|
|
@@ -11,9 +13,11 @@ module PhlexKit
|
|
|
11
13
|
div(class: "pk-sheet-backdrop", data: { action: "click->phlex-kit--sheet-content#close" })
|
|
12
14
|
div(**mix({ class: ["pk-sheet-content", SIDES.fetch(@side)].join(" ") }, @attrs)) do
|
|
13
15
|
block&.call
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
if @show_close_button
|
|
17
|
+
button(type: "button", class: "pk-overlay-close", data: { action: "click->phlex-kit--sheet-content#close" }) do
|
|
18
|
+
render Icon.new(:x, size: 15)
|
|
19
|
+
span(class: "pk-sr-only") { "Close" }
|
|
20
|
+
end
|
|
17
21
|
end
|
|
18
22
|
end
|
|
19
23
|
end
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
/ MenuAction / MenuBadge / MenuSkeleton / MenuSub / MenuSubItem / MenuSubButton
|
|
4
4
|
/ Separator / Input / Inset). ruby_ui's structure at collapsible:none, extended
|
|
5
5
|
to shadcn/ui's static part set; Tailwind → vanilla CSS on the palette tokens.
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
Collapsible modes: offcanvas (slide away) and icon (3rem rail — labels
|
|
7
|
+
hide, buttons become icon squares with pure-CSS hover tooltips), both with
|
|
8
|
+
the mobile drawer + scrim, the SidebarRail grab strip, ⌘B, and cookie
|
|
9
|
+
persistence via the phlex-kit--sidebar controller.
|
|
8
10
|
Theme tokens come from the global stylesheet. The sidebar reads the shared
|
|
9
11
|
palette by default; --pk-sidebar[-text|-accent|-border|-primary|-primary-ink]
|
|
10
12
|
are optional per-role overrides (shadcn sidebar-* parity), undefined by
|
|
@@ -164,11 +166,70 @@
|
|
|
164
166
|
Mobile (<768px): the rail is a fixed overlay drawer behind a scrim
|
|
165
167
|
(data-open). The trigger only shows inside an offcanvas wrapper. The 767px
|
|
166
168
|
cutoff matches sidebar_controller.js. */
|
|
167
|
-
.pk-sidebar-wrapper.collapsible-offcanvas .pk-sidebar
|
|
168
|
-
|
|
169
|
+
.pk-sidebar-wrapper.collapsible-offcanvas .pk-sidebar,
|
|
170
|
+
.pk-sidebar-wrapper.collapsible-icon .pk-sidebar {
|
|
171
|
+
position: relative;
|
|
172
|
+
transition: margin-left .2s ease, transform .2s ease, width .2s ease;
|
|
169
173
|
}
|
|
170
174
|
.pk-sidebar-wrapper.collapsible-offcanvas[data-collapsed] .pk-sidebar { margin-left: -16rem; }
|
|
171
175
|
|
|
176
|
+
/* Icon mode: the rail shrinks to a 3rem strip — labels and text hide, menu
|
|
177
|
+
buttons become centered icon squares, hover shows a data-tooltip bubble. */
|
|
178
|
+
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar { width: 3rem; }
|
|
179
|
+
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] :is(.pk-sidebar-group-label, .pk-sidebar-group-action, .pk-sidebar-menu-badge, .pk-sidebar-menu-action, .pk-sidebar-menu-sub, .pk-sidebar-input, .pk-sidebar-separator) { display: none; }
|
|
180
|
+
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] :is(.pk-sidebar-header, .pk-sidebar-content, .pk-sidebar-footer) {
|
|
181
|
+
padding-inline: .5rem;
|
|
182
|
+
align-items: center;
|
|
183
|
+
}
|
|
184
|
+
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button {
|
|
185
|
+
width: 2rem;
|
|
186
|
+
height: 2rem;
|
|
187
|
+
padding: .5rem;
|
|
188
|
+
justify-content: center;
|
|
189
|
+
}
|
|
190
|
+
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button > :not(svg) { display: none; }
|
|
191
|
+
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button[data-tooltip] { position: relative; }
|
|
192
|
+
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button[data-tooltip]:hover::after {
|
|
193
|
+
content: attr(data-tooltip);
|
|
194
|
+
position: absolute;
|
|
195
|
+
left: calc(100% + .625rem);
|
|
196
|
+
top: 50%;
|
|
197
|
+
transform: translateY(-50%);
|
|
198
|
+
z-index: 60;
|
|
199
|
+
width: max-content;
|
|
200
|
+
padding: .375rem .75rem;
|
|
201
|
+
border-radius: calc(var(--pk-radius) - 2px);
|
|
202
|
+
background: var(--pk-text);
|
|
203
|
+
color: var(--pk-bg);
|
|
204
|
+
font-size: .75rem;
|
|
205
|
+
pointer-events: none;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* Edge grab strip (SidebarRail) — invisible, click toggles. */
|
|
209
|
+
.pk-sidebar-rail {
|
|
210
|
+
position: absolute;
|
|
211
|
+
inset-block: 0;
|
|
212
|
+
right: -.5rem;
|
|
213
|
+
z-index: 20;
|
|
214
|
+
width: 1rem;
|
|
215
|
+
border: 0;
|
|
216
|
+
padding: 0;
|
|
217
|
+
background: transparent;
|
|
218
|
+
cursor: w-resize;
|
|
219
|
+
display: none;
|
|
220
|
+
}
|
|
221
|
+
.pk-sidebar-wrapper.collapsible-offcanvas .pk-sidebar-rail,
|
|
222
|
+
.pk-sidebar-wrapper.collapsible-icon .pk-sidebar-rail { display: block; }
|
|
223
|
+
.pk-sidebar-wrapper[data-collapsed] .pk-sidebar-rail { cursor: e-resize; }
|
|
224
|
+
.pk-sidebar-rail:hover::after {
|
|
225
|
+
content: "";
|
|
226
|
+
position: absolute;
|
|
227
|
+
inset-block: 0;
|
|
228
|
+
left: 50%;
|
|
229
|
+
width: 2px;
|
|
230
|
+
background: var(--pk-sidebar-border, var(--pk-border));
|
|
231
|
+
}
|
|
232
|
+
|
|
172
233
|
.pk-sidebar-trigger {
|
|
173
234
|
display: none;
|
|
174
235
|
align-items: center;
|
|
@@ -184,12 +245,18 @@
|
|
|
184
245
|
}
|
|
185
246
|
.pk-sidebar-trigger:hover { background: var(--pk-accent); }
|
|
186
247
|
.pk-sidebar-trigger svg { width: 1rem; height: 1rem; }
|
|
187
|
-
.collapsible-offcanvas .pk-sidebar-trigger { display: inline-flex; }
|
|
248
|
+
:is(.collapsible-offcanvas, .collapsible-icon) .pk-sidebar-trigger { display: inline-flex; }
|
|
188
249
|
|
|
189
250
|
.pk-sidebar-scrim { display: none; }
|
|
190
251
|
|
|
191
252
|
@media (max-width: 767px) {
|
|
192
|
-
|
|
253
|
+
/* Below the cutoff both collapsible modes become the overlay drawer. */
|
|
254
|
+
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar { width: 16rem; }
|
|
255
|
+
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] :is(.pk-sidebar-group-label, .pk-sidebar-group-action, .pk-sidebar-menu-badge, .pk-sidebar-menu-action, .pk-sidebar-menu-sub, .pk-sidebar-input, .pk-sidebar-separator) { display: revert; }
|
|
256
|
+
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button { width: 100%; height: auto; padding: .5rem; justify-content: flex-start; }
|
|
257
|
+
.pk-sidebar-wrapper.collapsible-icon[data-collapsed] .pk-sidebar-menu-button > :not(svg) { display: revert; }
|
|
258
|
+
.pk-sidebar-wrapper.collapsible-icon .pk-sidebar-rail { display: none; }
|
|
259
|
+
.pk-sidebar-wrapper:is(.collapsible-offcanvas, .collapsible-icon) .pk-sidebar {
|
|
193
260
|
position: fixed;
|
|
194
261
|
inset-block: 0;
|
|
195
262
|
left: 0;
|
|
@@ -199,8 +266,8 @@
|
|
|
199
266
|
transform: translateX(-100%);
|
|
200
267
|
box-shadow: 0 8px 30px rgb(0 0 0 / .35);
|
|
201
268
|
}
|
|
202
|
-
.pk-sidebar-wrapper.collapsible-offcanvas[data-open] .pk-sidebar { transform: none; }
|
|
203
|
-
.pk-sidebar-wrapper.collapsible-offcanvas[data-open] .pk-sidebar-scrim {
|
|
269
|
+
.pk-sidebar-wrapper:is(.collapsible-offcanvas, .collapsible-icon)[data-open] .pk-sidebar { transform: none; }
|
|
270
|
+
.pk-sidebar-wrapper:is(.collapsible-offcanvas, .collapsible-icon)[data-open] .pk-sidebar-scrim {
|
|
204
271
|
display: block;
|
|
205
272
|
position: fixed;
|
|
206
273
|
inset: 0;
|
|
@@ -213,5 +280,5 @@
|
|
|
213
280
|
}
|
|
214
281
|
|
|
215
282
|
@media (prefers-reduced-motion: reduce) {
|
|
216
|
-
.pk-sidebar-wrapper.collapsible-offcanvas .pk-sidebar { transition: none; }
|
|
283
|
+
.pk-sidebar-wrapper:is(.collapsible-offcanvas, .collapsible-icon) .pk-sidebar { transition: none; }
|
|
217
284
|
}
|
|
@@ -1,23 +1,38 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
|
|
3
3
|
// Connects to data-controller="phlex-kit--sidebar" — set by SidebarWrapper when
|
|
4
|
-
// collapsible: :offcanvas. One attribute per surface, both DOM-only so
|
|
5
|
-
// page cache never restores an open drawer (the wrapper also closes on
|
|
4
|
+
// collapsible: :offcanvas or :icon. One attribute per surface, both DOM-only so
|
|
5
|
+
// Turbo's page cache never restores an open drawer (the wrapper also closes on
|
|
6
6
|
// turbo:before-cache): `data-open` shows the mobile overlay drawer,
|
|
7
|
-
// `data-collapsed`
|
|
8
|
-
// matches sidebar.css's
|
|
7
|
+
// `data-collapsed` collapses the desktop rail (offcanvas slides it away; icon
|
|
8
|
+
// mode shrinks it to a 3rem strip). The 767px cutoff matches sidebar.css's
|
|
9
|
+
// media query. The desktop state persists in a `pk_sidebar_state` cookie so a
|
|
10
|
+
// host layout can render the collapsed rail server-side (no flash) via
|
|
11
|
+
// SidebarWrapper's `default_collapsed:`.
|
|
9
12
|
export default class extends Controller {
|
|
10
13
|
static MOBILE = "(max-width: 767px)"
|
|
14
|
+
static COOKIE = "pk_sidebar_state"
|
|
11
15
|
|
|
12
|
-
toggle() {
|
|
13
|
-
if (
|
|
14
|
-
|
|
16
|
+
toggle(event) {
|
|
17
|
+
if (event?.type === "keydown" && !(event.metaKey || event.ctrlKey)) return
|
|
18
|
+
|
|
19
|
+
if (this.mobile) {
|
|
20
|
+
this.element.toggleAttribute("data-open")
|
|
21
|
+
} else {
|
|
22
|
+
this.element.toggleAttribute("data-collapsed")
|
|
23
|
+
this.persist()
|
|
24
|
+
}
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
closeMobile() {
|
|
18
28
|
this.element.removeAttribute("data-open")
|
|
19
29
|
}
|
|
20
30
|
|
|
31
|
+
persist() {
|
|
32
|
+
const state = this.element.hasAttribute("data-collapsed") ? "collapsed" : "expanded"
|
|
33
|
+
document.cookie = `${this.constructor.COOKIE}=${state}; path=/; max-age=${60 * 60 * 24 * 7}; samesite=lax`
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
get mobile() {
|
|
22
37
|
return window.matchMedia(this.constructor.MOBILE).matches
|
|
23
38
|
}
|
|
@@ -2,16 +2,21 @@ module PhlexKit
|
|
|
2
2
|
# The interactive element of a SidebarMenuItem. `as:` is :button (default) or :a
|
|
3
3
|
# (pass `href:`); `active: true` marks the current page (drives the highlight via
|
|
4
4
|
# data-active — a named kwarg, not an attr, since `mix` would merge a repeated
|
|
5
|
-
# attribute rather than override).
|
|
5
|
+
# attribute rather than override). `tooltip:` labels the button while an
|
|
6
|
+
# icon-collapsed rail hides its text (pure-CSS hover bubble). Attrs pass
|
|
7
|
+
# through via mix. See sidebar.rb.
|
|
6
8
|
class SidebarMenuButton < BaseComponent
|
|
7
|
-
def initialize(as: :button, active: false, **attrs)
|
|
9
|
+
def initialize(as: :button, active: false, tooltip: nil, **attrs)
|
|
8
10
|
@as = as
|
|
9
11
|
@active = active
|
|
12
|
+
@tooltip = tooltip
|
|
10
13
|
@attrs = attrs
|
|
11
14
|
end
|
|
12
15
|
|
|
13
16
|
def view_template(&block)
|
|
14
|
-
|
|
17
|
+
base = { class: "pk-sidebar-menu-button", "data-active": (@active ? "true" : "false") }
|
|
18
|
+
base["data-tooltip"] = @tooltip if @tooltip
|
|
19
|
+
send(@as, **mix(base, @attrs), &block)
|
|
15
20
|
end
|
|
16
21
|
end
|
|
17
22
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module PhlexKit
|
|
2
|
+
# Invisible grab strip along the sidebar's trailing edge — clicking it
|
|
3
|
+
# toggles the collapse, like shadcn/ui's SidebarRail. Place it as the last
|
|
4
|
+
# child of a Sidebar inside a collapsible wrapper. See sidebar.rb.
|
|
5
|
+
class SidebarRail < BaseComponent
|
|
6
|
+
def initialize(**attrs)
|
|
7
|
+
@attrs = attrs
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def view_template
|
|
11
|
+
button(**mix({
|
|
12
|
+
type: :button,
|
|
13
|
+
class: "pk-sidebar-rail",
|
|
14
|
+
aria_label: "Toggle sidebar",
|
|
15
|
+
tabindex: "-1",
|
|
16
|
+
title: "Toggle sidebar",
|
|
17
|
+
data: { action: "click->phlex-kit--sidebar#toggle" }
|
|
18
|
+
}, @attrs))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|