Skip to content

Commit

Permalink
Feature/shadcn sidebar (#1744)
Browse files Browse the repository at this point in the history
* Added sidebar component

* Added sidebar css styling (default colors / needs tweaking)

* WIP - Sidebar progress

* more cleanup

* updated sidebar style

* Fixed sidebar styling

huntabyte/shadcn-svelte#1496

* Fixed main content area

* Workaround open mobile issue.

* Removed dead code

* Fixed whitespace
  • Loading branch information
niemyjski authored Nov 18, 2024
1 parent b5cf043 commit 19b73ab
Show file tree
Hide file tree
Showing 43 changed files with 1,136 additions and 120 deletions.
1 change: 1 addition & 0 deletions src/Exceptionless.Web/ClientApp/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"keyof",
"legos",
"lucene",
"lucide",
"nameof",
"navigatetofirstpage",
"oidc",
Expand Down
11 changes: 11 additions & 0 deletions src/Exceptionless.Web/ClientApp/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src/Exceptionless.Web/ClientApp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"upgrade": "ncu -i"
},
"devDependencies": {
"@iconify-json/lucide": "^1.2.14",
"@playwright/test": "^1.48.2",
"@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/kit": "^2.8.1",
Expand Down Expand Up @@ -77,4 +78,4 @@
"unplugin-icons": "^0.20.1"
},
"type": "module"
}
}
18 changes: 18 additions & 0 deletions src/Exceptionless.Web/ClientApp/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@
--ring: 221 39% 11%;

--radius: 0.375rem;

--sidebar-background: var(--background);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--accent-foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
}

.dark {
Expand Down Expand Up @@ -66,6 +75,15 @@
--destructive-foreground: 0 0% 100%;

--ring: 96 64.1% 45.88%;

--sidebar-background: var(--background);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--accent-foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,13 @@

{#if canPromote}
{#if !isPromoted}
<Button onclick={async () => await promote(title)} size="icon" title="Promote to Tab"><ArrowUpIcon /></Button>
<Button onclick={async () => await promote(title)} size="icon" title="Promote to Tab"
><ArrowUpIcon /><span class="sr-only">Promote to Tab</span></Button
>
{:else}
<Button onclick={async () => await demote(title)} size="icon" title="Demote Tab"><ArrowDownIcon /></Button>
<Button onclick={async () => await demote(title)} size="icon" title="Demote Tab"
><ArrowDownIcon /><span class="sr-only">Demote Tab</span></Button
>
{/if}
{/if}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
<ContextMenu.Root>
<ContextMenu.Trigger>
<Button onclick={toggleMode} size="icon" title="Toggle dark mode" variant="outline">
<IconWhiteBalanceSunny class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<IconMoonWaningCrescent class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<IconWhiteBalanceSunny class="rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<IconMoonWaningCrescent class="absolute ml-1 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span class="sr-only">Toggle theme</span>
</Button>
</ContextMenu.Trigger>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const SIDEBAR_COOKIE_NAME = "sidebar:state";
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
export const SIDEBAR_WIDTH = "16rem";
export const SIDEBAR_WIDTH_MOBILE = "18rem";
export const SIDEBAR_WIDTH_ICON = "3rem";
export const SIDEBAR_KEYBOARD_SHORTCUT = "b";
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { IsMobile } from "$lib/hooks/is-mobile.svelte.js";
import { getContext, setContext } from "svelte";
import { SIDEBAR_KEYBOARD_SHORTCUT } from "./constants.js";

type Getter<T> = () => T;

export type SidebarStateProps = {
/**
* A getter function that returns the current open state of the sidebar.
* We use a getter function here to support `bind:open` on the `Sidebar.Provider`
* component.
*/
open: Getter<boolean>;

/**
* A function that sets the open state of the sidebar. To support `bind:open`, we need
* a source of truth for changing the open state to ensure it will be synced throughout
* the sub-components and any `bind:` references.
*/
setOpen: (open: boolean) => void;
};

class SidebarState {
readonly props: SidebarStateProps;
open = $derived.by(() => this.props.open());
openMobile = $state(false);
setOpen: SidebarStateProps["setOpen"];
#isMobile: IsMobile;
state = $derived.by(() => (this.open ? "expanded" : "collapsed"));

constructor(props: SidebarStateProps) {
this.setOpen = props.setOpen;
this.#isMobile = new IsMobile();
this.props = props;
}

// Convenience getter for checking if the sidebar is mobile
// without this, we would need to use `sidebar.isMobile.current` everywhere
get isMobile() {
return this.#isMobile.current;
}

// Event handler to apply to the `<svelte:window>`
handleShortcutKeydown = (e: KeyboardEvent) => {
if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
this.toggle();
}
};

setOpenMobile = (value: boolean) => {
this.openMobile = value;
};

toggle = () => {
return this.#isMobile.current
? (this.openMobile = !this.openMobile)
: this.setOpen(!this.open);
};
}

const SYMBOL_KEY = "scn-sidebar";

/**
* Instantiates a new `SidebarState` instance and sets it in the context.
*
* @param props The constructor props for the `SidebarState` class.
* @returns The `SidebarState` instance.
*/
export function setSidebar(props: SidebarStateProps): SidebarState {
return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props));
}

