diff --git a/app/ycode/components/LayersTree.tsx b/app/ycode/components/LayersTree.tsx index 597deebd..d11806bb 100644 --- a/app/ycode/components/LayersTree.tsx +++ b/app/ycode/components/LayersTree.tsx @@ -35,7 +35,7 @@ import { useAuthStore } from '@/stores/useAuthStore'; // 6. Utils/lib import { cn } from '@/lib/utils'; import { flattenTree, type FlattenedItem } from '@/lib/tree-utilities'; -import { canHaveChildren, getLayerIcon, getLayerName, getCollectionVariable, isTextContentLayer, isRichTextLayer, getRichTextSublayers, getTextStyleSublayers, canMoveLayer, updateLayerProps, filterDisabledSliderLayers, getLayerCmsFieldBinding, extractBlockText } from '@/lib/layer-utils'; +import { canHaveChildren, getLayerIcon, getLayerName, getCollectionVariable, isTextContentLayer, isRichTextLayer, hasRichTextContent, getRichTextSublayers, getTextStyleSublayers, canMoveLayer, updateLayerProps, filterDisabledSliderLayers, getLayerCmsFieldBinding, extractBlockText } from '@/lib/layer-utils'; import { getBlockName } from '@/lib/templates/blocks'; import { MULTI_ASSET_COLLECTION_ID } from '@/lib/collection-field-utils'; import { hasStyleOverrides } from '@/lib/layer-style-utils'; @@ -252,7 +252,7 @@ const LayerRow = React.memo(function LayerRow({ // Component instances should not show children in the tree (unless editing master) // Children can only be edited via "Edit master component" const shouldHideChildren = isComponentInstance && !editingComponentId; - const hasContentSublayers = isRichTextLayer(node.layer) && getRichTextSublayers(node.layer).length > 0; + const hasContentSublayers = hasRichTextContent(node.layer); const hasStyleSublayers = isTextContentLayer(node.layer) && getTextStyleSublayers(node.layer).length > 0; const hasSublayers = hasContentSublayers || hasStyleSublayers; const effectiveHasChildren = (hasChildren && !shouldHideChildren) || hasSublayers; @@ -1883,7 +1883,7 @@ export default function LayersTree({ hasVisibleChildren = node.canHaveChildren && !collapsedIds.has(node.id); } else { // Real layer nodes: check actual children and sublayer presence - const hasAnySublayers = (isRichTextLayer(node.layer) && getRichTextSublayers(node.layer).length > 0) + const hasAnySublayers = hasRichTextContent(node.layer) || (isTextContentLayer(node.layer) && getTextStyleSublayers(node.layer).length > 0); hasVisibleChildren = (!collapsedIds.has(node.id)) && ( !!(node.layer.children && node.layer.children.length > 0) || hasAnySublayers diff --git a/lib/layer-utils.ts b/lib/layer-utils.ts index 12947dee..6c9b7953 100644 --- a/lib/layer-utils.ts +++ b/lib/layer-utils.ts @@ -631,6 +631,21 @@ function extractInlineMarks(block: any): string[] { * When a CMS field is bound, pass the resolved CMS content via `cmsContent` * so sublayers reflect the actual CMS item data. */ +/** + * Check if a richText layer has content blocks (either its own or via CMS binding). + * Used for determining collapsibility in the layers tree without requiring resolved CMS data. + */ +export function hasRichTextContent(layer: Layer): boolean { + if (!isRichTextLayer(layer)) return false; + const textVar = layer.variables?.text; + if (textVar?.type !== 'dynamic_rich_text') return false; + const layerDoc = (textVar.data as any)?.content; + if (!layerDoc?.content || !Array.isArray(layerDoc.content)) return false; + const binding = getCmsFieldBinding(layerDoc); + if (binding) return true; + return layerDoc.content.some((block: any) => block.type !== 'paragraph' || block.content?.length); +} + export function getRichTextSublayers(layer: Layer, cmsContent?: any): RichTextSublayer[] { const textVar = layer.variables?.text; if (textVar?.type !== 'dynamic_rich_text') return []; diff --git a/lib/text-format-utils.ts b/lib/text-format-utils.ts index 543e4d44..4c5213ab 100644 --- a/lib/text-format-utils.ts +++ b/lib/text-format-utils.ts @@ -442,8 +442,8 @@ function renderTextNode( /** * Render nested rich text content from a Tiptap JSON structure. * Used when a rich_text CMS field is inserted as an inline variable. - * Delegates to renderBlock with useSpanForParagraphs=true since this - * content is always nested inside another element. + * When useSpanForParagraphs is true (default), block elements render as spans + * to avoid invalid HTML nesting inside restrictive tags like

