Skip to content

Commit c8610db

Browse files
authored
Add sidebar component (#235)
* Setup sidebar * Add the sidebar group component * Add sidebar group label * Add the sidebar group content componeent * add sidebar menu * add sidebar menu item * rename group * add sidebar submenu item * add sidebar header * add sidebar footer * add sidebar group action * add sidebar menu action * fix state * add menu badge component * break long class strings into shorter ones * add sidebar menu sub * add sidebar menu sub item * fix variant instance not being set * add sidebar menu sub button * add sidebar menu skeleton * rename sidebar-wrapper group to sidebar * add the sidebar trigger component * add the sidebar controller * setup the sidebar controller * add a open flag * fix collapsible value * persist sidebar state in a cookie * fix icon attributes * lint files * rename sidebar trigger data identifier * add the sidebar rail component * add the sidebar input component * convert attributes to symbol * add sidebar inset component * setup the mobile sidebar * remove todo comments * add sidebar wrapper * move sidebar to another component * fix sidebar rail * fix inset styling * add tests * fix lint * add sidebar separator component * add separator to tests * fix broken variables * add .to_s to boolean data attributes * lint code * replace public_send with tag
1 parent 1a21bdb commit c8610db

28 files changed

+986
-0
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# frozen_string_literal: true
2+
3+
module RubyUI
4+
class CollapsiableSidebar < Base
5+
def initialize(side: :left, variant: :sidebar, collapsible: :offcanvas, open: true, **attrs)
6+
@side = side
7+
@variant = variant
8+
@collapsible = collapsible
9+
@open = open
10+
super(**attrs)
11+
end
12+
13+
def view_template(&)
14+
MobileSidebar(side: @side, **attrs, &)
15+
div(**mix(sidebar_attrs, attrs)) do
16+
div(**gap_element_attrs)
17+
div(**content_wrapper_attrs) do
18+
div(**content_attrs, &)
19+
end
20+
end
21+
end
22+
23+
private
24+
25+
def sidebar_attrs
26+
{
27+
class: "group peer hidden text-sidebar-foreground md:block",
28+
data: {
29+
state: @open ? "expanded" : "collapsed",
30+
collapsible: @open ? "" : @collapsible,
31+
variant: @variant,
32+
side: @side,
33+
collapsible_kind: @collapsible,
34+
ruby_ui__sidebar_target: "sidebar"
35+
}
36+
}
37+
end
38+
39+
def gap_element_attrs
40+
{
41+
class: [
42+
"relative w-[var(--sidebar-width)] bg-transparent transition-[width]",
43+
"duration-200 ease-linear",
44+
"group-data-[collapsible=offcanvas]:w-0",
45+
"group-data-[side=right]:rotate-180",
46+
variant_classes
47+
]
48+
}
49+
end
50+
51+
def content_wrapper_attrs
52+
{
53+
class: [
54+
"fixed inset-y-0 z-10 hidden h-svh w-[var(--sidebar-width)]",
55+
"transition-[left,right,width] duration-200 ease-linear md:flex",
56+
content_wrapper_side_classes,
57+
content_wrapper_variant_classes
58+
]
59+
}
60+
end
61+
62+
def content_attrs
63+
{
64+
class: [
65+
"flex h-full w-full flex-col bg-sidebar",
66+
"group-data-[variant=floating]:rounded-lg",
67+
"group-data-[variant=floating]:border",
68+
"group-data-[variant=floating]:border-sidebar-border",
69+
"group-data-[variant=floating]:shadow"
70+
],
71+
data: {
72+
sidebar: "sidebar"
73+
}
74+
}
75+
end
76+
77+
def variant_classes
78+
if %i[floating inset].include?(@variant)
79+
"group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
80+
else
81+
"group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)]"
82+
end
83+
end
84+
85+
def content_wrapper_side_classes
86+
return "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" if @side == :left
87+
88+
"right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]"
89+
end
90+
91+
def content_wrapper_variant_classes
92+
if %i[floating inset].include?(@variant)
93+
"p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
94+
else
95+
"group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)] group-data-[side=left]:border-r group-data-[side=right]:border-l"
96+
end
97+
end
98+
end
99+
end
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# frozen_string_literal: true
2+
3+
module RubyUI
4+
class MobileSidebar < Base
5+
SIDEBAR_WIDTH_MOBILE = "18rem"
6+
7+
def initialize(side: :left, **attrs)
8+
@side = side
9+
super(**attrs)
10+
end
11+
12+
def view_template(&)
13+
Sheet(**attrs) do
14+
SheetContent(
15+
side: @side,
16+
class: "w-[var(--sidebar-width)] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden",
17+
style: {
18+
"--sidebar-width": SIDEBAR_WIDTH_MOBILE
19+
},
20+
data: {
21+
sidebar: "sidebar",
22+
mobile: "true"
23+
}
24+
) do
25+
SheetHeader(class: "sr-only") do
26+
SheetTitle { "Sidebar" }
27+
SheetDescription { "Displays the mobile sidebar." }
28+
end
29+
div(class: "flex h-full w-full flex-col", &)
30+
end
31+
end
32+
end
33+
34+
private
35+
36+
def default_attrs
37+
{
38+
data: {
39+
ruby_ui__sidebar_target: "mobileSidebar",
40+
action: "ruby--ui-sidebar:open->ruby-ui--sheet#open:self"
41+
}
42+
}
43+
end
44+
end
45+
end
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# frozen_string_literal: true
2+
3+
module RubyUI
4+
class NonCollpapsibleSidebar < Base
5+
def view_template(&)
6+
div(**attrs, &)
7+
end
8+
9+
private
10+
11+
def default_attrs
12+
{
13+
class: "flex h-full w-[var(--sidebar-width)] flex-col bg-sidebar text-sidebar-foreground"
14+
}
15+
end
16+
end
17+
end

lib/ruby_ui/sidebar/sidebar.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# frozen_string_literal: true
2+
3+
module RubyUI
4+
class Sidebar < Base
5+
SIDES = %i[left right].freeze
6+
VARIANTS = %i[sidebar floating inset].freeze
7+
COLLAPSIBLES = %i[offcanvas icon none].freeze
8+
9+
def initialize(side: :left, variant: :sidebar, collapsible: :offcanvas, open: true, **attrs)
10+
raise ArgumentError, "Invalid side: #{side}." unless SIDES.include?(side.to_sym)
11+
raise ArgumentError "Invalid variant: #{variant}." unless VARIANTS.include?(variant.to_sym)
12+
raise ArgumentError, "Invalid collapsible: #{collapsible}." unless COLLAPSIBLES.include?(collapsible.to_sym)
13+
14+
@side = side.to_sym
15+
@variant = variant.to_sym
16+
@collapsible = collapsible.to_sym
17+
@open = open
18+
super(**attrs)
19+
end
20+
21+
def view_template(&)
22+
if @collapsible == :none
23+
NonCollapsiableSidebar(**attrs, &)
24+
else
25+
CollapsiableSidebar(side: @side, variant: @variant, collapsible: @collapsible, open: @open, **attrs, &)
26+
end
27+
end
28+
end
29+
end
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
module RubyUI
4+
class SidebarContent < Base
5+
def view_template(&)
6+
div(**attrs, &)
7+
end
8+
9+
private
10+
11+
def default_attrs
12+
{
13+
class: "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
14+
data: {
15+
sidebar: "content"
16+
}
17+
}
18+
end
19+
end
20+
end
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Controller } from "@hotwired/stimulus";
2+
3+
const SIDEBAR_COOKIE_NAME = "sidebar_state";
4+
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
5+
const State = {
6+
EXPANDED: "expanded",
7+
COLLAPSED: "collapsed",
8+
};
9+
const MOBILE_BREAKPOINT = 768;
10+
11+
export default class extends Controller {
12+
static targets = ["sidebar", "mobileSidebar"];
13+
14+
sidebarTargetConnected() {
15+
const { state, collapsibleKind } = this.sidebarTarget.dataset;
16+
17+
this.open = state === State.EXPANDED;
18+
this.collapsibleKind = collapsibleKind;
19+
}
20+
21+
toggle(e) {
22+
e.preventDefault();
23+
24+
if (this.#isMobile()) {
25+
this.#openMobileSidebar();
26+
27+
return;
28+
}
29+
30+
this.open = !this.open;
31+
this.onToggle();
32+
}
33+
34+
onToggle() {
35+
this.#updateSidebarState();
36+
this.#persistSidebarState();
37+
}
38+
39+
#updateSidebarState() {
40+
if (!this.hasSidebarTarget) {
41+
return;
42+
}
43+
44+
const { dataset } = this.sidebarTarget;
45+
46+
dataset.state = this.open ? State.EXPANDED : State.COLLAPSED;
47+
dataset.collapsible = this.open ? "" : this.collapsibleKind;
48+
}
49+
50+
#persistSidebarState() {
51+
document.cookie = `${SIDEBAR_COOKIE_NAME}=${this.open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
52+
}
53+
54+
#isMobile() {
55+
return window.innerWidth < MOBILE_BREAKPOINT;
56+
}
57+
58+
#openMobileSidebar() {
59+
if (!this.hasMobileSidebarTarget) {
60+
return;
61+
}
62+
63+
this.mobileSidebarTarget.dispatchEvent(
64+
new CustomEvent("ruby--ui-sidebar:open"),
65+
);
66+
}
67+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
module RubyUI
4+
class SidebarFooter < Base
5+
def view_template(&)
6+
div(**attrs, &)
7+
end
8+
9+
private
10+
11+
def default_attrs
12+
{
13+
class: "flex flex-col gap-2 p-2",
14+
data: {
15+
sidebar: "footer"
16+
}
17+
}
18+
end
19+
end
20+
end
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
module RubyUI
4+
class SidebarGroup < Base
5+
def view_template(&)
6+
div(**attrs, &)
7+
end
8+
9+
private
10+
11+
def default_attrs
12+
{
13+
class: "relative flex w-full min-w-0 flex-col p-2",
14+
data: {
15+
sidebar: "group"
16+
}
17+
}
18+
end
19+
end
20+
end
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# frozen_string_literal: true
2+
3+
module RubyUI
4+
class SidebarGroupAction < Base
5+
def initialize(as: :button, **attrs)
6+
@as = as
7+
super(**attrs)
8+
end
9+
10+
def view_template(&)
11+
tag(@as, **attrs, &)
12+
end
13+
14+
private
15+
16+
def default_attrs
17+
{
18+
class: [
19+
"absolute right-3 top-3.5 flex aspect-square w-5 items-center",
20+
"justify-center rounded-md p-0 text-sidebar-foreground",
21+
"outline-none ring-sidebar-ring transition-transform",
22+
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
23+
"focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
24+
"after:absolute after:-inset-2 after:md:hidden",
25+
"group-data-[collapsible=icon]:hidden"
26+
],
27+
data: {
28+
sidebar: "group-action"
29+
}
30+
}
31+
end
32+
end
33+
end
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
module RubyUI
4+
class SidebarGroupContent < Base
5+
def view_template(&)
6+
div(**attrs, &)
7+
end
8+
9+
private
10+
11+
def default_attrs
12+
{
13+
class: "w-full text-sm",
14+
data: {
15+
sidebar: "group-content"
16+
}
17+
}
18+
end
19+
end
20+
end

0 commit comments

Comments
 (0)