Files
windwiderstand/src/lib/components/PostActions.svelte
T
Peter Meier b7093b703a
Deploy / verify (push) Successful in 49s
Deploy / deploy (push) Successful in 1m0s
style(design): consistency sweep + UX upgrades
- Palette: gray/zinc/slate/neutral → stein, red → fire, amber → erde,
  sky → himmel, green → wald
- Footer: replaced raw rgb with stein tokens
- Radius: unified to rounded-xs across cards, buttons, inputs, pagination
- Buttons: .btn-primary/.btn-secondary now font-medium; added secondary
- Card hover: -translate-y-0.5 + shadow + wald-300 border
- Header active link: wald border-bottom + wald-700 text
- Mobile nav: bigger touch targets + wald active accent
- Pagination: prev/next text labels + wald-500 active state with
  separated shape/color classes to avoid Tailwind conflicts
- Lead paragraph: scoped .markdown > p:first-child for top-level
  MarkdownBlock only via [data-block-type="markdown"]
- Section dividers: subtle wald-200 gradient between content rows
- Global focus-visible ring (wald-500)
- Inline hex → palette tokens (Badge amber, Tag custom-color contrast)
- Font weights snapped to design system (300/400/500/600/700)
- transition-all → transition-colors where only color changes
- Removed em-dashes from user-visible templates

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 01:06:22 +02:00

211 lines
6.6 KiB
Svelte

