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