Skip to content

Commit

Permalink
feat: app shell component
Browse files Browse the repository at this point in the history
  • Loading branch information
jrasm91 committed Dec 19, 2024
1 parent 5f536ec commit a809842
Show file tree
Hide file tree
Showing 31 changed files with 438 additions and 186 deletions.
71 changes: 0 additions & 71 deletions src/docs/components/DualThemeLayout.svelte

This file was deleted.

31 changes: 31 additions & 0 deletions src/docs/components/ExampleLayout.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script lang="ts">
import Examples from '$docs/components/Examples.svelte';
import { Theme, type ExampleItem } from '$docs/constants.js';
import { Scrollable, Stack } from '@immich/ui';
type Props = {
name: string;
examples: ExampleItem[];
};
const { name, examples }: Props = $props();
</script>

<div class="flex h-full flex-col">
<!-- TODO replace with breadcrumb component -->
<nav
class="flex shrink-0 justify-between border-b border-gray-300 bg-light px-8 py-2 text-dark dark:border-gray-700"
>
<div class="flex items-center gap-2">
<a href="/" class="underline">Home</a>
<span>/</span>
<span class="capitalize">{name}</span>
</div>
</nav>

<Scrollable>
<Stack gap={4} class="max-w-screen-lg p-4">
<Examples theme={Theme.Dark} {examples} />
</Stack>
</Scrollable>
</div>
4 changes: 1 addition & 3 deletions src/docs/components/Navbar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@
const { children, theme = Theme.Dark }: Props = $props();
</script>

<nav
class="{theme} flex items-center justify-between gap-2 border-b border-gray-300 bg-light px-8 py-4 text-dark"
>
<nav class="{theme} flex items-center justify-between gap-2 p-2">
<a href="/" class="flex gap-2 text-4xl">
<Logo variant="inline" {theme} />
</a>
Expand Down
34 changes: 34 additions & 0 deletions src/lib/components/AppShell/AppShell.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script lang="ts">
import { withChildrenSnippets } from '$lib/common/use-child.svelte.js';
import Scrollable from '$lib/components/Scrollable/Scrollable.svelte';
import { ChildKey } from '$lib/constants.js';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
type Props = {
class?: string;
children?: Snippet;
};
const { class: className, children }: Props = $props();
const { getChildren: getChildSnippet } = withChildrenSnippets(ChildKey.AppShell);
const header = $derived(getChildSnippet(ChildKey.AppShellHeader));
const sidebar = $derived(getChildSnippet(ChildKey.AppShellSidebar));
</script>

