tfrere's picture
tfrere HF Staff
feat(frontend): editor refresh (embed studio, comment popover, shiki, top bar, hooks, styles)
76fc93a
// ---------------------------------------------------------------------------
// Generic atomic NodeView
//
// Renders any "atomic" component (no editable children) as a placeholder
// card showing the component type, icon, and its field values.
// ---------------------------------------------------------------------------
import React, { useCallback } from "react";
import { NodeViewWrapper } from "@tiptap/react";
import type { NodeViewProps } from "@tiptap/react";
import type { ComponentDef, ComponentField } from "./registry";
function AtomicFieldRow({
field,
value,
onChange,
}: {
field: ComponentField;
value: unknown;
onChange: (val: unknown) => void;
}) {
if (field.type === "boolean") {
return (
<label
style={{
display: "flex",
alignItems: "center",
gap: 6,
fontSize: 12,
color: "var(--muted-color)",
cursor: "pointer",
}}
>
<input
type="checkbox"
checked={!!value}
onChange={(e) => onChange(e.target.checked)}
style={{ accentColor: "var(--primary-color)" }}
/>
{field.label}
</label>
);
}
if (field.type === "select" && field.options) {
return (
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span style={{ fontSize: 12, color: "var(--muted-color)", minWidth: 60 }}>{field.label}</span>
<select
value={String(value ?? field.default ?? "")}
onChange={(e) => onChange(e.target.value)}
style={{
background: "var(--surface-bg)",
border: "1px solid var(--border-color)",
borderRadius: 4,
color: "var(--text-color)",
fontSize: 12,
padding: "2px 6px",
outline: "none",
flex: 1,
}}
>
{field.options.map((opt) => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
);
}
return (
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span style={{ fontSize: 12, color: "var(--muted-color)", minWidth: 60 }}>{field.label}</span>
<input
type="text"
value={String(value ?? "")}
placeholder={field.placeholder || field.label}
onChange={(e) => onChange(e.target.value)}
style={{
background: "var(--surface-bg)",
border: "1px solid var(--border-color)",
borderRadius: 4,
color: "var(--text-color)",
fontSize: 13,
padding: "4px 8px",
outline: "none",
flex: 1,
minWidth: 0,
}}
/>
</div>
);
}
export function makeAtomicView(def: ComponentDef) {
function AtomicNodeView({ node, updateAttributes }: NodeViewProps) {
const handleFieldChange = useCallback(
(fieldName: string, value: unknown) => {
updateAttributes({ [fieldName]: value });
},
[updateAttributes],
);
const primaryField = def.fields.find((f) => f.name === "src" || f.name === "title") || def.fields[0];
const primaryValue = primaryField ? String(node.attrs[primaryField.name] || "") : "";
return (
<NodeViewWrapper data-component={def.name}>
<div
contentEditable={false}
style={{
border: "1px dashed var(--border-color)",
borderRadius: 8,
background: "var(--surface-bg)",
margin: "0.75em 0",
padding: "12px 16px",
userSelect: "none",
}}
>
{/* Header */}
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: def.fields.length > 0 ? 10 : 0 }}>
<span style={{ fontSize: 16, lineHeight: 1 }}>{def.icon}</span>
<span style={{ fontSize: 13, fontWeight: 600, color: "var(--text-color)" }}>
{def.label}
</span>
{primaryValue && (
<span style={{ fontSize: 12, color: "var(--muted-color)", fontFamily: "monospace" }}>
{primaryValue}
</span>
)}
</div>
{/* Fields */}
{def.fields.length > 0 && (
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
{def.fields.map((f) => (
<AtomicFieldRow
key={f.name}
field={f}
value={node.attrs[f.name]}
onChange={(v) => handleFieldChange(f.name, v)}
/>
))}
</div>
)}
</div>
</NodeViewWrapper>
);
}
AtomicNodeView.displayName = `${def.tag}View`;
return AtomicNodeView;
}