diff --git a/src/components/settings/settings-formatter.ts b/src/components/settings/settings-formatter.ts index 4d09b9b..90fc0fa 100644 --- a/src/components/settings/settings-formatter.ts +++ b/src/components/settings/settings-formatter.ts @@ -334,12 +334,13 @@ export function buildIdIndex( /** * Evaluates whether a field should be displayed based on its dependencies. * - * Dependency keys are plain field ids (post-dependency_key cleanup). The - * value is read directly from the flat `values` map keyed by id. - * - * `idIndex` is kept as an optional parameter for backwards compatibility with - * call sites that still pass it; it's unused now that dependency_key is - * gone and `dep.key` always equals the field id. + * Dependency keys are authored as plain field ids. The flat `values` map is + * keyed by field id (current contract), so a direct lookup normally resolves. + * For resilience against older/alternate payloads that key `values` by the + * reconstructed dot-path (e.g. `page.section.field`), the lookup falls back to: + * 1. an explicit `idIndex` mapping (field id → stored key), when supplied; then + * 2. matching a stored key whose last dot-path segment equals the field id. + * This keeps id-keyed dependencies working regardless of how `values` is keyed. */ export function evaluateDependencies( element: SettingsElement, @@ -353,7 +354,18 @@ export function evaluateDependencies( return element.dependencies.every((dep) => { if (!dep.key) return true; - const currentValue = values[dep.key]; + let currentValue = values[dep.key]; + if (currentValue === undefined) { + const mapped = idIndex?.[dep.key]; + if (mapped !== undefined && values[mapped] !== undefined) { + currentValue = values[mapped]; + } else { + const match = Object.keys(values).find( + (k) => k === dep.key || k.split('.').pop() === dep.key + ); + if (match !== undefined) currentValue = values[match]; + } + } const comparison = dep.comparison || '=='; const expectedValue = dep.value; diff --git a/src/components/ui/radio-group.tsx b/src/components/ui/radio-group.tsx index 3b9d7dc..f8432e9 100644 --- a/src/components/ui/radio-group.tsx +++ b/src/components/ui/radio-group.tsx @@ -143,28 +143,33 @@ function RadioImageCard({ return ( p-0 so the illustration sits flush to the card edges. + "transition-colors has-data-checked:bg-transparent dark:has-data-checked:bg-transparent p-0 *:data-[slot=field]:p-0 group cursor-pointer rounded-xl overflow-hidden", currentValue === props.value && 'border-primary!', !disabled && "hover:border-primary" )}> -
+
- + {typeof label === 'string' ? {label} : label}
- -
+ +
{description && ( {description} @@ -172,7 +177,7 @@ function RadioImageCard({ )} {image && ( typeof image === 'string' ? ( - {typeof + {typeof ) : ( image ) diff --git a/src/components/ui/rich-text-editor.tsx b/src/components/ui/rich-text-editor.tsx index 709a68d..6ae38c3 100644 --- a/src/components/ui/rich-text-editor.tsx +++ b/src/components/ui/rich-text-editor.tsx @@ -52,12 +52,22 @@ function RichTextEditor({ const [fontFamily, setFontFamily] = React.useState("Sans Serif"); const [textStyle, setTextStyle] = React.useState("Paragraph"); - // Sync internal content with value prop if it changes and is different + // Sync the contentEditable DOM with the value/defaultValue prop. + // + // The contentEditable is intentionally NOT bound via dangerouslySetInnerHTML: + // React would re-apply innerHTML on every keystroke-driven re-render and + // collapse the selection to the start, so each typed character appears to + // prepend (the "reversed / RTL" typing effect). Here we only write innerHTML + // when the incoming content actually differs AND the editor is not focused, + // so external/programmatic updates still land but active typing keeps its caret. React.useEffect(() => { - if (value !== undefined && editorRef.current && value !== editorRef.current.innerHTML) { - editorRef.current.innerHTML = value; - } - }, [value]); + const el = editorRef.current; + if (!el) return; + const next = value ?? defaultValue ?? ""; + if (next === el.innerHTML) return; + if (document.activeElement === el) return; + el.innerHTML = next; + }, [value, defaultValue]); const executeCommand = (command: string, value?: string) => { document.execCommand(command, false, value); @@ -315,7 +325,6 @@ function RichTextEditor({ ref={editorRef} contentEditable className="w-full h-full px-3 py-2 text-sm outline-none bg-transparent prose prose-sm max-w-none" - dangerouslySetInnerHTML={{ __html: value ?? defaultValue }} suppressContentEditableWarning data-placeholder={placeholder} onInput={(e) => {