refactor(dashboard): ReaderDraftsModule mint left-border, card-head, row structure (TDD, #483)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-08 17:10:20 +02:00
parent e598f5a506
commit 60326cfb0a
2 changed files with 79 additions and 13 deletions

View File

@@ -12,24 +12,47 @@ interface Props {
const { drafts }: Props = $props(); const { drafts }: Props = $props();
</script> </script>
<div class="rounded-sm border border-line bg-surface p-6 shadow-sm"> <div
<h2 class="mb-5 text-xs font-bold tracking-widest text-ink-3 uppercase"> class="flex flex-col overflow-hidden rounded-sm border border-l-[3px] border-line border-l-brand-mint bg-surface"
{m.dashboard_reader_drafts_heading()} >
</h2> <!-- Card-head -->
<div class="flex items-center border-b border-line px-3 py-1.5">
<h3 class="text-[11px] font-bold tracking-[.12em] text-ink-3 uppercase">
{m.dashboard_reader_drafts_heading()}
</h3>
</div>
{#if drafts.length === 0} {#if drafts.length === 0}
<p class="font-sans text-sm text-ink-3">{m.dashboard_reader_drafts_empty()}</p> <p class="px-3 py-3 font-sans text-sm text-ink-3">{m.dashboard_reader_drafts_empty()}</p>
{:else} {:else}
<ul class="flex flex-col gap-2"> <ul class="flex flex-col">
{#each drafts as draft (draft.id)} {#each drafts as draft (draft.id)}
<li> <li>
<a <a
href="/geschichten/{draft.id}/edit" href="/geschichten/{draft.id}/edit"
class="flex min-h-[44px] items-center justify-between gap-4 rounded-sm py-2 transition-colors hover:text-brand-mint focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:outline-none" class="flex min-h-[44px] items-center justify-between border-b border-line/50 px-3 py-1.5 last:border-b-0 hover:bg-muted focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:outline-none"
> >
<span class="text-ink-1 truncate font-serif text-sm">{draft.title}</span> <span class="flex min-w-0 flex-col">
<span class="shrink-0 font-sans text-xs text-ink-3"> <span class="truncate font-serif text-sm text-brand-navy">{draft.title}</span>
{relativeTimeDe(new Date(draft.updatedAt))} <span class="text-[11px] text-ink-3">
{m.dashboard_reader_draft_meta({ relative: relativeTimeDe(new Date(draft.updatedAt)) })}
</span>
</span> </span>
<svg
width="7"
height="7"
viewBox="0 0 7 7"
fill="none"
aria-hidden="true"
class="shrink-0 text-ink-3"
>
<path
d="M1.5 1 L5.5 3.5 L1.5 6"
stroke="currentColor"
stroke-width="1.5"
fill="none"
/>
</svg>
</a> </a>
</li> </li>
{/each} {/each}

View File

@@ -36,10 +36,12 @@ describe('ReaderDraftsModule', () => {
await expect.element(link2).toHaveAttribute('href', '/geschichten/g2/edit'); await expect.element(link2).toHaveAttribute('href', '/geschichten/g2/edit');
}); });
it('shows heading "Meine Entwürfe"', async () => { it('shows heading as h3 (not h2)', async () => {
render(ReaderDraftsModule, { drafts: [draft1] }); render(ReaderDraftsModule, { drafts: [draft1] });
const heading = page.getByRole('heading', { name: /Meine Entwürfe/i }); const h3 = page.getByRole('heading', { level: 3 });
await expect.element(heading).toBeInTheDocument(); await expect.element(h3).toBeInTheDocument();
const h2 = page.getByRole('heading', { level: 2 });
await expect.element(h2).not.toBeInTheDocument();
}); });
it('shows empty state when drafts is empty', async () => { it('shows empty state when drafts is empty', async () => {
@@ -53,4 +55,45 @@ describe('ReaderDraftsModule', () => {
const emptyText = page.getByText(/Keine Entwürfe/i); const emptyText = page.getByText(/Keine Entwürfe/i);
await expect.element(emptyText).not.toBeInTheDocument(); await expect.element(emptyText).not.toBeInTheDocument();
}); });
it('card wrapper has mint left-border classes', async () => {
render(ReaderDraftsModule, { drafts: [draft1] });
const h3 = page.getByRole('heading', { level: 3 });
const card = ((await h3.element()) as HTMLElement).closest('div[class]');
const rootCard = card?.parentElement;
const cls = rootCard?.className ?? '';
expect(cls).toMatch(/border-l-\[3px\]/);
expect(cls).toMatch(/border-l-brand-mint/);
});
it('draft-row link has min-h-[44px] touch target', async () => {
render(ReaderDraftsModule, { drafts: [draft1] });
const link = page.getByRole('link', { name: /Mein erster Entwurf/ });
const cls = ((await link.element()) as HTMLElement).className;
expect(cls).toMatch(/min-h-\[44px\]/);
});
it('draft title has text-brand-navy class', async () => {
render(ReaderDraftsModule, { drafts: [draft1] });
const link = page.getByRole('link', { name: /Mein erster Entwurf/ });
const el = (await link.element()) as HTMLElement;
const titleEl = el.querySelector('[class*="text-brand-navy"]');
expect(titleEl).not.toBeNull();
expect(titleEl?.textContent?.trim()).toBe('Mein erster Entwurf');
});
it('draft meta contains "Entwurf" text', async () => {
render(ReaderDraftsModule, { drafts: [draft1] });
const link = page.getByRole('link', { name: /Mein erster Entwurf/ });
const el = (await link.element()) as HTMLElement;
expect(el.textContent).toMatch(/Entwurf/);
});
it('chevron SVG is present in each draft row', async () => {
render(ReaderDraftsModule, { drafts: [draft1] });
const link = page.getByRole('link', { name: /Mein erster Entwurf/ });
const el = (await link.element()) as HTMLElement;
const svg = el.querySelector('svg');
expect(svg).not.toBeNull();
});
}); });