Skip to content

Commit 34a1a93

Browse files
authored
feat: better sidebar control (#166)
1 parent 14435f0 commit 34a1a93

File tree

8 files changed

+60
-50
lines changed

8 files changed

+60
-50
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,8 @@
6868
"bits-ui": "^1.0.0-next.46",
6969
"tailwind-merge": "^2.5.4",
7070
"tailwind-variants": "^0.3.0"
71+
},
72+
"volta": {
73+
"node": "22.15.0"
7174
}
7275
}

src/docs/components/Navbar.svelte

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
<script lang="ts">
2-
import { Logo, Theme } from '@immich/ui';
2+
import { IconButton, Logo, Theme } from '@immich/ui';
3+
import { mdiMenu } from '@mdi/js';
34
import type { Snippet } from 'svelte';
45
56
type Props = {
67
theme?: Theme;
78
children?: Snippet;
9+
onToggleSidebar?: () => void;
810
};
911
10-
const { children, theme = Theme.Dark }: Props = $props();
12+
const { children, theme = Theme.Dark, onToggleSidebar }: Props = $props();
1113
</script>
1214

13-
<nav class="{theme} flex items-center justify-between gap-2 p-2">
14-
<a href="/" class="flex gap-2 text-4xl">
15+
<nav class="{theme} flex items-center gap-2 p-2">
16+
<IconButton
17+
shape="round"
18+
color="secondary"
19+
variant="ghost"
20+
size="medium"
21+
aria-label="Main menu"
22+
icon={mdiMenu}
23+
onclick={() => onToggleSidebar?.()}
24+
class="md:hidden"
25+
/>
26+
<a href="/" class="flex grow gap-2 text-4xl">
1527
<Logo variant="inline" />
1628
</a>
1729
{@render children?.()}

src/lib/components/AppShell/AppShellSidebar.svelte

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,25 @@
11
<script lang="ts">
2-
import { afterNavigate } from '$app/navigation';
3-
import IconButton from '$lib/components/IconButton/IconButton.svelte';
42
import Scrollable from '$lib/components/Scrollable/Scrollable.svelte';
53
import { ChildKey } from '$lib/constants.js';
64
import Child from '$lib/internal/Child.svelte';
75
import { cleanClass } from '$lib/utils.js';
8-
import { mdiClose, mdiMenu } from '@mdi/js';
9-
import type { Snippet } from 'svelte';
6+
import { type Snippet } from 'svelte';
107
118
type Props = {
129
class?: string;
1310
children: Snippet;
14-
noBorder?: boolean;
11+
open?: boolean;
1512
};
1613
17-
let { class: className, children, noBorder = false }: Props = $props();
18-
19-
afterNavigate(() => {
20-
if (!hidden) {
21-
hidden = true;
22-
}
23-
});
24-
25-
let hidden = $state(true);
14+
let { class: className, children, open = $bindable(true) }: Props = $props();
2615
</script>
2716

2817
<Child for={ChildKey.AppShell} as={ChildKey.AppShellSidebar}>
29-
<IconButton
30-
size="giant"
31-
onclick={() => (hidden = !hidden)}
32-
icon={hidden ? mdiMenu : mdiClose}
33-
shape="round"
34-
color={hidden ? 'primary' : 'secondary'}
35-
variant="filled"
36-
class="absolute bottom-2 end-4 m-2 opacity-100 md:hidden"
37-
aria-label="Menu"
38-
/>
3918
<Scrollable
4019
class={cleanClass(
41-
'h-dvh w-full shrink-0 bg-light pb-16 text-dark md:relative md:block md:w-min md:pb-0',
42-
43-
hidden ? 'hidden' : '',
20+
'relative shrink-0 bg-light text-dark transition-all duration-200',
21+
open ? 'w-[min(100vw,16rem)]' : 'w-[0px]',
4422
className,
45-
noBorder || 'border-e',
4623
)}
4724
>
4825
{@render children?.()}

src/lib/components/Form/PasswordInput.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
shape="round"
2424
color="secondary"
2525
{size}
26-
class="mr-1"
26+
class="me-1"
2727
icon={isVisible ? mdiEyeOffOutline : mdiEyeOutline}
2828
onclick={() => (isVisible = !isVisible)}
2929
title={isVisible ? t('hidePassword', translations) : t('showPassword', translations)}

