/* ui.jsx — 共通UI部品(v4) */ const E = window.UWEngine; const fmt = (n, sign) => n == null ? '—' : (sign && n > 0 ? '+' : '') + (Math.round(n * 10) / 10).toLocaleString('ja-JP'); const yen = (n) => '¥' + Math.round(n).toLocaleString('ja-JP'); const gTxt = (v, sign) => v == null ? T("-") : fmt(Math.round(v * 10) / 10, sign) + 'g'; /* 数字がテレテレと変わる(値が変わったときだけ短くアニメーション) */ function useCountUp(target, ms = 480) { const [v, setV] = React.useState(target); const from = React.useRef(target),tm = React.useRef([]); React.useEffect(() => { const a = from.current,b = target;tm.current.forEach(clearTimeout);tm.current = []; if (a === b || document.hidden || window.matchMedia('(prefers-reduced-motion: reduce)').matches) {from.current = b;setV(b);return;} const N = 14; for (let i = 1; i <= N; i++) tm.current.push(setTimeout(() => {const p = i / N,e = 1 - Math.pow(1 - p, 3),x = i === N ? b : a + (b - a) * e;from.current = x;setV(x);}, ms / N * i)); return () => tm.current.forEach(clearTimeout); }, [target]); return v; } function Num({ value, sign, unit = 'g', yenMode, className }) { const v = useCountUp(value || 0); const [bump, setBump] = React.useState(0);const prev = React.useRef(value); React.useEffect(() => {if (prev.current !== value) {setBump((b) => b + 1);prev.current = value;}}, [value]); return {yenMode ? yen(v) : <>{fmt(Math.round(v), sign)}{unit && {unit}}}; } function Step({ value, min = 0, max = 99, step = 1, onChange }) { return ( e.stopPropagation()}> {value} ); } function Toggle({ checked, disabled, onChange, children, title }) { return ( ); } function Chips({ options, value, onPick, sm, qty }) { return (
{options.map((o) => )}
); } /* 製品POP(名前を押すと表示。リンクは直接遷移せず、ここから開く) */ const PopCtx = React.createContext(() => {}); /* PName:製品名を押すと製品POP。edit=「変更する」で開くコンポーネント(内訳から開いたとき) */ function PName({ id, children, className, edit }) { const open = React.useContext(PopCtx); if (!id) return {children}; return ; } /* ===== 製品の選択回数の集計 ===== * 製品カード・追加ボタンを押すたびに記録する。 * 1) Google アナリティクス(サイト共通の gtag)にイベント「uw_select」を送る … 全ユーザーの集計はこちら(GA4 の「イベント」→ uw_select) * item_id=製品名(日本語名。EN版でも日本語名にそろえて集計できるようにする)/item_code=製品ID/uw_action/uw_system/uw_lang * 2) このブラウザの localStorage にも回数を保存 … 開発用バーの「選択回数」で確認(自分の操作の確認用) * gtag が無い環境(ローカルなど)では 1) は何もしない。 */ const TRACK_KEY = (window.UW_STORE || 'uw_jp') + '_select_counts'; function uwTrack(id, action, system) { if (!id) return; const r = (E.info(id) || {}).row || {},name = r.name_ja || (E.info(id) || {}).name || id; // 集計のキーは製品名(日本語名) try {const c = JSON.parse(localStorage.getItem(TRACK_KEY) || '{}');c[name] = (c[name] || 0) + 1;localStorage.setItem(TRACK_KEY, JSON.stringify(c));} catch (e) {} try {if (typeof window.gtag === 'function') window.gtag('event', 'uw_select', { item_id: name, item_name: name, item_code: id, uw_action: action || 'select', uw_system: system || '', uw_lang: window.UW_LANG || 'ja' });} catch (e) {} } function uwTrackCounts() {try {return JSON.parse(localStorage.getItem(TRACK_KEY) || '{}');} catch (e) {return {};}} function ProductPop({ id, onClose, priceMode, onEdit }) { React.useEffect(() => {const f = (e) => e.key === 'Escape' && onClose();addEventListener('keydown', f);return () => removeEventListener('keydown', f);}, []); const i = E.info(id),inside = i.table === 'cameras' || i.table === 'lenses' || ['gear', 'converter_ttl', 'converter_manual'].includes(i.cat); return (
e.stopPropagation()}>
{i.img ? : i.noPhoto ? null : {T("画像準備中")}}
{i.pn || T("製品番号 未登録")}
{i.name}
{!inside && } {i.note && } {priceMode && E.SHOW_PRICE && }
{T("陸上重量")}{gTxt(i.w)}
{T("水中重量(淡水)")}{gTxt(i.u, true)}
※{i.note}
{T("価格(税込)")}{i.price == null ? T("-") : yen(i.price * E.TAX)}
{i.url ? {T("製品ページ ↗")} : {T("製品ページは未登録です")}} {onEdit && }
); } /* カード:左に製品画像(DBの image_url)、右に名称・説明・重量 */ /* act=カード右側のボタン { label, onClick, disabled }。カードを押したときも同じ動作。押すと選択回数を記録(uwTrack) */ function Card({ id, on, dis, onClick, title, desc, rec, none, noWeight, children, badge, onRemove, acts, act }) { const i = id ? E.info(id) : null; const click0 = onClick; onClick = click0 && id ? () => {if (!on) uwTrack(id, 'card');click0();} : click0; if (act && !acts) acts = ; const inside = i && (i.table === 'cameras' || i.table === 'lenses' || ['gear', 'converter_ttl', 'converter_manual'].includes(i.cat)); return (
{if (!dis && (e.key === 'Enter' || e.key === ' ')) {e.preventDefault();onClick && onClick();}}}> {onRemove && }
{none || i && i.noPhoto ? none ? '—' : '' : i && i.img ? {e.target.style.display = 'none';}} /> : {T("画像")}
{T("準備中")}
}
{i && !title ? {i.name} : title}{rec && {T("おすすめ")}}{badge}
{i && i.pn &&
{i.pn}
} {desc &&
{desc}
} {i && !noWeight &&
{T("陸上")} {gTxt(i.w)}{!inside && <> {T("水中")} {gTxt(i.u, true)}}
} {children &&
e.stopPropagation()}>{children}
}
{acts &&
e.stopPropagation()}>{acts}
}
); } /* フロート:長さごとの上限本数と、混ぜる場合の組み合わせを分けて表示 */ function capSummary(size) { const pats = E.CAP[size] || []; const only = len => Math.max(0, ...pats.filter(p => p.every(x => x === len)).map(p => p.length)); const mixed = pats.filter(p => p.includes(40) && p.includes(65)).map(p => ({ n40: p.filter(x => x === 40).length, n65: p.filter(x => x === 65).length })); const tot = Math.max(0, ...pats.map(p => p.reduce((a, b) => a + b, 0))); return { m40: only(40), m65: only(65), mixed, tot }; } function Bar({ n, len }) { return {Array.from({ length: n }).map((_, i) => )}; } function FloatChart({ FL }) { return <> {E.ARMS.map(id => { const s = E.armSize(id), c = capSummary(s), d = E.DIM[id] || {}; return ; })}
{T("サイズ")}{T("有効長")}{T("長さ 40mm")}{T("長さ 65mm")}{T("長さを混ぜる場合")}
{s}
{id}
{d.effective_mm ? d.effective_mm + 'mm' : '–'}
{c.m40}{T("本まで")}
{c.m65 ? c.m65 + T("本まで") : '–'}
{c.mixed.length ? c.mixed.map((m, k) =>
65×{m.n65} + 40×{m.n40}
) : '–'}

{T("65mm は 40mm の約1.6本分の長さです。アームにおさまる合計の長さで判定しています。")} {T("複数のアームがある場合は、フロートを各アームに振り分けて装着できるかを判定します。")}

{FL.map(f => )}
{T("フロート")}{T("長さ × 太さ")}{T("陸上 / 水中")}
{f.name}
{f.id}
{(E.U[f.id] || {}).float_length} × φ{(E.U[f.id] || {}).float_width}{gTxt(f.w)} / {gTxt(f.u, true)}
; } function FiberChart() { const arms = E.ARMS.map(id => ({ id, s: E.armSize(id), d: E.DIM[id] || {} })); const GUIDE = { S: T("直付け~アームS 1本"), M: T("アームM 1本~S 2本"), L: T("アームL・M 2本以上") }; return <> {E.FIBER.map(f => { const i = E.info(f.pid), d = E.DIM[f.pid] || {}; return ; })}
{T("サイズ")}{T("製品")}{T("全長")}{T("対応するアーム構成の目安")}
{f.size}{i.name}
{i.pn}
{d.length_mm ? d.length_mm + 'mm' : '–'}{GUIDE[f.size] || '–'}
{arms.map(a => )}
{T("アーム")}{T("全長")}{T("有効長")}
{E.info(a.id).name}
{a.id}
{a.d.length_mm ? a.d.length_mm + 'mm' : '–'}{a.d.effective_mm ? a.d.effective_mm + 'mm' : '–'}

{T("ケーブルは、ハウジングの光出力口からストロボまでの距離(アームの有効長の合計+余裕)より長いものを選んでください。目安は実際の取り回しで変わります。")}

; } function SizeChart({ kind, onClose }) { React.useEffect(() => {const f = (e) => e.key === 'Escape' && onClose();addEventListener('keydown', f);return () => removeEventListener('keydown', f);}, []); const FL = E.FLOATS.map((id) => E.info(id)); return (
e.stopPropagation()}>

{T("サイズチャート")}

{kind === 'fiber' ? : }
); } function Drawer({ title, onClose, footer, children, back = T("コンポーネント") }) { React.useEffect(() => {const f = (e) => e.key === 'Escape' && onClose();addEventListener('keydown', f);return () => removeEventListener('keydown', f);}, []); return ( ); } /* 左右対称スイッチ(ストロボ・ライト・アーム共通) */ function SymSwitch({ st, set }) { return
set(E.setSym(st, v))}>{T("左右対称")}{T("ONのときは右と左に同じものを付けます")}
; } Object.assign(window, { uwTrack, uwTrackCounts, fmt, yen, gTxt, useCountUp, Num, Step, Toggle, Chips, PopCtx, PName, ProductPop, Card, SizeChart, Drawer, SymSwitch });