Compare commits

..

1 Commits

Author SHA1 Message Date
Marcel
53b482c5f2 fix(e2e): fix admin tag test (use existing tag) and annotation locator
Some checks failed
CI / Unit & Component Tests (push) Successful in 2m37s
CI / Backend Unit Tests (push) Successful in 2m18s
CI / E2E Tests (push) Failing after 30m45s
- Admin tag test: "Familie" never existed in the database; use "Fest"
  which is a real seeded tag, with a matching rename-back to restore state
- Annotation hash test: the broad `[data-testid^="annotation-"]` locator
  also matched `annotation-side-panel` (always in DOM, even when
  off-screen); extend the :not() exclusion list to cover it

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 11:34:42 +01:00
37 changed files with 219 additions and 389 deletions

View File

@@ -180,19 +180,19 @@ test.describe('Admin — tag management', () => {
// Wait for the tags list to render after the tab switch // Wait for the tags list to render after the tab switch
await page.waitForSelector('ul > li'); await page.waitForSelector('ul > li');
// Hover over the "Familie" row to reveal the opacity-0 action buttons // Hover over the "Fest" row to reveal the opacity-0 action buttons
const familieRow = page const festRow = page
.locator('ul > li') .locator('ul > li')
.filter({ has: page.locator('span', { hasText: /^Familie$/ }) }); .filter({ has: page.locator('span', { hasText: /^Fest$/ }) });
await familieRow.hover(); await festRow.hover();
await familieRow.getByRole('button', { name: 'Schlagwort bearbeiten' }).click(); await festRow.getByRole('button', { name: 'Schlagwort bearbeiten' }).click();
// After clicking edit, {#if editingTagId} replaces the span with a form — // After clicking edit, {#if editingTagId} replaces the span with a form —
// the familieRow filter no longer matches, so we find the input directly. // the festRow filter no longer matches, so we find the input directly.
await page.locator('input[name="name"]').fill('Familie (E2E)'); await page.locator('input[name="name"]').fill('Fest (E2E)');
await page.getByRole('button', { name: 'Speichern' }).click(); await page.getByRole('button', { name: 'Speichern' }).click();
await expect(page.getByText('Familie (E2E)')).toBeVisible(); await expect(page.getByText('Fest (E2E)')).toBeVisible();
await page.screenshot({ path: 'test-results/e2e/admin-tag-renamed.png' }); await page.screenshot({ path: 'test-results/e2e/admin-tag-renamed.png' });
}); });
@@ -205,14 +205,14 @@ test.describe('Admin — tag management', () => {
const renamedRow = page const renamedRow = page
.locator('ul > li') .locator('ul > li')
.filter({ has: page.locator('span', { hasText: /^Familie \(E2E\)$/ }) }); .filter({ has: page.locator('span', { hasText: /^Fest \(E2E\)$/ }) });
await renamedRow.hover(); await renamedRow.hover();
await renamedRow.getByRole('button', { name: 'Schlagwort bearbeiten' }).click(); await renamedRow.getByRole('button', { name: 'Schlagwort bearbeiten' }).click();
await page.locator('input[name="name"]').fill('Familie'); await page.locator('input[name="name"]').fill('Fest');
await page.getByRole('button', { name: 'Speichern' }).click(); await page.getByRole('button', { name: 'Speichern' }).click();
await expect(page.getByText('Familie')).toBeVisible(); await expect(page.getByText('Fest')).toBeVisible();
await page.screenshot({ path: 'test-results/e2e/admin-tag-restored.png' }); await page.screenshot({ path: 'test-results/e2e/admin-tag-restored.png' });
}); });
}); });

View File

