Release 0.1.1

This commit is contained in:
hnzio committed 2026-10-21 16:38:56 +02:00
1 parent 9f97d8407a
commit 7a49e66c37
16 files changed
+603 -361

No files matched your search

+1 -1
View File
@@ -3,7 +3,7 @@
"title": "Sendewerk", "title": "Sendewerk",
"description": "Redaktionelle Planung und Durchführung von heinzlive-Sendungen.", "description": "Redaktionelle Planung und Durchführung von heinzlive-Sendungen.",
"tagline": "Vom Fundstück bis zur Livesendung", "tagline": "Vom Fundstück bis zur Livesendung",
"version": "0.1.0", "version": "0.1.1",
"healthCheckPath": "/healthz", "healthCheckPath": "/healthz",
"httpPort": 3000, "httpPort": 3000,
"addons": { "addons": {
+6 -3
View File
@@ -3,7 +3,7 @@
## Etappe 1 ## Etappe 1
| Kriterium | Nachweis | Stand | | Kriterium | Nachweis | Stand |
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ---------------------------- | | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| Frischer Checkout startet ohne Cloudron | `npm ci` erfolgreich; README-Setup und lokaler Produktionsstart ausgeführt | erfüllt am 21.10.2026 | | Frischer Checkout startet ohne Cloudron | `npm ci` erfolgreich; README-Setup und lokaler Produktionsstart ausgeführt | erfüllt am 21.10.2026 |
| Typprüfung und Build erfolgreich | `npm run check`, `npm run build` | erfüllt am 21.10.2026 | | Typprüfung und Build erfolgreich | `npm run check`, `npm run build` | erfüllt am 21.10.2026 |
| Theme bleibt nach Neuladen erhalten | Playwright: System auf Hell wechseln, Reload, DOM und Schalterzustand prüfen | erfüllt am 21.10.2026 | | Theme bleibt nach Neuladen erhalten | Playwright: System auf Hell wechseln, Reload, DOM und Schalterzustand prüfen | erfüllt am 21.10.2026 |
@@ -11,7 +11,8 @@
| Migration wiederholt ausführbar | lokal mehrfach ausgeführt; Verhaltenstest führt Migration zweimal in temporärer DB aus | erfüllt am 21.10.2026 | | Migration wiederholt ausführbar | lokal mehrfach ausgeführt; Verhaltenstest führt Migration zweimal in temporärer DB aus | erfüllt am 21.10.2026 |
| Seed reproduzierbar | lokal und im Verhaltenstest zweimal ausgeführt; danach 4 Pieces, 1 Sendung und 3 Verwendungen | erfüllt am 21.10.2026 | | Seed reproduzierbar | lokal und im Verhaltenstest zweimal ausgeführt; danach 4 Pieces, 1 Sendung und 3 Verwendungen | erfüllt am 21.10.2026 |
| Tests verwenden getrennte Daten | Vitest und Playwright erzeugen je eigene Datenbank im Betriebssystem-Tempdir | erfüllt am 21.10.2026 | | Tests verwenden getrennte Daten | Vitest und Playwright erzeugen je eigene Datenbank im Betriebssystem-Tempdir | erfüllt am 21.10.2026 |
| Desktop und Smartphone ohne horizontales Scrollen | 4 Playwright-Abläufe; 1440x1000, 1280x900 und 390x844; Screenshots visuell kontrolliert | erfüllt am 21.10.2026 | | Desktop und Smartphone ohne horizontales Scrollen | 8 Playwright-Abläufe bei 390, 768, 960, 1280 und 1440 Pixeln; Screenshots visuell kontrolliert | erfüllt am 21.10.2026 |
| Kompakte Arbeitsoberfläche in beiden Farbmodi | 390, 768 und 1440 Pixel jeweils in Light/Dark; einspaltige Mobilkarten, lange Titel, Metadatenumbrüche und Leerzustände | erfüllt am 21.10.2026 |
| Cloudron-Pfade und Healthcheck vorbereitet | Manifestpfad `/app/data/sendewerk.sqlite`, `/healthz`, `start.sh`; JSON-Kernfelder und Bash-Syntax geprüft | vorbereitet; Container offen | | Cloudron-Pfade und Healthcheck vorbereitet | Manifestpfad `/app/data/sendewerk.sqlite`, `/healthz`, `start.sh`; JSON-Kernfelder und Bash-Syntax geprüft | vorbereitet; Container offen |
| Echte Cloudron-Prüfung | Installation, Backup/Restore, Proxy und read-only Laufzeit | offen, kein Serverzugang | | Echte Cloudron-Prüfung | Installation, Backup/Restore, Proxy und read-only Laufzeit | offen, kein Serverzugang |
@@ -23,7 +24,9 @@
- `npm run lint`: erfolgreich - `npm run lint`: erfolgreich
- `npm run check`: 0 Fehler und 0 Warnungen - `npm run check`: 0 Fehler und 0 Warnungen
- `npm run build`: erfolgreich mit `@sveltejs/adapter-node` - `npm run build`: erfolgreich mit `@sveltejs/adapter-node`
- `npm run test:e2e`: 4 von 4 Browsertests erfolgreich - `npm run test:e2e`: 8 von 8 Browsertests erfolgreich; 390, 768 und 1440 Pixel
jeweils in Light- und Darkmode sowie Tastaturbedienung, feste Navigation,
einspaltige Funduskarten, lange Inhalte und Überläufe geprüft
- Zwei getrennte `npm start`-Läufe: Healthcheck und persistente Daten erfolgreich - Zwei getrennte `npm start`-Läufe: Healthcheck und persistente Daten erfolgreich
- `bash -n start.sh` und programmatische Manifest-Kernfeldprüfung: erfolgreich - `bash -n start.sh` und programmatische Manifest-Kernfeldprüfung: erfolgreich
+6 -2
View File
@@ -66,8 +66,12 @@ getrennt.
## Oberflächenstruktur ## Oberflächenstruktur
Die Hauptnavigation umfasst Eingang, Fundus, Sendungen, Archiv und Verwaltung. Die Hauptnavigation umfasst Eingang, Fundus, Sendungen, Archiv und Verwaltung.
Auf breiten Ansichten stehen Fundus und Ablauf nebeneinander. Auf schmalen Auf dem Desktop bleibt eine feste Seitenleiste sichtbar; Verwaltung liegt an
Ansichten werden sie über eine gut erreichbare Umschaltung einzeln gezeigt. ihrem unteren Ende. Bis einschließlich `52rem` ersetzen eine obere Leiste und
die untere Hauptnavigation die Seitenleiste vollständig. Auf breiten Ansichten
stehen Fundus und Ablauf nebeneinander. Bis einschließlich `72rem` werden sie
über eine gut erreichbare Umschaltung einzeln gezeigt, damit auch schmale
Desktopfenster ohne horizontalen Überlauf bedienbar bleiben.
Der Live-Modus ist eine eigene, fokussierte Ansicht; die spätere Der Live-Modus ist eine eigene, fokussierte Ansicht; die spätere
Präsentationsausgabe erhält eine separate, strikt reduzierte Datenansicht. Präsentationsausgabe erhält eine separate, strikt reduzierte Datenansicht.
+13
View File
@@ -81,3 +81,16 @@ Vorschauseiten lesen echte persistente SQLite-Seed-Daten, bieten aber keine
funktionslosen Speichern- oder Live-Schaltflächen. Ein sichtbarer Hinweis funktionslosen Speichern- oder Live-Schaltflächen. Ein sichtbarer Hinweis
grenzt Gestaltungsvorschauen von den späteren Fachfunktionen ab. Damit wird grenzt Gestaltungsvorschauen von den späteren Fachfunktionen ab. Damit wird
keine noch nicht implementierte Funktion als fertig dargestellt. keine noch nicht implementierte Funktion als fertig dargestellt.
## D-007: Arbeitsansicht nach verfügbarem Platz
Die feste Desktop-Seitenleiste wird bis `52rem` verwendet und dann vollständig
durch mobile Kopf- und Fußnavigation ersetzt. Der Sendungsbau schaltet bereits
bis `72rem` von zwei Spalten auf eine bedienbare Pane-Umschaltung um. Dadurch
bleibt die Desktopnavigation in schmalen Fenstern erhalten, ohne Fundus und
Ablauf zusammenzudrücken.
Der Fundus erscheint auf dem Desktop als kompakte Liste und auf kleinen
Bildschirmen als Kartenfolge. Vorschau- und Schreibschutzinformationen sind in
einem nativen, tastaturbedienbaren `details`-Element gebündelt. Diese
Darstellungsentscheidung ändert weder Datenmodell noch Funktionsumfang.
+14
View File
@@ -13,6 +13,8 @@ lokaler Betriebsweg sind dokumentiert. Etappe 2 wurde nicht begonnen.
- persistentes Light-/Dark-/System-Theme - persistentes Light-/Dark-/System-Theme
- responsive Navigation und gekennzeichnete Vorschauen für Fundus, - responsive Navigation und gekennzeichnete Vorschauen für Fundus,
Sendungsbau und Live-Modus Sendungsbau und Live-Modus
- kompakte Desktop-Arbeitsansicht mit fester Seitenleiste, Fundusliste und
zweigeteiltem Sendungsbau; auf schmalen Ansichten Umschaltung statt Engpass
- Healthcheck, Produktionsstart und frühes Cloudron-Paket - Healthcheck, Produktionsstart und frühes Cloudron-Paket
- getrennte Unit-/Integrations- und Browser-Testdaten - getrennte Unit-/Integrations- und Browser-Testdaten
@@ -20,6 +22,18 @@ Die Vorschauen lesen echte Daten aus SQLite. Suche und mobile Pane-Umschaltung
funktionieren lokal; redaktionelle Schreiboperationen bleiben bewusst den funktionieren lokal; redaktionelle Schreiboperationen bleiben bewusst den
späteren Etappen vorbehalten. späteren Etappen vorbehalten.
Die abschließende Oberflächenüberarbeitung von Etappe 1 reduziert große
Seitenköpfe und verschachtelte Karten, bündelt den Schreibschutzhinweis
aufklappbar und hält Entwicklungsinformationen aus der Arbeitsoberfläche heraus.
Eingang und Archiv zeigen kurze Leerzustände, die Verwaltung trennt
Abschnittskategorien, Formate und Themen kompakt.
Die abschließende responsive Prüfung verdichtet Seitenleiste, Navigation und
Funduszeilen weiter. Mobile Funduskarten sind intern einspaltig; lange Titel,
Beschreibungen und Metadaten bleiben ohne feste Kartenhöhe sichtbar. Suche und
Statusfilter sind gleich hoch, und die Filtergruppe wird nur als Ganzes
umgebrochen.
## Nicht Bestandteil dieser Etappe ## Nicht Bestandteil dieser Etappe
Anmeldung, Rollen, echte Inhaltsbearbeitung, Autospeichern, Sendungsplanung, Anmeldung, Rollen, echte Inhaltsbearbeitung, Autospeichern, Sendungsplanung,
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "sendewerk", "name": "sendewerk",
"version": "0.1.0", "version": "0.1.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sendewerk", "name": "sendewerk",
"version": "0.1.0", "version": "0.1.1",
"dependencies": { "dependencies": {
"better-sqlite3": "^13.0.3" "better-sqlite3": "^13.0.3"
}, },
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "sendewerk", "name": "sendewerk",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.1",
"type": "module", "type": "module",
"engines": { "engines": {
"node": ">=22.17 <25" "node": ">=22.17 <25"
+319 -239
View File
@@ -25,7 +25,7 @@
--gold: #c99530; --gold: #c99530;
--danger: #a94a43; --danger: #a94a43;
--shadow: 0 18px 45px rgba(56, 52, 44, 0.08); --shadow: 0 18px 45px rgba(56, 52, 44, 0.08);
--sidebar-width: 15rem; --sidebar-width: 14rem;
--radius-large: 1.5rem; --radius-large: 1.5rem;
--radius-medium: 1rem; --radius-medium: 1rem;
--radius-small: 0.7rem; --radius-small: 0.7rem;
@@ -124,10 +124,11 @@ a {
display: flex; display: flex;
width: var(--sidebar-width); width: var(--sidebar-width);
flex-direction: column; flex-direction: column;
padding: 1.5rem 1rem 1.1rem; padding: 1.25rem 0.85rem 1rem;
border-right: 1px solid var(--line); border-right: 1px solid var(--line);
background: color-mix(in srgb, var(--bg-elevated) 90%, transparent); background: color-mix(in srgb, var(--bg-elevated) 90%, transparent);
backdrop-filter: blur(24px) saturate(1.3); backdrop-filter: blur(24px) saturate(1.3);
overflow-y: auto;
} }
.brand { .brand {
@@ -174,15 +175,16 @@ a {
.main-navigation { .main-navigation {
display: grid; display: grid;
gap: 0.28rem; gap: 0.28rem;
margin-top: 2.25rem; margin-top: 1.5rem;
} }
.main-navigation a { .main-navigation a,
.utility-navigation a {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.82rem; gap: 0.82rem;
min-height: 2.85rem; min-height: 2.65rem;
padding: 0.65rem 0.8rem; padding: 0.45rem 0.7rem;
border-radius: 0.8rem; border-radius: 0.8rem;
color: var(--text-muted); color: var(--text-muted);
font-weight: 590; font-weight: 590;
@@ -192,12 +194,14 @@ a {
transform 140ms ease; transform 140ms ease;
} }
.main-navigation a:hover { .main-navigation a:hover,
.utility-navigation a:hover {
background: color-mix(in srgb, var(--panel-strong) 66%, transparent); background: color-mix(in srgb, var(--panel-strong) 66%, transparent);
color: var(--text); color: var(--text);
} }
.main-navigation a.active { .main-navigation a.active,
.utility-navigation a.active {
background: var(--panel-strong); background: var(--panel-strong);
box-shadow: 0 7px 20px rgba(48, 48, 43, 0.07); box-shadow: 0 7px 20px rgba(48, 48, 43, 0.07);
color: var(--accent); color: var(--accent);
@@ -209,6 +213,12 @@ a {
margin-top: auto; margin-top: auto;
} }
.utility-navigation {
display: grid;
padding-top: 0.85rem;
border-top: 1px solid var(--line);
}
.theme-toggle { .theme-toggle {
display: flex; display: flex;
width: 100%; width: 100%;
@@ -240,33 +250,6 @@ a {
line-height: 1; line-height: 1;
} }
.stage-indicator {
display: flex;
align-items: center;
gap: 0.65rem;
padding: 0.72rem;
color: var(--text-muted);
}
.stage-indicator > span {
width: 0.48rem;
height: 0.48rem;
border-radius: 50%;
background: #58a66b;
box-shadow: 0 0 0 0.25rem color-mix(in srgb, #58a66b 14%, transparent);
}
.stage-indicator div {
display: grid;
}
.stage-indicator strong {
color: var(--text);
font-size: 0.75rem;
}
.stage-indicator small {
font-size: 0.69rem;
}
.mobile-header, .mobile-header,
.mobile-navigation { .mobile-navigation {
display: none; display: none;
@@ -280,7 +263,7 @@ a {
.page-wrap { .page-wrap {
width: min(100%, 92rem); width: min(100%, 92rem);
margin: 0 auto; margin: 0 auto;
padding: 3.4rem clamp(2rem, 4vw, 4.6rem) 5rem; padding: 2rem clamp(1.5rem, 3vw, 3.25rem) 3rem;
} }
.narrow-page { .narrow-page {
@@ -288,17 +271,16 @@ a {
} }
.page-heading { .page-heading {
margin-bottom: 1.75rem; margin-bottom: 1rem;
} }
.page-heading h1, .page-heading h1 {
.live-header h1 { max-width: 24ch;
max-width: 18ch; margin: 0;
margin: 0.22rem 0 0; font-size: clamp(1.7rem, 2.4vw, 2.3rem);
font-size: clamp(2rem, 4vw, 3.65rem);
font-weight: 690; font-weight: 690;
letter-spacing: -0.055em; letter-spacing: -0.04em;
line-height: 1.02; line-height: 1.08;
} }
.eyebrow { .eyebrow {
@@ -312,76 +294,98 @@ a {
.page-intro { .page-intro {
max-width: 42rem; max-width: 42rem;
margin: 0.9rem 0 0; margin: 0.35rem 0 0;
color: var(--text-muted); color: var(--text-muted);
font-size: 1.04rem; font-size: 0.88rem;
} }
.split-heading, .split-heading,
.show-heading, .show-heading {
.live-header {
display: flex; display: flex;
align-items: flex-end; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 2rem; gap: 1.25rem;
} }
.heading-stat { .heading-stat {
display: grid; display: flex;
min-width: 6.5rem; align-items: baseline;
padding: 0.8rem 1rem; gap: 0.38rem;
border-left: 1px solid var(--line-strong); padding: 0.45rem 0.7rem;
border: 1px solid var(--line);
border-radius: 0.65rem;
background: var(--panel);
} }
.heading-stat strong { .heading-stat strong {
font-size: 1.7rem; font-size: 1rem;
line-height: 1; line-height: 1;
} }
.heading-stat span { .heading-stat span {
margin-top: 0.25rem;
color: var(--text-muted); color: var(--text-muted);
font-size: 0.78rem; font-size: 0.7rem;
} }
.preview-notice { .preview-notice {
display: flex; margin: 0 0 0.85rem;
align-items: flex-start;
gap: 0.8rem;
margin: 0 0 1.3rem;
padding: 0.85rem 1rem;
border: 1px solid color-mix(in srgb, var(--accent) 22%, var(--line)); border: 1px solid color-mix(in srgb, var(--accent) 22%, var(--line));
border-radius: var(--radius-small); border-radius: var(--radius-small);
background: color-mix(in srgb, var(--accent-soft) 45%, transparent); background: color-mix(in srgb, var(--accent-soft) 45%, transparent);
color: var(--accent-text); color: var(--accent-text);
} }
.preview-notice strong { .preview-notice summary {
font-size: 0.79rem; display: flex;
align-items: center;
gap: 0.55rem;
min-height: 2.35rem;
padding: 0.45rem 0.7rem;
cursor: pointer;
list-style: none;
} }
.preview-notice summary::-webkit-details-marker {
display: none;
}
.preview-notice strong {
font-size: 0.72rem;
}
.preview-notice p { .preview-notice p {
margin: 0.08rem 0 0; margin: 0;
padding: 0 0.7rem 0.65rem 1.8rem;
color: var(--text-muted); color: var(--text-muted);
font-size: 0.76rem; font-size: 0.72rem;
} }
.preview-dot { .preview-dot {
flex: 0 0 auto; flex: 0 0 auto;
width: 0.55rem; width: 0.48rem;
height: 0.55rem; height: 0.48rem;
margin-top: 0.38rem;
border: 2px solid var(--accent); border: 2px solid var(--accent);
border-radius: 50%; border-radius: 50%;
} }
.preview-chevron {
margin-left: auto;
color: var(--text-muted);
font-size: 0.85rem;
transition: transform 140ms ease;
}
.preview-notice[open] .preview-chevron {
transform: rotate(180deg);
}
.library-toolbar { .library-toolbar {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 1rem; gap: 1rem;
margin-bottom: 1.25rem; margin-bottom: 0.75rem;
} }
.search-field, .search-field {
.compact-search {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.6rem; gap: 0.6rem;
@@ -391,22 +395,20 @@ a {
} }
.search-field { .search-field {
width: min(100%, 28rem); width: min(100%, 24rem);
height: 2.85rem; height: 2.5rem;
padding: 0 0.9rem; padding: 0 0.9rem;
border-radius: 0.88rem; border-radius: 0.88rem;
box-shadow: 0 6px 20px rgba(48, 48, 43, 0.035); box-shadow: 0 6px 20px rgba(48, 48, 43, 0.035);
} }
.search-field span:first-child, .search-field span:first-child {
.compact-search span:first-child {
font-size: 1.35rem; font-size: 1.35rem;
line-height: 1; line-height: 1;
transform: rotate(-15deg); transform: rotate(-15deg);
} }
.search-field input, .search-field input {
.compact-search input {
width: 100%; width: 100%;
border: 0; border: 0;
outline: 0; outline: 0;
@@ -414,8 +416,7 @@ a {
color: var(--text); color: var(--text);
} }
.search-field input::placeholder, .search-field input::placeholder {
.compact-search input::placeholder {
color: var(--text-faint); color: var(--text-faint);
} }
@@ -427,6 +428,10 @@ a {
background: var(--panel-soft); background: var(--panel-soft);
} }
.filter-tabs {
height: 2.5rem;
}
.filter-tabs button, .filter-tabs button,
.mobile-pane-switch button { .mobile-pane-switch button {
padding: 0.48rem 0.82rem; padding: 0.48rem 0.82rem;
@@ -439,6 +444,12 @@ a {
font-weight: 640; font-weight: 640;
} }
.filter-tabs button {
height: 2rem;
padding-block: 0.35rem;
white-space: nowrap;
}
.filter-tabs button.active, .filter-tabs button.active,
.mobile-pane-switch button.active { .mobile-pane-switch button.active {
background: var(--panel-strong); background: var(--panel-strong);
@@ -448,33 +459,30 @@ a {
.piece-grid { .piece-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0;
gap: 1rem; overflow: hidden;
border: 1px solid var(--line);
border-radius: var(--radius-medium);
background: var(--panel);
} }
.piece-card { .piece-card {
display: flex; display: grid;
grid-template-columns: minmax(0, 1.8fr) minmax(8.5rem, 0.9fr) minmax(12rem, auto);
align-items: center;
gap: 1rem;
min-width: 0; min-width: 0;
min-height: 17rem; padding: 0.85rem 1rem;
flex-direction: column; border-bottom: 1px solid var(--line);
padding: 1.25rem;
border: 1px solid var(--line);
border-radius: var(--radius-large);
background: var(--panel);
box-shadow: 0 8px 25px rgba(55, 52, 46, 0.035);
transition: transition:
border-color 150ms ease, background-color 150ms ease,
transform 150ms ease, border-color 150ms ease;
box-shadow 150ms ease;
} }
.piece-card:hover { .piece-card:last-child {
transform: translateY(-2px); border-bottom: 0;
border-color: var(--line-strong);
box-shadow: var(--shadow);
} }
.piece-card-topline,
.source-card > div, .source-card > div,
.live-kicker { .live-kicker {
display: flex; display: flex;
@@ -517,25 +525,37 @@ a {
color: var(--accent-text); color: var(--accent-text);
} }
.piece-card h2 { .piece-primary {
margin: 1.35rem 0 0.55rem; min-width: 0;
font-size: 1.35rem;
font-weight: 670;
letter-spacing: -0.025em;
line-height: 1.18;
} }
.piece-card > p { .piece-primary h2 {
margin: 0 0 0.2rem;
font-size: 0.98rem;
font-weight: 670;
letter-spacing: -0.015em;
line-height: 1.25;
overflow-wrap: anywhere;
}
.piece-primary p {
margin: 0; margin: 0;
color: var(--text-muted); color: var(--text-muted);
font-size: 0.88rem; font-size: 0.76rem;
line-height: 1.4;
}
.piece-classification {
display: grid;
min-width: 0;
gap: 0.45rem;
} }
.piece-tags { .piece-tags {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.35rem; gap: 0.35rem;
margin-top: 1.1rem; margin: 0;
} }
.piece-tags span { .piece-tags span {
@@ -546,25 +566,30 @@ a {
font-size: 0.67rem; font-size: 0.67rem;
} }
.piece-card footer { .piece-state {
display: flex; display: flex;
justify-content: space-between; align-items: flex-end;
gap: 1rem; flex-direction: column;
margin-top: auto; gap: 0.3rem;
padding-top: 1.1rem;
border-top: 1px solid var(--line);
color: var(--text-faint); color: var(--text-faint);
font-size: 0.72rem; font-size: 0.66rem;
} }
.piece-card footer b { .piece-facts {
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.25rem 0.7rem;
}
.piece-state b {
color: var(--text); color: var(--text);
font-weight: 680; font-weight: 680;
} }
.empty-state, .empty-state {
.planned-panel { padding: 1.5rem;
padding: clamp(2rem, 7vw, 5rem);
text-align: center; text-align: center;
} }
@@ -575,13 +600,16 @@ a {
.empty-state > span { .empty-state > span {
color: var(--text-faint); color: var(--text-faint);
font-size: 2rem; font-size: 1.4rem;
} }
.empty-state h2 { .empty-state h2 {
margin-bottom: 0.3rem; margin: 0.35rem 0 0.2rem;
font-size: 1rem;
} }
.empty-state p { .empty-state p {
margin: 0 0 0.8rem;
color: var(--text-muted); color: var(--text-muted);
font-size: 0.78rem;
} }
.empty-state button { .empty-state button {
padding: 0.65rem 0.9rem; padding: 0.65rem 0.9rem;
@@ -595,14 +623,14 @@ a {
.show-summary { .show-summary {
display: flex; display: flex;
align-items: stretch; align-items: center;
gap: 0.65rem; gap: 0.5rem;
} }
.show-summary > div { .show-summary > div {
display: grid; display: grid;
min-width: 5.8rem; min-width: 5.8rem;
padding: 0.6rem 0.85rem; padding: 0.35rem 0.7rem;
border-left: 1px solid var(--line-strong); border-left: 1px solid var(--line-strong);
} }
@@ -618,7 +646,8 @@ a {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.55rem; gap: 0.55rem;
padding: 0.7rem 0.9rem; min-height: 2.45rem;
padding: 0.55rem 0.8rem;
border-radius: 0.72rem; border-radius: 0.72rem;
background: var(--text); background: var(--text);
color: var(--bg); color: var(--bg);
@@ -631,22 +660,39 @@ a {
font-size: 1rem; font-size: 1rem;
} }
.show-heading-copy {
min-width: 0;
}
.show-title {
margin: 0.3rem 0 0;
font-size: 0.9rem;
font-weight: 680;
overflow-wrap: anywhere;
}
.show-date {
margin: 0.08rem 0 0;
color: var(--text-muted);
font-size: 0.72rem;
}
.mobile-pane-switch { .mobile-pane-switch {
display: none; display: none;
} }
.builder-workspace { .builder-workspace {
display: grid; display: grid;
grid-template-columns: minmax(18rem, 0.78fr) minmax(28rem, 1.35fr); grid-template-columns: minmax(15rem, 0.72fr) minmax(24rem, 1.45fr);
gap: 1rem; gap: 0.75rem;
align-items: start; align-items: start;
} }
.builder-pane { .builder-pane {
min-width: 0; min-width: 0;
padding: 1.15rem; padding: 0.9rem;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: var(--radius-large); border-radius: var(--radius-medium);
background: var(--panel); background: var(--panel);
} }
@@ -655,7 +701,7 @@ a {
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
gap: 1rem; gap: 1rem;
margin-bottom: 1rem; margin-bottom: 0.65rem;
} }
.pane-heading h2 { .pane-heading h2 {
@@ -672,27 +718,15 @@ a {
font-weight: 680; font-weight: 680;
} }
.compact-search {
height: 2.5rem;
padding: 0 0.75rem;
border-radius: 0.72rem;
}
.compact-search:has(input:disabled) {
opacity: 0.72;
}
.source-list { .source-list {
display: grid; display: grid;
gap: 0.55rem; gap: 0;
margin-top: 0.8rem; border-top: 1px solid var(--line);
} }
.source-card { .source-card {
padding: 0.9rem; padding: 0.72rem 0.2rem;
border: 1px solid var(--line); border-bottom: 1px solid var(--line);
border-radius: 0.9rem;
background: var(--panel-strong);
} }
.source-card > div > span:last-child { .source-card > div > span:last-child {
@@ -700,9 +734,10 @@ a {
font-size: 0.68rem; font-size: 0.68rem;
} }
.source-card h3 { .source-card h3 {
margin: 0.72rem 0 0.2rem; margin: 0.42rem 0 0.12rem;
font-size: 0.88rem; font-size: 0.88rem;
line-height: 1.25; line-height: 1.25;
overflow-wrap: anywhere;
} }
.source-card p { .source-card p {
margin: 0; margin: 0;
@@ -711,8 +746,8 @@ a {
} }
.reserve-box { .reserve-box {
margin-top: 0.8rem; margin-top: 0.65rem;
padding: 0.9rem; padding: 0.7rem;
border: 1px dashed color-mix(in srgb, var(--gold) 48%, var(--line)); border: 1px dashed color-mix(in srgb, var(--gold) 48%, var(--line));
border-radius: 0.9rem; border-radius: 0.9rem;
background: color-mix(in srgb, var(--gold) 7%, transparent); background: color-mix(in srgb, var(--gold) 7%, transparent);
@@ -745,14 +780,13 @@ a {
.run-list { .run-list {
display: grid; display: grid;
gap: 0.7rem; gap: 0;
border-top: 1px solid var(--line);
} }
.run-section { .run-section {
padding: 0.95rem; padding: 0.78rem 0.2rem;
border: 1px solid var(--line); border-bottom: 1px solid var(--line);
border-radius: 1rem;
background: var(--panel-strong);
} }
.run-section > header { .run-section > header {
@@ -770,6 +804,7 @@ a {
margin: 0.13rem 0 0; margin: 0.13rem 0 0;
font-size: 0.95rem; font-size: 0.95rem;
letter-spacing: -0.012em; letter-spacing: -0.012em;
overflow-wrap: anywhere;
} }
.section-number { .section-number {
@@ -787,32 +822,26 @@ a {
.usage-list { .usage-list {
display: grid; display: grid;
gap: 0.42rem; gap: 0.42rem;
margin: 0.8rem 0 0 2.8rem; margin: 0.55rem 0 0 2.8rem;
} }
.usage-row { .usage-row {
display: grid; display: grid;
grid-template-columns: auto minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr) auto;
align-items: center; align-items: center;
gap: 0.65rem; gap: 0.65rem;
padding: 0.68rem; padding: 0.55rem 0.65rem;
border-radius: 0.72rem; border-radius: 0.72rem;
background: var(--panel-soft); background: var(--panel-soft);
} }
.drag-hint {
color: var(--text-faint);
font-size: 1.05rem;
}
.usage-row div { .usage-row div {
display: grid; display: grid;
min-width: 0; min-width: 0;
} }
.usage-row strong { .usage-row strong {
overflow: hidden;
font-size: 0.78rem; font-size: 0.78rem;
text-overflow: ellipsis; overflow-wrap: anywhere;
white-space: nowrap;
} }
.usage-row small { .usage-row small {
color: var(--text-muted); color: var(--text-muted);
@@ -830,8 +859,8 @@ a {
color: var(--accent-text); color: var(--accent-text);
} }
.section-placeholder { .section-placeholder {
margin: 0.75rem 0 0 2.8rem; margin: 0.5rem 0 0 2.8rem;
padding: 0.65rem; padding: 0.5rem 0.65rem;
border: 1px dashed var(--line-strong); border: 1px dashed var(--line-strong);
border-radius: 0.7rem; border-radius: 0.7rem;
color: var(--text-muted); color: var(--text-muted);
@@ -842,11 +871,18 @@ a {
max-width: 88rem; max-width: 88rem;
} }
.live-header { .live-header {
margin-bottom: 1.5rem; display: flex;
align-items: center;
justify-content: space-between;
gap: 1.25rem;
margin-bottom: 1rem;
} }
.live-header h1 { .live-header h1 {
max-width: 24ch; max-width: 24ch;
font-size: clamp(1.65rem, 3vw, 2.65rem); margin: 0.18rem 0 0;
font-size: clamp(1.5rem, 2.5vw, 2.1rem);
letter-spacing: -0.035em;
line-height: 1.08;
} }
.live-pulse { .live-pulse {
@@ -899,7 +935,7 @@ a {
.live-current { .live-current {
min-width: 0; min-width: 0;
padding: clamp(1.4rem, 4vw, 3rem); padding: clamp(1.25rem, 3vw, 2.25rem);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 1.8rem; border-radius: 1.8rem;
background: var(--panel); background: var(--panel);
@@ -918,8 +954,8 @@ a {
} }
.live-current h2 { .live-current h2 {
max-width: 20ch; max-width: 20ch;
margin: 1.4rem 0 2rem; margin: 1rem 0 1.4rem;
font-size: clamp(2rem, 4vw, 3.4rem); font-size: clamp(1.8rem, 3.2vw, 2.8rem);
font-weight: 670; font-weight: 670;
letter-spacing: -0.05em; letter-spacing: -0.05em;
line-height: 1.03; line-height: 1.03;
@@ -933,7 +969,7 @@ a {
.moderation-card, .moderation-card,
.transition-card { .transition-card {
margin-top: 2rem; margin-top: 1.25rem;
padding: 1rem 1.1rem; padding: 1rem 1.1rem;
border-radius: 0.9rem; border-radius: 0.9rem;
background: var(--panel-soft); background: var(--panel-soft);
@@ -1070,43 +1106,49 @@ a {
} }
.planned-panel { .planned-panel {
margin-top: 2.5rem; display: grid;
grid-template-columns: auto minmax(0, 1fr);
column-gap: 0.85rem;
align-items: center;
margin-top: 1.25rem;
padding: 1rem;
text-align: left;
} }
.planned-panel h2 { .planned-panel h2 {
margin: 0.55rem 0; margin: 0;
font-size: 1.55rem; font-size: 0.98rem;
letter-spacing: -0.03em; letter-spacing: -0.015em;
} }
.planned-panel > p:last-child { .planned-panel > p:last-child {
max-width: 40rem; grid-column: 2;
margin: 0 auto; margin: 0.18rem 0 0;
color: var(--text-muted); color: var(--text-muted);
font-size: 0.78rem;
} }
.planned-icon { .planned-icon {
display: grid; display: grid;
width: 3.2rem; grid-row: 1 / span 2;
height: 3.2rem; width: 2.35rem;
margin: 0 auto 1rem; height: 2.35rem;
margin: 0;
place-items: center; place-items: center;
border-radius: 1rem; border-radius: 0.7rem;
background: var(--accent-soft); background: var(--accent-soft);
color: var(--accent); color: var(--accent);
font-size: 1.6rem; font-size: 1.15rem;
} }
.settings-grid { .settings-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem; gap: 0.75rem;
} }
.taxonomy-panel, .taxonomy-panel {
.planned-settings { padding: 1rem;
padding: 1.3rem;
} }
.taxonomy-panel h2, .taxonomy-panel h2 {
.planned-settings h2 { margin: 0.2rem 0 0.7rem;
margin: 0.25rem 0 1.1rem; font-size: 0.95rem;
font-size: 1.08rem;
} }
.taxonomy-list { .taxonomy-list {
display: flex; display: flex;
@@ -1117,11 +1159,13 @@ a {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.4rem; gap: 0.4rem;
padding: 0.42rem 0.68rem; min-width: 0;
padding: 0.32rem 0.55rem;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 2rem; border-radius: 2rem;
color: var(--text-muted); color: var(--text-muted);
font-size: 0.75rem; font-size: 0.7rem;
overflow-wrap: anywhere;
} }
.taxonomy-list i { .taxonomy-list i {
width: 0.48rem; width: 0.48rem;
@@ -1129,27 +1173,41 @@ a {
border-radius: 50%; border-radius: 50%;
background: var(--tax-color); background: var(--tax-color);
} }
.planned-settings { .settings-footnote {
background: color-mix(in srgb, var(--panel) 72%, var(--accent-soft)); margin: 0.75rem 0 0;
}
.planned-settings p:last-child {
color: var(--text-muted); color: var(--text-muted);
font-size: 0.82rem; font-size: 0.72rem;
} }
@media (min-width: 84rem) { @media (max-width: 72rem) {
.piece-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 70rem) {
.builder-workspace {
grid-template-columns: minmax(16rem, 0.72fr) minmax(24rem, 1.2fr);
}
.page-wrap { .page-wrap {
padding-inline: 2rem; padding-inline: 2rem;
} }
.mobile-pane-switch {
display: flex;
width: 100%;
margin-bottom: 0.65rem;
}
.mobile-pane-switch button {
flex: 1;
}
.builder-workspace {
display: block;
}
.builder-pane.mobile-hidden {
display: none;
}
}
@media (max-width: 64rem) and (min-width: 52.001rem) {
.library-toolbar {
align-items: stretch;
flex-direction: column;
}
.library-toolbar .filter-tabs {
align-self: flex-start;
}
} }
@media (max-width: 52rem) { @media (max-width: 52rem) {
@@ -1211,6 +1269,7 @@ a {
font-weight: 620; font-weight: 620;
} }
.mobile-navigation a.active { .mobile-navigation a.active {
background: var(--accent-soft);
color: var(--accent); color: var(--accent);
} }
.mobile-navigation a svg { .mobile-navigation a svg {
@@ -1219,24 +1278,20 @@ a {
} }
#main-content { #main-content {
margin-left: 0; margin-left: 0;
padding: var(--mobile-header) 0 var(--mobile-nav); padding: var(--mobile-header) 0 calc(var(--mobile-nav) + env(safe-area-inset-bottom) + 1rem);
} }
.page-wrap { .page-wrap {
width: 100%; width: 100%;
padding: 1.6rem 1rem 3rem; padding: 1.25rem 1rem 2rem;
} }
.page-heading h1 { .page-heading h1 {
font-size: clamp(2rem, 10vw, 3rem); font-size: 1.75rem;
} }
.split-heading,
.show-heading, .show-heading,
.live-header { .live-header {
align-items: flex-start; align-items: flex-start;
flex-direction: column; flex-direction: column;
gap: 1.15rem; gap: 0.75rem;
}
.heading-stat {
display: none;
} }
.library-toolbar { .library-toolbar {
align-items: stretch; align-items: stretch;
@@ -1253,10 +1308,50 @@ a {
flex: 1 0 auto; flex: 1 0 auto;
} }
.piece-grid { .piece-grid {
grid-template-columns: 1fr; gap: 0.7rem;
overflow: visible;
border: 0;
border-radius: 0;
background: transparent;
} }
.piece-card { .piece-card {
min-height: 15.5rem; display: flex;
align-items: stretch;
flex-direction: column;
gap: 0.7rem;
grid-template-columns: none;
padding: 1rem;
border: 1px solid var(--line);
border-radius: var(--radius-medium);
background: var(--panel);
}
.piece-card:last-child {
border-bottom: 1px solid var(--line);
}
.piece-classification {
gap: 0.55rem;
}
.piece-primary,
.piece-classification,
.piece-state {
min-width: 0;
width: 100%;
}
.piece-primary h2 {
font-size: 1.08rem;
}
.piece-state {
display: flex;
align-items: center;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
gap: 0.5rem 0.75rem;
padding-top: 0.7rem;
border-top: 1px solid var(--line);
}
.piece-facts {
justify-content: flex-start;
} }
.show-summary { .show-summary {
width: 100%; width: 100%;
@@ -1269,20 +1364,6 @@ a {
flex: 1 0 100%; flex: 1 0 100%;
justify-content: center; justify-content: center;
} }
.mobile-pane-switch {
display: flex;
width: 100%;
margin-bottom: 0.8rem;
}
.mobile-pane-switch button {
flex: 1;
}
.builder-workspace {
display: block;
}
.builder-pane.mobile-hidden {
display: none;
}
.builder-pane { .builder-pane {
padding: 0.85rem; padding: 0.85rem;
} }
@@ -1293,9 +1374,6 @@ a {
.usage-row { .usage-row {
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr) auto;
} }
.drag-hint {
display: none;
}
.live-header-actions { .live-header-actions {
width: 100%; width: 100%;
justify-content: space-between; justify-content: space-between;
@@ -1310,6 +1388,11 @@ a {
.live-current h2 { .live-current h2 {
margin: 1.1rem 0 1.5rem; margin: 1.1rem 0 1.5rem;
} }
.live-kicker {
align-items: flex-start;
flex-direction: column;
gap: 0.2rem;
}
.live-rail { .live-rail {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -1322,10 +1405,7 @@ a {
.page-wrap { .page-wrap {
padding-inline: 0.75rem; padding-inline: 0.75rem;
} }
.preview-notice { .piece-state {
padding: 0.75rem;
}
.piece-card footer {
align-items: flex-start; align-items: flex-start;
flex-direction: column; flex-direction: column;
gap: 0.25rem; gap: 0.25rem;
+7 -6
View File
@@ -2,12 +2,13 @@
let { area }: { area: string } = $props(); let { area }: { area: string } = $props();
</script> </script>
<aside class="preview-notice" aria-label="Hinweis zur Gestaltungsvorschau"> <details class="preview-notice">
<summary>
<span class="preview-dot" aria-hidden="true"></span> <span class="preview-dot" aria-hidden="true"></span>
<div> <strong>Vorschau · {area}</strong>
<strong>Gestaltungsvorschau: {area}</strong> <span class="preview-chevron" aria-hidden="true">⌄</span>
</summary>
<p> <p>
In Etappe 1 schreibgeschützt. Die gezeigten Inhalte stammen aus der lokalen SQLite-Datenbank. Diese Ansicht ist schreibgeschützt. Bearbeiten und Speichern folgen in einer späteren Etappe.
</p> </p>
</div> </details>
</aside>
+16 -9
View File
@@ -6,13 +6,14 @@
import ThemeToggle from '#lib/components/ThemeToggle.svelte'; import ThemeToggle from '#lib/components/ThemeToggle.svelte';
let { children } = $props(); let { children } = $props();
const navigation = [ const primaryNavigation = [
{ href: '/eingang', label: 'Eingang', icon: 'inbox' }, { href: '/eingang', label: 'Eingang', icon: 'inbox' },
{ href: '/fundus', label: 'Fundus', icon: 'library' }, { href: '/fundus', label: 'Fundus', icon: 'library' },
{ href: '/sendungen', label: 'Sendungen', icon: 'shows' }, { href: '/sendungen', label: 'Sendungen', icon: 'shows' },
{ href: '/archiv', label: 'Archiv', icon: 'archive' }, { href: '/archiv', label: 'Archiv', icon: 'archive' }
{ href: '/verwaltung', label: 'Verwaltung', icon: 'settings' }
] as const; ] as const;
const administration = { href: '/verwaltung', label: 'Verwaltung', icon: 'settings' } as const;
const mobileNavigation = [...primaryNavigation, administration];
function isActive(href: string) { function isActive(href: string) {
return ( return (
@@ -36,7 +37,7 @@
</a> </a>
<nav class="main-navigation" aria-label="Hauptnavigation"> <nav class="main-navigation" aria-label="Hauptnavigation">
{#each navigation as item (item.href)} {#each primaryNavigation as item (item.href)}
<a <a
href={resolve(item.href)} href={resolve(item.href)}
class:active={isActive(item.href)} class:active={isActive(item.href)}
@@ -49,11 +50,17 @@
</nav> </nav>
<div class="sidebar-footer"> <div class="sidebar-footer">
<nav class="utility-navigation" aria-label="Verwaltung">
<a
href={resolve(administration.href)}
class:active={isActive(administration.href)}
aria-current={isActive(administration.href) ? 'page' : undefined}
>
<NavIcon name={administration.icon} />
<span>{administration.label}</span>
</a>
</nav>
<ThemeToggle /> <ThemeToggle />
<div class="stage-indicator">
<span></span>
<div><strong>Etappe 1</strong><small>Lokaler Entwurf</small></div>
</div>
</div> </div>
</aside> </aside>
@@ -68,7 +75,7 @@
<main id="main-content">{@render children()}</main> <main id="main-content">{@render children()}</main>
<nav class="mobile-navigation" aria-label="Hauptnavigation"> <nav class="mobile-navigation" aria-label="Hauptnavigation">
{#each navigation as item (item.href)} {#each mobileNavigation as item (item.href)}
<a <a
href={resolve(item.href)} href={resolve(item.href)}
class:active={isActive(item.href)} class:active={isActive(item.href)}
+3 -10
View File
@@ -2,19 +2,12 @@
<div class="page-wrap narrow-page"> <div class="page-wrap narrow-page">
<header class="page-heading"> <header class="page-heading">
<p class="eyebrow">Bewahrte Sendungen</p>
<h1>Archiv</h1> <h1>Archiv</h1>
<p class="page-intro"> <p class="page-intro">Abgeschlossene Sendungen in ihrem damaligen Stand.</p>
Abgeschlossene Abläufe bleiben hier in ihrem damaligen Stand nachvollziehbar.
</p>
</header> </header>
<section class="quiet-panel planned-panel"> <section class="quiet-panel planned-panel">
<div class="planned-icon" aria-hidden="true">□</div> <div class="planned-icon" aria-hidden="true">□</div>
<p class="eyebrow">Geplant für Etappe 6</p> <h2>Noch keine Sendung archiviert</h2>
<h2>Historie braucht klare Regeln.</h2> <p>Abgeschlossene Sendungen werden hier später dauerhaft auffindbar.</p>
<p>
Stabile Snapshots, Verwendungshistorie und bewusste Archivierung werden gemeinsam
implementiert, statt jetzt nur eine leere Liste vorzutäuschen.
</p>
</section> </section>
</div> </div>
+3 -11
View File
@@ -2,20 +2,12 @@
<div class="page-wrap narrow-page"> <div class="page-wrap narrow-page">
<header class="page-heading"> <header class="page-heading">
<p class="eyebrow">Sammelstelle</p>
<h1>Eingang</h1> <h1>Eingang</h1>
<p class="page-intro"> <p class="page-intro">Neue Gedanken und Fundstücke vor der Übernahme in den Fundus.</p>
Neue Gedanken und spätere RSS-Fundstücke landen hier, bevor sie in den Fundus übernommen
werden.
</p>
</header> </header>
<section class="quiet-panel planned-panel"> <section class="quiet-panel planned-panel">
<div class="planned-icon" aria-hidden="true">+</div> <div class="planned-icon" aria-hidden="true">+</div>
<p class="eyebrow">Geplant für Etappe 3 und 7</p> <h2>Keine neuen Inhalte</h2>
<h2>Noch ist der Tresen leer.</h2> <p>Hier erscheinen später neue Gedanken und ausgewählte Feed-Fundstücke.</p>
<p>
Schnellaufnahme, Feed-Vorschau und bewusste Übernahme werden erst mit den dazugehörigen
Speicher- und Sicherheitsregeln umgesetzt.
</p>
</section> </section>
</div> </div>
+14 -13
View File
@@ -27,11 +27,8 @@
<div class="page-wrap"> <div class="page-wrap">
<header class="page-heading split-heading"> <header class="page-heading split-heading">
<div> <div>
<p class="eyebrow">Redaktioneller Fundus</p> <h1>Fundus</h1>
<h1>Gute Stücke, sofort griffbereit.</h1> <p class="page-intro">Vorbereitete Inhalte für kommende Sendungen.</p>
<p class="page-intro">
Vorbereitete Ideen, Gespräche und Fundstücke für die nächste Sendung.
</p>
</div> </div>
<div class="heading-stat" aria-label={`${data.pieces.length} Inhalte im Beispieldatensatz`}> <div class="heading-stat" aria-label={`${data.pieces.length} Inhalte im Beispieldatensatz`}>
<strong>{data.pieces.length}</strong><span>Inhalte</span> <strong>{data.pieces.length}</strong><span>Inhalte</span>
@@ -65,22 +62,26 @@
<div class="piece-grid"> <div class="piece-grid">
{#each filteredPieces as piece (piece.id)} {#each filteredPieces as piece (piece.id)}
<article class="piece-card" style={`--category: ${piece.categoryColor}`}> <article class="piece-card" style={`--category: ${piece.categoryColor}`}>
<div class="piece-card-topline"> <div class="piece-primary">
<span class="category-label"><i></i>{piece.category}</span>
<span class:ready={piece.status === 'ready'} class="status-label"
>{statusLabel[piece.status]}</span
>
</div>
<h2>{piece.title}</h2> <h2>{piece.title}</h2>
<p>{piece.summary}</p> <p>{piece.summary}</p>
</div>
<div class="piece-classification">
<span class="category-label"><i></i>{piece.category}</span>
<div class="piece-tags"> <div class="piece-tags">
<span>{piece.format}</span> <span>{piece.format}</span>
{#each piece.topics as topic (topic)}<span>{topic}</span>{/each} {#each piece.topics as topic (topic)}<span>{topic}</span>{/each}
</div> </div>
<footer> </div>
<div class="piece-state">
<span class:ready={piece.status === 'ready'} class="status-label"
>{statusLabel[piece.status]}</span
>
<span class="piece-facts">
<span><b>{formatClock(piece.durationSeconds)}</b> geplant</span> <span><b>{formatClock(piece.durationSeconds)}</b> geplant</span>
<span>{piece.sourceCount} {piece.sourceCount === 1 ? 'Quelle' : 'Quellen'}</span> <span>{piece.sourceCount} {piece.sourceCount === 1 ? 'Quelle' : 'Quellen'}</span>
</footer> </span>
</div>
</article> </article>
{/each} {/each}
</div> </div>
+6 -17
View File
@@ -18,9 +18,10 @@
<div class="page-wrap builder-page"> <div class="page-wrap builder-page">
<header class="page-heading show-heading"> <header class="page-heading show-heading">
<div> <div class="show-heading-copy">
<p class="eyebrow">Nächste Sendung · {data.show.startsAtBerlin} Uhr</p> <h1>Sendungen</h1>
<h1>{data.show.title}</h1> <p class="show-title">{data.show.title}</p>
<p class="show-date">{data.show.startsAtBerlin} Uhr</p>
</div> </div>
<div class="show-summary"> <div class="show-summary">
<div><strong>{plannedCount}</strong><span>Stücke</span></div> <div><strong>{plannedCount}</strong><span>Stücke</span></div>
@@ -57,15 +58,6 @@
</div> </div>
<span>{data.pieces.filter((piece) => piece.status === 'ready').length}</span> <span>{data.pieces.filter((piece) => piece.status === 'ready').length}</span>
</header> </header>
<label class="compact-search">
<span aria-hidden="true">⌕</span><span class="sr-only">Fundus durchsuchen</span>
<input
type="search"
placeholder="Fundus durchsuchen"
disabled
title="Die vollständige Suche folgt in Etappe 3"
/>
</label>
<div class="source-list"> <div class="source-list">
{#each data.pieces.filter((piece) => piece.status === 'ready') as piece (piece.id)} {#each data.pieces.filter((piece) => piece.status === 'ready') as piece (piece.id)}
<article class="source-card" style={`--category: ${piece.categoryColor}`}> <article class="source-card" style={`--category: ${piece.categoryColor}`}>
@@ -116,10 +108,9 @@
<div class="usage-list"> <div class="usage-list">
{#each section.usages.filter((usage) => !usage.isReserve) as usage (usage.id)} {#each section.usages.filter((usage) => !usage.isReserve) as usage (usage.id)}
<div class="usage-row"> <div class="usage-row">
<span class="drag-hint" aria-hidden="true">⠿</span>
<div> <div>
<strong>{usage.title}</strong><small <strong>{usage.title}</strong><small
>{formatClock(usage.durationSeconds)} · Eigene Sendungskopie</small >{formatClock(usage.durationSeconds)} · Stand für diese Sendung</small
> >
</div> </div>
<span class:current={usage.status === 'current'} class="usage-status" <span class:current={usage.status === 'current'} class="usage-status"
@@ -129,9 +120,7 @@
{/each} {/each}
</div> </div>
{:else} {:else}
<div class="section-placeholder"> <div class="section-placeholder">Spieleblock ohne vorbereiteten Fundusinhalt</div>
Eigenständiger Spieleblock · keine Piece-Kopie nötig
</div>
{/if} {/if}
</article> </article>
{/each} {/each}
+2 -13
View File
@@ -7,12 +7,8 @@
<div class="page-wrap narrow-page"> <div class="page-wrap narrow-page">
<header class="page-heading"> <header class="page-heading">
<p class="eyebrow">System & Struktur</p>
<h1>Verwaltung</h1> <h1>Verwaltung</h1>
<p class="page-intro"> <p class="page-intro">Abschnittskategorien, Formate und Themen im Überblick.</p>
Die bereits angelegte Taxonomie ist sichtbar; Bearbeitung und Benutzerverwaltung folgen mit
serverseitigen Rechten.
</p>
</header> </header>
<div class="settings-grid"> <div class="settings-grid">
<section class="quiet-panel taxonomy-panel"> <section class="quiet-panel taxonomy-panel">
@@ -38,13 +34,6 @@
{#each data.taxonomy.topics as topic (topic.id)}<span>{topic.name}</span>{/each} {#each data.taxonomy.topics as topic (topic.id)}<span>{topic.name}</span>{/each}
</div> </div>
</section> </section>
<section class="quiet-panel planned-settings">
<p class="eyebrow">Etappe 2</p>
<h2>Konten & Rollen</h2>
<p>
Barkeeper, Thekencrew und technische Zugänge werden erst mit vollständiger Authentifizierung
verwaltbar.
</p>
</section>
</div> </div>
<p class="settings-footnote">Konten und Rollen sind noch nicht verfügbar.</p>
</div> </div>
+190 -34
View File
@@ -1,77 +1,232 @@
import { expect, test } from '@playwright/test'; import { expect, test, type Page } from '@playwright/test';
test('desktop fundus is usable, filterable and keeps its theme', async ({ page }, testInfo) => { const routes = ['/fundus', '/sendungen', '/eingang', '/archiv', '/verwaltung', '/live'];
const viewports = [
{ width: 390, height: 844, theme: 'light' },
{ width: 390, height: 844, theme: 'dark' },
{ width: 768, height: 900, theme: 'light' },
{ width: 768, height: 900, theme: 'dark' },
{ width: 1440, height: 1000, theme: 'dark' },
{ width: 1440, height: 1000, theme: 'light' }
] as const;
async function expectNoHorizontalOverflow(page: Page) {
const dimensions = await page.evaluate(() => ({
scrollWidth: document.documentElement.scrollWidth,
clientWidth: document.documentElement.clientWidth
}));
expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth);
}
async function setTheme(page: Page, theme: 'light' | 'dark') {
await page.evaluate((value) => localStorage.setItem('sendewerk-theme', value), theme);
}
test('desktop fundus is a compact, filterable list with persistent themes', async ({
page
}, testInfo) => {
await page.setViewportSize({ width: 1440, height: 1000 }); await page.setViewportSize({ width: 1440, height: 1000 });
await page.goto('/fundus'); await page.goto('/fundus');
await expect( await expect(page.getByRole('heading', { name: 'Fundus', exact: true })).toBeVisible();
page.getByRole('heading', { name: 'Gute Stücke, sofort griffbereit.' }) await expect(page.locator('.sidebar')).toBeVisible();
).toBeVisible(); await expect(page.locator('.mobile-navigation')).toBeHidden();
await expect(page.getByText('Gestaltungsvorschau: Fundus')).toBeVisible(); await expect(page.locator('.utility-navigation a')).toHaveText('Verwaltung');
await expect(page.locator('.piece-card')).toHaveCount(4); await expect(page.locator('.piece-card')).toHaveCount(4);
const firstRow = await page.locator('.piece-card').nth(0).boundingBox();
const secondRow = await page.locator('.piece-card').nth(1).boundingBox();
expect(firstRow?.x).toBe(secondRow?.x);
expect(firstRow?.y).toBeLessThan(secondRow?.y ?? 0);
expect(firstRow?.height ?? 999).toBeLessThan(130);
const searchBox = await page.locator('.search-field').boundingBox();
const filtersBox = await page.locator('.filter-tabs').boundingBox();
expect(searchBox?.height).toBe(filtersBox?.height);
await expect(page.getByRole('button', { name: 'In Arbeit' })).toHaveCSS('white-space', 'nowrap');
const notice = page.locator('.preview-notice');
await expect(page.getByText('Vorschau · Fundus', { exact: true })).toBeVisible();
await expect(notice.getByText('Diese Ansicht ist schreibgeschützt.')).toBeHidden();
await notice.locator('summary').focus();
await expect(notice.locator('summary')).toBeFocused();
await page.keyboard.press('Enter');
await expect(notice.getByText('Diese Ansicht ist schreibgeschützt.')).toBeVisible();
await page.getByRole('searchbox', { name: 'Fundus durchsuchen' }).fill('Pixelart'); await page.getByRole('searchbox', { name: 'Fundus durchsuchen' }).fill('Pixelart');
await expect(page.locator('.piece-card')).toHaveCount(1); await expect(page.locator('.piece-card')).toHaveCount(1);
await expect( await page.getByRole('button', { name: 'In Arbeit' }).click();
page.getByRole('heading', { name: 'Pixelart, die nicht nostalgisch sein will' }) await expect(page.getByRole('heading', { name: 'Kein passendes Stück' })).toBeVisible();
).toBeVisible(); await page.getByRole('button', { name: 'Filter zurücksetzen' }).click();
await expect(page.locator('.piece-card')).toHaveCount(4);
await page.getByRole('button', { name: 'Darstellung: System' }).click(); await page.getByRole('button', { name: 'Darstellung: System' }).click();
await expect(page.locator('html')).toHaveAttribute('data-theme-preference', 'light'); await expect(page.locator('html')).toHaveAttribute('data-theme-preference', 'light');
await page.reload(); await page.reload();
await expect(page.locator('html')).toHaveAttribute('data-theme-preference', 'light'); await expect(page.locator('html')).toHaveAttribute('data-theme-preference', 'light');
await expect(page.getByRole('button', { name: 'Darstellung: Hell' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Darstellung: Hell' })).toBeVisible();
await expectNoHorizontalOverflow(page);
const dimensions = await page.evaluate(() => ({ await page.screenshot({ path: testInfo.outputPath('fundus-desktop-light.png'), fullPage: true });
scrollWidth: document.documentElement.scrollWidth,
clientWidth: document.documentElement.clientWidth
}));
expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth);
await page.screenshot({ path: testInfo.outputPath('fundus-desktop.png'), fullPage: true });
}); });
test('desktop builder shows fundus and run side by side', async ({ page }) => { test('desktop builder keeps the fundus beside the wider run sheet', async ({ page }, testInfo) => {
await page.setViewportSize({ width: 1440, height: 1000 }); await page.setViewportSize({ width: 1280, height: 900 });
await page.goto('/fundus');
await setTheme(page, 'dark');
await page.goto('/sendungen'); await page.goto('/sendungen');
await expect(page.getByText('Gestaltungsvorschau: Sendungsbau')).toBeVisible(); await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
await expect(page.getByRole('heading', { name: 'Sendungen', exact: true })).toBeVisible();
await expect(page.getByText('Herbstsendung: Kleine Welten', { exact: true })).toBeVisible();
await expect(page.locator('.source-pane')).toBeVisible(); await expect(page.locator('.source-pane')).toBeVisible();
await expect(page.locator('.run-pane')).toBeVisible(); await expect(page.locator('.run-pane')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Herbstsendung: Kleine Welten' })).toBeVisible(); await expect(page.locator('.mobile-pane-switch')).toBeHidden();
await expect( await expect(
page.locator('.show-summary strong').filter({ hasText: '1 Std. 25 Min.' }) page.locator('.show-summary strong').filter({ hasText: '1 Std. 25 Min.' })
).toBeVisible(); ).toBeVisible();
const boxes = await Promise.all([ const sourceBox = await page.locator('.source-pane').boundingBox();
page.locator('.source-pane').boundingBox(), const runBox = await page.locator('.run-pane').boundingBox();
page.locator('.run-pane').boundingBox() expect(sourceBox?.x).toBeLessThan(runBox?.x ?? 0);
]); expect(sourceBox?.width ?? 9999).toBeLessThan(runBox?.width ?? 0);
expect(boxes[0]?.x).toBeLessThan(boxes[1]?.x ?? 0); await expectNoHorizontalOverflow(page);
await page.screenshot({
path: testInfo.outputPath('sendungen-desktop-dark.png'),
fullPage: true
});
}); });
test('mobile builder switches panes without horizontal scrolling', async ({ page }, testInfo) => { test('narrow desktop retains the sidebar and switches the builder panes', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 }); await page.setViewportSize({ width: 960, height: 850 });
await page.goto('/sendungen'); await page.goto('/sendungen');
await expect(page.locator('.sidebar')).toBeVisible();
await expect(page.locator('.mobile-navigation')).toBeHidden();
await expect(page.locator('.mobile-pane-switch')).toBeVisible();
await expect(page.locator('.run-pane')).toBeVisible(); await expect(page.locator('.run-pane')).toBeVisible();
await expect(page.locator('.source-pane')).toBeHidden(); await expect(page.locator('.source-pane')).toBeHidden();
await page.getByRole('button', { name: 'Fundus', exact: true }).click(); await page.getByRole('button', { name: 'Fundus', exact: true }).click();
await expect(page.locator('.source-pane')).toBeVisible(); await expect(page.locator('.source-pane')).toBeVisible();
await expect(page.locator('.run-pane')).toBeHidden(); await expect(page.locator('.run-pane')).toBeHidden();
await expectNoHorizontalOverflow(page);
});
const dimensions = await page.evaluate(() => ({ test('mobile and tablet navigation do not obscure switchable content', async ({
scrollWidth: document.documentElement.scrollWidth, page
clientWidth: document.documentElement.clientWidth }, testInfo) => {
})); for (const width of [390, 768]) {
expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth); await page.setViewportSize({ width, height: 900 });
await page.goto('/sendungen');
await expect(page.locator('.sidebar')).toBeHidden();
await expect(page.locator('.mobile-navigation')).toBeVisible();
await expect(page.locator('.run-pane')).toBeVisible();
await page.getByRole('button', { name: 'Fundus', exact: true }).click();
await expect(page.locator('.source-pane')).toBeVisible();
await expect(page.locator('.run-pane')).toBeHidden();
await expectNoHorizontalOverflow(page);
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
const verticalSpace = await page.evaluate(() => {
const navigation = document.querySelector('.mobile-navigation')?.getBoundingClientRect();
const content = document.querySelector('.source-pane')?.getBoundingClientRect();
return { contentBottom: content?.bottom ?? 0, navigationTop: navigation?.top ?? 0 };
});
expect(verticalSpace.contentBottom).toBeLessThanOrEqual(verticalSpace.navigationTop + 1);
}
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/sendungen');
await page.screenshot({ path: testInfo.outputPath('sendungen-mobile.png'), fullPage: true }); await page.screenshot({ path: testInfo.outputPath('sendungen-mobile.png'), fullPage: true });
}); });
test('live preview exposes readable prompts and working text controls', async ({ page }) => { test('all work areas fit at required widths in light and dark mode', async ({ page }, testInfo) => {
for (const viewport of viewports) {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await page.goto('/fundus');
await setTheme(page, viewport.theme);
for (const route of routes) {
await page.goto(route);
await expect(page.locator('html')).toHaveAttribute('data-theme', viewport.theme);
await expectNoHorizontalOverflow(page);
}
}
await page.setViewportSize({ width: 768, height: 900 });
await page.goto('/fundus');
await setTheme(page, 'dark');
await page.goto('/verwaltung');
await page.screenshot({
path: testInfo.outputPath('verwaltung-tablet-dark.png'),
fullPage: true
});
});
test('long titles and longer fundus lists reflow without overflow', async ({ page }, testInfo) => {
await page.setViewportSize({ width: 1280, height: 900 });
await page.goto('/fundus');
await expect(page.locator('.piece-card')).toHaveCount(4);
await page.evaluate(() => {
const list = document.querySelector('.piece-grid');
const rows = Array.from(document.querySelectorAll('.piece-card'));
const title = rows[0]?.querySelector('h2');
if (title) {
title.textContent =
'Ein außergewöhnlich langer redaktioneller Titel über kleine Konsolen, große Ideen und unerwartete Wendungen';
}
for (let repetition = 0; repetition < 3; repetition += 1) {
for (const row of rows) list?.append(row.cloneNode(true));
}
});
await expect(page.locator('.piece-card')).toHaveCount(16);
await expectNoHorizontalOverflow(page);
await page.setViewportSize({ width: 390, height: 844 });
await expectNoHorizontalOverflow(page);
await expect(page.locator('.piece-card').first()).toHaveCSS('flex-direction', 'column');
const mobileCard = await page.locator('.piece-card').first().boundingBox();
const mobilePrimary = await page.locator('.piece-primary').first().boundingBox();
expect(mobilePrimary?.width ?? 0).toBeGreaterThan((mobileCard?.width ?? 0) - 40);
await expect(page.locator('.piece-card').first().getByRole('heading')).toContainText(
'Ein außergewöhnlich langer redaktioneller Titel'
);
await page.screenshot({
path: testInfo.outputPath('fundus-mobile-long-content.png'),
fullPage: true
});
});
test('empty states and administration stay concise', async ({ page }) => {
for (const width of [390, 1280]) {
await page.setViewportSize({ width, height: 900 });
for (const route of ['/eingang', '/archiv']) {
await page.goto(route);
const emptyState = page.locator('.planned-panel');
await expect(emptyState).toBeVisible();
const box = await emptyState.boundingBox();
expect(box?.height ?? 999).toBeLessThan(150);
await expectNoHorizontalOverflow(page);
}
}
await page.setViewportSize({ width: 1280, height: 900 });
await page.goto('/verwaltung');
const panels = page.locator('.taxonomy-panel');
await expect(panels).toHaveCount(3);
const boxes = await Promise.all([
panels.nth(0).boundingBox(),
panels.nth(1).boundingBox(),
panels.nth(2).boundingBox()
]);
expect(boxes[0]?.y).toBe(boxes[1]?.y);
expect(boxes[1]?.y).toBe(boxes[2]?.y);
});
test('live preview retains its existing readable controls', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 900 }); await page.setViewportSize({ width: 1280, height: 900 });
await page.goto('/live'); await page.goto('/live');
await expect(page.getByText('Gestaltungsvorschau: Live-Modus')).toBeVisible(); await expect(page.getByText('Vorschau · Live-Modus', { exact: true })).toBeVisible();
await expect(page.getByText('Eine Kurbel, ein gelber Knopf und sehr viel Charme')).toBeVisible(); await expect(page.getByText('Eine Kurbel, ein gelber Knopf und sehr viel Charme')).toBeVisible();
const before = await page const before = await page
.locator('.live-current') .locator('.live-current')
@@ -83,4 +238,5 @@ test('live preview exposes readable prompts and working text controls', async ({
expect(after).not.toBe(before); expect(after).not.toBe(before);
await page.getByText('Recherche öffnen').click(); await page.getByText('Recherche öffnen').click();
await expect(page.locator('.research-details')).toHaveAttribute('open', ''); await expect(page.locator('.research-details')).toHaveAttribute('open', '');
await expectNoHorizontalOverflow(page);
}); });