Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
IMAGE_NON_MANAGED_HTML_ATTRS,
resolveRenderedButtonVariant,
} from "../../../../lib/blocks/renderSemantics";
import { resolveNodeClasses } from "../../../../lib/blocks/resolveNodeClasses";
import { BUTTON_VARIANT_ATTRIBUTE } from "../../../../lib/blocks/buttonVariants";
import {
buildButtonContentRowStyle,
Expand Down Expand Up @@ -1065,7 +1064,7 @@ export function useStageLiveCanvasUpdates(
payload.props.icon !== undefined
? payload.props.icon
: (element.getAttribute("icon") ?? ""),
classNameValue: existingNode ? resolveNodeClasses(existingNode) : "",
classNameValue: existingNode?.props?.className,
ariaLabelValue:
payload.props.ariaLabel !== undefined
? payload.props.ariaLabel
Expand Down
16 changes: 14 additions & 2 deletions aria/lib/blocks/nodesToHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,18 @@ function getResponsiveStyleSelector(nodeId: string, tag = "div"): string {
return `${tag}.${getResponsiveStyleClass(nodeId)}`;
}

function getRenderedStyleTag(node: BuilderNode, props: JsonObject): string {
const normalizedType = normalizeContainerNodeType(node.type).toLowerCase();
if (
normalizedType === "icon" &&
getCanonicalIconIdFromValue(props.icon)
) {
return "svg";
}

return getNativeTagForRenderableNode(node, props) ?? "div";
}

function collectNodeResponsiveCssRules(
styles: StyleMap = {},
nodeId: string,
Expand Down Expand Up @@ -1319,7 +1331,7 @@ function collectResponsiveStyleRules(
);

if (Object.keys(renderStyles).length > 0) {
const tag = getNativeTagForRenderableNode(node, renderProps) ?? "div";
const tag = getRenderedStyleTag(node, renderProps);
collectNodeResponsiveCssRules(
renderStyles,
node.id,
Expand Down Expand Up @@ -1415,7 +1427,7 @@ export function collectNodeStylesheet(
);

if (hasRenderableStyles(resolvedStyles)) {
const tag = getNativeTagForRenderableNode(node, renderProps) ?? "div";
const tag = getRenderedStyleTag(node, renderProps);
collectNodeStylesheetRules(
resolvedStyles,
node.id,
Expand Down
45 changes: 45 additions & 0 deletions aria/tests/features/stage/useStageLiveCanvasUpdates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ vi.mock("../../../admin/features/Core", () => ({

import { useStageLiveCanvasUpdates } from "../../../admin/features/Stage/composables/useStageLiveCanvasUpdates";
import { CANVAS_DISABLED_ATTRIBUTE } from "../../../admin/features/Stage/utils/canvasRenderAttributes";
import { hydrateIconHost } from "../../../admin/features/Stage/utils/canvasIconHydration";

function createImageNode(id = "image-1"): BuilderNode {
return {
Expand Down Expand Up @@ -747,6 +748,50 @@ describe("useStageLiveCanvasUpdates", () => {
wrapper.unmount();
});

it("keeps node utility classes off the icon host during live icon updates", () => {
const stageDocument = createIconStageDocument();
const iframeRef = {
value: { contentDocument: stageDocument } as HTMLIFrameElement,
};
const iconNode = {
...createIconNode(),
props: {
icon: "i-lucide:star",
className: "icon-content",
},
classNames: {
base: ["size-6"],
},
} satisfies BuilderNode;

const { wrapper } = mountLiveUpdates({
iframeRef,
blocks: [iconNode],
});

bridgeCallbacks.propsUpdate?.({
nodeId: "icon-1",
props: {
icon: "i-lucide:activity",
},
source: "inspector-live",
});

expect(vi.mocked(hydrateIconHost)).toHaveBeenCalledWith(
expect.objectContaining({
iconValue: "i-lucide:activity",
classNameValue: "icon-content",
}),
);
expect(vi.mocked(hydrateIconHost)).not.toHaveBeenCalledWith(
expect.objectContaining({
classNameValue: "size-6",
}),
);

wrapper.unmount();
});

it("keeps disabled canvas buttons selectable during live prop updates", () => {
const stageDocument = createButtonStageDocument();
const iframeRef = {
Expand Down
27 changes: 27 additions & 0 deletions aria/tests/lib/nodesToHtml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,33 @@ describe("nodesToHtml", () => {
expect(html).not.toContain("aria-icon-alias.js");
});

it("targets inline canonical icon SVGs with generated size styles", () => {
const html = nodesToHtmlDocument(
[
createNode({
id: "feature-icon",
type: "icon",
props: {
icon: normalizeIconValue("i-lucide:circle-check"),
},
styles: {
width: { base: "60px" },
height: { base: "60px" },
fontSize: { base: "60px" },
},
}),
],
withIcons(),
);

expect(html).toContain('<svg xmlns="http://www.w3.org/2000/svg"');
expect(html).toContain('class="aria-feature-icon"');
expect(html).toContain(
"svg.aria-feature-icon { font-size: 60px; width: 60px; height: 60px; }",
);
expect(html).not.toContain("i.aria-feature-icon {");
});

it("does not leak raw icon props into rendered icon markup", () => {
const html = nodesToHtmlDocument(
[
Expand Down
44 changes: 44 additions & 0 deletions aria/tests/lib/rendering/renderPageDslToHtml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { renderPageDslToHtml } from "../../../lib/rendering/renderPageDslToHtml"
import { renderPageHtmlFromStorage } from "../../../lib/rendering/renderPageHtml";
import { createDefaultUniversalDesignSystem } from "../../../lib/styles/universalDesignSystem";
import type { AriaCollection, AriaEntryRecord } from "../../../lib/cms/schemas";
import { createIconAssetFetcher } from "../../helpers/iconAssetFetcher";

const fullWidthLayout: LayoutDSL = {
id: "full-width",
Expand Down Expand Up @@ -222,6 +223,49 @@ describe("renderPageDslToHtml", () => {
expect(html).not.toContain('rel="canonical"');
});

it("inlines canonical Lucide icons from the runtime static-assets binding", async () => {
const page: PageDSL = {
id: "icons",
slug: "icons",
title: "Icons",
nodes: [
{
id: "feature-icon",
type: "Icon",
props: {
icon: {
id: "lucide:star",
pack: "lucide",
name: "star",
source: "iconify",
version: "2026-02-25-snapshot",
},
ariaLabel: "Feature icon",
},
styles: {},
children: [],
},
],
};

const { html } = await renderPageDslToHtml({
page,
adapter: createAdapter(null),
locals: {
assetOrigin: "http://aria.test",
cfBindings: {
aria_assets: createIconAssetFetcher(),
},
},
});

expect(html).toContain(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"',
);
expect(html).toContain("<path");
expect(html).not.toContain('class="i-lucide:star"');
});

it("merges slot-only layout defaultContent into the document body", async () => {
const page: PageDSL = {
id: "home",
Expand Down
3 changes: 2 additions & 1 deletion src/pages/404.astro
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ try {
const rendered = await renderPageDslToHtml({
page: notFoundPage,
adapter,
locals: Astro.locals,
logger: (level, message, meta) => {
log(level, `[404] ${message}`, meta);
},
Expand Down Expand Up @@ -134,4 +135,4 @@ if (!html) {
}
---

<Fragment set:html={html} />
<Fragment set:html={html} />
5 changes: 5 additions & 0 deletions src/pages/[...slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Public requests resolve the published page revision only. Preview.
*/

import type { RuntimeLocals } from "../../aria/lib/cloudflare/env";
import { getStorageAdapterAsync } from "../../aria/lib/storage/getStorageAdapter";
import type { LayoutDSL, PageDSL } from "../../aria/lib/types/nodes";
import { LayoutDSLSchema, PageDSLSchema } from "../../aria/lib/schemas/nodes";
Expand Down Expand Up @@ -348,6 +349,7 @@ async function renderPageToHtml(
page: PageDSL,
requestPath: string,
adapter: Awaited<ReturnType<typeof getStorageAdapterAsync>>,
locals: RuntimeLocals,
logger: (level: string, message: string, meta?: Record<string, unknown>) => void,
accessMode?: PageDSL['accessMode'],
cms?: Parameters<typeof renderPageDslToHtml>[0]['cms'],
Expand Down Expand Up @@ -378,6 +380,7 @@ async function renderPageToHtml(
return renderPageDslToHtml({
page,
adapter,
locals,
pathOrSlug: requestPath,
globalCSSHref,
accessMode: accessMode ?? page.accessMode,
Expand Down Expand Up @@ -771,6 +774,7 @@ try {
localizedTemplate.page,
routePathname,
adapter,
Astro.locals,
logger,
policy.accessMode,
cmsOptions,
Expand Down Expand Up @@ -993,6 +997,7 @@ try {
page,
Astro.url.pathname,
adapter,
Astro.locals,
logger,
policy.accessMode,
undefined,
Expand Down
Loading