chore: formatter use 2 spaces instead of tabs (#273)

* chore: formatter use 2 spaces instead of tabs

* chore: format

* chore: use defaults

* chore: 120 width

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
Brandon Wees
2025-09-18 15:26:01 -04:00
committed by GitHub
co-authored by Jason Rasmussen
parent ebbe63d0f9
commit 7f756355f5
243 changed files with 5585 additions and 5831 deletions
+13 -14
View File
@@ -1,16 +1,15 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"tailwindFunctions": ["tv"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"tailwindFunctions": ["tv"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}
+4 -3
View File
@@ -1,5 +1,6 @@
{
"tailwindCSS.experimental.classRegex": [
["([\"'`][^\"'`]*.*?[\"'`])", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
"tailwindCSS.experimental.classRegex": [["([\"'`][^\"'`]*.*?[\"'`])", "[\"'`]([^\"'`]*).*?[\"'`]"]],
"editor.tabSize": 2,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
+9 -17
View File
@@ -14,26 +14,18 @@ Import components from `@immich/ui`. For example:
```html
<script lang="ts">
import {
Card,
CardBody,
CardHeader,
CardTitle,
CardDescription,
Heading,
Text,
} from '@immich/ui';
import { Card, CardBody, CardHeader, CardTitle, CardDescription, Heading, Text } from '@immich/ui';
</script>
<Card>
<CardHeader>
<CardTitle>@immich/ui</CardTitle>
<CardDescription>A component library</CardDescription>
</CardHeader>
<CardBody>
<Lorem />
</CardBody>
<CardFooter>Privacy should not be a luxury</CardFooter>
<CardHeader>
<CardTitle>@immich/ui</CardTitle>
<CardDescription>A component library</CardDescription>
</CardHeader>
<CardBody>
<Lorem />
</CardBody>
<CardFooter>Privacy should not be a luxury</CardFooter>
</Card>
```
+24 -24
View File
@@ -5,28 +5,28 @@ import globals from 'globals';
import ts from 'typescript-eslint';
export default ts.config(
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
{
languageOptions: {
parserOptions: {
parser: ts.parser,
extraFileExtensions: ['.svelte'],
},
},
},
{
ignores: ['build/', '.svelte-kit/', 'dist/'],
},
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
{
languageOptions: {
parserOptions: {
parser: ts.parser,
extraFileExtensions: ['.svelte'],
},
},
},
{
ignores: ['build/', '.svelte-kit/', 'dist/'],
},
);
+77 -77
View File
@@ -1,79 +1,79 @@
{
"name": "@immich/ui",
"version": "0.30.0",
"license": "GNU Affero General Public License version 3",
"scripts": {
"create": "node scripts/create.js",
"start": "npm run start:dev",
"start:dev": "vite dev",
"watch": "vite build --watch",
"build": "vite build && npm run package",
"preview": "vite preview",
"package": "npm run sync && svelte-package && publint",
"check": "npm run sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "npm run sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "eslint . --max-warnings 0",
"lint:fix": "npm run lint -- --fix",
"format": "prettier --check .",
"format:fix": "prettier --write .",
"test": "vitest",
"sync": "svelte-kit sync"
},
"files": [
"dist",
"!dist/**/*.test.*",
"!dist/**/*.spec.*"
],
"sideEffects": [
"**/*.css"
],
"svelte": "./dist/index.js",
"types": "./dist/index.d.ts",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"svelte": "./dist/index.js"
},
"./theme/default.css": "./dist/theme/default.css"
},
"peerDependencies": {
"svelte": "^5.0.0"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/kit": "^2.13.0",
"@sveltejs/package": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/postcss": "^4.1.7",
"@tailwindcss/vite": "^4.1.7",
"@types/eslint": "^9.6.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.7.0",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-svelte": "^3.9.3",
"globals": "^16.0.0",
"prettier": "^3.3.2",
"prettier-plugin-svelte": "^3.2.6",
"prettier-plugin-tailwindcss": "^0.6.11",
"publint": "^0.3.0",
"svelte": "^5.37.0",
"svelte-check": "^4.0.0",
"svelte-highlight": "^7.8.0",
"tailwindcss": "^4.1.7",
"typescript": "^5.0.0",
"typescript-eslint": "^8.34.1",
"vite": "^7.0.0",
"vitest": "^3.0.0"
},
"dependencies": {
"@mdi/js": "^7.4.47",
"bits-ui": "^2.9.8",
"simple-icons": "^15.14.0",
"tailwind-merge": "^3.0.0",
"tailwind-variants": "^3.0.0"
},
"volta": {
"node": "22.19.0"
}
"name": "@immich/ui",
"version": "0.30.0",
"license": "GNU Affero General Public License version 3",
"scripts": {
"create": "node scripts/create.js",
"start": "npm run start:dev",
"start:dev": "vite dev",
"watch": "vite build --watch",
"build": "vite build && npm run package",
"preview": "vite preview",
"package": "npm run sync && svelte-package && publint",
"check": "npm run sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "npm run sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "eslint . --max-warnings 0",
"lint:fix": "npm run lint -- --fix",
"format": "prettier --check .",
"format:fix": "prettier --write .",
"test": "vitest",
"sync": "svelte-kit sync"
},
"files": [
"dist",
"!dist/**/*.test.*",
"!dist/**/*.spec.*"
],
"sideEffects": [
"**/*.css"
],
"svelte": "./dist/index.js",
"types": "./dist/index.d.ts",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"svelte": "./dist/index.js"
},
"./theme/default.css": "./dist/theme/default.css"
},
"peerDependencies": {
"svelte": "^5.0.0"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/kit": "^2.13.0",
"@sveltejs/package": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/postcss": "^4.1.7",
"@tailwindcss/vite": "^4.1.7",
"@types/eslint": "^9.6.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.7.0",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-svelte": "^3.9.3",
"globals": "^16.0.0",
"prettier": "^3.3.2",
"prettier-plugin-svelte": "^3.2.6",
"prettier-plugin-tailwindcss": "^0.6.11",
"publint": "^0.3.0",
"svelte": "^5.37.0",
"svelte-check": "^4.0.0",
"svelte-highlight": "^7.8.0",
"tailwindcss": "^4.1.7",
"typescript": "^5.0.0",
"typescript-eslint": "^8.34.1",
"vite": "^7.0.0",
"vitest": "^3.0.0"
},
"dependencies": {
"@mdi/js": "^7.4.47",
"bits-ui": "^2.9.8",
"simple-icons": "^15.14.0",
"tailwind-merge": "^3.0.0",
"tailwind-variants": "^3.0.0"
},
"volta": {
"node": "22.19.0"
}
}
+3 -3
View File
@@ -1,5 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
plugins: {
'@tailwindcss/postcss': {},
},
};
+2 -2
View File
@@ -1,4 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["local>immich-app/.github:renovate-config"]
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["local>immich-app/.github:renovate-config"]
}
+23 -24
View File
@@ -6,8 +6,7 @@ const svelte = String.raw;
/**
* @param {string} value
*/
const asSlug = (value) =>
value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`).replace(/^-/, '');
const asSlug = (value) => value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`).replace(/^-/, '');
/**
* @param {string} filename
@@ -18,7 +17,7 @@ const readTemplate = (filename) => readFile(join(import.meta.dirname, 'templates
* @param {string} component
*/
const getPageData = (component) => {
return svelte`
return svelte`
<script lang="ts">
import ComponentExamples from '$docs/components/ComponentExamples.svelte';
import ComponentPage from '$docs/components/ComponentPage.svelte';
@@ -36,7 +35,7 @@ const getPageData = (component) => {
* @param {string} component
*/
const getExampleData = (component) => {
return svelte`
return svelte`
<script>
import { Text } from '@immich/ui';
</script>
@@ -46,32 +45,32 @@ const getExampleData = (component) => {
};
const create = async (component) => {
const componentData = await readTemplate('Component.svelte');
const slug = asSlug(component);
const files = [
{ file: `src/lib/components/${component}/${component}.svelte`, data: componentData },
{ file: `src/routes/components/${slug}/+page.svelte`, data: getPageData(component) },
{ file: `src/routes/components/${slug}/BasicExample.svelte`, data: getExampleData(component) },
];
const componentData = await readTemplate('Component.svelte');
const slug = asSlug(component);
const files = [
{ file: `src/lib/components/${component}/${component}.svelte`, data: componentData },
{ file: `src/routes/components/${slug}/+page.svelte`, data: getPageData(component) },
{ file: `src/routes/components/${slug}/BasicExample.svelte`, data: getExampleData(component) },
];
for (const { file: filepath, data } of files) {
console.log(`Writing ${filepath}`);
const directory = dirname(filepath);
await mkdir(directory, { recursive: true });
await writeFile(filepath, data);
}
for (const { file: filepath, data } of files) {
console.log(`Writing ${filepath}`);
const directory = dirname(filepath);
await mkdir(directory, { recursive: true });
await writeFile(filepath, data);
}
};
const componentName = process.argv[2];
if (!componentName) {
console.log(` usage: node scripts/create.js <component-name>`);
process.exit(1);
console.log(` usage: node scripts/create.js <component-name>`);
process.exit(1);
}
create(componentName)
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
+32 -32
View File
@@ -1,40 +1,40 @@
<script lang="ts">
import type { Color, Size } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
import { tv } from 'tailwind-variants';
import type { Color, Size } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
import { tv } from 'tailwind-variants';
type Props = {
size?: Size;
color?: Color;
class?: string;
children?: Snippet;
};
type Props = {
size?: Size;
color?: Color;
class?: string;
children?: Snippet;
};
const { class: className, color = 'primary', size = 'medium', children }: Props = $props();
const { class: className, color = 'primary', size = 'medium', children }: Props = $props();
const styles = tv({
base: '',
variants: {
size: {
tiny: 'text-xs',
small: 'text-sm',
medium: 'text-base',
large: 'text-lg',
giant: 'text-xl',
},
color: {
primary: 'text-primary',
secondary: 'text-secondary',
success: 'text-success',
danger: 'text-danger',
warning: 'text-warning',
info: 'text-info',
},
},
});
const styles = tv({
base: '',
variants: {
size: {
tiny: 'text-xs',
small: 'text-sm',
medium: 'text-base',
large: 'text-lg',
giant: 'text-xl',
},
color: {
primary: 'text-primary',
secondary: 'text-secondary',
success: 'text-success',
danger: 'text-danger',
warning: 'text-warning',
info: 'text-info',
},
},
});
</script>
<div class={cleanClass(styles({ size, color }), className)}>
{@render children?.()}
{@render children?.()}
</div>
+2 -2
View File
@@ -1,7 +1,7 @@
body.dark {
color-scheme: dark;
color-scheme: dark;
}
body:not(.dark) {
color-scheme: light;
color-scheme: light;
}
+7 -7
View File
@@ -1,13 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
+9 -9
View File
@@ -1,12 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" class="dark bg-light text-dark">
<div>%sveltekit.body%</div>
</body>
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" class="dark bg-light text-dark">
<div>%sveltekit.body%</div>
</body>
</html>
+15 -15
View File
@@ -1,23 +1,23 @@
<script lang="ts">
import { asComponentHref } from '$docs/utilities.js';
import { Button, Icon, VStack, Text } from '@immich/ui';
import { asComponentHref } from '$docs/utilities.js';
import { Button, Icon, VStack, Text } from '@immich/ui';
type Props = {
component: { name: string; icon: string };
};
type Props = {
component: { name: string; icon: string };
};
const { component }: Props = $props();
const { component }: Props = $props();
</script>
<Button
href={asComponentHref(component.name)}
variant="outline"
color="secondary"
class="h-24 w-44 rounded-2xl border border-gray-300 dark:border-gray-700"
size="large"
href={asComponentHref(component.name)}
variant="outline"
color="secondary"
class="h-24 w-44 rounded-2xl border border-gray-300 dark:border-gray-700"
size="large"
>
<VStack gap={2}>
<Icon icon={component.icon} size="1.5rem" aria-hidden />
<Text size="small">{component.name}</Text>
</VStack>
<VStack gap={2}>
<Icon icon={component.icon} size="1.5rem" aria-hidden />
<Text size="small">{component.name}</Text>
</VStack>
</Button>
@@ -1,12 +1,12 @@
<script lang="ts">
import { Text } from '@immich/ui';
import type { Snippet } from 'svelte';
import { Text } from '@immich/ui';
import type { Snippet } from 'svelte';
type Props = {
children: Snippet;
};
type Props = {
children: Snippet;
};
const { children }: Props = $props();
const { children }: Props = $props();
</script>
<Text color="muted">{@render children()}</Text>
+44 -52
View File
@@ -1,67 +1,59 @@
<script lang="ts">
import { type ExampleCardProps } from '$docs/constants.js';
import { Button, Card, CardBody, CardHeader, CardTitle, HStack, Theme } from '@immich/ui';
import { mdiEye, mdiXml } from '@mdi/js';
import { HighlightSvelte, LineNumbers } from 'svelte-highlight';
import atomOneDark from 'svelte-highlight/styles/atom-one-dark';
import { type ExampleCardProps } from '$docs/constants.js';
import { Button, Card, CardBody, CardHeader, CardTitle, HStack, Theme } from '@immich/ui';
import { mdiEye, mdiXml } from '@mdi/js';
import { HighlightSvelte, LineNumbers } from 'svelte-highlight';
import atomOneDark from 'svelte-highlight/styles/atom-one-dark';
const { title, component: Component, code, theme }: ExampleCardProps = $props();
const { title, component: Component, code, theme }: ExampleCardProps = $props();
let viewMode = $state<'code' | 'preview'>('preview');
let viewMode = $state<'code' | 'preview'>('preview');
const handleToggle = () => {
viewMode = viewMode === 'code' ? 'preview' : 'code';
};
const handleToggle = () => {
viewMode = viewMode === 'code' ? 'preview' : 'code';
};
function getCardBodyClass(viewMode: string, theme?: Theme): string {
if (viewMode === 'code') {
return 'p-0';
}
function getCardBodyClass(viewMode: string, theme?: Theme): string {
if (viewMode === 'code') {
return 'p-0';
}
if (theme === Theme.Light) {
return 'bg-white dark:bg-white';
} else if (theme === Theme.Dark) {
return 'bg-black dark:bg-black';
}
if (theme === Theme.Light) {
return 'bg-white dark:bg-white';
} else if (theme === Theme.Dark) {
return 'bg-black dark:bg-black';
}
return '';
}
return '';
}
const cardBodyClass = $derived(getCardBodyClass(viewMode, theme));
const cardBodyClass = $derived(getCardBodyClass(viewMode, theme));
</script>
<svelte:head>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html atomOneDark}
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html atomOneDark}
</svelte:head>
<Card>
<CardHeader>
<div class="flex justify-between">
<CardTitle tag="h2">{title}</CardTitle>
<HStack gap={1}>
<Button
leadingIcon={mdiEye}
disabled={viewMode === 'preview'}
onclick={handleToggle}
size="small">Preview</Button
>
<Button
leadingIcon={mdiXml}
disabled={viewMode === 'code'}
onclick={handleToggle}
size="small">Code</Button
>
</HStack>
</div>
</CardHeader>
<CardBody class={cardBodyClass}>
{#if viewMode === 'preview'}
<Component />
{:else}
<HighlightSvelte code={code.trim().replaceAll(/\t/gm, ' ')} let:highlighted>
<LineNumbers {highlighted} hideBorder wrapLines />
</HighlightSvelte>
{/if}
</CardBody>
<CardHeader>
<div class="flex justify-between">
<CardTitle tag="h2">{title}</CardTitle>
<HStack gap={1}>
<Button leadingIcon={mdiEye} disabled={viewMode === 'preview'} onclick={handleToggle} size="small"
>Preview</Button
>
<Button leadingIcon={mdiXml} disabled={viewMode === 'code'} onclick={handleToggle} size="small">Code</Button>
</HStack>
</div>
</CardHeader>
<CardBody class={cardBodyClass}>
{#if viewMode === 'preview'}
<Component />
{:else}
<HighlightSvelte code={code.trim().replaceAll(/\t/gm, ' ')} let:highlighted>
<LineNumbers {highlighted} hideBorder wrapLines />
</HighlightSvelte>
{/if}
</CardBody>
</Card>
+10 -10
View File
@@ -1,17 +1,17 @@
<script lang="ts">
import ExampleCard from '$docs/components/ComponentExampleCard.svelte';
import type { ExampleItem } from '$docs/constants.js';
import { Stack } from '@immich/ui';
import ExampleCard from '$docs/components/ComponentExampleCard.svelte';
import type { ExampleItem } from '$docs/constants.js';
import { Stack } from '@immich/ui';
type Props = {
examples: ExampleItem[];
};
type Props = {
examples: ExampleItem[];
};
const { examples }: Props = $props();
const { examples }: Props = $props();
</script>
<Stack gap={4} class="mt-4">
{#each examples as { title, code, component, theme } (title)}
<ExampleCard {title} {code} {component} {theme} />
{/each}
{#each examples as { title, code, component, theme } (title)}
<ExampleCard {title} {code} {component} {theme} />
{/each}
</Stack>
@@ -1,12 +1,11 @@
<script lang="ts">
import ComponentTipCard from '$docs/components/ComponentTipCard.svelte';
import { asComponentHref } from '$docs/utilities.js';
import { Link, Text } from '@immich/ui';
import ComponentTipCard from '$docs/components/ComponentTipCard.svelte';
import { asComponentHref } from '$docs/utilities.js';
import { Link, Text } from '@immich/ui';
</script>
<ComponentTipCard>
<Text
>This component can be used with the <Link href={asComponentHref('Field')}>Field</Link> component
to build forms</Text
>
<Text
>This component can be used with the <Link href={asComponentHref('Field')}>Field</Link> component to build forms</Text
>
</ComponentTipCard>
+7 -7
View File
@@ -1,14 +1,14 @@
<script lang="ts">
import { Alert } from '@immich/ui';
import type { Snippet } from 'svelte';
import { Alert } from '@immich/ui';
import type { Snippet } from 'svelte';
type Props = {
children: Snippet;
};
type Props = {
children: Snippet;
};
const { children }: Props = $props();
const { children }: Props = $props();
</script>
<Alert color="info" class="mt-4" title="Note">
{@render children()}
{@render children()}
</Alert>
+23 -23
View File
@@ -1,31 +1,31 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { Container, Heading, type ContainerSize } from '@immich/ui';
import type { Snippet } from 'svelte';
import { resolve } from '$app/paths';
import { Container, Heading, type ContainerSize } from '@immich/ui';
import type { Snippet } from 'svelte';
type Props = {
name: string;
size?: ContainerSize;
children?: Snippet;
};
type Props = {
name: string;
size?: ContainerSize;
children?: Snippet;
};
const { size = 'medium', name, children }: Props = $props();
const { size = 'medium', name, children }: Props = $props();
</script>
<div class="flex h-full flex-col">
<!-- TODO replace with breadcrumb component -->
<nav class="bg-light text-dark flex shrink-0 justify-between border-b p-4">
<div class="flex items-center gap-2">
<a href={resolve('/')} class="underline">Home</a>
<span>/</span>
<a href={resolve('/')} class="underline">Components</a>
<span>/</span>
<span class="capitalize">{name}</span>
</div>
</nav>
<!-- TODO replace with breadcrumb component -->
<nav class="bg-light text-dark flex shrink-0 justify-between border-b p-4">
<div class="flex items-center gap-2">
<a href={resolve('/')} class="underline">Home</a>
<span>/</span>
<a href={resolve('/')} class="underline">Components</a>
<span>/</span>
<span class="capitalize">{name}</span>
</div>
</nav>
<Container {size} class="flex flex-col p-4">
<Heading tag="h1" size="large">{name}</Heading>
{@render children?.()}
</Container>
<Container {size} class="flex flex-col p-4">
<Heading tag="h1" size="large">{name}</Heading>
{@render children?.()}
</Container>
</div>
+8 -8
View File
@@ -1,15 +1,15 @@
<script lang="ts">
import { Alert } from '@immich/ui';
import { mdiLightbulbOnOutline } from '@mdi/js';
import type { Snippet } from 'svelte';
import { Alert } from '@immich/ui';
import { mdiLightbulbOnOutline } from '@mdi/js';
import type { Snippet } from 'svelte';
type Props = {
children: Snippet;
};
type Props = {
children: Snippet;
};
const { children }: Props = $props();
const { children }: Props = $props();
</script>
<Alert color="success" class="mt-4" title="Tip" icon={mdiLightbulbOnOutline}>
{@render children()}
{@render children()}
</Alert>
+38 -38
View File
@@ -1,45 +1,45 @@
<script lang="ts">
import type { Color, Size } from '@immich/ui';
import { cleanClass } from '$lib/utils.js';
import { tv } from 'tailwind-variants';
import type { Color, Size } from '@immich/ui';
import { cleanClass } from '$lib/utils.js';
import { tv } from 'tailwind-variants';
type Props = {
color?: Color;
width?: Size | 'full';
height?: Size | 'full';
};
type Props = {
color?: Color;
width?: Size | 'full';
height?: Size | 'full';
};
const { width = 'medium', height = 'medium', color = 'primary' }: Props = $props();
const { width = 'medium', height = 'medium', color = 'primary' }: Props = $props();
const styles = tv({
base: '',
variants: {
color: {
primary: 'bg-primary',
secondary: 'bg-dark',
success: 'bg-success',
danger: 'bg-danger',
warning: 'bg-warning',
info: 'bg-info',
},
width: {
tiny: 'w-4',
small: 'w-8',
medium: 'w-16',
large: 'w-32',
giant: 'w-64',
full: 'w-full',
},
height: {
tiny: 'h-4',
small: 'h-8',
medium: 'h-16',
large: 'h-32',
giant: 'h-64',
full: 'h-full',
},
},
});
const styles = tv({
base: '',
variants: {
color: {
primary: 'bg-primary',
secondary: 'bg-dark',
success: 'bg-success',
danger: 'bg-danger',
warning: 'bg-warning',
info: 'bg-info',
},
width: {
tiny: 'w-4',
small: 'w-8',
medium: 'w-16',
large: 'w-32',
giant: 'w-64',
full: 'w-full',
},
height: {
tiny: 'h-4',
small: 'h-8',
medium: 'h-16',
large: 'h-32',
giant: 'h-64',
full: 'h-full',
},
},
});
</script>
<div class={cleanClass(styles({ width, height, color }))}></div>
+6 -6
View File
@@ -1,13 +1,13 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import type { Snippet } from 'svelte';
type Props = {
children: Snippet;
};
type Props = {
children: Snippet;
};
const { children }: Props = $props();
const { children }: Props = $props();
</script>
<div class="flex flex-wrap gap-4">
{@render children()}
{@render children()}
</div>
+12 -12
View File
@@ -1,19 +1,19 @@
<script lang="ts">
type Props = { count?: number };
const { count = 1 }: Props = $props();
type Props = { count?: number };
const { count = 1 }: Props = $props();
const lorem =
'Lorem ipsum dolor sit amet consectetur adipisicing elit. A ipsam tenetur accusantium impedit beatae omnis necessitatibus. Voluptatum blanditiis libero impedit, harum eius inventore nihil, officia voluptate dolorum error consequatur animi.';
const lorem =
'Lorem ipsum dolor sit amet consectetur adipisicing elit. A ipsam tenetur accusantium impedit beatae omnis necessitatibus. Voluptatum blanditiis libero impedit, harum eius inventore nihil, officia voluptate dolorum error consequatur animi.';
const make = (value: number) => {
let content = '';
for (let i = 0; i < value; i++) {
content += lorem;
}
return content;
};
const make = (value: number) => {
let content = '';
for (let i = 0; i < value; i++) {
content += lorem;
}
return content;
};
const text = $derived(make(count));
const text = $derived(make(count));
</script>
{text}
+24 -24
View File
@@ -1,31 +1,31 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { IconButton, Logo, Theme } from '@immich/ui';
import { mdiMenu } from '@mdi/js';
import type { Snippet } from 'svelte';
import { resolve } from '$app/paths';
import { IconButton, Logo, Theme } from '@immich/ui';
import { mdiMenu } from '@mdi/js';
import type { Snippet } from 'svelte';
type Props = {
theme?: Theme;
children?: Snippet;
onToggleSidebar?: () => void;
};
type Props = {
theme?: Theme;
children?: Snippet;
onToggleSidebar?: () => void;
};
const { children, theme = Theme.Dark, onToggleSidebar }: Props = $props();
const { children, theme = Theme.Dark, onToggleSidebar }: Props = $props();
</script>
<nav class="{theme} flex items-center gap-2 p-2">
<IconButton
shape="round"
color="secondary"
variant="ghost"
size="medium"
aria-label="Main menu"
icon={mdiMenu}
onclick={() => onToggleSidebar?.()}
class="md:hidden"
/>
<a href={resolve('/')} class="flex grow gap-2 text-4xl">
<Logo variant="inline" />
</a>
{@render children?.()}
<IconButton
shape="round"
color="secondary"
variant="ghost"
size="medium"
aria-label="Main menu"
icon={mdiMenu}
onclick={() => onToggleSidebar?.()}
class="md:hidden"
/>
<a href={resolve('/')} class="flex grow gap-2 text-4xl">
<Logo variant="inline" />
</a>
{@render children?.()}
</nav>
+108 -108
View File
@@ -1,123 +1,123 @@
import type { Theme } from '@immich/ui';
import {
mdiAccountCircle,
mdiAccountCircleOutline,
mdiAlertCircle,
mdiAlertCircleOutline,
mdiApplication,
mdiApplicationOutline,
mdiButtonCursor,
mdiCard,
mdiCardOutline,
mdiCheckboxMarked,
mdiCheckboxMultipleMarked,
mdiCheckboxMultipleMarkedOutline,
mdiCheckboxOutline,
mdiCloseCircle,
mdiCloseCircleOutline,
mdiCodeBraces,
mdiDotsCircle,
mdiFormatHeaderPound,
mdiFormDropdown,
mdiFormTextarea,
mdiFormTextbox,
mdiFormTextboxPassword,
mdiHelpBox,
mdiHelpBoxOutline,
mdiHomeCircle,
mdiHomeCircleOutline,
mdiImage,
mdiImageOutline,
mdiKeyboardVariant,
mdiLink,
mdiListBox,
mdiListBoxOutline,
mdiMenu,
mdiNumeric,
mdiPanVertical,
mdiPartyPopper,
mdiSquare,
mdiSquareOutline,
mdiThemeLightDark,
mdiToggleSwitch,
mdiToggleSwitchOutline,
mdiViewSequential,
mdiViewSequentialOutline,
mdiWindowMaximize,
mdiAccountCircle,
mdiAccountCircleOutline,
mdiAlertCircle,
mdiAlertCircleOutline,
mdiApplication,
mdiApplicationOutline,
mdiButtonCursor,
mdiCard,
mdiCardOutline,
mdiCheckboxMarked,
mdiCheckboxMultipleMarked,
mdiCheckboxMultipleMarkedOutline,
mdiCheckboxOutline,
mdiCloseCircle,
mdiCloseCircleOutline,
mdiCodeBraces,
mdiDotsCircle,
mdiFormatHeaderPound,
mdiFormDropdown,
mdiFormTextarea,
mdiFormTextbox,
mdiFormTextboxPassword,
mdiHelpBox,
mdiHelpBoxOutline,
mdiHomeCircle,
mdiHomeCircleOutline,
mdiImage,
mdiImageOutline,
mdiKeyboardVariant,
mdiLink,
mdiListBox,
mdiListBoxOutline,
mdiMenu,
mdiNumeric,
mdiPanVertical,
mdiPartyPopper,
mdiSquare,
mdiSquareOutline,
mdiThemeLightDark,
mdiToggleSwitch,
mdiToggleSwitchOutline,
mdiViewSequential,
mdiViewSequentialOutline,
mdiWindowMaximize,
} from '@mdi/js';
import type { Component } from 'svelte';
export enum DisplayOption {
Both = 'both',
Light = 'light',
Dark = 'dark',
Both = 'both',
Light = 'light',
Dark = 'dark',
}
export type ExampleItem = {
title: string;
code: string;
component: Component;
theme?: Theme;
title: string;
code: string;
component: Component;
theme?: Theme;
};
export type ExampleCardProps = ExampleItem;
export const componentGroups = [
{
title: 'Layout',
components: [
{ name: 'Alert', icon: mdiAlertCircleOutline, activeIcon: mdiAlertCircle },
{ name: 'AppShell', icon: mdiApplicationOutline, activeIcon: mdiApplication },
{ name: 'Card', icon: mdiCardOutline, activeIcon: mdiCard },
{ name: 'Container', icon: mdiSquareOutline, activeIcon: mdiSquare },
{ name: 'Navbar', icon: mdiMenu },
{ name: 'Modal', icon: mdiWindowMaximize },
{ name: 'ConfirmModal', icon: mdiCheckboxOutline },
{ name: 'Scrollable', icon: mdiPanVertical },
{ name: 'Stack', icon: mdiViewSequentialOutline, activeIcon: mdiViewSequential },
],
},
{
title: 'Forms',
components: [
{ name: 'Button', icon: mdiButtonCursor },
{ name: 'IconButton', icon: mdiHomeCircleOutline, activeIcon: mdiHomeCircle },
{ name: 'Checkbox', icon: mdiCheckboxOutline, activeIcon: mdiCheckboxMarked },
{ name: 'CloseButton', icon: mdiCloseCircleOutline, activeIcon: mdiCloseCircle },
{ name: 'Field', icon: mdiListBoxOutline, activeIcon: mdiListBox },
{ name: 'HelperText', icon: mdiHelpBoxOutline, activeIcon: mdiHelpBox },
{ name: 'Input', icon: mdiFormTextbox },
{ name: 'LoadingSpinner', icon: mdiDotsCircle },
{
name: 'MultiSelect',
icon: mdiCheckboxMultipleMarkedOutline,
activeIcon: mdiCheckboxMultipleMarked,
},
{ name: 'PasswordInput', icon: mdiFormTextboxPassword },
{ name: 'Select', icon: mdiFormDropdown },
{ name: 'Switch', icon: mdiToggleSwitchOutline, activeIcon: mdiToggleSwitch },
],
},
{
title: 'Text',
components: [
{ name: 'Code', icon: mdiCodeBraces },
{ name: 'Kbd', icon: mdiKeyboardVariant },
{ name: 'Text', icon: mdiFormatHeaderPound },
{ name: 'Textarea', icon: mdiFormTextarea },
{ name: 'Heading', icon: mdiFormTextbox },
{ name: 'Link', icon: mdiLink },
{ name: 'FormatBytes', icon: mdiNumeric },
],
},
{
title: 'Miscellaneous',
components: [
{ name: 'Avatar', icon: mdiAccountCircleOutline, activeIcon: mdiAccountCircle },
{ name: 'Logo', icon: mdiImageOutline, activeIcon: mdiImage },
{ name: 'CommandPalette', icon: mdiMenu, activeIcon: mdiMenu },
{ name: 'SupporterBadge', icon: mdiPartyPopper },
{ name: 'ThemeSwitcher', icon: mdiThemeLightDark },
],
},
{
title: 'Layout',
components: [
{ name: 'Alert', icon: mdiAlertCircleOutline, activeIcon: mdiAlertCircle },
{ name: 'AppShell', icon: mdiApplicationOutline, activeIcon: mdiApplication },
{ name: 'Card', icon: mdiCardOutline, activeIcon: mdiCard },
{ name: 'Container', icon: mdiSquareOutline, activeIcon: mdiSquare },
{ name: 'Navbar', icon: mdiMenu },
{ name: 'Modal', icon: mdiWindowMaximize },
{ name: 'ConfirmModal', icon: mdiCheckboxOutline },
{ name: 'Scrollable', icon: mdiPanVertical },
{ name: 'Stack', icon: mdiViewSequentialOutline, activeIcon: mdiViewSequential },
],
},
{
title: 'Forms',
components: [
{ name: 'Button', icon: mdiButtonCursor },
{ name: 'IconButton', icon: mdiHomeCircleOutline, activeIcon: mdiHomeCircle },
{ name: 'Checkbox', icon: mdiCheckboxOutline, activeIcon: mdiCheckboxMarked },
{ name: 'CloseButton', icon: mdiCloseCircleOutline, activeIcon: mdiCloseCircle },
{ name: 'Field', icon: mdiListBoxOutline, activeIcon: mdiListBox },
{ name: 'HelperText', icon: mdiHelpBoxOutline, activeIcon: mdiHelpBox },
{ name: 'Input', icon: mdiFormTextbox },
{ name: 'LoadingSpinner', icon: mdiDotsCircle },
{
name: 'MultiSelect',
icon: mdiCheckboxMultipleMarkedOutline,
activeIcon: mdiCheckboxMultipleMarked,
},
{ name: 'PasswordInput', icon: mdiFormTextboxPassword },
{ name: 'Select', icon: mdiFormDropdown },
{ name: 'Switch', icon: mdiToggleSwitchOutline, activeIcon: mdiToggleSwitch },
],
},
{
title: 'Text',
components: [
{ name: 'Code', icon: mdiCodeBraces },
{ name: 'Kbd', icon: mdiKeyboardVariant },
{ name: 'Text', icon: mdiFormatHeaderPound },
{ name: 'Textarea', icon: mdiFormTextarea },
{ name: 'Heading', icon: mdiFormTextbox },
{ name: 'Link', icon: mdiLink },
{ name: 'FormatBytes', icon: mdiNumeric },
],
},
{
title: 'Miscellaneous',
components: [
{ name: 'Avatar', icon: mdiAccountCircleOutline, activeIcon: mdiAccountCircle },
{ name: 'Logo', icon: mdiImageOutline, activeIcon: mdiImage },
{ name: 'CommandPalette', icon: mdiMenu, activeIcon: mdiMenu },
{ name: 'SupporterBadge', icon: mdiPartyPopper },
{ name: 'ThemeSwitcher', icon: mdiThemeLightDark },
],
},
];
+1 -1
View File
@@ -1,4 +1,4 @@
export const asSlug = (value: string) =>
value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`).replace(/^-/, '');
value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`).replace(/^-/, '');
export const asComponentHref = (componentName: string) => `/components/${asSlug(componentName)}`;
+72 -77
View File
@@ -1,39 +1,39 @@
import type { ActionReturn } from 'svelte/action';
export type Shortcut = {
key: string;
alt?: boolean;
ctrl?: boolean;
shift?: boolean;
meta?: boolean;
key: string;
alt?: boolean;
ctrl?: boolean;
shift?: boolean;
meta?: boolean;
};
export type ShortcutOptions<T = HTMLElement> = {
shortcut: Shortcut;
/** If true, the event handler will not execute if the event comes from an input field */
ignoreInputFields?: boolean;
onShortcut: (event: KeyboardEvent & { currentTarget: T }) => unknown;
preventDefault?: boolean;
shortcut: Shortcut;
/** If true, the event handler will not execute if the event comes from an input field */
ignoreInputFields?: boolean;
onShortcut: (event: KeyboardEvent & { currentTarget: T }) => unknown;
preventDefault?: boolean;
};
export const shortcutLabel = (shortcut: Shortcut) => {
let label = '';
let label = '';
if (shortcut.ctrl) {
label += 'Ctrl ';
}
if (shortcut.alt) {
label += 'Alt ';
}
if (shortcut.meta) {
label += 'Cmd ';
}
if (shortcut.shift) {
label += '⇧';
}
label += shortcut.key.toUpperCase();
if (shortcut.ctrl) {
label += 'Ctrl ';
}
if (shortcut.alt) {
label += 'Alt ';
}
if (shortcut.meta) {
label += 'Cmd ';
}
if (shortcut.shift) {
label += '⇧';
}
label += shortcut.key.toUpperCase();
return label;
return label;
};
/** Determines whether an event should be ignored. The event will be ignored if:
@@ -41,73 +41,68 @@ export const shortcutLabel = (shortcut: Shortcut) => {
* - The element dispatching the event is an input field
*/
export const shouldIgnoreEvent = (event: KeyboardEvent | ClipboardEvent): boolean => {
if (event.target === event.currentTarget) {
return false;
}
const type = (event.target as HTMLInputElement).type;
return ['textarea', 'text', 'date', 'datetime-local', 'email', 'password'].includes(type);
if (event.target === event.currentTarget) {
return false;
}
const type = (event.target as HTMLInputElement).type;
return ['textarea', 'text', 'date', 'datetime-local', 'email', 'password'].includes(type);
};
export const matchesShortcut = (event: KeyboardEvent, shortcut: Shortcut) => {
return (
shortcut.key.toLowerCase() === event.key.toLowerCase() &&
Boolean(shortcut.alt) === event.altKey &&
Boolean(shortcut.ctrl) === event.ctrlKey &&
Boolean(shortcut.shift) === event.shiftKey &&
Boolean(shortcut.meta) === event.metaKey
);
return (
shortcut.key.toLowerCase() === event.key.toLowerCase() &&
Boolean(shortcut.alt) === event.altKey &&
Boolean(shortcut.ctrl) === event.ctrlKey &&
Boolean(shortcut.shift) === event.shiftKey &&
Boolean(shortcut.meta) === event.metaKey
);
};
/** Bind a single keyboard shortcut to node. */
export const shortcut = <T extends HTMLElement>(
node: T,
option: ShortcutOptions<T>,
node: T,
option: ShortcutOptions<T>,
): ActionReturn<ShortcutOptions<T>> => {
const { update: shortcutsUpdate, destroy } = shortcuts(node, [option]);
const { update: shortcutsUpdate, destroy } = shortcuts(node, [option]);
return {
update(newOption) {
shortcutsUpdate?.([newOption]);
},
destroy,
};
return {
update(newOption) {
shortcutsUpdate?.([newOption]);
},
destroy,
};
};
/** Binds multiple keyboard shortcuts to node */
export const shortcuts = <T extends HTMLElement>(
node: T,
options: ShortcutOptions<T>[],
node: T,
options: ShortcutOptions<T>[],
): ActionReturn<ShortcutOptions<T>[]> => {
function onKeydown(event: KeyboardEvent) {
const ignoreShortcut = shouldIgnoreEvent(event);
for (const {
shortcut,
onShortcut,
ignoreInputFields = true,
preventDefault = true,
} of options) {
if (ignoreInputFields && ignoreShortcut) {
continue;
}
function onKeydown(event: KeyboardEvent) {
const ignoreShortcut = shouldIgnoreEvent(event);
for (const { shortcut, onShortcut, ignoreInputFields = true, preventDefault = true } of options) {
if (ignoreInputFields && ignoreShortcut) {
continue;
}
if (matchesShortcut(event, shortcut)) {
if (preventDefault) {
event.preventDefault();
}
onShortcut(event as KeyboardEvent & { currentTarget: T });
return;
}
}
}
if (matchesShortcut(event, shortcut)) {
if (preventDefault) {
event.preventDefault();
}
onShortcut(event as KeyboardEvent & { currentTarget: T });
return;
}
}
}
node.addEventListener('keydown', onKeydown);
node.addEventListener('keydown', onKeydown);
return {
update(newOptions) {
options = newOptions;
},
destroy() {
node.removeEventListener('keydown', onKeydown);
},
};
return {
update(newOptions) {
options = newOptions;
},
destroy() {
node.removeEventListener('keydown', onKeydown);
},
};
};
+10 -10
View File
@@ -7,15 +7,15 @@ const fieldKey = Symbol(withPrefix('field'));
export const setFieldContext = (field: FieldContext) => setContext(fieldKey, field);
export const hasFieldContext = (): boolean => hasContext(fieldKey);
export const getFieldContext = () => {
const {
label,
color = 'secondary',
invalid = false,
readOnly = false,
required = false,
disabled = false,
description,
} = (getContext(fieldKey) as FieldContext) || {};
const {
label,
color = 'secondary',
invalid = false,
readOnly = false,
required = false,
disabled = false,
description,
} = (getContext(fieldKey) as FieldContext) || {};
return { label, description, color, invalid, readOnly, required, disabled };
return { label, description, color, invalid, readOnly, required, disabled };
};
+7 -7
View File
@@ -5,13 +5,13 @@ import { setContext } from 'svelte';
import { SvelteMap } from 'svelte/reactivity';
export const withChildrenSnippets = (key: ChildKey) => {
const map = new SvelteMap<ChildKey, () => ChildData>();
const map = new SvelteMap<ChildKey, () => ChildData>();
setContext(withPrefix(key), {
register: (child: ChildKey, data: () => ChildData) => map.set(child, data),
});
setContext(withPrefix(key), {
register: (child: ChildKey, data: () => ChildData) => map.set(child, data),
});
return {
getChildren: (key: ChildKey) => map.get(key)?.(),
};
return {
getChildren: (key: ChildKey) => map.get(key)?.(),
};
};
+92 -94
View File
@@ -1,110 +1,108 @@
<script lang="ts">
import Card from '$lib/components/Card/Card.svelte';
import CardBody from '$lib/components/Card/CardBody.svelte';
import CloseButton from '$lib/components/CloseButton/CloseButton.svelte';
import Icon from '$lib/components/Icon/Icon.svelte';
import Text from '$lib/components/Text/Text.svelte';
import type { Color, Size } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import {
mdiAlertOutline,
mdiCheckCircleOutline,
mdiCloseCircleOutline,
mdiInformationVariantCircleOutline,
} from '@mdi/js';
import type { Snippet } from 'svelte';
import Card from '$lib/components/Card/Card.svelte';
import CardBody from '$lib/components/Card/CardBody.svelte';
import CloseButton from '$lib/components/CloseButton/CloseButton.svelte';
import Icon from '$lib/components/Icon/Icon.svelte';
import Text from '$lib/components/Text/Text.svelte';
import type { Color, Size } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import {
mdiAlertOutline,
mdiCheckCircleOutline,
mdiCloseCircleOutline,
mdiInformationVariantCircleOutline,
} from '@mdi/js';
import type { Snippet } from 'svelte';
type Props = {
color?: Color;
size?: Size;
icon?: string | false;
title?: string;
class?: string;
duration?: number;
closable?: boolean;
controlled?: boolean;
onClose?: () => void;
children?: Snippet;
};
type Props = {
color?: Color;
size?: Size;
icon?: string | false;
title?: string;
class?: string;
duration?: number;
closable?: boolean;
controlled?: boolean;
onClose?: () => void;
children?: Snippet;
};
const {
color = 'primary',
icon: iconOverride,
size = 'large',
title,
class: className,
duration,
controlled,
closable,
onClose,
children,
}: Props = $props();
const {
color = 'primary',
icon: iconOverride,
size = 'large',
title,
class: className,
duration,
controlled,
closable,
onClose,
children,
}: Props = $props();
let open = $state(true);
let open = $state(true);
const iconSizes: Record<Size, string> = {
tiny: '1em',
small: '1.25em',
medium: '1.5em',
large: '1.75em',
giant: '1.85em',
};
const iconSizes: Record<Size, string> = {
tiny: '1em',
small: '1.25em',
medium: '1.5em',
large: '1.75em',
giant: '1.85em',
};
const handleClose = () => {
if (!open) {
return;
}
const handleClose = () => {
if (!open) {
return;
}
if (!controlled) {
open = false;
}
if (!controlled) {
open = false;
}
onClose?.();
};
onClose?.();
};
if (duration) {
setTimeout(handleClose, duration);
}
if (duration) {
setTimeout(handleClose, duration);
}
const icons: Partial<Record<Color, string>> = {
success: mdiCheckCircleOutline,
warning: mdiAlertOutline,
danger: mdiCloseCircleOutline,
};
const icons: Partial<Record<Color, string>> = {
success: mdiCheckCircleOutline,
warning: mdiAlertOutline,
danger: mdiCloseCircleOutline,
};
const icon = $derived(
iconOverride === false
? undefined
: iconOverride || (icons[color] ?? mdiInformationVariantCircleOutline),
);
const icon = $derived(
iconOverride === false ? undefined : iconOverride || (icons[color] ?? mdiInformationVariantCircleOutline),
);
</script>
{#if open}
<Card {color} class={cleanClass(className)}>
<CardBody>
<div class="flex items-center justify-between">
<div class={cleanClass('flex gap-2')}>
{#if icon}
<div>
<Icon {icon} size={iconSizes[size]} />
</div>
{/if}
<Card {color} class={cleanClass(className)}>
<CardBody>
<div class="flex items-center justify-between">
<div class={cleanClass('flex gap-2')}>
{#if icon}
<div>
<Icon {icon} size={iconSizes[size]} />
</div>
{/if}
<div class="flex flex-col gap-2 self-center">
{#if title}
<Text {size} fontWeight={children ? 'bold' : undefined}>{title}</Text>
{/if}
{#if children}
{@render children()}
{/if}
</div>
</div>
{#if closable || onClose}
<div>
<CloseButton onclick={handleClose} />
</div>
{/if}
</div>
</CardBody>
</Card>
<div class="flex flex-col gap-2 self-center">
{#if title}
<Text {size} fontWeight={children ? 'bold' : undefined}>{title}</Text>
{/if}
{#if children}
{@render children()}
{/if}
</div>
</div>
{#if closable || onClose}
<div>
<CloseButton onclick={handleClose} />
</div>
{/if}
</div>
</CardBody>
</Card>
{/if}
+26 -26
View File
@@ -1,34 +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';
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;
};
type Props = {
class?: string;
children?: Snippet;
};
const { class: className, children }: Props = $props();
const { class: className, children }: Props = $props();
const { getChildren: getChildSnippet } = withChildrenSnippets(ChildKey.AppShell);
const header = $derived(getChildSnippet(ChildKey.AppShellHeader));
const sidebar = $derived(getChildSnippet(ChildKey.AppShellSidebar));
const { getChildren: getChildSnippet } = withChildrenSnippets(ChildKey.AppShell);
const header = $derived(getChildSnippet(ChildKey.AppShellHeader));
const sidebar = $derived(getChildSnippet(ChildKey.AppShellSidebar));
</script>
<div class={cleanClass('flex h-dvh flex-col overflow-hidden', className)}>
{#if header}
<header class="border-b">
{@render header?.snippet()}
</header>
{/if}
<div class="flex w-full grow overflow-y-auto">
{#if sidebar}
{@render sidebar?.snippet()}
{/if}
<Scrollable class="grow" resetOnNavigate>
{@render children?.()}
</Scrollable>
</div>
{#if header}
<header class="border-b">
{@render header?.snippet()}
</header>
{/if}
<div class="flex w-full grow overflow-y-auto">
{#if sidebar}
{@render sidebar?.snippet()}
{/if}
<Scrollable class="grow" resetOnNavigate>
{@render children?.()}
</Scrollable>
</div>
</div>
@@ -1,15 +1,15 @@
<script lang="ts">
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import type { Snippet } from 'svelte';
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import type { Snippet } from 'svelte';
type Props = {
children: Snippet;
};
type Props = {
children: Snippet;
};
let { children }: Props = $props();
let { children }: Props = $props();
</script>
<Child for={ChildKey.AppShell} as={ChildKey.AppShellHeader}>
{@render children?.()}
{@render children?.()}
</Child>
@@ -1,27 +1,27 @@
<script lang="ts">
import Scrollable from '$lib/components/Scrollable/Scrollable.svelte';
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import { cleanClass } from '$lib/utils.js';
import { type Snippet } from 'svelte';
import Scrollable from '$lib/components/Scrollable/Scrollable.svelte';
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import { cleanClass } from '$lib/utils.js';
import { type Snippet } from 'svelte';
type Props = {
class?: string;
children: Snippet;
open?: boolean;
};
type Props = {
class?: string;
children: Snippet;
open?: boolean;
};
let { class: className, children, open = $bindable(true) }: Props = $props();
let { class: className, children, open = $bindable(true) }: Props = $props();
</script>
<Child for={ChildKey.AppShell} as={ChildKey.AppShellSidebar}>
<Scrollable
class={cleanClass(
'bg-light text-dark relative shrink-0 transition-all duration-200',
open ? 'w-[min(100vw,16rem)]' : 'w-[0px]',
className,
)}
>
{@render children?.()}
</Scrollable>
<Scrollable
class={cleanClass(
'bg-light text-dark relative shrink-0 transition-all duration-200',
open ? 'w-[min(100vw,16rem)]' : 'w-[0px]',
className,
)}
>
{@render children?.()}
</Scrollable>
</Child>
+29 -35
View File
@@ -1,44 +1,38 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import type { Snippet } from 'svelte';
interface Props {
title?: string | undefined;
description?: string | undefined;
scrollbar?: boolean;
buttons?: Snippet;
children?: Snippet;
}
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 { 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%-(--spacing(16)))]' : 'top-0 h-full');
let scrollbarClass = $derived(scrollbar ? 'immich-scrollbar p-2 pb-8' : 'scrollbar-hidden');
let hasTitleClass = $derived(title ? 'top-16 h-[calc(100%-(--spacing(16)))]' : 'top-0 h-full');
</script>
<section class="relative">
{#if title || buttons}
<div
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}
<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}
{#if title || buttons}
<div
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}
<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>
<div class="{scrollbarClass} scrollbar-stable absolute {hasTitleClass} w-full overflow-y-auto">
{@render children?.()}
</div>
</section>
+45 -55
View File
@@ -1,66 +1,56 @@
<script lang="ts">
import type { Size } from '$lib/types.js';
import { tv } from 'tailwind-variants';
import type { Size } from '$lib/types.js';
import { tv } from 'tailwind-variants';
type Props = {
size?: Size;
color?:
| 'primary'
| 'pink'
| 'red'
| 'yellow'
| 'blue'
| 'green'
| 'purple'
| 'orange'
| 'gray'
| 'amber';
name: string;
};
type Props = {
size?: Size;
color?: 'primary' | 'pink' | 'red' | 'yellow' | 'blue' | 'green' | 'purple' | 'orange' | 'gray' | 'amber';
name: string;
};
const { color = 'primary', size = 'medium', name }: Props = $props();
const { color = 'primary', size = 'medium', name }: Props = $props();
const styles = tv({
base: 'flex h-full w-full items-center justify-center font-medium select-none',
variants: {
size: {
tiny: 'h-5 w-5 text-xs',
small: 'h-7 w-7 text-sm',
medium: 'h-10 w-10 text-base',
large: 'h-12 w-12 text-lg',
giant: 'h-16 w-16 text-xl',
},
color: {
primary: 'bg-primary text-light',
pink: 'text-light bg-pink-400',
red: 'text-light bg-red-500',
yellow: 'text-light bg-yellow-500',
purple: 'text-dark bg-purple-600',
orange: 'text-light bg-orange-600',
gray: 'text-dark bg-gray-600',
amber: 'text-light bg-amber-600',
blue: 'text-dark bg-blue-500',
green: 'text-dark bg-green-600',
},
},
});
const styles = tv({
base: 'flex h-full w-full items-center justify-center font-medium select-none',
variants: {
size: {
tiny: 'h-5 w-5 text-xs',
small: 'h-7 w-7 text-sm',
medium: 'h-10 w-10 text-base',
large: 'h-12 w-12 text-lg',
giant: 'h-16 w-16 text-xl',
},
color: {
primary: 'bg-primary text-light',
pink: 'text-light bg-pink-400',
red: 'text-light bg-red-500',
yellow: 'text-light bg-yellow-500',
purple: 'text-dark bg-purple-600',
orange: 'text-light bg-orange-600',
gray: 'text-dark bg-gray-600',
amber: 'text-light bg-amber-600',
blue: 'text-dark bg-blue-500',
green: 'text-dark bg-green-600',
},
},
});
const wrapper = tv({
base: 'overflow-hidden rounded-full shadow-md',
});
const wrapper = tv({
base: 'overflow-hidden rounded-full shadow-md',
});
const getInitials = (name: string) => {
return name
.split(' ')
.map((part) => part.at(0))
.join('')
.substr(0, 2)
.toUpperCase();
};
const getInitials = (name: string) => {
return name
.split(' ')
.map((part) => part.at(0))
.join('')
.substr(0, 2)
.toUpperCase();
};
const initials = $derived(getInitials(name));
const initials = $derived(getInitials(name));
</script>
<figure class={wrapper()}>
<span class={styles({ size, color })}>{initials}</span>
<span class={styles({ size, color })}>{initials}</span>
</figure>
+3 -3
View File
@@ -1,8 +1,8 @@
<script lang="ts">
import Button from '$lib/internal/Button.svelte';
import type { ButtonProps } from '$lib/types.js';
import Button from '$lib/internal/Button.svelte';
import type { ButtonProps } from '$lib/types.js';
let { ref = $bindable(null), ...props }: ButtonProps = $props();
let { ref = $bindable(null), ...props }: ButtonProps = $props();
</script>
<Button bind:ref {...props} />
+131 -135
View File
@@ -1,154 +1,150 @@
<script lang="ts">
import { withChildrenSnippets } from '$lib/common/use-child.svelte.js';
import IconButton from '$lib/components/IconButton/IconButton.svelte';
import Scrollable from '$lib/components/Scrollable/Scrollable.svelte';
import { ChildKey } from '$lib/constants.js';
import type { Color } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import { mdiChevronDown } from '@mdi/js';
import { type Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import { twMerge } from 'tailwind-merge';
import { tv } from 'tailwind-variants';
import { withChildrenSnippets } from '$lib/common/use-child.svelte.js';
import IconButton from '$lib/components/IconButton/IconButton.svelte';
import Scrollable from '$lib/components/Scrollable/Scrollable.svelte';
import { ChildKey } from '$lib/constants.js';
import type { Color } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import { mdiChevronDown } from '@mdi/js';
import { type Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import { twMerge } from 'tailwind-merge';
import { tv } from 'tailwind-variants';
type Props = HTMLAttributes<HTMLDivElement> & {
ref?: HTMLElement | null;
color?: Color;
shape?: 'round' | 'rectangle';
expanded?: boolean;
expandable?: boolean;
children: Snippet;
};
type Props = HTMLAttributes<HTMLDivElement> & {
ref?: HTMLElement | null;
color?: Color;
shape?: 'round' | 'rectangle';
expanded?: boolean;
expandable?: boolean;
children: Snippet;
};
let {
ref = $bindable(null),
color,
class: className,
shape = 'round',
expanded = $bindable(true),
expandable = false,
children,
...restProps
}: Props = $props();
let {
ref = $bindable(null),
color,
class: className,
shape = 'round',
expanded = $bindable(true),
expandable = false,
children,
...restProps
}: Props = $props();
const containerStyles = tv({
base: 'w-full overflow-hidden shadow-sm',
variants: {
shape: {
rectangle: '',
round: 'rounded-2xl',
},
border: {
true: 'border',
false: '',
},
},
});
const containerStyles = tv({
base: 'w-full overflow-hidden shadow-sm',
variants: {
shape: {
rectangle: '',
round: 'rounded-2xl',
},
border: {
true: 'border',
false: '',
},
},
});
const cardStyles = tv({
base: 'flex h-full w-full flex-col',
variants: {
color: {
primary: 'bg-primary/25 dark:bg-primary/25',
secondary: 'text-dark bg-gray-50 dark:bg-neutral-900 dark:text-white',
success: 'bg-success/15 dark:bg-success/30',
danger: 'bg-danger/15 dark:bg-danger/50',
warning: 'bg-warning/25 dark:bg-warning/50',
info: 'bg-info/25 dark:bg-info/50',
},
},
});
const cardStyles = tv({
base: 'flex h-full w-full flex-col',
variants: {
color: {
primary: 'bg-primary/25 dark:bg-primary/25',
secondary: 'text-dark bg-gray-50 dark:bg-neutral-900 dark:text-white',
success: 'bg-success/15 dark:bg-success/30',
danger: 'bg-danger/15 dark:bg-danger/50',
warning: 'bg-warning/25 dark:bg-warning/50',
info: 'bg-info/25 dark:bg-info/50',
},
},
});
const headerContainerStyles = tv({
base: 'p-4',
variants: {
padding: {
true: '',
false: 'pb-0',
},
border: {
true: 'border-b',
false: '',
},
},
});
const headerContainerStyles = tv({
base: 'p-4',
variants: {
padding: {
true: '',
false: 'pb-0',
},
border: {
true: 'border-b',
false: '',
},
},
});
const onToggle = () => {
expanded = !expanded;
};
const onToggle = () => {
expanded = !expanded;
};
const { getChildren: getChildSnippet } = withChildrenSnippets(ChildKey.Card);
const headerChild = $derived(getChildSnippet(ChildKey.CardHeader));
const bodyChild = $derived(getChildSnippet(ChildKey.CardBody));
const footerChild = $derived(getChildSnippet(ChildKey.CardFooter));
const { getChildren: getChildSnippet } = withChildrenSnippets(ChildKey.Card);
const headerChild = $derived(getChildSnippet(ChildKey.CardHeader));
const bodyChild = $derived(getChildSnippet(ChildKey.CardBody));
const footerChild = $derived(getChildSnippet(ChildKey.CardFooter));
const headerBorder = $derived(!color);
const headerPadding = $derived(headerBorder || !expanded);
const headerBorder = $derived(!color);
const headerPadding = $derived(headerBorder || !expanded);
const headerContainerClasses = $derived(
twMerge(
cleanClass(
headerContainerStyles({
padding: headerPadding,
border: headerBorder,
}),
headerChild?.class,
),
),
);
const headerContainerClasses = $derived(
twMerge(
cleanClass(
headerContainerStyles({
padding: headerPadding,
border: headerBorder,
}),
headerChild?.class,
),
),
);
</script>
{#snippet header()}
{#if expandable}
<button
type="button"
onclick={onToggle}
class={cleanClass('flex w-full items-center justify-between px-4', headerContainerClasses)}
>
<div class="flex flex-col">
{@render headerChild?.snippet()}
</div>
<div>
<IconButton
color="secondary"
icon={mdiChevronDown}
flopped={expanded}
variant="ghost"
shape="round"
size="large"
aria-label="Expand"
/>
</div>
</button>
{:else}
<div class={cleanClass('flex flex-col', headerContainerClasses)}>
{@render headerChild?.snippet()}
</div>
{/if}
{#if expandable}
<button
type="button"
onclick={onToggle}
class={cleanClass('flex w-full items-center justify-between px-4', headerContainerClasses)}
>
<div class="flex flex-col">
{@render headerChild?.snippet()}
</div>
<div>
<IconButton
color="secondary"
icon={mdiChevronDown}
flopped={expanded}
variant="ghost"
shape="round"
size="large"
aria-label="Expand"
/>
</div>
</button>
{:else}
<div class={cleanClass('flex flex-col', headerContainerClasses)}>
{@render headerChild?.snippet()}
</div>
{/if}
{/snippet}
<div
bind:this={ref}
class={cleanClass(containerStyles({ shape, border: !color }), className)}
{...restProps}
>
<div class={cleanClass(cardStyles({ color }))}>
{#if headerChild}
{@render header()}
{/if}
<div bind:this={ref} class={cleanClass(containerStyles({ shape, border: !color }), className)} {...restProps}>
<div class={cleanClass(cardStyles({ color }))}>
{#if headerChild}
{@render header()}
{/if}
{#if bodyChild && expanded}
<Scrollable class={twMerge(cleanClass('p-4', bodyChild?.class))}>
{@render bodyChild?.snippet()}
</Scrollable>
{/if}
{#if bodyChild && expanded}
<Scrollable class={twMerge(cleanClass('p-4', bodyChild?.class))}>
{@render bodyChild?.snippet()}
</Scrollable>
{/if}
{#if footerChild}
<div class={twMerge(cleanClass('flex items-center border-t p-4', footerChild.class))}>
{@render footerChild.snippet()}
</div>
{/if}
{#if footerChild}
<div class={twMerge(cleanClass('flex items-center border-t p-4', footerChild.class))}>
{@render footerChild.snippet()}
</div>
{/if}
{@render children()}
</div>
{@render children()}
</div>
</div>
+9 -9
View File
@@ -1,16 +1,16 @@
<script lang="ts">
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import type { Snippet } from 'svelte';
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import type { Snippet } from 'svelte';
type Props = {
class?: string;
children: Snippet;
};
type Props = {
class?: string;
children: Snippet;
};
let { class: className, children }: Props = $props();
let { class: className, children }: Props = $props();
</script>
<Child for={ChildKey.Card} as={ChildKey.CardBody} class={className}>
{@render children?.()}
{@render children?.()}
</Child>
@@ -1,16 +1,16 @@
<script lang="ts">
import Text from '$lib/components/Text/Text.svelte';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
import Text from '$lib/components/Text/Text.svelte';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
type Props = {
class?: string;
children: Snippet;
};
type Props = {
class?: string;
children: Snippet;
};
let { children, class: className }: Props = $props();
let { children, class: className }: Props = $props();
</script>
<Text size="small" class={cleanClass('text-dark/75', className)}>
{@render children?.()}
{@render children?.()}
</Text>
+9 -9
View File
@@ -1,16 +1,16 @@
<script lang="ts">
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import type { Snippet } from 'svelte';
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import type { Snippet } from 'svelte';
type Props = {
class?: string;
children: Snippet;
};
type Props = {
class?: string;
children: Snippet;
};
let { class: className, children }: Props = $props();
let { class: className, children }: Props = $props();
</script>
<Child for={ChildKey.Card} as={ChildKey.CardFooter} class={className}>
{@render children?.()}
{@render children?.()}
</Child>
+9 -9
View File
@@ -1,16 +1,16 @@
<script lang="ts">
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import type { Snippet } from 'svelte';
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import type { Snippet } from 'svelte';
type Props = {
class?: string;
children: Snippet;
};
type Props = {
class?: string;
children: Snippet;
};
let { class: className, children }: Props = $props();
let { class: className, children }: Props = $props();
</script>
<Child for={ChildKey.Card} as={ChildKey.CardHeader} class={className}>
{@render children?.()}
{@render children?.()}
</Child>
+14 -14
View File
@@ -1,21 +1,21 @@
<script lang="ts">
import Heading from '$lib/components/Heading/Heading.svelte';
import type { HeadingSize, HeadingTag } from '$lib/types.js';
import type { Snippet } from 'svelte';
import Heading from '$lib/components/Heading/Heading.svelte';
import type { HeadingSize, HeadingTag } from '$lib/types.js';
import type { Snippet } from 'svelte';
type Props = {
/**
* The HTML element type.
*/
tag?: HeadingTag;
class?: string;
size?: HeadingSize;
children: Snippet;
};
type Props = {
/**
* The HTML element type.
*/
tag?: HeadingTag;
class?: string;
size?: HeadingSize;
children: Snippet;
};
const { size = 'small', tag, class: className, children }: Props = $props();
const { size = 'small', tag, class: className, children }: Props = $props();
</script>
<Heading {tag} {size} class={className}>
{@render children?.()}
{@render children?.()}
</Heading>
+109 -110
View File
@@ -1,122 +1,121 @@
<script lang="ts">
import { getFieldContext } from '$lib/common/context.svelte.js';
import Icon from '$lib/components/Icon/Icon.svelte';
import Label from '$lib/components/Label/Label.svelte';
import Text from '$lib/components/Text/Text.svelte';
import type { Color, Shape, Size } from '$lib/types.js';
import { cleanClass, generateId } from '$lib/utils.js';
import { mdiCheck, mdiMinus } from '@mdi/js';
import { Checkbox as CheckboxPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
import { tv } from 'tailwind-variants';
import { getFieldContext } from '$lib/common/context.svelte.js';
import Icon from '$lib/components/Icon/Icon.svelte';
import Label from '$lib/components/Label/Label.svelte';
import Text from '$lib/components/Text/Text.svelte';
import type { Color, Shape, Size } from '$lib/types.js';
import { cleanClass, generateId } from '$lib/utils.js';
import { mdiCheck, mdiMinus } from '@mdi/js';
import { Checkbox as CheckboxPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
import { tv } from 'tailwind-variants';
type CheckboxProps = WithoutChildrenOrChild<CheckboxPrimitive.RootProps> & {
color?: Color;
shape?: Shape;
size?: Size;
};
type CheckboxProps = WithoutChildrenOrChild<CheckboxPrimitive.RootProps> & {
color?: Color;
shape?: Shape;
size?: Size;
};
let {
ref = $bindable(null),
checked = $bindable(false),
class: className,
color = 'primary',
shape = 'semi-round',
size = 'small',
...restProps
}: CheckboxProps = $props();
let {
ref = $bindable(null),
checked = $bindable(false),
class: className,
color = 'primary',
shape = 'semi-round',
size = 'small',
...restProps
}: CheckboxProps = $props();
const { readOnly, required, invalid, disabled, label, description, ...labelProps } =
$derived(getFieldContext());
const { readOnly, required, invalid, disabled, label, description, ...labelProps } = $derived(getFieldContext());
const containerStyles = tv({
base: 'ring-offset-background focus-visible:ring-ring peer data-[state=checked]:bg-primary box-content overflow-hidden border-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50',
variants: {
shape: {
rectangle: 'rounded-none',
'semi-round': 'rounded-lg',
round: 'rounded-full',
},
color: {
primary: 'border-primary',
secondary: 'border-dark',
success: 'border-success',
danger: 'border-danger',
warning: 'border-warning',
info: 'border-info',
},
size: {
tiny: 'size-4',
small: 'size-5',
medium: 'size-6',
large: 'size-8',
giant: 'size-10',
},
roundedSize: {
tiny: 'rounded-md',
small: 'rounded-md',
medium: 'rounded-md',
large: 'rounded-lg',
giant: 'rounded-xl',
},
},
});
const containerStyles = tv({
base: 'ring-offset-background focus-visible:ring-ring peer data-[state=checked]:bg-primary box-content overflow-hidden border-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50',
variants: {
shape: {
rectangle: 'rounded-none',
'semi-round': 'rounded-lg',
round: 'rounded-full',
},
color: {
primary: 'border-primary',
secondary: 'border-dark',
success: 'border-success',
danger: 'border-danger',
warning: 'border-warning',
info: 'border-info',
},
size: {
tiny: 'size-4',
small: 'size-5',
medium: 'size-6',
large: 'size-8',
giant: 'size-10',
},
roundedSize: {
tiny: 'rounded-md',
small: 'rounded-md',
medium: 'rounded-md',
large: 'rounded-lg',
giant: 'rounded-xl',
},
},
});
const iconStyles = tv({
variants: {
fullWidth: {
true: 'w-full',
},
color: {
primary: 'bg-primary text-light hover:bg-primary/80',
secondary: 'bg-dark text-light hover:bg-dark/80',
success: 'bg-success text-light hover:bg-success/80',
danger: 'bg-danger text-light hover:bg-danger/80',
warning: 'bg-warning text-light hover:bg-warning/80',
info: 'bg-info text-light hover:bg-info/80',
},
},
});
const iconStyles = tv({
variants: {
fullWidth: {
true: 'w-full',
},
color: {
primary: 'bg-primary text-light hover:bg-primary/80',
secondary: 'bg-dark text-light hover:bg-dark/80',
success: 'bg-success text-light hover:bg-success/80',
danger: 'bg-danger text-light hover:bg-danger/80',
warning: 'bg-warning text-light hover:bg-warning/80',
info: 'bg-info text-light hover:bg-info/80',
},
},
});
const id = generateId();
const inputId = `input-${id}`;
const labelId = `label-${id}`;
const descriptionId = $derived(description ? `description-${id}` : undefined);
const id = generateId();
const inputId = `input-${id}`;
const labelId = `label-${id}`;
const descriptionId = $derived(description ? `description-${id}` : undefined);
</script>
<div class="flex flex-col gap-1">
{#if label}
<Label id={labelId} for={inputId} {label} {...labelProps} />
{#if description}
<Text color="secondary" size="small" id={descriptionId}>{description}</Text>
{/if}
{/if}
{#if label}
<Label id={labelId} for={inputId} {label} {...labelProps} />
{#if description}
<Text color="secondary" size="small" id={descriptionId}>{description}</Text>
{/if}
{/if}
<CheckboxPrimitive.Root
bind:ref
class={cleanClass(
containerStyles({
size,
color: invalid ? 'danger' : color,
shape,
roundedSize: shape === 'semi-round' ? size : undefined,
}),
className,
)}
bind:checked
disabled={disabled || readOnly}
{required}
aria-readonly={disabled || readOnly}
aria-describedby={descriptionId}
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<div class={cleanClass('flex items-center justify-center text-current')}>
{#if indeterminate}
<Icon icon={mdiMinus} size="100%" class={cleanClass(iconStyles({ color }))} />
{:else if checked}
<Icon icon={mdiCheck} size="100%" class={cleanClass(iconStyles({ color }))} />
{/if}
</div>
{/snippet}
</CheckboxPrimitive.Root>
<CheckboxPrimitive.Root
bind:ref
class={cleanClass(
containerStyles({
size,
color: invalid ? 'danger' : color,
shape,
roundedSize: shape === 'semi-round' ? size : undefined,
}),
className,
)}
bind:checked
disabled={disabled || readOnly}
{required}
aria-readonly={disabled || readOnly}
aria-describedby={descriptionId}
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<div class={cleanClass('flex items-center justify-center text-current')}>
{#if indeterminate}
<Icon icon={mdiMinus} size="100%" class={cleanClass(iconStyles({ color }))} />
{:else if checked}
<Icon icon={mdiCheck} size="100%" class={cleanClass(iconStyles({ color }))} />
{/if}
</div>
{/snippet}
</CheckboxPrimitive.Root>
</div>
@@ -1,23 +1,18 @@
<script lang="ts">
import IconButton from '$lib/components/IconButton/IconButton.svelte';
import { t } from '$lib/services/translation.svelte.js';
import type { CloseButtonProps } from '$lib/types.js';
import { mdiClose } from '@mdi/js';
import IconButton from '$lib/components/IconButton/IconButton.svelte';
import { t } from '$lib/services/translation.svelte.js';
import type { CloseButtonProps } from '$lib/types.js';
import { mdiClose } from '@mdi/js';
const {
size = 'medium',
variant = 'ghost',
translations,
...restProps
}: CloseButtonProps = $props();
const { size = 'medium', variant = 'ghost', translations, ...restProps }: CloseButtonProps = $props();
</script>
<IconButton
{...restProps}
icon={mdiClose}
shape="round"
{variant}
{size}
color="secondary"
aria-label={t('close', translations)}
{...restProps}
icon={mdiClose}
shape="round"
{variant}
{size}
color="secondary"
aria-label={t('close', translations)}
/>
+58 -65
View File
@@ -1,76 +1,69 @@
<script lang="ts">
import type { Size, TextColor } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import { tv } from 'tailwind-variants';
import type { Size, TextColor } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import { tv } from 'tailwind-variants';
type Props = {
color?: TextColor;
size?: Size;
variant?: 'filled';
class?: string;
children: Snippet;
} & HTMLAttributes<HTMLElement>;
type Props = {
color?: TextColor;
size?: Size;
variant?: 'filled';
class?: string;
children: Snippet;
} & HTMLAttributes<HTMLElement>;
const {
class: className,
size = 'medium',
variant,
color = 'secondary',
children,
...restProps
}: Props = $props();
const { class: className, size = 'medium', variant, color = 'secondary', children, ...restProps }: Props = $props();
const styles = tv({
base: 'font-monospace',
variants: {
textColor: {
muted: 'text-gray-600 dark:text-gray-400',
primary: 'text-primary',
secondary: 'text-dark',
success: 'text-success',
danger: 'text-danger',
warning: 'text-warning',
info: 'text-info',
},
const styles = tv({
base: 'font-monospace',
variants: {
textColor: {
muted: 'text-gray-600 dark:text-gray-400',
primary: 'text-primary',
secondary: 'text-dark',
success: 'text-success',
danger: 'text-danger',
warning: 'text-warning',
info: 'text-info',
},
filled: {
true: 'rounded-lg p-3',
false: '',
},
filled: {
true: 'rounded-lg p-3',
false: '',
},
filledColor: {
false: '',
muted: 'text-light bg-gray-600 dark:bg-gray-800',
primary: 'text-primary dark:bg-primary/20 bg-gray-200 dark:text-gray-200',
secondary: 'text-light bg-gray-700 dark:bg-gray-200',
success: 'bg-success text-light',
danger: 'bg-danger text-light',
warning: 'bg-warning text-light',
info: 'bg-info text-dark',
},
filledColor: {
false: '',
muted: 'text-light bg-gray-600 dark:bg-gray-800',
primary: 'text-primary dark:bg-primary/20 bg-gray-200 dark:text-gray-200',
secondary: 'text-light bg-gray-700 dark:bg-gray-200',
success: 'bg-success text-light',
danger: 'bg-danger text-light',
warning: 'bg-warning text-light',
info: 'bg-info text-dark',
},
size: {
tiny: 'text-xs',
small: 'text-sm',
medium: 'text-base',
large: 'text-lg',
giant: 'text-xl',
},
},
});
size: {
tiny: 'text-xs',
small: 'text-sm',
medium: 'text-base',
large: 'text-lg',
giant: 'text-xl',
},
},
});
</script>
<code
class={cleanClass(
styles({
filled: variant === 'filled',
filledColor: variant === 'filled' && color,
textColor: color,
size,
}),
className,
)}
{...restProps}>{@render children()}</code
class={cleanClass(
styles({
filled: variant === 'filled',
filledColor: variant === 'filled' && color,
textColor: color,
size,
}),
className,
)}
{...restProps}>{@render children()}</code
>
@@ -1,150 +1,145 @@
<script lang="ts">
import { shortcuts } from '$lib/actions/shortcut.js';
import CloseButton from '$lib/components/CloseButton/CloseButton.svelte';
import CommandPaletteItem from '$lib/components/CommandPalette/CommandPaletteItem.svelte';
import Icon from '$lib/components/Icon/Icon.svelte';
import Input from '$lib/components/Input/Input.svelte';
import Modal from '$lib/components/Modal/Modal.svelte';
import ModalBody from '$lib/components/Modal/ModalBody.svelte';
import ModalFooter from '$lib/components/Modal/ModalFooter.svelte';
import ModalHeader from '$lib/components/Modal/ModalHeader.svelte';
import Stack from '$lib/components/Stack/Stack.svelte';
import Text from '$lib/components/Text/Text.svelte';
import { commandPaletteManager } from '$lib/services/command-palette-manager.svelte';
import { t } from '$lib/services/translation.svelte.js';
import type { TranslationProps } from '$lib/types.js';
import { mdiArrowDown, mdiArrowUp, mdiKeyboardEsc, mdiKeyboardReturn, mdiMagnify } from '@mdi/js';
import { shortcuts } from '$lib/actions/shortcut.js';
import CloseButton from '$lib/components/CloseButton/CloseButton.svelte';
import CommandPaletteItem from '$lib/components/CommandPalette/CommandPaletteItem.svelte';
import Icon from '$lib/components/Icon/Icon.svelte';
import Input from '$lib/components/Input/Input.svelte';
import Modal from '$lib/components/Modal/Modal.svelte';
import ModalBody from '$lib/components/Modal/ModalBody.svelte';
import ModalFooter from '$lib/components/Modal/ModalFooter.svelte';
import ModalHeader from '$lib/components/Modal/ModalHeader.svelte';
import Stack from '$lib/components/Stack/Stack.svelte';
import Text from '$lib/components/Text/Text.svelte';
import { commandPaletteManager } from '$lib/services/command-palette-manager.svelte';
import { t } from '$lib/services/translation.svelte.js';
import type { TranslationProps } from '$lib/types.js';
import { mdiArrowDown, mdiArrowUp, mdiKeyboardEsc, mdiKeyboardReturn, mdiMagnify } from '@mdi/js';
type Props = {
translations?: TranslationProps<
| 'search_placeholder'
| 'search_no_results'
| 'search_recently_used'
| 'command_palette_prompt_default'
>;
};
type Props = {
translations?: TranslationProps<
'search_placeholder' | 'search_no_results' | 'search_recently_used' | 'command_palette_prompt_default'
>;
};
let { translations }: Props = $props();
let { translations }: Props = $props();
let inputElement = $state<HTMLInputElement | null>(null);
let inputElement = $state<HTMLInputElement | null>(null);
const handleOpen = () => commandPaletteManager.open();
const handleClose = () => commandPaletteManager.close();
const handleUp = (event: KeyboardEvent) => handleNavigate(event, 'up');
const handleDown = (event: KeyboardEvent) => handleNavigate(event, 'down');
const handleSelect = (event: KeyboardEvent) => handleNavigate(event, 'select');
const handleNavigate = async (event: KeyboardEvent, direction: 'up' | 'down' | 'select') => {
if (!commandPaletteManager.isOpen) {
return;
}
const handleOpen = () => commandPaletteManager.open();
const handleClose = () => commandPaletteManager.close();
const handleUp = (event: KeyboardEvent) => handleNavigate(event, 'up');
const handleDown = (event: KeyboardEvent) => handleNavigate(event, 'down');
const handleSelect = (event: KeyboardEvent) => handleNavigate(event, 'select');
const handleNavigate = async (event: KeyboardEvent, direction: 'up' | 'down' | 'select') => {
if (!commandPaletteManager.isOpen) {
return;
}
event.preventDefault();
event.preventDefault();
switch (direction) {
case 'up': {
commandPaletteManager.up();
break;
}
switch (direction) {
case 'up': {
commandPaletteManager.up();
break;
}
case 'down': {
commandPaletteManager.down();
break;
}
case 'down': {
commandPaletteManager.down();
break;
}
case 'select': {
await commandPaletteManager.select();
break;
}
}
};
case 'select': {
await commandPaletteManager.select();
break;
}
}
};
</script>
<svelte:window
use:shortcuts={[
{ shortcut: { key: 'k', meta: true }, preventDefault: true, onShortcut: handleOpen },
{ shortcut: { key: 'k', ctrl: true }, preventDefault: true, onShortcut: handleOpen },
{ shortcut: { key: '/' }, preventDefault: true, onShortcut: handleOpen },
{ shortcut: { key: 'ArrowUp' }, ignoreInputFields: false, onShortcut: handleUp },
{ shortcut: { key: 'ArrowDown' }, ignoreInputFields: false, onShortcut: handleDown },
{ shortcut: { key: 'k', ctrl: true }, ignoreInputFields: false, onShortcut: handleUp },
{ shortcut: { key: 'k', meta: true }, ignoreInputFields: false, onShortcut: handleUp },
{ shortcut: { key: 'j', ctrl: true }, ignoreInputFields: false, onShortcut: handleDown },
{ shortcut: { key: 'j', meta: true }, ignoreInputFields: false, onShortcut: handleDown },
{ shortcut: { key: 'Enter' }, ignoreInputFields: false, onShortcut: handleSelect },
{ shortcut: { key: 'Escape' }, onShortcut: handleClose },
]}
use:shortcuts={[
{ shortcut: { key: 'k', meta: true }, preventDefault: true, onShortcut: handleOpen },
{ shortcut: { key: 'k', ctrl: true }, preventDefault: true, onShortcut: handleOpen },
{ shortcut: { key: '/' }, preventDefault: true, onShortcut: handleOpen },
{ shortcut: { key: 'ArrowUp' }, ignoreInputFields: false, onShortcut: handleUp },
{ shortcut: { key: 'ArrowDown' }, ignoreInputFields: false, onShortcut: handleDown },
{ shortcut: { key: 'k', ctrl: true }, ignoreInputFields: false, onShortcut: handleUp },
{ shortcut: { key: 'k', meta: true }, ignoreInputFields: false, onShortcut: handleUp },
{ shortcut: { key: 'j', ctrl: true }, ignoreInputFields: false, onShortcut: handleDown },
{ shortcut: { key: 'j', meta: true }, ignoreInputFields: false, onShortcut: handleDown },
{ shortcut: { key: 'Enter' }, ignoreInputFields: false, onShortcut: handleSelect },
{ shortcut: { key: 'Escape' }, onShortcut: handleClose },
]}
/>
{#if commandPaletteManager.isOpen}
<Modal size="large" onClose={handleClose} closeOnBackdropClick>
<ModalHeader>
<div class="flex place-items-center gap-1">
<Input
bind:ref={inputElement}
bind:value={commandPaletteManager.query}
placeholder={t('search_placeholder', translations)}
leadingIcon={mdiMagnify}
tabindex={1}
/>
<div>
<CloseButton onclick={() => commandPaletteManager.close()} class="md:hidden" />
</div>
</div>
</ModalHeader>
<ModalBody>
<Stack gap={2}>
{#if commandPaletteManager.query}
{#if commandPaletteManager.results.length === 0}
<Text>{t('search_no_results', translations)}</Text>
{/if}
{:else if commandPaletteManager.recentItems.length > 0}
<Text>{t('search_recently_used', translations)}</Text>
{:else}
<Text>{t('command_palette_prompt_default', translations)}</Text>
{/if}
<Modal size="large" onClose={handleClose} closeOnBackdropClick>
<ModalHeader>
<div class="flex place-items-center gap-1">
<Input
bind:ref={inputElement}
bind:value={commandPaletteManager.query}
placeholder={t('search_placeholder', translations)}
leadingIcon={mdiMagnify}
tabindex={1}
/>
<div>
<CloseButton onclick={() => commandPaletteManager.close()} class="md:hidden" />
</div>
</div>
</ModalHeader>
<ModalBody>
<Stack gap={2}>
{#if commandPaletteManager.query}
{#if commandPaletteManager.results.length === 0}
<Text>{t('search_no_results', translations)}</Text>
{/if}
{:else if commandPaletteManager.recentItems.length > 0}
<Text>{t('search_recently_used', translations)}</Text>
{:else}
<Text>{t('command_palette_prompt_default', translations)}</Text>
{/if}
{#if commandPaletteManager.results.length > 0}
<div class="flex flex-col">
{#each commandPaletteManager.results as item, i (i)}
<CommandPaletteItem
{item}
selected={commandPaletteManager.selectedIndex === i}
onRemove={commandPaletteManager.query
? undefined
: () => commandPaletteManager.remove(i)}
onSelect={() => commandPaletteManager.select(i)}
/>
{/each}
</div>
{/if}
</Stack>
</ModalBody>
<ModalFooter>
<div class="flex w-full justify-around">
<div class="flex gap-4">
<div class="flex place-items-center gap-1">
<span class="rounded bg-gray-300 p-1 dark:bg-gray-500">
<Icon icon={mdiKeyboardReturn} size="1rem" />
</span>
<Text size="small">to select</Text>
</div>
{#if commandPaletteManager.results.length > 0}
<div class="flex flex-col">
{#each commandPaletteManager.results as item, i (i)}
<CommandPaletteItem
{item}
selected={commandPaletteManager.selectedIndex === i}
onRemove={commandPaletteManager.query ? undefined : () => commandPaletteManager.remove(i)}
onSelect={() => commandPaletteManager.select(i)}
/>
{/each}
</div>
{/if}
</Stack>
</ModalBody>
<ModalFooter>
<div class="flex w-full justify-around">
<div class="flex gap-4">
<div class="flex place-items-center gap-1">
<span class="rounded bg-gray-300 p-1 dark:bg-gray-500">
<Icon icon={mdiKeyboardReturn} size="1rem" />
</span>
<Text size="small">to select</Text>
</div>
<div class="flex place-items-center gap-1">
<span class="flex gap-1 rounded bg-gray-300 p-1 dark:bg-gray-500">
<Icon icon={mdiArrowUp} size="1rem" />
<Icon icon={mdiArrowDown} size="1rem" />
</span>
<Text size="small">to navigate</Text>
</div>
<div class="flex place-items-center gap-1">
<span class="flex gap-1 rounded bg-gray-300 p-1 dark:bg-gray-500">
<Icon icon={mdiArrowUp} size="1rem" />
<Icon icon={mdiArrowDown} size="1rem" />
</span>
<Text size="small">to navigate</Text>
</div>
<div class="flex place-items-center gap-1">
<span class="rounded bg-gray-300 p-1 dark:bg-gray-500">
<Icon icon={mdiKeyboardEsc} size="1rem" />
</span>
<Text size="small">to close</Text>
</div>
</div>
</div>
</ModalFooter>
</Modal>
<div class="flex place-items-center gap-1">
<span class="rounded bg-gray-300 p-1 dark:bg-gray-500">
<Icon icon={mdiKeyboardEsc} size="1rem" />
</span>
<Text size="small">to close</Text>
</div>
</div>
</div>
</ModalFooter>
</Modal>
{/if}
@@ -1,71 +1,71 @@
<script lang="ts">
import Button from '$lib/components/Button/Button.svelte';
import Icon from '$lib/components/Icon/Icon.svelte';
import IconButton from '$lib/components/IconButton/IconButton.svelte';
import Text from '$lib/components/Text/Text.svelte';
import type { CommandItem } from '$lib/services/command-palette-manager.svelte';
import { mdiClose } from '@mdi/js';
import Button from '$lib/components/Button/Button.svelte';
import Icon from '$lib/components/Icon/Icon.svelte';
import IconButton from '$lib/components/IconButton/IconButton.svelte';
import Text from '$lib/components/Text/Text.svelte';
import type { CommandItem } from '$lib/services/command-palette-manager.svelte';
import { mdiClose } from '@mdi/js';
type Props = {
item: CommandItem;
selected: boolean;
onSelect: () => void;
onRemove?: () => void;
};
type Props = {
item: CommandItem;
selected: boolean;
onSelect: () => void;
onRemove?: () => void;
};
const { item, selected, onRemove, onSelect }: Props = $props();
const { item, selected, onRemove, onSelect }: Props = $props();
const handleRemove = (event: MouseEvent) => {
event.stopPropagation();
onRemove?.();
};
const handleRemove = (event: MouseEvent) => {
event.stopPropagation();
onRemove?.();
};
let ref = $state<HTMLElement | null>(null);
let ref = $state<HTMLElement | null>(null);
$effect(() => {
if (selected && ref) {
ref.scrollIntoView({ block: 'nearest', inline: 'start', behavior: 'smooth' });
}
});
$effect(() => {
if (selected && ref) {
ref.scrollIntoView({ block: 'nearest', inline: 'start', behavior: 'smooth' });
}
});
</script>
<div bind:this={ref} class="p-1">
<Button
onclick={() => onSelect()}
fullWidth
variant={selected ? 'outline' : 'ghost'}
color="secondary"
class="overflow-hidden border"
>
<div class="flex w-full place-items-center justify-between gap-2">
<div class="flex min-w-0 place-items-center gap-2">
<Icon icon={item.icon} size="2rem" class={item.iconClass} />
<div class="flex min-w-0 flex-col">
<div class="flex place-items-center gap-1">
<Text fontWeight="bold">{item.title}</Text>
</div>
{#if item.description}
<Text
size="small"
class="overflow-hidden text-ellipsis whitespace-nowrap"
color={selected ? undefined : 'muted'}>{item.description}</Text
>
{/if}
</div>
</div>
{#if onRemove}
<IconButton
size="small"
onclick={handleRemove}
icon={mdiClose}
shape="round"
variant="ghost"
color="secondary"
aria-label="Remove"
/>
{:else}
<span class="shrink-0">[{item.type}]</span>
{/if}
</div>
</Button>
<Button
onclick={() => onSelect()}
fullWidth
variant={selected ? 'outline' : 'ghost'}
color="secondary"
class="overflow-hidden border"
>
<div class="flex w-full place-items-center justify-between gap-2">
<div class="flex min-w-0 place-items-center gap-2">
<Icon icon={item.icon} size="2rem" class={item.iconClass} />
<div class="flex min-w-0 flex-col">
<div class="flex place-items-center gap-1">
<Text fontWeight="bold">{item.title}</Text>
</div>
{#if item.description}
<Text
size="small"
class="overflow-hidden text-ellipsis whitespace-nowrap"
color={selected ? undefined : 'muted'}>{item.description}</Text
>
{/if}
</div>
</div>
{#if onRemove}
<IconButton
size="small"
onclick={handleRemove}
icon={mdiClose}
shape="round"
variant="ghost"
color="secondary"
aria-label="Remove"
/>
{:else}
<span class="shrink-0">[{item.type}]</span>
{/if}
</div>
</Button>
</div>
@@ -1,57 +1,57 @@
<script lang="ts">
import Button from '$lib/components/Button/Button.svelte';
import Modal from '$lib/components/Modal/Modal.svelte';
import ModalBody from '$lib/components/Modal/ModalBody.svelte';
import ModalFooter from '$lib/components/Modal/ModalFooter.svelte';
import HStack from '$lib/components/Stack/HStack.svelte';
import type { Color } from '$lib/types.js';
import { t } from '$lib/services/translation.svelte.js';
import type { Snippet } from 'svelte';
import Button from '$lib/components/Button/Button.svelte';
import Modal from '$lib/components/Modal/Modal.svelte';
import ModalBody from '$lib/components/Modal/ModalBody.svelte';
import ModalFooter from '$lib/components/Modal/ModalFooter.svelte';
import HStack from '$lib/components/Stack/HStack.svelte';
import type { Color } from '$lib/types.js';
import { t } from '$lib/services/translation.svelte.js';
import type { Snippet } from 'svelte';
interface Props {
title?: string;
icon?: string | boolean;
prompt?: string;
confirmText?: string;
confirmColor?: Color;
disabled?: boolean;
size?: 'small' | 'medium';
onClose: (confirmed: boolean) => void;
promptSnippet?: Snippet;
}
interface Props {
title?: string;
icon?: string | boolean;
prompt?: string;
confirmText?: string;
confirmColor?: Color;
disabled?: boolean;
size?: 'small' | 'medium';
onClose: (confirmed: boolean) => void;
promptSnippet?: Snippet;
}
let {
title = t('confirm'),
icon,
prompt = t('prompt_default'),
confirmText = t('confirm'),
confirmColor = 'danger',
disabled = false,
size = 'small',
onClose,
promptSnippet,
}: Props = $props();
let {
title = t('confirm'),
icon,
prompt = t('prompt_default'),
confirmText = t('confirm'),
confirmColor = 'danger',
disabled = false,
size = 'small',
onClose,
promptSnippet,
}: Props = $props();
const handleConfirm = () => {
onClose(true);
};
const handleConfirm = () => {
onClose(true);
};
</script>
<Modal {title} onClose={() => onClose(false)} {size} {icon}>
<ModalBody>
{#if promptSnippet}{@render promptSnippet()}{:else}
<p>{prompt}</p>
{/if}
</ModalBody>
<ModalBody>
{#if promptSnippet}{@render promptSnippet()}{:else}
<p>{prompt}</p>
{/if}
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose(false)}>
{t('cancel')}
</Button>
<Button shape="round" color={confirmColor} fullWidth onclick={handleConfirm} {disabled}>
{confirmText}
</Button>
</HStack>
</ModalFooter>
<ModalFooter>
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose(false)}>
{t('cancel')}
</Button>
<Button shape="round" color={confirmColor} fullWidth onclick={handleConfirm} {disabled}>
{confirmText}
</Button>
</HStack>
</ModalFooter>
</Modal>
+25 -25
View File
@@ -1,33 +1,33 @@
<script lang="ts">
import type { Size } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
import { tv } from 'tailwind-variants';
import type { Size } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
import { tv } from 'tailwind-variants';
type Props = {
size?: Size | 'full';
class?: string;
center?: boolean;
children?: Snippet;
};
type Props = {
size?: Size | 'full';
class?: string;
center?: boolean;
children?: Snippet;
};
const { center, class: className, size = 'full', children }: Props = $props();
const { center, class: className, size = 'full', children }: Props = $props();
const styles = tv({
base: '',
variants: {
size: {
tiny: 'max-w-lg',
small: 'max-w-(--breakpoint-sm)',
medium: 'max-w-(--breakpoint-md)',
large: 'max-w-(--breakpoint-lg)',
giant: 'max-w-(--breakpoint-xl)',
full: 'w-full',
},
},
});
const styles = tv({
base: '',
variants: {
size: {
tiny: 'max-w-lg',
small: 'max-w-(--breakpoint-sm)',
medium: 'max-w-(--breakpoint-md)',
large: 'max-w-(--breakpoint-lg)',
giant: 'max-w-(--breakpoint-xl)',
full: 'w-full',
},
},
});
</script>
<div class={cleanClass(styles({ size }), center && 'mx-auto w-full', className)}>
{@render children?.()}
{@render children?.()}
</div>
+21 -21
View File
@@ -1,31 +1,31 @@
<script lang="ts">
import { setFieldContext } from '$lib/common/context.svelte.js';
import { withChildrenSnippets } from '$lib/common/use-child.svelte.js';
import { ChildKey } from '$lib/constants.js';
import type { FieldContext } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import { type Snippet } from 'svelte';
import { setFieldContext } from '$lib/common/context.svelte.js';
import { withChildrenSnippets } from '$lib/common/use-child.svelte.js';
import { ChildKey } from '$lib/constants.js';
import type { FieldContext } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import { type Snippet } from 'svelte';
type Props = FieldContext & {
class?: string;
children: Snippet;
};
type Props = FieldContext & {
class?: string;
children: Snippet;
};
const { class: className, children, ...props }: Props = $props();
const { class: className, children, ...props }: Props = $props();
const state = $state(props);
const state = $state(props);
setFieldContext(state);
setFieldContext(state);
const { getChildren: getChildSnippet } = withChildrenSnippets(ChildKey.Field);
const helperTextChildren = $derived(getChildSnippet(ChildKey.HelperText));
const { getChildren: getChildSnippet } = withChildrenSnippets(ChildKey.Field);
const helperTextChildren = $derived(getChildSnippet(ChildKey.HelperText));
</script>
<div class={cleanClass('w-full', className)}>
{@render children()}
{#if helperTextChildren}
<div class={cleanClass('pt-1', helperTextChildren.class)}>
{@render helperTextChildren.snippet()}
</div>
{/if}
{@render children()}
{#if helperTextChildren}
<div class={cleanClass('pt-1', helperTextChildren.class)}>
{@render helperTextChildren.snippet()}
</div>
{/if}
</div>
@@ -1,16 +1,16 @@
<script lang="ts">
import { getBytesWithUnit } from '$lib/utilities/byte-units.js';
import { getBytesWithUnit } from '$lib/utilities/byte-units.js';
type Props = {
bytes: number;
precision?: number;
variant?: 'short' | 'narrow';
};
type Props = {
bytes: number;
precision?: number;
variant?: 'short' | 'narrow';
};
const { bytes, precision, variant = 'short' }: Props = $props();
const { bytes, precision, variant = 'short' }: Props = $props();
const [value, unit] = $derived(getBytesWithUnit(bytes, precision));
const separator = $derived(variant === 'narrow' ? '' : ' ');
const [value, unit] = $derived(getBytesWithUnit(bytes, precision));
const separator = $derived(variant === 'narrow' ? '' : ' ');
</script>
<span>{value}{separator}{unit}</span>
+41 -47
View File
@@ -1,57 +1,51 @@
<script lang="ts">
import type {
FontWeight,
HeadingColor,
HeadingSize,
HeadingTag,
TextVariant,
} from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import Text from '$lib/internal/Text.svelte';
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import { tv } from 'tailwind-variants';
import type { FontWeight, HeadingColor, HeadingSize, HeadingTag, TextVariant } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import Text from '$lib/internal/Text.svelte';
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import { tv } from 'tailwind-variants';
type Props = {
/**
* The HTML element type.
*/
tag?: HeadingTag;
size?: HeadingSize;
color?: HeadingColor;
fontWeight?: FontWeight;
variant?: TextVariant;
class?: string;
type Props = {
/**
* The HTML element type.
*/
tag?: HeadingTag;
size?: HeadingSize;
color?: HeadingColor;
fontWeight?: FontWeight;
variant?: TextVariant;
class?: string;
children: Snippet;
} & HTMLAttributes<HTMLElement>;
children: Snippet;
} & HTMLAttributes<HTMLElement>;
const {
tag = 'p',
size = 'medium',
fontWeight = 'semi-bold',
class: className,
children,
...restProps
}: Props = $props();
const {
tag = 'p',
size = 'medium',
fontWeight = 'semi-bold',
class: className,
children,
...restProps
}: Props = $props();
const styles = tv({
base: 'leading-none tracking-tight',
variants: {
size: {
tiny: 'text-lg',
small: 'text-xl',
medium: 'text-2xl',
large: 'text-3xl',
giant: 'text-4xl',
title: 'text-5xl',
},
},
});
const styles = tv({
base: 'leading-none tracking-tight',
variants: {
size: {
tiny: 'text-lg',
small: 'text-xl',
medium: 'text-2xl',
large: 'text-3xl',
giant: 'text-4xl',
title: 'text-5xl',
},
},
});
const classList = $derived(cleanClass(styles({ size }), className));
const classList = $derived(cleanClass(styles({ size }), className));
</script>
<Text {tag} {fontWeight} class={classList} {...restProps}>
{@render children()}
{@render children()}
</Text>
+17 -17
View File
@@ -1,24 +1,24 @@
<script lang="ts">
import Text from '$lib/components/Text/Text.svelte';
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import type { TextColor } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
import Text from '$lib/components/Text/Text.svelte';
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import type { TextColor } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
type Props = {
color?: TextColor;
class?: string;
children?: Snippet;
};
type Props = {
color?: TextColor;
class?: string;
children?: Snippet;
};
let { class: className, children, color = 'muted' }: Props = $props();
let { class: className, children, color = 'muted' }: Props = $props();
</script>
<Child for={ChildKey.Field} as={ChildKey.HelperText}>
<div class={cleanClass(className)}>
<Text {color} size="small">
{@render children?.()}
</Text>
</div>
<div class={cleanClass(className)}>
<Text {color} size="small">
{@render children?.()}
</Text>
</div>
</Child>
+37 -42
View File
@@ -1,52 +1,47 @@
<script lang="ts">
import type { IconProps } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
import type { IconProps } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
const {
size = '1em',
viewBox = '0 0 24 24',
class: className = '',
flipped = false,
flopped = false,
spin = false,
strokeColor = 'transparent',
strokeWidth = 2,
role = 'img',
title,
icon,
color = 'currentColor',
description,
...restProps
}: IconProps & HTMLAttributes<EventTarget> = $props();
const {
size = '1em',
viewBox = '0 0 24 24',
class: className = '',
flipped = false,
flopped = false,
spin = false,
strokeColor = 'transparent',
strokeWidth = 2,
role = 'img',
title,
icon,
color = 'currentColor',
description,
...restProps
}: IconProps & HTMLAttributes<EventTarget> = $props();
</script>
<svg
width={size}
height={size}
{viewBox}
class={cleanClass(
className,
flipped && '-scale-x-100',
flopped && 'rotate-180',
spin && 'animate-spin',
)}
stroke={strokeColor}
stroke-width={strokeWidth}
{role}
{...restProps}
width={size}
height={size}
{viewBox}
class={cleanClass(className, flipped && '-scale-x-100', flopped && 'rotate-180', spin && 'animate-spin')}
stroke={strokeColor}
stroke-width={strokeWidth}
{role}
{...restProps}
>
{#if title}
<title>{title}</title>
{/if}
{#if description}
<desc>{description}</desc>
{/if}
<path d={typeof icon === 'string' ? icon : icon.path} fill={color} />
{#if title}
<title>{title}</title>
{/if}
{#if description}
<desc>{description}</desc>
{/if}
<path d={typeof icon === 'string' ? icon : icon.path} fill={color} />
</svg>
<style>
svg {
transition: transform 0.2s ease;
}
svg {
transition: transform 0.2s ease;
}
</style>
@@ -1,20 +1,13 @@
<script lang="ts">
import Button from '$lib/internal/Button.svelte';
import Icon from '$lib/components/Icon/Icon.svelte';
import type { IconButtonProps } from '$lib/types.js';
import Button from '$lib/internal/Button.svelte';
import Icon from '$lib/components/Icon/Icon.svelte';
import type { IconButtonProps } from '$lib/types.js';
const {
icon,
flipped,
flopped,
title,
'aria-label': ariaLabel,
...buttonProps
}: IconButtonProps = $props();
const { icon, flipped, flopped, title, 'aria-label': ariaLabel, ...buttonProps }: IconButtonProps = $props();
const buttonTitle = $derived(title || ariaLabel);
const buttonTitle = $derived(title || ariaLabel);
</script>
<Button icon {...buttonProps} title={buttonTitle} aria-label={ariaLabel}>
<Icon {icon} {flipped} {flopped} size="60%" aria-hidden />
<Icon {icon} {flipped} {flopped} size="60%" aria-hidden />
</Button>
+149 -152
View File
@@ -1,171 +1,168 @@
<script lang="ts">
import { getFieldContext } from '$lib/common/context.svelte.js';
import Icon from '$lib/components/Icon/Icon.svelte';
import Label from '$lib/components/Label/Label.svelte';
import Text from '$lib/components/Text/Text.svelte';
import type { InputProps } from '$lib/types.js';
import { cleanClass, generateId, isIconLike } from '$lib/utils.js';
import { tv } from 'tailwind-variants';
import { getFieldContext } from '$lib/common/context.svelte.js';
import Icon from '$lib/components/Icon/Icon.svelte';
import Label from '$lib/components/Label/Label.svelte';
import Text from '$lib/components/Text/Text.svelte';
import type { InputProps } from '$lib/types.js';
import { cleanClass, generateId, isIconLike } from '$lib/utils.js';
import { tv } from 'tailwind-variants';
let {
ref = $bindable(null),
containerRef = $bindable(null),
shape = 'semi-round',
size = 'medium',
class: className,
value = $bindable<string>(),
leadingIcon,
trailingIcon,
trailingText,
inputSize,
...restProps
}: InputProps = $props();
let {
ref = $bindable(null),
containerRef = $bindable(null),
shape = 'semi-round',
size = 'medium',
class: className,
value = $bindable<string>(),
leadingIcon,
trailingIcon,
trailingText,
inputSize,
...restProps
}: InputProps = $props();
const { label, description, readOnly, required, invalid, disabled, ...labelProps } =
$derived(getFieldContext());
const { label, description, readOnly, required, invalid, disabled, ...labelProps } = $derived(getFieldContext());
const iconStyles = tv({
base: 'flex flex-shrink-0 items-center justify-center',
variants: {
size: {
tiny: 'w-6',
small: 'w-8',
medium: 'w-10',
large: 'w-12',
giant: 'w-14',
},
},
});
const iconStyles = tv({
base: 'flex flex-shrink-0 items-center justify-center',
variants: {
size: {
tiny: 'w-6',
small: 'w-8',
medium: 'w-10',
large: 'w-12',
giant: 'w-14',
},
},
});
const containerStyles = tv({
base: 'flex w-full items-center bg-gray-200 outline-none disabled:cursor-not-allowed disabled:bg-gray-300 disabled:text-gray-400 dark:bg-gray-600 dark:disabled:bg-gray-800 dark:disabled:text-gray-200',
variants: {
shape: {
rectangle: 'rounded-none',
'semi-round': '',
round: 'rounded-full',
},
roundedSize: {
tiny: 'rounded-xl',
small: 'rounded-xl',
medium: 'rounded-2xl',
large: 'rounded-2xl',
giant: 'rounded-2xl',
},
invalid: {
true: 'border-danger/80 border',
false: '',
},
},
});
const containerStyles = tv({
base: 'flex w-full items-center bg-gray-200 outline-none disabled:cursor-not-allowed disabled:bg-gray-300 disabled:text-gray-400 dark:bg-gray-600 dark:disabled:bg-gray-800 dark:disabled:text-gray-200',
variants: {
shape: {
rectangle: 'rounded-none',
'semi-round': '',
round: 'rounded-full',
},
roundedSize: {
tiny: 'rounded-xl',
small: 'rounded-xl',
medium: 'rounded-2xl',
large: 'rounded-2xl',
giant: 'rounded-2xl',
},
invalid: {
true: 'border-danger/80 border',
false: '',
},
},
});
const inputStyles = tv({
base: 'flex-1 bg-transparent py-3 outline-none disabled:cursor-not-allowed',
variants: {
textSize: {
tiny: 'text-xs',
small: 'text-sm',
medium: 'text-base',
large: 'text-lg',
giant: 'text-xl',
},
leadingPadding: {
base: 'pl-4',
icon: 'pl-0',
},
trailingPadding: {
base: 'pr-4',
icon: 'pr-0',
},
},
});
const inputStyles = tv({
base: 'flex-1 bg-transparent py-3 outline-none disabled:cursor-not-allowed',
variants: {
textSize: {
tiny: 'text-xs',
small: 'text-sm',
medium: 'text-base',
large: 'text-lg',
giant: 'text-xl',
},
leadingPadding: {
base: 'pl-4',
icon: 'pl-0',
},
trailingPadding: {
base: 'pr-4',
icon: 'pr-0',
},
},
});
const trailingTextStyles = tv({
variants: {
padding: {
base: 'px-4',
icon: 'pl-4',
},
},
});
const trailingTextStyles = tv({
variants: {
padding: {
base: 'px-4',
icon: 'pl-4',
},
},
});
const id = generateId();
const inputId = `input-${id}`;
const labelId = `label-${id}`;
const descriptionId = $derived(description ? `description-${id}` : undefined);
const id = generateId();
const inputId = `input-${id}`;
const labelId = `label-${id}`;
const descriptionId = $derived(description ? `description-${id}` : undefined);
</script>
<div class="flex w-full flex-col gap-1" bind:this={containerRef}>
{#if label}
<Label id={labelId} for={inputId} {label} {...labelProps} />
{/if}
{#if label}
<Label id={labelId} for={inputId} {label} {...labelProps} />
{/if}
{#if description}
<Text color="secondary" size="small" id={descriptionId}>{description}</Text>
{/if}
{#if description}
<Text color="secondary" size="small" id={descriptionId}>{description}</Text>
{/if}
<div
class={cleanClass(
containerStyles({
shape,
roundedSize: shape === 'semi-round' ? size : undefined,
invalid,
}),
className,
)}
>
{#if leadingIcon}
<div tabindex="-1" class={iconStyles({ size })}>
{#if isIconLike(leadingIcon)}
<Icon size="60%" icon={leadingIcon} />
{:else}
{@render leadingIcon()}
{/if}
</div>
{/if}
<div
class={cleanClass(
containerStyles({
shape,
roundedSize: shape === 'semi-round' ? size : undefined,
invalid,
}),
className,
)}
>
{#if leadingIcon}
<div tabindex="-1" class={iconStyles({ size })}>
{#if isIconLike(leadingIcon)}
<Icon size="60%" icon={leadingIcon} />
{:else}
{@render leadingIcon()}
{/if}
</div>
{/if}
<input
id={inputId}
aria-labelledby={label && labelId}
{required}
aria-required={required}
{disabled}
aria-disabled={disabled}
aria-describedby={descriptionId}
readonly={readOnly}
size={inputSize}
aria-readonly={readOnly}
class={inputStyles({
textSize: size,
leadingPadding: leadingIcon ? 'icon' : 'base',
trailingPadding: trailingIcon || trailingText ? 'icon' : 'base',
})}
bind:this={ref}
bind:value
{...restProps}
/>
{#if trailingText}
<Text
{size}
color="muted"
class={trailingTextStyles({ padding: trailingIcon ? 'icon' : 'base' })}>{trailingText}</Text
>
{/if}
<input
id={inputId}
aria-labelledby={label && labelId}
{required}
aria-required={required}
{disabled}
aria-disabled={disabled}
aria-describedby={descriptionId}
readonly={readOnly}
size={inputSize}
aria-readonly={readOnly}
class={inputStyles({
textSize: size,
leadingPadding: leadingIcon ? 'icon' : 'base',
trailingPadding: trailingIcon || trailingText ? 'icon' : 'base',
})}
bind:this={ref}
bind:value
{...restProps}
/>
{#if trailingText}
<Text {size} color="muted" class={trailingTextStyles({ padding: trailingIcon ? 'icon' : 'base' })}
>{trailingText}</Text
>
{/if}
{#if trailingIcon}
<div tabindex="-1" class={iconStyles({ size })}>
{#if isIconLike(trailingIcon)}
<Icon size="60%" icon={trailingIcon} />
{:else}
{@render trailingIcon()}
{/if}
</div>
{/if}
</div>
{#if trailingIcon}
<div tabindex="-1" class={iconStyles({ size })}>
{#if isIconLike(trailingIcon)}
<Icon size="60%" icon={trailingIcon} />
{:else}
{@render trailingIcon()}
{/if}
</div>
{/if}
</div>
</div>
<style>
input::-ms-reveal {
display: none;
}
input::-ms-reveal {
display: none;
}
</style>
+25 -25
View File
@@ -1,33 +1,33 @@
<script lang="ts">
import type { Color, Size } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import { tv } from 'tailwind-variants';
import type { Color, Size } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import { tv } from 'tailwind-variants';
type Props = {
size?: Size;
color?: Color;
class?: string;
children?: Snippet;
} & HTMLAttributes<HTMLElement>;
type Props = {
size?: Size;
color?: Color;
class?: string;
children?: Snippet;
} & HTMLAttributes<HTMLElement>;
const { class: className, size = 'small', children, ...restProps }: Props = $props();
const { class: className, size = 'small', children, ...restProps }: Props = $props();
const styles = tv({
base: 'bg-subtle text-dark flex flex-col rounded-md border border-b-2 px-1 font-mono shadow',
variants: {
size: {
tiny: 'text-xs',
small: 'text-sm',
medium: 'text-base',
large: 'text-lg',
giant: 'text-xl',
},
},
});
const styles = tv({
base: 'bg-subtle text-dark flex flex-col rounded-md border border-b-2 px-1 font-mono shadow',
variants: {
size: {
tiny: 'text-xs',
small: 'text-sm',
medium: 'text-base',
large: 'text-lg',
giant: 'text-xl',
},
},
});
</script>
<kbd class={cleanClass(styles({ size }), className)} {...restProps}>
{@render children?.()}
{@render children?.()}
</kbd>
+6 -6
View File
@@ -1,12 +1,12 @@
<script lang="ts">
import { styles } from '$lib/styles.js';
import type { LabelProps } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import { styles } from '$lib/styles.js';
import type { LabelProps } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
const { label, size, color, class: className, children, ...restProps }: LabelProps = $props();
const { label, size, color, class: className, children, ...restProps }: LabelProps = $props();
</script>
<label class={cleanClass(styles.label({ size, color }), className)} {...restProps}>
{#if label}{label}{/if}
{@render children?.()}
{#if label}{label}{/if}
{@render children?.()}
</label>
+18 -25
View File
@@ -1,33 +1,26 @@
<script lang="ts">
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
import type { HTMLAnchorAttributes } from 'svelte/elements';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
import type { HTMLAnchorAttributes } from 'svelte/elements';
type Props = {
class?: string;
children: Snippet;
href: string;
underline?: boolean;
external?: boolean;
} & HTMLAnchorAttributes;
type Props = {
class?: string;
children: Snippet;
href: string;
underline?: boolean;
external?: boolean;
} & HTMLAnchorAttributes;
const {
href,
class: className,
underline = true,
external,
children,
...restProps
}: Props = $props();
const { href, class: className, underline = true, external, children, ...restProps }: Props = $props();
</script>
<a
{href}
draggable="false"
class={cleanClass(underline && 'underline', className)}
target={external ? '_blank' : undefined}
rel={external ? 'noopener noreferrer' : undefined}
{...restProps}
{href}
draggable="false"
class={cleanClass(underline && 'underline', className)}
target={external ? '_blank' : undefined}
rel={external ? 'noopener noreferrer' : undefined}
{...restProps}
>
{@render children()}
{@render children()}
</a>
@@ -1,54 +1,54 @@
<script lang="ts">
import type { Color, Size } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import { tv } from 'tailwind-variants';
import type { Color, Size } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import { tv } from 'tailwind-variants';
interface Props {
size?: Size;
color?: Color;
class?: string;
}
interface Props {
size?: Size;
color?: Color;
class?: string;
}
let { size = 'medium', color = 'primary', class: className }: Props = $props();
let { size = 'medium', color = 'primary', class: className }: Props = $props();
const styles = tv({
base: 'fill-primary animate-spin text-gray-400 dark:text-gray-600',
variants: {
size: {
tiny: 'h-3',
small: 'h-4',
medium: 'h-5',
large: 'h-6',
giant: 'h-12',
},
color: {
primary: 'fill-primary',
secondary: 'fill-dark',
success: 'fill-success',
danger: 'fill-danger',
warning: 'fill-warning',
info: 'fill-info',
},
},
});
const styles = tv({
base: 'fill-primary animate-spin text-gray-400 dark:text-gray-600',
variants: {
size: {
tiny: 'h-3',
small: 'h-4',
medium: 'h-5',
large: 'h-6',
giant: 'h-12',
},
color: {
primary: 'fill-primary',
secondary: 'fill-dark',
success: 'fill-success',
danger: 'fill-danger',
warning: 'fill-warning',
info: 'fill-info',
},
},
});
</script>
<div>
<svg
role="status"
class={cleanClass(styles({ color, size }), className)}
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
data-testid="loading-spinner"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
<svg
role="status"
class={cleanClass(styles({ color, size }), className)}
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
data-testid="loading-spinner"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
+53 -53
View File
@@ -1,66 +1,66 @@
<script lang="ts">
import futoDark from '$lib/assets/immich-logo-futo-dark.svg';
import futoLight from '$lib/assets/immich-logo-futo-light.svg';
import inlineDark from '$lib/assets/immich-logo-inline-dark.svg';
import inlineLight from '$lib/assets/immich-logo-inline-light.svg';
import stackedDark from '$lib/assets/immich-logo-stacked-dark.svg';
import stackedLight from '$lib/assets/immich-logo-stacked-light.svg';
import icon from '$lib/assets/immich-logo.svg';
import { theme } from '$lib/services/theme.svelte.js';
import { Theme, type Size } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import { tv } from 'tailwind-variants';
import futoDark from '$lib/assets/immich-logo-futo-dark.svg';
import futoLight from '$lib/assets/immich-logo-futo-light.svg';
import inlineDark from '$lib/assets/immich-logo-inline-dark.svg';
import inlineLight from '$lib/assets/immich-logo-inline-light.svg';
import stackedDark from '$lib/assets/immich-logo-stacked-dark.svg';
import stackedLight from '$lib/assets/immich-logo-stacked-light.svg';
import icon from '$lib/assets/immich-logo.svg';
import { theme } from '$lib/services/theme.svelte.js';
import { Theme, type Size } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import { tv } from 'tailwind-variants';
type Props = {
size?: Size | 'landing';
variant?: 'stacked' | 'inline' | 'logo' | 'icon' | 'stacked-futo';
class?: string;
};
type Props = {
size?: Size | 'landing';
variant?: 'stacked' | 'inline' | 'logo' | 'icon' | 'stacked-futo';
class?: string;
};
const { variant = 'logo', size = 'medium', class: className }: Props = $props();
const { variant = 'logo', size = 'medium', class: className }: Props = $props();
const getUrl = (variant: Props['variant']): string => {
switch (variant) {
case 'stacked': {
return theme.value === Theme.Light ? stackedLight : stackedDark;
}
const getUrl = (variant: Props['variant']): string => {
switch (variant) {
case 'stacked': {
return theme.value === Theme.Light ? stackedLight : stackedDark;
}
case 'inline': {
return theme.value === Theme.Light ? inlineLight : inlineDark;
}
case 'inline': {
return theme.value === Theme.Light ? inlineLight : inlineDark;
}
case 'stacked-futo': {
return theme.value === Theme.Light ? futoLight : futoDark;
}
case 'stacked-futo': {
return theme.value === Theme.Light ? futoLight : futoDark;
}
default: {
return icon;
}
}
};
default: {
return icon;
}
}
};
const styles = tv({
variants: {
size: {
tiny: 'h-8',
small: 'h-10',
medium: 'h-12',
large: 'h-16',
giant: 'h-24',
landing: 'h-64',
},
const styles = tv({
variants: {
size: {
tiny: 'h-8',
small: 'h-10',
medium: 'h-12',
large: 'h-16',
giant: 'h-24',
landing: 'h-64',
},
variant: {
stacked: '',
inline: '',
'stacked-futo': '',
logo: 'bg-light aspect-square rounded-full shadow-lg',
icon: 'aspect-square',
},
},
});
variant: {
stacked: '',
inline: '',
'stacked-futo': '',
logo: 'bg-light aspect-square rounded-full shadow-lg',
icon: 'aspect-square',
},
},
});
const src = $derived(getUrl(variant));
const src = $derived(getUrl(variant));
</script>
<img {src} class={cleanClass(styles({ size, variant }), className)} alt="Immich logo" />
+110 -114
View File
@@ -1,131 +1,127 @@
<script lang="ts">
import { withChildrenSnippets } from '$lib/common/use-child.svelte.js';
import Card from '$lib/components/Card/Card.svelte';
import CardBody from '$lib/components/Card/CardBody.svelte';
import CardFooter from '$lib/components/Card/CardFooter.svelte';
import CardHeader from '$lib/components/Card/CardHeader.svelte';
import CardTitle from '$lib/components/Card/CardTitle.svelte';
import CloseButton from '$lib/components/CloseButton/CloseButton.svelte';
import Icon from '$lib/components/Icon/Icon.svelte';
import Logo from '$lib/components/Logo/Logo.svelte';
import { ChildKey } from '$lib/constants.js';
import type { ModalSize } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import { Dialog } from 'bits-ui';
import { tick, type Snippet } from 'svelte';
import { tv } from 'tailwind-variants';
import { withChildrenSnippets } from '$lib/common/use-child.svelte.js';
import Card from '$lib/components/Card/Card.svelte';
import CardBody from '$lib/components/Card/CardBody.svelte';
import CardFooter from '$lib/components/Card/CardFooter.svelte';
import CardHeader from '$lib/components/Card/CardHeader.svelte';
import CardTitle from '$lib/components/Card/CardTitle.svelte';
import CloseButton from '$lib/components/CloseButton/CloseButton.svelte';
import Icon from '$lib/components/Icon/Icon.svelte';
import Logo from '$lib/components/Logo/Logo.svelte';
import { ChildKey } from '$lib/constants.js';
import type { ModalSize } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import { Dialog } from 'bits-ui';
import { tick, type Snippet } from 'svelte';
import { tv } from 'tailwind-variants';
type Props = {
title?: string;
icon?: string | boolean;
size?: ModalSize;
class?: string;
expandable?: boolean;
closeOnEsc?: boolean;
closeOnBackdropClick?: boolean;
children: Snippet;
onClose?: () => void;
};
type Props = {
title?: string;
icon?: string | boolean;
size?: ModalSize;
class?: string;
expandable?: boolean;
closeOnEsc?: boolean;
closeOnBackdropClick?: boolean;
children: Snippet;
onClose?: () => void;
};
let {
size = 'medium',
onClose,
icon = true,
title,
class: className,
closeOnEsc = true,
closeOnBackdropClick = false,
children,
}: Props = $props();
let {
size = 'medium',
onClose,
icon = true,
title,
class: className,
closeOnEsc = true,
closeOnBackdropClick = false,
children,
}: Props = $props();
const modalStyles = tv({
base: 'bg-light dark:bg-subtle border-subtle shadow-primary/20 flex rounded-none border shadow-sm sm:rounded-2xl dark:border-white/10',
variants: {
size: {
tiny: 'h-full sm:h-min md:max-w-sm',
small: 'h-full sm:h-min md:max-w-md',
medium: 'h-full sm:h-min md:max-w-(--breakpoint-sm)',
large: 'h-full sm:h-min md:max-w-(--breakpoint-md)',
giant: 'h-full sm:h-min md:max-w-(--breakpoint-lg)',
full: 'h-full w-full',
},
},
});
const modalStyles = tv({
base: 'bg-light dark:bg-subtle border-subtle shadow-primary/20 flex rounded-none border shadow-sm sm:rounded-2xl dark:border-white/10',
variants: {
size: {
tiny: 'h-full sm:h-min md:max-w-sm',
small: 'h-full sm:h-min md:max-w-md',
medium: 'h-full sm:h-min md:max-w-(--breakpoint-sm)',
large: 'h-full sm:h-min md:max-w-(--breakpoint-md)',
giant: 'h-full sm:h-min md:max-w-(--breakpoint-lg)',
full: 'h-full w-full',
},
},
});
const { getChildren: getChildSnippet } = withChildrenSnippets(ChildKey.Modal);
const headerChildren = $derived(getChildSnippet(ChildKey.ModalHeader));
const bodyChildren = $derived(getChildSnippet(ChildKey.ModalBody));
const footerChildren = $derived(getChildSnippet(ChildKey.ModalFooter));
const { getChildren: getChildSnippet } = withChildrenSnippets(ChildKey.Modal);
const headerChildren = $derived(getChildSnippet(ChildKey.ModalHeader));
const bodyChildren = $derived(getChildSnippet(ChildKey.ModalBody));
const footerChildren = $derived(getChildSnippet(ChildKey.ModalFooter));
const handleClose = async () => {
// wait for bits-ui to complete its event cycle
await tick();
await new Promise((resolve) => setTimeout(resolve, 10));
const handleClose = async () => {
// wait for bits-ui to complete its event cycle
await tick();
await new Promise((resolve) => setTimeout(resolve, 10));
onClose?.();
};
onClose?.();
};
let cardRef = $state<HTMLElement | null>(null);
let cardRef = $state<HTMLElement | null>(null);
const handleCloseOnClick = (event: Event) => {
if (!closeOnBackdropClick || cardRef?.contains(event.target as Node)) {
return;
}
const handleCloseOnClick = (event: Event) => {
if (!closeOnBackdropClick || cardRef?.contains(event.target as Node)) {
return;
}
onClose?.();
};
onClose?.();
};
</script>
<Dialog.Root open={true}>
<Dialog.Portal>
<Dialog.Overlay class="fixed start-0 top-0 flex h-dvh w-screen bg-black/30" />
<Dialog.Content
onkeydown={(e) => {
if (e.key === 'Escape') {
e.stopPropagation();
e.preventDefault();
if (closeOnEsc) {
handleClose();
}
}
}}
onclick={handleCloseOnClick}
class={cleanClass(
'fixed start-0 top-0 flex h-dvh w-screen items-center justify-center overflow-hidden sm:p-4',
)}
>
<div class={cleanClass('flex h-full w-full flex-col items-center justify-center')}>
<Card bind:ref={cardRef} class={cleanClass(modalStyles({ size }), className)}>
<CardHeader class="border-b border-gray-200 px-5 py-3 dark:border-white/10">
{#if headerChildren}
{@render headerChildren.snippet()}
{:else if title}
<div class="flex items-center justify-between gap-2">
{#if typeof icon === 'string'}
<Icon {icon} size="1.5rem" aria-hidden />
{:else if icon}
<Logo variant="icon" size="tiny" />
{/if}
<CardTitle tag="p" class="text-dark/90 grow text-lg font-semibold"
>{title}</CardTitle
>
<CloseButton class="-me-2" onclick={() => handleClose()} />
</div>
{/if}
</CardHeader>
<Dialog.Portal>
<Dialog.Overlay class="fixed start-0 top-0 flex h-dvh w-screen bg-black/30" />
<Dialog.Content
onkeydown={(e) => {
if (e.key === 'Escape') {
e.stopPropagation();
e.preventDefault();
if (closeOnEsc) {
handleClose();
}
}
}}
onclick={handleCloseOnClick}
class={cleanClass('fixed start-0 top-0 flex h-dvh w-screen items-center justify-center overflow-hidden sm:p-4')}
>
<div class={cleanClass('flex h-full w-full flex-col items-center justify-center')}>
<Card bind:ref={cardRef} class={cleanClass(modalStyles({ size }), className)}>
<CardHeader class="border-b border-gray-200 px-5 py-3 dark:border-white/10">
{#if headerChildren}
{@render headerChildren.snippet()}
{:else if title}
<div class="flex items-center justify-between gap-2">
{#if typeof icon === 'string'}
<Icon {icon} size="1.5rem" aria-hidden />
{:else if icon}
<Logo variant="icon" size="tiny" />
{/if}
<CardTitle tag="p" class="text-dark/90 grow text-lg font-semibold">{title}</CardTitle>
<CloseButton class="-me-2" onclick={() => handleClose()} />
</div>
{/if}
</CardHeader>
<CardBody class="grow px-5">
{@render bodyChildren?.snippet()}
</CardBody>
<CardBody class="grow px-5">
{@render bodyChildren?.snippet()}
</CardBody>
{#if footerChildren}
<CardFooter class="border-t border-gray-200 dark:border-white/10">
{@render footerChildren.snippet()}
</CardFooter>
{/if}
</Card>
</div>
</Dialog.Content>
</Dialog.Portal>
{#if footerChildren}
<CardFooter class="border-t border-gray-200 dark:border-white/10">
{@render footerChildren.snippet()}
</CardFooter>
{/if}
</Card>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
{@render children?.()}
+8 -8
View File
@@ -1,15 +1,15 @@
<script lang="ts">
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import type { Snippet } from 'svelte';
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import type { Snippet } from 'svelte';
type Props = {
children: Snippet;
};
type Props = {
children: Snippet;
};
let { children }: Props = $props();
let { children }: Props = $props();
</script>
<Child for={ChildKey.Modal} as={ChildKey.ModalBody}>
{@render children?.()}
{@render children?.()}
</Child>
+8 -8
View File
@@ -1,15 +1,15 @@
<script lang="ts">
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import type { Snippet } from 'svelte';
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import type { Snippet } from 'svelte';
type Props = {
children: Snippet;
};
type Props = {
children: Snippet;
};
let { children }: Props = $props();
let { children }: Props = $props();
</script>
<Child for={ChildKey.Modal} as={ChildKey.ModalFooter}>
{@render children?.()}
{@render children?.()}
</Child>
+8 -8
View File
@@ -1,15 +1,15 @@
<script lang="ts">
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import type { Snippet } from 'svelte';
import { ChildKey } from '$lib/constants.js';
import Child from '$lib/internal/Child.svelte';
import type { Snippet } from 'svelte';
type Props = {
children: Snippet;
};
type Props = {
children: Snippet;
};
let { children }: Props = $props();
let { children }: Props = $props();
</script>
<Child for={ChildKey.Modal} as={ChildKey.ModalHeader}>
{@render children?.()}
{@render children?.()}
</Child>
@@ -1,14 +1,14 @@
<script lang="ts">
import InternalSelect from '$lib/internal/Select.svelte';
import type { MultiSelectProps, SelectItem } from '$lib/types.js';
import InternalSelect from '$lib/internal/Select.svelte';
import type { MultiSelectProps, SelectItem } from '$lib/types.js';
type T = SelectItem;
type T = SelectItem;
let { onChange, values = $bindable([]), ...restProps }: MultiSelectProps<T> = $props();
let { onChange, values = $bindable([]), ...restProps }: MultiSelectProps<T> = $props();
const handleChange = (items: T[]) => {
onChange?.(items);
};
const handleChange = (items: T[]) => {
onChange?.(items);
};
</script>
<InternalSelect multiple bind:values onChange={handleChange} {...restProps} />
+5 -5
View File
@@ -1,11 +1,11 @@
<script lang="ts">
type Props = {
title: string;
};
type Props = {
title: string;
};
let { title }: Props = $props();
let { title }: Props = $props();
</script>
<div class="text-sm transition-all duration-200 md:text-sm">
<p class="py-2 ps-4">{title}</p>
<p class="py-2 ps-4">{title}</p>
</div>
+59 -61
View File
@@ -1,73 +1,71 @@
<script lang="ts">
import { page } from '$app/state';
import Icon from '$lib/components/Icon/Icon.svelte';
import Link from '$lib/components/Link/Link.svelte';
import type { IconProps } from '$lib/types.js';
import { tv } from 'tailwind-variants';
import { page } from '$app/state';
import Icon from '$lib/components/Icon/Icon.svelte';
import Link from '$lib/components/Link/Link.svelte';
import type { IconProps } from '$lib/types.js';
import { tv } from 'tailwind-variants';
type Props = {
title: string;
href: string;
external?: boolean;
active?: boolean;
variant?: 'compact';
isActive?: () => boolean;
icon?: string | IconProps;
activeIcon?: string | IconProps;
};
type Props = {
title: string;
href: string;
external?: boolean;
active?: boolean;
variant?: 'compact';
isActive?: () => boolean;
icon?: string | IconProps;
activeIcon?: string | IconProps;
};
const startsWithHref = () => page.url.pathname.startsWith(href);
const startsWithHref = () => page.url.pathname.startsWith(href);
let {
href,
external,
isActive: isActiveOverride,
title,
variant,
active: activeOverride,
icon,
activeIcon,
}: Props = $props();
let {
href,
external,
isActive: isActiveOverride,
title,
variant,
active: activeOverride,
icon,
activeIcon,
}: Props = $props();
const isActive = isActiveOverride ?? startsWithHref;
let active = $derived(activeOverride ?? isActive());
const isActive = isActiveOverride ?? startsWithHref;
let active = $derived(activeOverride ?? isActive());
const iconProps = $derived(typeof icon === 'string' ? { icon } : icon);
const activeIconProps = $derived(
typeof activeIcon === 'string' ? { icon: activeIcon } : activeIcon,
);
const iconProps = $derived(typeof icon === 'string' ? { icon } : icon);
const activeIconProps = $derived(typeof activeIcon === 'string' ? { icon: activeIcon } : activeIcon);
const styles = tv({
base: 'hover:bg-subtle hover:text-primary flex w-full place-items-center gap-4 rounded-e-full px-5 transition-[padding] delay-100 duration-100 group-hover:sm:px-5',
variants: {
active: {
true: 'bg-primary/10 text-primary',
false: '',
},
variant: {
default: 'py-3 ps-5',
compact: 'py-2 ps-3',
},
},
});
const styles = tv({
base: 'hover:bg-subtle hover:text-primary flex w-full place-items-center gap-4 rounded-e-full px-5 transition-[padding] delay-100 duration-100 group-hover:sm:px-5',
variants: {
active: {
true: 'bg-primary/10 text-primary',
false: '',
},
variant: {
default: 'py-3 ps-5',
compact: 'py-2 ps-3',
},
},
});
</script>
<Link
{href}
{external}
aria-current={active ? 'page' : undefined}
underline={false}
class={styles({ active, variant: variant ?? 'default' })}
{href}
{external}
aria-current={active ? 'page' : undefined}
underline={false}
class={styles({ active, variant: variant ?? 'default' })}
>
<div class="flex w-full place-items-center gap-4 truncate overflow-hidden">
{#if iconProps}
<Icon
size="1.375em"
class="shrink-0"
aria-hidden={true}
{...active && activeIconProps ? activeIconProps : iconProps}
/>
{/if}
<span class="text-sm font-medium">{title}</span>
</div>
<div class="flex w-full place-items-center gap-4 truncate overflow-hidden">
{#if iconProps}
<Icon
size="1.375em"
class="shrink-0"
aria-hidden={true}
{...active && activeIconProps ? activeIconProps : iconProps}
/>
{/if}
<span class="text-sm font-medium">{title}</span>
</div>
</Link>
@@ -1,38 +1,36 @@
<script lang="ts">
import IconButton from '$lib/components/IconButton/IconButton.svelte';
import Input from '$lib/components/Input/Input.svelte';
import { t } from '$lib/services/translation.svelte.js';
import type { PasswordInputProps } from '$lib/types.js';
import { mdiEyeOffOutline, mdiEyeOutline } from '@mdi/js';
import IconButton from '$lib/components/IconButton/IconButton.svelte';
import Input from '$lib/components/Input/Input.svelte';
import { t } from '$lib/services/translation.svelte.js';
import type { PasswordInputProps } from '$lib/types.js';
import { mdiEyeOffOutline, mdiEyeOutline } from '@mdi/js';
let {
value = $bindable<string>(),
translations,
isVisible = $bindable<boolean>(false),
color = 'secondary',
size,
...props
}: PasswordInputProps = $props();
let {
value = $bindable<string>(),
translations,
isVisible = $bindable<boolean>(false),
color = 'secondary',
size,
...props
}: PasswordInputProps = $props();
let labelValue = $derived(
isVisible ? t('hide_password', translations) : t('show_password', translations),
);
let labelValue = $derived(isVisible ? t('hide_password', translations) : t('show_password', translations));
</script>
<Input bind:value {size} type={isVisible ? 'text' : 'password'} {color} {...props}>
{#snippet trailingIcon()}
{#if value?.length > 0}
<IconButton
variant="ghost"
shape="round"
color="secondary"
{size}
class="me-1"
icon={isVisible ? mdiEyeOffOutline : mdiEyeOutline}
onclick={() => (isVisible = !isVisible)}
title={labelValue}
aria-label={labelValue}
/>
{/if}
{/snippet}
{#snippet trailingIcon()}
{#if value?.length > 0}
<IconButton
variant="ghost"
shape="round"
color="secondary"
{size}
class="me-1"
icon={isVisible ? mdiEyeOffOutline : mdiEyeOutline}
onclick={() => (isVisible = !isVisible)}
title={labelValue}
aria-label={labelValue}
/>
{/if}
{/snippet}
</Input>
+49 -49
View File
@@ -1,67 +1,67 @@
<script lang="ts">
import { afterNavigate } from '$app/navigation';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
import { afterNavigate } from '$app/navigation';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
type Props = {
class?: string;
children?: Snippet;
transition?: TransitionEvent;
ref?: HTMLDivElement;
resetOnNavigate?: boolean;
};
type Props = {
class?: string;
children?: Snippet;
transition?: TransitionEvent;
ref?: HTMLDivElement;
resetOnNavigate?: boolean;
};
let { resetOnNavigate = false, class: className, children, ref = $bindable() }: Props = $props();
let { resetOnNavigate = false, class: className, children, ref = $bindable() }: Props = $props();
afterNavigate(() => {
if (resetOnNavigate) {
ref?.scrollTo(0, 0);
}
});
afterNavigate(() => {
if (resetOnNavigate) {
ref?.scrollTo(0, 0);
}
});
</script>
<div bind:this={ref} class={cleanClass('immich-scrollbar h-full w-full overflow-auto', className)}>
{@render children?.()}
{@render children?.()}
</div>
<style>
/* width */
.immich-scrollbar::-webkit-scrollbar {
width: 8px;
height: 8px;
visibility: hidden;
}
/* Track */
.immich-scrollbar::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 16px;
visibility: hidden;
}
/* width */
.immich-scrollbar::-webkit-scrollbar {
width: 8px;
height: 8px;
visibility: hidden;
}
/* Track */
.immich-scrollbar::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 16px;
visibility: hidden;
}
/* Handle */
.immich-scrollbar::-webkit-scrollbar-thumb {
background: rgba(85, 86, 87, 0.408);
border-radius: 16px;
visibility: hidden;
}
/* Handle */
.immich-scrollbar::-webkit-scrollbar-thumb {
background: rgba(85, 86, 87, 0.408);
border-radius: 16px;
visibility: hidden;
}
/* Handle on hover */
.immich-scrollbar::-webkit-scrollbar-thumb:hover {
background: #4250afad;
border-radius: 16px;
}
/* Handle on hover */
.immich-scrollbar::-webkit-scrollbar-thumb:hover {
background: #4250afad;
border-radius: 16px;
}
/*
/*
* Show scrollbar elements when hovering or actively scrolling
* Applies to the main scrollbar, track, and thumb components
* Changes visibility from hidden to visible on user interaction
*/
.immich-scrollbar:hover::-webkit-scrollbar,
.immich-scrollbar:active::-webkit-scrollbar,
.immich-scrollbar:hover::-webkit-scrollbar-track,
.immich-scrollbar:active::-webkit-scrollbar-track,
.immich-scrollbar:hover::-webkit-scrollbar-thumb,
.immich-scrollbar:active::-webkit-scrollbar-thumb {
visibility: visible;
}
.immich-scrollbar:hover::-webkit-scrollbar,
.immich-scrollbar:active::-webkit-scrollbar,
.immich-scrollbar:hover::-webkit-scrollbar-track,
.immich-scrollbar:active::-webkit-scrollbar-track,
.immich-scrollbar:hover::-webkit-scrollbar-thumb,
.immich-scrollbar:active::-webkit-scrollbar-thumb {
visibility: visible;
}
</style>
+9 -9
View File
@@ -1,17 +1,17 @@
<script lang="ts">
import InternalSelect from '$lib/internal/Select.svelte';
import type { SelectItem, SelectProps } from '$lib/types.js';
import InternalSelect from '$lib/internal/Select.svelte';
import type { SelectItem, SelectProps } from '$lib/types.js';
type T = SelectItem;
type T = SelectItem;
let { onChange, value = $bindable(), ...restProps }: SelectProps<T> = $props();
let { onChange, value = $bindable(), ...restProps }: SelectProps<T> = $props();
let values = $derived(value ? [value] : []);
let values = $derived(value ? [value] : []);
const handleChange = (items: T[]) => {
value = items[0];
onChange?.(value);
};
const handleChange = (items: T[]) => {
value = items[0];
onChange?.(value);
};
</script>
<InternalSelect bind:values onChange={handleChange} {...restProps} />
+4 -4
View File
@@ -1,10 +1,10 @@
<script lang="ts">
import Stack from '$lib/components/Stack/Stack.svelte';
import type { HStackProps } from '$lib/types.js';
import Stack from '$lib/components/Stack/Stack.svelte';
import type { HStackProps } from '$lib/types.js';
const { class: className, children, ...props }: HStackProps = $props();
const { class: className, children, ...props }: HStackProps = $props();
</script>
<Stack direction="row" align="center" class={className} {...props}>
{@render children()}
{@render children()}
</Stack>
+62 -62
View File
@@ -1,70 +1,70 @@
<script lang="ts">
import type { StackProps } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import { tv } from 'tailwind-variants';
import type { StackProps } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import { tv } from 'tailwind-variants';
const {
align,
direction = 'column',
wrap = false,
class: className,
fullWidth,
fullHeight,
gap = 2,
children,
}: StackProps = $props();
const {
align,
direction = 'column',
wrap = false,
class: className,
fullWidth,
fullHeight,
gap = 2,
children,
}: StackProps = $props();
const styles = tv({
base: 'flex gap-2',
variants: {
direction: {
row: 'flex-row',
column: 'flex-col',
},
align: {
start: 'items-start',
center: 'items-center',
end: 'items-end',
},
fullWidth: {
true: 'w-full',
false: '',
},
fullHeight: {
true: 'h-full',
false: '',
},
gap: {
0: 'gap-0',
1: 'gap-1',
2: 'gap-2',
3: 'gap-3',
4: 'gap-4',
5: 'gap-5',
6: 'gap-6',
7: 'gap-7',
8: 'gap-8',
},
wrap: {
true: 'flex-wrap',
false: '',
},
},
});
const styles = tv({
base: 'flex gap-2',
variants: {
direction: {
row: 'flex-row',
column: 'flex-col',
},
align: {
start: 'items-start',
center: 'items-center',
end: 'items-end',
},
fullWidth: {
true: 'w-full',
false: '',
},
fullHeight: {
true: 'h-full',
false: '',
},
gap: {
0: 'gap-0',
1: 'gap-1',
2: 'gap-2',
3: 'gap-3',
4: 'gap-4',
5: 'gap-5',
6: 'gap-6',
7: 'gap-7',
8: 'gap-8',
},
wrap: {
true: 'flex-wrap',
false: '',
},
},
});
</script>
<div
class={cleanClass(
styles({
align,
direction,
gap,
wrap,
fullHeight,
fullWidth,
}),
className,
)}
class={cleanClass(
styles({
align,
direction,
gap,
wrap,
fullHeight,
fullWidth,
}),
className,
)}
>
{@render children()}
{@render children()}
</div>
+4 -4
View File
@@ -1,10 +1,10 @@
<script lang="ts">
import Stack from '$lib/components/Stack/Stack.svelte';
import type { VStackProps } from '$lib/types.js';
import Stack from '$lib/components/Stack/Stack.svelte';
import type { VStackProps } from '$lib/types.js';
const { class: className, children, ...props }: VStackProps = $props();
const { class: className, children, ...props }: VStackProps = $props();
</script>
<Stack direction="column" align="center" class={className} {...props}>
{@render children()}
{@render children()}
</Stack>
@@ -1,94 +1,94 @@
<script lang="ts">
import Logo from '$lib/components/Logo/Logo.svelte';
import Text from '$lib/components/Text/Text.svelte';
import type { Size } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
import { tv } from 'tailwind-variants';
import Logo from '$lib/components/Logo/Logo.svelte';
import Text from '$lib/components/Text/Text.svelte';
import type { Size } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
import { tv } from 'tailwind-variants';
type Props = {
effect?: 'hover' | 'always';
text?: string;
size?: Size;
children?: Snippet;
};
type Props = {
effect?: 'hover' | 'always';
text?: string;
size?: Size;
children?: Snippet;
};
const { effect = 'hover', text = 'Supporter', size = 'medium', children }: Props = $props();
const { effect = 'hover', text = 'Supporter', size = 'medium', children }: Props = $props();
const iconSize: Record<Size, Size> = {
tiny: 'tiny',
small: 'tiny',
medium: 'small',
large: 'medium',
giant: 'medium',
};
const iconSize: Record<Size, Size> = {
tiny: 'tiny',
small: 'tiny',
medium: 'small',
large: 'medium',
giant: 'medium',
};
const containerStyles = tv({
base: 'bg-secondary flex place-items-center gap-2 overflow-hidden rounded-lg transition-all',
variants: {
size: {
tiny: 'px-2 py-1',
small: 'px-2 py-1',
medium: 'px-3 py-2',
large: 'px-3 py-2',
giant: 'px-3 py-2',
},
effect: {
hover: 'supporter-effect-hover border-dark/25 border',
always: 'supporter-effect shadow',
},
},
});
const containerStyles = tv({
base: 'bg-secondary flex place-items-center gap-2 overflow-hidden rounded-lg transition-all',
variants: {
size: {
tiny: 'px-2 py-1',
small: 'px-2 py-1',
medium: 'px-3 py-2',
large: 'px-3 py-2',
giant: 'px-3 py-2',
},
effect: {
hover: 'supporter-effect-hover border-dark/25 border',
always: 'supporter-effect shadow',
},
},
});
</script>
<div class={cleanClass(containerStyles({ effect, size }))}>
{#if children}
{@render children()}
{:else}
<Logo size={iconSize[size]} variant="icon" />
<Text fontWeight="normal" color="secondary" {size}>{text}</Text>
{/if}
{#if children}
{@render children()}
{:else}
<Logo size={iconSize[size]} variant="icon" />
<Text fontWeight="normal" color="secondary" {size}>{text}</Text>
{/if}
</div>
<style>
.supporter-effect,
.supporter-effect-hover:hover {
position: relative;
background-clip: padding-box;
animation: gradient 10s ease infinite;
z-index: 1;
}
.supporter-effect,
.supporter-effect-hover:hover {
position: relative;
background-clip: padding-box;
animation: gradient 10s ease infinite;
z-index: 1;
}
.supporter-effect:after,
.supporter-effect-hover:hover:after {
position: absolute;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
background: linear-gradient(
to right,
rgba(16, 132, 254, 0.15),
rgba(229, 125, 175, 0.15),
rgba(254, 36, 29, 0.15),
rgba(255, 183, 0, 0.15),
rgba(22, 193, 68, 0.15)
);
content: '';
animation: gradient 10s ease infinite;
background-size: 400% 400%;
z-index: -1;
}
.supporter-effect:after,
.supporter-effect-hover:hover:after {
position: absolute;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
background: linear-gradient(
to right,
rgba(16, 132, 254, 0.15),
rgba(229, 125, 175, 0.15),
rgba(254, 36, 29, 0.15),
rgba(255, 183, 0, 0.15),
rgba(22, 193, 68, 0.15)
);
content: '';
animation: gradient 10s ease infinite;
background-size: 400% 400%;
z-index: -1;
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
</style>
+95 -96
View File
@@ -1,110 +1,109 @@
<script lang="ts">
import { getFieldContext } from '$lib/common/context.svelte.js';
import Label from '$lib/components/Label/Label.svelte';
import Text from '$lib/components/Text/Text.svelte';
import type { Color } from '$lib/types.js';
import { cleanClass, generateId } from '$lib/utils.js';
import { Switch, type WithoutChildrenOrChild } from 'bits-ui';
import { tv } from 'tailwind-variants';
import { getFieldContext } from '$lib/common/context.svelte.js';
import Label from '$lib/components/Label/Label.svelte';
import Text from '$lib/components/Text/Text.svelte';
import type { Color } from '$lib/types.js';
import { cleanClass, generateId } from '$lib/utils.js';
import { Switch, type WithoutChildrenOrChild } from 'bits-ui';
import { tv } from 'tailwind-variants';
type Props = {
checked?: boolean;
color?: Color;
disabled?: boolean;
class?: string;
} & WithoutChildrenOrChild<Switch.RootProps>;
type Props = {
checked?: boolean;
color?: Color;
disabled?: boolean;
class?: string;
} & WithoutChildrenOrChild<Switch.RootProps>;
let {
id = generateId(),
checked = $bindable(false),
ref = $bindable(null),
color = 'primary',
class: className,
...restProps
}: Props = $props();
let {
id = generateId(),
checked = $bindable(false),
ref = $bindable(null),
color = 'primary',
class: className,
...restProps
}: Props = $props();
const { readOnly, required, disabled, label, description, ...labelProps } =
$derived(getFieldContext());
const { readOnly, required, disabled, label, description, ...labelProps } = $derived(getFieldContext());
const enabled = $derived(checked && !disabled);
const enabled = $derived(checked && !disabled);
const wrapper = tv({
base: 'relative flex flex-col justify-center',
variants: {
disabled: {
true: 'cursor-not-allowed opacity-38',
false: 'cursor-pointer',
},
},
});
const wrapper = tv({
base: 'relative flex flex-col justify-center',
variants: {
disabled: {
true: 'cursor-not-allowed opacity-38',
false: 'cursor-pointer',
},
},
});
const bar = tv({
base: 'h-8 w-13 rounded-full border border-2',
variants: {
fillColor: {
default: 'border-gray-400 bg-gray-300 dark:border-gray-500 dark:bg-gray-400',
primary: 'bg-primary/50 dark:bg-primary border-transparent',
secondary: 'bg-dark/50 border-transparent',
success: 'bg-success/50 border-transparent',
danger: 'bg-danger/50 border-transparent',
warning: 'bg-warning/50 border-transparent',
info: 'bg-info/50 border-transparent',
},
},
});
const bar = tv({
base: 'h-8 w-13 rounded-full border border-2',
variants: {
fillColor: {
default: 'border-gray-400 bg-gray-300 dark:border-gray-500 dark:bg-gray-400',
primary: 'bg-primary/50 dark:bg-primary border-transparent',
secondary: 'bg-dark/50 border-transparent',
success: 'bg-success/50 border-transparent',
danger: 'bg-danger/50 border-transparent',
warning: 'bg-warning/50 border-transparent',
info: 'bg-info/50 border-transparent',
},
},
});
const dot = tv({
base: 'absolute h-4 w-4 origin-center rounded-full transition-transform duration-100',
variants: {
checked: {
true: 'translate-x-7 scale-150 rtl:-translate-x-7',
false: 'translate-x-2 rtl:-translate-x-2',
},
fillColor: {
default: 'bg-gray-400 dark:bg-gray-500',
primary: 'bg-[#4250af]',
secondary: 'bg-dark',
success: 'bg-success',
danger: 'bg-danger',
warning: 'bg-warning',
info: 'bg-info',
},
},
});
const dot = tv({
base: 'absolute h-4 w-4 origin-center rounded-full transition-transform duration-100',
variants: {
checked: {
true: 'translate-x-7 scale-150 rtl:-translate-x-7',
false: 'translate-x-2 rtl:-translate-x-2',
},
fillColor: {
default: 'bg-gray-400 dark:bg-gray-500',
primary: 'bg-[#4250af]',
secondary: 'bg-dark',
success: 'bg-success',
danger: 'bg-danger',
warning: 'bg-warning',
info: 'bg-info',
},
},
});
const inputId = `input-${id}`;
const labelId = `label-${id}`;
const descriptionId = $derived(description ? `description-${id}` : undefined);
const inputId = `input-${id}`;
const labelId = `label-${id}`;
const descriptionId = $derived(description ? `description-${id}` : undefined);
</script>
<Switch.Root
bind:checked
bind:ref
id={inputId}
disabled={disabled || readOnly}
{required}
class={cleanClass(label && 'w-full', className)}
aria-readonly={readOnly}
aria-labelledby={labelId}
aria-describedby={descriptionId}
{...restProps}
bind:checked
bind:ref
id={inputId}
disabled={disabled || readOnly}
{required}
class={cleanClass(label && 'w-full', className)}
aria-readonly={readOnly}
aria-labelledby={labelId}
aria-describedby={descriptionId}
{...restProps}
>
<Switch.Thumb>
{#snippet child()}
<div class={cleanClass(label && 'flex items-center justify-between gap-1')}>
{#if label}
<div class="text-start">
<Label id={labelId} for={inputId} {label} {...labelProps} />
{#if description}
<Text color="secondary" size="small" id={descriptionId}>{description}</Text>
{/if}
</div>
{/if}
<span class={wrapper({ disabled })}>
<span class={bar({ fillColor: enabled ? color : 'default' })}> </span>
<span class={dot({ checked: enabled, fillColor: enabled ? color : 'default' })}></span>
</span>
</div>
{/snippet}
</Switch.Thumb>
<Switch.Thumb>
{#snippet child()}
<div class={cleanClass(label && 'flex items-center justify-between gap-1')}>
{#if label}
<div class="text-start">
<Label id={labelId} for={inputId} {label} {...labelProps} />
{#if description}
<Text color="secondary" size="small" id={descriptionId}>{description}</Text>
{/if}
</div>
{/if}
<span class={wrapper({ disabled })}>
<span class={bar({ fillColor: enabled ? color : 'default' })}> </span>
<span class={dot({ checked: enabled, fillColor: enabled ? color : 'default' })}></span>
</span>
</div>
{/snippet}
</Switch.Thumb>
</Switch.Root>
+27 -34
View File
@@ -1,42 +1,35 @@
<script lang="ts">
import Text from '$lib/internal/Text.svelte';
import type { FontWeight, Size, TextColor, TextVariant } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import { tv } from 'tailwind-variants';
import Text from '$lib/internal/Text.svelte';
import type { FontWeight, Size, TextColor, TextVariant } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import { tv } from 'tailwind-variants';
type Props = {
size?: Size;
color?: TextColor;
fontWeight?: FontWeight;
variant?: TextVariant;
class?: string;
children: Snippet;
} & HTMLAttributes<HTMLElement>;
type Props = {
size?: Size;
color?: TextColor;
fontWeight?: FontWeight;
variant?: TextVariant;
class?: string;
children: Snippet;
} & HTMLAttributes<HTMLElement>;
const {
color,
size,
fontWeight = 'normal',
children,
class: className,
...restProps
}: Props = $props();
const { color, size, fontWeight = 'normal', children, class: className, ...restProps }: Props = $props();
const styles = tv({
variants: {
size: {
tiny: 'text-xs',
small: 'text-sm',
medium: 'text-base',
large: 'text-lg',
giant: 'text-xl',
},
},
});
const styles = tv({
variants: {
size: {
tiny: 'text-xs',
small: 'text-sm',
medium: 'text-base',
large: 'text-lg',
giant: 'text-xl',
},
},
});
</script>
<Text tag="p" {color} {fontWeight} class={cleanClass(styles({ size }), className)} {...restProps}>
{@render children()}
{@render children()}
</Text>
+103 -104
View File
@@ -1,120 +1,119 @@
<script lang="ts">
import { getFieldContext } from '$lib/common/context.svelte.js';
import Label from '$lib/components/Label/Label.svelte';
import Text from '$lib/components/Text/Text.svelte';
import type { TextareaProps } from '$lib/types.js';
import { cleanClass, generateId } from '$lib/utils.js';
import type { FormEventHandler } from 'svelte/elements';
import { tv } from 'tailwind-variants';
import { getFieldContext } from '$lib/common/context.svelte.js';
import Label from '$lib/components/Label/Label.svelte';
import Text from '$lib/components/Text/Text.svelte';
import type { TextareaProps } from '$lib/types.js';
import { cleanClass, generateId } from '$lib/utils.js';
import type { FormEventHandler } from 'svelte/elements';
import { tv } from 'tailwind-variants';
let {
ref = $bindable(null),
containerRef = $bindable(null),
shape = 'semi-round',
size = 'medium',
class: className,
grow,
value = $bindable<string>(),
...restProps
}: TextareaProps = $props();
let {
ref = $bindable(null),
containerRef = $bindable(null),
shape = 'semi-round',
size = 'medium',
class: className,
grow,
value = $bindable<string>(),
...restProps
}: TextareaProps = $props();
const { label, description, readOnly, required, invalid, disabled, ...labelProps } =
$derived(getFieldContext());
const { label, description, readOnly, required, invalid, disabled, ...labelProps } = $derived(getFieldContext());
const styles = tv({
base: 'w-full bg-gray-200 outline-none disabled:cursor-not-allowed disabled:bg-gray-300 disabled:text-gray-400 dark:bg-gray-600 dark:disabled:bg-gray-800 dark:disabled:text-gray-200',
variants: {
shape: {
rectangle: 'rounded-none',
'semi-round': '',
round: 'rounded-full',
},
padding: {
base: 'px-4 py-3',
round: 'px-5 py-3',
},
grow: {
true: 'resize-none',
false: '',
},
roundedSize: {
tiny: 'rounded-xl',
small: 'rounded-xl',
medium: 'rounded-2xl',
large: 'rounded-2xl',
giant: 'rounded-2xl',
},
textSize: {
tiny: 'text-xs',
small: 'text-sm',
medium: 'text-base',
large: 'text-lg',
giant: 'text-xl',
},
invalid: {
true: 'border-danger/80 border',
false: '',
},
},
});
const styles = tv({
base: 'w-full bg-gray-200 outline-none disabled:cursor-not-allowed disabled:bg-gray-300 disabled:text-gray-400 dark:bg-gray-600 dark:disabled:bg-gray-800 dark:disabled:text-gray-200',
variants: {
shape: {
rectangle: 'rounded-none',
'semi-round': '',
round: 'rounded-full',
},
padding: {
base: 'px-4 py-3',
round: 'px-5 py-3',
},
grow: {
true: 'resize-none',
false: '',
},
roundedSize: {
tiny: 'rounded-xl',
small: 'rounded-xl',
medium: 'rounded-2xl',
large: 'rounded-2xl',
giant: 'rounded-2xl',
},
textSize: {
tiny: 'text-xs',
small: 'text-sm',
medium: 'text-base',
large: 'text-lg',
giant: 'text-xl',
},
invalid: {
true: 'border-danger/80 border',
false: '',
},
},
});
const id = generateId();
const inputId = `input-${id}`;
const labelId = `label-${id}`;
const descriptionId = $derived(description ? `description-${id}` : undefined);
const id = generateId();
const inputId = `input-${id}`;
const labelId = `label-${id}`;
const descriptionId = $derived(description ? `description-${id}` : undefined);
const onInput: FormEventHandler<HTMLTextAreaElement> = (event) => {
const element = event.target as HTMLTextAreaElement;
if (element && grow) {
element.style.height = 'auto';
element.style.height = `${element.scrollHeight}px`;
}
const onInput: FormEventHandler<HTMLTextAreaElement> = (event) => {
const element = event.target as HTMLTextAreaElement;
if (element && grow) {
element.style.height = 'auto';
element.style.height = `${element.scrollHeight}px`;
}
restProps?.oninput?.(event);
};
restProps?.oninput?.(event);
};
</script>
<div class="flex w-full flex-col gap-1" bind:this={containerRef}>
{#if label}
<Label id={labelId} for={inputId} {label} {...labelProps} />
{/if}
{#if label}
<Label id={labelId} for={inputId} {label} {...labelProps} />
{/if}
{#if description}
<Text color="secondary" size="small" id={descriptionId}>{description}</Text>
{/if}
{#if description}
<Text color="secondary" size="small" id={descriptionId}>{description}</Text>
{/if}
<div class="relative w-full">
<textarea
oninput={onInput}
id={inputId}
aria-labelledby={label && labelId}
{required}
aria-required={required}
{disabled}
aria-disabled={disabled}
aria-describedby={descriptionId}
readonly={readOnly}
aria-readonly={readOnly}
class={cleanClass(
styles({
shape,
textSize: size,
padding: shape === 'round' ? 'round' : 'base',
grow,
roundedSize: shape === 'semi-round' ? size : undefined,
invalid,
}),
className,
)}
bind:this={ref}
bind:value
{...restProps}
></textarea>
</div>
<div class="relative w-full">
<textarea
oninput={onInput}
id={inputId}
aria-labelledby={label && labelId}
{required}
aria-required={required}
{disabled}
aria-disabled={disabled}
aria-describedby={descriptionId}
readonly={readOnly}
aria-readonly={readOnly}
class={cleanClass(
styles({
shape,
textSize: size,
padding: shape === 'round' ? 'round' : 'base',
grow,
roundedSize: shape === 'semi-round' ? size : undefined,
invalid,
}),
className,
)}
bind:this={ref}
bind:value
{...restProps}
></textarea>
</div>
</div>
<style>
textarea::-ms-reveal {
display: none;
}
textarea::-ms-reveal {
display: none;
}
</style>
@@ -1,52 +1,39 @@
<script lang="ts">
import IconButton from '$lib/components/IconButton/IconButton.svelte';
import { theme, toggleTheme } from '$lib/services/theme.svelte.js';
import { t } from '$lib/services/translation.svelte.js';
import {
Theme,
type Color,
type Size,
type TranslationProps,
type Variants,
} from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import { mdiWeatherNight, mdiWeatherSunny } from '@mdi/js';
import IconButton from '$lib/components/IconButton/IconButton.svelte';
import { theme, toggleTheme } from '$lib/services/theme.svelte.js';
import { t } from '$lib/services/translation.svelte.js';
import { Theme, type Color, type Size, type TranslationProps, type Variants } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import { mdiWeatherNight, mdiWeatherSunny } from '@mdi/js';
type Props = {
size?: Size;
class?: string;
color?: Color;
variant?: Variants;
translations?: TranslationProps<'dark_theme'>;
onChange?: (theme: Theme) => void;
};
type Props = {
size?: Size;
class?: string;
color?: Color;
variant?: Variants;
translations?: TranslationProps<'dark_theme'>;
onChange?: (theme: Theme) => void;
};
const {
color = 'primary',
variant = 'ghost',
size,
class: className,
translations,
onChange,
}: Props = $props();
const { color = 'primary', variant = 'ghost', size, class: className, translations, onChange }: Props = $props();
const handleToggleTheme = () => {
toggleTheme();
onChange?.(theme.value);
};
const handleToggleTheme = () => {
toggleTheme();
onChange?.(theme.value);
};
const themeIcon = $derived(theme.value === Theme.Light ? mdiWeatherSunny : mdiWeatherNight);
const themeIcon = $derived(theme.value === Theme.Light ? mdiWeatherSunny : mdiWeatherNight);
</script>
<IconButton
shape="round"
{color}
{size}
{variant}
icon={themeIcon}
onclick={handleToggleTheme}
class={cleanClass(className)}
aria-label={t('dark_theme', translations)}
role="switch"
aria-checked={theme.value === Theme.Dark}
shape="round"
{color}
{size}
{variant}
icon={themeIcon}
onclick={handleToggleTheme}
class={cleanClass(className)}
aria-label={t('dark_theme', translations)}
role="switch"
aria-checked={theme.value === Theme.Dark}
/>
+13 -13
View File
@@ -1,15 +1,15 @@
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',
CardFooter = 'card-footer',
Modal = 'modal',
ModalHeader = 'modal-header',
ModalBody = 'modal-body',
ModalFooter = 'modal-footer',
Field = 'field',
HelperText = 'helped-text',
AppShell = 'app-shell',
AppShellHeader = 'app-shell-header',
AppShellSidebar = 'app-shell-sidebar',
Card = 'card',
CardHeader = 'card-header',
CardBody = 'card-body',
CardFooter = 'card-footer',
Modal = 'modal',
ModalHeader = 'modal-header',
ModalBody = 'modal-body',
ModalFooter = 'modal-footer',
}
+177 -177
View File
@@ -1,193 +1,193 @@
<script lang="ts">
import Icon from '$lib/components/Icon/Icon.svelte';
import LoadingSpinner from '$lib/components/LoadingSpinner/LoadingSpinner.svelte';
import type { ButtonProps, Size } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import { Button as ButtonPrimitive } from 'bits-ui';
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
import { twMerge } from 'tailwind-merge';
import { tv } from 'tailwind-variants';
import Icon from '$lib/components/Icon/Icon.svelte';
import LoadingSpinner from '$lib/components/LoadingSpinner/LoadingSpinner.svelte';
import type { ButtonProps, Size } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import { Button as ButtonPrimitive } from 'bits-ui';
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
import { twMerge } from 'tailwind-merge';
import { tv } from 'tailwind-variants';
type InternalButtonProps = ButtonProps & {
/** when true, button width to height ratio is 1:1 */
icon?: boolean;
};
type InternalButtonProps = ButtonProps & {
/** when true, button width to height ratio is 1:1 */
icon?: boolean;
};
let {
ref = $bindable(null),
type = 'button',
href,
external,
variant = 'filled',
color = 'primary',
shape = 'semi-round',
size = 'medium',
loading = false,
fullWidth = false,
leadingIcon,
trailingIcon,
icon = false,
class: className = '',
children,
...restProps
}: InternalButtonProps = $props();
let {
ref = $bindable(null),
type = 'button',
href,
external,
variant = 'filled',
color = 'primary',
shape = 'semi-round',
size = 'medium',
loading = false,
fullWidth = false,
leadingIcon,
trailingIcon,
icon = false,
class: className = '',
children,
...restProps
}: InternalButtonProps = $props();
const disabled = $derived((restProps as HTMLButtonAttributes).disabled || loading);
const disabled = $derived((restProps as HTMLButtonAttributes).disabled || loading);
const buttonVariants = tv({
base: 'ring-offset-background focus-visible:ring-ring flex items-center justify-center gap-1 rounded-md text-sm font-medium whitespace-nowrap transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
variants: {
disabled: {
true: 'disabled:pointer-events-none disabled:opacity-50 aria-disabled:opacity-50',
false: 'cursor-pointer',
},
shape: {
rectangle: 'rounded-none',
'semi-round': 'rounded-xl',
round: 'rounded-full',
},
fullWidth: {
true: 'w-full',
},
textPadding: {
tiny: 'px-3 py-1',
small: 'px-4 py-2',
medium: 'px-5 py-2',
large: 'px-8 py-2.5',
giant: 'px-10 py-3',
},
textSize: {
tiny: 'text-xs',
small: 'text-sm',
medium: 'text-base',
large: 'text-lg',
giant: 'text-xl',
},
iconSize: {
tiny: 'h-6 w-6',
small: 'h-8 w-8',
medium: 'h-10 w-10',
large: 'h-12 w-12',
giant: 'h-14 w-14',
},
roundedSize: {
tiny: 'rounded-lg',
small: 'rounded-lg',
medium: 'rounded-xl',
large: 'rounded-xl',
giant: 'rounded-2xl',
},
filledColor: {
primary: 'bg-primary text-light hover:bg-primary/80',
secondary: 'bg-dark text-light hover:bg-dark/80',
success: 'bg-success text-light hover:bg-success/80',
danger: 'bg-danger text-light hover:bg-danger/80',
warning: 'bg-warning text-light hover:bg-warning/80',
info: 'bg-info text-light hover:bg-info/80',
},
outlineColor: {
primary: 'border-primary bg-primary/10 text-primary hover:bg-primary/20 border',
secondary: 'border-dark bg-dark/10 text-dark hover:bg-dark/20 border',
success: 'border-success bg-success/10 text-success hover:bg-success/20 border',
danger: 'border-danger bg-danger/10 text-danger hover:bg-danger/20 border',
warning: 'border-warning bg-warning/10 text-warning hover:bg-warning/20 border',
info: 'border-info bg-info/10 text-info hover:bg-info/20 border',
},
ghostColor: {
primary: 'text-primary hover:bg-primary/10',
secondary: 'text-dark hover:bg-dark/10',
success: 'text-success hover:bg-success/10',
danger: 'text-danger hover:bg-danger/10',
warning: 'text-warning hover:bg-warning/10',
info: 'text-info hover:bg-info/10',
},
},
});
const buttonVariants = tv({
base: 'ring-offset-background focus-visible:ring-ring flex items-center justify-center gap-1 rounded-md text-sm font-medium whitespace-nowrap transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
variants: {
disabled: {
true: 'disabled:pointer-events-none disabled:opacity-50 aria-disabled:opacity-50',
false: 'cursor-pointer',
},
shape: {
rectangle: 'rounded-none',
'semi-round': 'rounded-xl',
round: 'rounded-full',
},
fullWidth: {
true: 'w-full',
},
textPadding: {
tiny: 'px-3 py-1',
small: 'px-4 py-2',
medium: 'px-5 py-2',
large: 'px-8 py-2.5',
giant: 'px-10 py-3',
},
textSize: {
tiny: 'text-xs',
small: 'text-sm',
medium: 'text-base',
large: 'text-lg',
giant: 'text-xl',
},
iconSize: {
tiny: 'h-6 w-6',
small: 'h-8 w-8',
medium: 'h-10 w-10',
large: 'h-12 w-12',
giant: 'h-14 w-14',
},
roundedSize: {
tiny: 'rounded-lg',
small: 'rounded-lg',
medium: 'rounded-xl',
large: 'rounded-xl',
giant: 'rounded-2xl',
},
filledColor: {
primary: 'bg-primary text-light hover:bg-primary/80',
secondary: 'bg-dark text-light hover:bg-dark/80',
success: 'bg-success text-light hover:bg-success/80',
danger: 'bg-danger text-light hover:bg-danger/80',
warning: 'bg-warning text-light hover:bg-warning/80',
info: 'bg-info text-light hover:bg-info/80',
},
outlineColor: {
primary: 'border-primary bg-primary/10 text-primary hover:bg-primary/20 border',
secondary: 'border-dark bg-dark/10 text-dark hover:bg-dark/20 border',
success: 'border-success bg-success/10 text-success hover:bg-success/20 border',
danger: 'border-danger bg-danger/10 text-danger hover:bg-danger/20 border',
warning: 'border-warning bg-warning/10 text-warning hover:bg-warning/20 border',
info: 'border-info bg-info/10 text-info hover:bg-info/20 border',
},
ghostColor: {
primary: 'text-primary hover:bg-primary/10',
secondary: 'text-dark hover:bg-dark/10',
success: 'text-success hover:bg-success/10',
danger: 'text-danger hover:bg-danger/10',
warning: 'text-warning hover:bg-warning/10',
info: 'text-info hover:bg-info/10',
},
},
});
const spinnerSizes: Record<Size, Size> = {
tiny: 'tiny',
small: 'tiny',
medium: 'small',
large: 'medium',
giant: 'large',
};
const spinnerSizes: Record<Size, Size> = {
tiny: 'tiny',
small: 'tiny',
medium: 'small',
large: 'medium',
giant: 'large',
};
const classList = $derived(
cleanClass(
twMerge(
buttonVariants({
shape,
fullWidth,
textPadding: icon ? undefined : size,
textSize: size,
iconSize: icon ? size : undefined,
disabled,
roundedSize: shape === 'semi-round' ? size : undefined,
filledColor: variant === 'filled' ? color : undefined,
outlineColor: variant === 'outline' ? color : undefined,
ghostColor: variant === 'ghost' ? color : undefined,
}),
className,
),
),
);
const classList = $derived(
cleanClass(
twMerge(
buttonVariants({
shape,
fullWidth,
textPadding: icon ? undefined : size,
textSize: size,
iconSize: icon ? size : undefined,
disabled,
roundedSize: shape === 'semi-round' ? size : undefined,
filledColor: variant === 'filled' ? color : undefined,
outlineColor: variant === 'outline' ? color : undefined,
ghostColor: variant === 'ghost' ? color : undefined,
}),
className,
),
),
);
const iconSizes = {
tiny: 'h-4 w-4',
small: 'h-4 w-4',
medium: 'h-4 w-4',
large: 'h-6 w-6',
giant: 'h-8 w-8',
};
const iconSizes = {
tiny: 'h-4 w-4',
small: 'h-4 w-4',
medium: 'h-4 w-4',
large: 'h-6 w-6',
giant: 'h-8 w-8',
};
</script>
{#snippet content()}
{#if leadingIcon && !loading}
<div>
<Icon size="100%" class={iconSizes[size]} icon={leadingIcon} aria-hidden />
</div>
{/if}
{@render children?.()}
{#if trailingIcon}
<Icon size="100%" class={iconSizes[size]} icon={trailingIcon} aria-hidden />
{/if}
{#if leadingIcon && !loading}
<div>
<Icon size="100%" class={iconSizes[size]} icon={leadingIcon} aria-hidden />
</div>
{/if}
{@render children?.()}
{#if trailingIcon}
<Icon size="100%" class={iconSizes[size]} icon={trailingIcon} aria-hidden />
{/if}
{/snippet}
{#if href}
<a
bind:this={ref}
{href}
class={classList}
aria-disabled={disabled}
target={external ? '_blank' : undefined}
rel={external ? 'noopener noreferrer' : undefined}
{...restProps as HTMLAnchorAttributes}
>
{#if loading}
<div class="flex items-center justify-center gap-2">
<LoadingSpinner {color} size={spinnerSizes[size]} />
{@render content()}
</div>
{:else}
{@render content()}
{/if}
</a>
<a
bind:this={ref}
{href}
class={classList}
aria-disabled={disabled}
target={external ? '_blank' : undefined}
rel={external ? 'noopener noreferrer' : undefined}
{...restProps as HTMLAnchorAttributes}
>
{#if loading}
<div class="flex items-center justify-center gap-2">
<LoadingSpinner {color} size={spinnerSizes[size]} />
{@render content()}
</div>
{:else}
{@render content()}
{/if}
</a>
{:else}
<ButtonPrimitive.Root
bind:ref
class={classList}
type={type as HTMLButtonAttributes['type']}
{...restProps as HTMLButtonAttributes}
{disabled}
aria-disabled={disabled}
>
{#if loading}
<div class="flex items-center justify-center gap-2">
<LoadingSpinner {color} size={spinnerSizes[size]} />
{@render content()}
</div>
{:else}
{@render content()}
{/if}
</ButtonPrimitive.Root>
<ButtonPrimitive.Root
bind:ref
class={classList}
type={type as HTMLButtonAttributes['type']}
{...restProps as HTMLButtonAttributes}
{disabled}
aria-disabled={disabled}
>
{#if loading}
<div class="flex items-center justify-center gap-2">
<LoadingSpinner {color} size={spinnerSizes[size]} />
{@render content()}
</div>
{:else}
{@render content()}
{/if}
</ButtonPrimitive.Root>
{/if}
+21 -21
View File
@@ -1,28 +1,28 @@
<script lang="ts">
import { ChildKey } from '$lib/constants.js';
import type { ChildData } from '$lib/types.js';
import { withPrefix } from '$lib/utils.js';
import { getContext, type Snippet } from 'svelte';
import { ChildKey } from '$lib/constants.js';
import type { ChildData } from '$lib/types.js';
import { withPrefix } from '$lib/utils.js';
import { getContext, type Snippet } from 'svelte';
type ContextType = {
register: (key: ChildKey, data: () => ChildData) => void;
};
type Props = {
for: ChildKey;
as: ChildKey;
class?: string;
children: Snippet;
};
type ContextType = {
register: (key: ChildKey, data: () => ChildData) => void;
};
type Props = {
for: ChildKey;
as: ChildKey;
class?: string;
children: Snippet;
};
const { for: key, as, children, class: className }: Props = $props();
const { for: key, as, children, class: className }: Props = $props();
const context = getContext<ContextType>(withPrefix(key));
const context = getContext<ContextType>(withPrefix(key));
const data = $derived({ snippet: children, class: className });
const data = $derived({ snippet: children, class: className });
if (context) {
context.register(as, () => data);
} else {
console.log('Unable to find context for key:', key);
}
if (context) {
context.register(as, () => data);
} else {
console.log('Unable to find context for key:', key);
}
</script>
+135 -142
View File
@@ -1,163 +1,156 @@
<script lang="ts">
import { getFieldContext } from '$lib/common/context.svelte.js';
import Field from '$lib/components/Field/Field.svelte';
import Icon from '$lib/components/Icon/Icon.svelte';
import IconButton from '$lib/components/IconButton/IconButton.svelte';
import Input from '$lib/components/Input/Input.svelte';
import Label from '$lib/components/Label/Label.svelte';
import type { SelectCommonProps, SelectItem } from '$lib/types.js';
import { cleanClass, generateId } from '$lib/utils.js';
import { mdiArrowDown, mdiArrowUp, mdiCheck, mdiUnfoldMoreHorizontal } from '@mdi/js';
import { Select } from 'bits-ui';
import { getFieldContext } from '$lib/common/context.svelte.js';
import Field from '$lib/components/Field/Field.svelte';
import Icon from '$lib/components/Icon/Icon.svelte';
import IconButton from '$lib/components/IconButton/IconButton.svelte';
import Input from '$lib/components/Input/Input.svelte';
import Label from '$lib/components/Label/Label.svelte';
import type { SelectCommonProps, SelectItem } from '$lib/types.js';
import { cleanClass, generateId } from '$lib/utils.js';
import { mdiArrowDown, mdiArrowUp, mdiCheck, mdiUnfoldMoreHorizontal } from '@mdi/js';
import { Select } from 'bits-ui';
type T = SelectItem;
type T = SelectItem;
type Props = {
multiple?: boolean;
values: T[];
asLabel?: (items: T[]) => string;
onChange?: (values: T[]) => void;
} & SelectCommonProps<T>;
type Props = {
multiple?: boolean;
values: T[];
asLabel?: (items: T[]) => string;
onChange?: (values: T[]) => void;
} & SelectCommonProps<T>;
let {
data,
shape,
size = 'medium',
multiple = false,
values = $bindable([]),
onChange,
asLabel = (options: T[]) => options.map(({ label }) => label).join(', '),
placeholder,
class: className,
}: Props = $props();
let {
data,
shape,
size = 'medium',
multiple = false,
values = $bindable([]),
onChange,
asLabel = (options: T[]) => options.map(({ label }) => label).join(', '),
placeholder,
class: className,
}: Props = $props();
const asOptions = (items: string[] | T[]) => {
return items.map((item) => {
if (typeof item === 'string') {
return { value: item, label: item } as T;
}
const asOptions = (items: string[] | T[]) => {
return items.map((item) => {
if (typeof item === 'string') {
return { value: item, label: item } as T;
}
const label = item.label ?? item.value;
return { ...item, label };
});
};
const label = item.label ?? item.value;
return { ...item, label };
});
};
const options = $derived(asOptions(data));
const options = $derived(asOptions(data));
const { readOnly, required, invalid, disabled, label, ...labelProps } =
$derived(getFieldContext());
const { readOnly, required, invalid, disabled, label, ...labelProps } = $derived(getFieldContext());
const id = generateId();
const inputId = `input-${id}`;
const labelId = `label-${id}`;
const id = generateId();
const inputId = `input-${id}`;
const labelId = `label-${id}`;
const selectedLabel = $derived(asLabel(values));
const selectedLabel = $derived(asLabel(values));
let inputRef = $state<HTMLElement | null>(null);
let contentRef = $state<HTMLElement | null>(null);
let ref = $state<HTMLElement | null>(null);
let inputRef = $state<HTMLElement | null>(null);
let contentRef = $state<HTMLElement | null>(null);
let ref = $state<HTMLElement | null>(null);
$effect(() => {
if (ref && contentRef) {
contentRef.style.width = `${ref.clientWidth}px`;
}
});
$effect(() => {
if (ref && contentRef) {
contentRef.style.width = `${ref.clientWidth}px`;
}
});
const findOption = (value: string) => options.find((option) => option.value === value);
const findOption = (value: string) => options.find((option) => option.value === value);
const onValueChange = (items: string[] | string) => {
values = multiple
? ((items as string[]).map(findOption) as T[])
: [findOption(items as string) as T].filter(Boolean);
const onValueChange = (items: string[] | string) => {
values = multiple
? ((items as string[]).map(findOption) as T[])
: [findOption(items as string) as T].filter(Boolean);
onChange?.(values);
};
onChange?.(values);
};
let internalValue = $derived(
multiple ? values.map(({ value }) => value) : (values[0]?.value ?? ''),
);
let internalValue = $derived(multiple ? values.map(({ value }) => value) : (values[0]?.value ?? ''));
</script>
<div class={cleanClass('flex flex-col gap-1', className)} bind:this={ref}>
{#if label}
<Label id={labelId} for={inputId} {label} {...labelProps} />
{/if}
{#if label}
<Label id={labelId} for={inputId} {label} {...labelProps} />
{/if}
<Select.Root
type={multiple ? 'multiple' : 'single'}
bind:value={internalValue as never}
{onValueChange}
>
<Select.Trigger
{disabled}
class="w-full items-center gap-1 rounded-lg focus-visible:outline-none"
aria-label={placeholder}
>
<Field {readOnly} {required} {disabled} {invalid}>
<Input
bind:containerRef={inputRef}
id={inputId}
{size}
{shape}
{placeholder}
value={selectedLabel}
readonly
aria-labelledby={labelId}
aria-readonly
>
{#snippet trailingIcon()}
<IconButton
variant="ghost"
shape="round"
color="secondary"
class="m-1"
icon={mdiUnfoldMoreHorizontal}
{disabled}
aria-label="Expand"
/>
{/snippet}
</Input>
</Field>
</Select.Trigger>
<Select.Portal>
<Select.Content
bind:ref={contentRef}
class="bg-light text-dark max-h-96 rounded-xl border py-3 outline-none select-none"
sideOffset={10}
>
<Select.ScrollUpButton class="flex w-full items-center justify-center">
<Icon icon={mdiArrowUp} />
</Select.ScrollUpButton>
<Select.Viewport>
{#each options as { value, label, disabled }, i (i + value)}
<Select.Item
class={cleanClass(
'hover:bg-subtle data-[selected]:bg-primary/10 flex h-10 w-full items-center px-5 py-3 text-sm duration-75 outline-none select-none data-disabled:opacity-50',
disabled ? 'cursor-not-allowed' : 'cursor-pointer',
)}
{value}
{label}
{disabled}
>
{#snippet children({ selected })}
<div
class="flex cursor-pointer items-center justify-center gap-2 text-sm font-medium whitespace-nowrap transition-colors"
>
<span>{label}</span>
</div>
{#if selected}
<div class="ms-auto">
<Icon icon={mdiCheck} />
</div>
{/if}
{/snippet}
</Select.Item>
{/each}
</Select.Viewport>
<Select.ScrollDownButton class="flex w-full items-center justify-center">
<Icon icon={mdiArrowDown} />
</Select.ScrollDownButton>
</Select.Content>
</Select.Portal>
</Select.Root>
<Select.Root type={multiple ? 'multiple' : 'single'} bind:value={internalValue as never} {onValueChange}>
<Select.Trigger
{disabled}
class="w-full items-center gap-1 rounded-lg focus-visible:outline-none"
aria-label={placeholder}
>
<Field {readOnly} {required} {disabled} {invalid}>
<Input
bind:containerRef={inputRef}
id={inputId}
{size}
{shape}
{placeholder}
value={selectedLabel}
readonly
aria-labelledby={labelId}
aria-readonly
>
{#snippet trailingIcon()}
<IconButton
variant="ghost"
shape="round"
color="secondary"
class="m-1"
icon={mdiUnfoldMoreHorizontal}
{disabled}
aria-label="Expand"
/>
{/snippet}
</Input>
</Field>
</Select.Trigger>
<Select.Portal>
<Select.Content
bind:ref={contentRef}
class="bg-light text-dark max-h-96 rounded-xl border py-3 outline-none select-none"
sideOffset={10}
>
<Select.ScrollUpButton class="flex w-full items-center justify-center">
<Icon icon={mdiArrowUp} />
</Select.ScrollUpButton>
<Select.Viewport>
{#each options as { value, label, disabled }, i (i + value)}
<Select.Item
class={cleanClass(
'hover:bg-subtle data-[selected]:bg-primary/10 flex h-10 w-full items-center px-5 py-3 text-sm duration-75 outline-none select-none data-disabled:opacity-50',
disabled ? 'cursor-not-allowed' : 'cursor-pointer',
)}
{value}
{label}
{disabled}
>
{#snippet children({ selected })}
<div
class="flex cursor-pointer items-center justify-center gap-2 text-sm font-medium whitespace-nowrap transition-colors"
>
<span>{label}</span>
</div>
{#if selected}
<div class="ms-auto">
<Icon icon={mdiCheck} />
</div>
{/if}
{/snippet}
</Select.Item>
{/each}
</Select.Viewport>
<Select.ScrollDownButton class="flex w-full items-center justify-center">
<Icon icon={mdiArrowDown} />
</Select.ScrollDownButton>
</Select.Content>
</Select.Portal>
</Select.Root>
</div>
+42 -50
View File
@@ -1,60 +1,52 @@
<script lang="ts">
import type { FontWeight, HeadingColor, HeadingTag, TextVariant } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import { tv } from 'tailwind-variants';
import type { FontWeight, HeadingColor, HeadingTag, TextVariant } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import { tv } from 'tailwind-variants';
type Props = {
/**
* The HTML element type.
*/
tag?: HeadingTag;
fontWeight?: FontWeight;
variant?: TextVariant;
color?: HeadingColor;
class?: string;
type Props = {
/**
* The HTML element type.
*/
tag?: HeadingTag;
fontWeight?: FontWeight;
variant?: TextVariant;
color?: HeadingColor;
class?: string;
children: Snippet;
} & HTMLAttributes<HTMLElement>;
children: Snippet;
} & HTMLAttributes<HTMLElement>;
const {
tag = 'p',
color,
fontWeight,
variant,
class: className,
children,
...restProps
}: Props = $props();
const { tag = 'p', color, fontWeight, variant, class: className, children, ...restProps }: Props = $props();
const styles = tv({
variants: {
color: {
muted: 'text-gray-600 dark:text-gray-400',
primary: 'text-primary',
secondary: 'text-dark',
success: 'text-success',
danger: 'text-danger',
warning: 'text-warning',
info: 'text-info',
},
fontWeight: {
light: 'font-light',
normal: 'font-normal',
'semi-bold': 'font-semibold',
bold: 'font-bold',
'extra-bold': 'font-extrabold',
},
variant: {
italic: 'italic',
},
},
});
const styles = tv({
variants: {
color: {
muted: 'text-gray-600 dark:text-gray-400',
primary: 'text-primary',
secondary: 'text-dark',
success: 'text-success',
danger: 'text-danger',
warning: 'text-warning',
info: 'text-info',
},
fontWeight: {
light: 'font-light',
normal: 'font-normal',
'semi-bold': 'font-semibold',
bold: 'font-bold',
'extra-bold': 'font-extrabold',
},
variant: {
italic: 'italic',
},
},
});
const classList = $derived(cleanClass(styles({ color, fontWeight, variant }), className));
const classList = $derived(cleanClass(styles({ color, fontWeight, variant }), className));
</script>
<svelte:element this={tag} class={classList} {...restProps}>
{@render children()}
{@render children()}
</svelte:element>
@@ -1,124 +1,122 @@
import { goto } from '$app/navigation';
export type CommandItem = {
icon: string;
iconClass: string;
type: string;
title: string;
description?: string;
text: string;
icon: string;
iconClass: string;
type: string;
title: string;
description?: string;
text: string;
} & ({ href: string; external?: boolean } | { action: () => void });
export const asText = (...items: unknown[]) => {
return items
.filter((item) => item !== undefined && item !== null)
.map((items) => String(items))
.join('|')
.toLowerCase();
return items
.filter((item) => item !== undefined && item !== null)
.map((items) => String(items))
.join('|')
.toLowerCase();
};
const isEqual = (a: CommandItem, b: CommandItem) => {
return a.title === b.title && a.type === b.type;
return a.title === b.title && a.type === b.type;
};
const isMatch = (item: CommandItem, query: string): boolean => {
if (!query) {
return true;
}
if (!query) {
return true;
}
return item.text.includes(query);
return item.text.includes(query);
};
class CommandPaletteManager {
isEnabled = $state(false);
isOpen = $state(false);
query = $state('');
selectedIndex = $state(0);
private normalizedQuery = $derived(this.query.toLowerCase());
isEnabled = $state(false);
isOpen = $state(false);
query = $state('');
selectedIndex = $state(0);
private normalizedQuery = $derived(this.query.toLowerCase());
items: CommandItem[] = [];
filteredItems = $derived(
this.items.filter((item) => isMatch(item, this.normalizedQuery)).slice(0, 100),
);
recentItems = $state<CommandItem[]>([]);
results = $derived(this.query ? this.filteredItems : this.recentItems);
items: CommandItem[] = [];
filteredItems = $derived(this.items.filter((item) => isMatch(item, this.normalizedQuery)).slice(0, 100));
recentItems = $state<CommandItem[]>([]);
results = $derived(this.query ? this.filteredItems : this.recentItems);
enable() {
this.isEnabled = true;
}
enable() {
this.isEnabled = true;
}
async open() {
if (!this.isEnabled || this.isOpen) {
return;
}
async open() {
if (!this.isEnabled || this.isOpen) {
return;
}
this.selectedIndex = 0;
this.isOpen = true;
}
this.selectedIndex = 0;
this.isOpen = true;
}
close() {
if (!this.isEnabled || !this.isOpen) {
return;
}
close() {
if (!this.isEnabled || !this.isOpen) {
return;
}
this.query = '';
this.isOpen = false;
}
this.query = '';
this.isOpen = false;
}
async select(selectedIndex?: number) {
const selected = this.results[selectedIndex ?? this.selectedIndex];
if (!selected) {
return;
}
async select(selectedIndex?: number) {
const selected = this.results[selectedIndex ?? this.selectedIndex];
if (!selected) {
return;
}
// no duplicates
this.recentItems = this.recentItems.filter((item) => !isEqual(item, selected));
this.recentItems.unshift(selected);
this.recentItems = this.recentItems.slice(0, 5);
// no duplicates
this.recentItems = this.recentItems.filter((item) => !isEqual(item, selected));
this.recentItems.unshift(selected);
this.recentItems = this.recentItems.slice(0, 5);
if ('href' in selected) {
if (selected.href.startsWith('http') || selected.external) {
window.open(selected.href, '_blank');
} else {
// eslint-disable-next-line svelte/no-navigation-without-resolve
await goto(selected.href);
}
} else {
await selected.action();
}
if ('href' in selected) {
if (selected.href.startsWith('http') || selected.external) {
window.open(selected.href, '_blank');
} else {
// eslint-disable-next-line svelte/no-navigation-without-resolve
await goto(selected.href);
}
} else {
await selected.action();
}
this.close();
}
this.close();
}
remove(index: number) {
this.recentItems.splice(index, 1);
}
remove(index: number) {
this.recentItems.splice(index, 1);
}
up() {
this.selectedIndex = (this.selectedIndex - 1 + this.results.length) % this.results.length;
}
up() {
this.selectedIndex = (this.selectedIndex - 1 + this.results.length) % this.results.length;
}
down() {
this.selectedIndex = (this.selectedIndex + 1) % this.results.length;
}
down() {
this.selectedIndex = (this.selectedIndex + 1) % this.results.length;
}
reset() {
this.items = [];
this.isOpen = false;
this.query = '';
}
reset() {
this.items = [];
this.isOpen = false;
this.query = '';
}
addCommands(itemOrItems: CommandItem | CommandItem[]) {
const items = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
this.items.push(...items);
}
addCommands(itemOrItems: CommandItem | CommandItem[]) {
const items = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
this.items.push(...items);
}
removeCommands(itemOrItems: CommandItem | CommandItem[]) {
const items = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
for (const remoteItem of items) {
this.items = this.items.filter((item) => !isEqual(item, remoteItem));
}
}
removeCommands(itemOrItems: CommandItem | CommandItem[]) {
const items = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
for (const remoteItem of items) {
this.items = this.items.filter((item) => !isEqual(item, remoteItem));
}
}
}
export const commandPaletteManager = new CommandPaletteManager();
+36 -40
View File
@@ -2,57 +2,53 @@ import { mount, unmount, type Component, type ComponentProps } from 'svelte';
import ConfirmModal from '$lib/components/ConfirmModal/ConfirmModal.svelte';
type OnCloseData<T> = T extends { onClose: (data?: infer R) => void }
? R | undefined
: T extends { onClose: (data: infer R) => void }
? R
: never;
? R | undefined
: T extends { onClose: (data: infer R) => void }
? R
: never;
type ExtendsEmptyObject<T> = keyof T extends never ? never : T;
type StripValueIfOptional<T> = T extends undefined ? undefined : T;
// if the modal does not expect any props, makes the props param optional but also allows passing `{}` and `undefined`
type OptionalParamIfEmpty<T> =
ExtendsEmptyObject<T> extends never ? [] | [Record<string, never> | undefined] : [T];
type OptionalParamIfEmpty<T> = ExtendsEmptyObject<T> extends never ? [] | [Record<string, never> | undefined] : [T];
class ModalManager {
show<T extends object>(
Component: Component<T>,
...props: OptionalParamIfEmpty<Omit<T, 'onClose'>>
) {
return this.open(Component, ...props).onClose;
}
show<T extends object>(Component: Component<T>, ...props: OptionalParamIfEmpty<Omit<T, 'onClose'>>) {
return this.open(Component, ...props).onClose;
}
open<T extends object, K = OnCloseData<T>>(
Component: Component<T>,
...props: OptionalParamIfEmpty<Omit<T, 'onClose'>>
) {
let modal: object = {};
let onClose: (...args: [StripValueIfOptional<K>]) => Promise<void>;
open<T extends object, K = OnCloseData<T>>(
Component: Component<T>,
...props: OptionalParamIfEmpty<Omit<T, 'onClose'>>
) {
let modal: object = {};
let onClose: (...args: [StripValueIfOptional<K>]) => Promise<void>;
const deferred = new Promise<StripValueIfOptional<K>>((resolve) => {
onClose = async (...args: [StripValueIfOptional<K>]) => {
await unmount(modal);
// make sure bits-ui clean up finishes before resolving
setTimeout(() => resolve(args?.[0]), 10);
};
const deferred = new Promise<StripValueIfOptional<K>>((resolve) => {
onClose = async (...args: [StripValueIfOptional<K>]) => {
await unmount(modal);
// make sure bits-ui clean up finishes before resolving
setTimeout(() => resolve(args?.[0]), 10);
};
modal = mount(Component, {
target: document.body,
props: {
...((props?.[0] ?? {}) as T),
onClose,
},
});
});
modal = mount(Component, {
target: document.body,
props: {
...((props?.[0] ?? {}) as T),
onClose,
},
});
});
return {
onClose: deferred,
close: (...args: [StripValueIfOptional<K>]) => onClose(args[0]),
};
}
return {
onClose: deferred,
close: (...args: [StripValueIfOptional<K>]) => onClose(args[0]),
};
}
showDialog(options: Omit<ComponentProps<typeof ConfirmModal>, 'onClose'>) {
return this.show(ConfirmModal, options);
}
showDialog(options: Omit<ComponentProps<typeof ConfirmModal>, 'onClose'>) {
return this.show(ConfirmModal, options);
}
}
export const modalManager = new ModalManager();
+31 -31
View File
@@ -3,49 +3,49 @@ import { browser } from '$app/environment';
const asKey = (key: string) => `immich-ui-${key}`;
const readPreference = <T>(key: string, defaults: T) => {
if (!browser || !window?.localStorage) {
throw new Error('Local storage is not available');
}
if (!browser || !window?.localStorage) {
throw new Error('Local storage is not available');
}
const text = window.localStorage.getItem(asKey(key));
const stored = text ? (JSON.parse(text) as T) : {};
const text = window.localStorage.getItem(asKey(key));
const stored = text ? (JSON.parse(text) as T) : {};
return { ...defaults, ...stored };
return { ...defaults, ...stored };
};
const writePreference = <T>(key: string, value: T) => {
if (!browser || !window.localStorage) {
throw new Error('Local storage is not available');
}
const text = JSON.stringify(value);
window.localStorage.setItem(asKey(key), text);
if (!browser || !window.localStorage) {
throw new Error('Local storage is not available');
}
const text = JSON.stringify(value);
window.localStorage.setItem(asKey(key), text);
};
type PreferenceOptions<T> = {
key: string;
defaults: T;
onReadError?: (error: unknown) => void;
onWriteError?: (error: unknown) => void;
key: string;
defaults: T;
onReadError?: (error: unknown) => void;
onWriteError?: (error: unknown) => void;
};
export const preference = <T>(options: PreferenceOptions<T>) => {
const { key, defaults, onReadError, onWriteError } = options;
const { key, defaults, onReadError, onWriteError } = options;
let initialValue = defaults;
try {
initialValue = readPreference<T>(key, defaults);
} catch (error) {
onReadError?.(error);
}
let initialValue = defaults;
try {
initialValue = readPreference<T>(key, defaults);
} catch (error) {
onReadError?.(error);
}
const state = $state<T>(initialValue);
const state = $state<T>(initialValue);
const sync = () => {
try {
writePreference(key, state);
} catch (error) {
onWriteError?.(error);
}
};
const sync = () => {
try {
writePreference(key, state);
} catch (error) {
onWriteError?.(error);
}
};
return { state, sync };
return { state, sync };
};
+53 -54
View File
@@ -3,93 +3,92 @@ import { preference } from '$lib/services/preference.svelte.js';
import { Theme } from '$lib/types.js';
export type ThemeOptions = {
lightClass?: string;
darkClass?: string;
selector?: string;
lightClass?: string;
darkClass?: string;
selector?: string;
};
const defaultOptions: ThemeOptions = {
darkClass: 'dark',
selector: 'body',
darkClass: 'dark',
selector: 'body',
};
let options: ThemeOptions = defaultOptions;
export const setThemeOptions = (newOptions: ThemeOptions) =>
(options = { ...defaultOptions, ...newOptions });
export const setThemeOptions = (newOptions: ThemeOptions) => (options = { ...defaultOptions, ...newOptions });
type ThemePreference = { value: Theme };
const defaultTheme: ThemePreference = { value: Theme.Dark };
const { state, sync: syncToLocalStorage } = preference<ThemePreference>({
key: 'theme',
defaults: defaultTheme,
onReadError: (error) => console.log(`Preference read error: ${error}`),
onWriteError: (error) => console.log(`Preference write error: ${error}`),
key: 'theme',
defaults: defaultTheme,
onReadError: (error) => console.log(`Preference read error: ${error}`),
onWriteError: (error) => console.log(`Preference write error: ${error}`),
});
export const theme = state;
export const onThemeChange = () => {
syncToDom();
syncToLocalStorage();
syncToDom();
syncToLocalStorage();
};
const syncToDom = () => {
const { lightClass, darkClass, selector } = options;
if (!browser || !selector) {
return;
}
const { lightClass, darkClass, selector } = options;
if (!browser || !selector) {
return;
}
const element = document.querySelector(selector);
if (!element) {
return;
}
const element = document.querySelector(selector);
if (!element) {
return;
}
switch (theme.value) {
case Theme.Dark: {
if (lightClass) {
element.classList.remove(lightClass);
}
switch (theme.value) {
case Theme.Dark: {
if (lightClass) {
element.classList.remove(lightClass);
}
if (darkClass) {
element.classList.add(darkClass);
}
if (darkClass) {
element.classList.add(darkClass);
}
const darkReaderLock = document.createElement('meta');
darkReaderLock.name = 'darkreader-lock';
document.head.appendChild(darkReaderLock);
const darkReaderLock = document.createElement('meta');
darkReaderLock.name = 'darkreader-lock';
document.head.appendChild(darkReaderLock);
break;
}
break;
}
case Theme.Light: {
if (lightClass) {
element.classList.add(lightClass);
}
case Theme.Light: {
if (lightClass) {
element.classList.add(lightClass);
}
if (darkClass) {
element.classList.remove(darkClass);
}
if (darkClass) {
element.classList.remove(darkClass);
}
const darkReaderLock = document.querySelector('head > meta[name=darkreader-lock]');
if (darkReaderLock) {
document.head.removeChild(darkReaderLock);
}
const darkReaderLock = document.querySelector('head > meta[name=darkreader-lock]');
if (darkReaderLock) {
document.head.removeChild(darkReaderLock);
}
break;
}
}
break;
}
}
};
export const toggleTheme = () => {
theme.value = theme.value === Theme.Dark ? Theme.Light : Theme.Dark;
onThemeChange();
theme.value = theme.value === Theme.Dark ? Theme.Light : Theme.Dark;
onThemeChange();
};
export const initializeTheme = (options?: ThemeOptions) => {
if (options) {
setThemeOptions(options);
}
if (options) {
setThemeOptions(options);
}
syncToDom();
syncToDom();
};
+19 -21
View File
@@ -1,37 +1,35 @@
import type { TranslationProps } from '$lib/types.js';
const defaultTranslations = {
// common
cancel: 'Cancel',
close: 'Close',
confirm: 'Confirm',
search_placeholder: 'Search...',
search_no_results: 'No results',
search_recently_used: 'Recently used',
// common
cancel: 'Cancel',
close: 'Close',
confirm: 'Confirm',
search_placeholder: 'Search...',
search_no_results: 'No results',
search_recently_used: 'Recently used',
// modal
prompt_default: 'Are you sure you want to do this?',
// modal
prompt_default: 'Are you sure you want to do this?',
// password input
show_password: 'Show password',
hide_password: 'Hide password',
// password input
show_password: 'Show password',
hide_password: 'Hide password',
// theme switcher
dark_theme: 'Toggle dark theme',
// theme switcher
dark_theme: 'Toggle dark theme',
// command palette
command_palette_prompt_default: 'Quickly find pages, actions, or commands',
// command palette
command_palette_prompt_default: 'Quickly find pages, actions, or commands',
};
export type Translations = typeof defaultTranslations;
let translations = $state<Translations>(defaultTranslations);
export const translate = <T extends keyof Translations>(
key: T,
overrides?: TranslationProps<T>,
): string => overrides?.[key] ?? translations[key];
export const translate = <T extends keyof Translations>(key: T, overrides?: TranslationProps<T>): string =>
overrides?.[key] ?? translations[key];
export const t = translate;
export const setTranslations = (newTranslations: Partial<Translations>) => {
translations = { ...defaultTranslations, ...newTranslations };
translations = { ...defaultTranslations, ...newTranslations };
};
+59 -75
View File
@@ -1,84 +1,68 @@
<script lang="ts">
import Heading from '$lib/components/Heading/Heading.svelte';
import Stack from '$lib/components/Stack/Stack.svelte';
import VStack from '$lib/components/Stack/VStack.svelte';
import Text from '$lib/components/Text/Text.svelte';
import ThemeSwitcher from '$lib/components/ThemeSwitcher/ThemeSwitcher.svelte';
import { Constants } from '$lib/site/constants.js';
import SiteFooterLink from '$lib/site/SiteFooterLink.svelte';
import {
mdiBookshelf,
mdiChartGantt,
mdiCubeOutline,
mdiHomeSearchOutline,
mdiKey,
mdiOfficeBuildingOutline,
mdiScriptTextOutline,
mdiServerOutline,
mdiShoppingOutline,
} from '@mdi/js';
import { siAndroid, siApple, siDiscord, siGithub, siReddit } from 'simple-icons';
import Heading from '$lib/components/Heading/Heading.svelte';
import Stack from '$lib/components/Stack/Stack.svelte';
import VStack from '$lib/components/Stack/VStack.svelte';
import Text from '$lib/components/Text/Text.svelte';
import ThemeSwitcher from '$lib/components/ThemeSwitcher/ThemeSwitcher.svelte';
import { Constants } from '$lib/site/constants.js';
import SiteFooterLink from '$lib/site/SiteFooterLink.svelte';
import {
mdiBookshelf,
mdiChartGantt,
mdiCubeOutline,
mdiHomeSearchOutline,
mdiKey,
mdiOfficeBuildingOutline,
mdiScriptTextOutline,
mdiServerOutline,
mdiShoppingOutline,
} from '@mdi/js';
import { siAndroid, siApple, siDiscord, siGithub, siReddit } from 'simple-icons';
</script>
<div class="bg-dark/10 mt-16 rounded-t-3xl p-8">
<div class="mx-auto max-w-(--breakpoint-lg) xl:py-8">
<Stack gap={8}>
<div class="place-center grid grid-cols-2 gap-8 md:grid-cols-3 lg:grid-cols-5">
<Stack>
<Heading size="tiny">Social</Heading>
<SiteFooterLink href={Constants.Socials.Github} icon={siGithub} text="GitHub" />
<SiteFooterLink href={Constants.Socials.Discord} icon={siDiscord} text="Discord" />
<SiteFooterLink href={Constants.Socials.Reddit} icon={siReddit} text="Reddit" />
</Stack>
<div class="mx-auto max-w-(--breakpoint-lg) xl:py-8">
<Stack gap={8}>
<div class="place-center grid grid-cols-2 gap-8 md:grid-cols-3 lg:grid-cols-5">
<Stack>
<Heading size="tiny">Social</Heading>
<SiteFooterLink href={Constants.Socials.Github} icon={siGithub} text="GitHub" />
<SiteFooterLink href={Constants.Socials.Discord} icon={siDiscord} text="Discord" />
<SiteFooterLink href={Constants.Socials.Reddit} icon={siReddit} text="Reddit" />
</Stack>
<Stack>
<Heading size="tiny">Download</Heading>
<SiteFooterLink href={Constants.Get.Android} icon={siAndroid} text="Android" />
<SiteFooterLink href={Constants.Get.iOS} icon={siApple} text="iOS" />
<SiteFooterLink
href={Constants.Get.DockerCompose}
icon={mdiServerOutline}
text="Server"
/>
</Stack>
<Stack>
<Heading size="tiny">Download</Heading>
<SiteFooterLink href={Constants.Get.Android} icon={siAndroid} text="Android" />
<SiteFooterLink href={Constants.Get.iOS} icon={siApple} text="iOS" />
<SiteFooterLink href={Constants.Get.DockerCompose} icon={mdiServerOutline} text="Server" />
</Stack>
<Stack>
<Heading size="tiny">Company</Heading>
<SiteFooterLink
href={Constants.Socials.Futo}
icon={mdiOfficeBuildingOutline}
text="FUTO"
/>
<SiteFooterLink href={Constants.Sites.Buy} icon={mdiKey} text="Purchase" />
<SiteFooterLink href={Constants.Sites.Store} icon={mdiShoppingOutline} text="Merch" />
</Stack>
<Stack>
<Heading size="tiny">Company</Heading>
<SiteFooterLink href={Constants.Socials.Futo} icon={mdiOfficeBuildingOutline} text="FUTO" />
<SiteFooterLink href={Constants.Sites.Buy} icon={mdiKey} text="Purchase" />
<SiteFooterLink href={Constants.Sites.Store} icon={mdiShoppingOutline} text="Merch" />
</Stack>
<Stack>
<Heading size="tiny">Sites</Heading>
<SiteFooterLink
href={Constants.Sites.Docs}
icon={mdiScriptTextOutline}
text="Documentation"
/>
<SiteFooterLink href={Constants.Sites.My} icon={mdiHomeSearchOutline} text="My Immich" />
<SiteFooterLink href={Constants.Sites.Api} icon={mdiCubeOutline} text="Immich API" />
</Stack>
<Stack>
<Heading size="tiny">Sites</Heading>
<SiteFooterLink href={Constants.Sites.Docs} icon={mdiScriptTextOutline} text="Documentation" />
<SiteFooterLink href={Constants.Sites.My} icon={mdiHomeSearchOutline} text="My Immich" />
<SiteFooterLink href={Constants.Sites.Api} icon={mdiCubeOutline} text="Immich API" />
</Stack>
<Stack>
<Heading size="tiny">Miscellaneous</Heading>
<SiteFooterLink href={Constants.Pages.Roadmap} icon={mdiChartGantt} text="Roadmap" />
<SiteFooterLink
href={Constants.Pages.CursedKnowledge}
icon={mdiBookshelf}
text="Cursed Knowledge"
/>
</Stack>
</div>
<VStack class="text-center">
<Text size="large">This project is available under GNU AGPL v3 license.</Text>
<Text color="muted" variant="italic">Privacy should not be a luxury</Text>
<ThemeSwitcher color="secondary" />
</VStack>
</Stack>
</div>
<Stack>
<Heading size="tiny">Miscellaneous</Heading>
<SiteFooterLink href={Constants.Pages.Roadmap} icon={mdiChartGantt} text="Roadmap" />
<SiteFooterLink href={Constants.Pages.CursedKnowledge} icon={mdiBookshelf} text="Cursed Knowledge" />
</Stack>
</div>
<VStack class="text-center">
<Text size="large">This project is available under GNU AGPL v3 license.</Text>
<Text color="muted" variant="italic">Privacy should not be a luxury</Text>
<ThemeSwitcher color="secondary" />
</VStack>
</Stack>
</div>
</div>
+14 -14
View File
@@ -1,21 +1,21 @@
<script lang="ts">
import HStack from '$lib/components/Stack/HStack.svelte';
import Icon from '$lib/components/Icon/Icon.svelte';
import Link from '$lib/components/Link/Link.svelte';
import Text from '$lib/components/Text/Text.svelte';
import HStack from '$lib/components/Stack/HStack.svelte';
import Icon from '$lib/components/Icon/Icon.svelte';
import Link from '$lib/components/Link/Link.svelte';
import Text from '$lib/components/Text/Text.svelte';
type Props = {
href: string;
icon: string | { path: string };
text: string;
};
type Props = {
href: string;
icon: string | { path: string };
text: string;
};
let { href, icon, text }: Props = $props();
let { href, icon, text }: Props = $props();
</script>
<Link {href} external>
<HStack>
<Icon {icon} size="1.5em" />
<Text>{text}</Text>
</HStack>
<HStack>
<Icon {icon} size="1.5em" />
<Text>{text}</Text>
</HStack>
</Link>
+113 -113
View File
@@ -2,122 +2,122 @@ import { asText } from '$lib/services/command-palette-manager.svelte.js';
import { mdiOpenInNew } from '@mdi/js';
export const Constants = {
Socials: {
Futo: 'https://futo.org/',
Github: 'https://github.com/immich-app/immich',
Discord: 'https://discord.immich.app/',
Reddit: 'https://www.reddit.com/r/immich/',
Weblate: 'https://hosted.weblate.org/projects/immich/immich/',
},
Get: {
iOS: 'https://get.immich.app/ios',
Android: 'https://get.immich.app/android',
FDroid: 'https://get.immich.app/fdroid',
GithubRelease: 'https://github.com/immich-app/immich/releases/latest',
DockerCompose: 'https://get.immich.app/docker-compose',
},
Socials: {
Futo: 'https://futo.org/',
Github: 'https://github.com/immich-app/immich',
Discord: 'https://discord.immich.app/',
Reddit: 'https://www.reddit.com/r/immich/',
Weblate: 'https://hosted.weblate.org/projects/immich/immich/',
},
Get: {
iOS: 'https://get.immich.app/ios',
Android: 'https://get.immich.app/android',
FDroid: 'https://get.immich.app/fdroid',
GithubRelease: 'https://github.com/immich-app/immich/releases/latest',
DockerCompose: 'https://get.immich.app/docker-compose',
},
Sites: {
Api: 'https://api.immich.app/',
Demo: 'https://demo.immich.app/',
My: 'https://my.immich.app/',
Buy: 'https://buy.immich.app/',
Get: 'https://get.immich.app/',
Docs: 'https://docs.immich.app/',
Store: 'https://immich.store/',
},
Pages: {
CursedKnowledge: 'https://immich.app/cursed-knowledge',
Roadmap: 'https://immich.app/roadmap',
},
Npm: {
Sdk: 'https://www.npmjs.com/package/@immich/sdk',
},
Sites: {
Api: 'https://api.immich.app/',
Demo: 'https://demo.immich.app/',
My: 'https://my.immich.app/',
Buy: 'https://buy.immich.app/',
Get: 'https://get.immich.app/',
Docs: 'https://docs.immich.app/',
Store: 'https://immich.store/',
},
Pages: {
CursedKnowledge: 'https://immich.app/cursed-knowledge',
Roadmap: 'https://immich.app/roadmap',
},
Npm: {
Sdk: 'https://www.npmjs.com/package/@immich/sdk',
},
};
export const siteCommands = [
{
title: 'Immich Documentation',
description: 'View the Immich documentation',
href: Constants.Sites.Docs,
},
{
title: 'Buy Immich',
description: 'Support Immich by buying a product key.',
href: Constants.Sites.Buy,
},
{
title: 'My Immich',
description: 'Immich link proxy to redirect to your personal instance',
href: Constants.Sites.My,
},
{
title: 'Get Immich',
description: 'View downloads links for Immich apps and server',
href: Constants.Sites.Get,
},
{
title: 'Immich on the PlayStore',
description: 'View Immich on the Google Play Store',
href: Constants.Get.Android,
},
{
title: 'Immich on the iOS App Store',
description: 'View Immich on the iOS App Store',
href: Constants.Get.iOS,
},
{
title: 'Immich Demo',
description: 'Test out Immich with our public demo server',
href: Constants.Sites.Demo,
},
{
title: 'Immich Store',
description: 'Support the project by purchasing Immich merchandise',
href: Constants.Sites.Store,
},
{
title: 'Cursed Knowledge',
description: 'View our collection of cursed knowledge',
href: Constants.Pages.CursedKnowledge,
},
{
title: 'Roadmap',
description: 'View our project roadmap',
href: Constants.Pages.Roadmap,
},
{
title: 'reddit',
description: 'Join the Immich community on reddit',
href: Constants.Socials.Reddit,
},
{
title: 'GitHub',
description: 'View our project on GitHub',
href: Constants.Socials.Github,
},
{
title: 'Discord',
description: 'Join the conversation on Discord',
href: Constants.Socials.Discord,
},
{
title: 'Weblate',
description: 'Support the project by translating Immich on Weblate',
href: Constants.Socials.Weblate,
},
{
title: 'FUTO',
description: 'Learn more about FUTO, the company behind Immich',
href: Constants.Sites.Docs,
},
{
title: 'Immich Documentation',
description: 'View the Immich documentation',
href: Constants.Sites.Docs,
},
{
title: 'Buy Immich',
description: 'Support Immich by buying a product key.',
href: Constants.Sites.Buy,
},
{
title: 'My Immich',
description: 'Immich link proxy to redirect to your personal instance',
href: Constants.Sites.My,
},
{
title: 'Get Immich',
description: 'View downloads links for Immich apps and server',
href: Constants.Sites.Get,
},
{
title: 'Immich on the PlayStore',
description: 'View Immich on the Google Play Store',
href: Constants.Get.Android,
},
{
title: 'Immich on the iOS App Store',
description: 'View Immich on the iOS App Store',
href: Constants.Get.iOS,
},
{
title: 'Immich Demo',
description: 'Test out Immich with our public demo server',
href: Constants.Sites.Demo,
},
{
title: 'Immich Store',
description: 'Support the project by purchasing Immich merchandise',
href: Constants.Sites.Store,
},
{
title: 'Cursed Knowledge',
description: 'View our collection of cursed knowledge',
href: Constants.Pages.CursedKnowledge,
},
{
title: 'Roadmap',
description: 'View our project roadmap',
href: Constants.Pages.Roadmap,
},
{
title: 'reddit',
description: 'Join the Immich community on reddit',
href: Constants.Socials.Reddit,
},
{
title: 'GitHub',
description: 'View our project on GitHub',
href: Constants.Socials.Github,
},
{
title: 'Discord',
description: 'Join the conversation on Discord',
href: Constants.Socials.Discord,
},
{
title: 'Weblate',
description: 'Support the project by translating Immich on Weblate',
href: Constants.Socials.Weblate,
},
{
title: 'FUTO',
description: 'Learn more about FUTO, the company behind Immich',
href: Constants.Sites.Docs,
},
].map((site) => ({
icon: mdiOpenInNew,
type: 'Link',
iconClass: 'text-indigo-700 dark:text-indigo-200',
title: site.title,
description: site.description,
href: site.href,
external: true,
text: asText('Site', 'Link', site.title, site.description, site.href),
icon: mdiOpenInNew,
type: 'Link',
iconClass: 'text-indigo-700 dark:text-indigo-200',
title: site.title,
description: site.description,
href: site.href,
external: true,
text: asText('Site', 'Link', site.title, site.description, site.href),
}));
+21 -21
View File
@@ -1,25 +1,25 @@
import { tv } from 'tailwind-variants';
export const styles = {
label: tv({
base: '',
variants: {
size: {
tiny: 'text-xs',
small: 'text-sm',
medium: 'text-base',
large: 'text-lg',
giant: 'text-xl',
},
color: {
muted: 'text-gray-600 dark:text-gray-400',
primary: 'text-primary',
secondary: 'text-dark',
success: 'text-success',
danger: 'text-danger',
warning: 'text-warning',
info: 'text-info',
},
},
}),
label: tv({
base: '',
variants: {
size: {
tiny: 'text-xs',
small: 'text-sm',
medium: 'text-base',
large: 'text-lg',
giant: 'text-xl',
},
color: {
muted: 'text-gray-600 dark:text-gray-400',
primary: 'text-primary',
secondary: 'text-dark',
success: 'text-success',
danger: 'text-danger',
warning: 'text-warning',
info: 'text-info',
},
},
}),
};
+40 -40
View File
@@ -1,53 +1,53 @@
@import 'tailwindcss';
@theme inline {
--color-primary: rgb(var(--immich-ui-primary));
--color-light: rgb(var(--immich-ui-light));
--color-dark: rgb(var(--immich-ui-dark));
--color-success: rgb(var(--immich-ui-success));
--color-danger: rgb(var(--immich-ui-danger));
--color-warning: rgb(var(--immich-ui-warning));
--color-info: rgb(var(--immich-ui-info));
--color-subtle: rgb(var(--immich-ui-gray));
--color-primary: rgb(var(--immich-ui-primary));
--color-light: rgb(var(--immich-ui-light));
--color-dark: rgb(var(--immich-ui-dark));
--color-success: rgb(var(--immich-ui-success));
--color-danger: rgb(var(--immich-ui-danger));
--color-warning: rgb(var(--immich-ui-warning));
--color-info: rgb(var(--immich-ui-info));
--color-subtle: rgb(var(--immich-ui-gray));
}
@custom-variant dark (&:where(.dark, .dark *):not(.light));
@layer base {
:root,
.light {
/* light */
--immich-ui-primary: 66 80 175;
--immich-ui-dark: 20 22 26;
--immich-ui-light: 255 255 255;
--immich-ui-success: 16 188 99;
--immich-ui-danger: 200 60 60;
--immich-ui-warning: 216 143 64;
--immich-ui-info: 8 111 230;
--immich-ui-gray: 246 246 246;
:root,
.light {
/* light */
--immich-ui-primary: 66 80 175;
--immich-ui-dark: 20 22 26;
--immich-ui-light: 255 255 255;
--immich-ui-success: 16 188 99;
--immich-ui-danger: 200 60 60;
--immich-ui-warning: 216 143 64;
--immich-ui-info: 8 111 230;
--immich-ui-gray: 246 246 246;
--immich-ui-default-border: 209 213 219;
}
--immich-ui-default-border: 209 213 219;
}
.dark {
/* dark */
--immich-ui-primary: 172 203 250;
--immich-ui-light: 0 0 0;
--immich-ui-dark: 229 231 235;
--immich-ui-success: 72 237 152;
--immich-ui-danger: 246 125 125;
--immich-ui-warning: 254 197 132;
--immich-ui-info: 121 183 254;
--immich-ui-gray: 33 33 33;
.dark {
/* dark */
--immich-ui-primary: 172 203 250;
--immich-ui-light: 0 0 0;
--immich-ui-dark: 229 231 235;
--immich-ui-success: 72 237 152;
--immich-ui-danger: 246 125 125;
--immich-ui-warning: 254 197 132;
--immich-ui-info: 121 183 254;
--immich-ui-gray: 33 33 33;
--immich-ui-default-border: 33 33 33;
}
--immich-ui-default-border: 33 33 33;
}
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: rgb(var(--immich-ui-default-border));
}
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: rgb(var(--immich-ui-default-border));
}
}
+93 -93
View File
@@ -1,11 +1,11 @@
import type { Translations } from '$lib/services/translation.svelte.js';
import type { Snippet } from 'svelte';
import type {
HTMLAnchorAttributes,
HTMLButtonAttributes,
HTMLInputAttributes,
HTMLLabelAttributes,
HTMLTextareaAttributes,
HTMLAnchorAttributes,
HTMLButtonAttributes,
HTMLInputAttributes,
HTMLLabelAttributes,
HTMLTextareaAttributes,
} from 'svelte/elements';
export type Color = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info';
@@ -23,8 +23,8 @@ export type Variants = 'filled' | 'outline' | 'ghost';
export type Gap = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
export enum Theme {
Light = 'light',
Dark = 'dark',
Light = 'light',
Dark = 'dark',
}
export type TranslationProps<T extends keyof Translations> = { [K in T]?: string };
@@ -32,145 +32,145 @@ export type TranslationProps<T extends keyof Translations> = { [K in T]?: string
export type IconLike = string | { path: string };
export type IconProps = {
icon: IconLike;
title?: string;
description?: string;
size?: string;
color?: Color | 'currentColor' | string;
flipped?: boolean;
flopped?: boolean;
spin?: boolean;
class?: string;
viewBox?: string;
strokeWidth?: number;
strokeColor?: string;
icon: IconLike;
title?: string;
description?: string;
size?: string;
color?: Color | 'currentColor' | string;
flipped?: boolean;
flopped?: boolean;
spin?: boolean;
class?: string;
viewBox?: string;
strokeWidth?: number;
strokeColor?: string;
};
type ButtonOrAnchor =
| ({ href?: never; external?: never } & HTMLButtonAttributes)
| ({ href: string; external?: boolean } & HTMLAnchorAttributes);
| ({ href?: never; external?: never } & HTMLButtonAttributes)
| ({ href: string; external?: boolean } & HTMLAnchorAttributes);
type ButtonBase = {
size?: Size;
variant?: Variants;
class?: string;
color?: Color;
shape?: Shape;
size?: Size;
variant?: Variants;
class?: string;
color?: Color;
shape?: Shape;
};
export type ButtonProps = ButtonBase & {
ref?: HTMLElement | null;
fullWidth?: boolean;
loading?: boolean;
leadingIcon?: IconLike;
trailingIcon?: IconLike;
ref?: HTMLElement | null;
fullWidth?: boolean;
loading?: boolean;
leadingIcon?: IconLike;
trailingIcon?: IconLike;
} & ButtonOrAnchor;
export type CloseButtonProps = {
size?: Size;
variant?: Variants;
class?: string;
translations?: TranslationProps<'close'>;
size?: Size;
variant?: Variants;
class?: string;
translations?: TranslationProps<'close'>;
} & ButtonOrAnchor;
export type IconButtonProps = ButtonBase & {
icon: string;
flipped?: boolean;
flopped?: boolean;
'aria-label': string;
icon: string;
flipped?: boolean;
flopped?: boolean;
'aria-label': string;
} & ButtonOrAnchor;
type StackBaseProps = {
class?: string;
children: Snippet;
gap?: Gap;
wrap?: boolean;
fullWidth?: boolean;
fullHeight?: boolean;
class?: string;
children: Snippet;
gap?: Gap;
wrap?: boolean;
fullWidth?: boolean;
fullHeight?: boolean;
};
export type ChildData = {
snippet: Snippet;
class?: string;
snippet: Snippet;
class?: string;
};
export type StackProps = StackBaseProps & {
align?: 'start' | 'center' | 'end';
direction?: 'row' | 'column';
align?: 'start' | 'center' | 'end';
direction?: 'row' | 'column';
};
export type HStackProps = StackBaseProps;
export type VStackProps = StackBaseProps;
export type LabelProps = {
label?: string;
class?: string;
size?: Size;
color?: TextColor;
children?: Snippet;
label?: string;
class?: string;
size?: Size;
color?: TextColor;
children?: Snippet;
} & HTMLLabelAttributes;
export type FieldContext = {
label?: string;
description?: string;
invalid?: boolean;
disabled?: boolean;
required?: boolean;
readOnly?: boolean;
label?: string;
description?: string;
invalid?: boolean;
disabled?: boolean;
required?: boolean;
readOnly?: boolean;
} & LabelProps;
type BaseInputProps = {
ref?: HTMLInputElement | null;
class?: string;
value?: string;
size?: Size;
shape?: Shape;
inputSize?: HTMLInputAttributes['size'];
ref?: HTMLInputElement | null;
class?: string;
value?: string;
size?: Size;
shape?: Shape;
inputSize?: HTMLInputAttributes['size'];
} & Omit<HTMLInputAttributes, 'size' | 'type'>;
export type InputProps = BaseInputProps & {
containerRef?: HTMLElement | null;
type?: HTMLInputAttributes['type'];
leadingIcon?: IconLike | Snippet;
trailingIcon?: IconLike | Snippet;
trailingText?: string;
containerRef?: HTMLElement | null;
type?: HTMLInputAttributes['type'];
leadingIcon?: IconLike | Snippet;
trailingIcon?: IconLike | Snippet;
trailingText?: string;
};
export type PasswordInputProps = BaseInputProps & {
ref?: HTMLInputElement | null;
translations?: TranslationProps<'show_password' | 'hide_password'>;
isVisible?: boolean;
ref?: HTMLInputElement | null;
translations?: TranslationProps<'show_password' | 'hide_password'>;
isVisible?: boolean;
};
export type TextareaProps = {
ref?: HTMLTextAreaElement | null;
containerRef?: HTMLElement | null;
class?: string;
value?: string;
size?: Size;
shape?: Shape;
grow?: boolean;
ref?: HTMLTextAreaElement | null;
containerRef?: HTMLElement | null;
class?: string;
value?: string;
size?: Size;
shape?: Shape;
grow?: boolean;
} & HTMLTextareaAttributes;
export type SelectItem = {
label?: string;
value: string;
disabled?: boolean;
label?: string;
value: string;
disabled?: boolean;
};
export type SelectCommonProps<T extends SelectItem> = {
data: string[] | T[];
size?: Size;
shape?: Shape;
placeholder?: string;
class?: string;
data: string[] | T[];
size?: Size;
shape?: Shape;
placeholder?: string;
class?: string;
};
export type SelectProps<T extends SelectItem> = SelectCommonProps<T> & {
value?: T;
onChange?: (value: T) => void;
value?: T;
onChange?: (value: T) => void;
};
export type MultiSelectProps<T extends SelectItem> = SelectCommonProps<T> & {
values?: T[];
onChange?: (values: T[]) => void;
values?: T[];
onChange?: (values: T[]) => void;
};
+14 -25
View File
@@ -1,22 +1,14 @@
export const enum ByteUnit {
'B' = 'B',
'KiB' = 'KiB',
'MiB' = 'MiB',
'GiB' = 'GiB',
'TiB' = 'TiB',
'PiB' = 'PiB',
'EiB' = 'EiB',
'B' = 'B',
'KiB' = 'KiB',
'MiB' = 'MiB',
'GiB' = 'GiB',
'TiB' = 'TiB',
'PiB' = 'PiB',
'EiB' = 'EiB',
}
const byteUnits = [
ByteUnit.B,
ByteUnit.KiB,
ByteUnit.MiB,
ByteUnit.GiB,
ByteUnit.TiB,
ByteUnit.PiB,
ByteUnit.EiB,
];
const byteUnits = [ByteUnit.B, ByteUnit.KiB, ByteUnit.MiB, ByteUnit.GiB, ByteUnit.TiB, ByteUnit.PiB, ByteUnit.EiB];
/**
* Convert bytes to best human readable unit and number of that unit.
@@ -29,12 +21,9 @@ const byteUnits = [
* @returns size (number) and unit (string)
*/
export function getBytesWithUnit(bytes: number, maxPrecision = 1): [number, ByteUnit] {
const magnitude = Math.floor(Math.log(bytes === 0 ? 1 : bytes) / Math.log(1024));
const magnitude = Math.floor(Math.log(bytes === 0 ? 1 : bytes) / Math.log(1024));
return [
Number.parseFloat((bytes / 1024 ** magnitude).toFixed(maxPrecision)),
byteUnits[magnitude],
];
return [Number.parseFloat((bytes / 1024 ** magnitude).toFixed(maxPrecision)), byteUnits[magnitude]];
}
/**
@@ -49,8 +38,8 @@ export function getBytesWithUnit(bytes: number, maxPrecision = 1): [number, Byte
* @returns localized bytes with unit as string
*/
export function getByteUnitString(bytes: number, locale?: string, maxPrecision = 1): string {
const [size, unit] = getBytesWithUnit(bytes, maxPrecision);
return `${size.toLocaleString(locale)} ${unit}`;
const [size, unit] = getBytesWithUnit(bytes, maxPrecision);
return `${size.toLocaleString(locale)} ${unit}`;
}
/**
@@ -63,7 +52,7 @@ export function getByteUnitString(bytes: number, locale?: string, maxPrecision =
* @returns bytes (number)
*/
export function convertToBytes(size: number, unit: ByteUnit): number {
return size * 1024 ** byteUnits.indexOf(unit);
return size * 1024 ** byteUnits.indexOf(unit);
}
/**
@@ -76,5 +65,5 @@ export function convertToBytes(size: number, unit: ByteUnit): number {
* @returns bytes (number)
*/
export function convertFromBytes(bytes: number, unit: ByteUnit): number {
return bytes / 1024 ** byteUnits.indexOf(unit);
return bytes / 1024 ** byteUnits.indexOf(unit);
}
+11 -11
View File
@@ -2,17 +2,17 @@ import type { IconLike } from '$lib/types.js';
import { twMerge } from 'tailwind-merge';
export const cleanClass = (...classNames: unknown[]) => {
return twMerge(
classNames
.filter((className) => {
if (!className || typeof className === 'boolean') {
return false;
}
return twMerge(
classNames
.filter((className) => {
if (!className || typeof className === 'boolean') {
return false;
}
return typeof className === 'string';
})
.join(' '),
);
return typeof className === 'string';
})
.join(' '),
);
};
export const withPrefix = (key: string) => `immich-ui-${key}`;
@@ -21,5 +21,5 @@ let _count = 0;
export const generateId = (): string => `ui-id-${_count++}`;
export const isIconLike = (icon: unknown): icon is IconLike => {
return typeof icon === 'string' || !!(icon && typeof icon === 'object' && 'path' in icon);
return typeof icon === 'string' || !!(icon && typeof icon === 'object' && 'path' in icon);
};
+120 -128
View File
@@ -1,145 +1,137 @@
<script lang="ts">
import { beforeNavigate } from '$app/navigation';
import { page } from '$app/state';
import Navbar from '$docs/components/Navbar.svelte';
import { componentGroups } from '$docs/constants.js';
import { asComponentHref } from '$docs/utilities.js';
import {
AppShell,
AppShellHeader,
AppShellSidebar,
asText,
CommandPalette,
commandPaletteManager,
IconButton,
initializeTheme,
Input,
NavbarGroup,
NavbarItem,
siteCommands,
SiteFooter,
theme,
ThemeSwitcher,
toggleTheme,
} from '@immich/ui';
import '@immich/ui/theme/default.css';
import {
mdiHome,
mdiHomeOutline,
mdiMagnify,
mdiSlashForwardBox,
mdiThemeLightDark,
} from '@mdi/js';
import { MediaQuery } from 'svelte/reactivity';
import '../app.css';
import { beforeNavigate } from '$app/navigation';
import { page } from '$app/state';
import Navbar from '$docs/components/Navbar.svelte';
import { componentGroups } from '$docs/constants.js';
import { asComponentHref } from '$docs/utilities.js';
import {
AppShell,
AppShellHeader,
AppShellSidebar,
asText,
CommandPalette,
commandPaletteManager,
IconButton,
initializeTheme,
Input,
NavbarGroup,
NavbarItem,
siteCommands,
SiteFooter,
theme,
ThemeSwitcher,
toggleTheme,
} from '@immich/ui';
import '@immich/ui/theme/default.css';
import { mdiHome, mdiHomeOutline, mdiMagnify, mdiSlashForwardBox, mdiThemeLightDark } from '@mdi/js';
import { MediaQuery } from 'svelte/reactivity';
import '../app.css';
initializeTheme();
initializeTheme();
let { children } = $props();
let { children } = $props();
const sidebar = new MediaQuery(`min-width: 850px`);
let open = $derived(sidebar.current);
const sidebar = new MediaQuery(`min-width: 850px`);
let open = $derived(sidebar.current);
beforeNavigate(() => {
if (!sidebar.current) {
open = false;
}
});
beforeNavigate(() => {
if (!sidebar.current) {
open = false;
}
});
commandPaletteManager.reset();
commandPaletteManager.reset();
// links
commandPaletteManager.addCommands(siteCommands);
// links
commandPaletteManager.addCommands(siteCommands);
// commands
commandPaletteManager.addCommands([
{
icon: mdiThemeLightDark,
iconClass: 'text-gray-700 dark:text-gray-200',
type: 'Command',
title: 'Toggle theme',
description: 'Switch between light and dark theme',
action: () => toggleTheme(),
text: asText('Command', 'light', 'dark', 'theme', 'toggle'),
},
]);
// commands
commandPaletteManager.addCommands([
{
icon: mdiThemeLightDark,
iconClass: 'text-gray-700 dark:text-gray-200',
type: 'Command',
title: 'Toggle theme',
description: 'Switch between light and dark theme',
action: () => toggleTheme(),
text: asText('Command', 'light', 'dark', 'theme', 'toggle'),
},
]);
// components
for (const group of componentGroups) {
commandPaletteManager.addCommands(
group.components.map((component) => ({
icon: component.icon,
iconClass: '',
type: 'Component',
title: component.name,
description: `View the ${component.name} component`,
href: asComponentHref(component.name),
text: asText('Component', group.title, component.name),
})),
);
}
// components
for (const group of componentGroups) {
commandPaletteManager.addCommands(
group.components.map((component) => ({
icon: component.icon,
iconClass: '',
type: 'Component',
title: component.name,
description: `View the ${component.name} component`,
href: asComponentHref(component.name),
text: asText('Component', group.title, component.name),
})),
);
}
commandPaletteManager.enable();
commandPaletteManager.enable();
</script>
<AppShell>
<AppShellHeader>
<Navbar theme={theme.value} onToggleSidebar={() => (open = !open)}>
{#if commandPaletteManager.isEnabled}
<div class="hidden max-w-40 place-items-center p-1 lg:flex">
<Input
onfocus={() => commandPaletteManager.open()}
leadingIcon={mdiMagnify}
placeholder="Search..."
class="bg-subtle! rounded-full border px-2 py-2"
trailingIcon={mdiSlashForwardBox}
/>
</div>
<IconButton
icon={mdiMagnify}
shape="round"
variant="ghost"
color="secondary"
aria-label="Search"
class="lg:hidden"
onclick={() => commandPaletteManager.open()}
/>
{/if}
<ThemeSwitcher size="medium" />
</Navbar>
</AppShellHeader>
<AppShellHeader>
<Navbar theme={theme.value} onToggleSidebar={() => (open = !open)}>
{#if commandPaletteManager.isEnabled}
<div class="hidden max-w-40 place-items-center p-1 lg:flex">
<Input
onfocus={() => commandPaletteManager.open()}
leadingIcon={mdiMagnify}
placeholder="Search..."
class="bg-subtle! rounded-full border px-2 py-2"
trailingIcon={mdiSlashForwardBox}
/>
</div>
<IconButton
icon={mdiMagnify}
shape="round"
variant="ghost"
color="secondary"
aria-label="Search"
class="lg:hidden"
onclick={() => commandPaletteManager.open()}
/>
{/if}
<ThemeSwitcher size="medium" />
</Navbar>
</AppShellHeader>
<AppShellSidebar bind:open>
<div class="my-4 me-4">
<NavbarItem
title="Home"
icon={mdiHomeOutline}
activeIcon={mdiHome}
href="/"
isActive={() => page.url.pathname === '/'}
/>
{#each componentGroups as group (group.title)}
<NavbarGroup title={group.title} />
{#each group.components as component (component.name)}
<NavbarItem
href={asComponentHref(component.name)}
title={component.name}
icon={component.icon}
activeIcon={component.activeIcon}
/>
{/each}
{/each}
</div>
</AppShellSidebar>
<AppShellSidebar bind:open>
<div class="my-4 me-4">
<NavbarItem
title="Home"
icon={mdiHomeOutline}
activeIcon={mdiHome}
href="/"
isActive={() => page.url.pathname === '/'}
/>
{#each componentGroups as group (group.title)}
<NavbarGroup title={group.title} />
{#each group.components as component (component.name)}
<NavbarItem
href={asComponentHref(component.name)}
title={component.name}
icon={component.icon}
activeIcon={component.activeIcon}
/>
{/each}
{/each}
</div>
</AppShellSidebar>
<section class="flex h-full flex-col">
<div class="grow">
{@render children?.()}
</div>
<SiteFooter />
</section>
<section class="flex h-full flex-col">
<div class="grow">
{@render children?.()}
</div>
<SiteFooter />
</section>
</AppShell>
<CommandPalette
translations={{ command_palette_prompt_default: 'Quickly find components, links, and commands' }}
/>
<CommandPalette translations={{ command_palette_prompt_default: 'Quickly find components, links, and commands' }} />
+27 -28
View File
@@ -1,35 +1,34 @@
<script lang="ts">
import ComponentCard from '$docs/components/ComponentCard.svelte';
import Grid from '$docs/components/Grid.svelte';
import { componentGroups } from '$docs/constants.js';
import { Code, Container, Heading, Link, Stack, Text } from '@immich/ui';
import ComponentCard from '$docs/components/ComponentCard.svelte';
import Grid from '$docs/components/Grid.svelte';
import { componentGroups } from '$docs/constants.js';
import { Code, Container, Heading, Link, Stack, Text } from '@immich/ui';
</script>
<Container size="large" class="p-2">
<Stack class="flex flex-col gap-4 px-8 py-8" gap={8}>
<Heading tag="h1" size="title">@immich/ui</Heading>
<Text>
@immich/ui is a collection of <Link href="https://svelte.dev">Svelte</Link> components that are
shared across all Immich projects. It is designed to be a simple and easy-to-use library that provides
a consistent look and feel.
</Text>
<Stack class="flex flex-col gap-4 px-8 py-8" gap={8}>
<Heading tag="h1" size="title">@immich/ui</Heading>
<Text>
@immich/ui is a collection of <Link href="https://svelte.dev">Svelte</Link> components that are shared across all Immich
projects. It is designed to be a simple and easy-to-use library that provides a consistent look and feel.
</Text>
<Heading tag="h2" size="large">Install</Heading>
<Text>@immich/ui is published as an npm package. You can install it using npm.</Text>
<Code color="primary" variant="filled">npm i --save-dev @immich/ui</Code>
<Heading tag="h2" size="large">Install</Heading>
<Text>@immich/ui is published as an npm package. You can install it using npm.</Text>
<Code color="primary" variant="filled">npm i --save-dev @immich/ui</Code>
<Heading tag="h2" size="large">Components</Heading>
<Stack gap={8}>
{#each componentGroups as group (group.title)}
<Stack>
<Heading tag="h3" size="medium" class="mb-4">{group.title}</Heading>
<Grid>
{#each group.components as component (component.name)}
<ComponentCard {component} />
{/each}
</Grid>
</Stack>
{/each}
</Stack>
</Stack>
<Heading tag="h2" size="large">Components</Heading>
<Stack gap={8}>
{#each componentGroups as group (group.title)}
<Stack>
<Heading tag="h3" size="medium" class="mb-4">{group.title}</Heading>
<Grid>
{#each group.components as component (component.name)}
<ComponentCard {component} />
{/each}
</Grid>
</Stack>
{/each}
</Stack>
</Stack>
</Container>
+18 -18
View File
@@ -1,23 +1,23 @@
<script lang="ts">
import ComponentExamples from '$docs/components/ComponentExamples.svelte';
import ComponentPage from '$docs/components/ComponentPage.svelte';
import BasicExample from './BasicExample.svelte';
import basicExample from './BasicExample.svelte?raw';
import ColorExample from './ColorExample.svelte';
import colorExample from './ColorExample.svelte?raw';
import CustomIconExample from './CustomIconExample.svelte';
import customIconExample from './CustomIconExample.svelte?raw';
import EventExample from './EventExample.svelte';
import eventExample from './EventExample.svelte?raw';
import ComponentExamples from '$docs/components/ComponentExamples.svelte';
import ComponentPage from '$docs/components/ComponentPage.svelte';
import BasicExample from './BasicExample.svelte';
import basicExample from './BasicExample.svelte?raw';
import ColorExample from './ColorExample.svelte';
import colorExample from './ColorExample.svelte?raw';
import CustomIconExample from './CustomIconExample.svelte';
import customIconExample from './CustomIconExample.svelte?raw';
import EventExample from './EventExample.svelte';
import eventExample from './EventExample.svelte?raw';
</script>
<ComponentPage name="Alert">
<ComponentExamples
examples={[
{ title: 'Basic', code: basicExample, component: BasicExample },
{ title: 'Color', code: colorExample, component: ColorExample },
{ title: 'Custom Icon', code: customIconExample, component: CustomIconExample },
{ title: 'Event', code: eventExample, component: EventExample },
]}
/>
<ComponentExamples
examples={[
{ title: 'Basic', code: basicExample, component: BasicExample },
{ title: 'Color', code: colorExample, component: ColorExample },
{ title: 'Custom Icon', code: customIconExample, component: CustomIconExample },
{ title: 'Event', code: eventExample, component: EventExample },
]}
/>
</ComponentPage>

Some files were not shown because too many files have changed in this diff Show More