Files
paliad/frontend/src/admin-event-types.tsx
m ba2408eb51 feat(paliadin/inline-widget): t-paliad-161 Slice C — floating button + slide-out drawer
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.
2026-05-08 19:54:18 +02:00

159 lines
8.0 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";
export function renderAdminEventTypes(): 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" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<PWAHead />
<title data-i18n="admin.event_types.title">Event-Typen &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/admin/event-types" />
<BottomNav currentPath="/admin/event-types" />
<main>
<section className="tool-page">
<div className="container">
<div className="tool-header">
<div>
<h1 data-i18n="admin.event_types.heading">Event-Typen</h1>
<p className="tool-subtitle" data-i18n="admin.event_types.subtitle">
Firmenweite Event-Typen moderieren: archivieren, zusammenf&uuml;hren, private Typen befördern.
</p>
</div>
</div>
<div className="admin-team-controls">
<div className="glossar-search-wrap">
<svg className="glossar-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
<input
type="text"
id="aet-search"
className="glossar-search"
placeholder="Bezeichnung, Slug oder Author suchen..."
data-i18n-placeholder="admin.event_types.search.placeholder"
autocomplete="off"
/>
<span className="glossar-count" id="aet-count" />
</div>
<label className="admin-team-multi-opt">
<input type="checkbox" id="aet-show-archived" />
<span data-i18n="admin.event_types.show_archived">Archivierte anzeigen</span>
</label>
</div>
<div className="admin-team-actions" id="aet-bulk-actions" style="display:none">
<span id="aet-bulk-count" className="admin-team-muted" />
<button className="btn-primary" id="aet-bulk-archive" type="button" data-i18n="admin.event_types.action.archive_selected">
Ausgew&auml;hlte archivieren
</button>
<button className="btn-primary" id="aet-bulk-merge" type="button" data-i18n="admin.event_types.action.merge_selected">
Zusammenf&uuml;hren&hellip;
</button>
</div>
<div id="aet-feedback" className="form-msg" style="display:none" />
<h3 className="section-heading" data-i18n="admin.event_types.section.firm_wide">Firmenweite Typen</h3>
<div className="entity-table-wrap admin-team-table-wrap">
<table className="entity-table entity-table--readonly admin-team-table">
<thead>
<tr>
<th className="aet-col-check" />
<th data-i18n="admin.event_types.col.label">Bezeichnung</th>
<th data-i18n="admin.event_types.col.category">Kategorie</th>
<th data-i18n="admin.event_types.col.jurisdiction">Jurisdiktion</th>
<th data-i18n="admin.event_types.col.author">Author</th>
<th data-i18n="admin.event_types.col.created">Erstellt</th>
<th data-i18n="admin.event_types.col.usage">Verwendung</th>
<th data-i18n="admin.event_types.col.actions">Aktionen</th>
</tr>
</thead>
<tbody id="aet-tbody">
<tr><td colspan={8} className="admin-team-loading" data-i18n="admin.event_types.loading">Lade...</td></tr>
</tbody>
</table>
</div>
<div className="entity-empty" id="aet-empty" style="display:none">
<p data-i18n="admin.event_types.empty">Keine Treffer.</p>
</div>
<h3 className="section-heading" data-i18n="admin.event_types.section.private_pending">
Private Typen (zur Bef&ouml;rderung)
</h3>
<p className="tool-subtitle" data-i18n="admin.event_types.section.private_pending.hint">
Private Typen anderer Kolleg:innen, sortiert nach H&auml;ufigkeit. Bef&ouml;rdern macht den Typ firmenweit sichtbar.
</p>
<div className="entity-table-wrap admin-team-table-wrap">
<table className="entity-table entity-table--readonly admin-team-table">
<thead>
<tr>
<th data-i18n="admin.event_types.col.label">Bezeichnung</th>
<th data-i18n="admin.event_types.col.category">Kategorie</th>
<th data-i18n="admin.event_types.col.jurisdiction">Jurisdiktion</th>
<th data-i18n="admin.event_types.col.author">Author</th>
<th data-i18n="admin.event_types.col.usage">Verwendung</th>
<th data-i18n="admin.event_types.col.actions">Aktionen</th>
</tr>
</thead>
<tbody id="aet-private-tbody">
<tr><td colspan={6} className="admin-team-loading" data-i18n="admin.event_types.loading">Lade...</td></tr>
</tbody>
</table>
</div>
<div className="entity-empty" id="aet-private-empty" style="display:none">
<p data-i18n="admin.event_types.private.empty">Keine privaten Typen.</p>
</div>
</div>
</section>
</main>
{/* Merge modal — list of selected types as candidates, admin picks one
as winner. Confirms with usage count, then POST /merge atomically
redirects junction rows + archives losers. */}
<div className="modal-overlay" id="aet-merge-modal" style="display:none">
<div className="modal-card">
<div className="modal-header">
<h2 data-i18n="admin.event_types.merge.title">Typen zusammenf&uuml;hren</h2>
<button className="modal-close" id="aet-merge-close" type="button" aria-label="Close">&times;</button>
</div>
<p data-i18n="admin.event_types.merge.body" className="invite-modal-body">
W&auml;hlen Sie den Gewinner-Typ. Die Junction-Eintr&auml;ge der Verlierer werden auf den Gewinner umgeleitet, anschlie&szlig;end werden die Verlierer archiviert.
</p>
<form id="aet-merge-form" className="entity-form" autocomplete="off">
<div id="aet-merge-options" className="aet-merge-options" />
<p className="form-msg" id="aet-merge-msg" />
<div className="form-actions">
<button type="button" className="btn-cancel" id="aet-merge-cancel" data-i18n="common.cancel">Abbrechen</button>
<button type="submit" className="btn-primary" id="aet-merge-submit" data-i18n="admin.event_types.merge.submit">Zusammenf&uuml;hren</button>
</div>
</form>
</div>
</div>
<Footer />
<PaliadinWidget />
<script src="/assets/admin-event-types.js"></script>
</body>
</html>
);
}