Compare commits
14 Commits
feat/81-di
...
0bd7a70c96
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bd7a70c96 | ||
|
|
a570dff4e9 | ||
|
|
fcff7fbdb1 | ||
|
|
5cf6947040 | ||
|
|
d053f6dc40 | ||
|
|
afebaf4c53 | ||
|
|
1bfe0ab022 | ||
|
|
6ebae19984 | ||
|
|
fa9577052d | ||
|
|
a7eaa40852 | ||
|
|
c5e28ac18e | ||
|
|
d6f4ea05d9 | ||
|
|
065dd8fabd | ||
|
|
a967483cd9 |
@@ -108,9 +108,13 @@ public class DocumentService {
|
|||||||
|| (dto.getReceiverIds() != null && !dto.getReceiverIds().isEmpty());
|
|| (dto.getReceiverIds() != null && !dto.getReceiverIds().isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String titleToUse = (dto.getTitle() != null && !dto.getTitle().isBlank())
|
||||||
|
? dto.getTitle()
|
||||||
|
: titleFromFilename(filename);
|
||||||
|
|
||||||
Document doc = Document.builder()
|
Document doc = Document.builder()
|
||||||
.originalFilename(filename)
|
.originalFilename(filename)
|
||||||
.title(dto.getTitle())
|
.title(titleToUse)
|
||||||
.documentDate(dto.getDocumentDate())
|
.documentDate(dto.getDocumentDate())
|
||||||
.location(dto.getLocation())
|
.location(dto.getLocation())
|
||||||
.documentLocation(dto.getDocumentLocation())
|
.documentLocation(dto.getDocumentLocation())
|
||||||
|
|||||||
@@ -467,6 +467,62 @@ class DocumentServiceTest {
|
|||||||
assertThat(captor.getValue().getSender()).isNull();
|
assertThat(captor.getValue().getSender()).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── createDocument title fallback ────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createDocument_usesTitleFromFilename_whenDtoTitleIsNull() throws Exception {
|
||||||
|
DocumentUpdateDTO dto = new DocumentUpdateDTO();
|
||||||
|
// dto.title is null
|
||||||
|
MockMultipartFile file = new MockMultipartFile("file", "Brief_1965.pdf", "application/pdf", new byte[]{1});
|
||||||
|
Document saved = Document.builder().id(UUID.randomUUID()).title("Brief_1965")
|
||||||
|
.originalFilename("Brief_1965.pdf").status(DocumentStatus.PLACEHOLDER).build();
|
||||||
|
when(documentRepository.save(any())).thenReturn(saved);
|
||||||
|
when(documentRepository.findById(any())).thenReturn(Optional.of(saved));
|
||||||
|
when(fileService.uploadFile(any(), any())).thenReturn(new FileService.UploadResult("path", "hash"));
|
||||||
|
|
||||||
|
ArgumentCaptor<Document> captor = ArgumentCaptor.forClass(Document.class);
|
||||||
|
documentService.createDocument(dto, file);
|
||||||
|
|
||||||
|
verify(documentRepository, atLeastOnce()).save(captor.capture());
|
||||||
|
assertThat(captor.getAllValues().get(0).getTitle()).isEqualTo("Brief_1965");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createDocument_usesTitleFromFilename_whenDtoTitleIsBlank() throws Exception {
|
||||||
|
DocumentUpdateDTO dto = new DocumentUpdateDTO();
|
||||||
|
dto.setTitle(" ");
|
||||||
|
MockMultipartFile file = new MockMultipartFile("file", "Rechnung_1980.pdf", "application/pdf", new byte[]{1});
|
||||||
|
Document saved = Document.builder().id(UUID.randomUUID()).title("Rechnung_1980")
|
||||||
|
.originalFilename("Rechnung_1980.pdf").status(DocumentStatus.PLACEHOLDER).build();
|
||||||
|
when(documentRepository.save(any())).thenReturn(saved);
|
||||||
|
when(documentRepository.findById(any())).thenReturn(Optional.of(saved));
|
||||||
|
when(fileService.uploadFile(any(), any())).thenReturn(new FileService.UploadResult("path", "hash"));
|
||||||
|
|
||||||
|
ArgumentCaptor<Document> captor = ArgumentCaptor.forClass(Document.class);
|
||||||
|
documentService.createDocument(dto, file);
|
||||||
|
|
||||||
|
verify(documentRepository, atLeastOnce()).save(captor.capture());
|
||||||
|
assertThat(captor.getAllValues().get(0).getTitle()).isEqualTo("Rechnung_1980");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createDocument_keepsDtoTitle_whenProvided() throws Exception {
|
||||||
|
DocumentUpdateDTO dto = new DocumentUpdateDTO();
|
||||||
|
dto.setTitle("Mein Titel");
|
||||||
|
MockMultipartFile file = new MockMultipartFile("file", "scan.pdf", "application/pdf", new byte[]{1});
|
||||||
|
Document saved = Document.builder().id(UUID.randomUUID()).title("Mein Titel")
|
||||||
|
.originalFilename("scan.pdf").status(DocumentStatus.PLACEHOLDER).build();
|
||||||
|
when(documentRepository.save(any())).thenReturn(saved);
|
||||||
|
when(documentRepository.findById(any())).thenReturn(Optional.of(saved));
|
||||||
|
when(fileService.uploadFile(any(), any())).thenReturn(new FileService.UploadResult("path", "hash"));
|
||||||
|
|
||||||
|
ArgumentCaptor<Document> captor = ArgumentCaptor.forClass(Document.class);
|
||||||
|
documentService.createDocument(dto, file);
|
||||||
|
|
||||||
|
verify(documentRepository, atLeastOnce()).save(captor.capture());
|
||||||
|
assertThat(captor.getAllValues().get(0).getTitle()).isEqualTo("Mein Titel");
|
||||||
|
}
|
||||||
|
|
||||||
// ─── createDocument metadataComplete ─────────────────────────────────────
|
// ─── createDocument metadataComplete ─────────────────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ test.describe('Document list', () => {
|
|||||||
|
|
||||||
test('navigation bar shows active state for Dokumente', async ({ page }) => {
|
test('navigation bar shows active state for Dokumente', async ({ page }) => {
|
||||||
const navLink = page.getByRole('navigation').getByRole('link', { name: 'Dokumente' });
|
const navLink = page.getByRole('navigation').getByRole('link', { name: 'Dokumente' });
|
||||||
await expect(navLink).toHaveClass(/text-brand-navy/);
|
await expect(navLink).toHaveClass(/bg-nav-active/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('text search filters the document list', async ({ page }) => {
|
test('text search filters the document list', async ({ page }) => {
|
||||||
@@ -77,12 +77,49 @@ test.describe('Document detail', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe('New document', () => {
|
test.describe('New document', () => {
|
||||||
test('renders the upload form', async ({ page }) => {
|
test('renders the upload form with file input first', async ({ page }) => {
|
||||||
await page.goto('/documents/new');
|
await page.goto('/documents/new');
|
||||||
|
await page.waitForSelector('[data-hydrated]');
|
||||||
await expect(page.getByRole('heading', { name: /Neues Dokument/i })).toBeVisible();
|
await expect(page.getByRole('heading', { name: /Neues Dokument/i })).toBeVisible();
|
||||||
await expect(page.getByLabel('Titel')).toBeVisible();
|
// File input comes before the title field in DOM order
|
||||||
|
const fileInput = page.locator('input[type="file"]');
|
||||||
|
const titleInput = page.getByLabel('Titel');
|
||||||
|
await expect(fileInput).toBeVisible();
|
||||||
|
await expect(titleInput).toBeVisible();
|
||||||
|
const fileBox = await fileInput.boundingBox();
|
||||||
|
const titleBox = await titleInput.boundingBox();
|
||||||
|
expect(fileBox!.y).toBeLessThan(titleBox!.y);
|
||||||
await page.screenshot({ path: 'test-results/e2e/document-new.png' });
|
await page.screenshot({ path: 'test-results/e2e/document-new.png' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('title field is pre-filled from filename when a file is selected', async ({ page }) => {
|
||||||
|
await page.goto('/documents/new');
|
||||||
|
await page.waitForSelector('[data-hydrated]');
|
||||||
|
const PDF_FIXTURE = path.resolve(__dirname, 'fixtures/minimal.pdf');
|
||||||
|
const fileInput = page.locator('input[type="file"]');
|
||||||
|
await fileInput.setInputFiles({
|
||||||
|
name: 'Brief_1965.pdf',
|
||||||
|
mimeType: 'application/pdf',
|
||||||
|
buffer: fs.readFileSync(PDF_FIXTURE)
|
||||||
|
});
|
||||||
|
await expect(page.getByLabel('Titel')).toHaveValue('Brief_1965');
|
||||||
|
await page.screenshot({ path: 'test-results/e2e/document-new-filename-prefill.png' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('typed title is not overwritten when a file is selected', async ({ page }) => {
|
||||||
|
await page.goto('/documents/new');
|
||||||
|
await page.waitForSelector('[data-hydrated]');
|
||||||
|
await page.getByLabel('Titel').fill('Weihnachtsbrief 1965');
|
||||||
|
const PDF_FIXTURE = path.resolve(__dirname, 'fixtures/minimal.pdf');
|
||||||
|
const fileInput = page.locator('input[type="file"]');
|
||||||
|
await fileInput.setInputFiles({
|
||||||
|
name: 'Brief_1965.pdf',
|
||||||
|
mimeType: 'application/pdf',
|
||||||
|
buffer: fs.readFileSync(PDF_FIXTURE)
|
||||||
|
});
|
||||||
|
await expect(page.getByLabel('Titel')).toHaveValue('Weihnachtsbrief 1965');
|
||||||
|
await page.screenshot({ path: 'test-results/e2e/document-new-title-not-overwritten.png' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Document creation', () => {
|
test.describe('Document creation', () => {
|
||||||
@@ -91,12 +128,27 @@ test.describe('Document creation', () => {
|
|||||||
await page.waitForSelector('[data-hydrated]');
|
await page.waitForSelector('[data-hydrated]');
|
||||||
|
|
||||||
await page.getByLabel('Titel').fill('E2E Testbrief');
|
await page.getByLabel('Titel').fill('E2E Testbrief');
|
||||||
await page.getByRole('button', { name: /Speichern/i }).click();
|
await page.getByRole('button', { name: 'Speichern', exact: true }).click();
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/documents\/[^/]+$/);
|
await expect(page).toHaveURL(/\/documents\/[^/]+$/);
|
||||||
await expect(page.getByRole('heading', { name: 'E2E Testbrief' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'E2E Testbrief' })).toBeVisible();
|
||||||
await page.screenshot({ path: 'test-results/e2e/document-create.png' });
|
await page.screenshot({ path: 'test-results/e2e/document-create.png' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('user saves a document with only a file — title comes from filename', async ({ page }) => {
|
||||||
|
await page.goto('/documents/new');
|
||||||
|
await page.waitForSelector('[data-hydrated]');
|
||||||
|
const PDF_FIXTURE = path.resolve(__dirname, 'fixtures/minimal.pdf');
|
||||||
|
await page.locator('input[type="file"]').setInputFiles({
|
||||||
|
name: 'Brief_1965.pdf',
|
||||||
|
mimeType: 'application/pdf',
|
||||||
|
buffer: fs.readFileSync(PDF_FIXTURE)
|
||||||
|
});
|
||||||
|
await page.getByRole('button', { name: 'Speichern', exact: true }).click();
|
||||||
|
await expect(page).toHaveURL(/\/documents\/[^/]+$/);
|
||||||
|
await expect(page.getByRole('heading', { name: 'Brief_1965' })).toBeVisible();
|
||||||
|
await page.screenshot({ path: 'test-results/e2e/document-create-file-only.png' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Document editing', () => {
|
test.describe('Document editing', () => {
|
||||||
@@ -112,10 +164,10 @@ test.describe('Document editing', () => {
|
|||||||
await page.waitForSelector('[data-hydrated]');
|
await page.waitForSelector('[data-hydrated]');
|
||||||
|
|
||||||
await page.getByLabel('Titel').fill('E2E Testbrief (überarbeitet)');
|
await page.getByLabel('Titel').fill('E2E Testbrief (überarbeitet)');
|
||||||
await page.getByRole('button', { name: /Speichern/i }).click();
|
await page.getByRole('button', { name: 'Speichern', exact: true }).click();
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/documents\/[^/]+$/);
|
await expect(page).toHaveURL(/\/documents\/[^/]+$/);
|
||||||
await expect(page.getByText('E2E Testbrief (überarbeitet)')).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'E2E Testbrief (überarbeitet)' })).toBeVisible();
|
||||||
await page.screenshot({ path: 'test-results/e2e/document-edit-save.png' });
|
await page.screenshot({ path: 'test-results/e2e/document-edit-save.png' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -327,10 +379,12 @@ test.describe('PDF annotations — admin', () => {
|
|||||||
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 });
|
||||||
|
|
||||||
// Ensure annotation is visible before enabling annotate mode
|
// Ensure at least one annotation is visible before enabling annotate mode
|
||||||
await expect(page.locator('[data-testid^="annotation-"]').first()).toBeVisible({
|
await expect(page.locator('[data-testid^="annotation-"]').first()).toBeVisible({
|
||||||
timeout: 8000
|
timeout: 8000
|
||||||
});
|
});
|
||||||
|
// Record count now — the draw test may have created more than one annotation
|
||||||
|
const countBefore = await page.locator('[data-testid^="annotation-"]').count();
|
||||||
|
|
||||||
// Enable annotate mode to show delete buttons
|
// Enable annotate mode to show delete buttons
|
||||||
await page.getByRole('button', { name: /^annotieren$/i }).click();
|
await page.getByRole('button', { name: /^annotieren$/i }).click();
|
||||||
@@ -339,7 +393,7 @@ test.describe('PDF annotations — admin', () => {
|
|||||||
await expect(deleteBtn).toBeVisible({ timeout: 8000 });
|
await expect(deleteBtn).toBeVisible({ timeout: 8000 });
|
||||||
await deleteBtn.click();
|
await deleteBtn.click();
|
||||||
|
|
||||||
await expect(page.locator('[data-testid^="annotation-"]')).toHaveCount(0, {
|
await expect(page.locator('[data-testid^="annotation-"]')).toHaveCount(countBefore - 1, {
|
||||||
timeout: 8000
|
timeout: 8000
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -407,7 +461,10 @@ 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 });
|
||||||
|
|
||||||
await expect(page.locator('[data-testid^="annotation-"]')).toHaveCount(0, { timeout: 8000 });
|
// Use :not() to exclude the outdated-notice element whose testid also starts with "annotation-"
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-testid^="annotation-"]:not([data-testid="annotation-outdated-notice"])')
|
||||||
|
).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
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ test.describe('Document history panel', () => {
|
|||||||
await page.goto('/documents/new');
|
await page.goto('/documents/new');
|
||||||
await page.waitForSelector('[data-hydrated]');
|
await page.waitForSelector('[data-hydrated]');
|
||||||
await page.getByLabel('Titel').fill('E2E History Test Dokument');
|
await page.getByLabel('Titel').fill('E2E History Test Dokument');
|
||||||
await page.getByRole('button', { name: /Speichern/i }).click();
|
await page.getByRole('button', { name: 'Speichern', exact: true }).click();
|
||||||
// Wait for redirect to the new document's UUID-based URL (not /documents/new)
|
// Wait for redirect to the new document's UUID-based URL (not /documents/new)
|
||||||
await page.waitForURL(/\/documents\/[0-9a-f-]{36}$/);
|
await page.waitForURL(/\/documents\/[0-9a-f-]{36}$/);
|
||||||
docPath = new URL(page.url()).pathname;
|
docPath = new URL(page.url()).pathname;
|
||||||
@@ -34,7 +34,7 @@ test.describe('Document history panel', () => {
|
|||||||
await page.goto(`${docPath}/edit`);
|
await page.goto(`${docPath}/edit`);
|
||||||
await page.waitForSelector('[data-hydrated]');
|
await page.waitForSelector('[data-hydrated]');
|
||||||
await page.getByLabel('Titel').fill('E2E History Test Dokument (bearbeitet)');
|
await page.getByLabel('Titel').fill('E2E History Test Dokument (bearbeitet)');
|
||||||
await page.getByRole('button', { name: /Speichern/i }).click();
|
await page.getByRole('button', { name: 'Speichern', exact: true }).click();
|
||||||
await page.waitForURL(/\/documents\/[0-9a-f-]{36}$/);
|
await page.waitForURL(/\/documents\/[0-9a-f-]{36}$/);
|
||||||
|
|
||||||
await context.close();
|
await context.close();
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ test.describe('Conversations', () => {
|
|||||||
test('nav link is active on the conversations page', async ({ page }) => {
|
test('nav link is active on the conversations page', async ({ page }) => {
|
||||||
await page.goto('/conversations');
|
await page.goto('/conversations');
|
||||||
const navLink = page.getByRole('link', { name: 'Konversationen' });
|
const navLink = page.getByRole('link', { name: 'Konversationen' });
|
||||||
await expect(navLink).toHaveClass(/text-brand-navy/);
|
await expect(navLink).toHaveClass(/bg-nav-active/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sort toggle changes the button label', async ({ page }) => {
|
test('sort toggle changes the button label', async ({ page }) => {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
"form_placeholder_location": "z.B. Berlin, Wien…",
|
"form_placeholder_location": "z.B. Berlin, Wien…",
|
||||||
"form_label_sender": "Absender",
|
"form_label_sender": "Absender",
|
||||||
"form_label_receivers": "Empfänger",
|
"form_label_receivers": "Empfänger",
|
||||||
"form_label_title": "Titel *",
|
"form_label_title": "Titel",
|
||||||
"form_label_tags": "Schlagworte",
|
"form_label_tags": "Schlagworte",
|
||||||
"form_label_content": "Inhalt",
|
"form_label_content": "Inhalt",
|
||||||
"form_placeholder_content": "Kurze Beschreibung des Inhalts…",
|
"form_placeholder_content": "Kurze Beschreibung des Inhalts…",
|
||||||
@@ -75,6 +75,7 @@
|
|||||||
"doc_file_replace_label": "Neue Datei hochladen",
|
"doc_file_replace_label": "Neue Datei hochladen",
|
||||||
"doc_file_replace_note": "(ersetzt die aktuelle Datei)",
|
"doc_file_replace_note": "(ersetzt die aktuelle Datei)",
|
||||||
"doc_current_file_label": "Aktuelle Datei:",
|
"doc_current_file_label": "Aktuelle Datei:",
|
||||||
|
"doc_more_details": "Weitere Details",
|
||||||
"doc_new_heading": "Neues Dokument",
|
"doc_new_heading": "Neues Dokument",
|
||||||
"doc_edit_heading": "Bearbeiten",
|
"doc_edit_heading": "Bearbeiten",
|
||||||
"doc_section_details": "Details",
|
"doc_section_details": "Details",
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
"form_placeholder_location": "e.g. Berlin, Vienna…",
|
"form_placeholder_location": "e.g. Berlin, Vienna…",
|
||||||
"form_label_sender": "Sender",
|
"form_label_sender": "Sender",
|
||||||
"form_label_receivers": "Recipients",
|
"form_label_receivers": "Recipients",
|
||||||
"form_label_title": "Title *",
|
"form_label_title": "Title",
|
||||||
"form_label_tags": "Tags",
|
"form_label_tags": "Tags",
|
||||||
"form_label_content": "Content",
|
"form_label_content": "Content",
|
||||||
"form_placeholder_content": "Brief description of the content…",
|
"form_placeholder_content": "Brief description of the content…",
|
||||||
@@ -75,6 +75,7 @@
|
|||||||
"doc_file_replace_label": "Upload new file",
|
"doc_file_replace_label": "Upload new file",
|
||||||
"doc_file_replace_note": "(replaces the current file)",
|
"doc_file_replace_note": "(replaces the current file)",
|
||||||
"doc_current_file_label": "Current file:",
|
"doc_current_file_label": "Current file:",
|
||||||
|
"doc_more_details": "More details",
|
||||||
"doc_new_heading": "New document",
|
"doc_new_heading": "New document",
|
||||||
"doc_edit_heading": "Edit",
|
"doc_edit_heading": "Edit",
|
||||||
"doc_section_details": "Details",
|
"doc_section_details": "Details",
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
"form_placeholder_location": "p.ej. Berlín, Viena…",
|
"form_placeholder_location": "p.ej. Berlín, Viena…",
|
||||||
"form_label_sender": "Remitente",
|
"form_label_sender": "Remitente",
|
||||||
"form_label_receivers": "Destinatarios",
|
"form_label_receivers": "Destinatarios",
|
||||||
"form_label_title": "Título *",
|
"form_label_title": "Título",
|
||||||
"form_label_tags": "Etiquetas",
|
"form_label_tags": "Etiquetas",
|
||||||
"form_label_content": "Contenido",
|
"form_label_content": "Contenido",
|
||||||
"form_placeholder_content": "Breve descripción del contenido…",
|
"form_placeholder_content": "Breve descripción del contenido…",
|
||||||
@@ -75,6 +75,7 @@
|
|||||||
"doc_file_replace_label": "Subir nuevo archivo",
|
"doc_file_replace_label": "Subir nuevo archivo",
|
||||||
"doc_file_replace_note": "(reemplaza el archivo actual)",
|
"doc_file_replace_note": "(reemplaza el archivo actual)",
|
||||||
"doc_current_file_label": "Archivo actual:",
|
"doc_current_file_label": "Archivo actual:",
|
||||||
|
"doc_more_details": "Más detalles",
|
||||||
"doc_new_heading": "Nuevo documento",
|
"doc_new_heading": "Nuevo documento",
|
||||||
"doc_edit_heading": "Editar",
|
"doc_edit_heading": "Editar",
|
||||||
"doc_section_details": "Detalles",
|
"doc_section_details": "Detalles",
|
||||||
|
|||||||
@@ -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-white hover:bg-primary/80 disabled:opacity-40"
|
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"
|
||||||
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-white hover:bg-primary/80 disabled:opacity-40"
|
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"
|
||||||
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-white hover:bg-primary/80 disabled:opacity-40"
|
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"
|
||||||
disabled={posting || !newText.trim()}
|
disabled={posting || !newText.trim()}
|
||||||
onclick={postComment}
|
onclick={postComment}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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-white'
|
? 'bg-primary text-primary-fg'
|
||||||
: 'border border-primary text-ink hover:bg-primary hover:text-white'}"
|
: 'border border-primary text-ink hover:bg-primary hover:text-primary-fg'}"
|
||||||
>
|
>
|
||||||
<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"
|
||||||
@@ -118,7 +118,7 @@ const compactMeta = $derived.by(() => {
|
|||||||
{#if canWrite}
|
{#if canWrite}
|
||||||
<a
|
<a
|
||||||
href="/documents/{doc.id}/edit"
|
href="/documents/{doc.id}/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"
|
||||||
|
|||||||
@@ -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-white 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-primary-fg transition hover:bg-primary/80 disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
>
|
>
|
||||||
{m.history_compare_apply()}
|
{m.history_compare_apply()}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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-white"
|
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"
|
||||||
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-white"
|
class="flex h-8 w-8 items-center justify-center rounded-full bg-primary font-serif text-sm text-primary-fg"
|
||||||
>
|
>
|
||||||
{doc.sender.firstName[0]}{doc.sender.lastName[0]}
|
{doc.sender.firstName[0]}{doc.sender.lastName[0]}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ let {
|
|||||||
initialDocumentLocation = '',
|
initialDocumentLocation = '',
|
||||||
initialSummary = '',
|
initialSummary = '',
|
||||||
titleRequired = false,
|
titleRequired = false,
|
||||||
suggestedTitle = ''
|
suggestedTitle = '',
|
||||||
|
hideTitle = false
|
||||||
}: {
|
}: {
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
initialTitle?: string;
|
initialTitle?: string;
|
||||||
@@ -17,17 +18,12 @@ let {
|
|||||||
initialSummary?: string;
|
initialSummary?: string;
|
||||||
titleRequired?: boolean;
|
titleRequired?: boolean;
|
||||||
suggestedTitle?: string;
|
suggestedTitle?: string;
|
||||||
|
hideTitle?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let titleValue = $state(untrack(() => initialTitle));
|
|
||||||
let titleDirty = $state(false);
|
let titleDirty = $state(false);
|
||||||
|
let titleOverride = $state(untrack(() => initialTitle));
|
||||||
$effect(() => {
|
let titleValue = $derived(titleDirty ? titleOverride : suggestedTitle || titleOverride);
|
||||||
const suggested = suggestedTitle;
|
|
||||||
if (suggested && !untrack(() => titleDirty)) {
|
|
||||||
titleValue = suggested;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="rounded-sm border border-line bg-surface p-6 shadow-sm">
|
<div class="rounded-sm border border-line bg-surface p-6 shadow-sm">
|
||||||
@@ -36,25 +32,27 @@ $effect(() => {
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<!-- Titel -->
|
{#if !hideTitle}
|
||||||
<div>
|
<!-- Titel -->
|
||||||
<label for="title" class="mb-1 block text-sm font-medium text-ink-2"
|
<div>
|
||||||
>{m.form_label_title()}{#if titleRequired}
|
<label for="title" class="mb-1 block text-sm font-medium text-ink-2"
|
||||||
*{/if}</label
|
>{m.form_label_title()}{#if titleRequired}
|
||||||
>
|
*{/if}</label
|
||||||
<input
|
>
|
||||||
id="title"
|
<input
|
||||||
type="text"
|
id="title"
|
||||||
name="title"
|
type="text"
|
||||||
value={titleValue}
|
name="title"
|
||||||
oninput={(e) => {
|
value={titleValue}
|
||||||
titleValue = (e.target as HTMLInputElement).value;
|
oninput={(e) => {
|
||||||
titleDirty = true;
|
titleOverride = (e.target as HTMLInputElement).value;
|
||||||
}}
|
titleDirty = true;
|
||||||
required={titleRequired}
|
}}
|
||||||
class="block w-full rounded border border-line p-2 text-sm shadow-sm focus:border-ink focus:ring-ink"
|
required={titleRequired}
|
||||||
/>
|
class="block w-full rounded border border-line p-2 text-sm shadow-sm focus:border-ink focus:ring-ink"
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Aufbewahrungsort -->
|
<!-- Aufbewahrungsort -->
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -39,9 +39,6 @@ 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 -->
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ $effect(() => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="mx-auto max-w-7xl py-8 font-sans sm:px-6 lg:px-8">
|
<main class="mx-auto max-w-7xl px-4 py-8 font-sans sm:px-6 lg:px-8">
|
||||||
<SearchFilterBar
|
<SearchFilterBar
|
||||||
bind:q={q}
|
bind:q={q}
|
||||||
bind:from={from}
|
bind:from={from}
|
||||||
|
|||||||
@@ -1,19 +1,42 @@
|
|||||||
<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';
|
||||||
|
|
||||||
let { isAdmin = false }: { isAdmin?: boolean } = $props();
|
let { isAdmin = false }: { isAdmin?: boolean } = $props();
|
||||||
|
|
||||||
|
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">
|
<div class="flex items-center">
|
||||||
<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="font-sans text-xl font-bold tracking-widest text-ink uppercase"
|
<span class="hidden font-sans text-xl font-bold tracking-widest text-ink uppercase sm:inline"
|
||||||
>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="/"
|
||||||
@@ -56,4 +79,108 @@ let { isAdmin = false }: { isAdmin?: boolean } = $props();
|
|||||||
</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}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -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-white"
|
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"
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|||||||
@@ -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-white 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-primary-fg transition-opacity hover:opacity-80"
|
||||||
>
|
>
|
||||||
{userInitials}
|
{userInitials}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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 py-8 font-sans sm:px-6 lg:px-8">
|
<div class="mx-auto max-w-7xl px-4 py-8 font-sans sm:px-6 lg:px-8">
|
||||||
<div class="mb-8 flex items-center justify-between">
|
<div class="mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm: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-4 py-2 text-sm font-bold tracking-wide uppercase transition {activeTab ===
|
class="rounded-md px-2 py-2 text-sm font-bold tracking-wide uppercase transition sm:px-4 {activeTab ===
|
||||||
'users'
|
'users'
|
||||||
? 'bg-primary text-white'
|
? 'bg-primary text-primary-fg'
|
||||||
: '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-4 py-2 text-sm font-bold tracking-wide uppercase transition {activeTab ===
|
class="rounded-md px-2 py-2 text-sm font-bold tracking-wide uppercase transition sm:px-4 {activeTab ===
|
||||||
'groups'
|
'groups'
|
||||||
? 'bg-primary text-white'
|
? 'bg-primary text-primary-fg'
|
||||||
: '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-4 py-2 text-sm font-bold tracking-wide uppercase transition {activeTab ===
|
class="rounded-md px-2 py-2 text-sm font-bold tracking-wide uppercase transition sm:px-4 {activeTab ===
|
||||||
'tags'
|
'tags'
|
||||||
? 'bg-primary text-white'
|
? 'bg-primary text-primary-fg'
|
||||||
: '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-4 py-2 text-sm font-bold tracking-wide uppercase transition {activeTab ===
|
class="rounded-md px-2 py-2 text-sm font-bold tracking-wide uppercase transition sm:px-4 {activeTab ===
|
||||||
'system'
|
'system'
|
||||||
? 'bg-primary text-white'
|
? 'bg-primary text-primary-fg'
|
||||||
: '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
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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-white 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-primary-fg uppercase hover:bg-accent hover:text-ink md:w-auto"
|
||||||
>
|
>
|
||||||
{m.btn_create()}
|
{m.btn_create()}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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-white 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-primary-fg 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-white 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-primary-fg 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>
|
||||||
|
|||||||
@@ -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-white 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-primary-fg 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" />
|
||||||
|
|||||||
@@ -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-white uppercase transition-opacity hover:opacity-80"
|
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"
|
||||||
>
|
>
|
||||||
{m.btn_save()}
|
{m.btn_save()}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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-white uppercase transition-opacity hover:opacity-80"
|
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"
|
||||||
>
|
>
|
||||||
{m.btn_create()}
|
{m.btn_create()}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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-white 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-primary-fg 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-white"
|
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"
|
||||||
>
|
>
|
||||||
<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>
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ $effect(() => {
|
|||||||
|
|
||||||
const LS_KEY_HEIGHT = 'doc-panel-height';
|
const LS_KEY_HEIGHT = 'doc-panel-height';
|
||||||
const LS_KEY_TAB = 'doc-panel-tab';
|
const LS_KEY_TAB = 'doc-panel-tab';
|
||||||
|
const LS_KEY_OPEN = 'doc-panel-open';
|
||||||
|
|
||||||
let panelOpen = $state(false);
|
let panelOpen = $state(false);
|
||||||
let panelHeight = $state(0); // set to full height on mount
|
let panelHeight = $state(0); // set to full height on mount
|
||||||
@@ -79,6 +80,7 @@ onMount(() => {
|
|||||||
|
|
||||||
const savedHeight = localStorage.getItem(LS_KEY_HEIGHT);
|
const savedHeight = localStorage.getItem(LS_KEY_HEIGHT);
|
||||||
const savedTab = localStorage.getItem(LS_KEY_TAB);
|
const savedTab = localStorage.getItem(LS_KEY_TAB);
|
||||||
|
const savedOpen = localStorage.getItem(LS_KEY_OPEN);
|
||||||
|
|
||||||
if (savedTab && ['metadata', 'transcription', 'discussion', 'history'].includes(savedTab)) {
|
if (savedTab && ['metadata', 'transcription', 'discussion', 'history'].includes(savedTab)) {
|
||||||
activeTab = savedTab as DocumentPanelTab;
|
activeTab = savedTab as DocumentPanelTab;
|
||||||
@@ -90,6 +92,14 @@ onMount(() => {
|
|||||||
if (!isNaN(h) && h >= 80) panelHeight = h;
|
if (!isNaN(h) && h >= 80) panelHeight = h;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (savedOpen === 'true') {
|
||||||
|
panelOpen = true;
|
||||||
|
} else if (savedOpen === null && !doc?.filePath) {
|
||||||
|
// No prior state and no file — open to metadata so the panel is immediately useful.
|
||||||
|
panelOpen = true;
|
||||||
|
activeTab = 'metadata';
|
||||||
|
}
|
||||||
|
|
||||||
localStorageRestored = true;
|
localStorageRestored = true;
|
||||||
|
|
||||||
function onKeyDown(e: KeyboardEvent) {
|
function onKeyDown(e: KeyboardEvent) {
|
||||||
@@ -111,6 +121,7 @@ $effect(() => {
|
|||||||
if (!localStorageRestored) return;
|
if (!localStorageRestored) return;
|
||||||
localStorage.setItem(LS_KEY_HEIGHT, String(panelHeight));
|
localStorage.setItem(LS_KEY_HEIGHT, String(panelHeight));
|
||||||
localStorage.setItem(LS_KEY_TAB, activeTab);
|
localStorage.setItem(LS_KEY_TAB, activeTab);
|
||||||
|
localStorage.setItem(LS_KEY_OPEN, String(panelOpen));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -68,5 +68,6 @@ let selectedReceivers = $state(doc.receivers ?? []);
|
|||||||
<SaveBar docId={doc.id} />
|
<SaveBar docId={doc.id} />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<form id="mark-for-review-form" method="POST" action="?/markForReview" use:enhance></form>
|
||||||
<form id="delete-form" method="POST" action="?/delete" use:enhance></form>
|
<form id="delete-form" method="POST" action="?/delete" use:enhance></form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
import { enhance } from '$app/forms';
|
|
||||||
|
|
||||||
let { docId }: { docId: string } = $props();
|
let { docId }: { docId: string } = $props();
|
||||||
|
|
||||||
@@ -8,75 +7,79 @@ let confirmDelete = $state(false);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
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)]"
|
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"
|
||||||
>
|
>
|
||||||
<!-- Left: delete -->
|
<!-- Desktop: delete left, cancel+buttons right -->
|
||||||
<div class="flex items-center gap-3">
|
<!-- Mobile: action buttons stacked full-width, delete+cancel row at bottom -->
|
||||||
{#if confirmDelete}
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<span class="font-sans text-sm text-red-700">{m.doc_delete_confirm()}</span>
|
<!-- Primary actions first (top on mobile, right on desktop) -->
|
||||||
|
<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="delete-form"
|
form="mark-for-review-form"
|
||||||
class="rounded bg-red-600 px-4 py-1.5 text-sm font-bold text-white transition-colors hover:bg-red-700"
|
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"
|
||||||
>
|
>
|
||||||
{m.btn_delete()}
|
{m.btn_mark_for_review()}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="submit"
|
||||||
onclick={() => (confirmDelete = false)}
|
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"
|
||||||
class="text-sm text-ink-2 transition-colors hover:text-ink"
|
|
||||||
>
|
>
|
||||||
{m.btn_cancel()}
|
{m.btn_save()}
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
</div>
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- Right: cancel + mark for review + save -->
|
<!-- Secondary: delete + cancel (row on both mobile and desktop) -->
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center justify-between sm:justify-start sm:gap-4">
|
||||||
<a
|
{#if confirmDelete}
|
||||||
href="/documents/{docId}"
|
<span class="font-sans text-sm text-red-700">{m.doc_delete_confirm()}</span>
|
||||||
class="text-sm font-medium text-ink-2 transition-colors hover:text-ink"
|
<div class="flex items-center gap-3">
|
||||||
>
|
<button
|
||||||
{m.btn_cancel()}
|
type="submit"
|
||||||
</a>
|
form="delete-form"
|
||||||
<button
|
class="rounded bg-red-600 px-4 py-1.5 text-sm font-bold text-white transition-colors hover:bg-red-700"
|
||||||
type="submit"
|
>
|
||||||
form="mark-for-review-form"
|
{m.btn_delete()}
|
||||||
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>
|
||||||
>
|
<button
|
||||||
{m.btn_mark_for_review()}
|
type="button"
|
||||||
</button>
|
onclick={() => (confirmDelete = false)}
|
||||||
<button
|
class="text-sm text-ink-2 transition-colors hover:text-ink"
|
||||||
type="submit"
|
>
|
||||||
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_cancel()}
|
||||||
>
|
</button>
|
||||||
{m.btn_save()}
|
</div>
|
||||||
</button>
|
{: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>
|
||||||
|
<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>
|
||||||
|
|
||||||
<form id="mark-for-review-form" method="POST" action="?/markForReview" use:enhance></form>
|
|
||||||
|
|||||||
@@ -17,6 +17,30 @@ let selectedReceivers: { id: string; firstName: string; lastName: string }[] = $
|
|||||||
);
|
);
|
||||||
|
|
||||||
let parsedSuggestion = $state<FilenameParseResult>({});
|
let parsedSuggestion = $state<FilenameParseResult>({});
|
||||||
|
|
||||||
|
// Title is derived from the filename suggestion unless the user has typed something
|
||||||
|
let titleDirty = $state(false);
|
||||||
|
let titleOverride = $state('');
|
||||||
|
let titleValue = $derived(
|
||||||
|
titleDirty ? titleOverride : (parsedSuggestion.suggestedTitle ?? titleOverride)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Details panel: starts open when prefill data is present or a form error occurred.
|
||||||
|
// Auto-opens when filename parsing finds a date/sender, but never force-closes — user
|
||||||
|
// can always collapse the section manually.
|
||||||
|
let detailsOpen = $state(
|
||||||
|
!!(
|
||||||
|
untrack(() => data.initialSenderId) ||
|
||||||
|
untrack(() => data.initialReceivers).length > 0 ||
|
||||||
|
untrack(() => form)?.error
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (parsedSuggestion.dateIso || senderId || selectedReceivers.length > 0) {
|
||||||
|
detailsOpen = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto max-w-4xl px-4 py-8">
|
<div class="mx-auto max-w-4xl px-4 py-8">
|
||||||
@@ -49,47 +73,82 @@ let parsedSuggestion = $state<FilenameParseResult>({});
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<form method="POST" enctype="multipart/form-data" use:enhance class="space-y-6 pb-20">
|
<form method="POST" enctype="multipart/form-data" use:enhance class="space-y-6 pb-20">
|
||||||
<WhoWhenSection
|
<!-- File upload — prominent, at the top -->
|
||||||
bind:senderId={senderId}
|
|
||||||
bind:selectedReceivers={selectedReceivers}
|
|
||||||
initialSenderName={data.initialSenderName}
|
|
||||||
suggestedDateIso={parsedSuggestion.dateIso ?? ''}
|
|
||||||
suggestedSenderName={parsedSuggestion.personName ?? ''}
|
|
||||||
/>
|
|
||||||
<DescriptionSection
|
|
||||||
bind:tags={tags}
|
|
||||||
titleRequired={true}
|
|
||||||
suggestedTitle={parsedSuggestion.suggestedTitle ?? ''}
|
|
||||||
/>
|
|
||||||
<TranscriptionSection />
|
|
||||||
<FileSectionNew onfileParsed={(r) => (parsedSuggestion = r)} />
|
<FileSectionNew onfileParsed={(r) => (parsedSuggestion = r)} />
|
||||||
|
|
||||||
|
<!-- Standalone title card -->
|
||||||
|
<div class="rounded-sm border border-line bg-surface p-6 shadow-sm">
|
||||||
|
<label for="new-title" class="mb-1 block text-sm font-medium text-ink-2"
|
||||||
|
>{m.form_label_title()}</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="new-title"
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
value={titleValue}
|
||||||
|
oninput={(e) => {
|
||||||
|
titleOverride = (e.target as HTMLInputElement).value;
|
||||||
|
titleDirty = true;
|
||||||
|
}}
|
||||||
|
class="block w-full rounded border border-line p-2 text-sm shadow-sm focus:border-ink focus:ring-ink"
|
||||||
|
placeholder="Titel eingeben…"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Collapsible further details -->
|
||||||
|
<details
|
||||||
|
bind:open={detailsOpen}
|
||||||
|
class="group rounded-sm border border-line bg-surface shadow-sm"
|
||||||
|
>
|
||||||
|
<summary class="cursor-pointer list-none px-6 py-4">
|
||||||
|
<span class="text-xs font-bold tracking-widest text-ink-3 uppercase"
|
||||||
|
>{m.doc_more_details()}</span
|
||||||
|
>
|
||||||
|
</summary>
|
||||||
|
<div class="space-y-6 px-0 pb-6">
|
||||||
|
<WhoWhenSection
|
||||||
|
bind:senderId={senderId}
|
||||||
|
bind:selectedReceivers={selectedReceivers}
|
||||||
|
initialSenderName={data.initialSenderName}
|
||||||
|
suggestedDateIso={parsedSuggestion.dateIso ?? ''}
|
||||||
|
suggestedSenderName={parsedSuggestion.personName ?? ''}
|
||||||
|
/>
|
||||||
|
<DescriptionSection bind:tags={tags} hideTitle={true} />
|
||||||
|
<TranscriptionSection />
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<!-- Sticky Save Bar -->
|
<!-- Sticky Save Bar -->
|
||||||
<div
|
<div
|
||||||
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)]"
|
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"
|
||||||
>
|
>
|
||||||
<a href="/" class="text-sm font-medium text-ink-2 transition-colors hover:text-ink">
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
{m.btn_cancel()}
|
<a
|
||||||
</a>
|
href="/"
|
||||||
<div class="flex items-center gap-3">
|
class="order-last text-center text-sm font-medium text-ink-2 transition-colors hover:text-ink sm:order-first sm:text-left"
|
||||||
<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_save()}
|
{m.btn_cancel()}
|
||||||
</button>
|
</a>
|
||||||
<button
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
|
||||||
type="submit"
|
<button
|
||||||
name="metadataComplete"
|
type="submit"
|
||||||
value="true"
|
name="metadataComplete"
|
||||||
formaction="?/saveReviewed"
|
value="false"
|
||||||
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"
|
formaction="?/save"
|
||||||
>
|
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()}
|
>
|
||||||
</button>
|
{m.btn_save()}
|
||||||
|
</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>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
import { parseFilename, type FilenameParseResult } from '$lib/utils/filename';
|
import { parseFilename, stripExtension, type FilenameParseResult } from '$lib/utils/filename';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
onfileParsed
|
onfileParsed
|
||||||
@@ -8,31 +8,52 @@ let {
|
|||||||
onfileParsed?: (result: FilenameParseResult) => void;
|
onfileParsed?: (result: FilenameParseResult) => void;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
|
let selectedFilename = $state<string | null>(null);
|
||||||
|
|
||||||
function handleFileChange(e: Event) {
|
function handleFileChange(e: Event) {
|
||||||
const file = (e.target as HTMLInputElement).files?.[0];
|
const file = (e.target as HTMLInputElement).files?.[0];
|
||||||
if (file) onfileParsed?.(parseFilename(file.name));
|
if (!file) return;
|
||||||
|
selectedFilename = file.name;
|
||||||
|
const parsed = parseFilename(file.name);
|
||||||
|
const result: FilenameParseResult = {
|
||||||
|
...parsed,
|
||||||
|
suggestedTitle: parsed.suggestedTitle ?? stripExtension(file.name)
|
||||||
|
};
|
||||||
|
onfileParsed?.(result);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="rounded-sm border border-line bg-surface p-6 shadow-sm">
|
<div class="rounded-sm border border-line bg-surface shadow-sm">
|
||||||
<h2 class="mb-5 text-xs font-bold tracking-widest text-ink-3 uppercase">
|
<div class="border-b border-line px-6 py-4">
|
||||||
{m.doc_section_file()}
|
<h2 class="text-xs font-bold tracking-widest text-ink-3 uppercase">
|
||||||
</h2>
|
{m.doc_section_file()}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label for="file-upload" class="mb-1 block text-sm font-medium text-ink-2">
|
<label
|
||||||
{m.doc_file_upload_label()}
|
for="file-upload"
|
||||||
<span class="font-normal text-ink-3">({m.doc_file_upload_note()})</span>
|
class="flex cursor-pointer flex-col items-center gap-3 px-6 py-10 transition-colors hover:bg-muted/40"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="h-10 w-10 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-ink-1 text-sm font-medium">{selectedFilename}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="text-sm font-medium text-ink-2">{m.doc_file_upload_label()}</span>
|
||||||
|
<span class="text-xs text-ink-3">{m.doc_file_upload_note()}</span>
|
||||||
|
{/if}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input id="file-upload" type="file" name="file" onchange={handleFileChange} class="sr-only" />
|
||||||
id="file-upload"
|
|
||||||
type="file"
|
|
||||||
name="file"
|
|
||||||
onchange={handleFileChange}
|
|
||||||
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>
|
||||||
|
|||||||
@@ -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-gray-500 uppercase transition-colors hover:text-brand-navy"
|
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"
|
||||||
>
|
>
|
||||||
<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-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="bg-primary px-5 py-2 font-sans text-xs font-bold tracking-widest text-primary-fg uppercase transition-colors hover:bg-primary/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-brand-sand border bg-white shadow-sm">
|
<div class="border border-line bg-white shadow-sm">
|
||||||
<ul class="divide-brand-sand divide-y">
|
<ul class="divide-y divide-line-2">
|
||||||
{#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">
|
||||||
|
|||||||
@@ -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-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="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"
|
||||||
>
|
>
|
||||||
{m.btn_save_and_mark_reviewed()}
|
{m.btn_save_and_mark_reviewed()}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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-gray-500">
|
<p class="mt-2 max-w-xs font-sans text-sm text-ink-2">
|
||||||
{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-brand-navy px-6 py-2 font-sans text-xs font-bold tracking-widest text-white uppercase transition-colors hover:bg-brand-navy/90"
|
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"
|
||||||
>
|
>
|
||||||
{m.btn_back_to_overview()}
|
{m.btn_back_to_overview()}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="/enrich"
|
href="/enrich"
|
||||||
class="font-sans text-xs text-gray-400 underline-offset-4 transition-colors hover:text-brand-navy hover:underline"
|
class="font-sans text-xs text-ink-2 underline-offset-4 transition-colors hover:text-ink hover:underline"
|
||||||
>
|
>
|
||||||
{m.enrich_back_to_list()}
|
{m.enrich_back_to_list()}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ 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 -->
|
||||||
@@ -57,7 +54,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-white 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-primary-fg uppercase transition-colors hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
{m.forgot_password_submit()}
|
{m.forgot_password_submit()}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
--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;
|
||||||
@@ -58,7 +57,6 @@
|
|||||||
--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);
|
||||||
}
|
}
|
||||||
@@ -74,8 +72,8 @@
|
|||||||
--c-line-2: #eeede8;
|
--c-line-2: #eeede8;
|
||||||
|
|
||||||
--c-ink: #012851;
|
--c-ink: #012851;
|
||||||
--c-ink-2: #6b7280;
|
--c-ink-2: #4b5563; /* gray-600 — 7.6:1 on white, 6.6:1 on canvas — WCAG AA ✓ */
|
||||||
--c-ink-3: #9ca3af;
|
--c-ink-3: #6b7280; /* gray-500 — 4.8:1 on white — WCAG AA ✓; use only on surface, not canvas */
|
||||||
|
|
||||||
--c-accent: #a1dcd8;
|
--c-accent: #a1dcd8;
|
||||||
--c-accent-bg: rgba(161, 220, 216, 0.15);
|
--c-accent-bg: rgba(161, 220, 216, 0.15);
|
||||||
@@ -102,8 +100,8 @@
|
|||||||
--c-line-2: #222222;
|
--c-line-2: #222222;
|
||||||
|
|
||||||
--c-ink: #f0efe9;
|
--c-ink: #f0efe9;
|
||||||
--c-ink-2: #9ca3af;
|
--c-ink-2: #9ca3af; /* gray-400 — 7.5:1 on dark surface — WCAG AAA ✓ */
|
||||||
--c-ink-3: #6b7280;
|
--c-ink-3: #8b97a5; /* gray-450 — 6.5:1 on dark surface — WCAG AA ✓ */
|
||||||
|
|
||||||
--c-accent: #00c7b1;
|
--c-accent: #00c7b1;
|
||||||
--c-accent-bg: rgba(0, 199, 177, 0.12);
|
--c-accent-bg: rgba(0, 199, 177, 0.12);
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ 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)}
|
||||||
@@ -85,7 +82,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-white 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-primary-fg uppercase transition-colors hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
{m.login_btn_submit()}
|
{m.login_btn_submit()}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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-white 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-primary-fg transition-colors group-hover:bg-accent group-hover:text-ink"
|
||||||
>
|
>
|
||||||
{person.firstName[0]}{person.lastName[0]}
|
{person.firstName[0]}{person.lastName[0]}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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-white uppercase transition-colors hover:bg-primary/80"
|
class="rounded bg-primary px-5 py-2 text-sm font-bold tracking-widest text-primary-fg uppercase transition-colors hover:bg-primary/80"
|
||||||
>
|
>
|
||||||
{m.btn_save()}
|
{m.btn_save()}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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-white">
|
<span class="rounded-full bg-primary px-2 py-1 text-xs font-bold text-primary-fg">
|
||||||
{documents.length}
|
{documents.length}
|
||||||
</span>
|
</span>
|
||||||
{#if yearRange}
|
{#if yearRange}
|
||||||
|
|||||||
@@ -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-white uppercase transition-colors hover:bg-primary/80"
|
class="rounded bg-primary px-6 py-2 text-sm font-bold tracking-widest text-primary-fg uppercase transition-colors hover:bg-primary/80"
|
||||||
>
|
>
|
||||||
{m.btn_create()}
|
{m.btn_create()}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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-white 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-primary-fg uppercase transition-opacity hover:opacity-80"
|
||||||
>
|
>
|
||||||
{m.btn_save()}
|
{m.btn_save()}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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-white 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-primary-fg uppercase transition-opacity hover:opacity-80"
|
||||||
>
|
>
|
||||||
{m.btn_save()}
|
{m.btn_save()}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -12,9 +12,6 @@ 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 -->
|
||||||
@@ -86,7 +83,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-white 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-primary-fg uppercase transition-colors hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
{m.reset_password_submit()}
|
{m.reset_password_submit()}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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-white"
|
class="flex h-16 w-16 items-center justify-center rounded-full bg-primary text-primary-fg"
|
||||||
>
|
>
|
||||||
<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-white"
|
class="flex h-16 w-16 items-center justify-center rounded-full bg-primary text-primary-fg"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
class="h-8 w-8"
|
class="h-8 w-8"
|
||||||
|
|||||||
Reference in New Issue
Block a user