mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(coach): keep the message actions usable on touch and HTTP hosts
Hide the copy button on insecure-context (plain-HTTP) self-hosts where the Clipboard API is undefined: a new SSR-safe useClipboardSupported() detector mirrors the speech-synthesis hook so the button is absent rather than error-toasting on tap. Stop the per-message action row compressing below the 44px touch floor on narrow phones: the icon buttons keep their tap size (shrink-0 + min-w-11) and the row wraps instead of shrinking the up-to-six targets to ~30px.
This commit is contained in:
1 parent
802ea1c60a
commit
6e2bf34487
1 file changed
+26
-4
@@ -391,6 +391,24 @@ function useSpeechSynthesisSupported(): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.22.1 — feature-detect the async Clipboard API SSR-safe, mirroring the
|
||||
* Speech-Synthesis detection above. `navigator.clipboard` is `undefined` on
|
||||
* plain-HTTP self-hosts (a supported insecure-context config), so the copy
|
||||
* button must be absent there rather than rendering and error-toasting on tap.
|
||||
* `useSyncExternalStore` returns false on the server + first client paint and
|
||||
* resolves to the real capability after hydration — no hydration mismatch.
|
||||
*/
|
||||
function useClipboardSupported(): boolean {
|
||||
return useSyncExternalStore(
|
||||
() => () => {},
|
||||
() =>
|
||||
typeof navigator !== "undefined" &&
|
||||
typeof navigator.clipboard?.writeText === "function",
|
||||
() => false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.22 — score a synthesis voice for a target language. Higher is better;
|
||||
* a negative score marks a legacy/compact voice we would rather skip. Pure
|
||||
@@ -566,8 +584,8 @@ function useReadAloud(): {
|
||||
*/
|
||||
const COACH_ICON_BUTTON = cn(
|
||||
"text-muted-foreground hover:text-foreground focus-visible:ring-ring/50",
|
||||
"inline-flex size-11 items-center justify-center rounded outline-none",
|
||||
"focus-visible:ring-2 disabled:opacity-50 sm:size-8",
|
||||
"inline-flex size-11 min-w-11 shrink-0 items-center justify-center rounded",
|
||||
"outline-none focus-visible:ring-2 disabled:opacity-50 sm:size-8 sm:min-w-8",
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -615,6 +633,7 @@ function CopyMessageButton({
|
||||
strip: boolean;
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
const supported = useClipboardSupported();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const handle = useCallback(async () => {
|
||||
const text = strip ? stripChartTokens(content) : content;
|
||||
@@ -626,6 +645,9 @@ function CopyMessageButton({
|
||||
toast.error(t("insights.coach.copyMessageError"));
|
||||
}
|
||||
}, [content, strip, t]);
|
||||
// Absent on insecure-context (plain-HTTP) self-hosts where the Clipboard API
|
||||
// is undefined — render nothing rather than error-toast on tap.
|
||||
if (!supported) return null;
|
||||
const label = t("insights.coach.copyMessage");
|
||||
return (
|
||||
<button
|
||||
@@ -685,7 +707,7 @@ function BubbleTimestamp({
|
||||
const [open, setOpen] = useState(false);
|
||||
const label = formatters.dateTime(iso);
|
||||
return (
|
||||
<span className="relative inline-flex">
|
||||
<span className="relative inline-flex shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
data-slot="coach-bubble-timestamp"
|
||||
@@ -1446,7 +1468,7 @@ function ChatBubble({
|
||||
<div
|
||||
data-slot="coach-bubble-actions"
|
||||
className={cn(
|
||||
"flex items-center gap-0.5",
|
||||
"flex flex-wrap items-center gap-0.5",
|
||||
"sm:[@media(hover:hover)]:opacity-0",
|
||||
"sm:[@media(hover:hover)]:group-hover/assistant-bubble:opacity-100",
|
||||
"sm:[@media(hover:hover)]:group-focus-within/assistant-bubble:opacity-100",
|
||||
|
||||
Reference in new issue
Block a user