Unify markdown on one engine (Milkdown Crepe)

Replace the two-engine setup (MDXEditor for editing + react-markdown for
display) with a single Crepe-based component used for both. Editing is inline
WYSIWYG with full GFM that actually works — task lists, tables, and code
blocks via type-to-create / the slash menu — and read-only mode renders the
exact same way, so write and read can no longer drift (the root cause of the
heading/list/table/checkbox bugs).

- markdown-editor.tsx: Crepe wrapper supporting edit + readOnly, themed to the
  glass card via --crepe-* variable overrides (scoped to .milkdown so they win
  over frame-dark's own definitions).
- text-particle-view.tsx: render the message card with the read-only editor;
  drop the react-markdown component map.
- Remove now-unused deps: @mdxeditor/editor, react-markdown, remark-gfm,
  rehype-highlight, highlight.js.

The compose immersive mode (short, centered plain text) is unchanged.

https://claude.ai/code/session_019DU6V5z6Nr4Vu7b1fBDnq4
This commit is contained in:
Claude
2026-05-29 19:51:54 +00:00
parent 0e0ca86073
commit c10f4dc41d
5 changed files with 848 additions and 1251 deletions
@@ -1,155 +1,49 @@
/* Blend MDXEditor into the compose glass card, and restore prose typography.
*
* MDXEditor renders real <h1>/<ul>/<blockquote> elements and leans on the
* browser's default styles for their look — it ships no heading/list
* typography of its own. Tailwind's Preflight resets those defaults
* (headings -> `font-size: inherit`, lists -> `list-style: none`), which would
* otherwise make a heading or bullet look identical to body text. So we style
* the content elements explicitly here, mirroring the rendered display in
* text-particle-view.tsx so "write" and "read" match. */
/* Theme Milkdown Crepe to blend into the app's translucent "glass" surfaces.
* Crepe's frame-dark theme is a grayscale palette driven by --crepe-* custom
* properties; we override those to white-on-transparent with a blue accent so
* the editor (and the read-only display, which uses the same engine) sits on
* top of the existing card instead of painting its own opaque background. */
.llink-mdxeditor {
--baseBg: transparent;
--basePageBg: transparent;
/* Override the palette on `.milkdown` itself — frame-dark defines these vars
* there, so a lower-specificity ancestor selector would be ignored. */
.llink-crepe .milkdown {
--crepe-color-background: transparent;
--crepe-color-on-background: rgb(255 255 255 / 0.92);
--crepe-color-surface: rgb(255 255 255 / 0.06);
--crepe-color-surface-low: rgb(255 255 255 / 0.1);
--crepe-color-on-surface: #ffffff;
--crepe-color-on-surface-variant: rgb(255 255 255 / 0.6);
--crepe-color-outline: rgb(255 255 255 / 0.25);
--crepe-color-primary: #60a5fa;
--crepe-color-secondary: rgb(96 165 250 / 0.25);
--crepe-color-on-secondary: #ffffff;
--crepe-color-inverse: #ffffff;
--crepe-color-on-inverse: #0b0b0b;
--crepe-color-inline-code: #fca5a5;
--crepe-color-inline-area: rgb(255 255 255 / 0.1);
--crepe-color-error: #f87171;
--crepe-color-hover: rgb(255 255 255 / 0.08);
--crepe-color-selected: rgb(96 165 250 / 0.25);
background: transparent;
height: 100%;
box-shadow: none;
}
.llink-mdxeditor [class*="_editorRoot_"] {
background: transparent;
height: 100%;
}
.llink-mdx-content {
padding: 0 !important;
.llink-crepe .milkdown .ProseMirror {
padding: 0;
outline: none;
}
/* Editing context: fill the compose card and scroll internally so a long
* message stays inside the card rather than growing the whole overlay. */
.llink-crepe--fill,
.llink-crepe--fill .milkdown {
height: 100%;
color: rgb(255 255 255 / 0.92);
font-size: 0.95rem;
line-height: 1.7;
}
/* Headings */
.llink-mdx-content h1,
.llink-mdx-content h2,
.llink-mdx-content h3,
.llink-mdx-content h4,
.llink-mdx-content h5,
.llink-mdx-content h6 {
color: #fff;
line-height: 1.25;
}
.llink-mdx-content h1 {
font-size: 1.875rem;
font-weight: 700;
margin-bottom: 0.75rem;
}
.llink-mdx-content h2 {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.llink-mdx-content h3 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.llink-mdx-content h4 {
font-size: 1.125rem;
font-weight: 500;
margin-bottom: 0.25rem;
}
.llink-mdx-content h5 {
font-size: 1rem;
font-weight: 500;
margin-bottom: 0.25rem;
}
.llink-mdx-content h6 {
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.25rem;
.llink-crepe--fill .milkdown {
overflow-y: auto;
}
/* Block spacing */
.llink-mdx-content p {
margin-bottom: 0.75rem;
line-height: 1.625;
}
.llink-mdx-content > :last-child {
margin-bottom: 0;
}
/* Inline emphasis */
.llink-mdx-content strong {
font-weight: 600;
color: #fff;
}
.llink-mdx-content em {
font-style: italic;
}
.llink-mdx-content a {
color: #60a5fa;
text-decoration: underline;
}
/* Lists — Preflight strips the marker and indentation, so restore both */
.llink-mdx-content ul,
.llink-mdx-content ol {
margin-bottom: 0.75rem;
padding-left: 1.25rem;
}
.llink-mdx-content ul {
list-style: disc;
}
.llink-mdx-content ol {
list-style: decimal;
}
.llink-mdx-content li {
margin-bottom: 0.25rem;
line-height: 1.625;
}
/* Task-list items draw their own checkbox — no disc bullet. */
.llink-mdx-content li[class*="_listItem"] {
list-style: none;
}
/* Blockquote */
.llink-mdx-content blockquote {
margin-bottom: 0.75rem;
padding-left: 1rem;
border-left: 2px solid rgb(255 255 255 / 0.3);
font-style: italic;
color: rgb(255 255 255 / 0.7);
}
/* Inline code */
.llink-mdx-content code {
border-radius: 0.25rem;
background: rgb(255 255 255 / 0.1);
padding: 0.1rem 0.375rem;
font-size: 0.85em;
}
/* Horizontal rule */
.llink-mdx-content hr {
margin: 1rem 0;
border: none;
border-top: 1px solid rgb(255 255 255 / 0.1);
}
/* Tables */
.llink-mdx-content table {
margin-bottom: 0.75rem;
border-collapse: collapse;
width: auto;
}
.llink-mdx-content th,
.llink-mdx-content td {
border: 1px solid rgb(255 255 255 / 0.2);
padding: 0.375rem 0.625rem;
text-align: left;
}
.llink-mdx-content th {
background: rgb(255 255 255 / 0.08);
font-weight: 600;
.llink-crepe--fill .milkdown .ProseMirror {
min-height: 100%;
}