// ─── 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`
${msg("Layout modes")}
${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")}
${msg("Code tabs")}
${codeTabChip(host, "combined", msg("Combined"))}
${codeTabChip(host, "html", "HTML")}
${codeTabChip(host, "css", "CSS")}
${msg("Student mode")}
${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`
host.layout.setLayoutModeVisible(mode, !active)}
>
${label}
`;
}
function codeTabChip(
host: WebwriterWebsiteBuilder,
tab: CodeTab,
label: string,
) {
const active = !!host.visibleCodeTabs[tab];
return html`
host.setCodeTabVisible(tab, !active)}
>
${label}
`;
}
// ─── 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`
{
host.canvasBackground = p.value;
host.requestUpdate();
}}
style="
width: 22px; height: 22px;
border-radius: 4px;
border: 2px solid ${bg === p.value
? "var(--sl-color-primary-600)"
: "var(--sl-color-neutral-300)"};
background: ${p.value};
cursor: pointer;
padding: 0;
flex-shrink: 0;
box-shadow: ${bg === p.value ? "0 0 0 1px var(--sl-color-primary-300)" : "none"};
"
>
`,
)}
`;
// 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}
{ e.stopPropagation(); host.updateNode(n.id, { zIndex: zIdx + 1 }); }}>▲
${zIdx}
{ e.stopPropagation(); host.updateNode(n.id, { zIndex: Math.max(0, zIdx - 1) }); }}>▼
`;
})}
`;
}
// ─── 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);
}