// @ts-nocheck — vendored, see PROVENANCE.md
"use client";
import { motion } from "motion/react";
import {
FAB_MENU_GAP,
FAB_MENU_ITEM_H,
H,
Item,
Kind,
MEASURED,
NAV_BAR_H,
Palette,
Radii,
STATUS_BAR_H,
baseRadii,
cardFillOf,
onToken,
scaleR,
sizeOf,
variantShadow,
variantStyle,
SETTLE_MS,
RAIL_W,
RAIL_TOP,
RAIL_ITEM_H,
RAIL_GAP,
} from "../lib/tokens";
import { CircularProgress, LinearProgress, LoadingIndicator } from "./Loading";
import { t, useLang } from "../lib/i18n";
import { useTheme } from "../lib/theme";
/** weight of a heading or label: heavier under the emphasized type setting */
const useWeight = () => {
const emphasized = useTheme().emphasized;
return (normal: number, strong: number) => (emphasized ? strong : normal);
};
export function Icon({
name,
size = 24,
color,
fill,
weight,
}: {
name: string;
size?: number;
color?: string;
fill?: boolean;
weight?: number;
}) {
return (
{name}
);
}
const ellipsis = {
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
} as const;
const NO_BOX: Kind[] = [
"circularProgress",
"linearProgress",
"loadingIndicator",
"switch",
"checkbox",
"radio",
"slider",
"text",
"divider",
"splitButton",
"fabMenu",
"badge",
];
/** Padding follows M3: icon+label is tighter than label alone. */
export function ButtonContent({ item }: { item: Item }) {
const w = useWeight();
const hasIcon = !!item.icon;
const hasLabel = item.label.trim().length > 0;
const padX = hasLabel ? (hasIcon ? 22 : 26) : 16;
return (
{hasIcon && }
{hasLabel && {item.label}}
);
}
function ExtendedFabContent({ item }: { item: Item }) {
const w = useWeight();
const hasIcon = !!item.icon;
const hasLabel = item.label.trim().length > 0;
return (
{hasIcon && }
{hasLabel && {item.label}}
);
}
function ChipContent({ item, p }: { item: Item; p: Palette }) {
const on = !!item.checked;
const lead = on ? "check" : item.icon;
return (
{lead && }
{item.label}
);
}
function SwitchContent({ item, p }: { item: Item; p: Palette }) {
const hasLabel = item.label.trim().length > 0;
return (
{hasLabel && {item.label}}
);
}
/** the M3 switch track and handle, 52 × 32 */
function SwitchControl({ on, noCheck, p }: { on: boolean; noCheck?: boolean; p: Palette }) {
return (
{on && !noCheck && }
);
}
function CheckboxContent({ item, p }: { item: Item; p: Palette }) {
const on = !!item.checked;
const hasLabel = item.label.trim().length > 0;
return (
{on && }
{hasLabel && {item.label}}
);
}
function TextContent({ item, p }: { item: Item; p: Palette }) {
const w = useWeight();
const fs = item.size ?? 28;
return (
= 22 ? 600 : 500),
letterSpacing: fs >= 28 ? -0.25 : 0,
color: p.onSurface,
whiteSpace: "nowrap",
padding: "0 2px",
}}
>
{item.label || " "}
);
}
/** A split button: the labeled action and, after a hairline gap, a menu trigger.
* The two halves keep their own corners, so the box around them stays plain. */
function SplitButtonContent({ item, p }: { item: Item; p: Palette }) {
const w = useWeight();
const st = variantStyle(item.variant, p);
const outer = scaleR(28);
const inner = scaleR(8);
const hasLabel = item.label.trim().length > 0;
const shadow = variantShadow(item.variant);
return (
{item.icon && }
{hasLabel && {item.label}}
);
}
function RadioContent({ item, p }: { item: Item; p: Palette }) {
const on = !!item.checked;
const hasLabel = item.label.trim().length > 0;
return (
{on && }
{hasLabel && {item.label}}
);
}
/** A badge: a 6dp dot when it has no text, a 16dp pill with the count otherwise. */
function BadgeContent({ item, p }: { item: Item; p: Palette }) {
const text = item.label.trim();
if (!text) return ;
return (
{text}
);
}
/** Content for kinds that size to their text; rendered again offscreen to measure. */
export function MeasuredContent({ item, p }: { item: Item; p: Palette }) {
switch (item.kind) {
case "button":
return ;
case "extendedFab":
return ;
case "chip":
return ;
case "switch":
return ;
case "checkbox":
return ;
case "text":
return ;
case "splitButton":
return ;
case "radio":
return ;
case "badge":
return ;
default:
return null;
}
}
function Body({ item, p }: { item: Item; p: Palette }) {
const lang = useLang();
const w = useWeight();
const hasLabel = item.label.trim().length > 0;
const hasSupporting = !!item.supporting?.trim();
if (MEASURED.includes(item.kind)) return ;
switch (item.kind) {
case "box":
return item.checked ? (
) : null;
case "iconButton": {
const s = item.size ?? 48;
return (
{item.icon && }
);
}
case "fab": {
const s = item.size ?? 56;
return (
{item.icon && }
);
}
case "topAppBar":
return (
{item.icon && }
{item.label}
{item.icon2 && }
);
case "searchBar":
return (
{item.icon &&
}
{item.label}
{item.icon2 &&
}
);
case "card": {
const cw = item.size ?? 320;
return (
{!item.noImage && (
{item.src ? (
// eslint-disable-next-line @next/next/no-img-element

) : (
item.icon &&
)}
)}
{hasLabel && (
{item.label}
)}
{hasSupporting && (
{item.supporting}
)}
);
}
case "listItem": {
const iconBg = item.iconFill === "none" ? null : (item.iconFill ?? "primaryContainer");
return (
{item.icon && (
)}
{hasLabel &&
{item.label}
}
{hasSupporting && (
{item.supporting}
)}
{item.switch ?
: item.icon2 &&
}
);
}
case "dialog":
return (
{item.icon && (
)}
{hasLabel && (
{item.label}
)}
{hasSupporting && (
{item.supporting}
)}
{[t("cancel", lang), t("ok", lang)].map((t) => (
{t}
))}
);
case "snackbar":
return (
{item.label}
{hasSupporting && (
{item.supporting}
)}
);
case "textField": {
const filled = item.variant === "filled";
return (
{item.icon && }
{filled ? "" : ""}
{hasLabel && (
{item.label}
)}
{filled && (
)}
{hasSupporting && (
{item.supporting}
)}
);
}
case "slider": {
const v = Math.min(100, Math.max(0, item.value ?? 40)) / 100;
const w = item.size ?? 280;
const handleX = 2 + (w - 4) * v;
return (
);
}
case "image":
if (item.src) {
return (
// eslint-disable-next-line @next/next/no-img-element
);
}
return (
{item.icon && }
);
case "divider":
return (
);
case "navRail": {
const tabs = item.tabs ?? [];
return (
{tabs.map((t, i) => {
const on = i === Math.min(item.selected ?? 0, Math.max(0, tabs.length - 1));
const withLabel = t.label.trim().length > 0;
return (
{t.icon && }
{withLabel && (
{t.label}
)}
);
})}
);
}
case "bottomNav": {
const tabs = item.tabs ?? [];
return (
{tabs.map((t, i) => {
const on = i === Math.min(item.selected ?? 0, Math.max(0, tabs.length - 1));
const withLabel = t.label.trim().length > 0;
return (
{t.icon && }
{withLabel && (
{t.label}
)}
);
})}
);
}
case "circularProgress":
return (
);
case "linearProgress":
return (
);
case "fabMenu": {
const tabs = item.tabs ?? [];
const filled = item.variant === "filled";
const fabStyle = variantStyle(item.variant, p);
const itemStyle = filled
? { background: p.primaryContainer, color: p.onPrimaryContainer }
: { background: p.secondaryContainer, color: p.onSecondaryContainer };
return (
{tabs.map((tab, i) => (
{tab.icon && }
{tab.label}
))}
{item.icon && }
);
}
case "toolbar": {
const tabs = item.tabs ?? [];
const vibrant = item.variant === "filled";
return (
{tabs.map((tab, i) => (
{tab.icon && }
))}
);
}
case "tabs": {
const tabs = item.tabs ?? [];
return (
{tabs.map((tab, i) => {
const on = i === Math.min(item.selected ?? 0, Math.max(0, tabs.length - 1));
return (
{tab.label}
{on && (
)}
);
})}
);
}
case "loadingIndicator": {
const s = item.size ?? 48;
return (
);
}
}
return null;
}
function boxStyle(item: Item, p: Palette): React.CSSProperties {
if (NO_BOX.includes(item.kind)) return { background: "transparent", border: "none" };
switch (item.kind) {
case "box": {
const t = item.fill ?? "surfaceContainerLow";
return { background: p[t], color: onToken(t, p), border: "none" };
}
case "button":
case "iconButton":
case "fab":
case "extendedFab":
return variantStyle(item.variant, p);
case "chip":
if (item.checked) return { background: p.secondaryContainer, color: p.onSecondaryContainer, border: "none" };
return item.variant === "outlined"
? { background: "transparent", color: p.onSurfaceVariant, border: `1px solid ${p.outlineVariant}` }
: { background: p.surfaceContainerLow, color: p.onSurfaceVariant, border: "none" };
case "card":
return { background: p[cardFillOf(item)], border: item.variant === "outlined" ? `1px solid ${p.outlineVariant}` : "none" };
case "textField":
return item.variant === "filled"
? { background: p.surfaceContainerHighest, border: "none", color: p.onSurface }
: { background: p.surface, border: `1px solid ${p.outline}`, color: p.onSurface };
case "topAppBar":
case "bottomNav":
case "navRail":
return { background: p.surfaceContainer, border: "none", color: p.onSurface };
case "toolbar":
return item.variant === "filled"
? { background: p.primaryContainer, border: "none", color: p.onPrimaryContainer }
: { background: p.surfaceContainer, border: "none", color: p.onSurfaceVariant };
case "tabs":
return { background: p.surface, border: "none", color: p.onSurface };
case "searchBar":
return { background: p.surfaceContainerHigh, border: "none", color: p.onSurface };
case "dialog":
return { background: p.surfaceContainerHigh, border: "none", color: p.onSurface };
case "snackbar":
return { background: p.inverseSurface, border: "none", color: p.inverseOnSurface };
case "image":
return { background: p.surfaceContainerHighest, border: "none" };
case "listItem": {
const t = item.fill ?? "surfaceContainerLow";
return { background: p[t], border: "none", color: onToken(t, p) };
}
default:
return { background: p.surfaceContainerHigh, border: "none", color: p.onSurface };
}
}
function shadowOf(item: Item): string {
if (NO_BOX.includes(item.kind)) return "none";
switch (item.kind) {
case "button":
case "iconButton":
case "extendedFab":
return variantShadow(item.variant);
case "fab":
return "0 3px 8px rgba(0,0,0,0.18), 0 1px 3px rgba(0,0,0,0.12)";
case "card":
return item.variant === "elevated" ? "0 1px 3px rgba(0,0,0,0.20), 0 2px 6px rgba(0,0,0,0.10)" : "none";
case "dialog":
return "0 8px 24px rgba(0,0,0,0.18), 0 2px 6px rgba(0,0,0,0.10)";
case "snackbar":
return "0 3px 8px rgba(0,0,0,0.18)";
case "toolbar":
return "0 2px 6px rgba(0,0,0,0.14), 0 1px 2px rgba(0,0,0,0.10)";
default:
return "none";
}
}
export type { Radii };
/** Corner radii are driven continuously by the magnet; a stiff spring keeps them on the pointer. */
const RADIUS_TWEEN = { type: "spring" as const, stiffness: 900, damping: 48, mass: 0.4 };
export function M3Node({
item,
palette,
radii,
widths,
pressed,
dragging,
selected,
interactive = true,
onPointerDown,
}: {
item: Item;
palette: Palette;
radii?: Radii;
widths: Record;
pressed?: boolean;
dragging?: boolean;
selected?: boolean;
interactive?: boolean;
onPointerDown?: (e: React.PointerEvent) => void;
}) {
const r = radii ?? baseRadii(item);
const size = sizeOf(item, widths);
const measured = MEASURED.includes(item.kind) && !((item.kind === "switch" || item.kind === "button") && item.size);
const clips = !NO_BOX.includes(item.kind) && item.kind !== "textField";
return (
);
}
/** Plain (non-animated) rendering of a part; used where frames must be deterministic. */
export function M3Static({
item,
palette,
radii,
style,
}: {
item: Item;
palette: Palette;
radii?: Radii;
style?: React.CSSProperties;
}) {
const r = radii ?? baseRadii(item);
const size = sizeOf(item, {});
const measured = MEASURED.includes(item.kind) && !((item.kind === "switch" || item.kind === "button") && item.size);
const clips = !NO_BOX.includes(item.kind) && item.kind !== "textField";
return (
);
}