<script lang="ts">
import Icon from "@iconify/svelte";
import "$lib/iconify-offline";
import { env } from "$env/dynamic/public";
import { useTranslate, T } from "$lib/translations";
let {
url,
title,
readingTimeMinutes = null,
commentPageId = null,
commentEnvironment,
commentTargetId = "comments-section",
class: cls = "",
}: {
/** Absolute URL of the page being shared. */
url: string;
/** Page headline — used by the native share-sheet. */
title: string;
/** Estimated reading time. `null` hides the badge. */
readingTimeMinutes?: number | null;
/** Page id for the comment plugin. `null` hides the count badge. */
commentPageId?: string | null;
/** Optional space override for the comment count fetch. */
commentEnvironment?: string;
/** DOM id of the comment accordion (used by the count-badge click). */
commentTargetId?: string;
class?: string;
} = $props();
const t = useTranslate();
// ── Comment count fetch (only when commentPageId is set) ────────────────
let commentCount = $state<number | null>(null);
const cmsBase = (
env.PUBLIC_CMS_URL ||
import.meta.env.PUBLIC_CMS_URL ||
"http://localhost:3000"
).replace(/\/$/, "");
$effect(() => {
if (!commentPageId) {
commentCount = null;
return;
}
const fetchUrl = `${cmsBase}/api/comments/${encodeURIComponent(commentPageId)}/count${
commentEnvironment
? `?_environment=${encodeURIComponent(commentEnvironment)}`
: ""
}`;
let cancelled = false;
fetch(fetchUrl)
.then((r) => r.json())
.then((d) => {
if (cancelled) return;
commentCount = typeof d?.count === "number" ? d.count : 0;
})
.catch(() => {
if (!cancelled) commentCount = 0;
});
return () => {
cancelled = true;
};
});
function onCommentClick(e: MouseEvent) {
const el = document.getElementById(
commentTargetId,
) as HTMLDetailsElement | null;
if (!el) return;
e.preventDefault();
el.open = true;
el.scrollIntoView({ behavior: "smooth", block: "start" });
}
let copied = $state(false);
let shared = $state(false);
let copyTimer: ReturnType<typeof setTimeout> | null = null;
let shareTimer: ReturnType<typeof setTimeout> | null = null;
// Share button only renders when the browser actually has a native share-
// sheet — otherwise it would just be a duplicate of "Link kopieren".
// SSR has no `navigator`, so this stays false until client mount.
let canShare = $state(false);
$effect(() => {
canShare =
typeof navigator !== "undefined" &&
typeof (navigator as Navigator & { share?: unknown }).share ===
"function";
});
function flashCopied() {
copied = true;
if (copyTimer) clearTimeout(copyTimer);
copyTimer = setTimeout(() => (copied = false), 1500);
}
function flashShared() {
shared = true;
if (shareTimer) clearTimeout(shareTimer);
shareTimer = setTimeout(() => (shared = false), 1500);
}
async function copyLink() {
try {
await navigator.clipboard.writeText(url);
flashCopied();
} catch {
// Fallback: temporary <input> + execCommand for older browsers.
const el = document.createElement("input");
el.value = url;
document.body.appendChild(el);
el.select();
try {
document.execCommand("copy");
flashCopied();
} catch {
// give up — clipboard not available
} finally {
el.remove();
}
}
}
async function share() {
const nav = navigator as Navigator & {
share?: (data: ShareData) => Promise<void>;
};
if (typeof nav.share !== "function") return;
try {
await nav.share({ title, url });
flashShared();
} catch {
// user cancelled — no fallback, copy-link button covers that case
}
}
function print() {
window.print();
}
</script>
<!--
Post-actions toolbar. Each item is a rounded-full pill with a subtle
border so they read as visually equal "badges". Order:
[info badges] | [action buttons]
i.e. info-style items (count, reading time) stand left/right, the
three icon-only actions cluster in the middle.
-->
<div class="inline-flex flex-wrap items-center gap-2 {cls}">
{#if commentPageId}
<a
href="#{commentTargetId}"
onclick={onCommentClick}
aria-label={t(T.comments_jump_to)}
title={t(T.comments_jump_to)}
class="no-underline! inline-flex items-center gap-1 rounded-full border border-stein-200 bg-white px-2.5 py-1 text-xs font-medium text-stein-700! hover:bg-stein-50 hover:border-stein-300 transition-colors"
>
<Icon icon="mdi:comment-outline" class="size-3.5" />
<span class="tabular-nums">{commentCount ?? "…"}</span>
</a>
{/if}
<div class="inline-flex items-center rounded-full border border-stein-200 bg-white overflow-hidden divide-x divide-stein-200 print:hidden">
{#if canShare}
<button
type="button"
onclick={share}
class="inline-flex items-center px-2.5 py-1 text-stein-600 hover:bg-stein-50 hover:text-stein-900 transition-colors"
aria-label={t(T.post_action_share)}
title={t(T.post_action_share)}
>
<Icon
icon={shared ? "mdi:check" : "mdi:share-variant-outline"}
class="size-3.5"
/>
</button>
{/if}
<button
type="button"
onclick={copyLink}
class="inline-flex items-center px-2.5 py-1 text-stein-600 hover:bg-stein-50 hover:text-stein-900 transition-colors"
aria-label={copied ? t(T.post_action_copied) : t(T.post_action_copy)}
title={copied ? t(T.post_action_copied) : t(T.post_action_copy)}
>
<Icon icon={copied ? "mdi:check" : "mdi:link-variant"} class="size-3.5" />
</button>
<button
type="button"
onclick={print}
class="inline-flex items-center px-2.5 py-1 text-stein-600 hover:bg-stein-50 hover:text-stein-900 transition-colors"
aria-label={t(T.post_action_print)}
title={t(T.post_action_print)}
>
<Icon icon="mdi:printer-outline" class="size-3.5" />
</button>
</div>
{#if readingTimeMinutes != null && readingTimeMinutes > 0}
<span
class="inline-flex items-center gap-1 rounded-full border border-stein-200 bg-white px-2.5 py-1 text-xs text-stein-500"
title={t(T.post_action_reading_time, { minutes: readingTimeMinutes })}
>
<Icon icon="mdi:clock-outline" class="size-3.5" />
<span class="tabular-nums"
>{t(T.post_action_reading_time, { minutes: readingTimeMinutes })}</span
>
</span>
{/if}
</div>