// ─── ADD THIS BLOCK inside renderLayersPanel (or renderVisibilitySettings), // under the "Canvas" sl-details in webwriter-website-builder.ts render() ────── // // In webwriter-website-builder.ts render(), find the sl-details summary="Canvas" block. // Replace the inner content with the updated version from renderLayersPanel below, // which now includes the background color picker. // // Also in sidebar.ts, renderLayersPanel now accepts the full host and renders // the background color row regardless of layoutMode (moved outside the freeform guard). // ───────────────────────────────────────────────────────────────────────────── import { html } from "lit"; import { keyed } from "lit/directives/keyed.js"; import { msg } from "@lit/localize"; import type { WebwriterWebsiteBuilder } from "../../webwriter-website-builder"; import { ComponentRegistry } from "../../components/registry"; import type { BuilderNode, CodeTab, FlexItemSettings, FlexSettings, GridSettings, LayoutMode, } from "../types"; import { defaultFlexSettings, defaultGridSettings } from "../types"; import { tileGlyph } from "../palette-helpers"; // ─── Visibility Settings ────────────────────────────────────────────────────── export function renderVisibilitySettings(host: WebwriterWebsiteBuilder) { if (!host.isContentEditable) return null; const isStudent = !host.isContentEditable; return html`
${layoutModeChip(host, "freeform", msg("Freeform"), "arrows-move")} ${layoutModeChip(host, "flow", msg("Flow"), "arrow-down")} ${layoutModeChip(host, "flex", "Flex", "distribute-horizontal")} ${layoutModeChip(host, "grid", "Grid", "grid")}
${codeTabChip(host, "combined", msg("Combined"))} ${codeTabChip(host, "html", "HTML")} ${codeTabChip(host, "css", "CSS")}
${msg("Show component settings")} { host.showComponentSettingsInStudent = Boolean( (e.currentTarget as any).checked, ); host.requestUpdate(); }} >
${msg("Show sidebar")} { host.showSidebarInStudent = Boolean( (e.currentTarget as any).checked, ); host.requestUpdate(); }} >
${msg("Show toolbar")} { host.showToolbarInStudent = Boolean( (e.currentTarget as any).checked, ); host.requestUpdate(); }} >
${msg("Allow delete")} { host.allowDeleteInStudent = Boolean( (e.currentTarget as any).checked, ); host.requestUpdate(); }} >
${isStudent ? msg("Student mode") : msg("Author mode")}
`; } function layoutModeChip( host: WebwriterWebsiteBuilder, mode: LayoutMode, label: string, icon: string, ) { const active = !!host.visibleLayoutModes[mode]; return html` `; } function codeTabChip( host: WebwriterWebsiteBuilder, tab: CodeTab, label: string, ) { const active = !!host.visibleCodeTabs[tab]; return html` `; } // ─── Layout Settings ───────────────────────────────────────────────────────── export function renderLayoutSettings(host: WebwriterWebsiteBuilder) { if (host.layoutMode === "flex") { const flex = host.flexSettings; return keyed( "layout-flex", html`
host.layout.setFlexSettings({ direction: e.target.value as FlexSettings["direction"], })} > ${msg("Direction")} (flex-direction) row column
host.layout.setFlexSettings({ justify: e.target.value })} > ${msg("Justify content")} (justify-content) flex-start center flex-end space-between space-around space-evenly
host.layout.setFlexSettings({ align: e.target.value })} > ${msg("Align items")} (align-items) stretch flex-start center flex-end baseline
host.layout.setFlexSettings({ wrap: e.target.value as FlexSettings["wrap"], })} > ${msg("Wrap")} (flex-wrap) nowrap wrap
host.layout.setFlexSettings({ gap: String(e.target.value ?? ""), })} > ${msg("Gap")} (gap)
`, ); } if (host.layoutMode === "grid") { const grid = host.gridSettings; return keyed( "layout-grid", html`
host.layout.setGridSettings({ columns: String(e.target.value ?? ""), })} > ${msg("Columns")} (grid-template-columns)
host.layout.setGridSettings({ rows: String(e.target.value ?? ""), })} > ${msg("Rows")} (grid-template-rows)
host.layout.setGridSettings({ gap: String(e.target.value ?? ""), })} > ${msg("Gap")} (gap)
host.layout.setGridSettings({ autoFlow: e.target.value as GridSettings["autoFlow"], })} > ${msg("Auto flow")} (grid-auto-flow) row row dense column column dense
host.layout.setGridSettings({ justifyItems: e.target .value as GridSettings["justifyItems"], })} > ${msg("Justify items")} (justify-items) stretch start center end
host.layout.setGridSettings({ alignItems: e.target.value as GridSettings["alignItems"], })} > ${msg("Align items")} (align-items) start center end stretch
`, ); } return null; } // ─── Component Settings ─────────────────────────────────────────────────────── export function renderSelectedComponentSettings(host: WebwriterWebsiteBuilder) { const isStudent = !host.isContentEditable; if (isStudent && !host.showComponentSettingsInStudent) return null; const node = host.getSelectedNode(); if (!node) return null; if (node.isContainer) { return renderContainerSettings(host, node); } const component = ComponentRegistry[node.type]; if (!component) return null; const parentContainer = findParentContainer(host, node.id); const custom = component.settings ? component.settings({ data: node.data ?? {}, setData: (patch) => { host.updateNode(node.id, { data: { ...(node.data ?? {}), ...patch }, }); host.requestUpdate(); }, }) : null; const flowDisplayUI = host.layoutMode === "flow" && !parentContainer ? html`

