// @ts-nocheck — vendored, see PROVENANCE.md
"use client";
import { useEffect, useRef, useState } from "react";
import {
Action,
BACK_TARGET,
CONTENT_W,
Frame,
FramePreset,
HALF_W,
Item,
KIND_SPEC,
PHONE_H,
PHONE_W,
Kind,
NavTab,
Palette,
SWIPE_DIRS,
SwipeDir,
TAPPABLE,
TOGGLEABLE,
TRANSITIONS,
Transition,
VARIANTS,
Variant,
actionSlotsOf,
contentWidth,
defaultTabsFor,
framePresetOf,
cardFillOf,
frameSizeOf,
halfWidth,
isPhoneFrame,
toggleIcon,
iconSlotsOf,
setIconSlot,
variantStyle,
} from "../lib/tokens";
import { IconPicker } from "./IconPicker";
import { Icon } from "./M3Node";
import { ButtonRun, CornerIcon, Field, IconBtn, Section, Segmented, SizePresets, Slider, TidyButton, TidyState, Toggle, TokenChips } from "./ui";
import { AiWriteBtn } from "./AiPanel";
import { popHistory } from "../lib/ai";
import { KIND_TEXT, SWIPE_TEXT, TRANSITION_TEXT, t, useLang } from "../lib/i18n";
import { useHost } from "../lib/host";
/** A text field for a web address: what is typed stays in the box, and only a complete
* http(s) address (or an emptied box) reaches the part. */
function UrlField({ value, onChange, placeholder, p }: { value: string; onChange: (src: string | undefined) => void; placeholder: string; p: Palette }) {
const [text, setText] = useState(value);
useEffect(() => setText(value), [value]);
return (
setText(value)}>
{
setText(v);
const s = v.trim();
/* an emptied box removes a URL; a picked file (which shows as an empty box) is left alone */
if (!s && value) onChange(undefined);
else if (/^https?:\/\/\S+$/.test(s)) onChange(s);
}}
placeholder={placeholder}
p={p}
icon="link"
/>
);
}
export function variantsOf(kind: Kind): { key: Variant; label: string }[] {
const variants = VARIANTS.map((v) => ({ ...v, label: t(v.key) }));
switch (kind) {
case "card":
return [
{ key: "tonal", label: t("filled") },
{ key: "elevated", label: t("elevated") },
{ key: "outlined", label: t("outlined") },
];
case "textField":
return [
{ key: "outlined", label: t("outlined") },
{ key: "filled", label: t("filled") },
];
case "chip":
return [
{ key: "outlined", label: t("outlined") },
{ key: "tonal", label: t("elevated") },
];
case "fab":
case "extendedFab":
case "fabMenu":
return variants.filter((v) => v.key !== "text" && v.key !== "elevated" && v.key !== "outlined");
case "splitButton":
return variants.filter((v) => v.key !== "text");
case "toolbar":
return [
{ key: "tonal", label: t("standard") },
{ key: "filled", label: t("vibrant") },
];
case "iconButton":
return variants.filter((v) => v.key !== "elevated" && v.key !== "text").concat({
key: "text",
label: t("standard"),
});
default:
return variants;
}
}
export function VariantSwatch({
v,
label,
p,
on,
onClick,
small,
}: {
v: Variant;
label: string;
p: Palette;
on: boolean;
onClick: () => void;
small?: boolean;
}) {
const st = variantStyle(v, p);
const h = small ? 32 : 40;
return (
{on && }
{label}
);
}
const MAX_IMAGE_PX = 1200;
/** hover text for a width preset derived from the selected frame */
export const widthPresetLabel = (v: number, frameWidth = PHONE_W): string | undefined =>
v === frameWidth
? t("screenWidth")
: v === contentWidth(frameWidth)
? t("contentWidth")
: v === halfWidth(frameWidth)
? t("halfWidth")
: frameWidth !== PHONE_W && v === CONTENT_W
? t("columnWidth")
: undefined;
const heightPresetLabel = (v: number, frameHeight = PHONE_H): string | undefined =>
v === frameHeight ? t("screenHeight") : v === frameHeight / 2 ? t("halfHeight") : undefined;
export function FrameSizePicker({
frame,
palette: p,
onChange,
compact,
}: {
frame: Frame;
palette: Palette;
onChange: (preset: FramePreset) => void;
compact?: boolean;
}) {
const lang = useLang();
return (
options={[
{ key: "phone", icon: "smartphone", label: compact ? undefined : t("phoneFrame", lang), title: t("phoneFrame", lang) },
{ key: "desktop", icon: "desktop_windows", label: compact ? undefined : t("desktopFrame", lang), title: t("desktopFrame", lang) },
]}
value={framePresetOf(frame)}
onChange={onChange}
p={p}
height={compact ? 36 : 40}
grow={!compact}
/>
);
}
/** Downscale a picked file so the document stays small enough for localStorage. */
function readImage(file: File): Promise {
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(file);
const img = new Image();
img.onload = () => {
const s = Math.min(1, MAX_IMAGE_PX / Math.max(img.width, img.height));
const c = document.createElement("canvas");
c.width = Math.max(1, Math.round(img.width * s));
c.height = Math.max(1, Math.round(img.height * s));
c.getContext("2d")!.drawImage(img, 0, 0, c.width, c.height);
URL.revokeObjectURL(url);
resolve(c.toDataURL("image/webp", 0.86));
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error("image"));
};
img.src = url;
});
}
function FrameChips({
frames,
value,
onChange,
p,
back,
small,
}: {
frames: Frame[];
value: string | null;
onChange: (id: string | null) => void;
p: Palette;
/** offer "go back" as a target */
back?: boolean;
small?: boolean;
}) {
const lang = useLang();
const h = small ? 32 : 36;
const chip = (id: string | null, label: string, icon: string) => {
const on = value === id;
return (
onChange(id)}
className="m3-press"
style={{
height: h,
padding: "0 12px 0 8px",
borderRadius: h / 2,
border: "none",
background: on ? p.primary : p.surfaceContainerHigh,
color: on ? p.onPrimary : p.onSurfaceVariant,
fontSize: 13,
fontWeight: 600,
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
gap: 6,
maxWidth: "100%",
}}
>
{label}
);
};
return (
{chip(null, t("none", lang), "block")}
{back && chip(BACK_TARGET, t("goBack", lang), "arrow_back")}
{frames.map((f) => chip(f.id, f.name || t("screen", lang), isPhoneFrame(f) ? "smartphone" : "desktop_windows"))}
);
}
function TransitionPicker({ value, onChange, p }: { value: Transition; onChange: (t: Transition) => void; p: Palette }) {
const lang = useLang();
return (
options={TRANSITIONS.map((tr) => ({ key: tr.key, icon: tr.icon, title: TRANSITION_TEXT[lang][tr.key] }))}
value={value}
onChange={onChange}
p={p}
height={34}
/>
);
}
/** target frame (or back) plus the transition, for one tap target */
function ActionEditor({
frames,
action,
onChange,
p,
}: {
frames: Frame[];
action: Action | undefined;
onChange: (a: Action | undefined) => void;
p: Palette;
}) {
return (
onChange(to ? { to, transition: action?.transition ?? "slide" } : undefined)}
p={p}
back
/>
{action && action.to !== BACK_TARGET && (
onChange({ ...action, transition })} p={p} />
)}
);
}
/** what a field's AI button needs from the page; `reason` explains a disabled button */
export type AiHooks = { ready: boolean; reason?: string; busy: boolean; onRun: () => void; onCancel: () => void };
/** a multiline field with the AI button under it, fused with a button that swaps the AI text and the original once the AI has written it */
function AiField({ ai, history, onRestore, p, value, onChange, placeholder }: { ai: AiHooks; history?: string[]; onRestore: () => void; p: Palette; value: string; onChange: (v: string) => void; placeholder: string }) {
const lang = useLang();
return (
);
}
export function FrameInspector({
frame,
palette: p,
onChange,
onDelete,
onDuplicate,
onPreview,
prompt,
onSaveImage,
frames,
tidy,
onTidy,
ai,
onSize,
}: {
frame: Frame;
palette: Palette;
onChange: (patch: Partial ) => void;
onDelete: () => void;
onDuplicate: () => void;
onPreview: () => void;
prompt: string;
onSaveImage: () => Promise;
frames: Frame[];
/** what the tidy button offers: tidy the screen, undo the last tidy, or nothing (already tidy) */
tidy: TidyState;
onTidy: () => void;
ai: AiHooks;
onSize: (preset: FramePreset) => void;
}) {
const lang = useLang();
const host = useHost();
const [copied, setCopied] = useState(false);
const [saving, setSaving] = useState(false);
const [swipeDir, setSwipeDir] = useState("left");
useEffect(() => {
if (!copied) return;
const t = setTimeout(() => setCopied(false), 1400);
return () => clearTimeout(t);
}, [copied]);
const actionBtn = (icon: string, label: string, onClick: () => void, busy?: boolean) => (
{label}
);
return (
{t("screen", lang)}
onChange({ name })} placeholder={t("screenName", lang)} p={p} icon={isPhoneFrame(frame) ? "smartphone" : "desktop_windows"} />
onChange(popHistory(frame.note, frame.noteHistory, "note", "noteHistory"))} p={p} value={frame.note ?? ""} onChange={(note) => onChange({ note: note || undefined })} placeholder={t("screenDescription", lang)} />
onChange({ bg })} p={p} />
{frames.length > 1 && (
options={SWIPE_DIRS.map((d) => ({ key: d.key, icon: d.icon, title: SWIPE_TEXT[lang][d.key], dot: !!frame.swipe?.[d.key] }))}
value={swipeDir}
onChange={setSwipeDir}
p={p}
height={36}
/>
f.id !== frame.id)}
value={frame.swipe?.[swipeDir] ?? null}
onChange={(to) => {
const swipe = { ...(frame.swipe ?? {}) };
if (to) swipe[swipeDir] = to;
else delete swipe[swipeDir];
onChange({ swipe: Object.keys(swipe).length ? swipe : undefined });
}}
p={p}
small
/>
)}
{actionBtn(
copied ? "check" : "content_copy",
copied ? t("copied", lang) : t("prompt", lang),
async () => {
try {
let handled = false;
try { handled = host.onExport?.(prompt, null) === true; } catch {}
if (handled) { setCopied(true); return; }
await navigator.clipboard.writeText(prompt);
setCopied(true);
} catch {}
},
)}
{actionBtn(
"image",
saving ? t("saving", lang) : t("saveImage", lang),
async () => {
setSaving(true);
try {
await onSaveImage();
} finally {
setSaving(false);
}
},
saving,
)}
{prompt}
);
}
export function Inspector({
ai,
item,
palette: p,
frames,
frame,
onChange,
onDelete,
onDuplicate,
multi,
grouped,
onGroup,
onUngroup,
}: {
/** the AI button beside the behavior field */
ai: AiHooks;
item: Item | null;
palette: Palette;
frames: Frame[];
/** frame containing the selected part; its dimensions bound size controls */
frame?: Frame | null;
onChange: (patch: Partial- ) => void;
onDelete: () => void;
onDuplicate: () => void;
multi: number;
/** the selection is exactly one hand-made group */
grouped?: boolean;
onGroup?: () => void;
onUngroup?: () => void;
}) {
const lang = useLang();
const fileRef = useRef
(null);
const slots = item ? iconSlotsOf(item) : [];
const [slotKey, setSlotKey] = useState("icon");
const [pickerOpen, setPickerOpen] = useState(false);
const [actionSlot, setActionSlot] = useState("");
/** editing the "on" look of a toggle button instead of its normal look */
const [onTab, setOnTab] = useState(false);
useEffect(() => {
setSlotKey(item ? (iconSlotsOf(item)[0]?.key ?? "icon") : "icon");
setPickerOpen(false);
setActionSlot("");
setOnTab(false);
}, [item?.id, item?.kind, item?.tabs?.length]);
if (!item) {
if (multi > 1) {
const bigBtn = (icon: string, label: string, onClick?: () => void) => (
{label}
);
return (
{grouped ? t("group", lang) : lang === "en" ? `${multi} ${t("selectedParts", lang)}` : `${multi}${t("selectedParts", lang)}`}
{grouped ? bigBtn("ungroup", t("ungroup", lang), onUngroup) : bigBtn("group_work", t("makeGroup", lang), onGroup)}
{grouped ? t("groupEditNote", lang) : `${t("groupHint", lang)} (Ctrl+G)`}
);
}
return (
);
}
const spec = KIND_SPEC[item.kind];
const frameSize = frame ? frameSizeOf(frame) : { w: PHONE_W, h: PHONE_H };
const mapWidthPreset = (v: number) =>
v === PHONE_W ? frameSize.w : v === CONTENT_W ? contentWidth(frameSize.w) : v === HALF_W ? halfWidth(frameSize.w) : v;
const mapHeightPreset = (v: number) => (v === PHONE_H ? frameSize.h : v === PHONE_H / 2 ? frameSize.h / 2 : v);
const widthMax = (max: number) =>
max === PHONE_W ? frameSize.w : max === CONTENT_W ? contentWidth(frameSize.w) : max;
const heightMax = (max: number) => (max === PHONE_H ? frameSize.h : max);
const editOn = !!item.toggle && onTab;
/* the on-state is edited through the same text / icon / style controls:
* `shown` is what they display, `change` routes their patches into `toggle` */
const shown: Item = editOn
? {
...item,
label: item.toggle?.label ?? item.label,
icon: toggleIcon(item),
variant: item.toggle?.variant ?? item.variant,
}
: item;
const change = (patch: Partial- ) => {
if (!editOn) {
onChange(patch);
return;
}
const next = { ...(item.toggle ?? {}) };
if ("label" in patch) next.label = patch.label;
if ("icon" in patch) next.icon = patch.icon;
if ("variant" in patch) next.variant = patch.variant;
onChange({ toggle: next });
};
const activeSlot: { key: string; value: string | null } | undefined = (() => {
const s = slots.find((x) => x.key === slotKey) ?? slots[0];
return s && editOn && s.key === "icon" ? { ...s, value: shown.icon } : s;
})();
const actionSlots = actionSlotsOf(item);
const slotBtn = (key: string, label: string | undefined, icon: string | null, on: boolean, onClick: () => void, dim?: boolean) => (
{label && {label} }
);
const tabs: NavTab[] = item.tabs ?? [];
const variants = spec.hasVariant ? variantsOf(item.kind) : [];
const setTabCount = (n: number) => {
const next: NavTab[] = [];
const defaults = defaultTabsFor(item.kind);
for (let i = 0; i < n; i++) next.push(tabs[i] ? { ...tabs[i] } : { ...defaults[i % defaults.length] });
onChange({ tabs: next });
};
/** entries of a tab row have no icon; toolbar buttons have no label */
const tabIcons = item.kind !== "tabs";
const tabLabels = item.kind !== "toolbar";
const mainSlots = slots.filter((s) => !s.key.startsWith("tab:"));
const setTabLabel = (i: number, label: string) =>
onChange({ tabs: tabs.map((t, j) => (j === i ? { ...t, label } : t)) });
/** bars, rails and tab rows show one destination as selected */
const hasSelected = item.kind === "bottomNav" || item.kind === "navRail" || item.kind === "tabs";
const selectedTab = Math.min(item.selected ?? 0, Math.max(0, tabs.length - 1));
const hasRadius =
item.kind === "bottomNav" ||
item.kind === "navRail" ||
item.kind === "topAppBar" ||
item.kind === "card" ||
item.kind === "image" ||
item.kind === "box";
return (
{KIND_TEXT[lang][item.kind]?.noun ?? spec.label}
{TOGGLEABLE.includes(item.kind) && (
{
onChange({ toggle: on ? {} : undefined });
setOnTab(on);
setPickerOpen(false);
}}
p={p}
icon="swap_horiz"
label={t("toggle", lang)}
grow
/>
{item.toggle && (
<>
options={[
{ key: "off", icon: "radio_button_unchecked", label: t("normalState", lang) },
{ key: "on", icon: "check_circle", label: t("onState", lang) },
]}
value={onTab ? "on" : "off"}
onChange={(k) => {
setOnTab(k === "on");
setPickerOpen(false);
}}
p={p}
height={36}
/>
{editOn && {t("onStateHint", lang)}
}
>
)}
)}
{(spec.hasLabel || spec.hasSupporting) && (
{spec.hasLabel && (
change({ label })}
placeholder={t("label", lang)}
p={p}
icon="short_text"
/>
{item.kind === "text" && (
onChange({ bold: !item.bold })}
title={t("bold", lang)}
/>
)}
)}
{spec.hasSupporting && !editOn && (
onChange({ supporting })}
placeholder={item.kind === "snackbar" ? t("action", lang) : t("supporting", lang)}
p={p}
icon="notes"
/>
)}
)}
{spec.hasTabs && !editOn && (
({ key: String(n), label: String(n) }))}
value={String(tabs.length)}
onChange={(k) => setTabCount(Number(k))}
p={p}
height={36}
/>
{tabs.map((tab, i) => {
const on = slotKey === `tab:${i}` && pickerOpen;
return (
{hasSelected && (
onChange({ selected: i })}
title={t("selectedTab", lang)}
/>
)}
{tabIcons && (
{
setSlotKey(`tab:${i}`);
setPickerOpen(true);
}}
title={t("changeIcon", lang)}
aria-label={t("changeIcon", lang)}
className="m3-press"
style={{
width: 40,
height: 40,
flex: "0 0 auto",
borderRadius: 20,
border: "none",
background: on ? p.primary : p.surfaceContainerHigh,
color: on ? p.onPrimary : p.onSurfaceVariant,
cursor: "pointer",
display: "grid",
placeItems: "center",
}}
>
)}
{tabLabels && setTabLabel(i, v)} placeholder={t("label", lang)} p={p} height={40} />}
{tabIcons && tab.icon && (
onChange(setIconSlot(item, `tab:${i}`, null))} title={t("noIcon", lang)} />
)}
);
})}
)}
{(item.kind === "image" || item.kind === "card") && !editOn && (
{item.kind === "card" && (
onChange({ noImage: on ? undefined : true })} p={p} icon="image" label={t("imageArea", lang)} grow />
)}
{
const f = e.target.files?.[0];
e.target.value = "";
if (!f) return;
try {
onChange({ src: await readImage(f) });
} catch {}
}}
/>
fileRef.current?.click()}
className="m3-press"
style={{
flex: 1,
height: 44,
borderRadius: 22,
border: "none",
background: p.primary,
color: p.onPrimary,
fontSize: 13,
fontWeight: 600,
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
gap: 8,
}}
>
{t("pickImage", lang)}
{item.src && (
onChange({ src: undefined })} title={t("removeImage", lang)} />
)}
{/* a picture on the web by its address; a picked file shows as data and is not editable here */}
onChange({ src })} placeholder={t("imageUrl", lang)} p={p} />
)}
{mainSlots.length > 0 && activeSlot && !item.src && (
{mainSlots.map((s) =>
slotBtn(
s.key,
mainSlots.length > 1 ? s.label : undefined,
editOn && s.key === "icon" ? shown.icon : s.value,
s.key === activeSlot.key && pickerOpen,
() => {
const on = s.key === activeSlot.key && pickerOpen;
setSlotKey(s.key);
setPickerOpen(!on);
},
!s.value,
),
)}
{activeSlot.value && !activeSlot.key.startsWith("tab:") && (
{
// a slot without an icon cannot be tapped, so its action goes too
const patch: Partial- = setIconSlot(item, activeSlot.key, null);
if (!editOn && item.actions?.[activeSlot.key]) {
const actions = { ...item.actions };
delete actions[activeSlot.key];
patch.actions = Object.keys(actions).length ? actions : undefined;
}
change(patch);
}}
title={t("noIcon", lang)}
/>
)}
)}
{pickerOpen && activeSlot && (
change(setIconSlot(item, activeSlot.key, icon))}
palette={p}
/>
)}
{variants.length > 0 && (
{variants.map((v) => (
change({ variant: v.key })}
/>
))}
)}
{spec.hasFill && !editOn && (
onChange({ fill })}
p={p}
none={item.kind === "card"}
noneOn={item.kind === "card" && !item.fill}
onNone={() => onChange({ fill: undefined })}
/>
{item.kind === "listItem" && (
<>
{t("iconBackground", lang)}
onChange({ iconFill })}
p={p}
none
noneOn={item.iconFill === "none"}
onNone={() => onChange({ iconFill: "none" })}
/>
>
)}
)}
{(spec.hasChecked || spec.hasValue || spec.hasWavy || spec.hasContained || item.kind === "listItem") && !editOn && (
{item.kind === "listItem" && (
/* a switch at the trailing end takes the place of the trailing icon */
onChange({ switch: on || undefined })} p={p} icon="toggle_on" label={t("listSwitch", lang)} grow />
)}
{item.kind === "listItem" && item.switch && (
onChange({ checked })} p={p} icon="toggle_on" label={t("on", lang)} grow />
)}
{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)}
grow
/>
)}
{item.kind === "switch" && (
onChange({ noCheck: on ? undefined : true })} p={p} icon="check" label={t("thumbCheck", lang)} grow />
)}
{spec.hasContained && (
onChange({ contained })}
p={p}
icon="circle"
label={t("container", lang)}
grow
/>
)}
{spec.hasWavy && (
onChange({ wavy })} p={p} icon="airwave" label={t("wavy", lang)} grow />
)}
{spec.hasValue && item.kind !== "slider" && (
onChange({ value: on ? 60 : undefined })}
p={p}
icon="percent"
label={t("determinate", lang)}
grow
/>
)}
{spec.hasValue && (item.kind === "slider" || item.value !== undefined) && (
onChange({ value })}
p={p}
unit="%"
/>
)}
)}
{(spec.size || hasRadius) && !editOn && (
{spec.size && (
<>
onChange({ size })}
p={p}
unit={item.kind === "text" ? "sp" : ""}
/>
{spec.size.presets && (
{(item.kind === "button" || item.kind === "switch") && (
/* these two are as wide as their text unless a width was set; this chip goes back to that */
onChange({ size: undefined })}
aria-pressed={item.size === undefined}
className="m3-press"
style={{
height: 28,
padding: "0 12px",
borderRadius: 14,
border: "none",
background: item.size === undefined ? p.secondaryContainer : p.surfaceContainerHigh,
color: item.size === undefined ? p.onSecondaryContainer : p.onSurfaceVariant,
fontSize: 12,
fontWeight: 600,
cursor: "pointer",
}}
>
{t("autoWidth", lang)}
)}
a - b)}
value={item.size ?? spec.defSize ?? spec.w}
min={spec.size.min}
max={widthMax(spec.size.max)}
onChange={(size) => onChange({ size })}
p={p}
labelOf={item.kind === "text" ? undefined : (v) => widthPresetLabel(v, frameSize.w)}
/>
)}
>
)}
{spec.size2 && (
<>
onChange({ size2 })}
p={p}
/>
{spec.size2.presets && (
onChange({ size2 })}
p={p}
labelOf={(v) => heightPresetLabel(v, frameSize.h)}
/>
)}
>
)}
{hasRadius && (item.kind === "card" || item.kind === "image") && (
onChange({ radiusTop })}
p={p}
/>
)}
{hasRadius && item.kind === "box" && (
onChange(
each
? { corners: { tl: item.radiusTop ?? 0, tr: item.radiusTop ?? 0, bl: item.radiusBottom ?? 0, br: item.radiusBottom ?? 0 } }
: { corners: undefined, radiusTop: item.corners?.tl ?? item.radiusTop, radiusBottom: item.corners?.bl ?? item.radiusBottom },
)
}
p={p}
icon="crop_free"
label={t("cornersEach", lang)}
grow
/>
)}
{hasRadius && item.kind === "box" && item.corners && (
<>
{(["tl", "tr", "bl", "br"] as const).map((k) => (
}
title={t(k === "tl" ? "cornerTl" : k === "tr" ? "cornerTr" : k === "bl" ? "cornerBl" : "cornerBr", lang)}
value={item.corners![k]}
min={0}
max={40}
step={1}
onChange={(v) => onChange({ corners: { ...item.corners!, [k]: v } })}
p={p}
/>
))}
>
)}
{hasRadius && (item.kind === "bottomNav" || item.kind === "navRail" || item.kind === "topAppBar" || (item.kind === "box" && !item.corners)) && (
<>
{/* a rail's two sliders are its left and right sides; the fields are shared with the bars */}
}
title={t(item.kind === "navRail" ? "cornerLeft" : "cornerTop", lang)}
value={item.radiusTop ?? 0}
min={0}
max={40}
step={1}
onChange={(radiusTop) => onChange({ radiusTop })}
p={p}
/>
}
title={t(item.kind === "navRail" ? "cornerRight" : "cornerBottom", lang)}
value={item.radiusBottom ?? 0}
min={0}
max={40}
step={1}
onChange={(radiusBottom) => onChange({ radiusBottom })}
p={p}
/>
>
)}
)}
{(TAPPABLE.includes(item.kind) || actionSlots.length > 0) && frames.length > 0 && !editOn && (
{actionSlots.length > 0 ? (
options={actionSlots.map((s) => ({
key: s.key,
icon: s.value ?? undefined,
label: s.value ? undefined : s.label,
title: s.label,
dot: !!item.actions?.[s.key],
}))}
value={actionSlot || actionSlots[0].key}
onChange={setActionSlot}
p={p}
height={40}
/>
{(() => {
const key = actionSlot || actionSlots[0].key;
return (
{
const actions = { ...(item.actions ?? {}) };
if (a) actions[key] = a;
else delete actions[key];
onChange({ actions: Object.keys(actions).length ? actions : undefined });
}}
p={p}
/>
);
})()}
) : (
onChange({ action })} p={p} />
)}
)}
{!editOn && (
onChange(popHistory(item.note, item.noteHistory, "note", "noteHistory"))}
p={p}
value={item.note ?? ""}
onChange={(note) => onChange({ note })}
placeholder={item.kind === "button" || item.kind === "fab" || item.kind === "iconButton" || item.kind === "extendedFab" ? t("whenPressed", lang) : t("whatItDoes", lang)}
/>
)}
);
}