/**
* Retrieves the `SidebarState` instance from the context. This is a class instance,
* so you cannot destructure it.
* @returns The `SidebarState` instance.
*/
export function useSidebar(): SidebarState {
return getContext(Symbol.for(SYMBOL_KEY));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useSidebar } from "./context.svelte.js";
import Content from "./sidebar-content.svelte";
import Footer from "./sidebar-footer.svelte";
import GroupAction from "./sidebar-group-action.svelte";
import GroupContent from "./sidebar-group-content.svelte";
import GroupLabel from "./sidebar-group-label.svelte";
import Group from "./sidebar-group.svelte";
import Header from "./sidebar-header.svelte";
import Input from "./sidebar-input.svelte";
import Inset from "./sidebar-inset.svelte";
import MenuAction from "./sidebar-menu-action.svelte";
import MenuBadge from "./sidebar-menu-badge.svelte";
import MenuButton from "./sidebar-menu-button.svelte";
import MenuItem from "./sidebar-menu-item.svelte";
import MenuSkeleton from "./sidebar-menu-skeleton.svelte";
import MenuSubButton from "./sidebar-menu-sub-button.svelte";
import MenuSubItem from "./sidebar-menu-sub-item.svelte";
import MenuSub from "./sidebar-menu-sub.svelte";
import Menu from "./sidebar-menu.svelte";
import Provider from "./sidebar-provider.svelte";
import Rail from "./sidebar-rail.svelte";
import Separator from "./sidebar-separator.svelte";
import Trigger from "./sidebar-trigger.svelte";
import Root from "./sidebar.svelte";

export {
Content,
Footer,
Group,
GroupAction,
GroupContent,
GroupLabel,
Header,
Input,
Inset,
Menu,
MenuAction,
MenuBadge,
MenuButton,
MenuItem,
MenuSkeleton,
MenuSub,
MenuSubButton,
MenuSubItem,
Provider,
Rail,
Root,
Separator,
//
Root as Sidebar,
Content as SidebarContent,
Footer as SidebarFooter,
Group as SidebarGroup,
GroupAction as SidebarGroupAction,
GroupContent as SidebarGroupContent,
GroupLabel as SidebarGroupLabel,
Header as SidebarHeader,
Input as SidebarInput,
Inset as SidebarInset,
Menu as SidebarMenu,
MenuAction as SidebarMenuAction,
MenuBadge as SidebarMenuBadge,
MenuButton as SidebarMenuButton,
MenuItem as SidebarMenuItem,
MenuSkeleton as SidebarMenuSkeleton,
MenuSub as SidebarMenuSub,
MenuSubButton as SidebarMenuSubButton,
MenuSubItem as SidebarMenuSubItem,
Provider as SidebarProvider,
Rail as SidebarRail,
Separator as SidebarSeparator,
Trigger as SidebarTrigger,
Trigger,
useSidebar,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>

<div
bind:this={ref}
data-sidebar="content"
class={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
)}
{...restProps}
>
{@render children?.()}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>

<div
bind:this={ref}
data-sidebar="footer"
class={cn("flex flex-col gap-2 p-2", className)}
{...restProps}
>
{@render children?.()}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { Snippet } from "svelte";
import type { HTMLButtonAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
child,
...restProps
}: WithElementRef<HTMLButtonAttributes> & {
child?: Snippet<[{ props: Record<string, unknown> }]>;
} = $props();
const propObj = $derived({
class: cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",
className
),
"data-sidebar": "group-action",
...restProps,
});
</script>

{#if child}
{@render child({ props: propObj })}
{:else}
<button bind:this={ref} {...propObj}>
{@render children?.()}
</button>
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>

<div
bind:this={ref}
data-sidebar="group-content"
class={cn("w-full text-sm", className)}
{...restProps}
>
{@render children?.()}
</div>
Loading

0 comments on commit 19b73ab

Please sign in to comment.