Files
familienarchiv/design_handoff_familienarchiv_redesign/_AUTHORING_KIT.md
marcel fa510f3991
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m1s
CI / OCR Service Tests (push) Successful in 28s
CI / Backend Unit Tests (push) Successful in 6m21s
CI / fail2ban Regex (push) Successful in 44s
CI / Semgrep Security Scan (push) Successful in 27s
CI / Compose Bucket Idempotency (push) Successful in 1m8s
feat(redesign): token foundation close-out + design handoff (#854)
Adds the Mappe design handoff as in-repo ground truth and closes out the design-token foundation: --radius-sm/md/full + --shadow-sm/md tokens (distinct dark values in both dark blocks) and the canonical 10-color $lib/shared/avatarPalette.ts (AA-darkened sage/amber/sand swatches), guarded by avatarPalette.spec.ts. Closes #854.
2026-06-16 17:17:27 +02:00

15 KiB
Raw Blame History

Prototype Authoring Kit — "Mappe" redesign

You are authoring a .dc.html design prototype for the Familienarchiv redesign. These prototypes are the pixel ground truth for one screen. They are NOT production code — they run in a tiny prototyping runtime (support.js) and are opened directly in a browser.

Read alongside this kit: DESIGN_RULES.md (the binding visual law) and the existing prototype you are told to use as a template. When this kit and DESIGN_RULES.md disagree, DESIGN_RULES.md wins; when a rule and an existing rendered prototype disagree, the prototype wins.

Everything below is copy-verbatim. Do not invent new tokens, colors, fonts, radii, or spacings. Use only var(--c-*) / var(--font-*) / var(--shadow-*) and the literal values shown here.


1. File skeleton (copy exactly; fill the <main> and the renderVals())

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<link rel="stylesheet" href="colors_and_type.css">
<style>
  html,body{margin:0;padding:0;background:#f0efe9}
  *{box-sizing:border-box}
  input,button,textarea,select{font-family:inherit}
  ::selection{background:#a1dcd8;color:#012851}
  [data-theme='dark'] .dgicon{filter:invert(1) brightness(1.6)}
  .fa-link{transition:color .15s, background .15s, border-color .15s}
</style>
<script>(function(){try{if(localStorage.getItem('theme')==='dark'){document.documentElement.dataset.theme='dark';}}catch(e){}})();</script>
</helmet>
<div style="min-height:100vh; background:var(--c-canvas); color:var(--c-ink); font-family:var(--font-serif)">
  <dc-import name="ArchiveHeader" active="ACTIVE_KEY" hint-size="100%,68px"></dc-import>
  <main style="max-width:1180px; margin:0 auto; padding:40px 32px 80px">
    <!-- PAGE CONTENT HERE -->
  </main>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{}">
class Component extends DCLogic {
  av(name){
    const pal = ['#5a8a6a','#a0522d','#c17a00','#607080','#7a4f9a','#c0446e','#3060b0','#4a7a3a','#9a8040','#c05540'];
    let h = 0; for (let i=0;i<name.length;i++){ h = (h*31 + name.charCodeAt(i)) >>> 0; }
    const parts = name.trim().split(/\s+/);
    const initials = (parts[0][0] + (parts.length>1 ? parts[parts.length-1][0] : '')).toUpperCase();
    const bg = pal[h % pal.length];
    const base = { borderRadius:'999px', color:'#fff', display:'flex', alignItems:'center', justifyContent:'center', fontFamily:'var(--font-sans)', fontWeight:700, flexShrink:0, lineHeight:1, background:bg };
    return { bg, initials, name,
      a26:{ ...base, width:26, height:26, fontSize:10 },
      a28:{ ...base, width:28, height:28, fontSize:11 },
      a40:{ ...base, width:40, height:40, fontSize:14 },
      a48:{ ...base, width:48, height:48, fontSize:16 } };
  }
  renderVals(){
    return { /* data the template renders */ };
  }
}
</script>
</body>
</html>
  • ACTIVE_KEYdokumente · personen · geschichten · zeitstrahl · aktivitaeten · stammbaum · themen · regeln. Pick the section your page belongs to (e.g. a document edit page → dokumente; a person edit page → personen; a timeline event editor → zeitstrahl).
  • Auth pages (Anmeldung) have NO ArchiveHeader — they get their own branded shell (see §11).

2. Runtime constructs (this is all the runtime understands)

  • {{ expr }} — interpolate a value from renderVals() (path access: a.b, a[0]). Works in text and in any attribute, including style="{{ obj }}" where obj is a JS style object.
  • <sc-for list="{{ items }}" as="x" hint-placeholder-count="6"> … {{ x.foo }} … </sc-for>
  • <sc-if value="{{ flag }}" hint-placeholder-val="{{ false }}"> … </sc-if>
  • onClick="{{ handler }}" where handler is a function returned from renderVals().
  • The logic class is class Component extends DCLogic. renderVals() returns the flat data object. Use this.props.X to read a prop. this.state + this.setState({...}) for interactivity (rarely needed — prototypes are mostly static).
  • To inject a raw element from logic (e.g. an icon inside a loop), use React.createElement('img', { className:'dgicon', src:'assets/icons/Mail-MD.svg', style:{width:18,height:18,opacity:.5} }).
  • camelCase event/style props in the template: onClick, not onclick.

3. PageHeader (DESIGN_RULES §3) — every top-level page opens with this

<div style="margin-bottom:30px; display:flex; align-items:flex-end; justify-content:space-between; gap:24px; flex-wrap:wrap">
  <div style="border-left:4px solid var(--c-accent); padding-left:18px">
    <div style="font-family:var(--font-sans); font-size:12px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:var(--c-ink-3); margin-bottom:8px">EYEBROW</div>
    <h1 style="font-family:var(--font-serif); font-weight:700; font-size:46px; line-height:1.06; color:var(--c-ink); margin:0">Title</h1>
    <p style="font-family:var(--font-serif); font-style:italic; font-size:16px; color:var(--c-ink-2); margin:10px 0 0; max-width:520px">Lede sentence, Sie-form.</p>
  </div>
  <!-- right slot: a count span OR a primary button (see §6) -->
  <span style="font-family:var(--font-sans); font-size:12px; color:var(--c-ink-3)">147 Dokumente</span>
</div>

Edit/detail/form pages still open with a PageHeader (eyebrow like PERSON BEARBEITEN, EREIGNIS, NEUE PERSON). Immersive split-pane workbenches (document detail viewer, document/enrich edit) may use a compact top bar instead — follow your page brief.

4. Card (DESIGN_RULES §7)

Base card, with the 3px mint top-border archival signature (most content cards):

<div style="background:var(--c-surface); border:1px solid var(--c-line); border-top:3px solid var(--c-accent); box-shadow:var(--shadow-sm); border-radius:2px; padding:24px"></div>

Variants: inline/resume strip uses border-left:3px solid var(--c-accent) instead of the top border. Section caption inside a card:

<h2 style="font-family:var(--font-sans); font-size:12px; font-weight:700; letter-spacing:.12em; text-transform:uppercase; color:var(--c-ink-3); margin:0 0 16px">Section title</h2>

5. Segmented control (DESIGN_RULES §6) — filters / view switches / binary type picks

<div style="display:inline-flex; border:1px solid var(--c-line)">
  <span class="fa-link" style="font-family:var(--font-sans); font-size:12px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; color:var(--c-primary-fg); background:var(--c-primary); padding:10px 16px; cursor:pointer">Alle</span>
  <span class="fa-link" style="font-family:var(--font-sans); font-size:12px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; color:var(--c-ink-2); background:var(--c-surface); padding:10px 16px; border-left:1px solid var(--c-line); cursor:pointer">Zweitens</span>
  <!-- repeat inactive segments; each adds border-left:1px solid var(--c-line) -->
</div>

Active = background:var(--c-primary); color:var(--c-primary-fg). Inactive = background:var(--c-surface); color:var(--c-ink-2).

6. Buttons (DESIGN_RULES §2 casing law — all UPPERCASE Montserrat 700)

<!-- PRIMARY (navy) -->
<button class="fa-link" style="background:var(--c-primary); color:var(--c-primary-fg); border:none; padding:11px 20px; min-height:44px; font-family:var(--font-sans); font-size:12px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; cursor:pointer; border-radius:2px">Speichern</button>

<!-- SECONDARY (bordered) -->
<button class="fa-link" style="background:var(--c-surface); color:var(--c-ink-2); border:1px solid var(--c-line); padding:11px 20px; min-height:44px; font-family:var(--font-sans); font-size:12px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; cursor:pointer; border-radius:2px">Abbrechen</button>

<!-- DANGER (destructive — Löschen) -->
<a href="#" class="fa-link" style="display:inline-flex; align-items:center; min-height:44px; font-family:var(--font-sans); font-size:12px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--c-danger); text-decoration:none">Löschen</a>

7. Form field + label (DESIGN_RULES §1, §2)

<label style="display:block">
  <span style="display:block; font-family:var(--font-sans); font-size:12px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--c-ink-3); margin-bottom:6px">Titel</span>
  <input placeholder="z.B. Brief an Clara" style="width:100%; border:1px solid var(--c-line); background:var(--c-surface); padding:11px 14px; font-family:var(--font-serif); font-size:16px; color:var(--c-ink); outline:none; border-radius:2px">
</label>

Inputs are Tinos 16px (auth inputs 17px). Textareas same. Focus is shown by the runtime's default outline; if you add a visible focus style use a 2px var(--c-focus-ring) outline with 2px offset — never remove focus without a replacement.

8. MetaLine (DESIGN_RULES §7) — ·-separated, optional leading icon

<div style="display:flex; align-items:center; gap:8px; font-family:var(--font-sans); font-size:12px; color:var(--c-ink-2)">
  <img class="dgicon" src="assets/icons/Calendar-Add-MD.svg" style="width:14px; height:14px; opacity:.5">
  <span>14. März 1923</span><span>·</span><span>14 Dokumente</span><span>·</span><span>4 Personen</span>
</div>

9. Status dot (DESIGN_RULES §7) — 7px circle + UPPERCASE label

<span style="display:inline-flex; align-items:center; gap:7px">
  <span style="width:7px; height:7px; border-radius:999px; background:#5a8a6a"></span>
  <span style="font-family:var(--font-sans); font-size:10px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--c-ink-2)">Transkribiert</span>
</span>

Status colors: Transkribiert / Veröffentlicht / Bestätigt #5a8a6a · In Arbeit #c17a00 · Neu / Entwurf / Unbestätigt #607080. Never color alone — always the label too.

10. Empty state (DESIGN_RULES §7) — dashed, serif heading, German ellipsis

<div style="border:1px dashed var(--c-line); border-radius:2px; padding:48px 24px; text-align:center">
  <div style="font-family:var(--font-serif); font-size:20px; color:var(--c-ink); margin-bottom:8px">Noch keine Geschichten angelegt.</div>
  <div style="font-family:var(--font-sans); font-size:13px; color:var(--c-ink-3)">Beginnen Sie mit einem Brief…</div>
</div>

11. Auth shell (only for Anmeldung.dc.html — no ArchiveHeader)

<div style="min-height:100vh; display:flex; flex-direction:column; background:var(--c-canvas)">
  <header style="background:var(--c-header)">
    <div style="height:4px; background:#a1dcd8"></div>
    <div style="max-width:1180px; margin:0 auto; padding:0 32px; height:64px; display:flex; align-items:center; color:#fff">
      <span style="font-family:var(--font-sans); font-size:18px; font-weight:700; letter-spacing:.16em; text-transform:uppercase">Familienarchiv</span>
    </div>
  </header>
  <div style="flex:1; display:flex; align-items:center; justify-content:center; padding:40px 16px">
    <div style="width:100%; max-width:400px; background:var(--c-surface); border:1px solid var(--c-line); border-top:3px solid var(--c-accent); box-shadow:var(--shadow-sm); border-radius:2px; padding:36px 32px">
      <h1 style="font-family:var(--font-serif); font-size:30px; font-weight:700; color:var(--c-ink); margin:0 0 8px">Anmelden</h1>
      <!-- labelled 17px inputs (§7), full-width primary button (§6), secondary link row -->
    </div>
  </div>
</div>

Title is Tinos sentence-case (NOT an uppercase chrome label). Inputs 17px. Register variant uses max-width:640px and sectioned fields.

12. Icons (render as <img class="dgicon">, never inline SVG, never emoji)

<img class="dgicon" src="assets/icons/Mail-MD.svg" style="width:16px; height:16px; opacity:.4">

Available in assets/icons/: Account-MD · Arrow-Right-MD · Bookmarks-MD · Calendar-Add-MD · Chat-MD · Check-MD · Copy-Item-MD · Edit-Content-MD · Filter-MD · Folder-MD · Globe-MD · Library-MD · Location-MD · Mag-Glass-MD · Mail-MD · Refresh-MD · Upload-MD · View-More-MD. Use only these; pick the closest match. Icons rest at opacity:.4 (.5 on meta lines). They invert automatically in dark mode via the .dgicon rule in the skeleton.

13. Hard rules checklist (verify before you finish)

  • Every color is a var(--c-*) token — zero raw hex except the av() palette and the #a1dcd8 mint stripe inside the header/auth shell. No red-*/gray-*/Tailwind.
  • Casing law: UI chrome (labels, buttons, nav, captions, tags, status, eyebrow) is UPPERCASE Montserrat 700 + wide tracking; headlines & body & names are Tinos sentence case. Quotes use „…"; snippets are italic.
  • Cards carry the 3px mint top border (or 3px mint left for inline strips).
  • Touch targets ≥ 44px (min-height:44px) on buttons / interactive rows / icon buttons.
  • Icon-only buttons would carry an aria-label in production — add title="…" in the prototype so intent is clear.
  • No transforms, scale, translate, blur, glassmorphism, gradients, or emoji. Motion is color only.
  • German, formal Sie. Use realistic family-archive content (Kurrent/Sütterlin letters ~18941945; people like Herbert Cram, Clara Cram, Marie Cram, Eugenie de Gruyter).
  • Renders correctly in light AND dark — because you used tokens, it will. Do not hardcode anything that breaks dark mode.
  • Avatars use this.av(name) and the size objects (a26/a28/a40/a48). Same name → same color (10-color palette).

14. Realistic data

Pull field names / sections from the current Svelte page you are given so the prototype reflects what the app actually shows. Invent plausible German archival content for the values. Aim for enough rows/items to show the layout breathing (e.g. 69 grid cards, 48 list rows).

15. Person chip (multiselect / token input)

A selected person inside a form (person multiselect, sender/receiver token input, etc.) is a square 2px rectangle chip--radius-sm, matching inputs/cards across the app. The avatar inside stays round, but the chip container is NOT a round pill. (radius-full in §1 is for avatars, dots, and status/count pills — not person chips.) Use this exact shape everywhere a removable person appears:

<span style="display:inline-flex; align-items:center; gap:8px; background:var(--c-muted); border:1px solid var(--c-line); border-radius:2px; padding:4px 6px 4px 4px">
  <span style="{{ p.a26 }}">{{ p.initials }}</span>
  <span style="font-family:var(--font-serif); font-size:14px; color:var(--c-ink)">{{ p.name }}</span>
  <a href="#" title="Person entfernen" class="fa-link" style="display:inline-flex; align-items:center; justify-content:center; width:28px; height:28px; font-family:var(--font-sans); font-size:15px; font-weight:700; color:var(--c-ink-3); text-decoration:none">×</a>
</span>

The person name is content → Tinos serif. The chip itself is border-radius:2px. Wrap chip lists in display:flex; flex-wrap:wrap; gap:8px. (A read-only correspondent link uses the same square 2px container with no ×.)