From 2ec90ae4faa984eb282a3c903b19280efc68d705 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen <jason@rasm.me> Date: Fri, 20 Dec 2024 17:07:50 -0500 Subject: [PATCH] feat: navbar (#48) --- package-lock.json | 8 ++-- package.json | 2 +- src/app.css | 2 + src/docs/components/ExampleLayout.svelte | 2 +- src/docs/constants.ts | 14 ++++--- .../AppShell/AppShellSidebar.svelte | 6 ++- src/lib/components/AppShell/PageLayout.svelte | 2 +- src/lib/components/Navbar/NavbarGroup.svelte | 12 ++++++ src/lib/components/Navbar/NavbarItem.svelte | 31 ++++++++++++++ src/lib/index.ts | 2 + src/routes/+layout.svelte | 28 ++++++------- src/routes/+page.svelte | 2 +- .../components/app-shell/BasicExample.svelte | 40 ++++++++----------- src/routes/components/navbar/+page.svelte | 14 +++++++ .../components/navbar/BasicExample.svelte | 8 ++++ .../components/navbar/GroupExample.svelte | 12 ++++++ tailwind.config.ts | 1 + 17 files changed, 133 insertions(+), 53 deletions(-) create mode 100644 src/lib/components/Navbar/NavbarGroup.svelte create mode 100644 src/lib/components/Navbar/NavbarItem.svelte create mode 100644 src/routes/components/navbar/+page.svelte create mode 100644 src/routes/components/navbar/BasicExample.svelte create mode 100644 src/routes/components/navbar/GroupExample.svelte diff --git a/package-lock.json b/package-lock.json index 6aa80c3..6e3b535 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ }, "devDependencies": { "@sveltejs/adapter-static": "^3.0.6", - "@sveltejs/kit": "^2.0.0", + "@sveltejs/kit": "^2.13.0", "@sveltejs/package": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^5.0.2", "@types/eslint": "^9.6.0", @@ -1098,9 +1098,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.11.0.tgz", - "integrity": "sha512-VtHkM5i4qAIeO9hfYwKD6Hxn7Ik+RkYam9842RXw6YdtzuI+gsA8XepZs7FB/o7hjQBJCDmvXahiGAnff1QU6w==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.13.0.tgz", + "integrity": "sha512-6t6ne00vZx/TjD6s0Jvwt8wRLKBwbSAN1nhlOzcLUSTYX1hTp4eCBaTPB5Yz/lu+tYcvz4YPEEuPv3yfsNp2gw==", "dev": true, "hasInstallScript": true, "license": "MIT", diff --git a/package.json b/package.json index 80730b6..36114d9 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ }, "devDependencies": { "@sveltejs/adapter-static": "^3.0.6", - "@sveltejs/kit": "^2.0.0", + "@sveltejs/kit": "^2.13.0", "@sveltejs/package": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^5.0.2", "@types/eslint": "^9.6.0", diff --git a/src/app.css b/src/app.css index 83dbf94..23688ec 100644 --- a/src/app.css +++ b/src/app.css @@ -12,6 +12,7 @@ --immich-danger: 180 0 0; --immich-warning: 255 170 0; --immich-info: 14 165 233; + --immich-gray: 246 246 246; } .dark { @@ -23,5 +24,6 @@ --immich-danger: 239 68 68; --immich-warning: 255 170 0; --immich-info: 14 165 233; + --immich-gray: 33 33 33; } } diff --git a/src/docs/components/ExampleLayout.svelte b/src/docs/components/ExampleLayout.svelte index 882a55f..ed63c6d 100644 --- a/src/docs/components/ExampleLayout.svelte +++ b/src/docs/components/ExampleLayout.svelte @@ -14,7 +14,7 @@ <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" + class="flex shrink-0 justify-between border-b border-gray-300 bg-light p-4 text-dark dark:border-gray-700" > <div class="flex items-center gap-2"> <a href="/" class="underline">Home</a> diff --git a/src/docs/constants.ts b/src/docs/constants.ts index 736fce4..4af3afb 100644 --- a/src/docs/constants.ts +++ b/src/docs/constants.ts @@ -5,7 +5,7 @@ import { mdiCardOutline, mdiCheckboxMarked, mdiCloseCircle, - mdiCreditCardOutline, + mdiDotsCircle, mdiFormatHeaderPound, mdiFormTextbox, mdiFormTextboxPassword, @@ -13,6 +13,7 @@ import { mdiImage, mdiLink, mdiListBoxOutline, + mdiMenu, mdiNumeric, mdiPartyPopper, mdiViewSequential, @@ -40,16 +41,17 @@ export type ExampleCardProps = ExampleItem & { theme: Theme }; export const componentGroups = [ { - name: 'Layout', + title: 'Layout', components: [ { name: 'Alert', icon: mdiAlertCircleOutline }, { name: 'AppShell', icon: mdiApplicationOutline }, { name: 'Card', icon: mdiCardOutline }, + { name: 'Navbar', icon: mdiMenu }, { name: 'Stack', icon: mdiViewSequential }, ], }, { - name: 'Forms', + title: 'Forms', components: [ { name: 'Button', icon: mdiButtonCursor }, { name: 'IconButton', icon: mdiHomeCircle }, @@ -57,12 +59,12 @@ export const componentGroups = [ { name: 'CloseButton', icon: mdiCloseCircle }, { name: 'Field', icon: mdiListBoxOutline }, { name: 'Input', icon: mdiFormTextbox }, - { name: 'LoadingSpinner', icon: mdiCreditCardOutline }, + { name: 'LoadingSpinner', icon: mdiDotsCircle }, { name: 'PasswordInput', icon: mdiFormTextboxPassword }, ], }, { - name: 'Text', + title: 'Text', components: [ { name: 'Text', icon: mdiFormatHeaderPound }, { name: 'Heading', icon: mdiFormTextbox }, @@ -71,7 +73,7 @@ export const componentGroups = [ ], }, { - name: 'Immich', + title: 'Immich', components: [ { name: 'Logo', icon: mdiImage }, { name: 'SupporterBadge', icon: mdiPartyPopper }, diff --git a/src/lib/components/AppShell/AppShellSidebar.svelte b/src/lib/components/AppShell/AppShellSidebar.svelte index a98e048..0978ffd 100644 --- a/src/lib/components/AppShell/AppShellSidebar.svelte +++ b/src/lib/components/AppShell/AppShellSidebar.svelte @@ -8,16 +8,18 @@ type Props = { class?: string; children: Snippet; + noBorder?: boolean; }; - let { class: className, children }: Props = $props(); + let { class: className, children, noBorder = false }: 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', + 'hidden h-full shrink-0 border-gray-200 dark:border-gray-700 lg:block', className, + noBorder || 'border-r', )} > {@render children?.()} diff --git a/src/lib/components/AppShell/PageLayout.svelte b/src/lib/components/AppShell/PageLayout.svelte index 51be798..0b8c972 100644 --- a/src/lib/components/AppShell/PageLayout.svelte +++ b/src/lib/components/AppShell/PageLayout.svelte @@ -24,7 +24,7 @@ <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" + class="dark:border-immich-neutral 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} diff --git a/src/lib/components/Navbar/NavbarGroup.svelte b/src/lib/components/Navbar/NavbarGroup.svelte new file mode 100644 index 0000000..24b2f6f --- /dev/null +++ b/src/lib/components/Navbar/NavbarGroup.svelte @@ -0,0 +1,12 @@ +<script lang="ts"> + type Props = { + title: string; + }; + + let { title }: Props = $props(); +</script> + +<div class="text-xs transition-all duration-200"> + <p class="hidden px-6 py-4 uppercase group-hover:sm:block md:block">{title}</p> + <hr class="mx-4 mb-[31px] mt-8 block group-hover:sm:hidden md:hidden" /> +</div> diff --git a/src/lib/components/Navbar/NavbarItem.svelte b/src/lib/components/Navbar/NavbarItem.svelte new file mode 100644 index 0000000..a61a4d6 --- /dev/null +++ b/src/lib/components/Navbar/NavbarItem.svelte @@ -0,0 +1,31 @@ +<script lang="ts"> + import Icon from '$lib/components/Icon/Icon.svelte'; + import type { IconProps } from '$lib/types.js'; + import { tv } from 'tailwind-variants'; + + type Props = { + title: string; + active?: boolean; + href: string; + } & IconProps; + + let { href, title, active = false, ...iconProps }: Props = $props(); + + const styles = tv({ + base: 'hover:bg-neutral hover:text-primary flex w-full place-items-center gap-4 rounded-r-full py-3 transition-[padding] delay-100 duration-100 pl-5 group-hover:sm:px-5 md:px-5', + variants: { + active: { + true: 'bg-primary/10 text-primary', + false: '', + }, + }, + }); +</script> + +<a {href} draggable="false" aria-current={active ? 'page' : undefined} class={styles({ active })}> + <div class="flex w-full place-items-center gap-4 overflow-hidden truncate"> + <Icon size="1.5em" class="shrink-0" aria-hidden={true} {...iconProps} /> + <span class="text-sm font-medium">{title}</span> + </div> + <div></div> +</a> diff --git a/src/lib/index.ts b/src/lib/index.ts index 944ef80..b8a40f7 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -23,6 +23,8 @@ 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 NavbarGroup } from '$lib/components/Navbar/NavbarGroup.svelte'; +export { default as NavbarItem } from '$lib/components/Navbar/NavbarItem.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'; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index dfdccfb..edde63e 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,4 +1,5 @@ <script lang="ts"> + import { page } from '$app/state'; import Navbar from '$docs/components/Navbar.svelte'; import { componentGroups, Theme } from '$docs/constants.js'; import { asComponentHref } from '$docs/utilities.js'; @@ -7,19 +8,20 @@ AppShell, AppShellHeader, AppShellSidebar, - Heading, IconButton, - Link, - Stack, + NavbarGroup, + NavbarItem, } from '@immich/ui'; - import { mdiWeatherNight, mdiWeatherSunny } from '@mdi/js'; + import { mdiHome, 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); + const isActive = (path: string) => path === page.url.pathname; </script> <AppShell class="{theme.value} bg-light text-dark"> @@ -36,17 +38,15 @@ </Navbar> </AppShellHeader> - <AppShellSidebar class="p-4"> - <Stack class="min-w-[200px]"> - {#each componentGroups as group} - <Heading size="tiny">{group.name}</Heading> - <Stack class="pl-4"> - {#each group.components as component} - <Link href={asComponentHref(component.name)}>{component.name}</Link> - {/each} - </Stack> + <AppShellSidebar class="min-w-[225px] py-4 pr-4"> + <NavbarItem active={isActive('/')} title="Home" icon={mdiHome} href="/" /> + {#each componentGroups as group} + <NavbarGroup title={group.title} /> + {#each group.components as component} + {@const href = asComponentHref(component.name)} + <NavbarItem active={isActive(href)} title={component.name} icon={component.icon} {href} /> {/each} - </Stack> + {/each} </AppShellSidebar> <div class="h-full"> diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 048a717..cae1d92 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -25,7 +25,7 @@ <Stack gap={8}> {#each componentGroups as group} <Stack> - <Heading size="medium">{group.name}</Heading> + <Heading size="medium">{group.title}</Heading> <Grid> {#each group.components as component} <ComponentCard {component} /> diff --git a/src/routes/components/app-shell/BasicExample.svelte b/src/routes/components/app-shell/BasicExample.svelte index 7495a05..2c5ad46 100644 --- a/src/routes/components/app-shell/BasicExample.svelte +++ b/src/routes/components/app-shell/BasicExample.svelte @@ -1,6 +1,13 @@ <script lang="ts"> - import DecorativeBlock from '$docs/components/DecorativeBlock.svelte'; - import { AppShell, AppShellHeader, AppShellSidebar, Heading, Stack } from '@immich/ui'; + import { + AppShell, + AppShellHeader, + AppShellSidebar, + Heading, + NavbarItem, + Stack, + } from '@immich/ui'; + import { mdiHome } from '@mdi/js'; </script> <Stack> @@ -46,30 +53,17 @@ </div> </AppShellHeader> - <AppShellSidebar> - <div class="p-4"> - <Heading size="tiny">Sidebar Overflow</Heading> - <Stack> - <DecorativeBlock /> - <DecorativeBlock /> - <DecorativeBlock /> - <DecorativeBlock /> - <DecorativeBlock /> - <DecorativeBlock /> - </Stack> - </div> + <AppShellSidebar noBorder class="pt-2"> + <Stack> + <NavbarItem icon={mdiHome} title="Home" href="/" active /> + <NavbarItem icon={mdiHome} title="Home" href="/" /> + <NavbarItem icon={mdiHome} title="Home" href="/" /> + <NavbarItem icon={mdiHome} title="Home" href="/" /> + </Stack> </AppShellSidebar> <div class="p-4"> - <Heading size="tiny">Content Overflow</Heading> - <Stack> - <DecorativeBlock /> - <DecorativeBlock /> - <DecorativeBlock /> - <DecorativeBlock /> - <DecorativeBlock /> - <DecorativeBlock /> - </Stack> + <Heading size="tiny">Content</Heading> </div> </AppShell> </div> diff --git a/src/routes/components/navbar/+page.svelte b/src/routes/components/navbar/+page.svelte new file mode 100644 index 0000000..d824b4f --- /dev/null +++ b/src/routes/components/navbar/+page.svelte @@ -0,0 +1,14 @@ +<script lang="ts"> + import ExampleLayout from '$docs/components/ExampleLayout.svelte'; + import BasicExample from './BasicExample.svelte'; + import basicExample from './BasicExample.svelte?raw'; + import GroupExample from './GroupExample.svelte'; + import groupExample from './GroupExample.svelte?raw'; + + const examples = [ + { title: 'Basic', code: basicExample, component: BasicExample }, + { title: 'Group', code: groupExample, component: GroupExample }, + ]; +</script> + +<ExampleLayout name="Basic" {examples} /> diff --git a/src/routes/components/navbar/BasicExample.svelte b/src/routes/components/navbar/BasicExample.svelte new file mode 100644 index 0000000..bdaca84 --- /dev/null +++ b/src/routes/components/navbar/BasicExample.svelte @@ -0,0 +1,8 @@ +<script lang="ts"> + import { NavbarItem } from '@immich/ui'; + import { mdiHome } from '@mdi/js'; +</script> + +<div class="w-[200px]"> + <NavbarItem icon={mdiHome} title="Home" href="#" active /> +</div> diff --git a/src/routes/components/navbar/GroupExample.svelte b/src/routes/components/navbar/GroupExample.svelte new file mode 100644 index 0000000..953b0b8 --- /dev/null +++ b/src/routes/components/navbar/GroupExample.svelte @@ -0,0 +1,12 @@ +<script lang="ts"> + import NavbarGroup from '$lib/components/Navbar/NavbarGroup.svelte'; + import { NavbarItem } from '@immich/ui'; + import { mdiButtonPointer, mdiCardOutline, mdiHome } from '@mdi/js'; +</script> + +<div class="w-[200px]"> + <NavbarItem icon={mdiHome} title="Home" href="#" active /> + <NavbarGroup title="Components" /> + <NavbarItem icon={mdiButtonPointer} title="Button" href="#" /> + <NavbarItem icon={mdiCardOutline} title="Card" href="#" /> +</div> diff --git a/tailwind.config.ts b/tailwind.config.ts index 3e823e7..c31645e 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -13,6 +13,7 @@ export default { danger: 'rgb(var(--immich-danger) / <alpha-value>)', warning: 'rgb(var(--immich-warning) / <alpha-value>)', info: 'rgb(var(--immich-info) / <alpha-value>)', + neutral: 'rgb(var(--immich-gray) / <alpha-value>)', }, }, },