/* app.jsx — 画面全体(v5:JP/EN共通ベース) */ const EA = window.UWEngine; const SP = window.UW_STORE || 'uw_jp', KEY = SP + '_state_v4', FAV = SP + '_favorites'; const SHOW_PRICE = EA.SHOW_PRICE; const DEV = !!window.UW_DEV; // 開発用の表示(不足データのタブなど) const fresh = () => JSON.parse(JSON.stringify(EA.DEFAULT)); const load = () => { const d = fresh(); try { const s = JSON.parse(localStorage.getItem(KEY)); if (s && s.v === 4) { const m = Object.assign({}, d, s, { tg: Object.assign({}, d.tg, s.tg), ml: Object.assign({}, d.ml, s.ml) }); m.mounts = {};EA.MOUNTS.forEach((k) => {m.mounts[k] = Object.assign(EA.emptyMount(), (s.mounts || {})[k]);}); ['floats', 'adj', 'extra', 'off', 'visited'].forEach((k) => {if (!m[k] || typeof m[k] !== 'object') m[k] = {};}); if (!Array.isArray(m.custom)) m.custom = []; // TG の前面は合計2つまで(正面+上部のホルダー)。古い保存データで3つ以上ある場合は切り詰める const lz = Array.isArray(m.tg.lenses) ? m.tg.lenses : [];m.tg.lenses = lz.slice(0, m.tg.ringLight ? 1 : EA.MAX_FRONTS); if (m.tg.rig === 'none' && m.tg.top === 'holder') m.tg.top = 'none'; // RSバヨネットレンズホルダーは RIG が必要 return m; } } catch (e) {} return d; }; const TITLES = { camera: T("カメラ"), lens: T("レンズ・ポート"), rig: 'RIG', ring: T("リングライト"), lensTg: T("レンズ"), top: T("シューベース"), strobe: T("ストロボ"), light: T("ライト"), conv: T("コンバーター"), fiber: T("ストロボ接続"), arms: T("アーム"), clamp: T("クランプ"), floats: T("フロート"), add: T("未登録の製品を追加") }; const PANELS = () => ({ camera: PanelCamera, lens: PanelLens, rig: PanelRig, ring: PanelRing, lensTg: PanelTgLens, top: PanelTop, strobe: PanelStrobe, light: PanelLight, conv: PanelConverter, fiber: PanelFiber, arms: PanelArms, clamp: PanelClamp, floats: PanelFloats, add: PanelAdd }); function sumOf(res, pred, mode, sea) { const its = res.items.filter((i) => i.qty > 0 && pred(i)); if (!its.length) return null; if (SHOW_PRICE && mode === 'price') {const on = its.filter((i) => !i.off);return { v: on.reduce((a, i) => a + (i.priceIncl || 0), 0), price: true };} return { v: its.reduce((a, i) => a + (sea ? i.sea : i.fresh), 0), miss: its.some((i) => i.missing) }; } function componentList(st, res) { const E = EA,M = res.st.mounts,t = st.tg,tg = st.system === 'tg',L = []; const lightsOf = (strobe) => E.MOUNTS.filter((k) => M[k].pid && E.isStrobe(M[k].pid) === strobe && !(st.sym && k === 'L')). map((k) => (st.sym && k === 'R' ? T("左右") : E.MOUNT_NAME[k]) + T(":") + E.displayName(M[k].pid)).join(T("、")); if (tg) { const rigS = t.rig === 'none' ? T("-") : (t.rig === 'single' ? T("RSシングルグリップセット") + (t.grips > 1 ? T("+RSスタンダードグリップ×") + (t.grips - 1) : '') : T("RSメインフレーム") + (t.multi ? T("+マルチアングルグリップ") : t.grips ? T("+スタンダードグリップ×") + t.grips : T("(グリップなし)"))) + (t.ballBase ? T("・ボールベース×") + t.ballBase : ''); L.push({ k: 'rig', t: 'RIG', s: rigS, g: ['RIG', T("アクセサリー")] }); const recv = (E.RECEIVERS.find((r) => r.id === t.receiver) || {}).label; L.push({ k: 'ring', t: T("リングライト"), s: t.ringLight ? E.info(t.ringLight).name + (recv ? T("(") + recv + T(")") : '') : T("-"), g: [T("リングライト")] }); L.push({ k: 'lensTg', t: T("レンズ"), s: (t.lenses || []).length ? t.lenses.map((id) => E.info(id).name.replace(/^(WF|FIX)/, '')).join(T("、")) + (recv ? T("(") + recv + T(")") : '') : T("-"), g: [T("レンズ"), T("受け側(前面)")] }); L.push({ k: 'top', t: T("シューベース"), s: [t.top === 'shoemount' ? T("RSシューマウント") : t.top === 'holder' ? T("RSバヨネットレンズホルダー") : t.top === 'ballbase' ? T("RSボールベース") : null, t.shoeConv !== 'none' ? E.info(t.shoeConv).name : null].filter(Boolean).join(T(" + ")) || T("-"), g: [T("シューベース")] }); } L.push({ k: 'strobe', t: T("ストロボ"), s: lightsOf(true) || T("-"), g: [T("ストロボ")] }); L.push({ k: 'light', t: T("ライト"), s: lightsOf(false) || T("-"), g: [T("ライト")] }); if (!tg) {const cv = st.ml.converter;L.push({ k: 'conv', t: T("コンバーター"), s: cv ? E.info(cv).name : T("-"), g: [T("コンバーター")] });} if (!tg && M.T.pid) L.push({ k: 'shoeMl', t: T("シューベース"), s: T("アクセサリーシューボールベースII(自動)"), g: [T("シューベース")], noOpen: true }); const fb = res.items.filter((i) => i.group === T("ストロボ接続") && i.qty > 0); L.push({ k: 'fiber', t: T("ストロボ接続"), s: res.strobes ? fb.map((i) => i.name + '×' + i.qty).join(T("、")) : T("不要(ストロボなし)"), g: [T("ストロボ接続")] }); const arms = (st.sym ? ['R', 'T'] : ['R', 'L', 'T']).filter((k) => M[k].arms.length).map((k) => (st.sym && k === 'R' ? T("左右") : E.MOUNT_NAME[k]) + T(":") + M[k].arms.map(E.armSize).join('+')); L.push({ k: 'arms', t: T("アーム"), s: arms.join(T("、")) || T("-"), g: [T("アーム")] }); const cl = res.items.find((i) => i.key === 'auto:clamp'); L.push({ k: 'clamp', t: T("クランプ"), s: cl ? cl.name + ' ×' + cl.qty + (cl.adjusted ? T("(手動)") : T("(自動)")) : T("-"), g: [T("クランプ")], bad: cl && cl.qty !== res.clampAuto }); const fl = EA.FLOATS.filter((id) => st.floats[id]).map((id) => E.info(id).name.replace('HDB-', '') + '×' + st.floats[id]).join(T("、")); L.push({ k: 'floats', t: T("フロート"), s: fl || T("-"), g: [T("フロート")], bad: !res.floatOk, key: true }); const sea = st.water === 'sea'; L.forEach((x) => {x.sum = sumOf(res, (i) => x.g.includes(i.group), st.mode, sea);}); return L; } /* ---------- 主要システム(カメラ/レンズ) ---------- */ function MainSystem({ st, res, open, next }) { const tg = st.system === 'tg',cam = EA.CAM[tg ? st.tgCamera : st.mlCamera]; const lensOk = !tg && st.ml.lens && res.housing && EA.lensesForHousing(res.housing).includes(st.ml.lens); const port = res.items.filter((i) => i.group === T("レンズ・ポート") && ['port', 'ring'].includes(EA.info(i.id).cat)); const Box = ({ k, label, value, sub, img }) => ; return (
{T("主要システム")}
{!tg && i.name).join(T(" ・ ")) || T("ポート未選択") : null} img={port[0] && EA.info(port[0].id).img} />}
); } /* ---------- 結果 ---------- */ function Result({ st, set, res, openK, onHelp }) { const t = res.totals,sea = st.water === 'sea',price = SHOW_PRICE && st.mode === 'price'; const uwRaw = sea ? t.totalUnderwaterWeightSea : t.totalUnderwaterWeightFresh,other = sea ? t.totalUnderwaterWeightFresh : t.totalUnderwaterWeightSea; const uw = Math.round(uwRaw); // 判定は表示と同じ整数値で行う(300g は適正、301g から重い) const R = 1000,pos = (v) => 50 + Math.max(-50, Math.min(50, v / R * 50)) + '%'; const empty = !res.items.length,inRec = uw >= EA.REC_RANGE.min && uw <= EA.REC_RANGE.max; const hasArms = res.armPids.length > 0,hasFloats = res.items.some((i) => EA.FLOATS.includes(i.id) && i.qty > 0); const state = empty ? T("カメラから選択を始めましょう") : inRec ? T("快適な重量です") : uw < 0 ? T("浮いてしまいます") : uw < EA.REC_RANGE.min ? T("中性浮力に近い状態です") : uw <= 500 ? T("少し重めです。フロートを追加しましょう") : T("重めです。フロートで軽くしましょう"); return (
{price ? <>
{T("合計金額(税込)")}
{T("カメラ・レンズ・他社純正ハウジング・付属品の価格は含まれていません")}{res.noPrice.length ? T("。価格未登録 ") + res.noPrice.length + T(" 品目も除いています") : ''}
: <>
{T("水中重量(")}{sea ? T("海水") : T("淡水")}{T(")")}{T("参考値")}
{state}{res.incomplete.length ? T("(重量未登録 ") + res.incomplete.length + T(" 品目を除く)") : ''}
{!sea &&
*{T("淡水での重量です。海で使う場合は「海水」に切り替えてください。")}
} }
{SHOW_PRICE &&
} {!price &&
}
{!price && <>
{T("快適 +")}{EA.REC_RANGE.min}{T("〜+")}{EA.REC_RANGE.max}g
{[-1000, -500, 0, 500, 1000].map((v) =>
{v > 0 ? '+' + v : v}
)}
R ? ' over' : '')} style={{ left: pos(uw) }} />
{T("← 浮く")}{T("沈む →")}
} {!price && !empty && uw < 0 &&
⚠ {hasFloats ? T("浮いてしまいます。必要に応じてフロートを調整してください。") : T("浮いてしまいます。組み合わせを変更することで総重量を変化させられます。")}{T("(")}{fmt(Math.round(uw), true)}{T("g)")} {hasFloats && }
} {!price && !empty && uw > EA.REC_RANGE.max &&
{uw <= 500 ? T("少し重めです。フロートを追加しましょう。") : T("フロートを増やすと軽くなります。")}+{EA.REC_RANGE.min}{T("〜+")}{EA.REC_RANGE.max}{T("g 程度にすると水中で扱いやすくなります。")}
}
{price ? <>
{T("水中重量(")}{sea ? T("海水") : T("淡水")}{T(")")}
{fmt(Math.round(uw), true)} g
{T("陸上重量")}
{fmt(Math.round(t.totalAirWeight))} g
{T("価格未登録")}
{res.noPrice.length} {T("品目")}
: <>
{T("陸上重量")}
{sea ? T("淡水なら") : T("海水なら")}
{T("重量未登録")}
{res.incomplete.length} {T("品目")}
}
{res.errors.map((m, i) => )} {res.warns.map((m, i) => )} {res.recs.map((m, i) => )} {res.infos.map((m, i) =>
{T("ⓘ")} {m.t}
)} {!price && res.incomplete.length > 0 &&
{T("重量未登録のため計算に含めていない製品(")}{res.incomplete.length}{T(")")}
    {res.incomplete.map((i) =>
  • {i.name}{i.pn && {i.pn}}{i.w == null ? T("陸上重量なし") : T("水中重量なし")}
  • )}
}