${msg("Flow")}

host.updateNode(node.id, { display: e.target.value as "block" | "inline", })} > ${msg("Display")} (display) block inline
` : null; const rootGridUI = host.layoutMode === "grid" && !parentContainer ? renderGridPlacementUI(host, node) : null; const flexItemUI = parentContainer?.containerLayout === "flex" ? renderFlexItemSettings(host, node, parentContainer) : null; const containerGridUI = parentContainer?.containerLayout === "grid" ? renderGridPlacementUI(host, node) : null; const resolvedBindings = component.bindings?.(); const bindingsUI = resolvedBindings?.length ? html`

${msg("Content")}

${resolvedBindings.map((b) => { const current = node.data?.[b.key] == null ? "" : String(node.data[b.key]); return html`
{ const value = String((e.target as any).value ?? ""); host.updateNode(node.id, { data: { ...(node.data ?? {}), [b.key]: value }, }); host.requestUpdate(); }} >
`; })}
` : null; return html`
${custom ?? null} ${flowDisplayUI} ${rootGridUI} ${flexItemUI} ${containerGridUI} ${bindingsUI}
`; } // ─── Container Settings ─────────────────────────────────────────────────────── function renderContainerSettings( host: WebwriterWebsiteBuilder, node: BuilderNode, ) { const layout = node.containerLayout ?? "flex"; return html`

${msg("Container")}

host.ungroupContainer(node.id)} >${msg("Ungroup")}
host.updateNode(node.id, { containerLayout: e.target.value as LayoutMode, })} > Flow Flex Grid
${layout === "flex" ? renderContainerFlexSettings(host, node) : null} ${layout === "grid" ? renderContainerGridSettings(host, node) : null}
`; } function renderContainerFlexSettings( host: WebwriterWebsiteBuilder, node: BuilderNode, ) { const f = node.containerFlexSettings ?? defaultFlexSettings(); const set = (patch: Partial) => host.updateNode(node.id, { containerFlexSettings: { ...f, ...patch } }); return html`
set({ direction: e.target.value as FlexSettings["direction"] })}> ${msg("Direction")} (flex-direction) row column
set({ justify: e.target.value })}> ${msg("Justify content")} (justify-content) flex-start center flex-end space-between space-around space-evenly
set({ align: e.target.value })}> ${msg("Align items")} (align-items) stretch flex-start center flex-end baseline
set({ wrap: e.target.value as FlexSettings["wrap"] })}> ${msg("Wrap")} (flex-wrap) nowrap wrap
set({ gap: String(e.target.value ?? "") })}> ${msg("Gap")} (gap)
`; } function renderContainerGridSettings( host: WebwriterWebsiteBuilder, node: BuilderNode, ) { const g = node.containerGridSettings ?? defaultGridSettings(); const set = (patch: Partial) => host.updateNode(node.id, { containerGridSettings: { ...g, ...patch } }); return html`
set({ columns: String(e.target.value ?? "") })}> ${msg("Columns")} (grid-template-columns)
set({ rows: String(e.target.value ?? "") })}> ${msg("Rows")} (grid-template-rows)
set({ gap: String(e.target.value ?? "") })}> ${msg("Gap")} (gap)
set({ autoFlow: e.target.value as GridSettings["autoFlow"] })}> ${msg("Auto flow")} (grid-auto-flow) row row dense column column dense
set({ justifyItems: e.target.value as GridSettings["justifyItems"] })}> ${msg("Justify items")} (justify-items) stretch start center end
set({ alignItems: e.target.value as GridSettings["alignItems"] })}> ${msg("Align items")} (align-items) start center end stretch
`; } // ─── Grid Placement UI ──────────────────────────────────────────────────────── function renderGridPlacementUI(host: WebwriterWebsiteBuilder, node: BuilderNode) { const g = node.grid ?? {}; const update = (patch: Partial) => host.updateNode(node.id, { grid: { ...g, ...patch } }); return html`

