// data.jsx — shared context, design atoms, icons, mock data const { createContext, useContext, useState, useRef, useEffect } = React; /* ─────────────────────────── App context ─────────────────────────── */ const AppCtx = createContext(null); const useApp = () => useContext(AppCtx); /* ─────────────────────────── Simple line icons ─────────────────────────── Minimal geometric strokes for UI chrome only. */ function Icon({ name, size = 24, stroke = 'currentColor', sw = 1.8, fill = 'none', style }) { const p = { fill, stroke, strokeWidth: sw, strokeLinecap: 'round', strokeLinejoin: 'round' }; const paths = { home: <>, map: <>, heart: , gear: <>, plus: , chevR: , chevL: , chevD: , back: , lock: <>, camera: <>, note: <>, link: <>, mic: <>, send: , battery:<>, car: <>, train: <>, sun: <>, rain: <>, check: , sparkle:, copy: <>, share: <>, pin: <>, bag: <>, google: , arrow: , skip: , refresh:<>, edit: , clock: <>, moon: , walk: <>, cal: <>, chat: <>, route: <>, expand: , locate: <>, }; return ( ); } /* ─────────────────────────── Buttons & atoms ─────────────────────────── */ function Btn({ children, onClick, variant = 'primary', full, size = 'md', style, icon, disabled }) { const base = { fontFamily: 'var(--font-ui)', fontWeight: 700, cursor: disabled ? 'default' : 'pointer', border: 'none', borderRadius: 999, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 8, width: full ? '100%' : 'auto', transition: 'transform .15s ease, box-shadow .2s ease, background .2s ease', letterSpacing: '.02em', whiteSpace: 'nowrap', opacity: disabled ? 0.5 : 1, }; const sizes = { sm: { padding: '9px 16px', fontSize: 14 }, md: { padding: '14px 22px', fontSize: 16 }, lg: { padding: '17px 26px', fontSize: 17 }, }; const variants = { primary: { background: 'var(--ink)', color: 'var(--paper)', boxShadow: '0 8px 20px -8px rgba(61,54,44,.55)' }, rose: { background: 'var(--me)', color: '#fff', boxShadow: '0 10px 22px -10px var(--me)' }, amethyst:{ background: 'var(--amethyst)', color: '#fff', boxShadow: '0 10px 22px -10px var(--amethyst)' }, soft: { background: 'var(--surface)', color: 'var(--ink)', boxShadow: 'inset 0 0 0 1px var(--line-2)' }, ghost: { background: 'transparent', color: 'var(--ink-soft)' }, }; const ref = useRef(null); return ( ); } function Chip({ children, active, onClick, color = 'var(--ink)', style }) { return ( ); } /* card surface */ function Card({ children, style, onClick, pad = 18 }) { return (
{children}
); } /* small charm avatar (two interlocking rings = the amulet) */ function Charm({ size = 30, a = 'var(--me)', b = 'var(--partner)', linked = true }) { return ( ); } /* AI(相棒の犬)のチャットアイコン。丸く切り抜いて表示する */ function AiAvatar({ size = 28, style }) { return (
Amulet
); } /* avatar disc with initial */ function Avatar({ name = 'A', color = 'var(--me)', size = 38 }) { return (
{name}
); } /* striped image placeholder */ // 何も画像が無いときに出す共通フォールバック画像(静的サイト直下にホスト・差し替え可) const FALLBACK_IMG = '/fallback.webp'; function ImgSlot({ label = 'IMAGE', h = 120, r = 16, style, src }) { const [srcFailed, setSrcFailed] = React.useState(false); const [fbFailed, setFbFailed] = React.useState(false); // ① photoUrl(src)→ ② 失敗/未指定ならフォールバック画像 → ③ それも失敗したら色枠ラベル const useSrc = src && !srcFailed; if (useSrc || !fbFailed) { return ( {label} (useSrc ? setSrcFailed(true) : setFbFailed(true))} style={{ height: h, width: '100%', borderRadius: r, objectFit: 'cover', display: 'block', border: '1px solid var(--line)', ...style, }} /> ); } return (
{label}
); } /* section eyebrow */ function Eyebrow({ children, color = 'var(--ink-faint)' }) { return
{children}
; } /* ── Firestore イベント → 画面用の形に正規化(live: true が実データの印) ── */ function normalizeEvent(d, uid) { const stats = d.inputStats || {}; const mine = (uid && stats[uid] && stats[uid].messageCount) || 0; const theirs = Object.entries(stats) .filter(([k]) => k !== uid) .reduce((a, [, v]) => a + ((v && v.messageCount) || 0), 0); return { id: d.id, live: true, date: d.dateLabel || d.date || '', dow: d.dow || '', title: d.title || 'ただの週末', area: d.area || '', progress: Math.min(1, (mine + theirs) / 16), mine, theirs, weather: 'sun', planStatus: d.planStatus || 'none', raw: d, }; } /* ─────────────────────────── Shells ─────────────────────────── */ const SAFE_TOP = 50; const NAV_H = 86; // nav incl. home-indicator pad function TopBar({ title, onBack, right, sub, center }) { return (
{onBack ? ( ) :
}
{title &&
{title}
} {sub &&
{sub}
}
{right || (onBack ?
: null)}
); } function BottomNav({ active, go }) { const items = [ { id: 'home', icon: 'home', label: 'ホーム' }, { id: 'amulet-map', icon: 'map', label: 'マップ' }, { id: 'wish-list', icon: 'heart', label: '小言' }, { id: 'settings', icon: 'gear', label: '設定' }, ]; return (
{items.map(it => { const on = active === it.id; return ( ); })}
); } /* scroll body that respects safe areas */ function Body({ children, nav, pad = 18, style, scrollRef }) { return (
{children}
); } function Field({ label, value = '', onChange, placeholder, type = 'text', icon, area, rows = 3, right }) { const handle = (e) => onChange && onChange(e.target.value); return (