Files
paliad/frontend/src/kostenrechner.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

248 lines
12 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";
import { FIRM } from "./branding";
const ICON_CALC = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><line x1="8" y1="10" x2="8" y2="10.01"/><line x1="12" y1="10" x2="12" y2="10.01"/><line x1="16" y1="10" x2="16" y2="10.01"/><line x1="8" y1="14" x2="8" y2="14.01"/><line x1="12" y1="14" x2="12" y2="14.01"/><line x1="16" y1="14" x2="16" y2="14.01"/><line x1="8" y1="18" x2="16" y2="18"/></svg>';
function instanceRow(key: string, label: string, i18nKey: string, group: string, checked: boolean): string {
return (
<div className={`instance-card ${checked ? "enabled" : ""}`} data-instance={key} data-group={group}>
<div className="instance-header">
<input type="checkbox" id={`inst-${key}`} data-key={key} checked={checked} />
<label htmlFor={`inst-${key}`}>
<strong data-i18n={i18nKey}>{label}</strong>
</label>
<button className="instance-toggle" type="button" aria-label="Details">&#9662;</button>
</div>
<div className="instance-details" style="display:none">
{group === "UPC" ? (
<div className="instance-fields">
<div className="field-row">
<label data-i18n="kosten.fee.version">Geb&uuml;hrenversion:</label>
<select data-field="feeVersion">
<option value="2026" selected data-i18n="kosten.fee.from2026">Ab 2026</option>
<option value="pre2026" data-i18n="kosten.fee.pre2026">Vor 2026</option>
</select>
</div>
<div className="field-row">
<label><input type="checkbox" data-field="isSME" /> KMU / SME</label>
</div>
<div className="field-row">
<label><input type="checkbox" data-field="includeRevocation" /> <span data-i18n="kosten.revocation">Widerklage auf Nichtigkeit</span></label>
</div>
</div>
) : group === "EPA" ? (
<div className="instance-fields">
<div className="field-row">
<label><input type="checkbox" data-field="isSME" /> KMU / SME</label>
</div>
</div>
) : (
<div className="instance-fields">
<div className="field-row">
<label data-i18n="kosten.fee.schedule">Geb&uuml;hrenordnung:</label>
<select data-field="feeVersion">
<option value="Aktuell" selected data-i18n="kosten.fee.current">Aktuell (2025)</option>
<option value="2025">2025</option>
<option value="2021">2021</option>
<option value="2013">2013</option>
<option value="2005">2005</option>
</select>
</div>
<div className="field-row">
<label data-i18n="kosten.attorneys">Rechtsanw&auml;lte:</label>
<select data-field="numAttorneys">
<option value="0">0</option>
<option value="1" selected>1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
</div>
<div className="field-row">
<label data-i18n="kosten.patent.attorneys">Patentanw&auml;lte:</label>
<select data-field="numPatentAttorneys">
<option value="0">0</option>
<option value="1" selected>1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
</div>
<div className="field-row">
<label data-i18n="kosten.clients">Mandanten:</label>
<select data-field="numClients">
<option value="1" selected>1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
</div>
<div className="field-row">
<label><input type="checkbox" data-field="oralHearing" checked /> <span data-i18n="kosten.oral.hearing">M&uuml;ndl. Verhandlung</span></label>
</div>
</div>
)}
</div>
</div>
);
}
export function renderKostenrechner(): 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="kosten.title">Prozesskostenrechner &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/tools/kostenrechner" />
<BottomNav currentPath="/tools/kostenrechner" />
<div className="print-header" id="print-header">
<div className="print-header-brand">
<strong>Paliad</strong> &mdash; <span data-i18n="kosten.print.title">Kostenberechnung</span>
</div>
<div className="print-header-meta" id="print-meta"></div>
</div>
<main>
<section className="tool-page">
<div className="container">
<div className="tool-header">
<h1 data-i18n="kosten.heading">Prozesskostenrechner</h1>
<p className="tool-subtitle" data-i18n="kosten.subtitle">
Sch&auml;tzung der Verfahrenskosten f&uuml;r Patentverletzungs-, Nichtigkeits- und EPA-Verfahren.
</p>
</div>
<div className="tool-grid">
<div className="tool-input">
<div className="input-section">
<h3 data-i18n="kosten.streitwert">Streitwert</h3>
<div className="streitwert-group">
<div className="streitwert-input-row">
<span className="streitwert-prefix">EUR</span>
<input type="text" id="streitwert-input" className="streitwert-field" value="1.000.000" />
</div>
<input type="range" id="streitwert-slider" min="10000" max="50000000" step="10000" value="1000000" />
<div className="streitwert-presets">
<button type="button" data-value="100000">100k</button>
<button type="button" data-value="500000">500k</button>
<button type="button" data-value="1000000" className="active">1M</button>
<button type="button" data-value="5000000">5M</button>
<button type="button" data-value="10000000">10M</button>
<button type="button" data-value="30000000">30M</button>
</div>
</div>
</div>
<div className="input-section">
<h3 data-i18n="kosten.vat">MwSt</h3>
<select id="vat-rate">
<option value="0.19" selected>19%</option>
<option value="0.16">16%</option>
<option value="0" data-i18n="kosten.vat.foreign">0% (Ausland)</option>
</select>
</div>
<div className="input-section">
<h3 data-i18n="kosten.de.infringement">DE Verletzungsverfahren</h3>
{instanceRow("LG", "LG (Verletzung 1. Instanz)", "kosten.inst.lg", "DE", true)}
{instanceRow("OLG", "OLG (Berufung)", "kosten.inst.olg", "DE", false)}
{instanceRow("BGH_NZB", "BGH (Nichtzulassungsbeschwerde)", "kosten.inst.bgh_nzb", "DE", false)}
{instanceRow("BGH_REV", "BGH (Revision)", "kosten.inst.bgh_rev", "DE", false)}
</div>
<div className="input-section">
<h3 data-i18n="kosten.de.nullity">DE Nichtigkeitsverfahren</h3>
{instanceRow("BPatG", "BPatG (Nichtigkeitsverfahren)", "kosten.inst.bpatg", "DE", false)}
{instanceRow("BGH_NULLITY", "BGH (Nichtigkeitsberufung)", "kosten.inst.bgh_nullity", "DE", false)}
</div>
<div className="input-section">
<h3 data-i18n="kosten.upc">UPC</h3>
{instanceRow("UPC_FIRST", "UPC (1. Instanz)", "kosten.inst.upc_first", "UPC", false)}
{instanceRow("UPC_APPEAL", "UPC (Berufung)", "kosten.inst.upc_appeal", "UPC", false)}
</div>
<div className="input-section">
<h3 data-i18n="kosten.epa">EPA</h3>
{instanceRow("EPA_OPPOSITION", "Einspruch", "kosten.inst.epa_opposition", "EPA", false)}
{instanceRow("EPA_APPEAL", "Einspruchsbeschwerde", "kosten.inst.epa_appeal", "EPA", false)}
</div>
</div>
<div className="tool-results" id="results-panel">
<div className="result-card">
<div className="result-total-section">
<div className="result-total-label" data-i18n="kosten.total">Gesamtkosten</div>
<div className="result-total" id="result-grand-total">EUR 0,00</div>
</div>
<div id="result-breakdown">
<div className="result-empty" data-i18n="kosten.empty">
W&auml;hlen Sie mindestens eine Instanz.
</div>
</div>
<div className="result-actions" id="result-actions" style="display:none">
<button type="button" id="print-btn" className="result-action-btn" data-i18n="kosten.print">
Drucken
</button>
<button type="button" id="share-btn" className="result-action-btn" data-i18n="kosten.share">
Link kopieren
</button>
<button type="button" id="compare-btn" className="result-action-btn result-action-btn--accent" data-i18n="kosten.compare">
Vergleichen
</button>
</div>
</div>
</div>
</div>
<div id="comparison-container" className="comparison-container" style="display:none">
<div className="comparison-header">
<h2 data-i18n="kosten.scenario.diff">Szenariovergleich</h2>
<button type="button" id="compare-exit-btn" className="result-action-btn" data-i18n="kosten.compare.exit">
Vergleich beenden
</button>
</div>
<div className="comparison-grid">
<div className="comparison-col">
<h3 className="comparison-col-title" data-i18n="kosten.scenario.a">Szenario A</h3>
<div className="result-card" id="compare-result-a"></div>
</div>
<div className="comparison-col">
<h3 className="comparison-col-title" data-i18n="kosten.scenario.b">Szenario B</h3>
<div className="result-card" id="compare-result-b"></div>
</div>
</div>
<div className="comparison-diff" id="compare-diff"></div>
</div>
</div>
</section>
</main>
<div className="print-footer" id="print-footer">
<span data-i18n="kosten.print.disclaimer">Dieses Dokument dient ausschlie&szlig;lich der internen Verwendung und stellt keine Rechtsberatung dar. Alle Angaben ohne Gew&auml;hr.</span>
<span>{`© 2026 Paliad — ${FIRM}`}</span>
</div>
<Footer />
<PaliadinWidget />
<script src="/assets/kostenrechner.js"></script>
</body>
</html>
);
}