style(post): polish post header + actions toolbar
- PostActions: every item is a rounded-full pill with subtle border; the three icon-only actions (share/copy/print) cluster in one segmented control with divider lines, count + reading-time stand alone left and right - Share button only renders when `navigator.share` is available so desktop doesn't get a duplicate of "Copy link" - All action buttons drop their inline labels and use `title=` + `aria-label` for tooltip-only access; copy still flips icon to a checkmark for 1.5s on success - Empty comment state stacks vertically on mobile (icon above), goes horizontal + left-aligned on ≥sm - Post header rebuilt: image fixed at 150x150 (no more mobile blow-up), right column is `flex flex-col gap-3` so date/tags/actions share one rhythm instead of ad-hoc margins - Reading-time now also walks `row*Content` and pulls `content`/`body`/ `text` strings — Termin-Posts with empty body but markdown rows now show a meaningful estimate Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -237,10 +237,36 @@ export const load: PageServerLoad = async ({ params, locals, url, setHeaders })
|
||||
}
|
||||
|
||||
// Reading-time estimate. ~200 words per minute for German non-fiction.
|
||||
// Strips HTML tags, then counts whitespace-separated tokens. Includes only
|
||||
// the prose body (headline + content); rows aren't part of the prose flow.
|
||||
// Pulls text from headline + body markdown AND from referenced row blocks
|
||||
// (Termin-Posts have an empty body; their text lives in row1Content
|
||||
// markdown/text references). Walks the post tree once, collecting any
|
||||
// `content` / `body` / `text` string fields below row*Content. HTML tags
|
||||
// get stripped before the word count.
|
||||
const readingTimeMinutes = (() => {
|
||||
const plain = (contentHeadlineHtml + " " + contentBodyHtml)
|
||||
const parts: string[] = [contentHeadlineHtml, contentBodyHtml];
|
||||
const collect = (node: unknown) => {
|
||||
if (!node || typeof node !== "object") return;
|
||||
if (Array.isArray(node)) {
|
||||
for (const item of node) collect(item);
|
||||
return;
|
||||
}
|
||||
const obj = node as Record<string, unknown>;
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (typeof value === "string") {
|
||||
if (key === "content" || key === "body" || key === "text") {
|
||||
parts.push(value);
|
||||
}
|
||||
} else {
|
||||
collect(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
collect((post as { row1Content?: unknown }).row1Content);
|
||||
collect((post as { row2Content?: unknown }).row2Content);
|
||||
collect((post as { row3Content?: unknown }).row3Content);
|
||||
|
||||
const plain = parts
|
||||
.join(" ")
|
||||
.replace(/<[^>]+>/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
|
||||
@@ -24,10 +24,10 @@
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<div class="md:flex gap-2 items-end">
|
||||
<div class="my-4 md:flex md:items-start md:gap-4">
|
||||
{#if data.rawPostImageUrl}
|
||||
<div
|
||||
class="overflow-hidden inline-block my-4 border border-white self-start ring-1 ring-black/50 shadow-lg aspect-square"
|
||||
class="w-[150px] shrink-0 overflow-hidden border border-white ring-1 ring-black/50 shadow-lg aspect-square"
|
||||
>
|
||||
<RustyImage
|
||||
src={data.rawPostImageUrl}
|
||||
@@ -44,7 +44,7 @@
|
||||
</div>
|
||||
{:else if data.postImageUrl}
|
||||
<div
|
||||
class="overflow-hidden inline-block my-4 border border-white self-start ring-1 ring-black/50 shadow-lg aspect-square"
|
||||
class="w-[150px] shrink-0 overflow-hidden border border-white ring-1 ring-black/50 shadow-lg aspect-square"
|
||||
>
|
||||
<img
|
||||
src={data.postImageUrl}
|
||||
@@ -57,34 +57,32 @@
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-2 mb-4! md:mb-1 min-w-0 gap-2">
|
||||
{#if data.postDate || data.commentPageId}
|
||||
<div class="flex flex-nowrap gap-2 items-center shrink-0 mb-2">
|
||||
{#if data.postDate}
|
||||
<Tag label={data.postDate} variant="date" />
|
||||
{/if}
|
||||
|
||||
<!-- Right column: stacked rows with consistent rhythm -->
|
||||
<div class="mt-3 md:mt-0 min-w-0 flex flex-col gap-3">
|
||||
{#if data.postDate}
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Tag label={data.postDate} variant="date" />
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="flex flex-nowrap gap-2 items-center shrink-0 p-1 pb-4 -ml-1 overflow-x-auto overflow-y-hidden"
|
||||
>
|
||||
<Tags
|
||||
tags={data.post.postTag ?? []}
|
||||
variant="green"
|
||||
tagBase="/posts/tag"
|
||||
/>
|
||||
</div>
|
||||
{#if (data.post.postTag ?? []).length > 0}
|
||||
<div class="flex flex-wrap items-center gap-1.5 -ml-0.5">
|
||||
<Tags
|
||||
tags={data.post.postTag ?? []}
|
||||
variant="green"
|
||||
tagBase="/posts/tag"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<PostActions
|
||||
url={data.canonicalUrl}
|
||||
title={data.seoTitle}
|
||||
readingTimeMinutes={data.readingTimeMinutes}
|
||||
commentPageId={data.commentPageId}
|
||||
commentTargetId="comments-section"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-zinc-600">
|
||||
<PostActions
|
||||
url={data.canonicalUrl}
|
||||
title={data.seoTitle}
|
||||
readingTimeMinutes={data.readingTimeMinutes}
|
||||
commentPageId={data.commentPageId}
|
||||
commentTargetId="comments-section"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<article
|
||||
class="mt-8 h-entry"
|
||||
|
||||
Reference in New Issue
Block a user