Compare commits

...

3 Commits

Author SHA1 Message Date
Alex Tran da9e601ed5 segmentation and small fixes 2024-01-23 23:44:10 -06:00
Jason Rasmussen 88fd46dc9d feat: layout 2024-01-23 22:45:11 -05:00
Jason Rasmussen e320e31476 WIP 2024-01-23 22:08:25 -05:00
15 changed files with 603 additions and 0 deletions
+206
View File
@@ -0,0 +1,206 @@
<script lang="ts">
import {
mdiBookMultiple,
mdiButtonCursor,
mdiCardMultipleOutline,
mdiFormatHeader1,
mdiHeartMultiple,
mdiImageMultiple,
mdiPlus,
mdiSquare,
mdiTrashCan,
} from '@mdi/js';
import Button from '@ui/components/button.svelte';
import Card from '@ui/components/card.svelte';
import Heading from '@ui/components/heading.svelte';
import IconButton from '@ui/components/icon-button.svelte';
import Layout from '@ui/components/layout.svelte';
import SidebarItem from '@ui/components/sidebar-item.svelte';
import Sidebar from '@ui/components/sidebar.svelte';
import ThemeSwitcher from '@ui/components/theme-switcher.svelte';
import type { Color, Size } from '@ui/types';
import { colorTheme } from '../../lib/stores/preferences.store';
const sizes: Size[] = ['xs', 'sm', 'md', 'lg', 'xl'];
const colors: Color[] = ['primary', 'secondary', 'info', 'success', 'warning', 'danger'];
const scrollTo = (id: string) => {
const content = document.getElementById('content');
const anchor = document.getElementById(id);
if (!anchor || !content) {
return;
}
content.scrollTo({ top: anchor.offsetTop - 24, behavior: 'smooth' });
};
</script>
<Layout>
<!-- <AppBar slot="header" /> -->
<div class="h-full" slot="sidebar">
<Sidebar>
<SidebarItem title="Headings" icon={mdiFormatHeader1} on:click={() => scrollTo('headings')}></SidebarItem>
<SidebarItem title="Buttons" icon={mdiButtonCursor} on:click={() => scrollTo('buttons')}></SidebarItem>
<SidebarItem title="Cards" icon={mdiCardMultipleOutline} on:click={() => scrollTo('cards')}></SidebarItem>
<SidebarItem title="Sidebar" icon={mdiSquare} on:click={() => scrollTo('sidebar')}></SidebarItem>
</Sidebar>
</div>
<main class="m-8 pb-16">
<div class="mb-6 w-full bg-gray-200 dark:bg-gray-800 p-6 font-bold">
<Heading size="xl">IMMICH COMPONENTS</Heading>
</div>
<!-- THEME SWITCHER -->
<section class="py-4 flex flex-col gap-2">
<Heading size="xl">Theme Switcher</Heading>
<ThemeSwitcher on:theme={({ detail }) => ($colorTheme = { value: detail, system: false })}></ThemeSwitcher>
</section>
<hr />
<!-- HEADING -->
<section class="py-4 flex flex-col gap-2">
<Heading id="headings" size="xl">Headings</Heading>
{#each sizes as size}
<Heading {size}>Heading ({size})</Heading>
{/each}
</section>
<hr />
<!-- BUTTONS -->
<section class="py-4 flex flex-col gap-2">
<Heading id="buttons" size="xl">Buttons</Heading>
<Heading size="md">Colors</Heading>
<div class="flex gap-2">
<Button color="primary">Primary</Button>
<Button color="secondary">Secondary</Button>
<Button color="success">Success</Button>
<Button color="info">Info</Button>
<Button color="warning">Warning</Button>
<Button color="danger">Danger</Button>
</div>
<Heading size="md">Disabled</Heading>
<div class="flex gap-2">
<Button disabled color="primary">Primary</Button>
<Button disabled color="secondary">Secondary</Button>
<Button disabled color="success">Success</Button>
<Button disabled color="info">Info</Button>
<Button disabled color="warning">Warning</Button>
<Button disabled color="danger">Danger</Button>
</div>
<Heading size="md">Rounded</Heading>
<div class="flex align-top gap-2">
<Button rounded="full" color="secondary">Rounded</Button>
<Button rounded="semi" color="warning">Semi Rounded</Button>
<Button rounded={false} color="info">None</Button>
</div>
<Heading size="md">Sizes</Heading>
<div class="flex gap-2">
<Button size="xs">Extra Small</Button>
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<Button size="xl">Extra Large</Button>
</div>
<Heading size="md">Width</Heading>
<div class="flex flex-col gap-2 max-w-[500px]">
<Button fullWidth color="primary">Primary</Button>
<Button fullWidth color="secondary">Secondary</Button>
<Button fullWidth color="success">Success</Button>
<Button fullWidth color="info">Info</Button>
<Button fullWidth color="warning">Warning</Button>
<Button fullWidth color="danger">Danger</Button>
</div>
<Heading size="md">Icon Buttons</Heading>
<div class="flex gap-2 max-w-[500px]">
{#each colors as color}
<IconButton {color} icon={mdiPlus} />
{/each}
</div>
<div class="flex gap-2 max-w-[500px]">
{#each colors as color}
<IconButton transparent={false} {color} icon={mdiPlus} />
{/each}
</div>
<Heading size="md">Events</Heading>
<div class="flex gap-2">
<Button on:click={() => alert('Hello')}>on:click</Button>
</div>
</section>
<hr />
<!-- CARDS -->
<section class="py-4 flex flex-col gap-2">
<Heading id="cards" size="xl">Cards</Heading>
<div class="flex flex-col gap-2">
{#each sizes as size}
<Card {size}>
<Heading size="lg">Card ({size})</Heading>
<p class="dark:text-immich-dark-fg text-immich-fg">
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Soluta dignissimos cupiditate eum ut nesciunt
architecto excepturi veritatis facere exercitationem? Quasi maiores modi voluptatum impedit vero, quae
dolorum officia molestiae consequuntur.
</p>
</Card>
{/each}
</div>
</section>
<hr />
<!-- SIDEBAR -->
<section class="py-4 flex flex-col gap-2">
<Heading id="sidebar" size="xl">Sidebar</Heading>
<Sidebar>
<SidebarItem title="Item A" icon={mdiImageMultiple} isSelected></SidebarItem>
<SidebarItem title="Item B" icon={mdiHeartMultiple}></SidebarItem>
<SidebarItem title="Item C" icon={mdiBookMultiple}></SidebarItem>
<SidebarItem title="Item D" icon={mdiTrashCan}></SidebarItem>
</Sidebar>
</section>
<hr />
<!-- <section class="py-4 flex flex-col gap-2">
<Heading size="lg">Layouts</Heading>
</section> -->
<!-- <section class="py-4 flex flex-col gap-2">
<Heading size="lg">Layouts</Heading>
<div class="max-w-[750px] border border-white">
<Layout>
<div class="p-2" slot="header">
<Heading size="lg">Header</Heading>
</div>
<div class="p-2 w-full border border-white" slot="sidebar">
<Heading size="lg">Sidebar</Heading>
</div>
<div class="p-2 w-full border border-white h-[500px]">
<Heading size="lg">Content</Heading>
</div>
</Layout>
</div>
<div class="max-w-[750px] border border-white">
<Layout title="Content Title">
<div class="p-2" slot="header">
<Heading size="lg">Header</Heading>
</div>
<div class="p-2 w-full border border-white" slot="sidebar">
<Heading size="lg">Sidebar</Heading>
</div>
<div class="p-2 w-full border border-white h-[500px]">
<Heading size="md">Content</Heading>
</div>
</Layout>
</div>
</section> -->
</main>
</Layout>
+13
View File
@@ -0,0 +1,13 @@
import type { PageLoad } from './$types';
// export const ssr = false;
// export const csr = false;
export const load = (async () => {
return {
meta: {
title: 'Design',
description: 'Immich UI Design',
},
};
}) satisfies PageLoad;
+17
View File
@@ -0,0 +1,17 @@
<div class="absolute top-0 w-full z-[100] bg-transparent">
<div
class="grid grid-cols-[10%_80%_10%] justify-between md:grid-cols-[20%_60%_20%] lg:grid-cols-3 mx-2 mt-2 place-items-center rounded-lg p-2 transition-all dark:bg-immich-dark-gray bg-immich-dark-gray text-white"
>
<div class="flex place-items-center gap-6 justify-self-start dark:text-immich-dark-fg">
<slot name="leading" />
</div>
<div class="w-full">
<slot />
</div>
<div class="mr-4 flex place-items-center gap-1 justify-self-end">
<slot name="trailing" />
</div>
</div>
</div>
+138
View File
@@ -0,0 +1,138 @@
<script lang="ts">
import type { Color } from '../types';
export let type: HTMLButtonElement['type'] = 'button';
export let disabled = false;
export let title = '';
export let rounded: 'semi' | 'full' | boolean = true;
export let color: Color = 'primary';
export let transparent = false;
export let size: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'size' | 'icon' = 'md';
export let fullWidth = false;
const getBaseClasses = () => {
return 'inline-flex items-center justify-center transition-colors disabled:cursor-not-allowed disabled:opacity-60';
};
const getHoverClasses = () => {
if (transparent) {
return 'enabled:hover:bg-gray-100 enabled:dark:hover:bg-gray-700';
}
switch (color) {
case 'primary':
return 'enabled:dark:hover:bg-immich-dark-primary/80 enabled:hover:bg-immich-primary/90';
case 'secondary':
return 'enabled:dark:hover:bg-gray-200/90 enabled:hover:bg-gray-500/90 ';
case 'info':
return 'enabled:hover:bg-blue-300/90';
case 'success':
return 'enabled:hover:bg-green-300/90';
case 'warning':
return 'enabled:hover:bg-yellow-300/90';
case 'danger':
return 'enabled:hover:bg-red-300/90';
}
};
const getBackgroundClasses = () => {
switch (color) {
case 'primary':
return 'bg-immich-primary dark:bg-immich-dark-primary';
case 'secondary':
return 'bg-gray-500 dark:bg-gray-200';
case 'info':
return 'bg-blue-500';
case 'success':
return 'bg-green-500';
case 'warning':
return 'bg-yellow-500';
case 'danger':
return 'bg-red-500';
}
};
const getColorClasses = () => {
switch (color) {
case 'primary':
return transparent ? 'text-gray-500 dark:text-immich-dark-primary' : 'text-white dark:text-immich-dark-gray';
case 'secondary':
return transparent ? 'text-gray-500 dark:text-immich-dark-gray' : 'text-white dark:text-immich-dark-gray';
case 'info':
return transparent ? 'text-blue-500' : 'text-gray-800';
case 'success':
return transparent ? 'text-green-500' : 'text-gray-800';
case 'warning':
return transparent ? 'text-yellow-500' : 'text-gray-800';
case 'danger':
return transparent ? 'text-red-500' : 'text-white';
}
};
const getSizeClasses = () => {
switch (size) {
case 'xs':
return 'p-2 text-xs';
case 'sm':
return 'px-4 py-2 text-sm';
case 'icon':
return 'p-2.5';
case 'md':
return 'px-4 py-2 text-base';
case 'lg':
return 'px-6 py-3 text-lg';
case 'xl':
return 'px-8 py-4 text-2xl';
}
};
const getWidthClasses = () => {
return fullWidth ? 'w-full' : '';
};
const getRoundedClasses = () => {
switch (rounded) {
case 'full':
return 'rounded-full';
case true:
case 'semi':
return 'rounded-lg';
case false:
default:
return 'rounded-sm';
}
};
const className = [
transparent ? '' : getBackgroundClasses(),
getHoverClasses(),
getBaseClasses(),
getWidthClasses(),
getRoundedClasses(),
getColorClasses(),
getSizeClasses(),
].join(' ');
</script>
<div class={getWidthClasses()}>
<button on:click {type} {disabled} {title} class={className}>
<slot />
</button>
</div>
+28
View File
@@ -0,0 +1,28 @@
<script lang="ts">
import type { Size } from '../types';
export let size: Size | undefined = undefined;
const getSizeClasses = () => {
switch (size) {
case 'xs':
return 'max-w-64';
case 'sm':
return 'max-w-96';
case 'md':
return 'max-w-screen-sm';
case 'lg':
return 'max-w-screen-md';
case 'xl':
return 'max-w-screen-lg';
default:
return '';
}
};
const className = [getSizeClasses()].join(' ');
</script>
<div class="{className} flex max-w-s flex-col justify-between rounded-3xl bg-immich-gray p-5 dark:bg-immich-dark-gray">
<slot />
</div>
+43
View File
@@ -0,0 +1,43 @@
<script lang="ts">
import type { Size } from '../types';
export let id: string | undefined = undefined;
export let size: Size = 'xl';
export let color: 'primary' = 'primary';
const getColorClasses = () => {
switch (color) {
case 'primary':
return 'text-immich-primary dark:text-immich-dark-primary';
}
};
const getSizeClasses = () => {
switch (size) {
case 'xl':
return 'text-4xl';
case 'lg':
return 'text-2xl';
case 'md':
return 'text-xl';
case 'sm':
return 'text-lg';
case 'xs':
return 'text-md';
}
};
const className = [getColorClasses(), getSizeClasses()].join(' ');
</script>
{#if size === 'xl'}
<h1 {id} class={className}><slot /></h1>
{:else if size === 'lg'}
<h2 {id} class={className}><slot /></h2>
{:else if size === 'md'}
<h3 {id} class={className}><slot /></h3>
{:else if size === 'sm'}
<h4 {id} class={className}><slot /></h4>
{:else if size === 'xs'}
<h5 {id} class={className}><slot /></h5>
{/if}
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
import Icon from './icon.svelte';
import Button from './button.svelte';
import type { Color } from '../types';
export let icon = '';
export let title = '';
export let transparent = true;
export let color: Color = 'primary';
</script>
<Button {title} {transparent} {color} size="icon" rounded="full" on:click>
<slot>
<Icon path={icon} />
</slot>
</Button>
+36
View File
@@ -0,0 +1,36 @@
<script lang="ts">
import type { AriaRole } from 'svelte/elements';
export let size: string | number = '1em';
export let color = 'currentColor';
export let path: string;
export let title: string | null = null;
export let desc = '';
export let flipped = false;
let className = '';
export { className as class };
export let viewBox = '0 0 24 24';
export let role: AriaRole = 'img';
export let ariaHidden: boolean | undefined = undefined;
export let ariaLabel: string | undefined = undefined;
export let ariaLabelledby: string | undefined = undefined;
</script>
<svg
width={size}
height={size}
{viewBox}
class="{className} {flipped ? '-scale-x-100' : ''}"
{role}
aria-label={ariaLabel}
aria-hidden={ariaHidden}
aria-labelledby={ariaLabelledby}
>
{#if title}
<title>{title}</title>
{/if}
{#if desc}
<desc>{desc}</desc>
{/if}
<path d={path} fill={color} />
</svg>
+35
View File
@@ -0,0 +1,35 @@
<script lang="ts">
import Heading from './heading.svelte';
export let title = '';
const titleClass = title ? 'top-16 h-[calc(100%-theme(spacing.16))]' : 'top-0 h-full';
const hasHeader = !!$$slots.header;
const headerPadding = hasHeader ? 'pt-[var(--navbar-height)]' : '';
</script>
{#if hasHeader}
<header class="fixed z-[900] h-[var(--navbar-height)] w-screen">
<slot name="header" />
</header>
{/if}
<main
class="relative grid h-screen grid-cols-[theme(spacing.18)_auto] overflow-hidden bg-immich-bg {headerPadding} dark:bg-immich-dark-bg md:grid-cols-[theme(spacing.64)_auto]"
>
<slot name="sidebar" />
<section class="relative h-full">
{#if title}
<div
class="absolute flex h-16 w-full place-items-center justify-between border-b p-4 dark:border-immich-dark-gray dark:text-immich-dark-fg"
>
<Heading size="lg">{title}</Heading>
<slot name="buttons" />
</div>
{/if}
<div id="content" class="immich-scrollbar p-4 pb-8 scrollbar-stable absolute {titleClass} w-full overflow-y-auto">
<slot />
</div>
</section>
</main>
+31
View File
@@ -0,0 +1,31 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Icon from './icon.svelte';
export let title: string;
export let icon: string;
export let isSelected = false;
export let iconFlipped = false;
const dispatch = createEventDispatcher<{ click: void }>();
const onClick = () => dispatch('click');
const selectedClasses = isSelected
? 'bg-immich-primary/10 text-immich-primary hover:bg-immich-primary/25 dark:bg-immich-dark-primary/10 dark:text-immich-dark-primary'
: '';
</script>
<button
on:click={onClick}
on:keydown={onClick}
class="{selectedClasses} flex w-full place-items-center justify-between gap-4 rounded-r-full py-3 transition-[padding] delay-100 duration-100 hover:cursor-pointer hover:bg-immich-gray hover:text-immich-primary dark:text-immich-dark-fg dark:hover:bg-immich-dark-gray dark:hover:text-immich-dark-primary pl-5 group-hover:sm:px-5 md:px-5"
>
<div class="flex w-full place-items-center gap-4 overflow-hidden truncate">
<Icon path={icon} size="1.5em" class="shrink-0" flipped={iconFlipped} />
<p class="text-sm font-medium">{title}</p>
</div>
<div
class="h-0 overflow-hidden transition-[height] delay-1000 duration-100 sm:group-hover:h-auto group-hover:sm:overflow-visible md:h-auto md:overflow-visible"
></div>
</button>
+5
View File
@@ -0,0 +1,5 @@
<section
class="immich-scrollbar group relative z-10 flex w-18 flex-col gap-1 overflow-y-auto bg-immich-bg pt-8 transition-all duration-200 dark:bg-immich-dark-bg hover:sm:w-64 hover:sm:border-r hover:sm:pr-6 hover:sm:shadow-2xl hover:sm:dark:border-r-immich-dark-gray md:w-64 md:pr-6 hover:md:border-none hover:md:shadow-none"
>
<slot />
</section>
@@ -0,0 +1,24 @@
<script lang="ts">
import { moonPath, moonViewBox, sunPath, sunViewBox } from '$lib/assets/svg-paths';
import { Theme } from '$lib/constants';
import { createEventDispatcher } from 'svelte';
import IconButton from './icon-button.svelte';
import Icon from './icon.svelte';
export let theme: Theme = Theme.LIGHT;
export let title = 'Toggle theme';
const dispatch = createEventDispatcher<{ theme: Theme }>();
const onClick = () => {
theme = theme === Theme.LIGHT ? Theme.DARK : Theme.LIGHT;
dispatch('theme', theme);
};
</script>
<IconButton on:click={onClick} {title}>
{#if theme === Theme.LIGHT}
<Icon path={moonPath} viewBox={sunViewBox} class="h-6 w-6" />
{:else}
<Icon path={sunPath} viewBox={moonViewBox} class="h-6 w-6" />
{/if}
</IconButton>
+9
View File
@@ -0,0 +1,9 @@
export type Color = 'primary' | 'secondary' | 'warning' | 'success' | 'info' | 'danger';
export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
// should be the same values as the ones in the app.html
export enum Theme {
LIGHT = 'light',
DARK = 'dark',
}
+1
View File
@@ -23,6 +23,7 @@ const config = {
alias: {
$lib: 'src/lib',
'$lib/*': 'src/lib/*',
'@ui/*': 'src/ui/*',
'@api': 'src/api',
'@test-data': 'src/test-data',
},
+1
View File
@@ -16,6 +16,7 @@ const config = {
'xmlhttprequest-ssl': './node_modules/engine.io-client/lib/xmlhttprequest.js',
'@test-data': path.resolve(__dirname, './src/test-data'),
'@api': path.resolve('./src/api'),
'@ui': path.resolve('./src/ui'),
},
},
server: {