mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-21 01:11:33 +03:00
feat: modernize ui (#381)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
@@ -1,23 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { cn } from '$lib/utils/style';
|
||||
import { LucideChevronDown } from 'lucide-svelte';
|
||||
import { LucideChevronDown, type Icon as IconType } from 'lucide-svelte';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { Button } from './ui/button';
|
||||
import * as Card from './ui/card';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let {
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
defaultExpanded = false,
|
||||
icon,
|
||||
children
|
||||
}: {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
defaultExpanded?: boolean;
|
||||
icon?: typeof IconType;
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
|
||||
@@ -51,7 +53,12 @@
|
||||
<Card.Header class="cursor-pointer" onclick={toggleExpanded}>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>{title}</Card.Title>
|
||||
<Card.Title class="flex items-center gap-2 text-xl font-semibold">
|
||||
{#if icon}{@const Icon = icon}
|
||||
<Icon class="text-primary/80 h-5 w-5" />
|
||||
{/if}
|
||||
{title}
|
||||
</Card.Title>
|
||||
{#if description}
|
||||
<Card.Description>{description}</Card.Description>
|
||||
{/if}
|
||||
@@ -68,7 +75,7 @@
|
||||
</Card.Header>
|
||||
{#if expanded}
|
||||
<div transition:slide={{ duration: 200 }}>
|
||||
<Card.Content>
|
||||
<Card.Content class="bg-muted/20 pt-5">
|
||||
{@render children()}
|
||||
</Card.Content>
|
||||
</div>
|
||||
|
||||
59
frontend/src/lib/components/fade-wrapper.svelte
Normal file
59
frontend/src/lib/components/fade-wrapper.svelte
Normal file
@@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let {
|
||||
delay = 50,
|
||||
stagger = 150,
|
||||
children
|
||||
}: {
|
||||
delay?: number;
|
||||
stagger?: number;
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
|
||||
let containerNode: HTMLElement;
|
||||
|
||||
$effect(() => {
|
||||
page.route;
|
||||
applyAnimationDelays();
|
||||
});
|
||||
|
||||
function applyAnimationDelays() {
|
||||
if (containerNode) {
|
||||
const childNodes = Array.from(containerNode.children);
|
||||
childNodes.forEach((child, index) => {
|
||||
// Skip comment nodes and text nodes
|
||||
if (child.nodeType === 1) {
|
||||
const itemDelay = delay + index * stagger;
|
||||
(child as HTMLElement).style.setProperty('animation-delay', `${itemDelay}ms`);
|
||||
console.log(itemDelay);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<style>
|
||||
/* Base styles */
|
||||
.fade-wrapper {
|
||||
display: contents;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Apply these styles to all children */
|
||||
.fade-wrapper > * {
|
||||
animation-fill-mode: both;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
animation-delay: calc(var(--animation-delay, 0ms) + 0.1s);
|
||||
animation: fadeIn 0.8s ease-out forwards;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
</style>
|
||||
</svelte:head>
|
||||
|
||||
<div class="fade-wrapper" bind:this={containerNode}>
|
||||
{@render children()}
|
||||
</div>
|
||||
@@ -5,6 +5,7 @@
|
||||
import { LucideLoader, LucideRefreshCw, LucideUpload } from 'lucide-svelte';
|
||||
import { openConfirmDialog } from '../confirm-dialog';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import type UserService from '$lib/services/user-service';
|
||||
|
||||
let {
|
||||
userId,
|
||||
@@ -34,7 +35,7 @@
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
await updateCallback(file).catch(() => {
|
||||
imageDataURL = `/api/users/${userId}/profile-picture.png}`;
|
||||
imageDataURL = `/api/users/${userId}/profile-picture.png`;
|
||||
});
|
||||
isLoading = false;
|
||||
}
|
||||
@@ -55,62 +56,53 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex gap-5">
|
||||
<div class="flex w-full flex-col justify-between gap-5 sm:flex-row">
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold">{m.profile_picture()}</h3>
|
||||
{#if isLdapUser}
|
||||
<p class="text-muted-foreground mt-1 text-sm">
|
||||
{m.profile_picture_is_managed_by_ldap_server()}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-muted-foreground mt-1 text-sm">
|
||||
{m.click_profile_picture_to_upload_custom()}
|
||||
</p>
|
||||
<p class="text-muted-foreground mt-1 text-sm">{m.image_should_be_in_format()}</p>
|
||||
{/if}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="mt-5"
|
||||
on:click={onReset}
|
||||
disabled={isLoading || isLdapUser}
|
||||
>
|
||||
<LucideRefreshCw class="mr-2 h-4 w-4" />
|
||||
{m.reset_to_default()}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-6 sm:flex-row">
|
||||
<div class="shrink-0">
|
||||
{#if isLdapUser}
|
||||
<Avatar.Root class="h-24 w-24">
|
||||
<Avatar.Image class="object-cover" src={imageDataURL} />
|
||||
</Avatar.Root>
|
||||
{:else}
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<FileInput
|
||||
id="profile-picture-input"
|
||||
variant="secondary"
|
||||
accept="image/png, image/jpeg"
|
||||
onchange={onImageChange}
|
||||
>
|
||||
<div class="group relative h-28 w-28 rounded-full">
|
||||
<Avatar.Root class="h-full w-full transition-opacity duration-200">
|
||||
<Avatar.Image
|
||||
class="object-cover group-hover:opacity-10 {isLoading ? 'opacity-10' : ''}"
|
||||
src={imageDataURL}
|
||||
/>
|
||||
</Avatar.Root>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
{#if isLoading}
|
||||
<LucideLoader class="h-5 w-5 animate-spin" />
|
||||
{:else}
|
||||
<LucideUpload
|
||||
class="h-5 w-5 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<FileInput
|
||||
id="profile-picture-input"
|
||||
variant="secondary"
|
||||
accept="image/png, image/jpeg"
|
||||
onchange={onImageChange}
|
||||
>
|
||||
<div class="group relative h-24 w-24 rounded-full">
|
||||
<Avatar.Root class="h-full w-full transition-opacity duration-200">
|
||||
<Avatar.Image
|
||||
class="object-cover group-hover:opacity-30 {isLoading ? 'opacity-30' : ''}"
|
||||
src={imageDataURL}
|
||||
/>
|
||||
</Avatar.Root>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
{#if isLoading}
|
||||
<LucideLoader class="h-5 w-5 animate-spin" />
|
||||
{:else}
|
||||
<LucideUpload class="h-5 w-5 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
{/if}
|
||||
</div>
|
||||
</FileInput>
|
||||
</div>
|
||||
</div>
|
||||
</FileInput>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grow">
|
||||
<h3 class="font-medium">{m.profile_picture()}</h3>
|
||||
{#if isLdapUser}
|
||||
<p class="text-muted-foreground text-sm">
|
||||
{m.profile_picture_is_managed_by_ldap_server()}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-muted-foreground text-sm">
|
||||
{m.click_profile_picture_to_upload_custom()}
|
||||
</p>
|
||||
<p class="text-muted-foreground mb-2 text-sm">{m.image_should_be_in_format()}</p>
|
||||
<Button variant="outline" size="sm" on:click={onReset} disabled={isLoading || isLdapUser}>
|
||||
<LucideRefreshCw class="mr-2 h-4 w-4" />
|
||||
{m.reset_to_default()}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
75
frontend/src/lib/components/glass-row-item.svelte
Normal file
75
frontend/src/lib/components/glass-row-item.svelte
Normal file
@@ -0,0 +1,75 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '$lib/components/ui/tooltip';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { LucideCalendar, LucidePencil, LucideTrash, type Icon as IconType } from 'lucide-svelte';
|
||||
|
||||
let {
|
||||
icon,
|
||||
onRename,
|
||||
onDelete,
|
||||
label,
|
||||
description
|
||||
}: {
|
||||
icon: typeof IconType;
|
||||
onRename: () => void;
|
||||
onDelete: () => void;
|
||||
description?: string;
|
||||
label?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="bg-card hover:bg-muted/50 group rounded-lg p-3 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="bg-primary/10 text-primary mt-1 rounded-lg p-2">
|
||||
{#if icon}{@const Icon = icon}
|
||||
<Icon class="h-5 w-5" />
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="font-medium">{label}</p>
|
||||
</div>
|
||||
{#if description}
|
||||
<div class="text-muted-foreground mt-1 flex items-center text-xs">
|
||||
<LucideCalendar class="mr-1 h-3 w-3" />
|
||||
{description}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
on:click={onRename}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="h-8 w-8"
|
||||
aria-label={m.rename()}
|
||||
>
|
||||
<LucidePencil class="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{m.rename()}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
on:click={onDelete}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="hover:bg-destructive/10 hover:text-destructive h-8 w-8"
|
||||
aria-label={m.delete()}
|
||||
>
|
||||
<LucideTrash class="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{m.delete()}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -20,10 +20,15 @@
|
||||
>
|
||||
<div class="flex h-16 items-center">
|
||||
{#if !isAuthPage}
|
||||
<Logo class="mr-3 h-8 w-8" />
|
||||
<h1 class="text-lg font-medium" data-testid="application-name">
|
||||
{$appConfigStore.appName}
|
||||
</h1>
|
||||
<a
|
||||
href="/settings/account"
|
||||
class="flex items-center gap-3 transition-opacity hover:opacity-80"
|
||||
>
|
||||
<Logo class="h-8 w-8" />
|
||||
<h1 class="text-lg font-semibold tracking-tight" data-testid="application-name">
|
||||
{$appConfigStore.appName}
|
||||
</h1>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
|
||||
@@ -1,34 +1,46 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { cn } from '$lib/utils/style';
|
||||
import type { Snippet } from 'svelte';
|
||||
import * as Card from './ui/card';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let {
|
||||
children,
|
||||
showAlternativeSignInMethodButton = false
|
||||
showAlternativeSignInMethodButton = false,
|
||||
animate = false
|
||||
}: {
|
||||
children: Snippet;
|
||||
showAlternativeSignInMethodButton?: boolean;
|
||||
animate?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<!-- Desktop -->
|
||||
<div class="hidden h-screen items-center text-center lg:flex">
|
||||
<div class="h-full min-w-[650px] p-16 {showAlternativeSignInMethodButton ? 'pb-0' : ''}">
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="flex flex-grow flex-col items-center justify-center">
|
||||
<!-- Desktop with sliding reveal animation -->
|
||||
<div class="hidden h-screen items-center overflow-hidden text-center lg:flex">
|
||||
<!-- Content area that fades in after background slides -->
|
||||
<div
|
||||
class="relative z-10 flex h-full w-[650px] p-16 {cn(
|
||||
showAlternativeSignInMethodButton && 'pb-0',
|
||||
animate && 'animate-delayed-fade'
|
||||
)}"
|
||||
>
|
||||
<div class="flex h-full w-full flex-col overflow-hidden">
|
||||
<div class="relative flex flex-grow flex-col items-center justify-center overflow-auto">
|
||||
{@render children()}
|
||||
</div>
|
||||
{#if showAlternativeSignInMethodButton}
|
||||
<div class="mb-4 flex justify-center">
|
||||
<div
|
||||
class="mb-4 flex items-center justify-center"
|
||||
style={animate ? 'animation-delay: 1000ms;' : ''}
|
||||
>
|
||||
<a
|
||||
href={page.url.pathname == '/login'
|
||||
? '/login/alternative'
|
||||
: `/login/alternative?redirect=${encodeURIComponent(
|
||||
page.url.pathname + page.url.search
|
||||
)}`}
|
||||
class="text-muted-foreground text-xs"
|
||||
class="text-muted-foreground text-xs transition-colors hover:underline"
|
||||
>
|
||||
{m.dont_have_access_to_your_passkey()}
|
||||
</a>
|
||||
@@ -36,18 +48,22 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
src="/api/application-configuration/background-image"
|
||||
class="h-screen w-[calc(100vw-650px)] rounded-l-[60px] object-cover"
|
||||
alt={m.login_background()}
|
||||
/>
|
||||
|
||||
<!-- Background image with slide animation -->
|
||||
<div class="{cn(animate && 'animate-slide-bg-container')} absolute bottom-0 right-0 top-0 z-0">
|
||||
<img
|
||||
src="/api/application-configuration/background-image"
|
||||
class="h-screen rounded-l-[60px] object-cover {animate ? 'w-full' : 'w-[calc(100vw-650px)]'}"
|
||||
alt={m.login_background()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile -->
|
||||
<div
|
||||
class="flex h-screen items-center justify-center bg-[url('/api/application-configuration/background-image')] bg-cover bg-center text-center lg:hidden"
|
||||
>
|
||||
<Card.Root class="mx-3">
|
||||
<Card.Root class="mx-3 w-full max-w-md" style={animate ? 'animation-delay: 200ms;' : ''}>
|
||||
<Card.CardContent
|
||||
class="px-4 py-10 sm:p-10 {showAlternativeSignInMethodButton ? 'pb-3 sm:pb-3' : ''}"
|
||||
>
|
||||
@@ -59,7 +75,7 @@
|
||||
: `/login/alternative?redirect=${encodeURIComponent(
|
||||
page.url.pathname + page.url.search
|
||||
)}`}
|
||||
class="text-muted-foreground mt-7 flex justify-center text-xs"
|
||||
class="text-muted-foreground mt-7 flex justify-center text-xs transition-colors hover:underline"
|
||||
>
|
||||
{m.dont_have_access_to_your_passkey()}
|
||||
</a>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { cn } from '$lib/utils/style.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class={cn('p-6 pt-0', className)} {...$$restProps}>
|
||||
<div class={cn('bg-muted/20 p-6 pt-5 peer-[.card-header]:border-t', className)} {...$$restProps}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<p class={cn('text-sm text-muted-foreground mt-1', className)} {...$$restProps}>
|
||||
<p class={cn('text-muted-foreground mt-1 text-sm', className)} {...$$restProps}>
|
||||
<slot />
|
||||
</p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { cn } from '$lib/utils/style.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class={cn('flex flex-col space-y-1.5 p-6', className)} {...$$restProps}>
|
||||
<div class={cn('card-header peer flex flex-col space-y-1.5 p-6', className)} {...$$restProps}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<svelte:element
|
||||
this={tag}
|
||||
class={cn('text-xl font-semibold leading-none tracking-tight', className)}
|
||||
class={cn('flex items-center gap-2 text-xl font-semibold leading-none tracking-tight', className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)}
|
||||
class={cn('bg-card text-card-foreground overflow-hidden rounded-lg border shadow-sm', className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
|
||||
Reference in New Issue
Block a user