@@ -461,9 +461,11 @@ test.describe('PDF annotations — file hash versioning', () => {
await page.waitForSelector('[data-hydrated]'); await page.waitForSelector('[data-hydrated]');
await page.locator('canvas').first().waitFor({ state: 'visible', timeout: 20000 }); await page.locator('canvas').first().waitFor({ state: 'visible', timeout: 20000 });
// Use :not() to exclude the outdated-notice element whose testid also starts with "annotation-" // Use :not() to exclude the outdated-notice and side-panel elements whose testid also starts with "annotation-"
await expect( await expect(
page.locator('[data-testid^="annotation-"]:not([data-testid="annotation-outdated-notice"])') page.locator(
'[data-testid^="annotation-"]:not([data-testid="annotation-outdated-notice"]):not([data-testid="annotation-side-panel"])'
)
).toHaveCount(0, { timeout: 8000 }); ).toHaveCount(0, { timeout: 8000 });
await expect(page.locator('[data-testid="annotation-outdated-notice"]')).toBeVisible({ await expect(page.locator('[data-testid="annotation-outdated-notice"]')).toBeVisible({
timeout: 5000 timeout: 5000

View File

@@ -188,7 +188,7 @@ onMount(() => {
></textarea> ></textarea>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button <button
class="rounded bg-primary px-3 py-1.5 font-sans text-xs font-medium text-primary-fg hover:bg-primary/80 disabled:opacity-40" class="rounded bg-primary px-3 py-1.5 font-sans text-xs font-medium text-white hover:bg-primary/80 disabled:opacity-40"
disabled={posting} disabled={posting}
onclick={() => saveEdit(comment.id)} onclick={() => saveEdit(comment.id)}
> >
@@ -291,7 +291,7 @@ onMount(() => {
></textarea> ></textarea>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button <button
class="rounded bg-primary px-3 py-1.5 font-sans text-xs font-medium text-primary-fg hover:bg-primary/80 disabled:opacity-40" class="rounded bg-primary px-3 py-1.5 font-sans text-xs font-medium text-white hover:bg-primary/80 disabled:opacity-40"
disabled={posting} disabled={posting}
onclick={() => postReply(thread.id)} onclick={() => postReply(thread.id)}
> >
@@ -321,7 +321,7 @@ onMount(() => {
></textarea> ></textarea>
<div> <div>
<button <button
class="rounded bg-primary px-3 py-1.5 font-sans text-xs font-medium text-primary-fg hover:bg-primary/80 disabled:opacity-40" class="rounded bg-primary px-3 py-1.5 font-sans text-xs font-medium text-white hover:bg-primary/80 disabled:opacity-40"
disabled={posting || !newText.trim()} disabled={posting || !newText.trim()}
onclick={postComment} onclick={postComment}
> >

View File

@@ -107,7 +107,7 @@ function handleCountChange(count: number) {
</script> </script>
<div <div
class="z-30 flex shrink-0 flex-col border-t border-line bg-surface shadow-[0_-4px_16px_rgba(0,0,0,0.08)]" class="fixed right-0 bottom-0 left-0 z-30 flex flex-col border-t border-line bg-surface shadow-[0_-4px_16px_rgba(0,0,0,0.08)]"
style="height: {panelHeight}px" style="height: {panelHeight}px"
data-testid="bottom-panel" data-testid="bottom-panel"
> >
@@ -127,37 +127,35 @@ function handleCountChange(count: number) {
</div> </div>
<!-- Tab bar --> <!-- Tab bar -->
<div class="flex shrink-0 items-center border-b border-line bg-surface"> <div class="flex shrink-0 items-center border-b border-line bg-surface px-4">
<!-- Scrollable tabs area — hides scrollbar visually --> {#each tabs as tab (tab.id)}
<div <button
class="flex flex-1 items-center overflow-x-auto px-2 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden" onclick={() => openTab(tab.id)}
> class="mr-1 px-3 py-2.5 font-sans text-xs font-medium transition-colors {activeTab === tab.id && open
{#each tabs as tab (tab.id)} ? 'border-b-2 border-primary text-ink'
<button : 'text-ink-3 hover:text-ink'}"
onclick={() => openTab(tab.id)} aria-pressed={activeTab === tab.id && open}
class="mr-1 shrink-0 px-3 py-2.5 font-sans text-xs font-medium transition-colors {activeTab === tab.id && open >
? 'border-b-2 border-primary text-ink' {tab.label()}
: 'text-ink-3 hover:text-ink'}" {#if tab.id === 'discussion'}
aria-pressed={activeTab === tab.id && open} <span
> data-testid="discussion-count-badge"
{tab.label()} class="ml-1.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 font-sans text-[10px] font-bold text-primary-fg"
{#if tab.id === 'discussion'} >{discussionCount}</span
<span >
data-testid="discussion-count-badge" {/if}
class="ml-1.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 font-sans text-[10px] font-bold text-primary-fg" </button>
>{discussionCount}</span {/each}
>
{/if} <!-- spacer -->
</button> <div class="flex-1"></div>
{/each}
</div>
{#if open} {#if open}
<button <button
onclick={closePanel} onclick={closePanel}
data-testid="panel-close-btn" data-testid="panel-close-btn"
aria-label="Panel schließen" aria-label="Panel schließen"
class="mr-2 shrink-0 rounded p-1.5 text-ink-3 transition-colors hover:bg-muted hover:text-ink" class="rounded p-1.5 text-ink-3 transition-colors hover:bg-muted hover:text-ink"
> >
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />

View File

@@ -58,7 +58,7 @@ const compactMeta = $derived.by(() => {
</script> </script>
<div <div
class="z-20 flex shrink-0 items-center justify-between border-b border-line bg-surface px-3 py-3 shadow-sm sm:px-6" class="z-20 flex shrink-0 items-center justify-between border-b border-line bg-surface px-6 py-3 shadow-sm"
data-topbar data-topbar
> >
<!-- Left: back + title --> <!-- Left: back + title -->
@@ -102,8 +102,8 @@ const compactMeta = $derived.by(() => {
onclick={() => (annotateMode = !annotateMode)} onclick={() => (annotateMode = !annotateMode)}
aria-label={annotateMode ? m.doc_panel_annotate_stop() : m.doc_panel_annotate()} aria-label={annotateMode ? m.doc_panel_annotate_stop() : m.doc_panel_annotate()}
class="flex items-center gap-1.5 rounded px-3 py-1.5 font-sans text-xs font-medium transition {annotateMode class="flex items-center gap-1.5 rounded px-3 py-1.5 font-sans text-xs font-medium transition {annotateMode
? 'bg-primary text-primary-fg' ? 'bg-primary text-white'
: 'border border-primary text-ink hover:bg-primary hover:text-primary-fg'}" : 'border border-primary text-ink hover:bg-primary hover:text-white'}"
> >
<img <img
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Note/Note-Add-MD.svg" src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Note/Note-Add-MD.svg"
@@ -111,17 +111,14 @@ const compactMeta = $derived.by(() => {
aria-hidden="true" aria-hidden="true"
class="h-4 w-4 {annotateMode ? 'invert' : ''}" class="h-4 w-4 {annotateMode ? 'invert' : ''}"
/> />
<span class="hidden sm:inline" {annotateMode ? m.doc_panel_annotate_stop() : m.doc_panel_annotate()}
>{annotateMode ? m.doc_panel_annotate_stop() : m.doc_panel_annotate()}</span
>
</button> </button>
{/if} {/if}
{#if canWrite} {#if canWrite}
<a <a
href="/documents/{doc.id}/edit" href="/documents/{doc.id}/edit"
aria-label={m.btn_edit()} class="flex items-center gap-2 rounded border border-primary bg-transparent px-3 py-1.5 text-xs font-medium text-ink transition hover:bg-primary hover:text-white"
class="flex items-center gap-2 rounded border border-primary bg-transparent px-3 py-1.5 text-xs font-medium text-ink transition hover:bg-primary hover:text-primary-fg"
> >
<img <img
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Edit-Content-MD.svg" src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Edit-Content-MD.svg"
@@ -129,7 +126,7 @@ const compactMeta = $derived.by(() => {
aria-hidden="true" aria-hidden="true"
class="h-4 w-4" class="h-4 w-4"
/> />
<span class="hidden sm:inline">{m.btn_edit()}</span> {m.btn_edit()}
</a> </a>
{/if} {/if}

View File

@@ -328,7 +328,7 @@ $effect(() => {
<button <button
onclick={applyCompare} onclick={applyCompare}
disabled={!compareA || !compareB || compareA === compareB} disabled={!compareA || !compareB || compareA === compareB}
class="w-full rounded bg-primary px-3 py-1.5 font-sans text-xs font-medium text-primary-fg transition hover:bg-primary/80 disabled:cursor-not-allowed disabled:opacity-40" class="w-full rounded bg-primary px-3 py-1.5 font-sans text-xs font-medium text-white transition hover:bg-primary/80 disabled:cursor-not-allowed disabled:opacity-40"
> >
{m.history_compare_apply()} {m.history_compare_apply()}
</button> </button>

View File

@@ -100,7 +100,7 @@ let { doc }: { doc: Doc } = $props();
{#each doc.tags as tag (tag.id)} {#each doc.tags as tag (tag.id)}
<a <a
href="/?tag={encodeURIComponent(tag.name)}" href="/?tag={encodeURIComponent(tag.name)}"
class="inline-flex items-center rounded bg-muted px-2 py-0.5 text-xs font-bold tracking-wide text-ink uppercase transition-colors hover:bg-primary hover:text-primary-fg" class="inline-flex items-center rounded bg-muted px-2 py-0.5 text-xs font-bold tracking-wide text-ink uppercase transition-colors hover:bg-primary hover:text-white"
title={m.doc_tag_filter_title({ name: tag.name })} title={m.doc_tag_filter_title({ name: tag.name })}
> >
{tag.name} {tag.name}
@@ -131,7 +131,7 @@ let { doc }: { doc: Doc } = $props();
> >
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div <div
class="flex h-8 w-8 items-center justify-center rounded-full bg-primary font-serif text-sm text-primary-fg" class="flex h-8 w-8 items-center justify-center rounded-full bg-primary font-serif text-sm text-white"
> >
{doc.sender.firstName[0]}{doc.sender.lastName[0]} {doc.sender.firstName[0]}{doc.sender.lastName[0]}
</div> </div>

View File

@@ -39,6 +39,9 @@ const userInitials = $derived.by(() => {
<div class="min-h-screen bg-canvas" data-hydrated={hydrated || undefined}> <div class="min-h-screen bg-canvas" data-hydrated={hydrated || undefined}>
{#if !isAuthPage} {#if !isAuthPage}
<header class="sticky top-0 z-50 border-b border-line-2 bg-surface"> <header class="sticky top-0 z-50 border-b border-line-2 bg-surface">
<!-- De Gruyter Brill purple accent strip -->
<div class="h-1 bg-brand-purple"></div>
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"> <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="flex h-16 justify-between"> <div class="flex h-16 justify-between">
<!-- Logo & Nav --> <!-- Logo & Nav -->
@@ -46,8 +49,8 @@ const userInitials = $derived.by(() => {
<!-- Right Side --> <!-- Right Side -->
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<!-- Language selector (desktop only — mobile lives in nav drawer) --> <!-- Language selector -->
<div class="hidden items-center gap-1 border-r border-line pr-3 sm:flex"> <div class="flex items-center gap-1 border-r border-line pr-3">
{#each locales as locale (locale)} {#each locales as locale (locale)}
<button <button
type="button" type="button"

View File

@@ -67,7 +67,7 @@ $effect(() => {
}); });
</script> </script>
<main class="mx-auto max-w-7xl px-4 py-8 font-sans sm:px-6 lg:px-8"> <main class="mx-auto max-w-7xl py-8 font-sans sm:px-6 lg:px-8">
<SearchFilterBar <SearchFilterBar
bind:q={q} bind:q={q}
bind:from={from} bind:from={from}

View File

@@ -1,47 +1,19 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state';
import { untrack } from 'svelte';
import { m } from '$lib/paraglide/messages.js'; import { m } from '$lib/paraglide/messages.js';
import { setLocale, getLocale } from '$lib/paraglide/runtime';
let { isAdmin = false }: { isAdmin?: boolean } = $props(); let { isAdmin = false }: { isAdmin?: boolean } = $props();
const locales = ['DE', 'EN', 'ES'] as const;
const localeMap = { DE: 'de', EN: 'en', ES: 'es' } as const;
const activeLocale = $derived(getLocale().toUpperCase());
let mobileNavOpen = $state(false);
$effect(() => {
// Read pathname to establish the reactive dependency.
// Write via untrack so the effect doesn't re-run on its own write.
void page.url.pathname;
untrack(() => {
mobileNavOpen = false;
});
});
function closeMobileNav() {
mobileNavOpen = false;
}
function handleOverlayKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
mobileNavOpen = false;
}
}
</script> </script>
<div class="flex items-center"> <div class="flex">
<div class="mr-10 flex flex-shrink-0 items-center"> <div class="mr-10 flex flex-shrink-0 items-center">
<a href="/" class="flex items-center" aria-label="Familienarchiv"> <a href="/" class="flex items-center" aria-label="Familienarchiv">
<span class="hidden font-sans text-xl font-bold tracking-widest text-ink uppercase sm:inline" <span class="font-sans text-xl font-bold tracking-widest text-ink uppercase"
>Familienarchiv</span >Familienarchiv</span
> >
</a> </a>
</div> </div>
<!-- Desktop nav -->
<nav class="hidden items-center sm:flex sm:space-x-1"> <nav class="hidden items-center sm:flex sm:space-x-1">
<a <a
href="/" href="/"
@@ -84,124 +56,4 @@ function handleOverlayKeydown(event: KeyboardEvent) {
</a> </a>
{/if} {/if}
</nav> </nav>
<!-- Hamburger toggle (mobile only) -->
<button
class="ml-auto flex h-11 w-11 items-center justify-center rounded text-ink-2 transition-colors hover:bg-muted hover:text-ink sm:hidden"
aria-label={mobileNavOpen ? 'Menü schließen' : 'Menü öffnen'}
aria-expanded={mobileNavOpen}
aria-controls="mobile-nav"
onclick={() => (mobileNavOpen = !mobileNavOpen)}
>
{#if mobileNavOpen}
<!-- X icon -->
<svg
xmlns="http://www.w3.org/2000/svg"
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
{:else}
<!-- Hamburger icon -->
<svg
xmlns="http://www.w3.org/2000/svg"
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
{/if}
</button>
</div> </div>
<!-- Mobile nav overlay -->
{#if mobileNavOpen}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="fixed inset-0 top-[68px] z-40 sm:hidden" onkeydown={handleOverlayKeydown}>
<!-- Backdrop -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="absolute inset-0 bg-black/20" onclick={closeMobileNav}></div>
<!-- Panel -->
<div class="relative border-b border-line bg-surface shadow-md">
<nav id="mobile-nav">
<a
href="/"
class="block flex min-h-[44px] w-full items-center px-4 py-3 font-sans text-sm font-bold tracking-widest uppercase transition-colors
{page.url.pathname === '/' || page.url.pathname.startsWith('/documents')
? 'bg-nav-active text-ink'
: 'text-ink-2 hover:bg-muted hover:text-ink'}"
>
{m.nav_documents()}
</a>
<a
href="/persons"
class="block flex min-h-[44px] w-full items-center px-4 py-3 font-sans text-sm font-bold tracking-widest uppercase transition-colors
{page.url.pathname.startsWith('/persons')
? 'bg-nav-active text-ink'
: 'text-ink-2 hover:bg-muted hover:text-ink'}"
>
{m.nav_persons()}
</a>
<a
href="/conversations"
class="block flex min-h-[44px] w-full items-center px-4 py-3 font-sans text-sm font-bold tracking-widest uppercase transition-colors
{page.url.pathname.startsWith('/conversations')
? 'bg-nav-active text-ink'
: 'text-ink-2 hover:bg-muted hover:text-ink'}"
>
{m.nav_conversations()}
</a>
{#if isAdmin}
<a
href="/admin"
class="block flex min-h-[44px] w-full items-center px-4 py-3 font-sans text-sm font-bold tracking-widest uppercase transition-colors
{page.url.pathname.startsWith('/admin')
? 'bg-nav-active text-ink'
: 'text-ink-2 hover:bg-muted hover:text-ink'}"
>
{m.nav_admin()}
</a>
{/if}
<!-- Language switcher -->
<div class="flex items-center gap-2 border-t border-line px-4 py-3">
{#each locales as locale (locale)}
<button
type="button"
onclick={() => setLocale(localeMap[locale])}
class="min-h-[44px] px-3 font-sans text-sm tracking-widest transition-colors
{activeLocale === locale
? 'font-bold text-ink'
: 'font-normal text-ink-3 hover:text-ink'}"
>
{locale}
</button>
{/each}
</div>
</nav>
</div>
</div>
{/if}

View File

@@ -121,7 +121,7 @@ let {
{#each doc.tags as tag (tag.id)} {#each doc.tags as tag (tag.id)}
<button <button
type="button" type="button"
class="relative z-10 inline-flex cursor-pointer items-center rounded bg-muted px-2 py-1 text-[10px] font-bold tracking-widest text-ink uppercase transition-colors hover:bg-primary hover:text-primary-fg" class="relative z-10 inline-flex cursor-pointer items-center rounded bg-muted px-2 py-1 text-[10px] font-bold tracking-widest text-ink uppercase transition-colors hover:bg-primary hover:text-white"
onclick={(e) => { onclick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();

View File

@@ -146,7 +146,7 @@ $effect(() => {
? 'border-primary bg-accent-bg py-10 text-primary' ? 'border-primary bg-accent-bg py-10 text-primary'
: windowDragging : windowDragging
? 'border-primary/60 bg-accent-bg/50 py-10 text-primary/80' ? 'border-primary/60 bg-accent-bg/50 py-10 text-primary/80'
: 'border-ink/30 py-6 text-ink-3 hover:border-primary hover:text-primary'}" : 'border-ink/20 py-6 text-ink-3 hover:border-primary hover:text-primary'}"
ondragover={handleDragOver} ondragover={handleDragOver}
ondragleave={handleDragLeave} ondragleave={handleDragLeave}
ondrop={handleDrop} ondrop={handleDrop}

View File

@@ -33,7 +33,7 @@ function clickOutside(node: HTMLElement) {
aria-expanded={userMenuOpen} aria-expanded={userMenuOpen}
aria-haspopup="true" aria-haspopup="true"
onclick={() => (userMenuOpen = !userMenuOpen)} onclick={() => (userMenuOpen = !userMenuOpen)}
class="flex h-8 w-8 items-center justify-center rounded-full bg-primary font-sans text-xs font-bold text-primary-fg transition-opacity hover:opacity-80" class="flex h-8 w-8 items-center justify-center rounded-full bg-primary font-sans text-xs font-bold text-white transition-opacity hover:opacity-80"
> >
{userInitials} {userInitials}
</button> </button>

View File

@@ -11,37 +11,37 @@ let { data, form } = $props();
let activeTab = $state('users'); let activeTab = $state('users');
</script> </script>
<div class="mx-auto max-w-7xl px-4 py-8 font-sans sm:px-6 lg:px-8"> <div class="mx-auto max-w-7xl py-8 font-sans sm:px-6 lg:px-8">
<div class="mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div class="mb-8 flex items-center justify-between">
<h1 class="font-serif text-3xl text-ink">{m.admin_heading()}</h1> <h1 class="font-serif text-3xl text-ink">{m.admin_heading()}</h1>
<!-- Tabs --> <!-- Tabs -->
<div class="flex rounded-lg border border-line bg-surface p-1 shadow-sm"> <div class="flex rounded-lg border border-line bg-surface p-1 shadow-sm">
<button <button
class="rounded-md px-2 py-2 text-sm font-bold tracking-wide uppercase transition sm:px-4 {activeTab === class="rounded-md px-4 py-2 text-sm font-bold tracking-wide uppercase transition {activeTab ===
'users' 'users'
? 'bg-primary text-primary-fg' ? 'bg-primary text-white'
: 'text-ink-2 hover:text-ink'}" : 'text-ink-2 hover:text-ink'}"
onclick={() => (activeTab = 'users')}>{m.admin_tab_users()}</button onclick={() => (activeTab = 'users')}>{m.admin_tab_users()}</button
> >
<button <button
class="rounded-md px-2 py-2 text-sm font-bold tracking-wide uppercase transition sm:px-4 {activeTab === class="rounded-md px-4 py-2 text-sm font-bold tracking-wide uppercase transition {activeTab ===
'groups' 'groups'
? 'bg-primary text-primary-fg' ? 'bg-primary text-white'
: 'text-ink-2 hover:text-ink'}" : 'text-ink-2 hover:text-ink'}"
onclick={() => (activeTab = 'groups')}>{m.admin_tab_groups()}</button onclick={() => (activeTab = 'groups')}>{m.admin_tab_groups()}</button
> >
<button <button
class="rounded-md px-2 py-2 text-sm font-bold tracking-wide uppercase transition sm:px-4 {activeTab === class="rounded-md px-4 py-2 text-sm font-bold tracking-wide uppercase transition {activeTab ===
'tags' 'tags'
? 'bg-primary text-primary-fg' ? 'bg-primary text-white'
: 'text-ink-2 hover:text-ink'}" : 'text-ink-2 hover:text-ink'}"
onclick={() => (activeTab = 'tags')}>{m.admin_tab_tags()}</button onclick={() => (activeTab = 'tags')}>{m.admin_tab_tags()}</button
> >
<button <button
class="rounded-md px-2 py-2 text-sm font-bold tracking-wide uppercase transition sm:px-4 {activeTab === class="rounded-md px-4 py-2 text-sm font-bold tracking-wide uppercase transition {activeTab ===
'system' 'system'
? 'bg-primary text-primary-fg' ? 'bg-primary text-white'
: 'text-ink-2 hover:text-ink'}" : 'text-ink-2 hover:text-ink'}"
onclick={() => (activeTab = 'system')}>{m.admin_tab_system()}</button onclick={() => (activeTab = 'system')}>{m.admin_tab_system()}</button
> >

View File

@@ -212,7 +212,7 @@ function cancelEditGroup() {
<button <button
type="submit" type="submit"
class="w-full rounded bg-primary px-6 py-2 text-sm font-bold text-primary-fg uppercase hover:bg-accent hover:text-ink md:w-auto" class="w-full rounded bg-primary px-6 py-2 text-sm font-bold text-white uppercase hover:bg-accent hover:text-ink md:w-auto"
> >
{m.btn_create()} {m.btn_create()}
</button> </button>

View File

@@ -41,7 +41,7 @@ async function backfillFileHashes() {
<button <button
onclick={backfillVersions} onclick={backfillVersions}
disabled={backfillLoading} disabled={backfillLoading}
class="rounded bg-primary px-6 py-2 text-sm font-bold text-primary-fg uppercase transition hover:bg-accent hover:text-ink disabled:cursor-not-allowed disabled:opacity-50" class="rounded bg-primary px-6 py-2 text-sm font-bold text-white uppercase transition hover:bg-accent hover:text-ink disabled:cursor-not-allowed disabled:opacity-50"
> >
{backfillLoading ? '…' : m.admin_system_backfill_btn()} {backfillLoading ? '…' : m.admin_system_backfill_btn()}
</button> </button>
@@ -60,7 +60,7 @@ async function backfillFileHashes() {
<button <button
onclick={backfillFileHashes} onclick={backfillFileHashes}
disabled={backfillHashesLoading} disabled={backfillHashesLoading}
class="rounded bg-primary px-6 py-2 text-sm font-bold text-primary-fg uppercase transition hover:bg-accent hover:text-ink disabled:cursor-not-allowed disabled:opacity-50" class="rounded bg-primary px-6 py-2 text-sm font-bold text-white uppercase transition hover:bg-accent hover:text-ink disabled:cursor-not-allowed disabled:opacity-50"
> >
{backfillHashesLoading ? '…' : m.admin_system_backfill_hashes_btn()} {backfillHashesLoading ? '…' : m.admin_system_backfill_hashes_btn()}
</button> </button>

View File

@@ -20,7 +20,7 @@ let {
<h2 class="text-lg font-bold text-ink-2">{m.admin_section_users()}</h2> <h2 class="text-lg font-bold text-ink-2">{m.admin_section_users()}</h2>
<a <a
href="/admin/users/new" href="/admin/users/new"
class="inline-flex items-center gap-1 rounded-sm bg-primary px-4 py-2 font-sans text-xs font-bold tracking-widest text-primary-fg uppercase transition-opacity hover:opacity-80" class="inline-flex items-center gap-1 rounded-sm bg-primary px-4 py-2 font-sans text-xs font-bold tracking-widest text-white uppercase transition-opacity hover:opacity-80"
> >
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />

View File

@@ -85,7 +85,7 @@ const selectedGroupIds = $derived(data.editUser.groups?.map((g: { id: string })
</a> </a>
<button <button
type="submit" type="submit"
class="rounded-sm bg-primary px-5 py-2 font-sans text-xs font-bold tracking-widest text-primary-fg uppercase transition-opacity hover:opacity-80" class="rounded-sm bg-primary px-5 py-2 font-sans text-xs font-bold tracking-widest text-white uppercase transition-opacity hover:opacity-80"
> >
{m.btn_save()} {m.btn_save()}
</button> </button>

View File

@@ -61,7 +61,7 @@ let { data, form } = $props();
</a> </a>
<button <button
type="submit" type="submit"
class="rounded-sm bg-primary px-5 py-2 font-sans text-xs font-bold tracking-widest text-primary-fg uppercase transition-opacity hover:opacity-80" class="rounded-sm bg-primary px-5 py-2 font-sans text-xs font-bold tracking-widest text-white uppercase transition-opacity hover:opacity-80"
> >
{m.btn_create()} {m.btn_create()}
</button> </button>

View File

@@ -48,7 +48,7 @@ let {
<button <button
data-testid="conv-swap-btn" data-testid="conv-swap-btn"
onclick={onswapPersons} onclick={onswapPersons}
class="flex w-full items-center justify-center gap-2 border border-line px-3 py-2.5 text-xs font-bold tracking-widest text-ink uppercase transition-colors hover:bg-primary hover:text-primary-fg md:w-auto {senderId && class="flex w-full items-center justify-center gap-2 border border-line px-3 py-2.5 text-xs font-bold tracking-widest text-ink uppercase transition-colors hover:bg-primary hover:text-white md:w-auto {senderId &&
receiverId receiverId
? '' ? ''
: 'invisible'}" : 'invisible'}"
@@ -121,7 +121,7 @@ let {
<div> <div>
<button <button
onclick={ontoggleSort} onclick={ontoggleSort}
class="flex h-[42px] w-full items-center justify-center border border-line text-xs font-bold tracking-wide text-ink uppercase transition-colors hover:bg-primary hover:text-primary-fg" class="flex h-[42px] w-full items-center justify-center border border-line text-xs font-bold tracking-wide text-ink uppercase transition-colors hover:bg-primary hover:text-white"
> >
<span class="mr-2">{m.conv_sort_label()}</span> <span class="mr-2">{m.conv_sort_label()}</span>
<span>{sortDir === 'DESC' ? m.conv_sort_newest() : m.conv_sort_oldest()}</span> <span>{sortDir === 'DESC' ? m.conv_sort_newest() : m.conv_sort_oldest()}</span>

View File

@@ -2,24 +2,14 @@
import { m } from '$lib/paraglide/messages.js'; import { m } from '$lib/paraglide/messages.js';
let { originalFilename }: { originalFilename: string } = $props(); let { originalFilename }: { originalFilename: string } = $props();
let selectedFilename = $state<string | null>(null);
function handleFileChange(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0];
selectedFilename = file?.name ?? null;
}
</script> </script>
<div class="rounded-sm border border-line bg-surface shadow-sm"> <div class="rounded-sm border border-line bg-surface p-6 shadow-sm">
<div class="border-b border-line px-6 py-4"> <h2 class="mb-5 text-xs font-bold tracking-widest text-ink-3 uppercase">
<h2 class="text-xs font-bold tracking-widest text-ink-3 uppercase"> {m.doc_section_file()}
{m.doc_section_file()} </h2>
</h2>
</div>
<!-- Current file --> <div class="mb-4 flex items-center gap-3 rounded bg-muted px-3 py-2 text-sm text-ink-2">
<div class="flex items-center gap-3 border-b border-line px-6 py-3 text-sm text-ink-2">
<img <img
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/PDF-Document-MD.svg" src="/degruyter-icons/Simple/Medium-24px/SVG/Action/PDF-Document-MD.svg"
alt="" alt=""
@@ -32,31 +22,19 @@ function handleFileChange(e: Event) {
> >
</div> </div>
<!-- Replace file upload zone --> <label for="file-upload" class="mb-1 block text-sm font-medium text-ink-2">
<label {m.doc_file_replace_label()}
for="file-upload" <span class="font-normal text-ink-3">({m.doc_file_replace_note()})</span>
class="flex cursor-pointer flex-col items-center gap-3 px-6 py-8 transition-colors hover:bg-muted/40"
>
<svg
class="h-8 w-8 text-ink-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"
/>
</svg>
{#if selectedFilename}
<span class="text-sm font-medium text-ink">{selectedFilename}</span>
{:else}
<span class="text-sm font-medium text-ink-2">{m.doc_file_replace_label()}</span>
<span class="text-xs text-ink-3">{m.doc_file_replace_note()}</span>
{/if}
</label> </label>
<input id="file-upload" type="file" name="file" onchange={handleFileChange} class="sr-only" /> <input
id="file-upload"
type="file"
name="file"
class="block w-full cursor-pointer text-sm
text-ink-2 file:mr-4 file:rounded
file:border-0 file:bg-muted
file:px-4 file:py-2
file:text-sm file:font-semibold
file:text-ink hover:file:bg-muted"
/>
</div> </div>

View File

@@ -7,79 +7,73 @@ let confirmDelete = $state(false);
</script> </script>
<div <div
class="sticky bottom-0 z-10 -mx-4 border-t border-line bg-surface px-4 py-3 shadow-[0_-2px_8px_rgba(0,0,0,0.06)] sm:px-6 sm:py-4" class="sticky bottom-0 z-10 -mx-4 flex items-center justify-between border-t border-line bg-surface px-6 py-4 shadow-[0_-2px_8px_rgba(0,0,0,0.06)]"
> >
<!-- Desktop: delete left, cancel+buttons right --> <!-- Left: delete -->
<!-- Mobile: action buttons stacked full-width, delete+cancel row at bottom --> <div class="flex items-center gap-3">
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> {#if confirmDelete}
<!-- Primary actions first (top on mobile, right on desktop) --> <span class="font-sans text-sm text-red-700">{m.doc_delete_confirm()}</span>
<div class="flex flex-col gap-2 sm:order-last sm:flex-row sm:items-center sm:gap-4">
<button <button
type="submit" type="submit"
form="mark-for-review-form" form="delete-form"
class="w-full rounded-sm border border-line px-4 py-2.5 font-sans text-xs font-bold tracking-widest text-ink-2 uppercase transition-colors hover:bg-muted sm:w-auto sm:py-2" class="rounded bg-red-600 px-4 py-1.5 text-sm font-bold text-white transition-colors hover:bg-red-700"
> >
{m.btn_mark_for_review()} {m.btn_delete()}
</button> </button>
<button <button
type="submit" type="button"
class="w-full rounded bg-primary px-6 py-2.5 font-sans text-sm font-bold tracking-widest text-primary-fg uppercase transition-colors hover:bg-primary/80 sm:w-auto sm:py-2" onclick={() => (confirmDelete = false)}
class="text-sm text-ink-2 transition-colors hover:text-ink"
> >
{m.btn_save()} {m.btn_cancel()}
</button> </button>
</div> {:else}
<button
type="button"
onclick={() => (confirmDelete = true)}
class="flex items-center gap-1.5 rounded border border-red-300 px-4 py-1.5 text-sm font-bold text-red-600 transition-colors hover:border-red-600 hover:bg-red-50"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<polyline points="3 6 5 6 21 6" />
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
<path d="M10 11v6M14 11v6" />
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
</svg>
{m.btn_delete()}
</button>
{/if}
</div>
<!-- Secondary: delete + cancel (row on both mobile and desktop) --> <!-- Right: cancel + mark for review + save -->
<div class="flex items-center justify-between sm:justify-start sm:gap-4"> <div class="flex items-center gap-4">
{#if confirmDelete} <a
<span class="font-sans text-sm text-red-700">{m.doc_delete_confirm()}</span> href="/documents/{docId}"
<div class="flex items-center gap-3"> class="text-sm font-medium text-ink-2 transition-colors hover:text-ink"
<button >
type="submit" {m.btn_cancel()}
form="delete-form" </a>
class="rounded bg-red-600 px-4 py-1.5 text-sm font-bold text-white transition-colors hover:bg-red-700" <button
> type="submit"
{m.btn_delete()} form="mark-for-review-form"
</button> class="rounded-sm border border-gray-300 px-4 py-2 font-sans text-xs font-bold tracking-widest text-gray-600 uppercase transition-colors hover:bg-gray-50"
<button >
type="button" {m.btn_mark_for_review()}
onclick={() => (confirmDelete = false)} </button>
class="text-sm text-ink-2 transition-colors hover:text-ink" <button
> type="submit"
{m.btn_cancel()} class="rounded bg-primary px-6 py-2 text-sm font-bold tracking-widest text-white uppercase transition-colors hover:bg-primary/80"
</button> >
</div> {m.btn_save()}
{:else} </button>
<button
type="button"
onclick={() => (confirmDelete = true)}
class="flex items-center gap-1.5 rounded border border-red-300 px-4 py-1.5 text-sm font-bold text-red-600 transition-colors hover:border-red-600 hover:bg-red-50"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<polyline points="3 6 5 6 21 6" />
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
<path d="M10 11v6M14 11v6" />
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
</svg>
{m.btn_delete()}
</button>
<a
href="/documents/{docId}"
class="text-sm font-medium text-ink-2 transition-colors hover:text-ink"
>
{m.btn_cancel()}
</a>
{/if}
</div>
</div> </div>
</div> </div>

View File

@@ -120,35 +120,30 @@ $effect(() => {
<!-- Sticky Save Bar --> <!-- Sticky Save Bar -->
<div <div
class="sticky bottom-0 z-10 -mx-4 border-t border-line bg-surface px-4 py-3 shadow-[0_-2px_8px_rgba(0,0,0,0.06)] sm:px-6 sm:py-4" class="sticky bottom-0 z-10 -mx-4 flex items-center justify-between border-t border-line bg-surface px-6 py-4 shadow-[0_-2px_8px_rgba(0,0,0,0.06)]"
> >
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <a href="/" class="text-sm font-medium text-ink-2 transition-colors hover:text-ink">
<a {m.btn_cancel()}
href="/" </a>
class="order-last text-center text-sm font-medium text-ink-2 transition-colors hover:text-ink sm:order-first sm:text-left" <div class="flex items-center gap-3">
<button
type="submit"
name="metadataComplete"
value="false"
formaction="?/save"
class="rounded-sm border border-gray-300 px-5 py-2 font-sans text-xs font-bold tracking-widest text-gray-600 uppercase transition-colors hover:bg-gray-50"
> >
{m.btn_cancel()} {m.btn_save()}
</a> </button>
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3"> <button
<button type="submit"
type="submit" name="metadataComplete"
name="metadataComplete" value="true"
value="false" formaction="?/saveReviewed"
formaction="?/save" class="rounded-sm bg-brand-navy px-5 py-2 font-sans text-xs font-bold tracking-widest text-white uppercase transition-colors hover:bg-brand-navy/90"
class="w-full rounded-sm border border-line px-5 py-2.5 font-sans text-xs font-bold tracking-widest text-ink-2 uppercase transition-colors hover:bg-muted sm:w-auto sm:py-2" >
> {m.btn_save_and_mark_reviewed()}
{m.btn_save()} </button>
</button>
<button
type="submit"
name="metadataComplete"
value="true"
formaction="?/saveReviewed"
class="w-full rounded-sm bg-primary px-5 py-2.5 font-sans text-xs font-bold tracking-widest text-primary-fg uppercase transition-colors hover:bg-primary/90 sm:w-auto sm:py-2"
>
{m.btn_save_and_mark_reviewed()}
</button>
</div>
</div> </div>
</div> </div>
</form> </form>

View File

@@ -19,7 +19,7 @@ function formatUploadDate(createdAt: string): string {
<!-- Back Link --> <!-- Back Link -->
<a <a
href="/" href="/"
class="group mb-4 inline-flex items-center font-sans text-xs font-bold tracking-widest text-ink-2 uppercase transition-colors hover:text-ink" class="group mb-4 inline-flex items-center font-sans text-xs font-bold tracking-widest text-gray-500 uppercase transition-colors hover:text-brand-navy"
> >
<img <img
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Arrow/Arrow-Left-MD.svg" src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Arrow/Arrow-Left-MD.svg"
@@ -47,7 +47,7 @@ function formatUploadDate(createdAt: string): string {
{#if count > 0} {#if count > 0}
<a <a
href="/enrich/{documents[0].id}" href="/enrich/{documents[0].id}"
class="bg-primary px-5 py-2 font-sans text-xs font-bold tracking-widest text-primary-fg uppercase transition-colors hover:bg-primary/90" class="bg-brand-navy px-5 py-2 font-sans text-xs font-bold tracking-widest text-white uppercase transition-colors hover:bg-brand-navy/90"
> >
{m.enrich_list_start()} {m.enrich_list_start()}
</a> </a>
@@ -76,8 +76,8 @@ function formatUploadDate(createdAt: string): string {
</div> </div>
{:else} {:else}
<!-- Document Rows --> <!-- Document Rows -->
<div class="border border-line bg-white shadow-sm"> <div class="border-brand-sand border bg-white shadow-sm">
<ul class="divide-y divide-line-2"> <ul class="divide-brand-sand divide-y">
{#each documents as doc (doc.id)} {#each documents as doc (doc.id)}
<li class="group hover:bg-brand-sand/30 transition-colors duration-200"> <li class="group hover:bg-brand-sand/30 transition-colors duration-200">
<a href="/enrich/{doc.id}" class="flex items-center justify-between p-6"> <a href="/enrich/{doc.id}" class="flex items-center justify-between p-6">

View File

@@ -156,7 +156,7 @@ let selectedReceivers = $state(untrack(() => doc.receivers ?? []));
type="submit" type="submit"
form="save-form" form="save-form"
formaction="?/saveAndReview" formaction="?/saveAndReview"
class="rounded-sm bg-primary px-5 py-2 font-sans text-xs font-bold tracking-widest text-primary-fg uppercase transition-colors hover:bg-primary/90" class="rounded-sm bg-brand-navy px-5 py-2 font-sans text-xs font-bold tracking-widest text-white uppercase transition-colors hover:bg-brand-navy/90"
> >
{m.btn_save_and_mark_reviewed()} {m.btn_save_and_mark_reviewed()}
</button> </button>

View File

@@ -17,21 +17,21 @@ import { m } from '$lib/paraglide/messages.js';
{m.enrich_done_heading()} {m.enrich_done_heading()}
</h1> </h1>
<p class="mt-2 max-w-xs font-sans text-sm text-ink-2"> <p class="mt-2 max-w-xs font-sans text-sm text-gray-500">
{m.enrich_done_body()} {m.enrich_done_body()}
</p> </p>
<div class="mt-8 flex flex-col items-center gap-4"> <div class="mt-8 flex flex-col items-center gap-4">
<a <a
href="/" href="/"
class="bg-primary px-6 py-2 font-sans text-xs font-bold tracking-widest text-primary-fg uppercase transition-colors hover:bg-primary/90" class="bg-brand-navy px-6 py-2 font-sans text-xs font-bold tracking-widest text-white uppercase transition-colors hover:bg-brand-navy/90"
> >
{m.btn_back_to_overview()} {m.btn_back_to_overview()}
</a> </a>
<a <a
href="/enrich" href="/enrich"
class="font-sans text-xs text-ink-2 underline-offset-4 transition-colors hover:text-ink hover:underline" class="font-sans text-xs text-gray-400 underline-offset-4 transition-colors hover:text-brand-navy hover:underline"
> >
{m.enrich_back_to_list()} {m.enrich_back_to_list()}
</a> </a>

View File

@@ -5,6 +5,9 @@ let { form }: { form?: { error?: string; success?: boolean } } = $props();
</script> </script>
<div class="relative flex min-h-screen flex-col bg-surface"> <div class="relative flex min-h-screen flex-col bg-surface">
<!-- Accent strip -->
<div class="h-1 bg-brand-purple"></div>
<div class="flex flex-1 items-center justify-center px-4"> <div class="flex flex-1 items-center justify-center px-4">
<div class="w-full max-w-sm"> <div class="w-full max-w-sm">
<!-- Logo --> <!-- Logo -->
@@ -54,7 +57,7 @@ let { form }: { form?: { error?: string; success?: boolean } } = $props();
<button <button
type="submit" type="submit"
class="mt-2 w-full bg-primary py-2.5 font-sans text-xs font-bold tracking-widest text-primary-fg uppercase transition-colors hover:bg-primary/90" class="mt-2 w-full bg-primary py-2.5 font-sans text-xs font-bold tracking-widest text-white uppercase transition-colors hover:bg-primary/90"
> >
{m.forgot_password_submit()} {m.forgot_password_submit()}
</button> </button>

View File

@@ -10,6 +10,7 @@
--palette-mint: #a1dcd8; --palette-mint: #a1dcd8;
--palette-turquoise: #00c7b1; --palette-turquoise: #00c7b1;
--palette-sand: #f0efe9; --palette-sand: #f0efe9;
--palette-purple: #b4b9ff;
/* Typography */ /* Typography */
--font-sans: 'Montserrat', ui-sans-serif, system-ui, sans-serif; --font-sans: 'Montserrat', ui-sans-serif, system-ui, sans-serif;
@@ -57,6 +58,7 @@
--color-pdf-text: var(--c-pdf-text); --color-pdf-text: var(--c-pdf-text);
/* Static brand tokens (not themed) */ /* Static brand tokens (not themed) */
--color-brand-purple: var(--palette-purple);
--color-brand-navy: var(--palette-navy); --color-brand-navy: var(--palette-navy);
--color-brand-mint: var(--palette-mint); --color-brand-mint: var(--palette-mint);
} }
@@ -72,8 +74,8 @@
--c-line-2: #eeede8; --c-line-2: #eeede8;
--c-ink: #012851; --c-ink: #012851;
--c-ink-2: #4b5563; /* gray-600 — 7.6:1 on white, 6.6:1 on canvas — WCAG AA ✓ */ --c-ink-2: #6b7280;
--c-ink-3: #6b7280; /* gray-500 — 4.8:1 on white — WCAG AA ✓; use only on surface, not canvas */ --c-ink-3: #9ca3af;
--c-accent: #a1dcd8; --c-accent: #a1dcd8;
--c-accent-bg: rgba(161, 220, 216, 0.15); --c-accent-bg: rgba(161, 220, 216, 0.15);
@@ -96,12 +98,12 @@
--c-overlay: #242424; --c-overlay: #242424;
--c-muted: #252525; --c-muted: #252525;
--c-line: #3d3d3d; --c-line: #2e2e2e;
--c-line-2: #2e2e2e; --c-line-2: #222222;
--c-ink: #f0efe9; --c-ink: #f0efe9;
--c-ink-2: #9ca3af; /* gray-400 — 7.5:1 on dark surface — WCAG AAA ✓ */ --c-ink-2: #9ca3af;
--c-ink-3: #8b97a5; /* gray-450 — 6.5:1 on dark surface — WCAG AA ✓ */ --c-ink-3: #6b7280;
--c-accent: #00c7b1; --c-accent: #00c7b1;
--c-accent-bg: rgba(0, 199, 177, 0.12); --c-accent-bg: rgba(0, 199, 177, 0.12);
@@ -124,8 +126,8 @@
--c-overlay: #242424; --c-overlay: #242424;
--c-muted: #252525; --c-muted: #252525;
--c-line: #3d3d3d; --c-line: #2e2e2e;
--c-line-2: #2e2e2e; --c-line-2: #222222;
--c-ink: #f0efe9; --c-ink: #f0efe9;
--c-ink-2: #9ca3af; --c-ink-2: #9ca3af;

View File

@@ -10,6 +10,9 @@ const activeLocale = $derived(getLocale().toUpperCase());
</script> </script>
<div class="relative flex min-h-screen flex-col bg-canvas"> <div class="relative flex min-h-screen flex-col bg-canvas">
<!-- DGB purple accent strip -->
<div class="h-1 bg-brand-purple"></div>
<!-- Language switcher --> <!-- Language switcher -->
<div class="absolute top-4 right-4 flex items-center gap-1"> <div class="absolute top-4 right-4 flex items-center gap-1">
{#each locales as locale (locale)} {#each locales as locale (locale)}
@@ -82,7 +85,7 @@ const activeLocale = $derived(getLocale().toUpperCase());
<button <button
type="submit" type="submit"
class="mt-2 w-full bg-primary py-2.5 font-sans text-xs font-bold tracking-widest text-primary-fg uppercase transition-colors hover:bg-primary/90" class="mt-2 w-full bg-primary py-2.5 font-sans text-xs font-bold tracking-widest text-white uppercase transition-colors hover:bg-primary/90"
> >
{m.login_btn_submit()} {m.login_btn_submit()}
</button> </button>

View File

@@ -23,7 +23,7 @@ function handleSearch() {
} }
</script> </script>
<div class="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8"> <div class="mx-auto max-w-7xl py-12 sm:px-6 lg:px-8">
<!-- Header Area --> <!-- Header Area -->
<div <div
class="mb-10 flex flex-col justify-between gap-6 border-b border-ink/10 pb-6 md:flex-row md:items-end" class="mb-10 flex flex-col justify-between gap-6 border-b border-ink/10 pb-6 md:flex-row md:items-end"
@@ -107,7 +107,7 @@ function handleSearch() {
<!-- Avatar --> <!-- Avatar -->
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<div <div
class="flex h-12 w-12 items-center justify-center rounded-full bg-primary font-serif text-lg text-primary-fg transition-colors group-hover:bg-accent group-hover:text-ink" class="flex h-12 w-12 items-center justify-center rounded-full bg-primary font-serif text-lg text-white transition-colors group-hover:bg-accent group-hover:text-ink"
> >
{person.firstName[0]}{person.lastName[0]} {person.firstName[0]}{person.lastName[0]}
</div> </div>

View File

@@ -143,7 +143,7 @@ $effect(() => {
<div class="flex gap-3"> <div class="flex gap-3">
<button <button
type="submit" type="submit"
class="rounded bg-primary px-5 py-2 text-sm font-bold tracking-widest text-primary-fg uppercase transition-colors hover:bg-primary/80" class="rounded bg-primary px-5 py-2 text-sm font-bold tracking-widest text-white uppercase transition-colors hover:bg-primary/80"
> >
{m.btn_save()} {m.btn_save()}
</button> </button>

View File

@@ -44,7 +44,7 @@ const visibleDocuments = $derived(
<div class="mb-10"> <div class="mb-10">
<div class="mb-6 flex items-center gap-3 border-b border-ink/10 pb-2"> <div class="mb-6 flex items-center gap-3 border-b border-ink/10 pb-2">
<h2 class="font-serif text-xl text-ink">{heading}</h2> <h2 class="font-serif text-xl text-ink">{heading}</h2>
<span class="rounded-full bg-primary px-2 py-1 text-xs font-bold text-primary-fg"> <span class="rounded-full bg-primary px-2 py-1 text-xs font-bold text-white">
{documents.length} {documents.length}
</span> </span>
{#if yearRange} {#if yearRange}

View File

@@ -89,7 +89,7 @@ let { form } = $props();
</a> </a>
<button <button
type="submit" type="submit"
class="rounded bg-primary px-6 py-2 text-sm font-bold tracking-widest text-primary-fg uppercase transition-colors hover:bg-primary/80" class="rounded bg-primary px-6 py-2 text-sm font-bold tracking-widest text-white uppercase transition-colors hover:bg-primary/80"
> >
{m.btn_create()} {m.btn_create()}
</button> </button>

View File

@@ -70,7 +70,7 @@ let {
<button <button
type="submit" type="submit"
class="mt-5 rounded-sm bg-primary px-5 py-2 font-sans text-xs font-bold tracking-widest text-primary-fg uppercase transition-opacity hover:opacity-80" class="mt-5 rounded-sm bg-primary px-5 py-2 font-sans text-xs font-bold tracking-widest text-white uppercase transition-opacity hover:opacity-80"
> >
{m.btn_save()} {m.btn_save()}
</button> </button>

View File

@@ -115,7 +115,7 @@ function handleBirthDateInput(e: Event) {
<button <button
type="submit" type="submit"
class="mt-5 rounded-sm bg-primary px-5 py-2 font-sans text-xs font-bold tracking-widest text-primary-fg uppercase transition-opacity hover:opacity-80" class="mt-5 rounded-sm bg-primary px-5 py-2 font-sans text-xs font-bold tracking-widest text-white uppercase transition-opacity hover:opacity-80"
> >
{m.btn_save()} {m.btn_save()}
</button> </button>

View File

@@ -12,6 +12,9 @@ let {
</script> </script>
<div class="relative flex min-h-screen flex-col bg-surface"> <div class="relative flex min-h-screen flex-col bg-surface">
<!-- Accent strip -->
<div class="h-1 bg-brand-purple"></div>
<div class="flex flex-1 items-center justify-center px-4"> <div class="flex flex-1 items-center justify-center px-4">
<div class="w-full max-w-sm"> <div class="w-full max-w-sm">
<!-- Logo --> <!-- Logo -->
@@ -83,7 +86,7 @@ let {
<button <button
type="submit" type="submit"
class="mt-2 w-full bg-primary py-2.5 font-sans text-xs font-bold tracking-widest text-primary-fg uppercase transition-colors hover:bg-primary/90" class="mt-2 w-full bg-primary py-2.5 font-sans text-xs font-bold tracking-widest text-white uppercase transition-colors hover:bg-primary/90"
> >
{m.reset_password_submit()} {m.reset_password_submit()}
</button> </button>

View File

@@ -45,13 +45,13 @@ const initials = $derived.by(() => {
<div class="mb-5 flex justify-center"> <div class="mb-5 flex justify-center">
{#if initials} {#if initials}
<div <div
class="flex h-16 w-16 items-center justify-center rounded-full bg-primary text-primary-fg" class="flex h-16 w-16 items-center justify-center rounded-full bg-primary text-white"
> >
<span class="font-serif text-xl font-bold">{initials}</span> <span class="font-serif text-xl font-bold">{initials}</span>
</div> </div>
{:else} {:else}
<div <div
class="flex h-16 w-16 items-center justify-center rounded-full bg-primary text-primary-fg" class="flex h-16 w-16 items-center justify-center rounded-full bg-primary text-white"
> >
<svg <svg
class="h-8 w-8" class="h-8 w-8"