or

. */ function renderNestedRichTextContent( richTextValue: any, @@ -458,6 +458,7 @@ function renderNestedRichTextContent( components?: Component[], renderComponentBlock?: RenderComponentBlockFn, ancestorComponentIds?: Set, + useSpanForParagraphs = true, ): React.ReactNode[] { if (!richTextValue) { return []; @@ -478,7 +479,7 @@ function renderNestedRichTextContent( if (parsed.type === 'doc' && Array.isArray(parsed.content)) { return parsed.content.map((block: any, blockIdx: number) => - renderBlock(block, blockIdx, collectionItemData, pageCollectionItemData, textStyles, true, isEditMode, linkContext, timezone, layerDataMap, components, renderComponentBlock, ancestorComponentIds) + renderBlock(block, blockIdx, collectionItemData, pageCollectionItemData, textStyles, useSpanForParagraphs, isEditMode, linkContext, timezone, layerDataMap, components, renderComponentBlock, ancestorComponentIds) ).filter(Boolean); } @@ -500,6 +501,7 @@ function renderInlineContent( components?: Component[], renderComponentBlock?: RenderComponentBlockFn, ancestorComponentIds?: Set, + useSpanForParagraphs = true, ): React.ReactNode[] { return content.flatMap((node, idx) => { const key = `node-${idx}`; @@ -537,6 +539,7 @@ function renderInlineContent( components, renderComponentBlock, ancestorComponentIds, + useSpanForParagraphs, ); } } @@ -699,7 +702,7 @@ function renderBlock( ); const tag = hasBlockContent ? 'div' : useSpanForParagraphs ? 'span' : 'p'; - return React.createElement(tag, paragraphProps, ...renderInlineContent(block.content, collectionItemData, pageCollectionItemData, textStyles, isEditMode, linkContext, timezone, layerDataMap, components, renderComponentBlock, ancestorComponentIds)); + return React.createElement(tag, paragraphProps, ...renderInlineContent(block.content, collectionItemData, pageCollectionItemData, textStyles, isEditMode, linkContext, timezone, layerDataMap, components, renderComponentBlock, ancestorComponentIds, useSpanForParagraphs)); } if (block.type === 'heading') { @@ -718,7 +721,7 @@ function renderBlock( if (isEditMode) { headingProps['data-style'] = styleKey; } - return React.createElement(tag, headingProps, ...renderInlineContent(block.content, collectionItemData, pageCollectionItemData, textStyles, isEditMode, linkContext, timezone, layerDataMap, components, renderComponentBlock, ancestorComponentIds)); + return React.createElement(tag, headingProps, ...renderInlineContent(block.content, collectionItemData, pageCollectionItemData, textStyles, isEditMode, linkContext, timezone, layerDataMap, components, renderComponentBlock, ancestorComponentIds, useSpanForParagraphs)); } if (block.type === 'bulletList') { @@ -935,7 +938,8 @@ export function renderRichText( paragraph.content[0].attrs?.variable?.data?.field_type === 'rich_text'; if (hasSoleRichTextVariable) { - const inlineContent = renderInlineContent(paragraph.content, collectionItemData, pageCollectionItemData, textStyles, isEditMode, linkContext, timezone, layerDataMap, components, renderComponentBlock, ancestorComponentIds); + const nestedUseSpans = isSimpleTextElement ? true : useSpanForParagraphs; + const inlineContent = renderInlineContent(paragraph.content, collectionItemData, pageCollectionItemData, textStyles, isEditMode, linkContext, timezone, layerDataMap, components, renderComponentBlock, ancestorComponentIds, nestedUseSpans); return Array.isArray(inlineContent) ? inlineContent : [inlineContent]; } @@ -946,7 +950,7 @@ export function renderRichText( } return null; } - const inlineContent = renderInlineContent(paragraph.content, collectionItemData, pageCollectionItemData, textStyles, isEditMode, linkContext, timezone, layerDataMap, components, renderComponentBlock, ancestorComponentIds); + const inlineContent = renderInlineContent(paragraph.content, collectionItemData, pageCollectionItemData, textStyles, isEditMode, linkContext, timezone, layerDataMap, components, renderComponentBlock, ancestorComponentIds, useSpanForParagraphs); if (isEditMode && !isSimpleTextElement) { const paragraphClass = textStyles?.paragraph?.classes ?? DEFAULT_TEXT_STYLES.paragraph?.classes ?? ''; const children = Array.isArray(inlineContent) ? inlineContent : [inlineContent];