// @ts-nocheck — vendored, see PROVENANCE.md "use client"; import { useEffect, useState } from "react"; import { motion, useDragControls } from "motion/react"; import { CONTRASTS, Contrast, FONTS, Item, KIND_SPEC, NavTab, PALETTES, Palette, SHAPES, ShapeScale, Theme, defaultTabsFor, iconSlotsOf, setIconSlot } from "../lib/tokens"; import { ensureFontLoaded } from "../lib/theme"; import { KIND_TEXT, LANGS, Lang, t, useLang } from "../lib/i18n"; import { IconPicker } from "./IconPicker"; import { Icon } from "./M3Node"; import { VariantSwatch, variantsOf } from "./Inspector"; import { Field, IconBtn, Segmented, Toggle } from "./ui"; /** Sheet that slides up from the bottom edge; the canvas above stays usable. * Dragging the handle moves the sheet with the finger; a flick or a long pull closes it. */ export function BottomSheet({ p, onClose, children }: { p: Palette; onClose: () => void; children: React.ReactNode }) { const lang = useLang(); const controls = useDragControls(); return ( { if (info.offset.y > 90 || info.velocity.y > 600) onClose(); }} style={{ position: "absolute", left: 0, right: 0, bottom: 0, maxHeight: "72%", display: "flex", flexDirection: "column", borderTopLeftRadius: 28, borderTopRightRadius: 28, background: p.surfaceContainerLow, boxShadow: "0 -6px 24px rgba(0,0,0,0.16)", zIndex: 60, paddingBottom: "calc(var(--bottom-ui, 0px) + env(safe-area-inset-bottom))", }} >
{children}
); } function Row({ icon, label, p, children }: { icon: string; label: string; p: Palette; children: React.ReactNode }) { return (
{label}
{children}
); } /** The compact phone editor: text, icon, style, state and a one-line note. */ export function MobileInspector({ item, palette: p, onChange, onDelete, onDuplicate, onClose, }: { item: Item; palette: Palette; onChange: (patch: Partial) => void; onDelete: () => void; onDuplicate: () => void; onClose: () => void; }) { const lang = useLang(); const spec = KIND_SPEC[item.kind]; const slots = iconSlotsOf(item).filter((s) => !s.key.startsWith("tab:")); const [slotKey, setSlotKey] = useState(slots[0]?.key ?? "icon"); const [pickerOpen, setPickerOpen] = useState(false); useEffect(() => { setSlotKey(iconSlotsOf(item)[0]?.key ?? "icon"); setPickerOpen(false); }, [item.id]); const activeSlot = slots.find((s) => s.key === slotKey) ?? slots[0]; const variants = spec.hasVariant ? variantsOf(item.kind) : []; const tabs: NavTab[] = item.tabs ?? []; const [tabSlot, setTabSlot] = useState(null); const setTabCount = (n: number) => { const defaults = defaultTabsFor(item.kind); const next: NavTab[] = []; for (let i = 0; i < n; i++) next.push(tabs[i] ? { ...tabs[i] } : { ...defaults[i % defaults.length] }); onChange({ tabs: next }); }; return (
{KIND_TEXT[lang][item.kind]?.noun ?? spec.label}
{(spec.hasLabel || spec.hasSupporting) && (
{spec.hasLabel && (
onChange({ label })} placeholder={t("label", lang)} p={p} icon="short_text" height={48} /> {item.kind === "text" && ( onChange({ bold: !item.bold })} title={t("bold", lang)} /> )}
)} {spec.hasSupporting && ( onChange({ supporting })} placeholder={item.kind === "snackbar" ? t("action", lang) : t("supporting", lang)} p={p} icon="notes" height={48} /> )}
)} {spec.hasTabs && ( ({ key: String(n), label: String(n) }))} value={String(tabs.length)} onChange={(k) => setTabCount(Number(k))} p={p} height={44} />
{tabs.map((tab, i) => (
{item.kind !== "tabs" && ( setTabSlot(tabSlot === i ? null : i)} title={t("changeIcon", lang)} /> )} {item.kind !== "toolbar" && ( onChange({ tabs: tabs.map((x, j) => (j === i ? { ...x, label } : x)) })} placeholder={t("label", lang)} p={p} height={48} /> )}
))}
{tabSlot !== null && tabs[tabSlot] && (
onChange(setIconSlot(item, `tab:${tabSlot}`, icon))} palette={p} />
)}
)} {slots.length > 0 && activeSlot && (
{slots.map((s) => { const on = s.key === activeSlot.key && pickerOpen; return ( ); })} {activeSlot.value && ( )}
{pickerOpen && (
onChange(setIconSlot(item, activeSlot.key, icon))} palette={p} />
)}
)} {variants.length > 0 && (
{variants.map((v) => ( onChange({ variant: v.key })} /> ))}
)} {spec.hasChecked && ( onChange({ checked })} p={p} icon={item.kind === "chip" ? "check_circle" : item.kind === "box" ? "drag_handle" : "toggle_on"} label={item.kind === "chip" ? t("selected", lang) : item.kind === "box" ? t("handle", lang) : t("on", lang)} /> )} onChange({ note })} placeholder={["button", "fab", "iconButton", "extendedFab"].includes(item.kind) ? t("whenPressed", lang) : t("whatItDoes", lang)} p={p} icon="bolt" height={48} />
); } /** The language list, one row per language. */ export function MobileLang({ palette: p, lang, onLang }: { palette: Palette; lang: Lang; onLang: (l: Lang) => void }) { return (
{LANGS.map((l) => { const on = l.key === lang; return ( ); })}
); } /** The theme sheet: palette, light / dark, shape, type and motion, sized for thumbs. */ export function MobileSettings({ palette: p, paletteKey, onPalette, theme, onTheme, }: { palette: Palette; paletteKey: string; onPalette: (key: string) => void; theme: Theme; onTheme: (patch: Partial) => void; }) { const lang = useLang(); const shapeLabel = (k: ShapeScale) => (k === "square" ? t("shapeSquare", lang) : k === "full" ? t("shapeFull", lang) : t("shapeRounded", lang)); return (
options={[ { key: "light", icon: "light_mode", label: t("light", lang) }, { key: "dark", icon: "dark_mode", label: t("dark", lang) }, ]} value={theme.dark ? "dark" : "light"} onChange={(k) => onTheme({ dark: k === "dark" })} p={p} height={44} />
onTheme({ bothModes })} p={p} icon="routine" label={t("bothModes", lang)} grow />
options={CONTRASTS.map((c) => ({ key: c.key, label: c.key === "high" ? t("contrastHigh", lang) : c.key === "medium" ? t("contrastMedium", lang) : t("contrastStandard", lang) }))} value={theme.contrast} onChange={(contrast) => onTheme({ contrast })} p={p} height={44} />
{PALETTES.map((pal) => { const on = pal.key === paletteKey; return ( ); })}
options={SHAPES.map((s) => ({ key: s.key, icon: s.icon, label: shapeLabel(s.key) }))} value={theme.shape} onChange={(shape) => onTheme({ shape })} p={p} height={44} />
{FONTS.map((f) => { const on = theme.font === f.key; ensureFontLoaded(f.key); return ( ); })}
onTheme({ emphasized })} p={p} icon="format_bold" label={t("emphasized", lang)} />
options={[ { key: "standard", label: t("motionStandard", lang) }, { key: "expressive", label: t("motionExpressive", lang) }, ]} value={theme.motion} onChange={(motion) => onTheme({ motion })} p={p} height={44} />
); } /** Edit / duplicate / delete for the selected part, sized for thumbs. */ export function MobileActionBar({ p, onEdit, onDuplicate, onDelete, }: { p: Palette; onEdit: () => void; onDuplicate: () => void; onDelete: () => void; }) { const lang = useLang(); return (
); }