// --------------------------------------------------------------------------- // 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 ( ); } if (field.type === "select" && field.options) { return (
{field.label}
); } return (
{field.label} 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, }} />
); } 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 (
{/* Header */}
0 ? 10 : 0 }}> {def.icon} {def.label} {primaryValue && ( {primaryValue} )}
{/* Fields */} {def.fields.length > 0 && (
{def.fields.map((f) => ( handleFieldChange(f.name, v)} /> ))}
)}
); } AtomicNodeView.displayName = `${def.tag}View`; return AtomicNodeView; }