// @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 (
);
}
/** 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 (
);
}