188c5f506c
Add an inline spoiler (Telegram/Discord-style hidden text): a TipTap mark `spoiler` rendered as <span data-spoiler="true" class="spoiler">, blurred via CSS and revealed on click (UI-only is-revealed class, never persisted). - packages/editor-ext: the Spoiler mark (inclusive:false, set/toggle/unset commands, ||text|| input rule), exported; a lossless turndown rule emitting raw inline HTML; round-trip test. - apps/client: SpoilerView mark-view (ReactMarkViewRenderer, Link pattern), registration in extensions, bubble-menu toggle button (editable only), CSS (blur + @media print reveal), en/ru i18n. - apps/server: register Spoiler in collaboration.util tiptapExtensions so the mark survives HTML<->JSON export/index/import/Yjs; a test proving the public share keeps the spoiler (it isn't stripped with comments). No keyboard shortcut: the proposed Mod-Shift-s collides with Strike (and Mod-Shift-h with Highlight); the ||text|| input rule + the bubble-menu button cover ergonomics. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
21 lines
737 B
TypeScript
21 lines
737 B
TypeScript
import { MarkViewContent, MarkViewProps } from "@tiptap/react";
|
|
import { useState } from "react";
|
|
|
|
// Click-to-reveal spoiler. The revealed state is UI-only and is never written to
|
|
// the document: toggling only adds/removes the `is-revealed` class (CSS removes
|
|
// the blur). renderHTML never emits `is-revealed`, so it can't leak into the
|
|
// doc/clipboard. Works the same in editor, read-only and public-share views.
|
|
export default function SpoilerView(_props: MarkViewProps) {
|
|
const [revealed, setRevealed] = useState(false);
|
|
|
|
return (
|
|
<span
|
|
className={revealed ? "spoiler is-revealed" : "spoiler"}
|
|
data-spoiler="true"
|
|
onClick={() => setRevealed((v) => !v)}
|
|
>
|
|
<MarkViewContent />
|
|
</span>
|
|
);
|
|
}
|