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.
15 KiB
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_KEY∈dokumente · 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 NOArchiveHeader— they get their own branded shell (see §11).
2. Runtime constructs (this is all the runtime understands)
{{ expr }}— interpolate a value fromrenderVals()(path access:a.b,a[0]). Works in text and in any attribute, includingstyle="{{ obj }}"whereobjis 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 }}"wherehandleris a function returned fromrenderVals().- The logic class is
class Component extends DCLogic.renderVals()returns the flat data object. Usethis.props.Xto 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, notonclick.
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 theav()palette and the#a1dcd8mint stripe inside the header/auth shell. Nored-*/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-labelin production — addtitle="…"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 ~1894–1945; 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. 6–9 grid cards, 4–8 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 ×.)