tfrere's picture
tfrere HF Staff
feat(editor): WYSIWYG NodeView for HfUser cards
7814104
Raw
History Blame Contribute Delete
5.78 kB
// ---------------------------------------------------------------------------
// 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";