feat: Checklisten — interactive filing checklists with localStorage state
Six bilingual patent-workflow checklists (UPC Statement of Claim, Defence, Confidentiality Application, Representative Registration; BPatG Nullity; EPO Opposition) with grouped items, rule references, and tips. Index page lists cards with regime filter and per-checklist progress; detail page persists check state in localStorage (patholo:checklist:<slug>), shows a live progress bar, supports reset and print, and submits feedback via Supabase checklisten_feedback.
This commit is contained in:
@@ -8,6 +8,8 @@ import { renderDownloads } from "./src/downloads";
|
||||
import { renderLinks } from "./src/links";
|
||||
import { renderGlossar } from "./src/glossar";
|
||||
import { renderGebuehrentabellen } from "./src/gebuehrentabellen";
|
||||
import { renderChecklisten } from "./src/checklisten";
|
||||
import { renderChecklistenDetail } from "./src/checklisten-detail";
|
||||
|
||||
const DIST = join(import.meta.dir, "dist");
|
||||
|
||||
@@ -27,6 +29,8 @@ async function build() {
|
||||
join(import.meta.dir, "src/client/links.ts"),
|
||||
join(import.meta.dir, "src/client/glossar.ts"),
|
||||
join(import.meta.dir, "src/client/gebuehrentabellen.ts"),
|
||||
join(import.meta.dir, "src/client/checklisten.ts"),
|
||||
join(import.meta.dir, "src/client/checklisten-detail.ts"),
|
||||
],
|
||||
outdir: join(DIST, "assets"),
|
||||
naming: "[name].js",
|
||||
@@ -56,6 +60,8 @@ async function build() {
|
||||
await Bun.write(join(DIST, "links.html"), renderLinks());
|
||||
await Bun.write(join(DIST, "glossar.html"), renderGlossar());
|
||||
await Bun.write(join(DIST, "gebuehrentabellen.html"), renderGebuehrentabellen());
|
||||
await Bun.write(join(DIST, "checklisten.html"), renderChecklisten());
|
||||
await Bun.write(join(DIST, "checklisten-detail.html"), renderChecklistenDetail());
|
||||
|
||||
console.log("Build complete \u2192 dist/");
|
||||
}
|
||||
|
||||
95
frontend/src/checklisten-detail.tsx
Normal file
95
frontend/src/checklisten-detail.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { h } from "./jsx";
|
||||
import { Sidebar } from "./components/Sidebar";
|
||||
import { Footer } from "./components/Footer";
|
||||
|
||||
export function renderChecklistenDetail(): string {
|
||||
return "<!DOCTYPE html>" + (
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title data-i18n="checklisten.title">Checkliste — patHoLo</title>
|
||||
<link rel="stylesheet" href="/assets/global.css" />
|
||||
</head>
|
||||
<body className="has-sidebar">
|
||||
<Sidebar currentPath="/checklisten" />
|
||||
|
||||
<main>
|
||||
<section className="tool-page">
|
||||
<div className="container">
|
||||
<a href="/checklisten" className="checklist-back">
|
||||
<span className="checklist-back-arrow">←</span>
|
||||
<span data-i18n="checklisten.back">Zurück zur Übersicht</span>
|
||||
</a>
|
||||
|
||||
<div className="tool-header checklist-detail-header">
|
||||
<div className="checklist-detail-head-row">
|
||||
<div>
|
||||
<h1 id="checklist-title"> </h1>
|
||||
<p className="tool-subtitle" id="checklist-subtitle"> </p>
|
||||
<dl className="checklist-meta" id="checklist-meta" />
|
||||
</div>
|
||||
<div className="checklist-actions">
|
||||
<button type="button" id="btn-print" className="btn-ghost" data-i18n="checklisten.print">Drucken</button>
|
||||
<button type="button" id="btn-reset" className="btn-ghost" data-i18n="checklisten.reset">Zurücksetzen</button>
|
||||
<button type="button" id="btn-feedback" className="btn-suggest">
|
||||
<span data-i18n="checklisten.feedback.btn">Feedback</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="checklist-progress">
|
||||
<div className="checklist-progress-bar">
|
||||
<div className="checklist-progress-fill" id="progress-fill" />
|
||||
</div>
|
||||
<span className="checklist-progress-label" id="progress-label">0 / 0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="checklist-groups" className="checklist-groups" />
|
||||
|
||||
<div className="checklist-print-footer">
|
||||
<p className="checklist-disclaimer" data-i18n="checklisten.disclaimer">
|
||||
Hinweis: Diese Checklisten dienen als Gedächtnisstütze und ersetzen keine Prüfung im Einzelfall. Maßgeblich sind die jeweils geltenden Verfahrensregeln.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{/* Feedback modal */}
|
||||
<div className="modal-overlay" id="feedback-modal" style="display:none">
|
||||
<div className="modal-card">
|
||||
<div className="modal-header">
|
||||
<h2 data-i18n="checklisten.feedback.title">Feedback zur Checkliste</h2>
|
||||
<button className="modal-close" id="modal-close" type="button">×</button>
|
||||
</div>
|
||||
<form id="feedback-form">
|
||||
<div className="form-field">
|
||||
<label htmlFor="feedback-type" data-i18n="checklisten.feedback.type">Art</label>
|
||||
<select id="feedback-type" required>
|
||||
<option value="error" data-i18n="checklisten.feedback.error">Fehler gefunden</option>
|
||||
<option value="missing" data-i18n="checklisten.feedback.missing">Fehlender Punkt</option>
|
||||
<option value="suggestion" data-i18n="checklisten.feedback.suggestion">Verbesserungsvorschlag</option>
|
||||
<option value="other" data-i18n="checklisten.feedback.other">Sonstiges</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label htmlFor="feedback-message" data-i18n="checklisten.feedback.message">Nachricht</label>
|
||||
<textarea id="feedback-message" rows={4} required />
|
||||
</div>
|
||||
<div className="form-actions">
|
||||
<button type="button" className="btn-cancel" id="modal-cancel" data-i18n="checklisten.feedback.cancel">Abbrechen</button>
|
||||
<button type="submit" className="btn-submit" data-i18n="checklisten.feedback.submit">Absenden</button>
|
||||
</div>
|
||||
<p className="form-msg" id="feedback-msg" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
<script src="/assets/checklisten-detail.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
44
frontend/src/checklisten.tsx
Normal file
44
frontend/src/checklisten.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { h } from "./jsx";
|
||||
import { Sidebar } from "./components/Sidebar";
|
||||
import { Footer } from "./components/Footer";
|
||||
|
||||
export function renderChecklisten(): string {
|
||||
return "<!DOCTYPE html>" + (
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title data-i18n="checklisten.title">Checklisten — patHoLo</title>
|
||||
<link rel="stylesheet" href="/assets/global.css" />
|
||||
</head>
|
||||
<body className="has-sidebar">
|
||||
<Sidebar currentPath="/checklisten" />
|
||||
|
||||
<main>
|
||||
<section className="tool-page">
|
||||
<div className="container">
|
||||
<div className="tool-header">
|
||||
<h1 data-i18n="checklisten.heading">Checklisten</h1>
|
||||
<p className="tool-subtitle" data-i18n="checklisten.subtitle">
|
||||
Interaktive Checklisten für typische Verfahrensschritte vor UPC, BPatG und EPA.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="checklist-filters" id="checklist-filters">
|
||||
<button className="filter-pill active" data-regime="all" type="button" data-i18n="checklisten.filter.all">Alle</button>
|
||||
<button className="filter-pill" data-regime="UPC" type="button">UPC</button>
|
||||
<button className="filter-pill" data-regime="DE" type="button" data-i18n="checklisten.filter.de">DE</button>
|
||||
<button className="filter-pill" data-regime="EPA" type="button">EPA</button>
|
||||
</div>
|
||||
|
||||
<div className="checklist-grid" id="checklist-grid" />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<script src="/assets/checklisten.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
272
frontend/src/client/checklisten-detail.ts
Normal file
272
frontend/src/client/checklisten-detail.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import { initI18n, onLangChange, t, getLang } from "./i18n";
|
||||
import { initSidebar } from "./sidebar";
|
||||
|
||||
interface ChecklistItem {
|
||||
labelDE: string;
|
||||
labelEN: string;
|
||||
noteDE?: string;
|
||||
noteEN?: string;
|
||||
rule?: string;
|
||||
}
|
||||
|
||||
interface ChecklistGroup {
|
||||
titleDE: string;
|
||||
titleEN: string;
|
||||
items: ChecklistItem[];
|
||||
}
|
||||
|
||||
interface Checklist {
|
||||
slug: string;
|
||||
titleDE: string;
|
||||
titleEN: string;
|
||||
descriptionDE: string;
|
||||
descriptionEN: string;
|
||||
regime: string;
|
||||
courtDE: string;
|
||||
courtEN: string;
|
||||
deadlineDE?: string;
|
||||
deadlineEN?: string;
|
||||
referenceDE?: string;
|
||||
referenceEN?: string;
|
||||
groups: ChecklistGroup[];
|
||||
}
|
||||
|
||||
let checklist: Checklist | null = null;
|
||||
let state: Record<string, boolean> = {};
|
||||
|
||||
function storageKey(slug: string): string {
|
||||
return `patholo:checklist:${slug}`;
|
||||
}
|
||||
|
||||
function esc(s: string): string {
|
||||
const d = document.createElement("div");
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function itemKey(groupIdx: number, itemIdx: number): string {
|
||||
return `g${groupIdx}-i${itemIdx}`;
|
||||
}
|
||||
|
||||
function loadState() {
|
||||
if (!checklist) return;
|
||||
try {
|
||||
const raw = localStorage.getItem(storageKey(checklist.slug));
|
||||
state = raw ? (JSON.parse(raw) as Record<string, boolean>) : {};
|
||||
} catch {
|
||||
state = {};
|
||||
}
|
||||
}
|
||||
|
||||
function saveState() {
|
||||
if (!checklist) return;
|
||||
localStorage.setItem(storageKey(checklist.slug), JSON.stringify(state));
|
||||
}
|
||||
|
||||
function totalItems(): number {
|
||||
if (!checklist) return 0;
|
||||
return checklist.groups.reduce((n, g) => n + g.items.length, 0);
|
||||
}
|
||||
|
||||
function doneItems(): number {
|
||||
return Object.values(state).filter(Boolean).length;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const slug = window.location.pathname.split("/").pop() ?? "";
|
||||
const resp = await fetch(`/api/checklisten/${encodeURIComponent(slug)}`);
|
||||
if (!resp.ok) {
|
||||
document.title = "404 — patHoLo";
|
||||
const title = document.getElementById("checklist-title")!;
|
||||
title.textContent = t("checklisten.notfound");
|
||||
return;
|
||||
}
|
||||
checklist = await resp.json();
|
||||
loadState();
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
if (!checklist) return;
|
||||
renderHeader();
|
||||
renderGroups();
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
function renderHeader() {
|
||||
if (!checklist) return;
|
||||
const isEN = getLang() === "en";
|
||||
const title = isEN ? checklist.titleEN : checklist.titleDE;
|
||||
const desc = isEN ? checklist.descriptionEN : checklist.descriptionDE;
|
||||
const court = isEN ? checklist.courtEN : checklist.courtDE;
|
||||
const deadline = isEN ? checklist.deadlineEN : checklist.deadlineDE;
|
||||
const reference = isEN ? checklist.referenceEN : checklist.referenceDE;
|
||||
|
||||
document.title = `${title} — patHoLo`;
|
||||
document.getElementById("checklist-title")!.textContent = title;
|
||||
document.getElementById("checklist-subtitle")!.textContent = desc;
|
||||
|
||||
const courtLabel = isEN ? "Court / Authority" : "Gericht / Behörde";
|
||||
const deadlineLabel = isEN ? "Deadline" : "Frist";
|
||||
const refLabel = isEN ? "Reference" : "Rechtsgrundlage";
|
||||
const regimeLabel = isEN ? "Regime" : "Bereich";
|
||||
|
||||
const parts: string[] = [];
|
||||
parts.push(`<div class="checklist-meta-item"><dt>${regimeLabel}</dt><dd><span class="checklist-regime checklist-regime-${esc(checklist.regime)}">${esc(checklist.regime)}</span></dd></div>`);
|
||||
parts.push(`<div class="checklist-meta-item"><dt>${courtLabel}</dt><dd>${esc(court)}</dd></div>`);
|
||||
if (deadline) {
|
||||
parts.push(`<div class="checklist-meta-item"><dt>${deadlineLabel}</dt><dd>${esc(deadline)}</dd></div>`);
|
||||
}
|
||||
if (reference) {
|
||||
parts.push(`<div class="checklist-meta-item"><dt>${refLabel}</dt><dd>${esc(reference)}</dd></div>`);
|
||||
}
|
||||
document.getElementById("checklist-meta")!.innerHTML = parts.join("");
|
||||
}
|
||||
|
||||
function renderGroups() {
|
||||
if (!checklist) return;
|
||||
const isEN = getLang() === "en";
|
||||
const container = document.getElementById("checklist-groups")!;
|
||||
|
||||
container.innerHTML = checklist.groups.map((g, gi) => {
|
||||
const groupTitle = isEN ? g.titleEN : g.titleDE;
|
||||
const items = g.items.map((item, ii) => {
|
||||
const key = itemKey(gi, ii);
|
||||
const checked = !!state[key];
|
||||
const label = isEN ? item.labelEN : item.labelDE;
|
||||
const note = isEN ? item.noteEN : item.noteDE;
|
||||
const rule = item.rule;
|
||||
|
||||
const noteHTML = note ? `<p class="checklist-item-note">${esc(note)}</p>` : "";
|
||||
const ruleHTML = rule ? `<span class="checklist-item-rule">${esc(rule)}</span>` : "";
|
||||
|
||||
return `<li class="checklist-item${checked ? " checked" : ""}" data-key="${key}">
|
||||
<label class="checklist-item-label">
|
||||
<input type="checkbox" class="checklist-checkbox" data-key="${key}"${checked ? " checked" : ""} />
|
||||
<span class="checklist-item-body">
|
||||
<span class="checklist-item-row">
|
||||
<span class="checklist-item-text">${esc(label)}</span>
|
||||
${ruleHTML}
|
||||
</span>
|
||||
${noteHTML}
|
||||
</span>
|
||||
</label>
|
||||
</li>`;
|
||||
}).join("");
|
||||
|
||||
return `<section class="checklist-group">
|
||||
<h2 class="checklist-group-title">${esc(groupTitle)}</h2>
|
||||
<ol class="checklist-list">${items}</ol>
|
||||
</section>`;
|
||||
}).join("");
|
||||
|
||||
container.querySelectorAll<HTMLInputElement>(".checklist-checkbox").forEach((cb) => {
|
||||
cb.addEventListener("change", () => {
|
||||
const key = cb.dataset.key!;
|
||||
state[key] = cb.checked;
|
||||
saveState();
|
||||
const li = cb.closest(".checklist-item");
|
||||
if (li) li.classList.toggle("checked", cb.checked);
|
||||
updateProgress();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateProgress() {
|
||||
const total = totalItems();
|
||||
const done = doneItems();
|
||||
const pct = total === 0 ? 0 : Math.round((done / total) * 100);
|
||||
|
||||
const fill = document.getElementById("progress-fill");
|
||||
if (fill) (fill as HTMLElement).style.width = `${pct}%`;
|
||||
|
||||
const label = document.getElementById("progress-label");
|
||||
if (label) {
|
||||
const doneLabel = getLang() === "en" ? "done" : "erledigt";
|
||||
label.textContent = `${done} / ${total} ${doneLabel}`;
|
||||
}
|
||||
}
|
||||
|
||||
function initReset() {
|
||||
document.getElementById("btn-reset")!.addEventListener("click", () => {
|
||||
if (!checklist) return;
|
||||
const ok = confirm(t("checklisten.reset.confirm"));
|
||||
if (!ok) return;
|
||||
state = {};
|
||||
saveState();
|
||||
renderGroups();
|
||||
updateProgress();
|
||||
});
|
||||
}
|
||||
|
||||
function initPrint() {
|
||||
document.getElementById("btn-print")!.addEventListener("click", () => {
|
||||
window.print();
|
||||
});
|
||||
}
|
||||
|
||||
function initFeedback() {
|
||||
const modal = document.getElementById("feedback-modal")!;
|
||||
const form = document.getElementById("feedback-form")!;
|
||||
const msg = document.getElementById("feedback-msg")!;
|
||||
|
||||
document.getElementById("btn-feedback")!.addEventListener("click", () => {
|
||||
msg.textContent = "";
|
||||
msg.className = "form-msg";
|
||||
modal.style.display = "flex";
|
||||
});
|
||||
|
||||
document.getElementById("modal-close")!.addEventListener("click", () => { modal.style.display = "none"; });
|
||||
document.getElementById("modal-cancel")!.addEventListener("click", () => { modal.style.display = "none"; });
|
||||
modal.addEventListener("click", (e) => { if (e.target === e.currentTarget) modal.style.display = "none"; });
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
if (!checklist) return;
|
||||
const submitBtn = form.querySelector(".btn-submit") as HTMLButtonElement;
|
||||
const payload = {
|
||||
feedback_type: (document.getElementById("feedback-type") as HTMLSelectElement).value,
|
||||
checklist: checklist.slug,
|
||||
message: (document.getElementById("feedback-message") as HTMLTextAreaElement).value.trim(),
|
||||
};
|
||||
|
||||
if (!payload.message) {
|
||||
msg.textContent = t("checklisten.feedback.error.required");
|
||||
msg.className = "form-msg form-msg-error";
|
||||
return;
|
||||
}
|
||||
|
||||
submitBtn.disabled = true;
|
||||
try {
|
||||
const resp = await fetch("/api/checklisten/feedback", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
msg.textContent = t("checklisten.feedback.error.generic");
|
||||
msg.className = "form-msg form-msg-error";
|
||||
return;
|
||||
}
|
||||
msg.textContent = t("checklisten.feedback.success");
|
||||
msg.className = "form-msg form-msg-success";
|
||||
(document.getElementById("feedback-message") as HTMLTextAreaElement).value = "";
|
||||
setTimeout(() => { modal.style.display = "none"; }, 1500);
|
||||
} catch {
|
||||
msg.textContent = t("checklisten.feedback.error.generic");
|
||||
msg.className = "form-msg form-msg-error";
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initI18n();
|
||||
initSidebar();
|
||||
initReset();
|
||||
initPrint();
|
||||
initFeedback();
|
||||
onLangChange(renderAll);
|
||||
load();
|
||||
});
|
||||
104
frontend/src/client/checklisten.ts
Normal file
104
frontend/src/client/checklisten.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { initI18n, onLangChange, t, getLang } from "./i18n";
|
||||
import { initSidebar } from "./sidebar";
|
||||
|
||||
interface ChecklistSummary {
|
||||
slug: string;
|
||||
titleDE: string;
|
||||
titleEN: string;
|
||||
descriptionDE: string;
|
||||
descriptionEN: string;
|
||||
regime: string;
|
||||
courtDE: string;
|
||||
courtEN: string;
|
||||
itemCount: number;
|
||||
}
|
||||
|
||||
let allChecklists: ChecklistSummary[] = [];
|
||||
let activeRegime = "all";
|
||||
|
||||
function storageKey(slug: string): string {
|
||||
return `patholo:checklist:${slug}`;
|
||||
}
|
||||
|
||||
function progressFor(slug: string, total: number): { done: number; total: number } {
|
||||
try {
|
||||
const raw = localStorage.getItem(storageKey(slug));
|
||||
if (!raw) return { done: 0, total };
|
||||
const obj = JSON.parse(raw) as Record<string, boolean>;
|
||||
const done = Object.values(obj).filter(Boolean).length;
|
||||
return { done: Math.min(done, total), total };
|
||||
} catch {
|
||||
return { done: 0, total };
|
||||
}
|
||||
}
|
||||
|
||||
function esc(s: string): string {
|
||||
const d = document.createElement("div");
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const resp = await fetch("/api/checklisten");
|
||||
if (!resp.ok) return;
|
||||
allChecklists = await resp.json();
|
||||
render();
|
||||
}
|
||||
|
||||
function render() {
|
||||
const grid = document.getElementById("checklist-grid")!;
|
||||
const isEN = getLang() === "en";
|
||||
|
||||
const filtered = activeRegime === "all"
|
||||
? allChecklists
|
||||
: allChecklists.filter((c) => c.regime === activeRegime);
|
||||
|
||||
if (filtered.length === 0) {
|
||||
grid.innerHTML = `<p class="checklist-empty" data-i18n="checklisten.empty">${t("checklisten.empty")}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = filtered.map((c) => {
|
||||
const title = isEN ? c.titleEN : c.titleDE;
|
||||
const desc = isEN ? c.descriptionEN : c.descriptionDE;
|
||||
const court = isEN ? c.courtEN : c.courtDE;
|
||||
const { done, total } = progressFor(c.slug, c.itemCount);
|
||||
const pct = total === 0 ? 0 : Math.round((done / total) * 100);
|
||||
const doneLabel = isEN ? "done" : "erledigt";
|
||||
return `<a href="/checklisten/${esc(c.slug)}" class="checklist-card">
|
||||
<div class="checklist-card-top">
|
||||
<span class="checklist-regime checklist-regime-${esc(c.regime)}">${esc(c.regime)}</span>
|
||||
<span class="checklist-card-count">${total} ${isEN ? "items" : "Punkte"}</span>
|
||||
</div>
|
||||
<h2 class="checklist-card-title">${esc(title)}</h2>
|
||||
<p class="checklist-card-desc">${esc(desc)}</p>
|
||||
<p class="checklist-card-court">${esc(court)}</p>
|
||||
<div class="checklist-card-progress">
|
||||
<div class="checklist-progress-bar">
|
||||
<div class="checklist-progress-fill" style="width:${pct}%"></div>
|
||||
</div>
|
||||
<span class="checklist-progress-label">${done} / ${total} ${doneLabel}</span>
|
||||
</div>
|
||||
</a>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function initFilters() {
|
||||
const container = document.getElementById("checklist-filters")!;
|
||||
container.addEventListener("click", (e) => {
|
||||
const btn = (e.target as HTMLElement).closest<HTMLButtonElement>(".filter-pill");
|
||||
if (!btn) return;
|
||||
container.querySelectorAll(".filter-pill").forEach((p) => p.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
activeRegime = btn.dataset.regime ?? "all";
|
||||
render();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initI18n();
|
||||
initSidebar();
|
||||
initFilters();
|
||||
onLangChange(render);
|
||||
load();
|
||||
});
|
||||
@@ -18,6 +18,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"nav.links": "Links",
|
||||
"nav.glossar": "Glossar",
|
||||
"nav.gebuehrentabellen": "Geb\u00fchrentabellen",
|
||||
"nav.checklisten": "Checklisten",
|
||||
"nav.logout": "Abmelden",
|
||||
|
||||
// Footer
|
||||
@@ -275,6 +276,35 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
// Index — Geb\u00fchrentabellen card
|
||||
"index.gebuehren.title": "Geb\u00fchrentabellen",
|
||||
"index.gebuehren.desc": "Interaktive Geb\u00fchrentabellen f\u00fcr GKG, RVG, UPC, EPA und PatKostG. Streitwert eingeben, Geb\u00fchr ablesen.",
|
||||
|
||||
// Checklisten
|
||||
"index.checklisten.title": "Checklisten",
|
||||
"index.checklisten.desc": "Interaktive Checklisten f\u00fcr UPC-, DE- und EPA-Verfahren. Fortschritt wird lokal gespeichert.",
|
||||
"checklisten.title": "Checklisten \u2014 patHoLo",
|
||||
"checklisten.heading": "Checklisten",
|
||||
"checklisten.subtitle": "Interaktive Checklisten f\u00fcr typische Verfahrensschritte vor UPC, BPatG und EPA. Abhaken, ausdrucken, kein Punkt vergessen.",
|
||||
"checklisten.filter.all": "Alle",
|
||||
"checklisten.filter.de": "DE",
|
||||
"checklisten.empty": "Keine Checklisten in dieser Kategorie.",
|
||||
"checklisten.back": "Zur\u00fcck zur \u00dcbersicht",
|
||||
"checklisten.print": "Drucken",
|
||||
"checklisten.reset": "Zur\u00fccksetzen",
|
||||
"checklisten.reset.confirm": "Alle H\u00e4kchen dieser Checkliste wirklich zur\u00fccksetzen?",
|
||||
"checklisten.notfound": "Checkliste nicht gefunden.",
|
||||
"checklisten.disclaimer": "Hinweis: Diese Checklisten dienen als Ged\u00e4chtnisst\u00fctze und ersetzen keine Pr\u00fcfung im Einzelfall. Ma\u00dfgeblich sind die jeweils geltenden Verfahrensregeln.",
|
||||
"checklisten.feedback.btn": "Feedback",
|
||||
"checklisten.feedback.title": "Feedback zur Checkliste",
|
||||
"checklisten.feedback.type": "Art",
|
||||
"checklisten.feedback.error": "Fehler gefunden",
|
||||
"checklisten.feedback.missing": "Fehlender Punkt",
|
||||
"checklisten.feedback.suggestion": "Verbesserungsvorschlag",
|
||||
"checklisten.feedback.other": "Sonstiges",
|
||||
"checklisten.feedback.message": "Nachricht",
|
||||
"checklisten.feedback.submit": "Absenden",
|
||||
"checklisten.feedback.cancel": "Abbrechen",
|
||||
"checklisten.feedback.success": "Danke f\u00fcr Ihr Feedback!",
|
||||
"checklisten.feedback.error.required": "Bitte geben Sie eine Nachricht ein.",
|
||||
"checklisten.feedback.error.generic": "Fehler beim Senden. Bitte versuchen Sie es erneut.",
|
||||
},
|
||||
|
||||
en: {
|
||||
@@ -286,6 +316,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"nav.links": "Links",
|
||||
"nav.glossar": "Glossary",
|
||||
"nav.gebuehrentabellen": "Fee Schedules",
|
||||
"nav.checklisten": "Checklists",
|
||||
"nav.logout": "Sign Out",
|
||||
|
||||
// Footer
|
||||
@@ -543,6 +574,35 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
// Index \u2014 Geb\u00fchrentabellen card
|
||||
"index.gebuehren.title": "Fee Schedules",
|
||||
"index.gebuehren.desc": "Interactive fee schedules for GKG, RVG, UPC, EPO, and PatKostG. Enter a dispute value, read the fee.",
|
||||
|
||||
// Checklisten
|
||||
"index.checklisten.title": "Checklists",
|
||||
"index.checklisten.desc": "Interactive filing checklists for UPC, German, and EPO proceedings. Progress is stored locally.",
|
||||
"checklisten.title": "Checklists \u2014 patHoLo",
|
||||
"checklisten.heading": "Checklists",
|
||||
"checklisten.subtitle": "Interactive checklists for typical procedural steps before the UPC, German Patent Court, and EPO. Tick off, print, miss nothing.",
|
||||
"checklisten.filter.all": "All",
|
||||
"checklisten.filter.de": "DE",
|
||||
"checklisten.empty": "No checklists in this category.",
|
||||
"checklisten.back": "Back to overview",
|
||||
"checklisten.print": "Print",
|
||||
"checklisten.reset": "Reset",
|
||||
"checklisten.reset.confirm": "Really reset all checkboxes for this checklist?",
|
||||
"checklisten.notfound": "Checklist not found.",
|
||||
"checklisten.disclaimer": "Note: These checklists are aides-memoire only and do not replace case-by-case review. The applicable procedural rules are controlling.",
|
||||
"checklisten.feedback.btn": "Feedback",
|
||||
"checklisten.feedback.title": "Checklist feedback",
|
||||
"checklisten.feedback.type": "Type",
|
||||
"checklisten.feedback.error": "Error found",
|
||||
"checklisten.feedback.missing": "Missing item",
|
||||
"checklisten.feedback.suggestion": "Improvement suggestion",
|
||||
"checklisten.feedback.other": "Other",
|
||||
"checklisten.feedback.message": "Message",
|
||||
"checklisten.feedback.submit": "Submit",
|
||||
"checklisten.feedback.cancel": "Cancel",
|
||||
"checklisten.feedback.success": "Thank you for your feedback!",
|
||||
"checklisten.feedback.error.required": "Please enter a message.",
|
||||
"checklisten.feedback.error.generic": "Error submitting. Please try again.",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ const ICON_DOWNLOAD = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor
|
||||
const ICON_LINK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>';
|
||||
const ICON_BOOK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>';
|
||||
const ICON_TABLE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/></svg>';
|
||||
const ICON_CHECK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>';
|
||||
const ICON_GLOBE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10A15.3 15.3 0 0 1 12 2z"/></svg>';
|
||||
const ICON_LOGOUT = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>';
|
||||
const ICON_PIN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4v6l-2 4h10l-2-4V4"/><line x1="12" y1="16" x2="12" y2="21"/><line x1="8" y1="4" x2="16" y2="4"/></svg>';
|
||||
@@ -45,6 +46,7 @@ export function Sidebar({ currentPath }: SidebarProps): string {
|
||||
{navItem("/tools/kostenrechner", ICON_CALC, "nav.kostenrechner", "Kostenrechner", currentPath)}
|
||||
{navItem("/tools/fristenrechner", ICON_CLOCK, "nav.fristenrechner", "Fristenrechner", currentPath)}
|
||||
{navItem("/tools/gebuehrentabellen", ICON_TABLE, "nav.gebuehrentabellen", "Gebührentabellen", currentPath)}
|
||||
{navItem("/checklisten", ICON_CHECK, "nav.checklisten", "Checklisten", currentPath)}
|
||||
{navItem("/glossar", ICON_BOOK, "nav.glossar", "Glossar", currentPath)}
|
||||
{navItem("/downloads", ICON_DOWNLOAD, "nav.downloads", "Downloads", currentPath)}
|
||||
{navItem("/links", ICON_LINK, "nav.links", "Links", currentPath)}
|
||||
|
||||
@@ -10,6 +10,7 @@ const ICON_CLOCK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" s
|
||||
const ICON_DOWNLOAD = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>';
|
||||
const ICON_GLOSSAR = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>';
|
||||
const ICON_TABLE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/></svg>';
|
||||
const ICON_CHECK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>';
|
||||
|
||||
export function renderIndex(): string {
|
||||
return "<!DOCTYPE html>" + (
|
||||
@@ -84,6 +85,12 @@ export function renderIndex(): string {
|
||||
<h2 data-i18n="index.gebuehren.title">Gebührentabellen</h2>
|
||||
<p data-i18n="index.gebuehren.desc">Interaktive Gebührentabellen für GKG, RVG, UPC, EPA und PatKostG. Streitwert eingeben, Gebühr ablesen.</p>
|
||||
</a>
|
||||
|
||||
<a href="/checklisten" className="card card-link">
|
||||
<div className="card-icon" dangerouslySetInnerHTML={{ __html: ICON_CHECK }} />
|
||||
<h2 data-i18n="index.checklisten.title">Checklisten</h2>
|
||||
<p data-i18n="index.checklisten.desc">Interaktive Checklisten für UPC-, DE- und EPA-Verfahren. Fortschritt wird lokal gespeichert.</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -2804,3 +2804,382 @@ input[type="range"]::-moz-range-thumb {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Checklisten --- */
|
||||
|
||||
.checklist-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.checklist-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.checklist-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
padding: 1.1rem 1.2rem 1.2rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-surface);
|
||||
text-decoration: none;
|
||||
color: var(--color-text);
|
||||
transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.checklist-card:hover {
|
||||
border-color: var(--color-accent);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.checklist-card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.checklist-card-count {
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.checklist-card-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.checklist-card-desc {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.checklist-card-court {
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-text-muted);
|
||||
margin: 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.checklist-card-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.checklist-regime {
|
||||
display: inline-block;
|
||||
padding: 0.15rem 0.55rem;
|
||||
border-radius: 99px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
background: rgba(101, 163, 13, 0.12);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.checklist-regime-UPC {
|
||||
background: rgba(101, 163, 13, 0.12);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.checklist-regime-DE {
|
||||
background: rgba(26, 54, 93, 0.08);
|
||||
color: #1b365d;
|
||||
}
|
||||
|
||||
.checklist-regime-EPA {
|
||||
background: rgba(100, 100, 122, 0.12);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.checklist-empty {
|
||||
color: var(--color-text-muted);
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* --- Checklist detail --- */
|
||||
|
||||
.checklist-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: none;
|
||||
margin-bottom: 1rem;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.checklist-back:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.checklist-back-arrow {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.checklist-detail-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.checklist-detail-head-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.checklist-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.55rem 0.9rem;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-muted);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.checklist-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem 1.5rem;
|
||||
margin-top: 0.85rem;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.checklist-meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.checklist-meta-item dt {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--color-text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.checklist-meta-item dd {
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.checklist-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.checklist-progress-bar {
|
||||
flex: 1;
|
||||
height: 0.4rem;
|
||||
background: var(--color-border);
|
||||
border-radius: 99px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.checklist-progress-fill {
|
||||
height: 100%;
|
||||
background: var(--color-accent);
|
||||
border-radius: 99px;
|
||||
transition: width 0.2s ease;
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
.checklist-progress-label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.checklist-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.75rem;
|
||||
}
|
||||
|
||||
.checklist-group-title {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--color-text-muted);
|
||||
padding-bottom: 0.45rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.checklist-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.checklist-item {
|
||||
padding: 0.55rem 0.6rem;
|
||||
border-radius: var(--radius);
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.checklist-item:hover {
|
||||
background: rgba(101, 163, 13, 0.04);
|
||||
}
|
||||
|
||||
.checklist-item.checked .checklist-item-text {
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.checklist-item-label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.7rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checklist-checkbox {
|
||||
width: 1.05rem;
|
||||
height: 1.05rem;
|
||||
margin-top: 0.2rem;
|
||||
accent-color: var(--color-accent);
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checklist-item-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.checklist-item-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.6rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.checklist-item-text {
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.checklist-item-rule {
|
||||
font-size: 0.72rem;
|
||||
padding: 0.1rem 0.45rem;
|
||||
border-radius: 99px;
|
||||
background: rgba(26, 54, 93, 0.06);
|
||||
color: #1b365d;
|
||||
font-family: var(--font-mono);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.checklist-item-note {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.checklist-print-footer {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.checklist-disclaimer {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.checklist-detail-head-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.checklist-actions {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.checklist-back,
|
||||
.checklist-actions,
|
||||
.checklist-filters,
|
||||
.checklist-card-progress {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.checklist-item {
|
||||
break-inside: avoid;
|
||||
border-bottom: 1px dotted #ccc;
|
||||
padding: 0.3rem 0;
|
||||
}
|
||||
|
||||
.checklist-item:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.checklist-checkbox {
|
||||
/* Keep checkbox visible with actual state */
|
||||
accent-color: #000;
|
||||
}
|
||||
|
||||
.checklist-group {
|
||||
break-inside: avoid-page;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.checklist-progress {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.checklist-progress-bar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user