<div class={cleanClass('flex h-screen flex-col overflow-hidden', className)}>
{#if header}
<header class="border-b border-gray-300 dark:border-gray-700">
{@render header?.()}
</header>
{/if}
<div class="flex w-full grow overflow-y-auto">
{#if sidebar}
{@render sidebar()}
{/if}
<Scrollable class="grow">
{@render children?.()}
</Scrollable>
</div>
</div>
15 changes: 15 additions & 0 deletions src/lib/components/AppShell/AppShellHeader.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script lang="ts">
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import type { Snippet } from 'svelte';
type Props = {
children: Snippet;
};
let { children }: Props = $props();
</script>

<Child for={ChildKey.AppShell} as={ChildKey.AppShellHeader}>
{@render children?.()}
</Child>
25 changes: 25 additions & 0 deletions src/lib/components/AppShell/AppShellSidebar.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script lang="ts">
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import { cleanClass } from '$lib/utils.js';
import Scrollable from '$lib/components/Scrollable/Scrollable.svelte';
import type { Snippet } from 'svelte';
type Props = {
class?: string;
children: Snippet;
};
let { class: className, children }: Props = $props();
</script>

<Child for={ChildKey.AppShell} as={ChildKey.AppShellSidebar}>
<Scrollable
class={cleanClass(
'hidden h-full shrink-0 border-r border-gray-200 dark:border-gray-700 lg:block',
className,
)}
>
{@render children?.()}
</Scrollable>
</Child>
44 changes: 44 additions & 0 deletions src/lib/components/AppShell/PageLayout.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
title?: string | undefined;
description?: string | undefined;
scrollbar?: boolean;
buttons?: Snippet;
children?: Snippet;
}
let {
title = undefined,
description = undefined,
scrollbar = true,
buttons,
children,
}: Props = $props();
let scrollbarClass = $derived(scrollbar ? 'immich-scrollbar p-2 pb-8' : 'scrollbar-hidden');
let hasTitleClass = $derived(title ? 'top-16 h-[calc(100%-theme(spacing.16))]' : 'top-0 h-full');
</script>

<section class="relative">
{#if title || buttons}
<div
class="dark:border-immich-dark-gray dark:text-immich-dark-fg absolute flex h-16 w-full place-items-center justify-between border-b p-4"
>
<div class="flex items-center gap-2">
{#if title}
<div class="font-medium" tabindex="-1">{title}</div>
{/if}
{#if description}
<p class="text-sm text-gray-400 dark:text-gray-600">{description}</p>
{/if}
</div>
{@render buttons?.()}
</div>
{/if}

<div class="{scrollbarClass} scrollbar-stable absolute {hasTitleClass} w-full overflow-y-auto">
{@render children?.()}
</div>
</section>
40 changes: 40 additions & 0 deletions src/lib/components/Scrollable/Scrollable.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<script lang="ts">
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
type Props = {
class?: string;
children?: Snippet;
};
const { class: className, children }: Props = $props();
</script>

<div class={cleanClass('immich-scrollbar overflow-y-auto', className)}>
{@render children?.()}
</div>

<style>
/* width */
.immich-scrollbar::-webkit-scrollbar {
width: 8px;
}
/* Track */
.immich-scrollbar::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 16px;
}
/* Handle */
.immich-scrollbar::-webkit-scrollbar-thumb {
background: rgba(85, 86, 87, 0.408);
border-radius: 16px;
}
/* Handle on hover */
.immich-scrollbar::-webkit-scrollbar-thumb:hover {
background: #4250afad;
border-radius: 16px;
}
</style>
3 changes: 3 additions & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export enum ChildKey {
Field = 'field',
HelperText = 'helped-text',
AppShell = 'app-shell',
AppShellHeader = 'app-shell-header',
AppShellSidebar = 'app-shell-sidebar',
Card = 'card',
CardHeader = 'card-header',
CardBody = 'card-body',
Expand Down
4 changes: 4 additions & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export { default as Alert } from '$lib/components/Alert/Alert.svelte';
export { default as AppShell } from '$lib/components/AppShell/AppShell.svelte';
export { default as AppShellHeader } from '$lib/components/AppShell/AppShellHeader.svelte';
export { default as AppShellSidebar } from '$lib/components/AppShell/AppShellSidebar.svelte';
export { default as Button } from '$lib/components/Button/Button.svelte';
export { default as Card } from '$lib/components/Card/Card.svelte';
export { default as CardBody } from '$lib/components/Card/CardBody.svelte';
Expand All @@ -19,6 +22,7 @@ export { default as IconButton } from '$lib/components/IconButton/IconButton.sve
export { default as Link } from '$lib/components/Link/Link.svelte';
export { default as LoadingSpinner } from '$lib/components/LoadingSpinner/LoadingSpinner.svelte';
export { default as Logo } from '$lib/components/Logo/Logo.svelte';
export { default as Scrollable } from '$lib/components/Scrollable/Scrollable.svelte';
export { default as HStack } from '$lib/components/Stack/HStack.svelte';
export { default as Stack } from '$lib/components/Stack/Stack.svelte';
export { default as VStack } from '$lib/components/Stack/VStack.svelte';
Expand Down
3 changes: 3 additions & 0 deletions src/lib/services/theme.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Theme } from '$docs/constants.js';

export const theme = $state<{ value: Theme }>({ value: Theme.Dark });
67 changes: 64 additions & 3 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,69 @@
<script lang="ts">
import Navbar from '$docs/components/Navbar.svelte';
import { Theme } from '$docs/constants.js';
import AppShellHeader from '$lib/components/AppShell/AppShellHeader.svelte';
import { theme } from '$lib/services/theme.svelte.js';
import { AppShell, AppShellSidebar, Heading, IconButton, Link, Stack } from '@immich/ui';
import { mdiWeatherNight, mdiWeatherSunny } from '@mdi/js';
import '../app.css';
let { children } = $props();
const handleToggleTheme = () =>
(theme.value = theme.value === Theme.Dark ? Theme.Light : Theme.Dark);
const themeIcon = $derived(theme.value === Theme.Light ? mdiWeatherSunny : mdiWeatherNight);
</script>

<main>
{@render children()}
</main>
<AppShell class="{theme.value} bg-light text-dark">
<AppShellHeader>
<Navbar theme={theme.value}>
<IconButton
size="large"
shape="round"
color="primary"
variant="ghost"
icon={themeIcon}
onclick={handleToggleTheme}
/>
</Navbar>
</AppShellHeader>

<AppShellSidebar class="p-4">
<Stack class="min-w-[200px]">
<Heading size="tiny">Layout</Heading>
<Stack class="pl-4">
<Link href="/examples/app-shell">AppShell</Link>
<Link href="/examples/alert">Alert</Link>
<Link href="/examples/card">Card</Link>
<Link href="/examples/stack">Stack</Link>
</Stack>
<Heading size="tiny">Forms</Heading>
<Stack class="pl-4">
<Link href="/examples/button">Button</Link>
<Link href="/examples/icon-button">IconButton</Link>
<Link href="/examples/checkbox">Checkbox</Link>
<Link href="/examples/close-button">CloseButton</Link>
<Link href="/examples/field">Field</Link>
<Link href="/examples/input">Input</Link>
<Link href="/examples/loading-spinner">LoadingSpinner</Link>
<Link href="/examples/password-input">PasswordInput</Link>
</Stack>
<Heading size="tiny">Text</Heading>
<Stack class="pl-4">
<Link href="/examples/heading">Heading</Link>
<Link href="/examples/text">Text</Link>
<Link href="/examples/link">Link</Link>
</Stack>

<Heading size="tiny">Immich</Heading>
<Stack class="pl-4">
<Link href="/examples/logo">Logo</Link>
<Link href="/examples/supporter-badge">SupporterBadge</Link>
</Stack>
</Stack>
</AppShellSidebar>

<div class="h-full">
{@render children()}
</div>
</AppShell>
Loading

0 comments on commit a809842

Please sign in to comment.