| |
| |
| |
| |
| |
| |
|
|
| 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; |
| } |
|
|