Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
76af8c7
Setup sidebar
lsouoliveira Mar 20, 2025
8e235d5
Add the sidebar group component
lsouoliveira Mar 20, 2025
acddae5
Add sidebar group label
lsouoliveira Mar 20, 2025
f07ff5c
Add the sidebar group content componeent
lsouoliveira Mar 20, 2025
2ab1180
add sidebar menu
lsouoliveira Mar 20, 2025
80c649c
add sidebar menu item
lsouoliveira Mar 20, 2025
f2b2fc1
rename group
lsouoliveira Mar 21, 2025
18e4cff
add sidebar submenu item
lsouoliveira Mar 21, 2025
d16d1e6
add sidebar header
lsouoliveira Mar 21, 2025
3841250
add sidebar footer
lsouoliveira Mar 21, 2025
ee42dd0
add sidebar group action
lsouoliveira Mar 21, 2025
b4e99f2
add sidebar menu action
lsouoliveira Mar 21, 2025
82d587e
fix state
lsouoliveira Mar 21, 2025
8c43722
add menu badge component
lsouoliveira Mar 21, 2025
13d371b
break long class strings into shorter ones
lsouoliveira Mar 22, 2025
5bdf56e
add sidebar menu sub
lsouoliveira Mar 22, 2025
2e622c1
add sidebar menu sub item
lsouoliveira Mar 22, 2025
99fe49a
fix variant instance not being set
lsouoliveira Mar 22, 2025
c12ed44
add sidebar menu sub button
lsouoliveira Mar 22, 2025
54bcfb1
add sidebar menu skeleton
lsouoliveira Mar 22, 2025
9f23812
rename sidebar-wrapper group to sidebar
lsouoliveira Mar 22, 2025
3f24041
add the sidebar trigger component
lsouoliveira Mar 22, 2025
3f20f17
add the sidebar controller
lsouoliveira Mar 22, 2025
f65cdb6
setup the sidebar controller
lsouoliveira Mar 22, 2025
578da75
add a open flag
lsouoliveira Mar 22, 2025
bd8e932
fix collapsible value
lsouoliveira Mar 22, 2025
20b573d
persist sidebar state in a cookie
lsouoliveira Mar 22, 2025
696bbf2
fix icon attributes
lsouoliveira Mar 22, 2025
4d22ec9
lint files
lsouoliveira Mar 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions lib/ruby_ui/sidebar/collapsiable_sidebar.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# frozen_string_literal: true

module RubyUI
class CollapsiableSidebar < Base
def initialize(side: :left, variant: :sidebar, **attrs)
@side = side
@variant = variant
super(**attrs)
end

def view_template(&)
div(**attrs) do
div(**gap_element_attrs)
div(**content_wrapper_attrs) do
div(**content_attrs, &)
end
end
end

private

def default_attrs
{
class: "peer hidden text-sidebar-foreground md:block"
}
end

def gap_element_attrs
{
class: [
"relative w-[--sidebar-width] bg-transparent transition-[width]",
"duration-200 ease-linear",
"group-data-[collapsible=offcanvas]/sidebar:w-0",
"group-data-[side=right]/sidebar:rotate-180",
variant_classes
]
}
end

def content_wrapper_attrs
{
class: [
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width]",
"transition-[left,right,width] duration-200 ease-linear md:flex",
content_wrapper_side_classes,
content_wrapper_variant_classes
]
}
end

def content_attrs
{
class: [
"flex h-full w-full flex-col bg-sidebar",
"group-data-[variant=floating]/sidebar:rounded-lg",
"group-data-[variant=floating]/sidebar:border",
"group-data-[variant=floating]/sidebar:border-sidebar-border",
"group-data-[variant=floating]/sidebar:shadow"
],
data: {
sidebar: "sidebar"
}
}
end

def variant_classes
if %i[floating inset].include?(@variant)
"group-data-[collapsible=icon]/sidebar:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
else
"group-data-[collapsible=icon]/sidebar:w-[--sidebar-width-icon]"
end
end

def content_wrapper_side_classes
return "left-0 group-data-[collapsible=offcanvas]/sidebar:left-[calc(var(--sidebar-width)*-1)]" if @side == :left

"right-0 group-data-[collapsible=offcanvas]/sidebar:right-[calc(var(--sidebar-width)*-1)]"
end

def content_wrapper_variant_classes
if %i[floating inset].include?(@variant)
"p-2 group-data-[collapsible=icon]/sidebar:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
else
"group-data-[collapsible=icon]/sidebar:w-[--sidebar-width-icon] group-data-[side=left]/sidebar:border-r group-data-[side=right]/sidebar:border-l"
end
end
end
end
16 changes: 16 additions & 0 deletions lib/ruby_ui/sidebar/mobile_sidebar.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

module RubyUI
class MobileSidebar < Base
def view_template(&)
div(**attrs) do
end
end

private

def default_attrs
{}
end
end
end
17 changes: 17 additions & 0 deletions lib/ruby_ui/sidebar/non_collapsible_sidebar.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module RubyUI
class NonCollpapsibleSidebar < Base
def view_template(&)
div(**attrs, &)
end

private

def default_attrs
{
class: "flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground"
}
end
end
end
63 changes: 63 additions & 0 deletions lib/ruby_ui/sidebar/sidebar.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# frozen_string_literal: true

# "--sidebar-width": SIDEBAR_WIDTH,
# "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,

# TODO: Add keyboard events
# TODO: Open only mobile or normal sidebar
# TODO: state => expanded, collapsed
# TODO: cache

# const MOBILE_BREAKPOINT = 768