{T("※ 表示値は登録データに基づく")}{T("参考値")}{T("です。個体差・計測条件・付属品の有無により実際の重量とは異なります。")} {T("重量はバッテリー・SDカードなどを含む状態の値です。")}

); } /* ---------- 内訳 ---------- */ /* 内訳のグループ → 「変更する」で開くコンポーネント */ const editKey = (g, tg) => ({ [T("ハウジング・カメラ")]: 'camera', RIG: 'rig', [T("アクセサリー")]: 'rig', [T("リングライト")]: 'ring', [T("レンズ")]: 'lensTg', [T("受け側(前面)")]: 'lensTg', [T("シューベース")]: tg ? 'top' : 'strobeTop', [T("ストロボ")]: 'strobe', [T("ライト")]: 'light', [T("コンバーター")]: 'conv', [T("ストロボ接続")]: 'fiber', [T("アーム")]: 'arms', [T("クランプ")]: 'clamp', [T("フロート")]: 'floats', [T("レンズ・ポート")]: 'lens', [T("追加した製品")]: 'add' })[g] || null; function Breakdown({ st, set, res, onAdd }) { const groups = [],price = SHOW_PRICE && st.mode === 'price',sea = st.water === 'sea',tg = st.system === 'tg'; res.items.forEach((i) => {let g = groups.find((x) => x.n === i.group);if (!g) groups.push(g = { n: i.group, its: [] });g.its.push(i);}); const toggleOff = (it) => {const s = JSON.parse(JSON.stringify(st));if (s.off[it.key]) delete s.off[it.key];else s.off[it.key] = 1;set(s);}; return (
{T("内訳")}{res.items.filter((i) => i.qty > 0).length} {T("品目")} {!price && {T("陸上重量 / 水中重量(")}{sea ? T("海水") : T("淡水")}{T(")")}} {price && {T("OFFにした製品は合計から除外")}} {Object.keys(st.adj).length > 0 && }
{!groups.length &&
{T("まだ何も選択されていません。")}
} {groups.map((g) =>
{g.n}
{g.its.map((it) =>
{price ? !it.priceNA ? toggleOff(it)} title={T("合計に含める")} /> : : null}
{it.table === 'cameras' || it.table === 'lenses' || !it.id ? {it.name} : {it.name}}{it.pn && {it.pn}} {it.tag === T("自動") && {it.tag}}{it.note &&
※ {it.note}
}
{it.fixed ? ×{it.qty} : set(EA.setQty(st, it, v))} />}
{price ? it.priceNA ? {T("対象外")} : it.priceIncl == null ? T("-") : {yen(it.priceIncl)} : it.included ? {it.inclNote || T("UWL-24M52BY付属")} : <>{gTxt(it.w == null ? null : it.w * it.qty)}/{it.missing && it.exposed ? T("-") : gTxt(sea ? it.sea : it.fresh, true)}}
{it.fixed ? : }
)}
)}
{price ? T("税込価格。カメラ・レンズの価格は含みません。") : T("カメラ・レンズ・ギアはハウジングの中に入るため浮力は増えず、陸上重量がそのまま沈む側に加わります。")}{T("構成で決まる製品(×数量のみの行)はコンポーネントから変更してください。")}
); } /* ---------- ガイド ---------- */ function Guide({ onClose, tab0 }) { const [tab, setTab] = React.useState(typeof tab0 === 'string' ? tab0 : 'use'); React.useEffect(() => {const f = (e) => e.key === 'Escape' && onClose();addEventListener('keydown', f);return () => removeEventListener('keydown', f);}, []); return (
e.stopPropagation()}>

{T("利用ガイド")}

{[['use', T("使い方")], ['why', T("水中重量の重要性")], ['calc', T("計算方法")]].map(([k, l]) => )}
{tab === 'use' &&
  1. {T("TG/ミラーレスを選ぶ")} {T("ページ上部のボタンで切り替えます。")}
  2. {T("主要システムを選ぶ")} {T("「カメラ」を押すとハウジングが自動で決まります。ミラーレスは続けて「レンズ・ポート」を選びます。")}
  3. {T("コンポーネントを順に選ぶ")} {T("コンポーネントの一覧を押すと選択パネルが開きます(構成図は開いたまま)。「次へ」で次の項目に進みます。次に選ぶ項目は")}{T("次はこちら")}{T("で示します。")}
  4. {T("左右対称")} {T("ストロボ・ライト・アームは初期状態で左右対称です。OFFにすると右・左・上を個別に選べます。")}
  5. {T("構成図")} {T("画像や「+」を押すと、その部分のコンポーネントが開きます。コンポーネントを開いても構成図は残るので、見ながら選べます。")}
  6. {T("フロート")} {T("「フロート」を開くと、水中重量を +")}{EA.REC_RANGE.min}{T("〜+")}{EA.REC_RANGE.max}{T("g にする組み合わせを提案します。押すとそのまま反映されます。")}
  7. {T("内訳")} {T("数量の ± と × で増減・削除できます。製品名を押すと製品情報と製品ページへのリンクが表示されます。")}
  8. {SHOW_PRICE &&
  9. {T("重量/価格")} {T("価格に切り替えると税込の合計金額を表示します(カメラ・レンズは含みません)。内訳のスイッチをOFFにした製品は合計から除きます。")}
  10. }
  11. {T("おすすめ構成・お気に入り")} {T("ページ上部のおすすめ構成を押すと一式を読み込みます。今の構成は「お気に入りに保存」で保存できます(このブラウザに保存)。")}
} {tab === 'why' &&

{T("水中撮影に集中するために。")}

{T("水中では、機材が重すぎると腕が疲れて構図が安定せず、軽すぎる(浮く)と手を離したときに機材が流され、ブレやすくなります。")}

{T("ハウジング・ポート・ストロボの組み合わせに合わせてフロートを追加し、")}{T("ほんの少し沈む(+")}{EA.REC_RANGE.min}{T("〜+")}{EA.REC_RANGE.max}{T("g)")}{T("重さに整えると、手を離しても機材がゆっくり沈むだけで、片手でも楽に扱えます。")}

{T("機材に合わせて最適な重さにして、水中撮影に没頭できるシステムを組みましょう。")}

} {tab === 'calc' &&
W₀{T("陸上重量(製品の重さ)")}
W₁{T("淡水中重量(淡水 ρ=1.000 で計測した値。マイナス=浮く)")}
W₂{T("海水中重量(ρ=1.025)")}

{T("浮力 F₁ = W₀ − W₁(=排水体積 V)。海水の浮力 F₂ = V × 1.025。")}
{T("W₂ = W₀ − 1.025 ×(W₀ − W₁)")}

{T("カメラ・レンズ・ギア・ハウジング内部のコンバーターは、ハウジングの中に入るため体積(浮力)は増えません。陸上重量をそのまま沈む側に加えます。")}

{T("全体の水中重量は、各製品の水中重量の合計です。")}

{T("登録データは計測値・公称値を含む参考値です。実際の機材では個体差や付属品により異なります。")}

}
); } /* ---------- おすすめ構成・お気に入り ---------- */ function Presets({ st, set, notify, onClose }) { const recs = EA.DB.uw_recommended_configs.filter((r) => r.system_type === st.system).sort((a, b) => a.sort_order - b.sort_order); const [favs, setFavs] = React.useState(() => {try {return JSON.parse(localStorage.getItem(FAV)) || [];} catch (e) {return [];}}); React.useEffect(() => {localStorage.setItem(FAV, JSON.stringify(favs));}, [favs]); const mine = favs.filter((f) => f.st.system === st.system); const save = () => {const name = prompt(T("お気に入りの名前"), (EA.CAM[st.system === 'tg' ? st.tgCamera : st.mlCamera] || {}).name_ja || T("構成"));if (!name) return;setFavs([...favs, { id: Date.now(), name, st }]);notify(T("お気に入りに保存しました"));}; return (
{T("おすすめ構成(")}{st.system === 'tg' ? 'TG' : T("ミラーレス")}{T(")")}{onClose && }
{recs.map((r, i) => )}
{T("お気に入り")}
{mine.length ?
{mine.map((f) =>
)}
:

{T("保存した構成はありません")}

}
); } function Calc({ st, set, onHelp }) { const res = React.useMemo(() => EA.build(st), [st]); const stRef = React.useRef(st);stRef.current = st; const [open, setOpen] = React.useState(null),[chart, setChart] = React.useState(null),[toast, setToast] = React.useState(null),[pop, setPop] = React.useState(null),[psOpen, setPsOpen] = React.useState(false), [topHint, setTopHint] = React.useState(false); const notify = (m) => {setToast(m);clearTimeout(window.__tt);window.__tt = setTimeout(() => setToast(null), 3600);}; const next = EA.nextStep(st, res); const list = componentList(st, res); const P = PANELS()[open]; const sea = st.water === 'sea',price = SHOW_PRICE && st.mode === 'price',uw = sea ? res.totals.totalUnderwaterWeightSea : res.totals.totalUnderwaterWeightFresh; const mark = (k) => {if (!k || st.visited[k]) return st;const s = JSON.parse(JSON.stringify(st));s.visited[k] = true;return s;}; const openK = (k) => {if (k === 'shoeMl') return;if (k === 'strobeTop') { setTopHint(true); k = 'strobe'; } else setTopHint(false);setOpen(k);}; // パネルからの遷移:go(完了した項目, 明示的な行き先) const go = (done, target, newSt) => { const base = newSt || stRef.current,dk = done; let s1 = base;if (dk && !base.visited[dk]) {s1 = JSON.parse(JSON.stringify(base));s1.visited[dk] = true;} if (s1 !== stRef.current) set(s1); const nx = target || EA.nextStep(s1, EA.build(s1), done); setOpen(nx && nx !== done ? nx : null); }; const close = () => {if (open && !['add', 'clamp', 'fiber', 'top'].includes(open)) set(mark(open));setOpen(null);}; const setSys = (sys) => {set({ ...st, system: sys, adj: {}, off: {} });const cam = sys === 'tg' ? st.tgCamera : st.mlCamera;if (!cam) setTimeout(() => setOpen('camera'), 150);}; const bdRef = React.useRef(),dgRef = React.useRef(),resRef = React.useRef(); const resetAll = () => {if (confirm(T("選択をすべて解除して、最初から選び直しますか?"))) {set({ ...fresh(), system: st.system, water: st.water, mode: st.mode });setOpen('camera');}}; const isMobile = () => window.matchMedia && window.matchMedia('(max-width: 900px)').matches; // ドロワー下部のボタンは PC・スマホ共通で「閉じる」ではなく「次へ」にする(mob は他で使う場合のために残している) const [mob, setMob] = React.useState(isMobile()); React.useEffect(() => { if (!window.matchMedia) return; const mq = window.matchMedia('(max-width: 900px)'),f = () => setMob(mq.matches); f();mq.addEventListener ? mq.addEventListener('change', f) : mq.addListener(f); return () => {mq.removeEventListener ? mq.removeEventListener('change', f) : mq.removeListener(f);}; }, []); // 「次へ」を出す項目:PC・スマホ共通で、選択の流れ(EA.flow)に入っている項目(シューベース・クランプ・ストロボ接続・未登録追加は「閉じる」のまま) const footNext = !!open && open !== 'floats' && EA.flow(st, res).includes(open); // 構成図の高さを CSS 変数に(スマホは選択パネルを構成図の下に出す) React.useEffect(() => { const el = dgRef.current;if (!el) return; const upd = () => document.documentElement.style.setProperty('--dgh', Math.round(el.getBoundingClientRect().height) + 'px'); upd();let ro = null;if (window.ResizeObserver) {ro = new ResizeObserver(upd);ro.observe(el);} addEventListener('resize', upd);return () => {if (ro) ro.disconnect();removeEventListener('resize', upd);}; }, []); // コンポーネントを開くとき、構成図が見える位置までスクロール(PCは先頭、スマホは構成図が画面上端に固定される位置) React.useEffect(() => { if (!open || !dgRef.current) return; const el = dgRef.current,top = el.getBoundingClientRect().top + window.scrollY; window.scrollTo({ top: isMobile() ? Math.max(0, top) : 0, behavior: 'smooth' }); }, [open]); const toResult = () => {const el = resRef.current;if (el) window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - (isMobile() ? (dgRef.current ? dgRef.current.getBoundingClientRect().height : 0) + 8 : 12), behavior: 'smooth' });}; const uwColor = uw >= EA.REC_RANGE.min && uw <= EA.REC_RANGE.max ? 'var(--on)' : uw < 0 ? 'var(--float)' : 'var(--sink)'; return (
{psOpen && <>
setPsOpen(false)} />
{set(s);setOpen(null);setPsOpen(false);}} notify={notify} onClose={() => setPsOpen(false)} />
}