${msg("Grid placement")}

update({ area: String(e.target.value ?? "") })}> ${msg("Area")} (grid-area)
${gridNumberInput(msg("Column start"), "grid-column-start", g.colStart, (v) => update({ colStart: v }))} ${gridNumberInput(msg("Column span"), "grid-column", g.colSpan, (v) => update({ colSpan: v }))} ${gridNumberInput(msg("Row start"), "grid-row-start", g.rowStart, (v) => update({ rowStart: v }))} ${gridNumberInput(msg("Row span"), "grid-row", g.rowSpan, (v) => update({ rowSpan: v }))}
`; } function gridNumberInput(label: string, property: string, value: number | undefined, onChange: (v: number | undefined) => void) { return html`
{ const v = Number(e.target.value); onChange(Number.isFinite(v) ? Math.max(1, v) : undefined); }} > ${label} (${property})
`; } // ─── Flex Item Settings ─────────────────────────────────────────────────────── function renderFlexItemSettings(host: WebwriterWebsiteBuilder, node: BuilderNode, parent: BuilderNode) { const fi = node.flexItem ?? {}; const set = (patch: Partial) => host.updateNode(node.id, { flexItem: { ...fi, ...patch } }); return html`

${msg("Flex item")}

${msg("Parent direction:")} ${parent.containerFlexSettings?.direction ?? "row"}
set({ alignSelf: e.target.value as FlexItemSettings["alignSelf"] })}> ${msg("Align self")} (align-self) auto (${msg("inherit parent")}) flex-start center flex-end stretch baseline
${flexNumberInput(msg("Flex grow"), "flex-grow", fi.flexGrow, "0", (v) => set({ flexGrow: v }))} ${flexNumberInput(msg("Flex shrink"), "flex-shrink", fi.flexShrink, "1", (v) => set({ flexShrink: v }))}
set({ flexBasis: String(e.target.value ?? "") || undefined })}> ${msg("Flex basis")} (flex-basis)
`; } function flexNumberInput(label: string, property: string, value: number | undefined, placeholder: string, onChange: (v: number | undefined) => void) { return html`
{ const v = Number(e.target.value); onChange(Number.isFinite(v) ? Math.max(0, v) : undefined); }} > ${label} (${property})
`; } // ─── Layers Panel + Background Color ───────────────────────────────────────── // This is the full renderLayersPanel — it now always renders the background // color picker, and renders the layers list only in freeform + author mode. export function renderLayersPanel(host: WebwriterWebsiteBuilder) { const bg = host.canvasBackground ?? "#ffffff"; // Background color presets const presets = [ { label: msg("White"), value: "#ffffff" }, { label: msg("Off-white"), value: "#f8fafc" }, { label: msg("Light grey"),value: "#f1f5f9" }, { label: msg("Dark"), value: "#0f172a" }, { label: msg("Midnight"), value: "#1e293b" }, { label: msg("Charcoal"), value: "#18181b" }, ]; const bgSection = html`
${msg("Background color")}
{ host.canvasBackground = (e.target as HTMLInputElement).value; host.requestUpdate(); }} style=" width: 32px; height: 32px; border: 1px solid var(--sl-color-neutral-300); border-radius: 6px; padding: 2px; cursor: pointer; background: none; flex-shrink: 0; " /> { const val = String(e.target.value ?? "").trim(); if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(val)) { host.canvasBackground = val; host.requestUpdate(); } }} style="flex:1;min-width:50px;" > { host.canvasBackground = "#ffffff"; host.requestUpdate(); }} >
${presets.map( (p) => html` `, )}
`; // Layers list: only in freeform + author mode (unchanged from original) if (host.layoutMode !== "freeform" || !host.isContentEditable) { return bgSection; } const nodes = [...host.freeformNodes].sort( (a, b) => (b.zIndex ?? 0) - (a.zIndex ?? 0), ); return html` ${bgSection}
${msg("Higher = in front. Click to select.")}
${nodes.map((n) => { const comp = n.isContainer ? null : ComponentRegistry[n.type]; const label = comp?.label?.() ?? n.type; const selected = host.selectedNodeId === n.id; const zIdx = n.zIndex ?? 0; return html`
host.selectNodeId(n.id)} > ${tileGlyph(n.type)} ${label}
${zIdx}
`; })}
`; } // ─── Helper ─────────────────────────────────────────────────────────────────── function findParentContainer(host: WebwriterWebsiteBuilder, nodeId: string): BuilderNode | null { const search = (nodes: BuilderNode[], targetId: string): BuilderNode | null => { for (const n of nodes) { if (n.children?.some((c) => c.id === targetId)) return n; if (n.children?.length) { const found = search(n.children, targetId); if (found) return found; } } return null; }; return search(host.activeNodes, nodeId); }