Skip to content
26 changes: 19 additions & 7 deletions src/components/settings/settings-formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down
19 changes: 12 additions & 7 deletions src/components/ui/radio-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,36 +143,41 @@ function RadioImageCard({
return (
<FieldGroup className={cn(disabled && "opacity-50")}>
<FieldLabel className={cn(
"transition-colors has-data-checked:bg-transparent dark:has-data-checked:bg-transparent p-0 group cursor-pointer group",
// The base FieldLabel injects `*:data-[slot=field]:p-3`, which targets the
// child Field with higher specificity than a `p-0!` on the Field itself, so
// it can't be overridden there (esp. under the global !important strategy).
// Neutralise it at the root via the same child-targeting utility — twMerge
// collapses p-3 -> 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"
)}>
<Field
orientation={orientation}
data-disabled={disabled}
className="flex flex-col p-0!"
className="flex flex-col"
data-testid={`settings-field-${props.id}`}
>
<div className={cn( 'w-full flex flex-row items-center justify-between gap-3 border-b border-border group-hover:border-primary p-3', position === "right" && "flex-row-reverse", currentValue === props.value && 'border-primary!')}>
<div className={cn( 'w-full flex flex-row items-center justify-between gap-3 border-b border-border group-hover:border-primary px-5 py-4', position === "right" && "flex-row-reverse", currentValue === props.value && 'border-primary!')}>
<RadioGroupItem
className={cn("disabled:opacity-100", className)}
disabled={disabled}
{...props}
/>
<FieldTitle className="font-bold">
<FieldTitle className="font-bold text-base text-foreground">
{typeof label === 'string' ? <RawHTML>{label}</RawHTML> : label}
</FieldTitle>
</div>
<FieldContent className={cn('p-3 flex items-center justify-center')} >
<div className="flex flex-col items-start gap-3 w-full">
<FieldContent className={cn('px-5 py-4 w-full')} >
<div className="flex flex-col gap-3 w-full">
{description && (
<FieldDescription className="text-center">
{description}
</FieldDescription>
)}
{image && (
typeof image === 'string' ? (
<img src={image} alt={typeof label === 'string' ? label : 'Option image'} className="w-full h-auto object-contain" />
<img src={image} alt={typeof label === 'string' ? label : 'Option image'} className="block w-full h-auto object-contain" />
) : (
image
)
Expand Down
21 changes: 15 additions & 6 deletions src/components/ui/rich-text-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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) => {
Expand Down
Loading