module RubyUI
class Sidebar < Base
SIDEBAR_WIDTH = "16rem"
SIDEBAR_WIDTH_ICON = "3rem"

SIDES = %i[left right].freeze
VARIANTS = %i[sidebar floating inset].freeze
COLLAPSIBLES = %i[offcanvas icon none].freeze

def initialize(side: :left, variant: :sidebar, collapsible: :offcanvas, open: true, **attrs)
raise ArgumentError, "Invalid side: #{side}. Must be one of #{SIDES}." unless SIDES.include?(side)
raise ArgumentError, "Invalid variant: #{variant}. Must be one of #{VARIANTS}." unless VARIANTS.include?(variant)
raise ArgumentError, "Invalid collapsible: #{collapsible}. Must be one of #{COLLAPSIBLES}." unless COLLAPSIBLES.include?(collapsible)

@side = side
@variant = variant
@collapsible = collapsible
@open = open
super(**attrs)
end

def view_template(&)
div(**attrs) do
if @collapsible == :none
NonCollapsiableSidebar(&)
else
MobileSidebar(&)
CollapsiableSidebar(&)
end
end
end

private

def default_attrs
{
class: "group/sidebar has-[[data-variant=inset]]:bg-sidebar",
style: "--sidebar-width: #{SIDEBAR_WIDTH}; --sidebar-width-icon: #{SIDEBAR_WIDTH_ICON};",
data: {
controller: "ruby-ui--sidebar",
state: @open ? "expanded" : "collapsed",
collapsible: @open ? "" : @collapsible,
variant: @variant,
side: @side,
ruby_ui__sidebar_open_value: @open.to_s,
ruby_ui__sidebar_collapsible_value: @collapsible
}
}
end
end
end
20 changes: 20 additions & 0 deletions lib/ruby_ui/sidebar/sidebar_content.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module RubyUI
class SidebarContent < Base
def view_template(&)
div(**attrs, &)
end

private

def default_attrs
{
class: "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]/sidebar:overflow-hidden",
data: {
sidebar: "content"
}
}
end
end
end
66 changes: 66 additions & 0 deletions lib/ruby_ui/sidebar/sidebar_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Controller } from "@hotwired/stimulus";

const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const TRIGGER_SELECTOR = "[data-sidebar='trigger']";
const State = {
EXPANDED: "expanded",
COLLAPSED: "collapsed",
};

export default class extends Controller {
static values = {
open: {
type: Boolean,
default: true,
},
collapsible: {
type: String,
default: "offcanvas",
},
};

connect() {
this.#setupTriggers();
}

disconnect() {
this.#removeTriggers();
}

toggle() {
this.openValue = !this.openValue;
}

openValueChanged() {
this.#toggleSidebarDataState();
this.#persistSidebarState();
}

#setupTriggers() {
this.#triggerElements().forEach((trigger) => {
trigger.addEventListener("click", this.toggle.bind(this));
});
}

#removeTriggers() {
this.#triggerElements().forEach((trigger) => {
trigger.removeEventListener("click", this.toggle.bind(this));
});
}

#triggerElements() {
return document.querySelectorAll(TRIGGER_SELECTOR);
}

#toggleSidebarDataState() {
const { dataset } = this.element;

dataset.state = this.openValue ? State.EXPANDED : State.COLLAPSED;
dataset.collapsible = this.openValue ? "" : this.collapsibleValue;
}

#persistSidebarState() {
document.cookie = `${SIDEBAR_COOKIE_NAME}=${this.openValue}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
}
}
20 changes: 20 additions & 0 deletions lib/ruby_ui/sidebar/sidebar_footer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module RubyUI
class SidebarFooter < Base
def view_template(&)
div(**attrs, &)
end

private

def default_attrs
{
class: "flex flex-col gap-2 p-2",
data: {
sidebar: "footer"
}
}
end
end
end
20 changes: 20 additions & 0 deletions lib/ruby_ui/sidebar/sidebar_group.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module RubyUI
class SidebarGroup < Base
def view_template(&)
div(**attrs, &)
end

private

def default_attrs
{
class: "relative flex w-full min-w-0 flex-col p-2",
data: {
sidebar: "group"
}
}
end
end
end
33 changes: 33 additions & 0 deletions lib/ruby_ui/sidebar/sidebar_group_action.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

module RubyUI
class SidebarGroupAction < Base
def initialize(as: "button", **attrs)
@as = as
super(**attrs)
end

def view_template(&)
public_send(@as, **attrs, &)
end

private

def default_attrs
{
class: [
"absolute right-3 top-3.5 flex aspect-square w-5 items-center",
"justify-center rounded-md p-0 text-sidebar-foreground",
"outline-none ring-sidebar-ring transition-transform",
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
"focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]/sidebar:hidden"
],
data: {
sidebar: "group-action"
}
}
end
end
end
20 changes: 20 additions & 0 deletions lib/ruby_ui/sidebar/sidebar_group_content.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module RubyUI
class SidebarGroupContent < Base
def view_template(&)
div(**attrs, &)
end

private

def default_attrs
{
class: "w-full text-sm",
data: {
sidebar: "group-content"
}
}
end
end
end
26 changes: 26 additions & 0 deletions lib/ruby_ui/sidebar/sidebar_group_label.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

module RubyUI
class SidebarGroupLabel < Base
def view_template(&)
div(**attrs, &)
end

private

def default_attrs
{
class: [
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs",
"font-medium text-sidebar-foreground/70 outline-none",
"ring-sidebar-ring transition-[margin,opacity] duration-200",
"ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]/sidebar:-mt-8 group-data-[collapsible=icon]/sidebar:opacity-0"
],
data: {
sidebar: "group-label"
}
}
end
end
end
Loading