6 file(s), +206 / -46 lines
| Path | Role | + | - |
|---|---|---|---|
| package.json | Config | +1 | -0 |
| pnpm-lock.yaml | Config | +22 | -0 |
| src/components/shared/editable-surface-preset.test.ts | Component | +58 | -0 |
| src/components/shared/editable-surface-preset.ts | Component | +74 | -0 |
| src/components/shared/editable-surface.stories.tsx | Component | +21 | -0 |
| src/components/shared/editable-surface.tsx | Component | +30 | -46 |
This is the KEYSTONE of the editing-substrate epic (EPIC-2023). It installs
markdown as the serialization substrate under EditableSurface
and introduces a preset seam so later stories can turn on
real formatting without touching the primitive again. Ships exactly one
preset — plain — which reproduces today's behaviour with
zero data migration and no visible change.
@tiptap/markdown added (v3.27.1, matching the
installed TipTap). It is included in the plain preset's extension set, so
editor.getMarkdown() and
setContent(md, { contentType: "markdown" }) now exist on the
surface — the substrate every later preset builds on.
EditableSurfacePreset type + resolvePreset()
(editable-surface-preset.ts). A preset resolves to a
{ extensions, toContent, toValue } strategy. The component is
now preset-driven; preset defaults to "plain", so
every existing call site (the Brief sections) is unchanged.
getMarkdown(), not
getText(), compared against a markdown baseline captured at
load/commit. For plain this behaves identically (no formatting is
available), but it is the foundation: once a richer preset lets a user
bold a word, that formatting change trips the unsaved-changes icon.
plain loads & saves as literal text (not via markdown)
The obvious reading — "plain just runs stored strings through
setContent(md, { contentType: "markdown" }) ↔
getMarkdown()" — corrupts real data. Probing the installed
library against representative counsel prose showed:
| Stored value | Markdown round-trip result |
|---|---|
Claim is < ₹42 Cr | Claim is < ₹42 Cr — < HTML-escaped |
See <Exhibit-7> now | See now — <Exhibit-7> parsed as an HTML tag and dropped |
# Summary | rendered as an <h1> heading — visible change |
a **bold** clause | rendered bold — visible change |
So markdown-parsing the plain preset would break three acceptance criteria
at once (literal < stays safe, zero migration,
no visible change). The plain preset therefore keeps the literal
text path — toContent builds a paragraph-per-line ProseMirror
doc with no HTML/markdown parsing, and toValue is
getText({ blockSeparator: "\n" }). A bare string round-trips as
itself; a <, a leading #, and **
all render verbatim. The markdown machinery is installed for
later presets, not applied to plain data. When Story-B adds
light/full, it flips those two functions to the
markdown path and adds a toolbar — resolvePreset is the only
file that changes.
src/components/shared/editable-surface-preset.ts — new: the preset type, strategy, and resolvePreset().src/components/shared/editable-surface.tsx — preset-driven load/save; markdown-baseline dirty detection.src/components/shared/editable-surface-preset.test.ts — new: substrate bare-string identity + plain-preset verbatim/<-safe round-trip.src/components/shared/editable-surface.stories.tsx — new PlainPresetLiteralText story proving the unchanged case.light / full presets and the formatting toolbar — Story-B.pnpm codegen run; no wire-contract change.
pnpm lint (clean), pnpm build (green), and the full
pnpm test suite (996 tests, incl. the new round-trip test) all
pass. The PlainPresetLiteralText Storybook story demonstrates a
value containing <, <Exhibit-7>, and a
leading # saving byte-for-byte unchanged.