Files
m3e-canvas/web/vendor/components/PartsPalette.tsx
T
pastilhasandClaude Fable 5 240d63940d 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>
2026-09-05 12:00:17 +01:00

124 lines
4.1 KiB
TypeScript

"use client";
import { useMemo, useState } from "react";
import { CATEGORIES, KIND_ORDER, KIND_SPEC, Category, Kind, Palette } from "../lib/tokens";
import { Icon } from "./M3Node";
import { KIND_TEXT, t, useLang } from "../lib/i18n";
import { Field, Section, Tile } from "./ui";
const CATEGORY_TEXT = {
ja: { actions: "操作", navigation: "ナビゲーション", containment: "コンテナ", inputs: "入力", content: "コンテンツ", progress: "進捗" },
zh: { actions: "操作", navigation: "导航", containment: "容器", inputs: "输入", content: "内容", progress: "进度" },
ko: { actions: "동작", navigation: "내비게이션", containment: "컨테이너", inputs: "입력", content: "콘텐츠", progress: "진행 상태" },
} satisfies Record<string, Record<Category, string>>;
export function PartsPalette({
palette: p,
favorites,
onToggleFavorite,
onPartPointerDown,
overBin,
}: {
palette: Palette;
favorites: Kind[];
onToggleFavorite: (k: Kind) => void;
onPartPointerDown: (e: React.PointerEvent, kind: Kind) => void;
overBin: boolean;
}) {
const lang = useLang();
const [q, setQ] = useState("");
const labelOf = (k: Kind) => lang === "en" ? KIND_SPEC[k].label : KIND_TEXT[lang][k]?.noun ?? KIND_SPEC[k].label;
const filtered = useMemo(() => {
const s = q.trim().toLowerCase();
if (!s) return KIND_ORDER;
return KIND_ORDER.filter((k) => {
const sp = KIND_SPEC[k];
return labelOf(k).toLowerCase().includes(s) || sp.label.toLowerCase().includes(s) || sp.noun.includes(s) || k.toLowerCase().includes(s);
});
}, [q, lang]);
const tile = (k: Kind) => {
const s = KIND_SPEC[k];
return (
<Tile
key={k}
icon={s.paletteIcon}
label={labelOf(k)}
p={p}
onPointerDown={(e) => onPartPointerDown(e, k)}
starred={favorites.includes(k)}
onStar={() => onToggleFavorite(k)}
/>
);
};
const grid: React.CSSProperties = {
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(78px, 1fr))",
gap: 6,
};
return (
<div style={{ display: "flex", flexDirection: "column", height: "100%", position: "relative" }}>
<div style={{ padding: "12px 12px 8px" }}>
<Field value={q} onChange={setQ} placeholder={t("search", lang)} p={p} icon="search" height={40} />
</div>
<div className="no-scrollbar" style={{ flex: 1, overflowY: "auto", overflowX: "hidden", padding: "0 8px" }}>
{!q && favorites.length > 0 && (
<Section id="fav" icon="star" title={t("favorites", lang)} p={p}>
<div style={grid}>{favorites.filter((k) => KIND_SPEC[k]).map(tile)}</div>
</Section>
)}
{q ? (
<div style={{ ...grid, padding: "4px 4px 12px" }}>
{filtered.map(tile)}
{filtered.length === 0 && (
<div style={{ gridColumn: "1 / -1", color: p.outline, fontSize: 13, padding: 12, textAlign: "center" }}>
<Icon name="search_off" size={28} />
</div>
)}
</div>
) : (
CATEGORIES.map((c) => (
<Section key={c.key} id={`cat:${c.key}`} icon={c.icon} title={lang === "en" ? c.label : CATEGORY_TEXT[lang][c.key]} p={p}>
<div style={grid}>{KIND_ORDER.filter((k) => KIND_SPEC[k].category === c.key).map(tile)}</div>
</Section>
))
)}
</div>
{overBin && (
<div
style={{
position: "absolute",
inset: 0,
background: "rgba(179,38,30,0.10)",
display: "grid",
placeItems: "center",
pointerEvents: "none",
color: p.error,
borderRadius: "inherit",
}}
>
<div
style={{
width: 72,
height: 72,
borderRadius: 36,
background: p.errorContainer,
color: p.onErrorContainer,
display: "grid",
placeItems: "center",
boxShadow: "0 4px 14px rgba(0,0,0,0.14)",
}}
>
<Icon name="delete" size={34} />
</div>
</div>
)}
</div>
);
}