LENZ BRIEFAUSGABE

This commit is contained in:
Simon Martens
2025-11-17 14:59:32 +01:00
parent 6901989292
commit 938cdeb27b
11 changed files with 842 additions and 413 deletions

View File

@@ -11,7 +11,10 @@
--color-blue-900: oklch(37.9% 0.146 265.522);
--color-blue-950: oklch(28.2% 0.091 267.935);
--color-slate-50: oklch(98.4% 0.003 247.858);
--color-slate-100: oklch(96.8% 0.007 247.896);
--color-slate-200: oklch(92.9% 0.013 255.508);
--color-slate-400: oklch(70.4% 0.04 256.788);
--color-slate-500: oklch(55.4% 0.046 257.417);
--color-slate-600: oklch(44.6% 0.043 257.281);
--color-slate-700: oklch(37.2% 0.044 257.287);
--color-slate-800: oklch(27.9% 0.041 260.031);
@@ -43,10 +46,13 @@
--text-2xl--line-height: calc(2 / 1.5);
--text-6xl: 3.75rem;
--text-6xl--line-height: 1;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--tracking-wide: 0.025em;
--leading-normal: 1.5;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
@@ -211,9 +217,23 @@
.collapse {
visibility: collapse;
}
.invisible {
visibility: hidden;
}
.visible {
visibility: visible;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border-width: 0;
}
.absolute {
position: absolute;
}
@@ -423,6 +443,9 @@
.h-full {
height: 100%;
}
.max-h-64 {
max-height: calc(var(--spacing) * 64);
}
.min-h-screen {
min-height: 100vh;
}
@@ -435,9 +458,15 @@
.max-w-none {
max-width: none;
}
.flex-1 {
flex: 1;
}
.flex-shrink {
flex-shrink: 1;
}
.shrink-0 {
flex-shrink: 0;
}
.flex-grow {
flex-grow: 1;
}
@@ -481,9 +510,53 @@
.flex-wrap {
flex-wrap: wrap;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.gap-2 {
gap: calc(var(--spacing) * 2);
}
.gap-8 {
gap: calc(var(--spacing) * 8);
}
.space-y-1 {
:where(& > :not(:last-child)) {
--tw-space-y-reverse: 0;
margin-block-start: calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));
margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)));
}
}
.space-y-1\.5 {
:where(& > :not(:last-child)) {
--tw-space-y-reverse: 0;
margin-block-start: calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));
margin-block-end: calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)));
}
}
.space-y-2 {
:where(& > :not(:last-child)) {
--tw-space-y-reverse: 0;
margin-block-start: calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));
margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)));
}
}
.space-y-4 {
:where(& > :not(:last-child)) {
--tw-space-y-reverse: 0;
margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));
margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)));
}
}
.space-y-5 {
:where(& > :not(:last-child)) {
--tw-space-y-reverse: 0;
margin-block-start: calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));
margin-block-end: calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)));
}
}
.space-y-6 {
:where(& > :not(:last-child)) {
--tw-space-y-reverse: 0;
@@ -504,6 +577,20 @@
.gap-x-1\.5 {
column-gap: calc(var(--spacing) * 1.5);
}
.divide-y {
:where(& > :not(:last-child)) {
--tw-divide-y-reverse: 0;
border-bottom-style: var(--tw-border-style);
border-top-style: var(--tw-border-style);
border-top-width: calc(1px * var(--tw-divide-y-reverse));
border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
}
}
.divide-slate-100 {
:where(& > :not(:last-child)) {
border-color: var(--color-slate-100);
}
}
.self-end {
align-self: flex-end;
}
@@ -513,6 +600,9 @@
.justify-self-end {
justify-self: flex-end;
}
.overflow-y-auto {
overflow-y: auto;
}
.rounded {
border-radius: 0.25rem;
}
@@ -542,6 +632,15 @@
.border-gray-300 {
border-color: var(--color-gray-300);
}
.border-slate-100 {
border-color: var(--color-slate-100);
}
.border-slate-200 {
border-color: var(--color-slate-200);
}
.border-slate-900 {
border-color: var(--color-slate-900);
}
.border-t-gray-200 {
border-top-color: var(--color-gray-200);
}
@@ -551,18 +650,30 @@
.bg-gray-900 {
background-color: var(--color-gray-900);
}
.bg-slate-900 {
background-color: var(--color-slate-900);
}
.bg-white {
background-color: var(--color-white);
}
.mask-repeat {
mask-repeat: repeat;
}
.p-2 {
padding: calc(var(--spacing) * 2);
}
.p-3 {
padding: calc(var(--spacing) * 3);
}
.px-0\.5 {
padding-inline: calc(var(--spacing) * 0.5);
}
.px-2 {
padding-inline: calc(var(--spacing) * 2);
}
.px-3 {
padding-inline: calc(var(--spacing) * 3);
}
.px-8 {
padding-inline: calc(var(--spacing) * 8);
}
@@ -581,6 +692,9 @@
.pb-0 {
padding-bottom: calc(var(--spacing) * 0);
}
.pb-1 {
padding-bottom: calc(var(--spacing) * 1);
}
.pb-1\.5 {
padding-bottom: calc(var(--spacing) * 1.5);
}
@@ -593,6 +707,9 @@
.text-justify {
text-align: justify;
}
.text-left {
text-align: left;
}
.align-bottom {
vertical-align: bottom;
}
@@ -632,6 +749,19 @@
font-size: var(--text-xl);
line-height: var(--tw-leading, var(--text-xl--line-height));
}
.text-xs {
font-size: var(--text-xs);
line-height: var(--tw-leading, var(--text-xs--line-height));
}
.text-\[0\.7rem\] {
font-size: 0.7rem;
}
.text-\[0\.65rem\] {
font-size: 0.65rem;
}
.text-\[0\.75rem\] {
font-size: 0.75rem;
}
.leading-none {
--tw-leading: 1;
line-height: 1;
@@ -640,10 +770,18 @@
--tw-font-weight: var(--font-weight-bold);
font-weight: var(--font-weight-bold);
}
.font-medium {
--tw-font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-medium);
}
.font-semibold {
--tw-font-weight: var(--font-weight-semibold);
font-weight: var(--font-weight-semibold);
}
.tracking-wide {
--tw-tracking: var(--tracking-wide);
letter-spacing: var(--tracking-wide);
}
.text-wrap {
text-wrap: wrap;
}
@@ -669,6 +807,15 @@
.text-gray-800 {
color: var(--color-gray-800);
}
.text-slate-500 {
color: var(--color-slate-500);
}
.text-slate-600 {
color: var(--color-slate-600);
}
.text-slate-700 {
color: var(--color-slate-700);
}
.text-white {
color: var(--color-white);
}
@@ -733,9 +880,17 @@
-webkit-font-smoothing: auto;
-moz-osx-font-smoothing: auto;
}
.placeholder-slate-400 {
&::placeholder {
color: var(--color-slate-400);
}
}
.opacity-0 {
opacity: 0%;
}
.opacity-70 {
opacity: 70%;
}
.opacity-100 {
opacity: 100%;
}
@@ -797,6 +952,20 @@
-webkit-user-select: all;
user-select: all;
}
.hover\:border-slate-400 {
&:hover {
@media (hover: hover) {
border-color: var(--color-slate-400);
}
}
}
.hover\:bg-slate-100 {
&:hover {
@media (hover: hover) {
background-color: var(--color-slate-100);
}
}
}
.hover\:text-slate-900 {
&:hover {
@media (hover: hover) {
@@ -811,6 +980,38 @@
}
}
}
.focus\:border-slate-400 {
&:focus {
border-color: var(--color-slate-400);
}
}
.focus\:ring-1 {
&:focus {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
}
.focus\:ring-slate-400 {
&:focus {
--tw-ring-color: var(--color-slate-400);
}
}
.focus\:outline-none {
&:focus {
--tw-outline-style: none;
outline-style: none;
}
}
.lg\:w-72 {
@media (width >= 64rem) {
width: calc(var(--spacing) * 72);
}
}
.lg\:flex-row {
@media (width >= 64rem) {
flex-direction: row;
}
}
.print\:hidden {
@media print {
display: none;
@@ -1293,7 +1494,8 @@
line-height: var(--tw-leading, var(--text-sm--line-height));
color: var(--color-slate-800);
}
.text .page {
.text .count .page,
.text .notes .page {
visibility: hidden;
font-family: var(--font-sans);
font-size: var(--text-sm);
@@ -1392,6 +1594,11 @@
inherits: false;
initial-value: 0;
}
@property --tw-divide-y-reverse {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-border-style {
syntax: "*";
inherits: false;
@@ -1405,6 +1612,10 @@
syntax: "*";
inherits: false;
}
@property --tw-tracking {
syntax: "*";
inherits: false;
}
@property --tw-ordinal {
syntax: "*";
inherits: false;
@@ -1604,9 +1815,11 @@
--tw-skew-x: initial;
--tw-skew-y: initial;
--tw-space-y-reverse: 0;
--tw-divide-y-reverse: 0;
--tw-border-style: solid;
--tw-leading: initial;
--tw-font-weight: initial;
--tw-tracking: initial;
--tw-ordinal: initial;
--tw-slashed-zero: initial;
--tw-numeric-figure: initial;

View File

@@ -30,7 +30,26 @@
</div>
<div class="text flex flex-row print:flex-col">
{{- Safe $model.text -}}
<div class="count">
{{- Safe $model.text.count -}}
</div>
<div class="fulltext">
{{- if $model.text.pages }}
{{- range $page := $model.text.pages }}
<div
class="page"
data-page-index="{{ $page.Index }}"
data-starts-inline="{{ if $page.StartsInline }}true{{ else }}false{{ end }}">
{{- $page.HTML | Safe -}}
</div>
{{- end -}}
{{- else }}
{{- Safe $model.text.html -}}
{{- end -}}
</div>
<div class="notes">
{{- Safe $model.text.notes -}}
</div>
</div>
<div class="traditions mt-12 pt-3 border-t-gray-200 border-t-1 max-w-[90ch] print:border-none">

View File

@@ -1,45 +1,194 @@
{{ $model := . }}
{{ $query := $model.query }}
{{ $personJSON := Safe .personJSON }}
{{ $placeJSON := Safe .placeJSON }}
<nav class="print:hidden">
{{- range $i, $range := .ranges -}}
{{- if $range.Letters -}}
<a
class="inline-block stdlink px-0.5"
href="/briefe?range={{ $range.Label }}"
{{ if eq $model.selectedRange $i -}}aria-current="page"{{- end }}
>{{ $range.Label }}</a
>
{{- end -}}
{{- end -}}
<a
class="inline-block stdlink px-0.5"
href="/briefe?range=all"
{{ if .all -}}aria-current="page"{{- end }}
>Alle</a
>
</nav>
{{- if .ranges -}}
{{- if ne .selectedRange -1 -}}
{{- $selectedRangeData := index .ranges .selectedRange -}}
<div class="flex flex-row gap-x-1">
<div>Briefe für {{ $selectedRangeData.Label }}</div>
<div>({{ len $selectedRangeData.Letters }})</div>
<div class="flex flex-col gap-8 lg:flex-row">
<aside class="lg:w-72 shrink-0 space-y-5 print:hidden font-sans">
<nav class="border border-slate-200 rounded bg-white shadow p-3 space-y-2">
<div class="text-[0.75rem] font-semibold uppercase tracking-wide text-slate-600">Jahre</div>
<ul class="space-y-1 text-xs font-medium text-slate-700">
{{- range $i, $range := .ranges -}}
{{- if $range.Letters -}}
<li>
<a
class="flex items-center justify-between rounded border px-2 py-1 transition {{ if eq $model.selectedRange $i }}border-slate-900 bg-slate-900 text-white{{ else }}border-slate-200 hover:border-slate-400{{ end }}"
href="/briefe?range={{ $range.Label }}{{ $query }}">
<span>{{ $range.Label }}</span>
<span class="text-[0.65rem] opacity-70">{{ len $range.Letters }}</span>
</a>
</li>
{{- end -}}
{{- end -}}
<li>
<a
class="flex items-center justify-between rounded border px-2 py-1 transition {{ if .all }}border-slate-900 bg-slate-900 text-white{{ else }}border-slate-200 hover:border-slate-400{{ end }}"
href="/briefe?range=all{{ $query }}">
<span>Alle</span>
</a>
</li>
</ul>
</nav>
<div class="space-y-4">
<filter-list
class="block"
data-title="Personen"
data-param="person"
data-active="{{ .person }}"
data-items='{{ $personJSON }}'>
<div class="text-sm text-slate-500">Filter werden geladen …</div>
</filter-list>
<filter-list
class="block"
data-title="Orte"
data-param="ort"
data-active="{{ .ort }}"
data-items='{{ $placeJSON }}'>
<div class="text-sm text-slate-500">Filter werden geladen …</div>
</filter-list>
</div>
{{ template "_letterlist" $selectedRangeData.Letters -}}
{{- end -}}
</aside>
{{- if .all -}}
{{- range $range := .ranges -}}
{{- if $range.Letters -}}
<div class="mb-8">
<div class="flex flex-row gap-x-1 mb-4">
<div class="font-semibold">{{ $range.Label }}</div>
<div>({{ len $range.Letters }})</div>
</div>
{{ template "_letterlist" $range.Letters -}}
<section class="flex-1">
{{- if .ranges -}}
{{- if ne .selectedRange -1 -}}
{{- $selectedRangeData := index .ranges .selectedRange -}}
<div class="flex flex-row gap-x-1">
<div>Briefe für {{ $selectedRangeData.Label }}</div>
<div>({{ len $selectedRangeData.Letters }})</div>
</div>
{{ template "_letterlist" $selectedRangeData.Letters -}}
{{- end -}}
{{- if .all -}}
{{- range $range := .ranges -}}
{{- if $range.Letters -}}
<div class="mb-8">
<div class="flex flex-row gap-x-1 mb-4">
<div class="font-semibold">{{ $range.Label }}</div>
<div>({{ len $range.Letters }})</div>
</div>
{{ template "_letterlist" $range.Letters -}}
</div>
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
</section>
</div>
<script type="module">
(() => {
if (customElements.get("filter-list")) {
return;
}
class FilterList extends HTMLElement {
connectedCallback() {
if (this._init) {
return;
}
this._init = true;
this.param = this.dataset.param || "";
this.title = this.dataset.title || "";
this.activeValue = this.dataset.active || "";
try {
this.items = JSON.parse(this.dataset.items || "[]");
} catch (err) {
console.error("filter-list: failed to parse dataset items", err);
this.items = [];
}
this.render();
}
render() {
this.innerHTML = `
<div class="border border-slate-200 rounded bg-white shadow p-3 space-y-1.5 font-sans">
<div class="flex items-center justify-between gap-2 pb-1 border-b border-slate-100">
<h3 class="text-[0.75rem] font-semibold uppercase tracking-wide text-slate-600">${this.title}</h3>
<button type="button" data-role="clear" class="text-[0.7rem] text-slate-500 hover:text-slate-900 ${this.activeValue ? "" : "invisible"}">Zurücksetzen</button>
</div>
<div>
<label class="sr-only" for="${this.param}-filter-input">Filter ${this.title}</label>
<input
id="${this.param}-filter-input"
type="search"
placeholder="Suchen"
class="w-full rounded border border-slate-200 bg-white px-3 py-1 text-xs text-slate-700 placeholder-slate-400 focus:border-slate-400 focus:outline-none focus:ring-1 focus:ring-slate-400"/>
</div>
<ul class="max-h-64 overflow-y-auto divide-y divide-slate-100 text-[0.7rem]" data-role="list"></ul>
<p class="text-xs text-slate-500 hidden" data-role="empty">Keine Treffer</p>
</div>
`;
this.list = this.querySelector('[data-role="list"]');
this.empty = this.querySelector('[data-role="empty"]');
this.input = this.querySelector('input[type="search"]');
this.clearButton = this.querySelector('[data-role="clear"]');
this.input?.addEventListener("input", () => this.update());
this.clearButton?.addEventListener("click", () => {
this.activeValue = "";
this.navigate();
});
this.update();
}
update() {
if (!this.list) {
return;
}
const query = (this.input?.value || "").trim().toLowerCase();
const results = this.items.filter((item) => item.name?.toLowerCase().includes(query));
this.list.innerHTML = "";
results.forEach((item) => {
const li = document.createElement("li");
li.className = "";
const btn = document.createElement("button");
btn.type = "button";
const isActive = String(item.id) === String(this.activeValue);
btn.className = `w-full text-left px-2 py-1 rounded transition ${
isActive
? "bg-slate-900 text-white"
: "text-slate-700 hover:bg-slate-100"
}`;
btn.textContent = item.name;
btn.addEventListener("click", () => {
this.activeValue = isActive ? "" : String(item.id);
this.navigate();
});
li.appendChild(btn);
this.list.appendChild(li);
});
if (this.empty) {
this.empty.classList.toggle("hidden", results.length > 0);
}
}
navigate() {
if (!this.param) {
return;
}
const url = new URL(window.location.href);
const otherParam = this.param === "person" ? "ort" : "person";
url.searchParams.delete(otherParam);
if (this.activeValue) {
url.searchParams.set(this.param, this.activeValue);
url.searchParams.set("range", "all");
} else {
url.searchParams.delete(this.param);
}
window.location.href = url.pathname + url.search;
}
}
customElements.define("filter-list", FilterList);
})();
</script>

View File

@@ -405,7 +405,8 @@
@apply text-sm text-slate-800;
}
.text .page {
.text .count .page,
.text .notes .page {
@apply font-sans text-sm text-slate-600 invisible;
}