UNDERWATER WEIGHT CALCULATOR SYSTEM

{T("水中重量計算システム")}

{T("ハウジング・ポート・ストロボ・アームを組み合わせて、水中での重さを計算します。フロートで +")}{EA.REC_RANGE.min}{T("〜+")}{EA.REC_RANGE.max}{T("g に整えると、水中で機材が扱いやすくなります。")}

{T("※ 計算結果はあくまで目安です。製造時の個体差や使用環境、経年劣化などにより、実際の重量にはばらつきがあります。")}

{T("コンポーネント")}
{list.map((x) => )}
setOpen('add')} />
{price ? : } {price ? T("税込(カメラ・レンズ除く)") : (sea ? T("海水") : T("淡水")) + T("・参考値")}
{P &&
{price ? T("合計(税込)") : T("水中重量(") + (sea ? T("海水") : T("淡水")) + T(")")}{price ? : }
}>

} {chart && setChart(null)} />} {pop && setPop(null)} priceMode={price} onEdit={pop.edit ? () => {const k = pop.edit;setPop(null);openK(k);} : null} />} {toast &&

{toast}
} ); } function Missing() { const list = React.useMemo(() => EA.missingList(), []),groups = [...new Set(list.map((x) => x.group))]; const csv = () => { const rows = [['group', 'id', 'product_number', 'name', 'missing', 'weight_g', 'underwater_weight_g']].concat(list.map((x) => [x.group, x.id, x.pn || '', x.name, x.missing.join('/'), '', ''])); const b = new Blob(['\uFEFF' + rows.map((r) => r.map((v) => /[",]/.test(v) ? '"' + String(v).replace(/"/g, '""') + '"' : v).join(',')).join('\r\n')], { type: 'text/csv' }); const a = document.createElement('a');a.href = URL.createObjectURL(b);a.download = 'uw_missing.csv';a.click(); }; return (

{T("計算に使う製品のうち、重量が未登録のもの")} {list.length} {T("件。カメラ・レンズ・ギアは陸上重量のみ必要です。")}

{groups.map((g) => {list.filter((x) => x.group === g).map((x) => )})}
{T("製品")}{T("ID / 製品番号")}{T("不足")}
{g}{T("(")}{list.filter((x) => x.group === g).length}{T(")")}
{x.name}{x.id}{x.pn && x.pn !== x.id ? ' / ' + x.pn : ''}{x.missing.join(T("・"))}
); } /* 開発用:このブラウザで押した製品の回数(全ユーザーの集計は Google アナリティクスのイベント uw_select) */ function Counts() { const [c, setC] = React.useState(uwTrackCounts); const rows = Object.keys(c).map((name) => ({ name, n: c[name] })).sort((a, b) => b.n - a.n); return

{T("このブラウザで製品カード・追加ボタンを押した回数です。全ユーザーの集計は Google アナリティクスのイベント「uw_select」で確認できます。")}

{rows.map((r) => )}
{T("製品名")}{T("回数")}
{r.name}{r.n}
{!rows.length &&

{T("まだ記録がありません")}

}
; } function App() { const [st, setSt] = React.useState(load); const [tab, setTab] = React.useState(() => localStorage.getItem(SP + '_tab') || 'calc'); const [help, setHelp] = React.useState(false); // false/true/開くタブ名('calc' など) React.useEffect(() => {localStorage.setItem(KEY, JSON.stringify(st));}, [st]); React.useEffect(() => {localStorage.setItem(SP + '_tab', tab);}, [tab]); return ( <> {/* 開発用ツール(不足データなど)。公開時は HTML の UW_CONFIG.devTools を false にすると消える */} {DEV &&
{T("開発用")} {T("公開時は UW_CONFIG.devTools: false で非表示")}
} {!DEV || tab === 'calc' ? setHelp(typeof t === 'string' ? t : true)} /> : tab === 'count' ? : } {help && setHelp(false)} />} ); } ReactDOM.createRoot(document.getElementById('root')).render();