src/lib/components/Navbar/NavbarItem.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
isActive?: () => boolean;
1313
} & { icon?: string & Omit<IconProps, 'icon'> };
1414
15-
const matchesPath = () => page.url.pathname === href;
15+
const startsWithHref = () => page.url.pathname.startsWith(href);
1616
1717
let {
1818
href,
@@ -23,11 +23,11 @@
2323
...iconProps
2424
}: Props = $props();
2525
26-
const isActive = isActiveOverride ?? matchesPath;
26+
const isActive = isActiveOverride ?? startsWithHref;
2727
let active = $derived(activeOverride ?? isActive());
2828
2929
const styles = tv({
30-
base: 'flex w-full place-items-center gap-4 transition-[padding] delay-100 duration-100 hover:bg-subtle hover:text-primary group-hover:sm:px-5 md:rounded-e-full md:px-5',
30+
base: 'flex w-full place-items-center gap-4 rounded-e-full px-5 transition-[padding] delay-100 duration-100 hover:bg-subtle hover:text-primary group-hover:sm:px-5',
3131
variants: {
3232
active: {
3333
true: 'bg-primary/10 text-primary',

src/lib/utils.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1+
import { twMerge } from 'tailwind-merge';
2+
13
export const cleanClass = (...classNames: unknown[]) => {
2-
return classNames
3-
.filter((className) => {
4-
if (!className || typeof className === 'boolean') {
5-
return false;
6-
}
4+
return twMerge(
5+
classNames
6+
.filter((className) => {
7+
if (!className || typeof className === 'boolean') {
8+
return false;
9+
}
710

8-
return typeof className === 'string';
9-
})
10-
.join(' ');
11+
return typeof className === 'string';
12+
})
13+
.join(' '),
14+
);
1115
};
1216

1317
export const withPrefix = (key: string) => `immich-ui-${key}`;

src/routes/+layout.svelte

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script lang="ts">
2+
import { beforeNavigate } from '$app/navigation';
23
import Footer from '$docs/components/Footer.svelte';
34
import Navbar from '$docs/components/Navbar.svelte';
45
import { componentGroups } from '$docs/constants.js';
@@ -14,22 +15,32 @@
1415
ThemeSwitcher,
1516
} from '@immich/ui';
1617
import { mdiHome } from '@mdi/js';
18+
import { MediaQuery } from 'svelte/reactivity';
1719
import '../app.css';
1820
1921
initializeTheme();
2022
2123
let { children } = $props();
24+
25+
const sidebar = new MediaQuery(`min-width: 850px`);
26+
let open = $derived(sidebar.current);
27+
28+
beforeNavigate(() => {
29+
if (!sidebar.current) {
30+
open = false;
31+
}
32+
});
2233
</script>
2334

2435
<AppShell>
2536
<AppShellHeader>
26-
<Navbar theme={theme.value}>
27-
<ThemeSwitcher size="giant" />
37+
<Navbar theme={theme.value} onToggleSidebar={() => (open = !open)}>
38+
<ThemeSwitcher size="medium" />
2839
</Navbar>
2940
</AppShellHeader>
3041

31-
<AppShellSidebar class="min-w-[250px]">
32-
<div class="mr-0 mt-4 md:mr-4">
42+
<AppShellSidebar bind:open>
43+
<div class="my-4 me-4">
3344
<NavbarItem title="Home" icon={mdiHome} href="/" />
3445
{#each componentGroups as group}
3546
<NavbarGroup title={group.title} />

src/routes/components/app-shell/BasicExample.svelte

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script lang="ts">
2+
import Button from '$lib/internal/Button.svelte';
23
import {
34
AppShell,
45
AppShellHeader,
@@ -8,6 +9,8 @@
89
Stack,
910
} from '@immich/ui';
1011
import { mdiHome } from '@mdi/js';
12+
13+
let open = $state(true);
1114
</script>
1215

1316
<Stack>
@@ -53,7 +56,7 @@
5356
</div>
5457
</AppShellHeader>
5558

56-
<AppShellSidebar noBorder class="pt-2">
59+
<AppShellSidebar class="pt-2" bind:open>
5760
<Stack>
5861
<NavbarItem icon={mdiHome} title="Home" href="/" active />
5962
<NavbarItem icon={mdiHome} title="Item 1" href="#" />
@@ -63,7 +66,7 @@
6366
</AppShellSidebar>
6467

6568
<div class="p-4">
66-
<Heading size="tiny">Content</Heading>
69+
<Button onclick={() => (open = !open)}>Toggle Sidebar</Button>
6770
</div>
6871
</AppShell>
6972
</div>

0 commit comments

Comments
 (0)