From fd9a7ee5fb06227dd94108a9520f031b1aadda20 Mon Sep 17 00:00:00 2001 From: appdevelopsk Date: Wed, 9 Sep 2026 19:20:53 +0900 Subject: [PATCH] =?UTF-8?q?feat(toolify):=20=E5=85=A8=E3=83=84=E3=83=BC?= =?UTF-8?q?=E3=83=AB=E3=81=A7=E7=B5=90=E6=9E=9C=20permalink=20=E3=82=92?= =?UTF-8?q?=E6=9C=89=E5=8A=B9=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ?s= による結果共有はこれまで useShareableState を自前で呼ぶ 7 本だけの 対応だった。残り 216 本に個別の restore コールバックを書くのは現実的でなく、 実装差異による取りこぼしも避けられないため、DOM を総なめして直列化する 汎用フックを ToolInteractionTracker と同じ「バブリングを1箇所で拾う」方式で 追加する。ShareBar は既に ?s= を監視しているので無変更で追従する。 設計上の要点: - 構造キー(type + 同種内連番)。素の通し番号だと mode で表示本数が変わる ツール 35 本で復元先が 1 つずれ、別の欄に値が入る。 - 2 パス復元。select/radio/モードボタンを先に当て、rAF で現れた欄を埋める。 - ネイティブ setter。React 19 の制御コンポーネントは el.value 直接代入を 無視する(value tracker が「変化なし」と判断し onChange が出ない)。 - モード切替が button のツール向けに、選択中(bg-brand-600)の index を b キーで保存し復元時に click し直す。 自前実装の 7 本は MANUAL_SHARE_SLUGS で除外し、?s= の二重書き込みを防ぐ。 リストが腐らないよう、実際に useShareableState を import しているツールと 一致することをテストで固定する。 検証: typecheck clean / vitest 26/26 / CDP e2e 10/10 / ランダム 30 本スイープ。 Claude-Session: https://claude.ai/code/session_015fXFezC8ABkuZga4497Jw2 --- site/src/components/tools/ToolFrame.tsx | 8 +- .../tools/ToolInteractionTracker.tsx | 9 + site/src/lib/hooks/useAutoShareableState.ts | 233 ++++++++++++++++++ site/src/lib/tools/manual-share.test.ts | 27 ++ site/src/lib/tools/manual-share.ts | 19 ++ 5 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 site/src/lib/hooks/useAutoShareableState.ts create mode 100644 site/src/lib/tools/manual-share.test.ts create mode 100644 site/src/lib/tools/manual-share.ts diff --git a/site/src/components/tools/ToolFrame.tsx b/site/src/components/tools/ToolFrame.tsx index 3371d4c4..69cb4362 100644 --- a/site/src/components/tools/ToolFrame.tsx +++ b/site/src/components/tools/ToolFrame.tsx @@ -11,6 +11,7 @@ import { ShareBar } from "@/components/tools/ShareBar"; import { FavoriteButton } from "@/components/tools/FavoriteButton"; import { siteConfig } from "@/lib/config"; import { CATEGORY_CONFIG } from "@/lib/tools/categories"; +import { MANUAL_SHARE_SLUGS } from "@/lib/tools/manual-share"; import type { ToolMeta } from "@/lib/tools/types"; interface Props { @@ -54,7 +55,12 @@ export function ToolFrame({ meta, title, description, related, children, article 押し出されていた(個別ツール着地の engaged≒0% の構造要因)。 まず道具を見せ、広告は結果の下に置く(2026-08-22)。 */}
- + {children}
diff --git a/site/src/components/tools/ToolInteractionTracker.tsx b/site/src/components/tools/ToolInteractionTracker.tsx index 14aab9e1..7718c89f 100644 --- a/site/src/components/tools/ToolInteractionTracker.tsx +++ b/site/src/components/tools/ToolInteractionTracker.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, type ReactNode } from "react"; import { trackCalculate, trackCopyResult, trackPresetClick } from "@/lib/analytics/events"; +import { useAutoShareableState } from "@/lib/hooks/useAutoShareableState"; /** * ツール本体を包み、GA4 の calculate / copy_result / preset_click を一括計装する。 @@ -25,6 +26,7 @@ export function ToolInteractionTracker({ slug, locale, category, + shareable = true, children, }: { slug: string; @@ -32,6 +34,11 @@ export function ToolInteractionTracker({ locale?: string; /** ToolMeta.category。GA4 custom dimension `category` に載せる。 */ category?: string; + /** + * 入力値を ?s= に載せる汎用共有リンクを有効にするか。 + * ツール自身が useShareableState を持つ場合は false(二重書き込み回避)。 + */ + shareable?: boolean; children: ReactNode; }) { const ref = useRef(null); @@ -82,5 +89,7 @@ export function ToolInteractionTracker({ }; }, [slug, locale, category]); + useAutoShareableState(ref, slug, shareable); + return
{children}
; } diff --git a/site/src/lib/hooks/useAutoShareableState.ts b/site/src/lib/hooks/useAutoShareableState.ts new file mode 100644 index 00000000..6f6a6b6b --- /dev/null +++ b/site/src/lib/hooks/useAutoShareableState.ts @@ -0,0 +1,233 @@ +"use client"; + +import { useEffect, type RefObject } from "react"; + +/** + * ツール本体の DOM 入力を丸ごと ?s= に載せ、共有リンクから復元する。 + * + * なぜ DOM 総なめ方式か: useShareableState は (slug, value, restore) を + * ツール側が手書きする契約で、223本中7本しか採用されていない。残り216本に + * 個別の restore コールバックを書くのは非現実的で、実装差異による取りこぼしも + * 避けられない。入力の実体はどれも DOM 上の input/select/textarea なので、 + * ToolInteractionTracker と同じく「バブリングを1箇所で拾う」方が網羅的。 + * + * キーは「出現位置」ではなく構造キー(type + 同種内の連番)。理由は + * triangle-calculator のように mode によって表示される input 本数が変わる + * ツールが35本あり、素の通し番号だと復元先が1つずれて別の項目に値が入るため。 + * 構造キーなら、表示中のフィールド群が変わっても同種フィールドの対応が保たれる。 + * + * 復元は2パスで行う。select(モード切替)を先に当ててから、それによって + * 新しく現れた input を次のフレームで埋める。1パスだと「sss モードの共有リンクを + * right モードの初期表示に流し込む」ことになり、値が入らない/誤った欄に入る。 + * + * React 19 の制御コンポーネントは el.value への直接代入を無視する(内部の + * value tracker が「変化なし」と判断して onChange を出さない)ため、 + * ネイティブ setter で書いてから input/change を dispatch する。 + * + * モード切替が