Spaces:
Runtime error
Runtime error
| // --------------------------------------------------------------------------- | |
| // HfUser NodeView | |
| // | |
| // WYSIWYG rendition of the Hugging Face user card, mirroring the DOM | |
| // emitted by `app/src/components/HfUser.astro` in the upstream | |
| // research-article-template so what you see in the editor matches | |
| // what the publisher writes. | |
| // | |
| // Why a dedicated view (vs. the generic AtomicView): | |
| // The generic placeholder shows a dashed card with form fields, | |
| // which made authors guess what the final block looks like. The | |
| // upstream component is small and visual (avatar + name + handle), | |
| // so it's worth reproducing inline and revealing the form only | |
| // when the node is selected (or when the username is still empty, | |
| // right after insertion). | |
| // | |
| // Anchors: | |
| // We use <span> instead of <a> inside the editor so accidental | |
| // clicks don't navigate away from the document. The publisher | |
| // transformer (publisher/transformers/hf-user.ts) re-renders the | |
| // same DOM with real <a href> tags for the static HTML output. | |
| // --------------------------------------------------------------------------- | |
| import { useCallback } from "react"; | |
| import { NodeViewWrapper } from "@tiptap/react"; | |
| import type { NodeViewProps } from "@tiptap/react"; | |
| function FieldRow({ | |
| label, | |
| value, | |
| placeholder, | |
| onChange, | |
| }: { | |
| label: string; | |
| value: string; | |
| placeholder?: string; | |
| onChange: (val: string) => void; | |
| }) { | |
| return ( | |
| <div style={{ display: "flex", alignItems: "center", gap: 8 }}> | |
| <span | |
| style={{ | |
| fontSize: 12, | |
| color: "var(--muted-color)", | |
| minWidth: 90, | |
| }} | |
| > | |
| {label} | |
| </span> | |
| <input | |
| type="text" | |
| value={value} | |
| placeholder={placeholder ?? label} | |
| onChange={(e) => onChange(e.target.value)} | |
| style={{ | |
| flex: 1, | |
| minWidth: 0, | |
| padding: "4px 8px", | |
| background: "var(--page-bg)", | |
| border: "1px solid var(--border-color)", | |
| borderRadius: 4, | |
| color: "var(--text-color)", | |
| fontSize: 13, | |
| outline: "none", | |
| }} | |
| /> | |
| </div> | |
| ); | |
| } | |
| export function HfUserNodeView({ | |
| node, | |
| updateAttributes, | |
| selected, | |
| }: NodeViewProps) { | |
| const username = String(node.attrs.username || "").trim(); | |
| const name = String(node.attrs.name || "").trim(); | |
| const url = String(node.attrs.url || "").trim(); | |
| const displayName = name || username || "username"; | |
| const avatarSrc = username | |
| ? `https://huggingface.co/api/users/${encodeURIComponent(username)}/avatar` | |
| : ""; | |
| const urlPlaceholder = username | |
| ? `https://huggingface.co/${username}` | |
| : "https://huggingface.co/…"; | |
| const handleAttr = useCallback( | |
| (key: string) => (value: string) => updateAttributes({ [key]: value }), | |
| [updateAttributes], | |
| ); | |
| // Show the inline editor on selection or while the card is empty | |
| // (right after insertion, prompting the author to fill at least | |
| // the username). | |
| const showEditor = selected || !username; | |
| return ( | |
| <NodeViewWrapper data-component="hfUser"> | |
| <div contentEditable={false} style={{ margin: "0.75em 0" }}> | |
| <div | |
| className="hf-user" | |
| style={ | |
| selected | |
| ? { | |
| outline: "2px solid var(--primary-color)", | |
| outlineOffset: 2, | |
| borderRadius: 12, | |
| } | |
| : undefined | |
| } | |
| > | |
| <div className="hf-user__left"> | |
| {avatarSrc ? ( | |
| <img | |
| className="hf-user__avatar" | |
| src={avatarSrc} | |
| alt={`${displayName} avatar`} | |
| width={44} | |
| height={44} | |
| loading="lazy" | |
| decoding="async" | |
| referrerPolicy="no-referrer" | |
| /> | |
| ) : ( | |
| <div | |
| className="hf-user__avatar" | |
| aria-hidden="true" | |
| style={{ | |
| display: "flex", | |
| alignItems: "center", | |
| justifyContent: "center", | |
| background: "var(--surface-bg)", | |
| color: "var(--muted-color)", | |
| fontSize: 18, | |
| fontWeight: 700, | |
| }} | |
| > | |
| ? | |
| </div> | |
| )} | |
| <span className="hf-user__text"> | |
| <span className="hf-user__name">{displayName}</span> | |
| <span className="hf-user__row"> | |
| <span className="hf-user__username"> | |
| @{username || "username"} | |
| </span> | |
| </span> | |
| </span> | |
| </div> | |
| </div> | |
| {showEditor && ( | |
| <div | |
| style={{ | |
| display: "flex", | |
| flexDirection: "column", | |
| gap: 6, | |
| marginTop: 8, | |
| padding: "10px 12px", | |
| border: "1px dashed var(--border-color)", | |
| borderRadius: 8, | |
| background: "var(--surface-bg)", | |
| maxWidth: 360, | |
| }} | |
| > | |
| <FieldRow | |
| label="Username" | |
| value={username} | |
| placeholder="username" | |
| onChange={handleAttr("username")} | |
| /> | |
| <FieldRow | |
| label="Display name" | |
| value={name} | |
| placeholder="Full Name" | |
| onChange={handleAttr("name")} | |
| /> | |
| <FieldRow | |
| label="URL" | |
| value={url} | |
| placeholder={urlPlaceholder} | |
| onChange={handleAttr("url")} | |
| /> | |
| </div> | |
| )} | |
| </div> | |
| </NodeViewWrapper> | |
| ); | |
| } | |
| HfUserNodeView.displayName = "HfUserView"; | |