import { html } from "lit";
import { repeat } from "lit/directives/repeat.js";
import { msg } from "@lit/localize";
import type { WebwriterWebsiteBuilder } from "../../webwriter-website-builder";
import { sortedNodes } from "../layout";
import { ComponentRegistry } from "../../components/registry";
import type { BuilderNode } from "../types";
import { defaultFlexSettings, defaultGridSettings } from "../types";
/**
* Canvas rendering: freeform, flow, flex, grid layouts and node wrappers.
*/
export function renderCanvasInner(host: WebwriterWebsiteBuilder) {
const nodes = host.activeNodes;
const bg = host.canvasBackground ?? "#ffffff"; // ← reads from host
if (nodes.length === 0) {
return html`
${msg("drop components here")}
`;
}
if (host.layoutMode === "freeform") {
return html`
${repeat(nodes, (n) => n.id, (n) => renderNodeFreeform(host, n))}
`;
}
if (host.layoutMode === "flow") {
return html`
${repeat(sortedNodes(nodes), (n) => n.id, (n) => renderNodeFlow(host, n))}
`;
}
if (host.layoutMode === "flex") {
return html`
${repeat(sortedNodes(nodes), (n) => n.id, (n) => renderNodeFlow(host, n))}
`;
}
// Grid
return html`
${repeat(sortedNodes(nodes), (n) => n.id, (n) => renderNodeFlow(host, n))}
`;
}
export function renderNodeFreeform(host: WebwriterWebsiteBuilder, n: BuilderNode) {
const comp = n.isContainer ? null : ComponentRegistry[n.type];
if (!comp && !n.isContainer) return null;
const pos = n.pos ?? { x: 32, y: 32 };
const selected = host.selectedNodeId === n.id || host.selectedIds.has(n.id);
const zIdx = n.zIndex ?? 0;
return html`
host.selection.onWrapperPointerDown(e, n.id)}
@click=${(e: MouseEvent) => host.selection.onWrapperClick(e, n.id)}
@dblclick=${(e: MouseEvent) => host.selection.handleNodeDblClick(e, n.id)}
>
${n.isContainer
? renderContainerContent(host, n)
: comp!.render(n.data ?? comp!.defaultData)}
`;
}
export function renderNodeFlow(host: WebwriterWebsiteBuilder, n: BuilderNode) {
const comp = n.isContainer ? null : ComponentRegistry[n.type];
if (!comp && !n.isContainer) return null;
const selected = host.selectedNodeId === n.id || host.selectedIds.has(n.id);
const display = n.display ?? "block";
const style = host.layoutMode === "grid" ? gridItemStyle(host, n) : "";
return html`
host.selection.onWrapperPointerDown(e, n.id)}
@click=${(e: MouseEvent) => host.selection.onWrapperClick(e, n.id)}
@dblclick=${(e: MouseEvent) => host.selection.handleNodeDblClick(e, n.id)}
>
${n.isContainer
? renderContainerContent(host, n)
: comp!.render(n.data ?? comp!.defaultData)}
`;
}
export function renderContainerContent(host: WebwriterWebsiteBuilder, n: BuilderNode) {
const children = sortedNodes(n.children ?? []);
const style = containerInlineStyle(n);
const isEmpty = children.length === 0;
return html`
${isEmpty
? html`
${msg("Empty container — add items via the sidebar")}
`
: repeat(children, (c) => c.id, (c) => renderNestedNode(host, c, n))}
`;
}
export function renderNestedNode(
host: WebwriterWebsiteBuilder,
n: BuilderNode,
parent: BuilderNode,
) {
const comp = n.isContainer ? null : ComponentRegistry[n.type];
if (!comp && !n.isContainer) return null;
const selected = host.selectedNodeId === n.id;
const parentIsFocused = host.focusedContainerId === parent.id;
const display = n.display ?? "block";
const gridStyle = parent.containerLayout === "grid" ? gridItemStyle(host, n) : "";
const flexStyle = flexItemInlineStyle(n, parent);
const combinedStyle = [gridStyle, flexStyle].filter(Boolean).join(";");
return html`
{
if (parentIsFocused) {
e.stopPropagation();
host.selection.onWrapperPointerDown(e, n.id);
}
}}
@click=${(e: MouseEvent) => {
if (parentIsFocused) {
e.stopPropagation();
host.selection.onWrapperClick(e, n.id);
}
}}
@dblclick=${(e: MouseEvent) => {
if (n.isContainer) host.selection.handleNodeDblClick(e, n.id);
}}
>
${n.isContainer
? renderContainerContent(host, n)
: comp!.render(n.data ?? comp!.defaultData)}
`;
}
// ─── Style helpers ────────────────────────────────────────────────────────────
export function gridItemStyle(host: WebwriterWebsiteBuilder, n: BuilderNode): string {
if (host.layoutMode !== "grid" && n.grid == null) return "";
const g = n.grid ?? {};
const css: string[] = [];
const area = (g.area ?? "").trim();
if (area) {
css.push(`grid-area:${area}`);
} else {
if (typeof g.colStart === "number") {
css.push(`grid-column:${g.colStart} / span ${Math.max(1, Number(g.colSpan ?? 1))}`);
} else if (typeof g.colSpan === "number") {
css.push(`grid-column:span ${Math.max(1, g.colSpan)}`);
}
if (typeof g.rowStart === "number") {
css.push(`grid-row:${g.rowStart} / span ${Math.max(1, Number(g.rowSpan ?? 1))}`);
} else if (typeof g.rowSpan === "number") {
css.push(`grid-row:span ${Math.max(1, g.rowSpan)}`);
}
}
if (g.justifySelf) css.push(`justify-self:${g.justifySelf}`);
if (g.alignSelf) css.push(`align-self:${g.alignSelf}`);
return css.join(";");
}
export function flexItemInlineStyle(n: BuilderNode, parent: BuilderNode): string {
if (parent.containerLayout !== "flex") return "";
const fi = n.flexItem;
if (!fi) return "";
const parts: string[] = [];
if (fi.flexGrow != null) parts.push(`flex-grow:${fi.flexGrow}`);
if (fi.flexShrink != null) parts.push(`flex-shrink:${fi.flexShrink}`);
if (fi.flexBasis) parts.push(`flex-basis:${fi.flexBasis}`);
if (fi.alignSelf) parts.push(`align-self:${fi.alignSelf}`);
return parts.join(";");
}
export function containerInlineStyle(n: BuilderNode): string {
const layout = n.containerLayout ?? "flex";
const parts: string[] = [];
if (layout === "flex") {
const f = n.containerFlexSettings ?? defaultFlexSettings();
parts.push(
`display:flex`,
`flex-direction:${f.direction ?? "row"}`,
`justify-content:${f.justify ?? "flex-start"}`,
`align-items:${f.align ?? "stretch"}`,
`flex-wrap:${f.wrap ?? "nowrap"}`,
`gap:${f.gap ?? "12px"}`,
);
} else if (layout === "grid") {
const g = n.containerGridSettings ?? defaultGridSettings();
parts.push(
`display:grid`,
`grid-template-columns:${g.columns ?? "repeat(3,1fr)"}`,
`grid-auto-rows:${g.rows ?? "auto"}`,
`gap:${g.gap ?? "12px"}`,
`grid-auto-flow:${g.autoFlow ?? "row"}`,
`justify-items:${g.justifyItems ?? "stretch"}`,
`align-items:${g.alignItems ?? "start"}`,
);
}
return parts.join(";");
}