From da4d7f37ff19878ce6bfb7ecd38378dd495971a3 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 16 Jun 2026 17:14:57 +0200 Subject: [PATCH] docs(redesign): add Mappe design handoff as milestone-15 ground truth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The binding visual spec for the Mappe Visual Redesign (milestone #15): DESIGN_RULES.md, _AUTHORING_KIT.md, EPIC.md, and the pixel-ground-truth prototypes/*.dc.html. Every redesign issue (#853–#882) cites this directory by repo-relative path, so it must live in-repo for the per-issue worktrees to share one source of truth. Docs are committed already aligned to the codebase + #854 resolutions: the live token name is --c-tag-sand (not --c-tag-sand-tag), the theme key is localStorage['theme'] (not fa-theme), and §5 notes the AA-darkened avatar swatches shipped in avatarPalette.ts. Co-Authored-By: Claude Opus 4.8 --- .../DESIGN_RULES.md | 210 +++ .../EPIC.md | 217 +++ .../README.md | 125 ++ .../_AUTHORING_KIT.md | 275 ++++ .../prototypes/Aktivitaeten.dc.html | 112 ++ .../prototypes/Anmeldung.dc.html | 256 +++ .../prototypes/Anreicherung.dc.html | 282 ++++ .../prototypes/ArchiveHeader.dc.html | 68 + .../prototypes/Dokument-Bearbeiten.dc.html | 274 +++ .../prototypes/Dokument-Detail.dc.html | 244 +++ .../prototypes/Dokumente-Liste.dc.html | 219 +++ .../prototypes/Dokumente.dc.html | 174 ++ .../prototypes/Ereignis-Editor.dc.html | 288 ++++ .../prototypes/Geschichte-Editor.dc.html | 401 +++++ .../prototypes/Geschichte.dc.html | 160 ++ .../prototypes/Geschichten.dc.html | 112 ++ .../prototypes/Hilfe-Transkription.dc.html | 174 ++ .../prototypes/PersonDetail.dc.html | 273 +++ .../prototypes/PersonForm.dc.html | 268 +++ .../prototypes/PersonReview.dc.html | 170 ++ .../prototypes/Personen.dc.html | 117 ++ .../prototypes/Profil.dc.html | 180 ++ .../prototypes/Regeln.dc.html | 130 ++ .../prototypes/Stammbaum.dc.html | 221 +++ .../prototypes/Themen.dc.html | 190 +++ .../prototypes/Zeitstrahl.dc.html | 191 +++ .../prototypes/assets/icons/Account-MD.svg | 7 + .../assets/icons/Arrow-Right-MD.svg | 7 + .../prototypes/assets/icons/Bookmarks-MD.svg | 7 + .../assets/icons/Calendar-Add-MD.svg | 7 + .../prototypes/assets/icons/Chat-MD.svg | 7 + .../prototypes/assets/icons/Check-MD.svg | 7 + .../prototypes/assets/icons/Copy-Item-MD.svg | 7 + .../assets/icons/Edit-Content-MD.svg | 7 + .../prototypes/assets/icons/Filter-MD.svg | 7 + .../prototypes/assets/icons/Folder-MD.svg | 7 + .../prototypes/assets/icons/Globe-MD.svg | 7 + .../prototypes/assets/icons/Library-MD.svg | 7 + .../prototypes/assets/icons/Location-MD.svg | 7 + .../prototypes/assets/icons/Mag-Glass-MD.svg | 7 + .../prototypes/assets/icons/Mail-MD.svg | 7 + .../prototypes/assets/icons/Refresh-MD.svg | 7 + .../prototypes/assets/icons/Upload-MD.svg | 7 + .../prototypes/assets/icons/View-More-MD.svg | 7 + .../prototypes/colors_and_type.css | 254 +++ .../prototypes/support.js | 1464 +++++++++++++++++ 46 files changed, 7175 insertions(+) create mode 100644 design_handoff_familienarchiv_redesign/DESIGN_RULES.md create mode 100644 design_handoff_familienarchiv_redesign/EPIC.md create mode 100644 design_handoff_familienarchiv_redesign/README.md create mode 100644 design_handoff_familienarchiv_redesign/_AUTHORING_KIT.md create mode 100644 design_handoff_familienarchiv_redesign/prototypes/Aktivitaeten.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/Anmeldung.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/Anreicherung.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/ArchiveHeader.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/Dokument-Bearbeiten.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/Dokument-Detail.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/Dokumente-Liste.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/Dokumente.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/Ereignis-Editor.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/Geschichte-Editor.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/Geschichte.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/Geschichten.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/Hilfe-Transkription.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/PersonDetail.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/PersonForm.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/PersonReview.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/Personen.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/Profil.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/Regeln.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/Stammbaum.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/Themen.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/Zeitstrahl.dc.html create mode 100644 design_handoff_familienarchiv_redesign/prototypes/assets/icons/Account-MD.svg create mode 100644 design_handoff_familienarchiv_redesign/prototypes/assets/icons/Arrow-Right-MD.svg create mode 100644 design_handoff_familienarchiv_redesign/prototypes/assets/icons/Bookmarks-MD.svg create mode 100644 design_handoff_familienarchiv_redesign/prototypes/assets/icons/Calendar-Add-MD.svg create mode 100644 design_handoff_familienarchiv_redesign/prototypes/assets/icons/Chat-MD.svg create mode 100644 design_handoff_familienarchiv_redesign/prototypes/assets/icons/Check-MD.svg create mode 100644 design_handoff_familienarchiv_redesign/prototypes/assets/icons/Copy-Item-MD.svg create mode 100644 design_handoff_familienarchiv_redesign/prototypes/assets/icons/Edit-Content-MD.svg create mode 100644 design_handoff_familienarchiv_redesign/prototypes/assets/icons/Filter-MD.svg create mode 100644 design_handoff_familienarchiv_redesign/prototypes/assets/icons/Folder-MD.svg create mode 100644 design_handoff_familienarchiv_redesign/prototypes/assets/icons/Globe-MD.svg create mode 100644 design_handoff_familienarchiv_redesign/prototypes/assets/icons/Library-MD.svg create mode 100644 design_handoff_familienarchiv_redesign/prototypes/assets/icons/Location-MD.svg create mode 100644 design_handoff_familienarchiv_redesign/prototypes/assets/icons/Mag-Glass-MD.svg create mode 100644 design_handoff_familienarchiv_redesign/prototypes/assets/icons/Mail-MD.svg create mode 100644 design_handoff_familienarchiv_redesign/prototypes/assets/icons/Refresh-MD.svg create mode 100644 design_handoff_familienarchiv_redesign/prototypes/assets/icons/Upload-MD.svg create mode 100644 design_handoff_familienarchiv_redesign/prototypes/assets/icons/View-More-MD.svg create mode 100644 design_handoff_familienarchiv_redesign/prototypes/colors_and_type.css create mode 100644 design_handoff_familienarchiv_redesign/prototypes/support.js diff --git a/design_handoff_familienarchiv_redesign/DESIGN_RULES.md b/design_handoff_familienarchiv_redesign/DESIGN_RULES.md new file mode 100644 index 00000000..4eb07dab --- /dev/null +++ b/design_handoff_familienarchiv_redesign/DESIGN_RULES.md @@ -0,0 +1,210 @@ +# Familienarchiv — Design Rules (binding) + +This is the visual law for the redesign. It promotes the informal `Regeln` page into +enforceable specs. **When this doc and a prototype disagree, the prototype wins** — these +values are transcribed from the prototypes, but the rendered file is ground truth. + +Built on the **De Gruyter Brill** corporate identity. Tone: restrained, archival, +formal-respectful — institutional, not cute. German-first, formal **Sie**, never emoji. + +--- + +## 1. Design tokens + +Port `prototypes/colors_and_type.css` verbatim into the app (Tailwind 4 `@theme` or CSS +custom properties). Components reference **semantic** tokens, never raw hex. + +### Color — light mode + +| Token | Value | Use | +|---|---|---| +| `--c-canvas` | `#f0efe9` | Page background (warm sand). Flat, no gradients. | +| `--c-surface` | `#ffffff` | Cards, inputs, menus. | +| `--c-muted` | `#f5f4ef` | Subtle fills (mission-control tiles, hover wash). | +| `--c-line` | `#e4e2d7` | 1px borders. | +| `--c-line-2` | `#eeede8` | Inner dividers / row separators. | +| `--c-ink` | `#012851` | Primary text (navy). | +| `--c-ink-2` | `#4b5563` | Secondary / body text. | +| `--c-ink-3` | `#6b7280` | Meta, placeholder, captions. | +| `--c-primary` | `#012851` | Primary buttons, active segment, header. | +| `--c-primary-fg` | `#ffffff` | Text on primary. | +| `--c-accent` | `#a1dcd8` | **Mint — decorative only.** Top stripes, left rules, underlines, timeline spine. **Never carries text.** | +| `--c-accent-bg` | `rgba(161,220,216,.15)` | Tinted note/skill backgrounds. | +| `--c-header` | `#012851` | Header bar (always navy, both themes). | +| `--c-turquoise` | `#00c7b1` | Transcription mode only. | +| `--c-danger` | `#c0392b` | Destructive actions (Löschen). | +| `--c-focus-ring` | `#012851` | 2px focus outline, 2px offset (mint in dark). | + +### Color — dark mode (`:root[data-theme='dark']`) + +Navy-tinted. Key flips: `--c-canvas:#010e1e`, `--c-surface:#011526`, `--c-muted:#011a30`, +`--c-line:#0d3358`, `--c-ink:#f0efe9`, `--c-ink-2:#9ca3af`, `--c-ink-3:#8b97a5`. +**`--c-accent` flips to turquoise `#00c7b1`**, and `--c-primary` flips to mint `#a1dcd8` +with `--c-primary-fg:#012851`. Full set in `colors_and_type.css`. + +### Person / avatar palette (deterministic — see §5) + +`#5a8a6a #a0522d #c17a00 #607080 #7a4f9a #c0446e #3060b0 #4a7a3a #9a8040 #c05540` + +> The live avatar constant is `$lib/shared/avatarPalette.ts` (single source of +> truth). Three of these hues fail the ≥4.5:1 white-initials contrast floor and +> ship as AA-darkened variants there (sage `#527e61`, amber `#a46800`, +> sand `#897239`); the bright hues above remain the decorative tag-dot colors. + +### Tag dot colors + +`--c-tag-sage #5a8a6a`, `--c-tag-sienna #a0522d`, `--c-tag-amber #c17a00`, +`--c-tag-slate #607080`, `--c-tag-violet #7a4f9a`, `--c-tag-rose #c0446e`, +`--c-tag-cobalt #3060b0`, `--c-tag-moss #4a7a3a`, `--c-tag-sand #9a8040`, +`--c-tag-coral #c05540`. + +### Badge types (Personen) + +| Type | bg / text / border | +|---|---| +| Institution | `#e8eff7` / `#1a4971` / `#c4d5e8` | +| Gruppe | `#f0e8f5` / `#5a2d6f` / `#d8c5e3` | +| Unbekannt | `#fdf4e3` / `#7a5a0a` / `#f0ddb3` | + +### Radius / shadow + +- `--radius-sm: 2px` — cards, inputs, buttons, segmented control. **The default.** +- `--radius-md: 4px` — tag chips/badges only. +- `--radius-full: 9999px` — avatars, dots, pills. +- `--shadow-sm: 0 1px 2px 0 rgb(0 0 0/.05)` — resting cards. +- `--shadow-md: 0 4px 6px -1px rgb(0 0 0/.1), 0 2px 4px -2px rgb(0 0 0/.1)` — dropdowns. + +--- + +## 2. Typography + +Two families. **Montserrat** (`--font-sans`) for all UI chrome; **Tinos** (`--font-serif`) +for headlines, body, letter content, transcriptions, story prose. + +| Role | Family | Size / weight | Treatment | +|---|---|---|---| +| Page title (`h1`) | Tinos | **46px / 700**, line-height 1.06 | sentence case | +| Story detail title | Tinos | 38px / 700, lh 1.15 | sentence case | +| Card title (`h3`) | Tinos | 19–24px / 700 | sentence case | +| Body / letter / snippet | Tinos | 15–18px, lh 1.55–1.75 | snippets *italic*, quotes use `„…“` | +| **Rubric / eyebrow label** | Montserrat | **12px / 700**, `letter-spacing:.14em`, UPPERCASE | above every page title | +| Section caption | Montserrat | 11–12px / 700, `.12–.14em`, UPPERCASE | card headers | +| Button / nav label | Montserrat | 11–12px / 700, `.08–.1em`, UPPERCASE | | +| Tag chip label | Montserrat | 10px / 700, `.13–.15em`, UPPERCASE | | +| Meta line | Montserrat | 12px / 400 | counts, dates; separated by ` · ` | + +**Casing law:** UI chrome (labels, buttons, nav, captions, tags) is ALL CAPS + wide +tracking, Montserrat bold. Headlines and document titles are sentence case in Tinos serif. + +--- + +## 3. Shared component: page header (eyebrow + title) + +Every top-level page opens with this block. **Build it once** as a `PageHeader` +component (props: eyebrow, title, lede, optional right-side count or action). + +``` +
+ Montserrat 12px/700, .14em, UPPERCASE, color --c-ink-3, margin-bottom 8px +

Tinos 46px/700, lh 1.06, color --c-ink + Tinos italic 16px, color --c-ink-2, margin-top 10px, max-width 520px +``` + +The **4px mint left rule** is the signature. A right-aligned count +(`38 Personen`, `147 Dokumente · 38 Personen`) or a primary action button sits opposite via +`justify-content:space-between; align-items:flex-end`. + +Page shell for every screen: `min-height:100vh; background:var(--c-canvas)`, header on top, +then `
`. + +--- + +## 4. Shared component: app header + nav (`ArchiveHeader`) + +Single sticky header reused on every page. **This is the most important thing to extract +into one component** — it is currently duplicated and must not be. + +- `position:sticky; top:0; z-index:50; background:var(--c-header)`. +- **4px mint stripe** (`#a1dcd8`) across the very top, above the bar. +- Bar: `max-width:1180px; margin:0 auto; padding:0 32px; height:64px; display:flex; align-items:center`, white text. +- Wordmark `FAMILIENARCHIV`: Montserrat 18px/700, `letter-spacing:.16em`, UPPERCASE, `margin-right:28px`. +- Nav items: Montserrat 11px/700, `.07em`, UPPERCASE, color `rgba(255,255,255,.6)`, + `line-height:44px`. **Active** item → color `#fff` + `border-bottom:2px solid #a1dcd8`. + Nav order: Dokumente · Personen · Briefwechsel · Geschichten · Zeitstrahl · Aktivitäten + (· Regeln, internal). Each links to its route; pass the active key as a prop. +- Right cluster: **Hell / Dunkel** theme toggle (segmented; active segment = mint bg + `#a1dcd8` + navy text) and a round user avatar chip (`MR`, white bg, navy text, 32px). +- **Theme toggle** writes `localStorage['theme']` (`'light'|'dark'`) and sets + `document.documentElement.dataset.theme`. A tiny inline boot script in `` reads it + before paint to avoid a flash. Map this to the app's existing theme mechanism if one + exists; otherwise replicate. +- Motion: `transition-colors` only. No transforms, no scale on press. + +--- + +## 5. Shared primitive: avatar + deterministic color + +Every person is a round avatar with initials, colored by a hash of the name so the same +person is always the same color across every screen. **Extract this into one util + +component** — it is currently copy-pasted into 6 files. + +```js +// palette = the 10 person colors in §1 +function avatarFor(name) { + 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(); + return { bg: palette[h % palette.length], initials }; +} +``` + +- Always a perfect circle (`border-radius:full`), white initials in Montserrat 700, `line-height:1`. +- Sizes in use: **48px**/16px (selectors, person cards), **40px**/14px (story byline, feed, + Briefwechsel rows), **26px**/10px (overlapping stacks — `margin-left:-6px` + + `2px solid var(--c-surface)` ring), **28px** (timeline person nodes). +- The color **distinguishes, it does not decorate** — never restyle it for emphasis. + +--- + +## 6. Shared component: segmented control (filters / views) + +Inline filter switch used on Personen, Geschichten, Zeitstrahl, Aktivitäten, and the header +theme toggle. + +- `display:inline-flex; border:1px solid var(--c-line)` (radius 0 / sm; segments share borders). +- Each segment: Montserrat 12px/700, `.08em`, UPPERCASE, `padding:9–10px 16px`, `cursor:pointer`. +- **Active** segment: `background:var(--c-primary); color:var(--c-primary-fg)`. +- Inactive: `background:var(--c-surface); color:var(--c-ink-2)`, `border-left:1px solid var(--c-line)` between segments. + +--- + +## 7. Cards, metadata, empty states + +**Card:** `background:var(--c-surface); border:1px solid var(--c-line); box-shadow:var(--shadow-sm); border-radius:2px; padding:20–24px`. +Most content cards add a **3px mint top border** (`border-top:3px solid var(--c-accent)`) as +the archival signature; some use a **3px mint left border** for inline/resume strips. + +**Metadata line:** one Montserrat 12px line, items separated by ` · `, often led by a 14px +De Gruyter icon at `opacity:.5`. Example: `📅 14. März 1923 · 14 Dokumente · 4 Personen` +(icon is an ``, not emoji). + +**Status dots:** 7px circle + UPPERCASE label. Transkribiert/Veröffentlicht `#5a8a6a`, +In Arbeit `#c17a00`, Neu/Entwurf `#607080`. + +**Empty state:** dashed `1px var(--c-line)` border, centered. Serif heading +(`Noch keine Geschichten angelegt.`) + Montserrat sub line ending in the German ellipsis +(`Beginnen Sie mit einem Brief…`). Quiet, Sie-form, helpful — never cute. + +--- + +## 8. Motion, hover, imagery + +- All motion is **color**, `transition: color/background/border .15–.2s`. No transform, no + scale, no press-down. +- Rows: `hover:bg-muted/50`. Links: 2px mint underline at `text-underline-offset:3px`, + `text-decoration-thickness:2px`. Header nav: `white/60 → white`. +- Backdrops `bg-black/20`. **No backdrop-blur, no glassmorphism, no gradients, no noise.** +- Imagery is warm aged letter scans; UI chrome stays cool navy to contrast. +- De Gruyter icons: black strokes as ``, `opacity-40` resting tint (`.65` on the dark + header), globally inverted in dark mode. diff --git a/design_handoff_familienarchiv_redesign/EPIC.md b/design_handoff_familienarchiv_redesign/EPIC.md new file mode 100644 index 00000000..7d98601f --- /dev/null +++ b/design_handoff_familienarchiv_redesign/EPIC.md @@ -0,0 +1,217 @@ +# EPIC: Familienarchiv Visual Redesign ("Mappe") + +> Implement **in order**. Stories 1–4 are foundation and shared primitives — every screen +> depends on them. Do not start a screen story until 1–4 are merged, or the header, avatar, +> and tokens get re-implemented per screen and drift. Read `DESIGN_RULES.md` first; open the +> matching `prototypes/*.dc.html` for pixel ground truth while building each story. + +## Epic goal + +Reskin the entire Familienarchiv app to the unified "Mappe" archival direction and ship +three new sections (Geschichten, Zeitstrahl, Aktivitäten). All copy German-first via +Paraglide; light + dark mode; De Gruyter icon convention preserved. + +## Epic-level acceptance criteria + +- [ ] All eight screens match their prototype in light **and** dark mode. +- [ ] Header, page-header, avatar, segmented control, and card exist as **single shared + components** — zero duplication of the header markup or the avatar-color function. +- [ ] No raw hex in components; everything references the semantic tokens. +- [ ] Every visible string is a Paraglide message key, German authored first; `en`/`es` + stubs added. +- [ ] Icons rendered as ``, invert correctly in dark mode. +- [ ] No gradients, blur, emoji, or transform-based motion introduced. + +--- + +> **Reframe (2026-06-16): this is alignment, not greenfield.** An audit of the live codebase +> found the substrate already in place — the full token system (`DESIGN_RULES §1–2`), dark +> mode, the app header, and the three "new" sections (Geschichten, Zeitstrahl, Aktivitäten) +> all already exist. So **Stories 1–4 below are close-out + extraction**, not from-scratch +> builds, and Stories 5–11 are "align the existing screen to its prototype," not new pages. +> The detailed, trackable breakdown lives in the Gitea milestone **"Mappe Visual Redesign"** +> (issues split into *shared components* then *pages*); **Phase B** at the bottom of this file +> lists the screens that previously had no prototype and now do. + +## Story 1 — Foundation: tokens, fonts, theme + +**Goal:** establish the visual substrate the whole app reads from. + +- Port `prototypes/colors_and_type.css` into the app's token layer (Tailwind 4 `@theme` / + `layout.css`). Keep every variable name in `DESIGN_RULES.md §1`. +- Wire Montserrat + Tinos (or licensed Gotham/Times) and the `--font-sans`/`--font-serif` vars. +- Implement light/dark via `:root[data-theme='dark']` + the pre-paint boot script that reads + `localStorage['theme']`. Add the global `img[src*='degruyter-icons']{filter:invert(1)}` + dark rule. + +**Done when:** a throwaway page using `var(--c-*)` tokens renders correct in both themes; no +flash of wrong theme on reload. + +## Story 2 — Shared: app header + nav (`ArchiveHeader`) + +**Spec:** `DESIGN_RULES.md §4`. Prototype: `ArchiveHeader.dc.html`. + +- One sticky header component: mint stripe, wordmark, nav with active-key prop, theme + toggle, user chip. Nav routes to all sections. +- Theme toggle drives the Story 1 mechanism. + +**Done when:** header renders identically on every route; active item shows the mint +underline; toggle flips theme and persists. + +## Story 3 — Shared: avatar + deterministic color + +**Spec:** `DESIGN_RULES.md §5`. + +- `avatarFor(name)` util (hash → palette index + initials) + an `` + component supporting 26/28/40/48px and the overlapping-stack ring variant. + +**Done when:** the same name yields the same color everywhere; stacks overlap with the +surface-colored ring. + +## Story 4 — Shared: page-header, segmented control, card, metadata, empty state + +**Spec:** `DESIGN_RULES.md §3, §6, §7`. + +- `PageHeader` (eyebrow + 4px mint left rule + serif h1 + italic lede + right slot). +- `SegmentedControl` (active = navy). `Card` (mint top/left border variants). `MetaLine` + (` · ` separated, optional leading icon). `EmptyState` (dashed, serif + ellipsis). + +**Done when:** each primitive matches the prototype and is consumed by the screen stories +below — not re-styled inline. + +--- + +## Story 5 — Dokumente (dashboard / search results) + +**Route:** `/`. **Prototype:** `Dokumente.dc.html`. + +PageHeader (eyebrow "Archiv", title "Dokumente", lede, right count "147 Dokumente · 38 +Personen"). Then: a **search bar card** (input `Titel, Personen, Tags durchsuchen…` + +"Datum ↓" sort + "Filter" buttons); a **resume strip** (mint left border, "Weiter bei:" + +italic underlined doc link); a **`1fr 320px` grid** — left: "Zuletzt hinzugefügt" list +(title · avatar stack · right-aligned date `width:128px` · status dot+label `width:118px`), +right column: **upload dropzone** (dashed, Upload icon, `PDF, JPG, PNG, TIFF bis 50 MB`) + +"Benötigt Metadaten" card; below full-width **Mission Control** — 3 tiles (Segmentierung / +Transkription / Zur Überprüfung), each with a caption, a pill "skill" hint, a weekly count, +and a list of linked items. Right column collapses below `lg`; main goes full-width. + +**Done when:** matches prototype both themes; status dots use the §7 colors; grid collapses. + +## Story 6 — Personen (directory) + +**Route:** `/persons`. **Prototype:** `Personen.dc.html`. + +PageHeader (eyebrow "Verzeichnis") + right count "38 Personen". Search input +(`z.B. Oma Frieda, Onkel Karl…`) + segmented control (Alle / Personen / Institutionen / +Gruppen). **3-column card grid**; each card: 48px avatar, serif name, relation sub, optional +type **badge** (Institution/Gruppe/Unbekannt — §1 colors), divider, meta line +`✉ N Briefe · N Dokumente`. Cards carry the 3px mint top border. + +**Done when:** badge colors correct; avatar colors deterministic; grid responsive. + +## Story 7 — Briefwechsel — DROPPED + +The two-person letter-exchange feature was removed from the product. Its prototype, route, and +nav entry no longer exist. Skip. + +## Story 8 — Geschichten (story collections list) — NEW + +**Route:** `/geschichten`. **Prototype:** `Geschichten.dc.html`. + +PageHeader (eyebrow "Sammlungen") + primary button "Neue Geschichte". Segmented control +(Alle / Veröffentlicht / In Arbeit / Entwurf) + Filter button. **2-column card grid**; each +card (link, mint top border): tag chips (dot + UPPERCASE label), serif 24px title, serif dek, +meta line `📅 range · N Dokumente · N Personen`, footer with overlapping avatar stack + status +dot/label. Cards link to the story detail. + +**Done when:** tag dots and status colors correct; cards link to Story 9. + +## Story 9 — Geschichte (single story detail) — NEW + +**Route:** `/geschichten/:id`. **Prototype:** `Geschichte.dc.html`. **Two variants** +(`variant` prop): **"Lesereise"** (a guided reading — intro + narration blocks + letter +cards + annotation notes) and **"Sammlung"** (a collection — intro + "Erwähnte Dokumente" +list). Centered `max-width:880px` article card (mint top border, `padding:48px 56px`): +type badge, 38px serif title, byline row (author avatar + name + "zusammengestellt am …" + +Bearbeiten / Löschen actions), intro paragraph, then ordered **blocks**: + +- **narration** — 3px mint left rule, serif italic 18px. +- **letter** — clickable row: 40px tile w/ Mail icon, serif title, meta `date · von X an Y`, + trailing Arrow-Right icon. +- **note** — mint-tinted (`--c-accent-bg`) left-rule box, "Anmerkung" caption + serif italic. + +**Done when:** both variants render from the prop; block types styled per spec; Löschen uses +`--c-danger`. + +## Story 10 — Zeitstrahl (timeline) — NEW + +**Route:** `/zeitstrahl`. **Prototype:** `Zeitstrahl.dc.html`. + +PageHeader (eyebrow "Chronik") + right count. Segmented control (Alle / Briefe / Personen / +Ereignisse) + a small legend. **Centered vertical spine** (`max-width:760px`, 2px mint center +line). Item types stacked on the spine: **year** pill (navy), **summary** card (count + a +12-bar monthly-density mini chart in mint + range labels), **letter** cards **alternating +left/right** with a spine dot (`2px solid --c-primary`) and optional tag pill, **person** +node (28px navy circle glyph + name + derived meta), **curated** node (★, mint left rule), +**historical** band (full-width, Globe icon, serif italic, top/bottom hairline). + +**Done when:** spine centered; letters alternate; bars scale to value %; all five item types +render. + +## Story 11 — Aktivitäten (activity feed) — NEW + +**Route:** `/aktivitaeten`. **Prototype:** `Aktivitaeten.dc.html`. + +PageHeader (eyebrow "Verlauf") + "Aktualisieren" button. Segmented control (Alle / +Transkription / Uploads / Personen). Feed grouped by day (Heute / Gestern / Diese Woche), +each group a UPPERCASE caption + rows. Each **row**: 40px avatar with a small **action-icon +badge** bottom-right (Check/Upload/Chat/Edit/…), then a sentence — bold actor name + +Montserrat verb + *italic underlined* target link — and a time sub. + +**Done when:** grouping + icon badges match; target links styled with mint underline. + +## Story 12 (optional) — Regeln (internal style reference) + +**Prototype:** `Regeln.dc.html`. Internal page documenting the seven blocks (Typografie, +Farbe, Seitenkopf, Steuerung, Avatare, Metadaten/Leerzustände). Build only if the team wants +a living in-app reference; otherwise `DESIGN_RULES.md` is the canonical record. Gate behind +admin/dev. + +--- + +## Suggested order & dependencies + +``` +1 Tokens ─┬─ 2 Header ──┐ + ├─ 3 Avatar ──┼─→ 5 Dokumente, 6 Personen, + └─ 4 Primitives┘ 8 Geschichten → 9 Geschichte, + 10 Zeitstrahl, 11 Aktivitäten (parallelizable) + 12 Regeln (optional, last) + → then Phase B (added screens), all depend on 1–4 +``` + +--- + +## Phase B — Added screens (previously un-prototyped pages) + +These are the pages the original handoff never covered. Each now has a hifi `.dc.html` +prototype in `prototypes/`. All depend on the shared primitives from Stories 1–4 and are +parallelizable among themselves. **Each maps to one Gitea page-issue** in the milestone. +Admin + OCR pages are explicitly **out of scope** here (phase-2 milestone). + +| # | Screen | Prototype | Route(s) | Done when | +|---|---|---|---|---| +| B1 | Dokumente-Liste | `Dokumente-Liste.dc.html` | `/documents` | PageHeader; search card; AND/OR segmented; grouped mint-top cards; avatar-stack rows; 7px status dot+label; pagination | +| B2 | Dokument-Detail | `Dokument-Detail.dc.html` | `/documents/[id]` | compact top bar w/ mint accent bar; PDF pane + transcription panel w/ Lesen/Bearbeiten segmented + turquoise mode; details card | +| B3 | Dokument-Bearbeiten | `Dokument-Bearbeiten.dc.html` | `/documents/[id]/edit`, `/new`, `/bulk-edit` | split pane; progress strip; Wer&Wann + Beschreibung cards; dropzone (new); action bar (Löschen danger / Abbrechen / Zur Überprüfung / Speichern) | +| B4 | PersonDetail | `PersonDetail.dc.html` | `/persons/[id]` | PageHeader; 2-col mint-top cards; deterministic avatar; correspondents + relationships + letter lists | +| B5 | PersonForm | `PersonForm.dc.html` | `/persons/new`, `/[id]/edit` | PageHeader; Stammdaten card w/ type segmented; caps labels; Namensverlauf; edit-only merge danger zone; save bar | +| B6 | PersonReview | `PersonReview.dc.html` | `/persons/review` | PageHeader + count; row card w/ muted avatar; idle/rename/merge states; danger merge+delete; confirm dialog; empty state | +| B7 | Geschichte-Editor | `Geschichte-Editor.dc.html` | `/geschichten/new`, `/[id]/edit` | type-pick segmented; prose editor toolbar; journey editor w/ **color-only** drag; sidebar status+persons; save bar | +| B8 | Ereignis-Editor | `Ereignis-Editor.dc.html` | `/zeitstrahl/events/new`, `/[id]/edit` | PageHeader; Wann&Was card w/ type segmented + date precision + danger error; persons/docs sidebar; save bar | +| B9 | Stammbaum | `Stammbaum.dc.html` | `/stammbaum` | PageHeader + count; node cards w/ **§5 avatar** in resting/selected/dimmed; line connectors; side panel; zoom controls; empty state | +| B10 | Themen | `Themen.dc.html` | `/themen` | PageHeader + count; segmented filter; mint-top cards w/ **§1 tag-dot** (not stripe); child rows; empty state | +| B11 | Anreicherung | `Anreicherung.dc.html` | `/enrich`, `/[id]`, `/done` | list (status rows) / step (progress bar + split pane + action bar) / done (success card) | +| B12 | Profil | `Profil.dc.html` | `/profile`, `/users/[id]` | PageHeader; 2-col data/password cards; token banners; notifications; public profile card w/ avatar | +| B13 | Anmeldung | `Anmeldung.dc.html` | `/login`, `/register`, `/forgot-password`, `/reset-password` | self-contained branded shell (no app header); Tinos sentence-case titles; 17px inputs; token banners | +| B14 | Hilfe-Transkription | `Hilfe-Transkription.dc.html` | `/hilfe/transkription` | PageHeader; article column; rule cards w/ De Gruyter icons (no emoji); fixes `border-brand-sand`/`bg-white` bugs | diff --git a/design_handoff_familienarchiv_redesign/README.md b/design_handoff_familienarchiv_redesign/README.md new file mode 100644 index 00000000..6fea2bc4 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/README.md @@ -0,0 +1,125 @@ +# Handoff: Familienarchiv — Visual Redesign ("Mappe" direction) + +## Overview + +This package hands off a **complete visual redesign of the Familienarchiv web app** to a +developer using Claude Code. Familienarchiv is a private digital family archive +(German-first SvelteKit app) for digitising, transcribing, tagging and searching +historical correspondence. + +The redesign unifies every page under one system — internally called **"Mappe"** (German +for *folder/portfolio*): a warm, archival, institutional look built on the De Gruyter Brill +corporate identity. It also **adds three new sections** that did not exist in the old app +(**Geschichten**, **Zeitstrahl**, **Aktivitäten**) alongside redesigns of the existing ones. + +The work is broken into an **epic with foundation-first stories**. Read the three docs in +this order: + +1. **`README.md`** (this file) — what's here, fidelity, how to use it. +2. **`DESIGN_RULES.md`** — the binding visual law (tokens, type, the shared patterns). + *Read this before writing any code.* +3. **`EPIC.md`** — the epic + ordered stories + acceptance criteria. *Implement in order.* + +## About the design files + +The files in `prototypes/` are **design references created in HTML** — high-fidelity +prototypes that show the intended look and behaviour exactly. **They are not the production +codebase and must not be shipped as-is.** They are authored as "Design Components" +(`.dc.html`) for a prototyping runtime (`support.js`); that runtime is **not** part of the +target app. + +Your task is to **recreate these designs inside the existing Familienarchiv codebase** +(SvelteKit 2 + Svelte 5 + Tailwind 4 + Paraglide i18n) using its established patterns — +real Svelte components, real routes, real i18n message keys. Where the prototype uses a +made-up runtime construct (``, ``, `renderVals()`), map it to the +codebase's idiom (a Svelte component import, an `{#each}` block, component props/state). + +### Why HTML prototypes are the source of truth + +Every colour, size, weight, spacing and radius is written **inline** in the prototype +markup, and the files **render**. When the written spec and a prototype disagree, **the +prototype wins** — open it in a browser and measure. Treat `DESIGN_RULES.md` and `EPIC.md` +as the *intent and ordering*, and the prototypes as the *pixel ground truth*. + +To view a prototype: open any `prototypes/*.dc.html` in a browser (they are self-contained; +`colors_and_type.css`, `support.js` and `assets/icons/` sit alongside them). Use the +**Hell / Dunkel** toggle in the header to verify dark mode. + +## Fidelity + +**High-fidelity (hifi).** Final colours, typography, spacing, radii, shadows, copy and +interaction intent are all decided. Recreate pixel-perfectly using the codebase's existing +libraries. Do not re-interpret the visual language — apply it. + +## What's in this bundle + +``` +design_handoff_familienarchiv_redesign/ +├── README.md ← you are here +├── DESIGN_RULES.md ← binding tokens + shared patterns (the "rules") +├── EPIC.md ← epic, stories, acceptance criteria (implement in order) +├── _AUTHORING_KIT.md ← the contract every prototype was authored against (copy-verbatim snippets) +└── prototypes/ ← runnable hifi references (source of truth) + │ ── original section screens ── + ├── ArchiveHeader.dc.html shared header + nav + theme toggle + ├── Dokumente.dc.html dashboard + ├── Personen.dc.html person directory + ├── Geschichten.dc.html curated story collections (list) + ├── Geschichte.dc.html single story detail (2 variants) + ├── Zeitstrahl.dc.html chronological timeline + ├── Aktivitaeten.dc.html activity feed + ├── Regeln.dc.html the design-rules spec page (internal reference) + │ ── added screens (the previously un-prototyped pages) ── + ├── Dokumente-Liste.dc.html document search / grouped results + ├── Dokument-Detail.dc.html document viewer + transcription workbench + ├── Dokument-Bearbeiten.dc.html edit / new / bulk-edit (variant prop) + ├── PersonDetail.dc.html person detail (read) + ├── PersonForm.dc.html person new / edit (variant prop) + ├── PersonReview.dc.html provisional-person triage workflow + ├── Geschichte-Editor.dc.html story authoring: new / story / journey (variant prop) + ├── Ereignis-Editor.dc.html timeline event new / edit (variant prop) + ├── Stammbaum.dc.html family tree + ├── Themen.dc.html topics / tag directory + ├── Anreicherung.dc.html enrich workflow: list / step / done (variant prop) + ├── Profil.dc.html account settings + public profile (variant prop) + ├── Anmeldung.dc.html auth: login / register / forgot / reset (variant prop, no app header) + ├── Hilfe-Transkription.dc.html transcription help / guidelines + │ ── shared assets ── + ├── colors_and_type.css design tokens (port into the app) + ├── support.js prototype runtime — DO NOT ship + └── assets/icons/ De Gruyter "Simple" icons used by the screens +``` + +> **Status note (2026-06-16).** Two facts updated this bundle since the original handoff: +> 1. **This is now an alignment effort, not a greenfield reskin.** The token system +> (`DESIGN_RULES §1–2`), dark mode, the app header, and the three "new" sections +> (Geschichten, Zeitstrahl, Aktivitäten) **already exist in the codebase**. Foundation +> Stories 1–4 are *close-out + extraction* of the missing shared primitives (`PageHeader`, +> `Avatar`/`avatarFor`, `SegmentedControl`, `Card`, `MetaLine`, `EmptyState`, `StatusDot`), +> not from-scratch builds. See the Gitea milestone **"Mappe Visual Redesign"** for the +> issue-level breakdown (shared components, then pages). +> 2. **Briefwechsel was dropped** — that feature was removed from the product, so its +> prototype and nav entry are gone. Admin + OCR pages are deferred to a phase-2 milestone. + +## Assets + +- **Icons** — De Gruyter "Simple" Medium-24px SVGs in `prototypes/assets/icons/`. In the + real app these live at `static/degruyter-icons/Simple/…` and are rendered as `` + tags. Reuse the existing app copies; this bundle ships only the subset the redesign + touches so you can see which are needed. +- **Fonts** — Montserrat + Tinos via Google Fonts (substitutes for Gotham + Times). The + `@import` is at the top of `colors_and_type.css`. If the team has the licensed Gotham/Times + faces, swap them in and keep the variable names. +- **Logo** — none. The brand is the wordmark `FAMILIENARCHIV` (Montserrat Bold, uppercase, + `letter-spacing:.16em`) on the navy header. + +## Target codebase notes + +- **i18n**: all copy in the prototypes is German. The app is German-first with `en`/`es` + translations (Paraglide). Every visible string must become a message key, German written + first. Existing keys live in `frontend/messages/{de,en,es}.json`. +- **Icons as ``**: keep the app convention — never inline SVG, never icon fonts. Dark + mode inverts them globally via `img[src*='degruyter-icons'] { filter: invert(1); }`. +- **Tailwind 4**: the prototypes use inline styles for clarity. Port them to the codebase's + Tailwind utilities / `@theme` tokens — but the *values* must match the tokens in + `DESIGN_RULES.md` exactly. diff --git a/design_handoff_familienarchiv_redesign/_AUTHORING_KIT.md b/design_handoff_familienarchiv_redesign/_AUTHORING_KIT.md new file mode 100644 index 00000000..4b511916 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/_AUTHORING_KIT.md @@ -0,0 +1,275 @@ +# 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 `
` and the `renderVals()`) + +```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 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. +- ` … {{ x.foo }} … ` +- `` +- `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 + +```html +
+
+
EYEBROW
+

Title

+

Lede sentence, Sie-form.

+
+ + 147 Dokumente +
+``` + +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): + +```html +
+``` + +Variants: inline/resume strip uses `border-left:3px solid var(--c-accent)` instead of the top +border. Section caption inside a card: + +```html +

Section title

+``` + +## 5. Segmented control (DESIGN_RULES §6) — filters / view switches / binary type picks + +```html +
+ Alle + Zweitens + +
+``` + +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) + +```html + + + + + + + +Löschen +``` + +## 7. Form field + label (DESIGN_RULES §1, §2) + +```html + +``` + +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 + +```html +
+ + 14. März 1923·14 Dokumente·4 Personen +
+``` + +## 9. Status dot (DESIGN_RULES §7) — 7px circle + UPPERCASE label + +```html + + + Transkribiert + +``` + +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 + +```html +
+
Noch keine Geschichten angelegt.
+
Beginnen Sie mit einem Brief…
+
+``` + +## 11. Auth shell (only for `Anmeldung.dc.html` — no ArchiveHeader) + +```html +
+
+
+
+ Familienarchiv +
+
+
+
+

Anmelden

+ +
+
+
+``` + +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 ``, never inline SVG, never emoji) + +```html + +``` + +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 + ~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: + +```html + + {{ p.initials }} + {{ p.name }} + × + +``` + +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 `×`.) diff --git a/design_handoff_familienarchiv_redesign/prototypes/Aktivitaeten.dc.html b/design_handoff_familienarchiv_redesign/prototypes/Aktivitaeten.dc.html new file mode 100644 index 00000000..c5417a77 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/Aktivitaeten.dc.html @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + +
+ +
+ +
+
+
Verlauf
+

Aktivitäten

+

Wer zuletzt transkribiert, ergänzt und kuratiert hat — die Hände hinter dem Archiv.

+
+ +
+ + +
+
+ Alle + Transkription + Uploads + Personen +
+
+ + + +
+
{{ g.day }}
+ +
+
+ {{ a.initials }} + + {{ a.iconEl }} + +
+
+

+ {{ a.name }} {{ a.verb }} {{ a.target }} +

+
{{ a.time }}
+
+
+
+
+
+
+
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/Anmeldung.dc.html b/design_handoff_familienarchiv_redesign/prototypes/Anmeldung.dc.html new file mode 100644 index 00000000..56ff53b9 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/Anmeldung.dc.html @@ -0,0 +1,256 @@ + + + + + + + + + + + + + + + + +
+ + +
+
+
+ Familienarchiv +
+
+ +
+
+ + + +
+ +

Anmelden

+

Willkommen zurück im Familienarchiv.

+ + +
+ + E-Mail oder Passwort ist nicht korrekt. +
+ + +
+ + Zu viele Versuche. Bitte warten Sie eine Minute, bevor Sie es erneut versuchen. +
+ +
+ +
+ +
+ +
+ + + + + + +
+
+ + + +
+ +
+
Einladung angenommen
+

Konto erstellen

+

Legen Sie Ihren Zugang an, um Briefe zu lesen, zu transkribieren und mitzuschreiben.

+
+ +
+ + +
+

Über Sie

+
+ + +
+
+ + +
+

Konto

+ + + + + +

Mindestens 8 Zeichen.

+ + + +
+ + +
+

Benachrichtigungen

+ +
+ + + +

+ Schon ein Konto? + Anmelden +

+ +
+
+
+ + + +
+ +

Passwort zurücksetzen

+

Geben Sie Ihre E-Mail-Adresse ein. Wir senden Ihnen einen Link, mit dem Sie ein neues Passwort vergeben können.

+ + +
+ + Falls ein Konto zu dieser Adresse besteht, ist der Link unterwegs. Prüfen Sie Ihr Postfach. +
+ + + + + + + +
+
+ + + +
+ +

Neues Passwort

+

Vergeben Sie ein neues Passwort für Ihr Konto. Danach werden Sie zur Anmeldung weitergeleitet.

+ + + + + + + + + +
+
+ +
+
+ + +
+ Familienarchiv +
+ +
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/Anreicherung.dc.html b/design_handoff_familienarchiv_redesign/prototypes/Anreicherung.dc.html new file mode 100644 index 00000000..91c9953d --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/Anreicherung.dc.html @@ -0,0 +1,282 @@ + + + + + + + + + + + + + + +
+ + + + +
+ + +
+
+
Aufgabe
+

Anreicherung

+

Ergänzen Sie fehlende Angaben Stück für Stück — wir führen Sie durch jeden Brief.

+
+
+ + {{ count }} Dokumente offen +
+
+ + +
+

Benötigt Metadaten

+ + +
+ + {{ d.title }} + + + {{ d.status }} + +
+ + {{ d.date }} +
+
+
+ +

{{ count }} Dokumente warten auf Pflichtangaben.

+
+ + + +
+
Alles angereichert — nichts zu tun.
+
Jeder Brief trägt Titel, Datum und Absender…
+
+
+
+
+ + + +
+ + +
+ ← Zurück +

{{ doc.title }}

+ Schritt {{ doc.step }} von {{ doc.total }} +
+ + +
+ Pflichtfelder {{ doc.filled }} / {{ doc.required }} +
+
+
+
+ + +
+ + +
+
+ +
+
+
+
Wien, im August 1924
+
Meine liebe Clara,
+
die Reise verlief gut, doch der Zug nach Mariahilf hatte Verspätung. Onkel Walter lässt herzlich grüßen und fragt nach den Kindern.
+
Die Tage hier sind warm; ich denke oft an unseren Garten und an Euch alle daheim.
+
In Liebe, Herbert
+
Seite 1 von 1
+
+
+
+ + +
+
+ + +
+

Wer & Wann

+ + + + + + + + +
+ + +
+

Beschreibung

+ + + +
+
+ Optional +
+
+ + + + + +
+ + +
+
+
+ + +
+ Überspringen +
+ + +
+
+
+
+
+
+ + + +
+
+
+ +
+

Geschafft!

+

Alle Briefe dieser Sitzung sind angereichert. Vielen Dank — Ihre Sorgfalt hält das Archiv beieinander.

+ +
+
+
+
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/ArchiveHeader.dc.html b/design_handoff_familienarchiv_redesign/prototypes/ArchiveHeader.dc.html new file mode 100644 index 00000000..dd01a502 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/ArchiveHeader.dc.html @@ -0,0 +1,68 @@ + + + + + + + + + + + + + +
+
+
+ Familienarchiv + +
+
+ + +
+
MR
+
+
+
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/Dokument-Bearbeiten.dc.html b/design_handoff_familienarchiv_redesign/prototypes/Dokument-Bearbeiten.dc.html new file mode 100644 index 00000000..9e649efe --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/Dokument-Bearbeiten.dc.html @@ -0,0 +1,274 @@ + + + + + + + + + + + + + + +
+ + + +
+
+ + + {{ backLabel }} + + + + {{ workbenchTitle }} +
+ +
+ Erforderliche Felder {{ reqFilled }} / {{ reqTotal }} +
+
+
+
+
+ + +
+
+ + +
+ + + +
+ +
+ {{ pdfFilename }} + +
+ +
+
+
Im Felde, den 12. März 1916
+
Meine innig geliebte Clara, nun sind es bald zwei Jahre, dass ich Dich nicht mehr in den Armen halten durfte …
+
+
… (Kurrentschrift, Seite 1 von 4) …
+
Dein Dich ewig liebender Herbert
+
+
+
+
+ + + +
+ +
+
+
+ + +
+
+ + + +
+
Geteilte Metadaten
+

Felder werden ergänzt, nicht ersetzt.

+
+
+ + +
+

Wer & Wann

+ + + + + +
+ Empfänger {{ additiveSuffix }} +
+ + + {{ r.initials }} + {{ r.name }} + + + +
+
+ + +
+ + +
+ + + +
+ + +
+

Beschreibung

+ + + + + + + + +
+ Schlagworte {{ additiveSuffix }} +
+ + + + {{ t.label }} + + + +
+
+
+ +
+ + +
+ + Löschen + +
+ Abbrechen + + +
+
+
+
+
+
+
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/Dokument-Detail.dc.html b/design_handoff_familienarchiv_redesign/prototypes/Dokument-Detail.dc.html new file mode 100644 index 00000000..6e36c9cf --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/Dokument-Detail.dc.html @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + +
+ + + +
+
+ + + ← Zurück + +
+ + +
+
{{ signatur }} · Feldpost
+

{{ title }}

+
+ + +
+ + {{ p.initials }} + +
+ + + + + {{ status }} + + +
+ + + + +
+ + +
+ + + +
+
+ + +
+
+ + +
+
Details
+
+
+
Datum
+
31. Oktober 1915
+
+
+
Ort
+
Feldpost — Westfront
+
+
+
Status
+ + + {{ status }} + +
+
+
+ + + + + +
+
Schlagwörter
+ +
+ + +
+
Geschichten
+ Feldpost: Herbert an der Westfront 1914–1918 +
Marcel Raddatz · 11. Juni 2026
+ + + 3 Anmerkungen + +
+
+
+
+ + +
+ + +
+
+ +
+ +
Feldpostbrief — Seite 1
+
{{ signatur }} · Kurrentschrift
+
+
+ + +
+ + Seite 1 von 2 + +
+
+ + +
+ + +
+
+ +
+ Lesen + Bearbeiten +
+ + + Transkriptionsmodus + +
+ 4 Abschnitte · zuletzt 12. Juni 2026 +
+ + +
+ +
+

{{ b.text }}

+ +
+ Markiert · 1 Anmerkung +
+
+
+
+
+
+
+
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/Dokumente-Liste.dc.html b/design_handoff_familienarchiv_redesign/prototypes/Dokumente-Liste.dc.html new file mode 100644 index 00000000..1766637d --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/Dokumente-Liste.dc.html @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + +
+ +
+ + +
+
+
Archiv
+

Dokumente

+

Durchsuchen Sie den Bestand nach Titel, Person oder Schlagwort — gebündelt nach Jahr.

+
+ 147 Dokumente · 38 Personen +
+ + +
+ +
+
+ + +
+ + +
+ + +
+ + +
+
Schlagwörter
+
+ + + {{ t.name }} + + +
+ Und + Oder +
+
+
+ + +
+ + + + +
+ + +
+ +
+
+
+ + +
+

147 Treffer

+ +
+ + + +
+
+ {{ g.label }} +
+
+ +
+ {{ d.title }} +
+
+ + {{ p.initials }} + +
+ {{ d.date }} + + + {{ d.status }} + +
+
+
+
+
+
+ + +
+ Zurück + 1 + 2 + 3 + + 8 + Weiter +
+ +
+
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/Dokumente.dc.html b/design_handoff_familienarchiv_redesign/prototypes/Dokumente.dc.html new file mode 100644 index 00000000..4c4fdeef --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/Dokumente.dc.html @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + +
+ +
+ +
+
+
Archiv
+

Dokumente

+

Jüngste Eingänge, offene Aufgaben und der schnelle Weg zum nächsten Stück.

+
+ 147 Dokumente · 38 Personen +
+ + +
+
+ + +
+ + +
+ + +
+ Weiter bei: + Brief an Frieda aus dem Harz +
+ +
+ +
+
Zuletzt hinzugefügt
+ +
+ {{ d.title }} +
+
+ + {{ p.initials }} + +
+ {{ d.date }} + {{ d.status }} +
+
+
+

147 Dokumente · 38 Personen

+
+ + +
+
+
+ + Dateien hier ablegen oder klicken + PDF, JPG, PNG, TIFF bis 50 MB +
+
+
+
Benötigt Metadaten
+ +
+ {{ m }} +
+
+ Alle anzeigen → +
+
+
+ + +
+
Mission Control
+
+ +
+
+

{{ c.heading }}

+ {{ c.skill }} +

{{ c.weekly }}

+
+ +
+
+
+
+
+
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/Ereignis-Editor.dc.html b/design_handoff_familienarchiv_redesign/prototypes/Ereignis-Editor.dc.html new file mode 100644 index 00000000..2fc62b14 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/Ereignis-Editor.dc.html @@ -0,0 +1,288 @@ + + + + + + + + + + + + + + +
+ +
+ + + ← Zurück zum Zeitstrahl + + +
+
{{ eyebrow }}
+

{{ title }}

+

{{ lede }}

+
+ +
+ + +
+ + +
+

Wann & Was

+ + + + + +
+
Ereignistyp
+
+ + {{ seg.label }} + +
+
+ + +
+ Datum +
+ + +
+
+ Genauigkeit: + + {{ p.label }} + · + +
+
+
+ + +
+

Beschreibung

+ +
+
+ + +
+ + +
+

Beteiligte Personen

+ + +
+ + + {{ p.initials }} + {{ p.name }} + × + + +
+
+ +
+
Noch keine Person verknüpft.
+
Beginnen Sie zu tippen…
+
+
+ + +
+ + +
+
+ + +
+

Verknüpfte Briefe

+ + +
+ +
+ {{ d.iconEl }} +
+
{{ d.title }}
+
{{ d.meta }}
+
+ × +
+
+
+
+ +
+
Noch kein Brief verknüpft.
+
Verknüpfen Sie einen Brief aus dem Archiv…
+
+
+ + +
+ + +
+
+
+
+ + +
+
+ + Löschen + + Ereignisse erscheinen im Zeitstrahl. +
+
+ Abbrechen + +
+
+ +
+
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/Geschichte-Editor.dc.html b/design_handoff_familienarchiv_redesign/prototypes/Geschichte-Editor.dc.html new file mode 100644 index 00000000..ac44d32b --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/Geschichte-Editor.dc.html @@ -0,0 +1,401 @@ + + + + + + + + + + + + + + +
+ +
+ + + ← {{ backLabel }} + + +
+
{{ eyebrow }}
+

{{ title }}

+

{{ lede }}

+
+ + + +
+ + +
Art der Geschichte
+
+ Lesereise + Sammlung +
+ + +
+ + +
+
+ +

Lesereise

+
+

Eine geführte Abfolge von Briefen in fester Reihenfolge — die Lesenden gehen Schritt für Schritt durch den Briefwechsel. Zwischen den Dokumenten setzen Sie eigene Anmerkungen, die den Bogen spannen.

+
Ideal für: die Feldpost Herberts 1914–1918 chronologisch erzählt.
+
+ + +
+
+ +

Sammlung

+
+

Ein frei geschriebener Prosatext mit Überschriften und Absätzen, in den Sie einzelne Briefe als Belege einbetten. Sie führen die Feder — die Dokumente stützen Ihre Erzählung.

+
Ideal für: ein Porträt der drei Kinder über mehrere Jahrzehnte.
+
+ +
+ + + +
+
+ + + +
+ + +
+ + +
+ +
+ + +
+
+ + {{ t.iconEl }}{{ t.label }} + +
+
+ + +
+
+

Herbert und Clara Cram hatten drei Kinder: Hannemarie, Clara-Eugenie und Kurt-Georg. Die Briefe dieser Sammlung drehen sich um sie — als Kleinkinder, als Schüler, als junge Erwachsene im Zweiten Weltkrieg.

+ +

Die Töchter im Krieg

+

Clara-Eugenie dient bei der Flak, während Hannemarie 1945 mitten im Krieg heiratet. Durch die Briefe der Eltern und Großeltern entsteht das Bild einer ganzen Generation, die im Schatten der großen Geschichte aufwuchs:

+ +
+

„Schreibt mir, sobald Ihr könnt — die Tage sind lang ohne ein Wort von Euch."

+
+ +

Clara schreibt an ihre Kinder, die Großeltern schreiben über sie, Freunde erkundigen sich nach ihnen

+
+
+ +
+ + + +
+
+ + + +
+ + +
+ + + + + + + + +
Stationen der Lesereise
+ +
+ + + + +
+
+ + + + + + + + + +
+
{{ it.title }}
+
{{ it.meta }}
+
+ + × +
+ + + +
+
Anmerkung
+ +
+
+ + + +
+
+ +
+
+ + + + + Dokument hinzufügen + + + +
+
Variante: leere Lesereise
+
Noch keine Briefe in dieser Lesereise.
+
Fügen Sie über „Dokument hinzufügen" den ersten Brief hinzu…
+
+ +
+ + + +
+
+ + + +
+ Löschen +
+ + +
+
+
+ +
+
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/Geschichte.dc.html b/design_handoff_familienarchiv_redesign/prototypes/Geschichte.dc.html new file mode 100644 index 00000000..3c1751b0 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/Geschichte.dc.html @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + +
+ +
+ + ← Zurück zu Geschichten + + +
+
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/Geschichten.dc.html b/design_handoff_familienarchiv_redesign/prototypes/Geschichten.dc.html new file mode 100644 index 00000000..e7e201e4 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/Geschichten.dc.html @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + +
+ +
+ +
+
+
Sammlungen
+

Geschichten

+

Kuratierte Erzählungen aus Briefen, Karten und Fotografien des Archivs.

+
+ +
+ + +
+
+ Alle + Veröffentlicht + In Arbeit + Entwurf +
+ +
+ + +
+
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/Hilfe-Transkription.dc.html b/design_handoff_familienarchiv_redesign/prototypes/Hilfe-Transkription.dc.html new file mode 100644 index 00000000..67272f92 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/Hilfe-Transkription.dc.html @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + +
+ + +
+ + +
+
+
Hilfe
+

Transkription

+

Damit alle Briefe einheitlich übertragen werden — gleich, wer am Schreibtisch sitzt — finden Sie hier die Konventionen für das Familienarchiv.

+
+
+ + +

Die Briefe dieses Archivs sind in Kurrent und Sütterlin geschrieben — der deutschen Schreibschrift des späten 19. und frühen 20. Jahrhunderts. Übertragen Sie den Text so getreu wie möglich, ohne ihn zu modernisieren. Wo Sie unsicher sind, machen Sie die Unsicherheit sichtbar, statt sie zu verstecken. Diese Seite wächst mit: sobald wir eine neue Konvention beschließen, landet sie hier.

+ + +
+

Weiterführend

+

Die Kurrent- und Sütterlin-Alphabete sind bei Wikipedia ausführlich erklärt — mit Buchstabentafeln zum Vergleichen. Hier auf dieser Seite stehen nur unsere eigenen Vereinbarungen für dieses Archiv.

+ + + Kurrentschrift auf Wikipedia + + +
+ + +

Regeln für die Transkription

+ + +
+ +
+
+ {{ r.iconEl }} +

{{ r.title }}

+
+

{{ r.body }}

+ + + +
+
Beispiel
+
+ {{ r.in }} + + {{ r.out }} +
+
+
+
+
+
+ + +

In Klärung

+

Diese Fragen besprechen wir noch. Stoßen Sie beim Transkribieren darauf, treffen Sie eine plausible Wahl und notieren Sie sie im Kommentar des Blocks.

+
+ + + + {{ c }} + + +
+ + +
+
+ + + +

Fehlt eine Regel?

+
+

Stolpern Sie über eine Situation, die hier nicht steht — schreiben Sie einen Kommentar beim betreffenden Block. Wir sammeln die offenen Fälle und besprechen sie beim nächsten Familientreffen.

+
+ +
+
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/PersonDetail.dc.html b/design_handoff_familienarchiv_redesign/prototypes/PersonDetail.dc.html new file mode 100644 index 00000000..24d1a074 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/PersonDetail.dc.html @@ -0,0 +1,273 @@ + + + + + + + + + + + + + + +
+ +
+ + +
+
+
Person
+

{{ person.name }}

+

{{ person.lede }}

+
+ +
+ + +
+ + +
+ + +
+
+ {{ person.initials }} +
+

{{ person.name }}

+ + {{ person.badge.label }} + +
+
+ + +

„{{ person.alias }}"

+
+ +

+ {{ person.birthYear }} {{ person.deathYear }} +

+ +
+ + +
+ + {{ person.letters }} Briefe·{{ person.docs }} Dokumente +
+
+ + +
+

Namensverlauf

+
    + +
  1. + {{ n.kind }} + {{ n.name }} + {{ n.year }} +
  2. +
    +
+
+
+ + +
+ + + + + +
+

Beziehungen

+ +
+ + +
+
+

Briefe — Gesendet

+ {{ sentCount }} gesamt +
+ +
+ + +
+
+

Briefe — Empfangen

+ {{ receivedCount }} gesamt +
+ +
+ + + + +
+
+
+
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/PersonForm.dc.html b/design_handoff_familienarchiv_redesign/prototypes/PersonForm.dc.html new file mode 100644 index 00000000..88a755a9 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/PersonForm.dc.html @@ -0,0 +1,268 @@ + + + + + + + + + + + + + + +
+ +
+ + + ← Zurück zu Personen + + +
+
{{ eyebrow }}
+

{{ title }}

+

{{ lede }}

+
+ + +
+

Stammdaten

+ + +
+
Personentyp
+
+ + {{ seg.label }} + +
+
+ + +
+ + +
+ + + + + + + + +
+
+ Geburtsdatum + +
+ Genauigkeit:Tag·Monat·Jahr +
+
+
+ Sterbedatum + +
+ Genauigkeit:Tag·Monat·Jahr +
+
+
+ + + + + + +
+ + +
+

Namensverlauf

+ + +
+ +
+
+ {{ a.kind }} + {{ a.name }} +
+ Entfernen +
+
+
+
+ +
+
Noch keine früheren Namen erfasst.
+
Ergänzen Sie Geburts- oder Ehenamen unten…
+
+
+ + +
+
Namen hinzufügen
+
+ + + + +
+
+
+ + + +
+

Zusammenführen

+

Diese Person in eine andere überführen. Alle Briefe und Verknüpfungen wandern zur Zielperson — {{ title }} wird danach gelöscht.

+ +
+ + +
+
+
+ + +
+ Verwerfen +
+ + Löschen + + +
+
+ +
+
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/PersonReview.dc.html b/design_handoff_familienarchiv_redesign/prototypes/PersonReview.dc.html new file mode 100644 index 00000000..6a235d6d --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/PersonReview.dc.html @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + +
+ +
+ + +
+
+
Prüfen
+

Personen prüfen

+

Vom Import erzeugte, noch nicht bestätigte Personen. Bitte prüfen Sie jeden Eintrag — bestätigen, umbenennen, zusammenführen oder löschen.

+
+ 12 zu prüfen +
+ + +
+ +
+ + +
+ + + + + +
+
{{ r.name }}
+ +
+ + {{ r.docs }} Dokumente·zuerst gesehen {{ r.firstSeen }} +
+
+ + +
+
+ Bestätigen + Umbenennen + Zusammenführen +
+ Löschen +
+
+ + + +
+
+ + +
+ + +
+
+
+
+ + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
Keine Personen zu prüfen.
+
Alle Einträge wurden bestätigt — neue erscheinen nach dem nächsten Import…
+
+ + +
+ Zurück + 1 + 2 + Weiter +
+ +
+ + +
+
+

Person löschen

+

Diese Person wird endgültig gelöscht. Dokumentverweise bleiben erhalten, verlieren aber diese Person.

+
+ + +
+
+
+ +
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/Personen.dc.html b/design_handoff_familienarchiv_redesign/prototypes/Personen.dc.html new file mode 100644 index 00000000..54d85d4f --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/Personen.dc.html @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + +
+ +
+ +
+
+
Verzeichnis
+

Personen

+

Jede Hand, die schrieb oder genannt wurde — Absender, Empfänger, Erwähnte.

+
+ 38 Personen +
+ + +
+
+ + +
+
+ Alle + Personen + Institutionen + Gruppen +
+
+ +
+ +
+
+ {{ p.initials }} +
+

{{ p.name }}

+
{{ p.relation }}
+
+ + {{ p.badge.label }} + +
+
+
+ + {{ p.letters }} Briefe·{{ p.docCount }} Dokumente +
+
+
+
+
+
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/Profil.dc.html b/design_handoff_familienarchiv_redesign/prototypes/Profil.dc.html new file mode 100644 index 00000000..186c08c1 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/Profil.dc.html @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + +
+ +
+ + + + + +
+
+
Konto
+

Profil

+

Verwalten Sie Ihre persönlichen Daten, Ihr Passwort und Ihre Benachrichtigungen.

+
+
+ + +
+ + Ihre Änderungen wurden gespeichert. +
+ + +
+ + +
+

Persönliche Daten

+
+ + + +
+ +
+ + +
+

Passwort ändern

+
+ + + +
+ +
+
+ + +
+

Benachrichtigungen

+ + + + + +
+ +
+ + + + + +
+
+
Mitglied
+

{{ av.name }}

+

Mitglied des Familienarchivs.

+
+
+ + +
+
+ +
+ {{ av.initials }} +
+ +
+

{{ av.name }}

+
+ +
+
+ E-Mail + marcel@raddatz.cloud +
+
+
+ Kontakt + Berlin · erreichbar per E-Mail +
+
+
+ Beigetreten + März 2024 +
+
+
+
+ +
+ +
+
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/Regeln.dc.html b/design_handoff_familienarchiv_redesign/prototypes/Regeln.dc.html new file mode 100644 index 00000000..87199c5a --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/Regeln.dc.html @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + +
+ +
+ +
+
Vereinheitlichtes System
+

Ein Regelwerk für das Familienarchiv

+

Sieben Bausteine, die jede Seite teilt — damit Titel, Steuerung, Karten und Metadaten überall gleich klingen. Diese Richtung — „Mappe" — liegt allen Seiten zugrunde.

+
+ +
+ + +
+
01 — Typografie
+
+
+ Seitentitel + Tinos · Serif +
+
+ Rubrik / Label + Montserrat · Versal +
+
+ Fließtext, Briefinhalt und Transkription + Tinos · 16 +
+
+ 14. März 1923 · 4 Personen + Montserrat · Meta +
+
+
+ + +
+
02 — Farbe
+
+
Navy — Tinte & Primärfläche
+
Sand — Hintergrund
+
Mint — nur Linie, Streifen, Unterton
+
Türkis — Transkriptionsmodus
+
+

Keine Verläufe. Mint trägt nie Text.

+
+ + +
+
03 — Seitenkopf
+
+
Geschichten
+
Seitentitel
+

Rubrik in Versalien, großer Serif-Titel, Mint-Steg links. Rechts steht die Zählung.

+
+
+ + +
+
04 — Steuerung
+

Segmentgruppe für Filter & Ansichten

+
+ Alle + Veröffentlicht + Entwurf +
+
+ + +
+
05 — Personen & Avatare
+
+ FR + KM + AB + WR + MH +
+

Die Farbe wird je Person aus dem Namen berechnet und bleibt stabil — sie unterscheidet, sie schmückt nicht. Form immer rund, Initialen in Montserrat.

+
+ + +
+
06 — Metadaten & Leerzustände
+
+
+

Eine Metazeile, immer gleich getrennt mit ·

+
+ + 14. März 1923·14 Dokumente·4 Personen +
+
+
+

Leerzustand — ruhig, Sie-Form, mit Auslassung

+
+

Noch keine Geschichten angelegt.

+

Beginnen Sie mit einem Brief…

+
+
+
+
+ +
+
+
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/Stammbaum.dc.html b/design_handoff_familienarchiv_redesign/prototypes/Stammbaum.dc.html new file mode 100644 index 00000000..c6c3da62 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/Stammbaum.dc.html @@ -0,0 +1,221 @@ + + + + + + + + + + + + + + +
+ +
+ + +
+
+
Stammbaum
+

Familie Cram

+

Drei Generationen, verwoben durch Brief und Blut — wählen Sie eine Person, um ihre Linie zu verfolgen.

+
+ 38 Personen · 4 Generationen +
+ + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
G 1
+
G 2
+
G 3
+ + + +
+ {{ n.initials }} +
+
{{ n.name }}
+
{{ n.dates }}
+
+
+
+ + +
+ + + +
+
+ + + +
+ + +
+
Alternativ — leerer Zustand
+
+
Noch kein Stammbaum vorhanden.
+
Verknüpfen Sie zwei Personen, um die erste Linie zu zeichnen…
+
+
+ +
+
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/Themen.dc.html b/design_handoff_familienarchiv_redesign/prototypes/Themen.dc.html new file mode 100644 index 00000000..c53a4f79 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/Themen.dc.html @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + +
+ +
+ + +
+
+
Themen
+

Themen

+

Worüber die Familie schrieb — Schlagworte, nach denen Sie die Sammlung durchstöbern können.

+
+ 24 Themen · 312 Dokumente +
+ + +
+ Alle + Mit Dokumenten + Alphabetisch +
+ + +
+ + + + + +
+
Noch kein Thema vergeben.
+
Verschlagworten Sie einen Brief, um hier ein Thema anzulegen…
+
+
+ +
+
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/Zeitstrahl.dc.html b/design_handoff_familienarchiv_redesign/prototypes/Zeitstrahl.dc.html new file mode 100644 index 00000000..d39f82af --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/Zeitstrahl.dc.html @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + +
+ +
+ +
+
+
Chronik
+

Zeitstrahl

+

Briefdichte, Lebensdaten und kuratierte Stationen — die Sammlung im Lauf der Jahre.

+
+ 147 Dokumente · 1914–1948 +
+ + +
+
+ Alle + Briefe + Personen + Ereignisse +
+
+ Person + Kuratiert + Historisch +
+
+ + +
+
+
+ + + +
{{ it.label }}
+
+ + +
+
+
+ + {{ it.count }} +
+ Briefe anzeigen → +
+
+ +
+
+
+
+ {{ it.from }}Monats-Dichte{{ it.to }} +
+
+
+ + +
+
+
+ + {{ it.title }} +
+
{{ it.meta }}
+ + {{ it.tag.label }} + +
+ +
+
+ + +
+ {{ it.glyph }} +
+
{{ it.name }}
+
{{ it.meta }}
+
+
+
+ + +
+ +
+
{{ it.title }}
+
{{ it.meta }}
+
+ +
+
+ + +
+ + {{ it.title }} + {{ it.meta }} +
+
+ +
+
+
+
+
+
+ + + diff --git a/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Account-MD.svg b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Account-MD.svg new file mode 100644 index 00000000..fef46f08 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Account-MD.svg @@ -0,0 +1,7 @@ + + + 🧩 Icons/Simple/Action/24px/Account-MD + + + + \ No newline at end of file diff --git a/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Arrow-Right-MD.svg b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Arrow-Right-MD.svg new file mode 100644 index 00000000..93ee1e69 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Arrow-Right-MD.svg @@ -0,0 +1,7 @@ + + + 🧩 Icons/Simple/Action/24px/Arrow/Arrow-Right-MD + + + + \ No newline at end of file diff --git a/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Bookmarks-MD.svg b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Bookmarks-MD.svg new file mode 100644 index 00000000..20b567ea --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Bookmarks-MD.svg @@ -0,0 +1,7 @@ + + + 🧩 Icons/Simple/Action/24px/Bookmarks-MD + + + + \ No newline at end of file diff --git a/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Calendar-Add-MD.svg b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Calendar-Add-MD.svg new file mode 100644 index 00000000..68f92c38 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Calendar-Add-MD.svg @@ -0,0 +1,7 @@ + + + 🧩 Icons/Simple/Action/24px/Calendar/Calendar-Add-MD + + + + \ No newline at end of file diff --git a/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Chat-MD.svg b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Chat-MD.svg new file mode 100644 index 00000000..51f4c501 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Chat-MD.svg @@ -0,0 +1,7 @@ + + + 🧩 Icons/Simple/Action/24px/Chat-MD + + + + \ No newline at end of file diff --git a/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Check-MD.svg b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Check-MD.svg new file mode 100644 index 00000000..480294a6 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Check-MD.svg @@ -0,0 +1,7 @@ + + + 🧩 Icons/Simple/Action/24px/Check/Check-Isolation-MD + + + + \ No newline at end of file diff --git a/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Copy-Item-MD.svg b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Copy-Item-MD.svg new file mode 100644 index 00000000..fa69e659 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Copy-Item-MD.svg @@ -0,0 +1,7 @@ + + + 🧩 Icons/Simple/Action/24px/Copy-Item-MD + + + + \ No newline at end of file diff --git a/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Edit-Content-MD.svg b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Edit-Content-MD.svg new file mode 100644 index 00000000..e0265ade --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Edit-Content-MD.svg @@ -0,0 +1,7 @@ + + + 🧩 Icons/Simple/Action/24px/Edit-Content-MD + + + + \ No newline at end of file diff --git a/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Filter-MD.svg b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Filter-MD.svg new file mode 100644 index 00000000..472e73b6 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Filter-MD.svg @@ -0,0 +1,7 @@ + + + 🧩 Icons/Simple/Action/24px/Filter/Filter-Outline-MD + + + + \ No newline at end of file diff --git a/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Folder-MD.svg b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Folder-MD.svg new file mode 100644 index 00000000..81d25d91 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Folder-MD.svg @@ -0,0 +1,7 @@ + + + 🧩 Icons/Simple/Action/24px/Folder-MD + + + + \ No newline at end of file diff --git a/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Globe-MD.svg b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Globe-MD.svg new file mode 100644 index 00000000..6e99588f --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Globe-MD.svg @@ -0,0 +1,7 @@ + + + 🧩 Icons/Simple/Action/24px/Globe-MD + + + + \ No newline at end of file diff --git a/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Library-MD.svg b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Library-MD.svg new file mode 100644 index 00000000..f233330e --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Library-MD.svg @@ -0,0 +1,7 @@ + + + 🧩 Icons/Simple/Action/24px/Library-MD + + + + \ No newline at end of file diff --git a/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Location-MD.svg b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Location-MD.svg new file mode 100644 index 00000000..299ec7be --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Location-MD.svg @@ -0,0 +1,7 @@ + + + 🧩 Icons/Simple/Action/24px/Location-MD + + + + \ No newline at end of file diff --git a/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Mag-Glass-MD.svg b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Mag-Glass-MD.svg new file mode 100644 index 00000000..a9f21dec --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Mag-Glass-MD.svg @@ -0,0 +1,7 @@ + + + 🧩 Icons/Simple/Action/24px/Mag-Glass-MD + + + + \ No newline at end of file diff --git a/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Mail-MD.svg b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Mail-MD.svg new file mode 100644 index 00000000..e8675780 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Mail-MD.svg @@ -0,0 +1,7 @@ + + + 🧩 Icons/Simple/Action/24px/Mail-MD + + + + \ No newline at end of file diff --git a/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Refresh-MD.svg b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Refresh-MD.svg new file mode 100644 index 00000000..b9c319b2 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Refresh-MD.svg @@ -0,0 +1,7 @@ + + + 🧩 Icons/Simple/Action/24px/Refresh-MD + + + + \ No newline at end of file diff --git a/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Upload-MD.svg b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Upload-MD.svg new file mode 100644 index 00000000..13b39fd4 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/Upload-MD.svg @@ -0,0 +1,7 @@ + + + 🧩 Icons/Simple/Action/24px/Upload-MD + + + + \ No newline at end of file diff --git a/design_handoff_familienarchiv_redesign/prototypes/assets/icons/View-More-MD.svg b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/View-More-MD.svg new file mode 100644 index 00000000..fc09b022 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/assets/icons/View-More-MD.svg @@ -0,0 +1,7 @@ + + + 🧩 Icons/Simple/Action/24px/View-More-MD + + + + \ No newline at end of file diff --git a/design_handoff_familienarchiv_redesign/prototypes/colors_and_type.css b/design_handoff_familienarchiv_redesign/prototypes/colors_and_type.css new file mode 100644 index 00000000..daff11c0 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/colors_and_type.css @@ -0,0 +1,254 @@ +/* ============================================================ + Familienarchiv — Design System Tokens + Extracted from frontend/src/routes/layout.css + ============================================================ */ + +/* ─── Fonts ─── */ +/* Tinos = Times substitute, Montserrat = Gotham substitute + (De Gruyter Brill CI) — loaded from Google Fonts. */ +@import url('https://fonts.googleapis.com/css2?family=Tinos:ital,wght@0,400;0,700;1,400;1,700&family=Montserrat:wght@400;500;600;700&display=swap'); + +:root { + /* ─── Font families ─── */ + --font-sans: 'Montserrat', ui-sans-serif, system-ui, sans-serif; + --font-serif: 'Tinos', 'Times New Roman', Georgia, serif; + + /* ─── Raw brand palette (never used directly in components) ─── */ + --palette-navy: #012851; + --palette-mint: #a1dcd8; + --palette-turquoise: #00c7b1; + --palette-sand: #f0efe9; + + /* ─── Semantic surfaces ─── */ + --c-canvas: #f0efe9; /* app background (sand) */ + --c-surface: #ffffff; /* cards / inputs */ + --c-overlay: #ffffff; /* menus, popovers */ + --c-muted: #f5f4ef; /* subtle fills */ + + /* ─── Borders ─── */ + --c-line: #e4e2d7; + --c-line-2: #eeede8; + + /* ─── Text (ink) ─── */ + --c-ink: #012851; /* navy — primary text */ + --c-ink-2: #4b5563; /* gray-600 — body secondary */ + --c-ink-3: #6b7280; /* gray-500 — meta / placeholder */ + + /* ─── Accent: decorative mint ─── */ + --c-accent: #a1dcd8; + --c-accent-bg: rgba(161, 220, 216, 0.15); + + /* ─── Primary interactive (navy) ─── */ + --c-primary: #012851; + --c-primary-fg: #ffffff; + + /* ─── Header — always brand-navy ─── */ + --c-header: #012851; + + /* ─── Turquoise — transcription accent ─── */ + --c-turquoise: #00c7b1; + --c-turquoise-fg: #ffffff; + + /* ─── Focus ring (navy in light mode) ─── */ + --c-focus-ring: #012851; + + /* ─── Danger ─── */ + --c-danger: #c0392b; + --c-danger-fg: #ffffff; + + /* ─── Warning (amber AA on white) ─── */ + --c-warning: #b45309; + --c-warning-fg: #ffffff; + + /* ─── PDF viewer chrome ─── */ + --c-pdf-bg: #ebebeb; + --c-pdf-ctrl: #d8d8d8; + --c-pdf-text: #333333; + + /* ─── Tag dot colors (decorative) ─── */ + --c-tag-sage: #5a8a6a; + --c-tag-sienna: #a0522d; + --c-tag-amber: #c17a00; + --c-tag-slate: #607080; + --c-tag-violet: #7a4f9a; + --c-tag-rose: #c0446e; + --c-tag-cobalt: #3060b0; + --c-tag-moss: #4a7a3a; + --c-tag-sand: #9a8040; + --c-tag-coral: #c05540; + + /* ─── Person badge types ─── */ + --c-badge-institution-bg: #e8eff7; + --c-badge-institution-text: #1a4971; + --c-badge-institution-border: #c4d5e8; + --c-badge-group-bg: #f0e8f5; + --c-badge-group-text: #5a2d6f; + --c-badge-group-border: #d8c5e3; + --c-badge-unknown-bg: #fdf4e3; + --c-badge-unknown-text: #7a5a0a; + --c-badge-unknown-border: #f0ddb3; + + /* ─── Spacing / radii ─── */ + --radius-none: 0; + --radius-sm: 2px; /* cards, inputs — "rounded-sm" in tailwind */ + --radius-md: 4px; + --radius-full: 9999px; + + /* ─── Shadows ─── */ + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + + /* ─── Breakpoints (reference only) ─── */ + --breakpoint-xs: 375px; + + /* ─── Typographic sizes ─── */ + --text-huge: 4rem; /* 64px — hero */ +} + +/* ─── Dark mode (navy-tinted) ─── */ +:root[data-theme='dark'] { + color-scheme: dark; + + --c-canvas: #010e1e; + --c-surface: #011526; + --c-overlay: #011e38; + --c-muted: #011a30; + + --c-line: #0d3358; + --c-line-2: #092843; + + --c-ink: #f0efe9; + --c-ink-2: #9ca3af; + --c-ink-3: #8b97a5; + + --c-accent: #00c7b1; + --c-accent-bg: rgba(0, 199, 177, 0.12); + + --c-primary: #a1dcd8; + --c-primary-fg: #012851; + + --c-header: #012851; + + --c-focus-ring: #a1dcd8; + + --c-pdf-bg: #010e1e; + --c-pdf-ctrl: #011526; + --c-pdf-text: #f0efe9; + + --c-danger: #e55347; + --c-danger-fg: #ffffff; +} + +/* ─── Base element styles ─── */ +body { + background-color: var(--c-canvas); + color: var(--c-ink); + font-family: var(--font-serif); + font-size: 16px; + line-height: 1.5; +} + +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-sans); + font-weight: 600; + color: var(--c-ink); +} + +/* ─── Semantic typography + The app uses Montserrat for UI labels / headings / buttons and + Tinos for body, letter content, transcriptions, and document + titles. Labels are almost always UPPERCASE + tracking-widest. ─── */ + +.ds-display { + font-family: var(--font-sans); + font-size: 64px; /* text-huge */ + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; + line-height: 1.05; + color: var(--c-ink); +} + +.ds-h1 { + font-family: var(--font-sans); + font-size: 32px; + font-weight: 700; + letter-spacing: 0.1em; /* tracking-widest */ + text-transform: uppercase; + color: var(--c-ink); +} + +.ds-h2 { + font-family: var(--font-sans); + font-size: 20px; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--c-ink); +} + +.ds-h3 { /* section title — serif, used for documents */ + font-family: var(--font-serif); + font-size: 20px; + font-weight: 500; + color: var(--c-ink); +} + +.ds-body { + font-family: var(--font-serif); + font-size: 16px; + line-height: 1.6; + color: var(--c-ink); +} + +.ds-body-sm { + font-family: var(--font-sans); + font-size: 14px; + color: var(--c-ink-2); +} + +.ds-meta { /* timestamps, counts, field meta */ + font-family: var(--font-sans); + font-size: 12px; + color: var(--c-ink-3); +} + +.ds-label { /* form labels, section captions */ + font-family: var(--font-sans); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--c-ink-2); +} + +.ds-label-xs { /* tag chip style — extra-tiny caps */ + font-family: var(--font-sans); + font-size: 10px; + font-weight: 700; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--c-ink); +} + +.ds-button-label { /* buttons, nav items */ + font-family: var(--font-sans); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.ds-link { + color: var(--c-ink); + text-decoration: underline; + text-decoration-color: var(--c-accent); + text-underline-offset: 4px; + text-decoration-thickness: 2px; +} + +.ds-italic-quote { /* search snippet / excerpt */ + font-family: var(--font-serif); + font-style: italic; + color: var(--c-ink-2); +} diff --git a/design_handoff_familienarchiv_redesign/prototypes/support.js b/design_handoff_familienarchiv_redesign/prototypes/support.js new file mode 100644 index 00000000..ae3a5037 --- /dev/null +++ b/design_handoff_familienarchiv_redesign/prototypes/support.js @@ -0,0 +1,1464 @@ +// GENERATED from dc-runtime/src/*.ts — do not edit. Rebuild with `cd dc-runtime && bun run build`. +"use strict"; +(() => { + var __defProp = Object.defineProperty; + var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; + var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + + // src/react.ts + function getReact() { + const R = window.React; + if (!R) throw new Error("dc-runtime: window.React is not available yet"); + return R; + } + function getReactDOM() { + const RD = window.ReactDOM; + if (!RD) throw new Error("dc-runtime: window.ReactDOM is not available yet"); + return RD; + } + var h = ((...args) => getReact().createElement( + ...args + )); + + // src/parse.ts + function parseDcDocument(doc) { + const dc = doc.querySelector("x-dc"); + if (!dc) return null; + const scriptEl = doc.querySelector("script[data-dc-script]"); + const { props, preview } = parseDataProps( + scriptEl?.getAttribute("data-props") ?? null + ); + return { + template: dc.innerHTML, + js: scriptEl ? scriptEl.textContent || "" : "", + props, + preview + }; + } + function parseDcText(src) { + const openMatch = /]*)?>/.exec(src); + if (!openMatch) return null; + const close = src.lastIndexOf(""); + if (close === -1 || close < openMatch.index) return null; + const template = src.slice(openMatch.index + openMatch[0].length, close); + const doc = new DOMParser().parseFromString(src, "text/html"); + const scriptEl = doc.querySelector("script[data-dc-script]"); + const { props, preview } = parseDataProps( + scriptEl?.getAttribute("data-props") ?? null + ); + return { + template, + js: scriptEl ? scriptEl.textContent || "" : "", + props, + preview + }; + } + function parseDataProps(raw) { + if (!raw) return { props: null, preview: null }; + let parsed; + try { + parsed = JSON.parse(raw); + } catch { + return { props: null, preview: null }; + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return { props: null, preview: null }; + } + const obj = parsed; + const preview = obj.$preview && typeof obj.$preview === "object" ? obj.$preview : null; + const rest = {}; + for (const k of Object.keys(obj)) { + if (k[0] !== "$") rest[k] = obj[k]; + } + return { props: Object.keys(rest).length ? rest : null, preview }; + } + function dcNameFromPath(pathname) { + let p = pathname || ""; + try { + p = decodeURIComponent(p); + } catch { + } + const base = p.split("/").pop() || "Root"; + return base.replace(/\.dc\.html$/, "").replace(/\.html?$/, "") || "Root"; + } + + // src/boot.ts + var BASE_CSS = ` + .sc-placeholder{background:rgba(255,255,255,.3);border:1px solid rgba(0,0,0,.5); + border-radius:2px;box-sizing:border-box;overflow:hidden} + @keyframes sc-shine{0%{background-position:100% 50%}100%{background-position:0% 50%}} + html.sc-dc-streaming .sc-placeholder, + html.sc-dc-streaming .sc-interp.sc-missing{position:relative; + background:color-mix(in srgb,currentColor 5%,transparent); + border-color:transparent} + html.sc-dc-streaming .sc-placeholder::before, + html.sc-dc-streaming .sc-interp.sc-missing::before{content:''; + position:absolute;inset:0;pointer-events:none; + background:linear-gradient(90deg,rgba(217,119,87,0) 25%,rgba(247,225,211,.95) 37%,rgba(217,119,87,0) 63%); + background-size:400% 100%;animation:sc-shine 1.4s ease infinite} + html.sc-dc-streaming .sc-placeholder:nth-child(n+9 of .sc-placeholder)::before, + html.sc-dc-streaming .sc-interp.sc-missing:nth-child(n+9 of .sc-interp.sc-missing)::before{animation:none; + background:color-mix(in srgb,currentColor 8%,transparent)} + .sc-placeholder-error{padding:4px 8px;font:11px/1.4 ui-monospace,monospace; + color:rgba(0,0,0,.7);word-break:break-word} + .sc-interp.sc-missing{display:inline-block;width:2em;height:1em;overflow:hidden; + vertical-align:text-bottom;background:rgba(255,255,255,.3);border:1px solid rgba(0,0,0,.5); + border-radius:2px;box-sizing:border-box;color:transparent; + user-select:none} + .sc-interp.sc-unresolved{font-family:ui-monospace,monospace;font-size:.85em; + color:rgba(0,0,0,.5);background:rgba(0,0,0,.05);border-radius:3px; + padding:0 3px} + .sc-host.sc-has-error{position:relative} + .sc-logic-error{position:absolute;top:8px;left:8px;z-index:2147483647;max-width:60ch; + padding:6px 10px;background:#b00020;color:#fff;font:12px/1.4 ui-monospace,monospace; + border-radius:4px;white-space:pre-wrap;pointer-events:none} + /* Mirrors PRINT_BASELINE_CSS in apps/web deck-stage-export.ts \u2014 keep both + in sync until dc-runtime regains a build step. */ + @media print { + @page { margin: 0.5cm; } + html, body { print-color-adjust: exact; -webkit-print-color-adjust: exact; } + section, article, figure, table { break-inside: avoid; } + *, *::before, *::after { + animation-delay: -99s !important; animation-duration: .001s !important; + animation-iteration-count: 1 !important; animation-fill-mode: both !important; + animation-play-state: running !important; transition-duration: 0s !important; + } + } + `; + var FULL_PAGE_CSS = "html,body{height:100%;margin:0}#dc-root,#dc-root>.sc-host{height:100%}"; + function rootNameForDocument(doc, loc) { + let bootPath = loc.pathname || ""; + if (!/\.dc\.html?$/i.test(safeDecode(bootPath))) { + try { + bootPath = new URL(doc.baseURI || "/").pathname; + } catch { + } + } + return dcNameFromPath(bootPath); + } + function safeDecode(s) { + try { + return decodeURIComponent(s); + } catch { + return s; + } + } + function boot(runtime, doc = document) { + const parsed = parseDcDocument(doc); + if (!parsed) return null; + const React = getReact(); + const rootName = rootNameForDocument(doc, location); + runtime.markFetched(rootName); + runtime.adoptParsed(rootName, parsed); + fetch(location.href).then((res) => res.ok ? res.text() : "").then((t) => { + const raw = t ? parseDcText(t) : null; + if (raw?.template) runtime.updateHtml(rootName, raw.template); + }).catch(() => { + }); + const dc = doc.querySelector("x-dc"); + const hostEl = doc.createElement("div"); + hostEl.id = "dc-root"; + dc.replaceWith(hostEl); + if (!parsed.preview) { + const s = doc.createElement("style"); + s.textContent = FULL_PAGE_CSS; + doc.head.appendChild(s); + } + const Root = runtime.getDC(rootName); + const entry = runtime.registry.get(rootName); + function StandaloneRoot() { + const [, setTick] = React.useState(0); + React.useEffect(() => { + const sub = () => setTick((n) => n + 1); + entry.subs.add(sub); + return () => { + entry.subs.delete(sub); + }; + }, []); + return h(Root, entry.propOverrides || null); + } + const ReactDOM = getReactDOM(); + if (ReactDOM.createRoot) + ReactDOM.createRoot(hostEl).render(h(StandaloneRoot)); + else ReactDOM.render(h(StandaloneRoot), hostEl); + return rootName; + } + + // src/expr.ts + var IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*/; + var NUMBER_RE = /^-?\d+(\.\d+)?$/; + function resolve(vals, src) { + const expr = String(src).trim(); + if (!expr) return void 0; + if (expr[0] === "(" && expr[expr.length - 1] === ")" && parensWrapWhole(expr)) { + return resolve(vals, expr.slice(1, -1)); + } + const eq = findTopLevelEquality(expr); + if (eq) { + const lv = resolve(vals, expr.slice(0, eq.index)); + const rv = resolve(vals, expr.slice(eq.index + eq.op.length)); + switch (eq.op) { + case "===": + return lv === rv; + case "!==": + return lv !== rv; + case "==": + return lv == rv; + default: + return lv != rv; + } + } + if (expr[0] === "!") return !resolve(vals, expr.slice(1)); + if (expr === "true") return true; + if (expr === "false") return false; + if (expr === "null") return null; + if (expr === "undefined") return void 0; + if (NUMBER_RE.test(expr)) return Number(expr); + if (expr.length >= 2 && (expr[0] === '"' || expr[0] === "'") && expr[expr.length - 1] === expr[0]) { + return expr.slice(1, -1); + } + return resolvePath(vals, expr); + } + function parensWrapWhole(expr) { + let depth = 0; + for (let i = 0; i < expr.length - 1; i++) { + if (expr[i] === "(") depth++; + else if (expr[i] === ")") { + depth--; + if (depth === 0) return false; + } + } + return true; + } + function findTopLevelEquality(expr) { + let depth = 0; + for (let i = 0; i < expr.length; i++) { + const c = expr[i]; + if (c === "[" || c === "(") depth++; + else if (c === "]" || c === ")") depth--; + else if (depth === 0 && (c === "=" || c === "!") && expr[i + 1] === "=") { + if (i > 0 && (expr[i - 1] === "=" || expr[i - 1] === "!")) continue; + if (!expr.slice(0, i).trim()) continue; + const op = expr[i + 2] === "=" ? c + "==" : c + "="; + return { index: i, op }; + } + } + return null; + } + function resolvePath(vals, expr) { + const head = expr.match(IDENT_RE); + if (!head) return void 0; + let cur = vals == null ? void 0 : vals[head[0]]; + let i = head[0].length; + while (i < expr.length) { + if (expr[i] === ".") { + const m = expr.slice(i + 1).match(IDENT_RE) || expr.slice(i + 1).match(/^\d+/); + if (!m) return void 0; + cur = cur == null ? void 0 : cur[m[0]]; + i += 1 + m[0].length; + } else if (expr[i] === "[") { + let depth = 1; + let j = i + 1; + while (j < expr.length && depth > 0) { + if (expr[j] === "[") depth++; + else if (expr[j] === "]") { + depth--; + if (depth === 0) break; + } + j++; + } + if (depth !== 0) return void 0; + const key = resolve(vals, expr.slice(i + 1, j)); + cur = cur == null ? void 0 : cur[key]; + i = j + 1; + } else { + return void 0; + } + } + return cur; + } + + // src/encode.ts + var CAMEL_ATTR = "sc-camel-"; + var RAW_WRAP = { + select: "sc-raw-select", + table: "sc-raw-table", + tbody: "sc-raw-tbody", + thead: "sc-raw-thead", + tfoot: "sc-raw-tfoot", + tr: "sc-raw-tr", + td: "sc-raw-td", + th: "sc-raw-th", + caption: "sc-raw-caption" + }; + var RAW_UNWRAP = Object.fromEntries( + Object.entries(RAW_WRAP).map(([k, v]) => [v, k]) + ); + var EVENT_MAP = { + onclick: "onClick", + onchange: "onChange", + oninput: "onInput", + onsubmit: "onSubmit", + onkeydown: "onKeyDown", + onkeyup: "onKeyUp", + onkeypress: "onKeyPress", + onmousedown: "onMouseDown", + onmouseup: "onMouseUp", + onmouseenter: "onMouseEnter", + onmouseleave: "onMouseLeave", + onfocus: "onFocus", + onblur: "onBlur", + ondoubleclick: "onDoubleClick", + oncontextmenu: "onContextMenu" + }; + var ATTRS = `(?:[^>"']|"[^"]*"|'[^']*')*`; + var IMPORT_SELF_CLOSE_RE = new RegExp( + "<(x-import|dc-import)(" + ATTRS + ")/>", + "gi" + ); + var CAMEL_ATTR_RE = /(\s)([a-z]+[A-Z][A-Za-z0-9]*)(\s*=)/g; + function encodeCase(html) { + html = html.replace( + IMPORT_SELF_CLOSE_RE, + (_, t, a) => "<" + t + a + ">" + ); + html = html.replace(/)/gi, "/gi, ""); + html = html.replace( + CAMEL_ATTR_RE, + (_, sp, name, eq) => sp + CAMEL_ATTR + name.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()) + eq + ); + for (const [real, alias] of Object.entries(RAW_WRAP)) { + html = html.replace( + new RegExp("(])", "gi"), + "$1" + alias + ); + } + return html; + } + function kebabToCamel(s) { + return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); + } + function cssToObj(css) { + const o = {}; + for (const decl of css.split(";")) { + const i = decl.indexOf(":"); + if (i < 0) continue; + const prop = decl.slice(0, i).trim(); + o[prop.startsWith("--") ? prop : kebabToCamel(prop)] = decl.slice(i + 1).trim(); + } + return o; + } + function compileAttr(raw) { + const whole = raw.match(/^\s*\{\{([\s\S]+?)\}\}\s*$/); + if (whole) { + const path = whole[1]; + return (vals) => resolve(vals, path); + } + if (raw.includes("{{")) { + const parts = raw.split(/\{\{([\s\S]+?)\}\}/g); + return (vals) => parts.map((s, i) => i & 1 ? resolve(vals, s) ?? "" : s).join(""); + } + return () => raw; + } + + // src/compile.ts + function collectProps(node, isComponent, host) { + const propGetters = []; + const pseudoClasses = []; + let hintSize = null; + for (const { name, value } of [...node.attributes]) { + if (name === "sc-name" || name === "data-dc-tpl") continue; + let key = name; + if (key.startsWith(CAMEL_ATTR)) + key = kebabToCamel(key.slice(CAMEL_ATTR.length)); + if (key === "hint-size") { + hintSize = value; + continue; + } + if (key.startsWith("style-")) { + pseudoClasses.push(host.pseudoClass(key.slice(6), value)); + continue; + } + if (isComponent) { + if (key.includes("-")) key = kebabToCamel(key); + } else { + if (key === "class") key = "className"; + else if (key === "for") key = "htmlFor"; + else if (key.startsWith("on")) + key = EVENT_MAP[key] || "on" + key[2].toUpperCase() + key.slice(3); + } + propGetters.push([key, compileAttr(value)]); + } + return { propGetters, pseudoClasses, hintSize }; + } + var HOST_STYLE_PROPS = /* @__PURE__ */ new Set([ + "position", + "left", + "right", + "top", + "bottom", + "inset", + "width", + "height", + "z-index", + "transform" + ]); + function hostPositionStyle(style) { + const all = typeof style === "string" ? cssToObj(style) : style != null && typeof style === "object" ? style : null; + if (!all) return void 0; + const out = {}; + for (const [k, v] of Object.entries(all)) { + const kebab = k.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()); + if (HOST_STYLE_PROPS.has(kebab)) out[k] = v; + } + return Object.keys(out).length ? out : void 0; + } + function compileTemplate(html, host) { + const tpl = document.createElement("template"); + //! nosemgrep: direct-inner-html-assignment + tpl.innerHTML = encodeCase(html); + let tplN = 0; + (function stamp(node) { + if (node.nodeType === Node.ELEMENT_NODE) { + node.setAttribute("data-dc-tpl", String(tplN++)); + } + for (const c of node.childNodes) stamp(c); + })(tpl.content); + const builders = walkChildren(tpl.content, host); + const render = ((vals, ctx) => builders.map((b, i) => b(vals || {}, ctx, i))); + render.__annotated = tpl.innerHTML; + return render; + } + function walkChildren(node, host) { + return [...node.childNodes].map((c) => walk(c, host)).filter((b) => b != null); + } + function walk(node, host) { + if (node.nodeType === Node.TEXT_NODE) return walkText(node); + if (node.nodeType !== Node.ELEMENT_NODE) return null; + const el = node; + const tag = el.tagName.toLowerCase(); + if (tag === "sc-for") return walkFor(el, host); + if (tag === "sc-if") return walkIf(el, host); + if (tag === "x-import") return walkXImport(el, host); + if (tag === "sc-helmet") return host.helmet(el); + if (tag === "dc-import") return walkComponent(el, host); + return walkElement(el, host); + } + var warnedHoles = /* @__PURE__ */ new Set(); + function warnUnresolved(ctx, what) { + const key = (ctx?.__name || "?") + "\0" + what; + if (warnedHoles.has(key)) return; + warnedHoles.add(key); + console.warn("[dc-runtime] " + (ctx?.__name || "template") + ": " + what); + } + function walkText(node) { + const txt = node.nodeValue ?? ""; + if (!txt.includes("{{")) { + if (!txt.trim() && !txt.includes(" ")) return null; + return () => txt; + } + const parts = txt.split(/\{\{([\s\S]+?)\}\}/g); + return (vals, ctx, key) => h( + getReact().Fragment, + { key }, + ...parts.map((p, i) => { + if (!(i & 1)) return p; + const v = resolve(vals, p); + if (v === void 0) { + if (!ctx?.__streamingNow) { + if (document.body?.hasAttribute("data-dc-editor-on")) { + return h( + "span", + { key: i, className: "sc-interp sc-unresolved" }, + "{{ " + p.trim() + " }}" + ); + } + warnUnresolved( + ctx, + "{{ " + p.trim() + " }} never resolved \u2014 rendered as empty" + ); + return null; + } + return h( + "span", + { key: i, className: "sc-interp sc-missing" }, + p.trim() + ); + } + if (getReact().isValidElement(v) || Array.isArray(v)) { + return h(getReact().Fragment, { key: i }, v); + } + if (v === null || typeof v === "boolean") return null; + return h("span", { key: i, className: "sc-interp" }, String(v)); + }) + ); + } + function walkFor(el, host) { + const listGet = compileAttr(el.getAttribute("list") || ""); + const asName = el.getAttribute("as") || "item"; + const hintN = parseInt(el.getAttribute("hint-placeholder-count") || "0", 10); + const kids = walkChildren(el, host); + const listSrc = el.getAttribute("list") || ""; + return (vals, ctx, key) => { + let list = listGet(vals); + if (!Array.isArray(list)) { + if (!ctx?.__streamingNow) { + if (list !== void 0 && list !== null) { + warnUnresolved( + ctx, + 'sc-for list="' + listSrc + '" is not an array (' + typeof list + ")" + ); + } + list = []; + } else { + list = hintN > 0 ? Array(hintN).fill(void 0) : []; + } + } + return h( + getReact().Fragment, + { key }, + list.map((item, i) => { + const sub = { ...vals, [asName]: item, $index: i }; + return h( + getReact().Fragment, + { key: i }, + kids.map((b, j) => b(sub, ctx, j)) + ); + }) + ); + }; + } + function walkIf(el, host) { + const valGet = compileAttr(el.getAttribute("value") || ""); + const hintRaw = el.getAttribute("hint-placeholder-val"); + const hintGet = hintRaw != null ? compileAttr(hintRaw) : null; + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + let v = valGet(vals); + if (v === void 0 && hintGet && ctx?.__streamingNow) v = hintGet(vals); + return v ? h( + getReact().Fragment, + { key }, + kids.map((b, j) => b(vals, ctx, j)) + ) : null; + }; + } + function walkComponent(el, host) { + const name = el.getAttribute("name") || el.getAttribute("component") || ""; + el.removeAttribute("name"); + el.removeAttribute("component"); + const tplId = el.getAttribute("data-dc-tpl"); + const styleRaw = el.getAttribute("style"); + el.removeAttribute("style"); + const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; + const { propGetters, hintSize } = collectProps(el, true, host); + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + const props = { + key, + __hintSize: hintSize, + __tplId: tplId, + __hostStyle: styleGet ? hostPositionStyle(styleGet(vals)) : void 0 + }; + for (const [k, g] of propGetters) props[k] = g(vals); + if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); + return h(host.component(name), props); + }; + } + function walkXImport(el, host) { + const globalNameGet = compileAttr( + el.getAttribute("component-from-global-scope") || "" + ); + const exportNameGet = compileAttr( + el.getAttribute("component") || el.getAttribute("name") || "" + ); + const url = el.getAttribute("from") || el.getAttribute("src") || el.getAttribute("import") || ""; + const kind = /\.(jsx|tsx)(\?|#|$)/i.test(url) ? "jsx" : "js"; + const tplId = el.getAttribute("data-dc-tpl"); + const styleRaw = el.getAttribute("style"); + el.removeAttribute("style"); + const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; + const wrap = tplId != null || styleGet != null; + const { propGetters, hintSize } = collectProps(el, true, host); + const hasContent = el.children.length > 0 || !!(el.textContent || "").trim(); + const kids = hasContent ? walkChildren(el, host) : []; + const urlBindable = url.includes("{{"); + if (url && !urlBindable) host.loadExternal(kind, url); + const evalName = (g, vals) => { + const v = g(vals); + const s = v == null ? "" : String(v); + return s.includes("{{") ? "" : s; + }; + return (vals, ctx, key) => { + const globalName = evalName(globalNameGet, vals); + const name = globalName || evalName(exportNameGet, vals); + const C = !name || urlBindable ? null : globalName ? host.resolveExternalGlobal(url, globalName) : host.resolveExternal(url, name); + const hostStyle = styleGet ? hostPositionStyle(styleGet(vals)) : void 0; + const wrapper = wrap ? { + key, + className: "sc-host-x", + "data-dc-tpl": tplId, + style: hostStyle || { display: "contents" } + } : null; + if (!C) { + const error = urlBindable ? "x-import `from` cannot contain {{ \u2026 }} \u2014 module URLs are resolved at parse time; use a literal URL" : host.resolveExternalError(url, name); + const ph = host.placeholder({ + key: wrapper ? void 0 : key, + name, + hintSize, + error + }); + return wrapper ? h("div", wrapper, ph) : ph; + } + const props = wrapper ? {} : { key }; + for (const [k, g] of propGetters) { + if (k === "component" || k === "componentFromGlobalScope" || k === "name" || k === "from" || k === "src" || k === "import") { + continue; + } + props[k] = g(vals); + } + if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); + return wrapper ? h("div", wrapper, h(C, props)) : h(C, props); + }; + } + function walkElement(el, host) { + const realTag = RAW_UNWRAP[el.localName] || el.localName; + const tplId = el.getAttribute("data-dc-tpl"); + const { propGetters, pseudoClasses } = collectProps(el, false, host); + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + const props = { key, "data-dc-tpl": tplId }; + for (const [k, g] of propGetters) { + let v = g(vals); + if (k === "style" && typeof v === "string") v = cssToObj(v); + if ((k === "value" || k === "checked") && v === void 0) { + v = k === "checked" ? false : ""; + } + props[k] = v; + } + if (pseudoClasses.length) { + props.className = [props.className, ...pseudoClasses].filter(Boolean).join(" "); + } + return h(realTag, props, ...kids.map((b, j) => b(vals, ctx, j))); + }; + } + + // src/logic.ts + var StreamableLogic = class { + constructor(props) { + __publicField(this, "props"); + __publicField(this, "state", {}); + /** Back-pointer to the wrapper component, installed after construction. */ + __publicField(this, "__host"); + this.props = props || {}; + } + setState(update, cb) { + this.__host && this.__host.__setLogicState(update, cb); + } + forceUpdate() { + this.__host && this.__host.forceUpdate(); + } + componentDidMount() { + } + componentDidUpdate(_prevProps) { + } + componentWillUnmount() { + } + /** The flat object the template renders against (merged over props). */ + renderVals() { + return {}; + } + }; + function evalDcLogic(src) { + //! nosemgrep: eval-and-function-constructor + const fn = new Function( + "DCLogic", + "StreamableLogic", + "React", + src + '\n;return (typeof Component!=="undefined"&&Component)||undefined;' + ); + return fn(StreamableLogic, StreamableLogic, getReact()); + } + + // src/component.ts + function Placeholder({ + name, + hintSize, + streaming, + error + }) { + const [w, hgt] = (hintSize || "100%,60px").split(","); + return h( + "div", + { + className: "sc-placeholder" + (streaming ? " sc-streaming" : ""), + style: { width: w.trim(), height: hgt && hgt.trim() }, + title: name + }, + error ? h( + "div", + { className: "sc-placeholder-error" }, + (name ? name + ": " : "") + error + ) : null + ); + } + function hintToMin(hint) { + if (!hint) return void 0; + const [w, hgt] = hint.split(","); + return { minWidth: w.trim(), minHeight: hgt && hgt.trim() }; + } + function createComponentFactory(registry, ensureFetched) { + const React = getReact(); + const AncestorContext = React.createContext([]); + class StreamableComponent extends React.Component { + constructor(props) { + super(props); + __publicField(this, "__name"); + __publicField(this, "__sub"); + __publicField(this, "__needsDidMount", false); + /** Snapshot of the registry's streaming flags taken at render time — + * builders read it off the RenderCtx (this) to pick placeholder vs + * render-nothing for unresolved values. */ + __publicField(this, "__streamingNow", false); + __publicField(this, "logic"); + this.__name = props.__name; + this.state = { __v: 0, __err: null }; + this.__sub = () => { + this.__reconcileLogic(); + if (this.state.__err) this.setState({ __err: null }); + this.forceUpdate(); + }; + this.__makeLogic(registry.get(this.__name).Logic, null); + ensureFetched(this.__name); + } + /** Error-boundary hook: a render crash anywhere in this DC's subtree + * (its own template, an x-import'd component, a child DC without its + * own deeper boundary) lands here instead of unmounting the page. */ + static getDerivedStateFromError(e) { + return { __err: e instanceof Error && e.message ? e.message : String(e) }; + } + componentDidCatch(e, info) { + console.error( + "[dc-runtime] render error in <" + this.__name + ">:", + e, + info?.componentStack || "" + ); + } + /** Instantiate the logic class (or the no-op base) and adopt `prevState` + * over its initial state — used both at mount and on hot-swap. */ + __makeLogic(Logic, prevState) { + const L = Logic || StreamableLogic; + try { + this.logic = new L(this.__userProps()); + } catch (e) { + console.error(e); + registry.get(this.__name).logicError = this.__name + ": " + (e instanceof Error && e.message ? e.message : String(e)); + this.logic = new StreamableLogic( + this.__userProps() + ); + } + this.logic.__host = this; + if (prevState) + this.logic.state = { ...this.logic.state || {}, ...prevState }; + } + /** The props the author's logic + template see — internal __-prefixed + * wiring stripped. */ + __userProps() { + const { __name, __hintSize, __tplId, __hostStyle, ...rest } = this.props; + return rest; + } + __setLogicState(update, cb) { + const prev = this.logic.state; + const patch = typeof update === "function" ? update(prev) : update; + this.logic.state = { ...prev, ...patch }; + this.setState((s) => ({ __v: s.__v + 1 }), cb); + } + /** Swap the logic instance when the registry's Logic class changed + * (streaming completion, hot reload). State carries over; didMount + * re-fires after the swap commits so refs exist. */ + __reconcileLogic() { + const Next = registry.get(this.__name).Logic; + const Cur = this.logic.constructor; + if (Next === Cur || !Next && Cur === StreamableLogic) + return; + try { + this.logic.componentWillUnmount(); + } catch (e) { + console.error(e); + } + this.__makeLogic(Next, this.logic.state); + this.__needsDidMount = true; + } + componentDidMount() { + registry.get(this.__name).subs.add(this.__sub); + try { + this.logic.componentDidMount(); + } catch (e) { + console.error(e); + } + } + componentDidUpdate(prevProps) { + this.logic.props = this.__userProps(); + if (this.__needsDidMount) { + this.__needsDidMount = false; + try { + this.logic.componentDidMount(); + } catch (e) { + console.error(e); + } + } else { + try { + this.logic.componentDidUpdate(prevProps); + } catch (e) { + console.error(e); + } + } + } + componentWillUnmount() { + registry.get(this.__name).subs.delete(this.__sub); + try { + this.logic.componentWillUnmount(); + } catch (e) { + console.error(e); + } + } + render() { + const r = registry.get(this.__name); + const cls = "sc-host" + (r.htmlStreaming ? " sc-streaming-html" : "") + (r.jsStreaming ? " sc-streaming-js" : ""); + const hintStyle = r.htmlStreaming ? hintToMin(this.props.__hintSize) : void 0; + const hostStyle = this.props.__hostStyle || hintStyle ? { ...hintStyle || {}, ...this.props.__hostStyle || {} } : void 0; + const hostBase = { + className: cls, + style: hostStyle, + "data-sc-name": this.__name, + "data-dc-tpl": this.props.__tplId + }; + const chain = Array.isArray(this.context) ? this.context : []; + if (chain.includes(this.__name)) { + const cycle = [ + ...chain.slice(chain.indexOf(this.__name)), + this.__name + ].join(" \u2192 "); + return h( + "div", + { ...hostBase, className: cls + " sc-has-error" }, + h(Placeholder, { + name: this.__name, + hintSize: this.props.__hintSize, + error: "circular import: " + cycle + }) + ); + } + if (this.state.__err) { + return h( + "div", + { ...hostBase, className: cls + " sc-has-error" }, + h( + "div", + { className: "sc-logic-error" }, + this.__name + ": " + this.state.__err + ), + h(Placeholder, { + name: this.__name, + hintSize: this.props.__hintSize, + error: this.state.__err + }) + ); + } + if (!r.tpl) { + return h( + "div", + hostBase, + h(Placeholder, { name: this.__name, hintSize: this.props.__hintSize }) + ); + } + const userProps = this.__userProps(); + this.logic.props = userProps; + let vals = userProps; + let renderErr = r.logicError; + try { + vals = { ...userProps, ...this.logic.renderVals() || {} }; + } catch (e) { + console.error(e); + renderErr = this.__name + ".renderVals(): " + (e instanceof Error && e.message ? e.message : String(e)); + } + this.__streamingNow = !!(r.htmlStreaming || r.jsStreaming); + return h( + "div", + { ...hostBase, className: cls + (renderErr ? " sc-has-error" : "") }, + renderErr && h("div", { className: "sc-logic-error" }, renderErr), + h( + AncestorContext.Provider, + { value: [...chain, this.__name] }, + r.tpl(vals, this) + ) + ); + } + } + __publicField(StreamableComponent, "contextType", AncestorContext); + const named = /* @__PURE__ */ new Map(); + function getDC(name) { + const hit = named.get(name); + if (hit) return hit; + function Dispatcher(p) { + const [, setTick] = React.useState(0); + React.useEffect(() => { + const sub = () => setTick((n) => n + 1); + registry.get(name).subs.add(sub); + return () => { + registry.get(name).subs.delete(sub); + }; + }, []); + ensureFetched(name); + return h(StreamableComponent, { ...p, __name: name }); + } + Dispatcher.displayName = name; + named.set(name, Dispatcher); + return Dispatcher; + } + return { + getDC, + StreamableComponent + }; + } + + // src/external.ts + var isCustomElementName = (n) => !n.includes(".") && n.includes("-"); + function isRenderableType(g) { + if (typeof g === "function") return !isElementClass(g); + return typeof g === "object" && g !== null && typeof g.$$typeof === "symbol"; + } + function resolveDottedPath(root, name) { + let cur = root; + for (const seg of name.split(".")) { + if (cur == null) return void 0; + cur = cur[seg]; + } + return cur; + } + var BABEL_URL = "https://unpkg.com/@babel/standalone@7.26.4/babel.min.js"; + var GLOBAL_POLL_INTERVAL_MS = 50; + var GLOBAL_POLL_TIMEOUT_MS = 3e4; + function createExternalModules(onResolved) { + const cache = /* @__PURE__ */ new Map(); + let babelLoading = null; + const reportedMissing = /* @__PURE__ */ new Map(); + const polling = /* @__PURE__ */ new Set(); + function ensureBabel() { + if (window.Babel) return Promise.resolve(); + if (babelLoading) return babelLoading; + babelLoading = new Promise((res, rej) => { + const s = document.createElement("script"); + s.src = BABEL_URL; + s.crossOrigin = "anonymous"; + s.onload = () => res(); + s.onerror = rej; + document.head.appendChild(s); + }); + return babelLoading; + } + function load(kind, url) { + if (cache.has(url)) return; + cache.set(url, null); + console.info("[dc-runtime] x-import: loading", url, "(" + kind + ")"); + const ready = kind === "jsx" ? ensureBabel() : Promise.resolve(); + ready.then(() => fetch(url)).then((r) => { + if (!r.ok) throw new Error("HTTP " + r.status); + return r.text(); + }).then((src) => { + const code = kind === "jsx" ? window.Babel.transform(src, { + filename: url, + presets: ["react", "typescript"] + }).code : src; + const module = { exports: {} }; + const before = new Set(Object.keys(window)); + //! nosemgrep: eval-and-function-constructor + new Function("React", "module", "exports", "require", code)( + getReact(), + module, + module.exports, + () => ({}) + ); + const globals = {}; + for (const k of Object.keys(window)) { + if (!before.has(k) && typeof window[k] === "function") { + globals[k] = window[k]; + } + } + cache.set(url, { mod: module.exports, globals }); + console.info( + "[dc-runtime] x-import: loaded", + url, + "\u2014 exports:", + Object.keys(module.exports), + "window globals:", + Object.keys(globals) + ); + onResolved(); + }).catch((e) => { + cache.set(url, { + mod: {}, + globals: {}, + error: "failed to load: " + (e instanceof Error && e.message ? e.message : String(e)) + }); + console.error( + "[dc-runtime] x-import: FAILED to load", + url, + "(" + kind + ")", + e + ); + onResolved(); + }); + } + function resolve2(url, name) { + const entry = cache.get(url); + if (!entry) return null; + const { mod, globals } = entry; + const C = mod && mod[name] || globals && globals[name] || typeof window !== "undefined" && window[name] || mod && mod.default; + if (typeof C === "function") return C; + const key = url + "\0" + name; + if (!reportedMissing.has(key)) { + reportedMissing.set( + key, + entry.error || 'no export named "' + name + '" (has: ' + Object.keys(mod).join(", ") + ")" + ); + console.error( + "[dc-runtime] x-import: module", + url, + "loaded but has no component named", + JSON.stringify(name), + "\u2014 available exports:", + Object.keys(mod), + "window globals:", + Object.keys(globals), + ". The module must `module.exports = {" + name + "}` or set `window." + name + "`." + ); + } + return null; + } + function waitForGlobal(name) { + if (polling.has(name)) return; + polling.add(name); + const started = Date.now(); + const isCE = isCustomElementName(name); + const tick = () => { + const found = isCE ? customElements.get(name) : isRenderableType(resolveDottedPath(window, name)); + if (found) { + polling.delete(name); + onResolved(); + return; + } + if (Date.now() - started >= GLOBAL_POLL_TIMEOUT_MS) { + console.warn( + "[dc-runtime] x-import: global", + JSON.stringify(name), + "never appeared on window after " + GLOBAL_POLL_TIMEOUT_MS + "ms" + ); + return; + } + setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); + }; + setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); + } + function resolveGlobal(url, name) { + const isCE = isCustomElementName(name); + if (!url) { + if (isCE) { + if (customElements.get(name)) return name; + waitForGlobal(name); + return null; + } + const g2 = resolveDottedPath(window, name); + if (isRenderableType(g2)) return g2; + waitForGlobal(name); + return null; + } + const entry = cache.get(url); + if (!entry) return null; + if (isCE && customElements.get(name)) return name; + const g = entry.globals[name] ?? resolveDottedPath(window, name); + if (isRenderableType(g)) return g; + if (name.includes(".")) return null; + const key = url + "\0global\0" + name; + if (!reportedMissing.has(key)) { + reportedMissing.set(key, null); + if (isCE && !customElements.get(name)) { + console.warn( + "[dc-runtime] x-import:", + url, + "loaded but no custom element", + JSON.stringify(name), + "is registered and window." + name + " is not a function \u2014 rendering <" + name + "> as an unknown element." + ); + } + } + return name; + } + function getError(url, name) { + const entry = cache.get(url); + if (entry?.error) return entry.error; + return reportedMissing.get(url + "\0" + name) || null; + } + return { load, resolve: resolve2, resolveGlobal, getError }; + } + function isElementClass(g) { + try { + return typeof g === "function" && typeof HTMLElement !== "undefined" && g.prototype instanceof HTMLElement; + } catch { + return false; + } + } + + // src/helmet.ts + function createHelmetManager(doc, isStreaming) { + const mounted = /* @__PURE__ */ new Set(); + const live = /* @__PURE__ */ new Map(); + function compile(node) { + const raw = [...node.children]; + const helmetClosed = node.nextSibling != null || node.parentNode?.nextSibling != null; + return (_vals, ctx) => { + const name = ctx && ctx.__name || ""; + const streaming = !!(name && isStreaming(name)); + for (let i = 0; i < raw.length; i++) { + const child = raw[i]; + const tag = child.tagName; + const mayBePartial = streaming && !helmetClosed && i === raw.length - 1; + if (tag === "SCRIPT") { + if (mayBePartial) continue; + const key = "SCRIPT|" + (child.getAttribute("src") || child.textContent || ""); + if (mounted.has(key)) continue; + mounted.add(key); + const el = doc.createElement("script"); + for (const { name: an, value } of [...child.attributes]) + el.setAttribute(an, value); + if (child.textContent) el.textContent = child.textContent; + doc.head.appendChild(el); + } else if (tag === "LINK" || tag === "META") { + if (mayBePartial) continue; + const key = tag + "|" + (child.getAttribute("href") || child.getAttribute("src") || child.outerHTML); + if (mounted.has(key)) continue; + mounted.add(key); + doc.head.appendChild(child.cloneNode(true)); + } else { + const key = name + "|" + i; + let el = live.get(key); + if (!el || el.tagName !== tag) { + if (el) el.remove(); + el = doc.createElement(tag.toLowerCase()); + live.set(key, el); + doc.head.appendChild(el); + } + for (const { name: an, value } of [...child.attributes]) { + if (el.getAttribute(an) !== value) el.setAttribute(an, value); + } + if (el.textContent !== child.textContent) + el.textContent = child.textContent; + } + } + return null; + }; + } + return { compile }; + } + + // src/pseudo.ts + function createPseudoSheet(doc) { + let el = null; + const cache = /* @__PURE__ */ new Map(); + let n = 0; + return (pseudo, css) => { + const k = pseudo + "|" + css; + const hit = cache.get(k); + if (hit) return hit; + if (!el) { + el = doc.createElement("style"); + doc.head.appendChild(el); + } + const cls = "scp" + (n++).toString(36); + const sel = pseudo === "before" || pseudo === "after" ? "." + cls + "::" + pseudo : "." + cls + ":" + pseudo; + el.sheet.insertRule(sel + "{" + css + "}", el.sheet.cssRules.length); + cache.set(k, cls); + return cls; + }; + } + + // src/registry.ts + function createRegistry() { + const entries = /* @__PURE__ */ Object.create(null); + function get(name) { + return entries[name] || (entries[name] = { + html: "", + tpl: null, + Logic: null, + jsStreaming: false, + htmlStreaming: false, + ver: 0, + subs: /* @__PURE__ */ new Set(), + fetched: false + }); + } + function bump(name) { + const r = get(name); + r.ver++; + for (const fn of r.subs) fn(); + } + return { + entries, + get, + bump, + bumpAll() { + for (const n in entries) bump(n); + } + }; + } + + // src/runtime.ts + var COMPONENT_DIR = "."; + function createRuntime(doc = document) { + const registry = createRegistry(); + const pseudoClass = createPseudoSheet(doc); + const helmet = createHelmetManager( + doc, + (name) => registry.get(name).htmlStreaming + ); + const external = createExternalModules(() => registry.bumpAll()); + const factory = createComponentFactory(registry, ensureFetched); + const host = { + component: (name) => factory.getDC(name), + placeholder: (props) => h(Placeholder, props), + helmet: (node) => helmet.compile(node), + loadExternal: (kind, url) => external.load(kind, url), + resolveExternal: (url, name) => external.resolve(url, name), + resolveExternalGlobal: (url, name) => external.resolveGlobal(url, name), + resolveExternalError: (url, name) => external.getError(url, name), + pseudoClass + }; + function ensureFetched(name) { + const r = registry.get(name); + if (r.fetched) return; + r.fetched = true; + const url = COMPONENT_DIR + "/" + name + ".dc.html"; + fetch(url).then((res) => { + if (!res.ok) { + console.error( + "[dc-runtime] sibling fetch for <" + name + "/> failed:", + url, + "returned", + res.status, + "\u2014 the reference renders as an empty placeholder." + ); + return ""; + } + return res.text(); + }).then((t) => { + if (!t) return; + const parsed = parseDcText(t); + if (!parsed) { + console.error( + "[dc-runtime] sibling fetch for <" + name + "/>:", + url, + "has no block \u2014 not a Design Component." + ); + return; + } + if (parsed.props) r.propsMeta = parsed.props; + if (parsed.preview) r.preview = parsed.preview; + if (parsed.template && !r.html) updateHtml(name, parsed.template); + if (parsed.js && !r.Logic) updateJs(name, parsed.js); + }).catch( + (e) => console.error( + "[dc-runtime] sibling fetch for <" + name + "/> threw:", + url, + e + ) + ); + } + function updateHtml(name, html) { + const r = registry.get(name); + r.html = html; + try { + r.tpl = compileTemplate(html, host); + } catch (e) { + console.error("[dc-runtime] template compile FAILED for", name, e); + } + registry.bump(name); + } + function updateJs(name, src) { + const r = registry.get(name); + const seq = r.jsSeq = (r.jsSeq || 0) + 1; + try { + const Cls = evalDcLogic(src); + if (r.jsSeq !== seq) return; + if (typeof Cls !== "function") { + r.logicError = name + ".dc.html: