diff --git a/aria/admin/features/Stage/composables/useStageLiveCanvasUpdates.ts b/aria/admin/features/Stage/composables/useStageLiveCanvasUpdates.ts index c9501e5..37f6a7a 100644 --- a/aria/admin/features/Stage/composables/useStageLiveCanvasUpdates.ts +++ b/aria/admin/features/Stage/composables/useStageLiveCanvasUpdates.ts @@ -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, @@ -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 diff --git a/aria/lib/blocks/nodesToHtml.ts b/aria/lib/blocks/nodesToHtml.ts index 47edead..a8500e3 100644 --- a/aria/lib/blocks/nodesToHtml.ts +++ b/aria/lib/blocks/nodesToHtml.ts @@ -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, @@ -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, @@ -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, diff --git a/aria/tests/features/stage/useStageLiveCanvasUpdates.test.ts b/aria/tests/features/stage/useStageLiveCanvasUpdates.test.ts index b7cac72..899bd64 100644 --- a/aria/tests/features/stage/useStageLiveCanvasUpdates.test.ts +++ b/aria/tests/features/stage/useStageLiveCanvasUpdates.test.ts @@ -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 { @@ -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 = { diff --git a/aria/tests/lib/nodesToHtml.test.ts b/aria/tests/lib/nodesToHtml.test.ts index 778fe5b..a2a5bdc 100644 --- a/aria/tests/lib/nodesToHtml.test.ts +++ b/aria/tests/lib/nodesToHtml.test.ts @@ -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(' { const html = nodesToHtmlDocument( [ diff --git a/aria/tests/lib/rendering/renderPageDslToHtml.test.ts b/aria/tests/lib/rendering/renderPageDslToHtml.test.ts index 3926b09..16d87c3 100644 --- a/aria/tests/lib/rendering/renderPageDslToHtml.test.ts +++ b/aria/tests/lib/rendering/renderPageDslToHtml.test.ts @@ -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", @@ -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( + ' { const page: PageDSL = { id: "home", diff --git a/src/pages/404.astro b/src/pages/404.astro index ef7d157..54fa3a3 100644 --- a/src/pages/404.astro +++ b/src/pages/404.astro @@ -86,6 +86,7 @@ try { const rendered = await renderPageDslToHtml({ page: notFoundPage, adapter, + locals: Astro.locals, logger: (level, message, meta) => { log(level, `[404] ${message}`, meta); }, @@ -134,4 +135,4 @@ if (!html) { } --- - \ No newline at end of file + diff --git a/src/pages/[...slug].astro b/src/pages/[...slug].astro index 73137fb..197b548 100644 --- a/src/pages/[...slug].astro +++ b/src/pages/[...slug].astro @@ -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"; @@ -348,6 +349,7 @@ async function renderPageToHtml( page: PageDSL, requestPath: string, adapter: Awaited>, + locals: RuntimeLocals, logger: (level: string, message: string, meta?: Record) => void, accessMode?: PageDSL['accessMode'], cms?: Parameters[0]['cms'], @@ -378,6 +380,7 @@ async function renderPageToHtml( return renderPageDslToHtml({ page, adapter, + locals, pathOrSlug: requestPath, globalCSSHref, accessMode: accessMode ?? page.accessMode, @@ -771,6 +774,7 @@ try { localizedTemplate.page, routePathname, adapter, + Astro.locals, logger, policy.accessMode, cmsOptions, @@ -993,6 +997,7 @@ try { page, Astro.url.pathname, adapter, + Astro.locals, logger, policy.accessMode, undefined,