The inline Paliadin chat surface — reachable from every authenticated
page, replacing the standalone /paliadin route as the primary entry
point. The standalone page survives as the dedicated full-screen mode
(the drawer's "↗ fullscreen" action links to it).
Components:
- frontend/src/components/PaliadinWidget.tsx — emits the floating
trigger button (bottom-right, lime ✨, owner-revealed by JS), a
scrim, and the right-edge slide-out drawer with header (reset /
fullscreen / close), context chip, message stream, empty-state
starter list, and textarea+send form. Loads /assets/paliadin-widget.js.
- frontend/src/client/paliadin-widget.ts — runtime. /api/me probe
reveals the trigger when caller matches PaliadinOwnerEmail (with
optional is_paliadin_owner flag fast-path); Cmd+J / Ctrl+J shortcut
toggles open/close (Cmd+K stays reserved for global search per
client/search.ts). Uses computePaliadinContext() (Slice B) per send
so route + entity + selection flow into every turn. SSE consumer
writes assistant bubbles; localStorage persists per-session history.
- frontend/src/client/paliadin-starters.ts — per-route starter prompt
registry. 14 routes covered (dashboard, projects.*, deadlines.*,
appointments.*, agenda, events, inbox, tools.*, glossary, courts) +
a _default fallback. Bilingual (DE/EN); prompts ending in `: ` seed
the textarea for the user to finish; fully-formed prompts auto-send.
- 39 authenticated TSX pages get a `<PaliadinWidget />` element after
`<Footer />` via a mechanical pass. paliadin.tsx (the standalone)
is intentionally excluded — its dedicated UI is the widget's
fullscreen escape hatch, not a place to overlay another widget.
- frontend/build.ts registers the new bundle.
- frontend/src/styles/global.css gains ~280 lines of widget CSS
(trigger / scrim / drawer / header / context-chip / messages /
bubbles / starters / form / send-btn) using only existing tokens.
Mobile (≤640px): drawer goes full-screen; trigger lifts above
bottom-nav slots.
- 11 new i18n keys × 2 langs = 22 entries under paliadin.widget.*.
Visibility predicate (paliadin-context.shouldSendContext) hides the
widget on /paliadin, /login, /onboarding. Owner-only gate stays on
PaliadinOwnerEmail.
Build clean: i18n 1955 → 1966 keys, IIFE-wrapped 218KB bundle, go test
green.
Refs: docs/design-paliadin-inline-2026-05-08.md §3, §5.
156 lines
8.7 KiB
TypeScript
156 lines
8.7 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 editor (t-paliad-144 Phase A2). Powers /views/new (blank
|
|
// slate) and /views/{slug}/edit (mode chosen at hydration via path
|
|
// inspection). One TSX, one bundle (client/views-editor.ts).
|
|
|
|
export function renderViewsEditor(): 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.editor.title">Ansicht bearbeiten — 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">
|
|
<div className="tool-header">
|
|
<h1 id="editor-heading" data-i18n="views.editor.heading.new">Neue Ansicht</h1>
|
|
<p className="tool-subtitle" data-i18n="views.editor.subtitle">
|
|
Wählen Sie Quellen, Filter und Darstellung. Änderungen speichern Sie unten.
|
|
</p>
|
|
</div>
|
|
|
|
<form id="editor-form" className="entity-form" novalidate>
|
|
<fieldset className="form-section">
|
|
<legend data-i18n="views.editor.section.identity">Bezeichnung</legend>
|
|
<div className="form-field">
|
|
<label htmlFor="editor-name" data-i18n="views.editor.field.name">Name</label>
|
|
<input id="editor-name" type="text" required maxlength={200} />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="editor-slug" data-i18n="views.editor.field.slug">Slug (URL)</label>
|
|
<input id="editor-slug" type="text" required pattern="^[a-z0-9][a-z0-9-]{0,62}$" maxlength={63} />
|
|
<small className="form-hint" data-i18n="views.editor.hint.slug">
|
|
Kleinbuchstaben, Ziffern und Bindestriche — nicht reservierte Wörter.
|
|
</small>
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="editor-icon" data-i18n="views.editor.field.icon">Icon</label>
|
|
<select id="editor-icon">
|
|
<option value="" data-i18n="views.editor.icon.default">Standard (Ordner)</option>
|
|
<option value="clock" data-i18n="views.editor.icon.clock">Uhr</option>
|
|
<option value="calendar" data-i18n="views.editor.icon.calendar">Kalender</option>
|
|
<option value="bell" data-i18n="views.editor.icon.bell">Glocke</option>
|
|
<option value="folder" data-i18n="views.editor.icon.folder">Ordner</option>
|
|
<option value="users" data-i18n="views.editor.icon.users">Personen</option>
|
|
<option value="building" data-i18n="views.editor.icon.building">Gebäude</option>
|
|
</select>
|
|
</div>
|
|
<div className="form-field form-field-checkbox">
|
|
<label>
|
|
<input id="editor-show-count" type="checkbox" />
|
|
<span data-i18n="views.editor.field.show_count">Treffer-Anzahl in der Sidebar anzeigen</span>
|
|
</label>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<fieldset className="form-section">
|
|
<legend data-i18n="views.editor.section.sources">Quellen</legend>
|
|
<p className="form-hint" data-i18n="views.editor.hint.sources">Welche Datenarten zeigt diese Ansicht?</p>
|
|
<div className="form-field form-field-checkbox-group">
|
|
<label><input type="checkbox" name="source" value="deadline" /> <span data-i18n="views.source.deadline">Fristen</span></label>
|
|
<label><input type="checkbox" name="source" value="appointment" /> <span data-i18n="views.source.appointment">Termine</span></label>
|
|
<label><input type="checkbox" name="source" value="project_event" /> <span data-i18n="views.source.project_event">Projekt-Verlauf</span></label>
|
|
<label><input type="checkbox" name="source" value="approval_request" /> <span data-i18n="views.source.approval_request">Genehmigungen</span></label>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<fieldset className="form-section">
|
|
<legend data-i18n="views.editor.section.scope">Geltungsbereich</legend>
|
|
<div className="form-field">
|
|
<label htmlFor="editor-scope-mode" data-i18n="views.editor.field.scope_mode">Projekte</label>
|
|
<select id="editor-scope-mode">
|
|
<option value="all_visible" data-i18n="views.scope.all_visible">Alle sichtbaren</option>
|
|
<option value="my_subtree" data-i18n="views.scope.my_subtree">Mein Teilbaum</option>
|
|
</select>
|
|
</div>
|
|
<div className="form-field form-field-checkbox">
|
|
<label>
|
|
<input id="editor-personal-only" type="checkbox" />
|
|
<span data-i18n="views.editor.field.personal_only">Nur persönliche</span>
|
|
</label>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<fieldset className="form-section">
|
|
<legend data-i18n="views.editor.section.time">Zeitraum</legend>
|
|
<div className="form-field">
|
|
<label htmlFor="editor-time-horizon" data-i18n="views.editor.field.horizon">Horizont</label>
|
|
<select id="editor-time-horizon">
|
|
<option value="next_7d" data-i18n="views.horizon.next_7d">Nächste 7 Tage</option>
|
|
<option value="next_30d" data-i18n="views.horizon.next_30d">Nächste 30 Tage</option>
|
|
<option value="next_90d" data-i18n="views.horizon.next_90d">Nächste 90 Tage</option>
|
|
<option value="past_30d" data-i18n="views.horizon.past_30d">Letzte 30 Tage</option>
|
|
<option value="past_90d" data-i18n="views.horizon.past_90d">Letzte 90 Tage</option>
|
|
<option value="any" data-i18n="views.horizon.any">Beliebig</option>
|
|
</select>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<fieldset className="form-section">
|
|
<legend data-i18n="views.editor.section.render">Darstellung</legend>
|
|
<div className="form-field">
|
|
<label htmlFor="editor-shape" data-i18n="views.editor.field.shape">Form</label>
|
|
<select id="editor-shape">
|
|
<option value="list" data-i18n="views.shape.list">Liste</option>
|
|
<option value="cards" data-i18n="views.shape.cards">Karten</option>
|
|
<option value="calendar" data-i18n="views.shape.calendar">Kalender</option>
|
|
</select>
|
|
</div>
|
|
<div className="form-field" id="editor-list-density-group">
|
|
<label htmlFor="editor-list-density" data-i18n="views.editor.field.density">Dichte</label>
|
|
<select id="editor-list-density">
|
|
<option value="comfortable" data-i18n="views.density.comfortable">Bequem</option>
|
|
<option value="compact" data-i18n="views.density.compact">Kompakt</option>
|
|
</select>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<div className="entity-form-feedback" id="editor-feedback" hidden />
|
|
|
|
<div className="entity-form-actions">
|
|
<button type="submit" className="btn-primary btn-cta-lime" id="editor-save" data-i18n="views.editor.save">
|
|
Speichern
|
|
</button>
|
|
<a href="/views" className="btn-secondary" data-i18n="views.editor.cancel">Abbrechen</a>
|
|
<button type="button" className="btn-danger" id="editor-delete" hidden data-i18n="views.editor.delete">
|
|
Löschen
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
<Footer />
|
|
<PaliadinWidget />
|
|
</main>
|
|
|
|
<script src="/assets/views-editor.js" defer />
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|