vendor m3e-canvas 5c83ac6, with the import rewrite scripted

upstream imports through a @/ alias, and this platform already binds
@/* to src/apps/officer-web and @/components/* to its own shared
components — so an unrewritten import resolves to the WRONG module
rather than failing. the rewrite is mechanical and lives in
scripts/vendor.sh so a re-vendor produces a readable diff.

lib/ai.ts and AiPanel are vendored despite being unwanted: deleting them
meant editing vendored code, and Canvas.tsx references them throughout.
a faithful copy carrying an unused module beats a doctored one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 12:00:17 +01:00
co-authored by Claude Fable 5
commit 240d63940d
39 changed files with 17299 additions and 0 deletions
+341
View File
@@ -0,0 +1,341 @@
"use client";
import { ReactNode, useMemo, useState } from "react";
import { Reorder, useDragControls } from "motion/react";
import { Frame, Group, Item, KIND_SPEC, Palette, explodeGroup, isPhoneFrame } from "../lib/tokens";
import { Icon } from "./M3Node";
import { Lang, KIND_TEXT, t, useLang } from "../lib/i18n";
/* Rows never animate their size: opening a row only adds rows under it, so
* nothing stretches. Only the drag itself moves.
*
* The panel is a tree with two kinds of level. A z level lists what is drawn on
* top of what, front first: the runs of a screen, and the runs hidden inside a
* free group. A run level lists the parts of one connected run in reading order
* (left to right, top to bottom). Every level is reordered by dragging the
* handle; the page turns the new order back into a group. */
function nameOf(it: Item, lang: Lang) {
const spec = KIND_SPEC[it.kind];
const noun = KIND_TEXT[lang][it.kind]?.noun ?? spec.label;
return it.label.trim() || (it.kind === "iconButton" || it.kind === "fab" ? (it.icon ?? noun) : noun);
}
function runLabel(g: Group, lang: Lang) {
const first = g.items[0];
const noun = KIND_TEXT[lang][first.kind]?.noun ?? KIND_SPEC[first.kind].label;
return g.free ? `${t("group", lang)} × ${g.items.length}` : g.items.length > 1 ? `${noun} × ${g.items.length}` : nameOf(first, lang);
}
function Row({
id,
p,
depth,
icon,
label,
on,
onSelect,
open,
onToggle,
onDragging,
children,
}: {
id: string;
p: Palette;
depth: number;
icon: ReactNode;
label: string;
on: boolean;
onSelect: (add: boolean) => void;
/** set when the row can open to show what it holds */
open?: boolean;
onToggle?: () => void;
onDragging: (dragging: boolean) => void;
children?: ReactNode;
}) {
const lang = useLang();
const controls = useDragControls();
const h = depth === 0 ? 40 : 36;
return (
<Reorder.Item value={id} layout="position" transition={{ layout: { duration: 0 } }} dragListener={false} dragControls={controls} onDragStart={() => onDragging(true)} onDragEnd={() => onDragging(false)} style={{ listStyle: "none", display: "flex", flexDirection: "column", gap: 4, position: "relative" }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: 6,
height: h,
padding: "0 6px 0 2px",
marginLeft: depth * 14,
borderRadius: depth === 0 ? 14 : 12,
background: on ? p.secondaryContainer : depth === 0 ? p.surfaceContainerLow : p.surface,
color: on ? p.onSecondaryContainer : p.onSurface,
userSelect: "none",
}}
>
<span
onPointerDown={(e) => {
e.preventDefault();
controls.start(e);
}}
style={{ cursor: "grab", color: p.outline, display: "grid", placeItems: "center", width: depth === 0 ? 24 : 20, height: h, touchAction: "none" }}
>
<Icon name="drag_indicator" size={18} />
</span>
<button
onClick={(e) => onSelect(e.shiftKey)}
style={{
flex: 1,
minWidth: 0,
height: h,
display: "flex",
alignItems: "center",
gap: 8,
border: "none",
background: "transparent",
color: "inherit",
cursor: "pointer",
padding: 0,
textAlign: "left",
}}
>
<span style={{ display: "inline-flex", gap: 2, color: on ? p.onSecondaryContainer : p.primary }}>{icon}</span>
<span style={{ fontSize: 12, fontWeight: depth === 0 ? 600 : 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{label}</span>
</button>
{onToggle && (
<button
onClick={onToggle}
title={t(open ? "hideParts" : "showParts", lang)}
aria-expanded={open}
className="m3-press"
style={{ width: 28, height: 28, borderRadius: 14, border: "none", background: "transparent", color: on ? p.onSecondaryContainer : p.onSurfaceVariant, cursor: "pointer", padding: 0, display: "grid", placeItems: "center", flex: "0 0 auto" }}
>
<span style={{ display: "inline-flex", transform: open ? "rotate(90deg)" : "none", transition: "transform 160ms" }}>
<Icon name="chevron_right" size={20} />
</span>
</button>
)}
</div>
{open && children}
</Reorder.Item>
);
}
/** One reorderable level: drag the handles; `values` is the shown order. */
function Level({ values, onReorder, children }: { values: string[]; onReorder: (next: string[]) => void; children: ReactNode }) {
return (
<Reorder.Group axis="y" values={values} onReorder={onReorder} style={{ display: "flex", flexDirection: "column", gap: 4, padding: 0, margin: 0 }}>
{children}
</Reorder.Group>
);
}
/** the parts of one connected run, in reading order */
function RunParts({
run,
p,
depth,
sel,
onSelect,
onReorder,
onDragging,
}: {
run: Group;
p: Palette;
depth: number;
sel: Set<string>;
onSelect: (itemIds: string[], add: boolean) => void;
/** the run's parts in a new reading order */
onReorder: (ids: string[]) => void;
onDragging: (dragging: boolean) => void;
}) {
const lang = useLang();
const ids = run.items.map((it) => it.id);
return (
<Level values={ids} onReorder={onReorder}>
{run.items.map((it) => (
<Row
key={it.id}
id={it.id}
p={p}
depth={depth}
icon={<Icon name={KIND_SPEC[it.kind].paletteIcon} size={16} />}
label={nameOf(it, lang)}
on={sel.has(it.id)}
onSelect={(add) => onSelect([it.id], add)}
onDragging={onDragging}
/>
))}
</Level>
);
}
/** The z-order of one screen, top layer first, reordered by dragging the handles.
* A connected run opens to its parts in reading order; a free group opens to the
* runs it holds, front first, and each of those opens in turn. */
export function LayersPanel({
p,
frames,
frameId,
onFrame,
groups,
widths,
selectedIds,
onSelect,
onReorder,
onReorderItems,
onDragging,
}: {
p: Palette;
frames: Frame[];
frameId: string | null;
onFrame: (id: string) => void;
/** the runs on the chosen frame, in canvas order (bottom first) */
groups: Group[];
widths: Record<string, number>;
selectedIds: string[];
/** `add` is set when Shift was held, to extend the selection */
onSelect: (itemIds: string[], add: boolean) => void;
/** new order, top layer first */
onReorder: (topFirst: string[]) => void;
/** a group's parts in a new order: back to front for a free group, reading order for a run */
onReorderItems: (groupId: string, ids: string[]) => void;
/** a drag on any level starting or ending, so the page can record one undo step for the whole drag */
onDragging: (dragging: boolean) => void;
}) {
const lang = useLang();
const topFirst = useMemo(() => [...groups].reverse(), [groups]);
const ids = topFirst.map((g) => g.id);
const sel = new Set(selectedIds);
const [openIds, setOpenIds] = useState<Set<string>>(() => new Set());
const toggle = (id: string) =>
setOpenIds((cur) => {
const next = new Set(cur);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
/** the runs hidden in a free group, front first, and the flat back-to-front list they make */
const freeRuns = (g: Group) => [...explodeGroup(g, widths)].reverse();
const flatten = (runsTopFirst: Group[]) => [...runsTopFirst].reverse().flatMap((r) => r.items.map((it) => it.id));
const groupBody = (g: Group, depth: number) => {
if (!g.free) {
return <RunParts run={g} p={p} depth={depth} sel={sel} onSelect={onSelect} onReorder={(order) => onReorderItems(g.id, order)} onDragging={onDragging} />;
}
const runs = freeRuns(g);
const runIds = runs.map((r) => r.id);
const byId = new Map(runs.map((r) => [r.id, r]));
const reorderRuns = (next: string[]) => onReorderItems(g.id, flatten(next.map((id) => byId.get(id)).filter((r): r is Group => !!r)));
return (
<Level values={runIds} onReorder={reorderRuns}>
{runs.map((r) => {
const many = r.items.length > 1;
const open = many && openIds.has(r.id);
return (
<Row
key={r.id}
id={r.id}
p={p}
depth={depth}
icon={r.items.slice(0, 3).map((it, k) => <Icon key={k} name={KIND_SPEC[it.kind].paletteIcon} size={16} />)}
label={runLabel(r, lang)}
on={r.items.some((it) => sel.has(it.id))}
onSelect={(add) => onSelect(r.items.map((it) => it.id), add)}
open={many ? open : undefined}
onToggle={many ? () => toggle(r.id) : undefined}
onDragging={onDragging}
>
<RunParts
run={r}
p={p}
depth={depth + 1}
sel={sel}
onSelect={onSelect}
onDragging={onDragging}
onReorder={(order) => {
/* the run's members take each other's places in the list; every other part keeps its own */
const members = new Set(r.items.map((it) => it.id));
let k = 0;
onReorderItems(
g.id,
g.items.map((it) => (members.has(it.id) ? order[k++] : it.id)),
);
}}
/>
</Row>
);
})}
</Level>
);
};
return (
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
{frames.length > 1 && (
<div className="no-scrollbar" style={{ display: "flex", gap: 6, padding: "12px 12px 4px", overflowX: "auto", flex: "0 0 auto" }}>
{frames.map((f) => {
const on = f.id === frameId;
return (
<button
key={f.id}
onClick={() => onFrame(f.id)}
className="m3-press"
style={{
height: 32,
padding: "0 12px 0 8px",
borderRadius: 16,
border: "none",
background: on ? p.primary : p.surfaceContainerHigh,
color: on ? p.onPrimary : p.onSurfaceVariant,
fontSize: 12,
fontWeight: 600,
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
gap: 6,
whiteSpace: "nowrap",
flex: "0 0 auto",
}}
>
<Icon name={isPhoneFrame(f) ? "smartphone" : "desktop_windows"} size={16} />
{f.name || t("screen", lang)}
</button>
);
})}
</div>
)}
<div className="no-scrollbar" style={{ flex: 1, overflowY: "auto", padding: "8px 10px 12px" }}>
{topFirst.length === 0 ? (
<div style={{ padding: 24, textAlign: "center", color: p.outline, fontSize: 12 }}>
<Icon name="layers_clear" size={32} />
<div style={{ marginTop: 8 }}>{t("noLayers", lang)}</div>
</div>
) : (
<Level values={ids} onReorder={onReorder}>
{topFirst.map((g) => {
const canOpen = g.free || g.items.length > 1;
const open = canOpen && openIds.has(g.id);
return (
<Row
key={g.id}
id={g.id}
p={p}
depth={0}
icon={g.free ? <Icon name="group_work" size={18} /> : g.items.slice(0, 3).map((it, k) => <Icon key={k} name={KIND_SPEC[it.kind].paletteIcon} size={18} />)}
label={runLabel(g, lang)}
on={g.items.some((it) => sel.has(it.id))}
onSelect={(add) => onSelect(g.items.map((it) => it.id), add)}
open={canOpen ? open : undefined}
onToggle={canOpen ? () => toggle(g.id) : undefined}
onDragging={onDragging}
>
{groupBody(g, 1)}
</Row>
);
})}
</Level>
)}
</div>
</div>
);
}