Files
paliad/frontend/src/views.tsx
mAi 06c826a818 feat(t-paliad-211): mount filter-bar on Custom Views runner
The /views/{slug} runner now mounts the same FilterBar primitive that
/events and /inbox use. The saved view's filter_spec becomes the bar's
baseline, axes are picked client-side per the view's data sources so a
deadline-only view exposes deadline_status, an approval-driven view
exposes approval_viewer_role + approval_status + approval_entity_type,
etc. Universal axes (time, personal_only, sort) always render.

Per-session tweaks overlay the saved baseline without mutating the
stored row; the URL round-trips state through the bar's existing codec
so deep-links share the active narrow. "Speichern als Sicht" stays
available on user-owned views so a tweaked narrow can be forked into a
new saved view.

Shape axis is intentionally excluded from the bar — the existing
top-of-page shape chip cluster (list / cards / calendar / timeline)
already plays that role and switching now mutates the cached render
spec without re-hitting the substrate.

Empty-state hint reuses the saved filter summary as before; the bar's
onResult handler hides all shape hosts when the rows array is empty.
2026-05-18 17:45:30 +02:00

134 lines
7.1 KiB
TypeScript

import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { PaliadinWidget } from "./components/PaliadinWidget";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
import { PWAHead } from "./components/PWAHead";
// Custom Views shell (t-paliad-144 Phase A2). One TSX powers /views (the
// landing) and /views/{slug} (a specific view). The client bundle reads
// window.location.pathname to decide which mode to render.
//
// Hydration: client/views.ts loads the saved or system view via /api/views
// and dispatches to the matching render-shape component (list / cards /
// calendar — Q4 lock-in 2026-05-07: 3 shapes, no separate "activity").
export function renderViews(): string {
return "<!DOCTYPE html>" + (
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#BFF355" />
<PWAHead />
<title data-i18n="views.title">Ansichten &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/views" />
<BottomNav currentPath="/views" />
<main>
<section className="tool-page">
<div className="container">
{/* Header — populated by client/views.ts from the loaded view name. */}
<div className="tool-header" id="views-header">
<div className="entity-header-row">
<div>
<h1 id="views-heading" data-i18n="views.heading">Ansichten</h1>
<p className="tool-subtitle" id="views-subtitle" data-i18n="views.subtitle">
Eigene Ansichten &uuml;ber Ihre Daten &mdash; Filter und Darstellung speicherbar.
</p>
</div>
<div className="views-header-actions" id="views-header-actions">
{/* Edit + delete buttons inserted by client/views.ts when on a custom view. */}
</div>
</div>
</div>
{/* Toolbar — shape switcher (3 shapes per Q4 lock-in). */}
<div className="views-toolbar" id="views-toolbar" hidden>
<div className="agenda-chip-row" role="tablist" id="views-shape-chips" aria-label="Form">
<button type="button" className="agenda-chip" data-shape="list" role="tab" data-i18n="views.shape.list">Liste</button>
<button type="button" className="agenda-chip" data-shape="cards" role="tab" data-i18n="views.shape.cards">Karten</button>
<button type="button" className="agenda-chip" data-shape="calendar" role="tab" data-i18n="views.shape.calendar">Kalender</button>
<button type="button" className="agenda-chip" data-shape="timeline" role="tab" data-i18n="views.shape.timeline">Timeline</button>
</div>
<div className="views-toolbar-spacer" />
<a href="#" className="btn-secondary btn-small" id="views-save-as" data-i18n="views.save_as" hidden>
Als Ansicht speichern
</a>
</div>
{/* Filter bar host — t-paliad-211. mountFilterBar appends its
own toolbar element here; the saved view's filter_spec
becomes the bar's baseline, axes are chosen client-side
per the view's data sources. */}
<div className="views-filter-bar" id="views-filter-bar" hidden />
{/* Empty / onboarding state — shown on bare /views with no saved views. */}
<div className="views-onboarding" id="views-onboarding" hidden>
<h2 data-i18n="views.onboarding.title">Eigene Ansichten &mdash; was ist das?</h2>
<p data-i18n="views.onboarding.body">
Eine Ansicht ist eine gespeicherte Filterkombination &mdash; z.&thinsp;B. &bdquo;Fristen meiner Projekte in den n&auml;chsten 14 Tagen&ldquo;.
Ansichten erscheinen als eigene Buttons in der Sidebar.
</p>
<div className="views-onboarding-actions">
<a href="/views/new" className="btn-primary btn-cta-lime" data-i18n="views.onboarding.create">
Beispiel-Ansicht erstellen
</a>
</div>
</div>
{/* Inaccessible-projects toast (Q17 attribution). */}
<div className="views-toast" id="views-toast" hidden>
<span className="views-toast-text" id="views-toast-text" />
<button type="button" className="views-toast-close" id="views-toast-close" aria-label="Close">&times;</button>
</div>
{/* Loading + error + empty states (mutually exclusive). */}
<div className="views-loading" id="views-loading" data-i18n="views.loading">L&auml;dt &hellip;</div>
<div className="views-error" id="views-error" hidden>
<p id="views-error-message" />
<a href="/views" className="btn-secondary btn-small" data-i18n="views.error.back">Zur&uuml;ck zur Ansichten-&Uuml;bersicht</a>
</div>
<div className="views-empty" id="views-empty" hidden>
<p data-i18n="views.empty.title">Keine Eintr&auml;ge gefunden.</p>
<p className="views-empty-hint" id="views-empty-hint" />
</div>
{/* Render targets — only the active shape is visible. */}
<div className="views-shape-host views-shape-list" id="views-shape-list" hidden />
<div className="views-shape-host views-shape-cards" id="views-shape-cards" hidden />
<div className="views-shape-host views-shape-calendar" id="views-shape-calendar" hidden />
<div className="views-shape-host views-shape-timeline" id="views-shape-timeline" hidden>
{/* CV-chart caveat banner — design §13.4: ViewService
doesn't run the fristenrechner calculator, so Custom
Views show actual events only. One-time-per-session
dismissible (sessionStorage). */}
<div className="views-timeline-caveat" id="views-timeline-caveat" hidden>
<span data-i18n="views.timeline.caveat.body">
Custom Views zeigen nur eingetretene Ereignisse. F&uuml;r prognostizierte Fristen das Projekt-Chart &ouml;ffnen.
</span>
<button
type="button"
className="views-timeline-caveat-close"
id="views-timeline-caveat-close"
aria-label="Schlie&szlig;en"
>&times;</button>
</div>
<div id="views-timeline-chart-host" />
</div>
</div>
</section>
<Footer />
<PaliadinWidget />
</main>
<script src="/assets/views.js" defer />
</body>
</html>
);
}