Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: app shell component #46

Merged
merged 1 commit into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading