3318fe9e17
Adds a small toolbar next to the comment-count badge with four actions: - Share — uses `navigator.share()` on supporting devices, falls back to copy-link otherwise - Copy link — clipboard API with `<input>` + `execCommand` fallback, flashes a checkmark for 1.5s on success - Print — `window.print()`, hidden via `print:hidden` so it doesn't show up in the printed output itself - Reading time — server-computed from headline+body HTML at ~200 wpm, hidden when 0 Reading time + canonical URL come from `+page.server.ts` so SSR output is identical and shareable URLs render correctly. All labels go through the translations layer (5 new `post_action_*` keys). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
121 lines
3.6 KiB
Svelte
121 lines
3.6 KiB
Svelte
<script lang="ts">
|
|
import Icon from "@iconify/svelte";
|
|
import "$lib/iconify-offline";
|
|
import { useTranslate, T } from "$lib/translations";
|
|
|
|
let {
|
|
url,
|
|
title,
|
|
readingTimeMinutes = null,
|
|
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;
|
|
class?: string;
|
|
} = $props();
|
|
|
|
const t = useTranslate();
|
|
|
|
let copied = $state(false);
|
|
let shared = $state(false);
|
|
let copyTimer: ReturnType<typeof setTimeout> | null = null;
|
|
let shareTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
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 w = window as unknown as { navigator: Navigator & { share?: (data: ShareData) => Promise<void> } };
|
|
if (typeof w.navigator.share === "function") {
|
|
try {
|
|
await w.navigator.share({ title, url });
|
|
flashShared();
|
|
return;
|
|
} catch {
|
|
// user cancelled or share failed → fall through to copy
|
|
}
|
|
}
|
|
await copyLink();
|
|
}
|
|
|
|
function print() {
|
|
window.print();
|
|
}
|
|
</script>
|
|
|
|
<div class="inline-flex flex-wrap items-center gap-x-2 gap-y-1 {cls}">
|
|
<button
|
|
type="button"
|
|
onclick={share}
|
|
class="inline-flex items-center gap-1 text-xs font-medium text-zinc-600 hover:text-zinc-900 transition-colors"
|
|
aria-label={t(T.post_action_share)}
|
|
>
|
|
<Icon icon={shared ? "mdi:check" : "mdi:share-variant-outline"} class="size-4" />
|
|
<span class="hidden sm:inline">{shared ? t(T.post_action_copied) : t(T.post_action_share)}</span>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onclick={copyLink}
|
|
class="inline-flex items-center gap-1 text-xs font-medium text-zinc-600 hover:text-zinc-900 transition-colors"
|
|
aria-label={t(T.post_action_copy)}
|
|
>
|
|
<Icon icon={copied ? "mdi:check" : "mdi:link-variant"} class="size-4" />
|
|
<span class="hidden sm:inline">{copied ? t(T.post_action_copied) : t(T.post_action_copy)}</span>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onclick={print}
|
|
class="inline-flex items-center gap-1 text-xs font-medium text-zinc-600 hover:text-zinc-900 transition-colors print:hidden"
|
|
aria-label={t(T.post_action_print)}
|
|
>
|
|
<Icon icon="mdi:printer-outline" class="size-4" />
|
|
<span class="hidden sm:inline">{t(T.post_action_print)}</span>
|
|
</button>
|
|
|
|
{#if readingTimeMinutes != null && readingTimeMinutes > 0}
|
|
<span
|
|
class="inline-flex items-center gap-1 text-xs text-zinc-500"
|
|
title={t(T.post_action_reading_time, { minutes: readingTimeMinutes })}
|
|
>
|
|
<Icon icon="mdi:clock-outline" class="size-4" />
|
|
<span class="tabular-nums">{t(T.post_action_reading_time, { minutes: readingTimeMinutes })}</span>
|
|
</span>
|
|
{/if}
|
|
</div>
|