From d39b7ae67cbd5ee1cfb5bd4b7de792719a1b3913 Mon Sep 17 00:00:00 2001 From: claude code agent 227 Date: Sun, 28 Jun 2026 23:38:48 +0300 Subject: [PATCH] refactor(editor): dedupe alt/caption controls via shared hook (F4) Extract the ~110 duplicated lines into one parameterized useImageTextFieldControl and make useAltTextControl/useCaptionControl thin wrappers. Behavior identical; t("...") literals stay in the wrappers so i18n extraction keeps working. sanitizeCaption still exported for its unit test. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../common/use-alt-text-control.tsx | 129 ++-------------- .../components/common/use-caption-control.tsx | 131 ++-------------- .../common/use-image-text-field-control.tsx | 145 ++++++++++++++++++ 3 files changed, 177 insertions(+), 228 deletions(-) create mode 100644 apps/client/src/features/editor/components/common/use-image-text-field-control.tsx diff --git a/apps/client/src/features/editor/components/common/use-alt-text-control.tsx b/apps/client/src/features/editor/components/common/use-alt-text-control.tsx index 1a43f9d7..2f1e8eb5 100644 --- a/apps/client/src/features/editor/components/common/use-alt-text-control.tsx +++ b/apps/client/src/features/editor/components/common/use-alt-text-control.tsx @@ -1,16 +1,7 @@ -import React, { useCallback, useEffect, useState } from "react"; import { Editor } from "@tiptap/react"; -import { - ActionIcon, - Button, - Group, - Paper, - Text, - Textarea, - Tooltip, -} from "@mantine/core"; import { IconAlt } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; +import { useImageTextFieldControl } from "@/features/editor/components/common/use-image-text-field-control.tsx"; const ALT_MAX_LENGTH = 300; @@ -27,113 +18,25 @@ type UseAltTextControlArgs = { currentAlt: string; }; +// Thin wrapper over the shared image text-field popover; see +// useImageTextFieldControl. The t("...") literals stay here so they remain +// statically extractable for i18n. export function useAltTextControl({ editor, nodeName, currentAlt, }: UseAltTextControlArgs) { const { t } = useTranslation(); - const [showInput, setShowInput] = useState(false); - const [draft, setDraft] = useState(""); - - const open = useCallback(() => { - setDraft(currentAlt || ""); - setShowInput(true); - }, [currentAlt]); - - useEffect(() => { - const handler = () => { - if (!editor.isActive(nodeName)) { - setShowInput(false); - } - }; - editor.on("selectionUpdate", handler); - return () => { - editor.off("selectionUpdate", handler); - }; - }, [editor, nodeName]); - - const cancel = useCallback(() => { - setShowInput(false); - }, []); - - const save = useCallback(() => { - editor - .chain() - .focus(undefined, { scrollIntoView: false }) - .updateAttributes(nodeName, { alt: sanitizeAlt(draft) || undefined }) - .run(); - setShowInput(false); - }, [editor, nodeName, draft]); - - const onKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { - e.preventDefault(); - save(); - } else if (e.key === "Escape") { - e.preventDefault(); - cancel(); - } - }, - [save, cancel], - ); - - const button = ( - - - - - - ); - - const panel = showInput ? ( - - - {t("Alt text")} - - - {t("Describe this for accessibility.")} - -