Spaces:
Runtime error
Runtime error
| // --------------------------------------------------------------------------- | |
| // 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; | |
| } | |