Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ This package contains extracted UI components from the Open CRM frontend, design
- **InputGroup** — Composite input with addons and buttons
- **Combobox** — Searchable dropdown with chip support (based on Base UI)
- **TagMultiSelect** — Multi-select tag picker with colored chips
- **MarkdownEditor** — WYSIWYG Markdown editor that round-trips all supported Markdown constructs without data loss
- **MarkdownView** — Read-only Markdown renderer with structural output (headings, lists, task lists, blockquotes, code)

## Usage

Expand Down
78 changes: 78 additions & 0 deletions docs/upgrade-to-0.10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Upgrade prompt: `@open-elements/ui` 0.9.x → 0.10.0

`@open-elements/ui` 0.10.0 fixes a **data-loss bug** in `MarkdownEditor` and a **rendering gap** in `MarkdownView`. Both components previously trimmed the TipTap schema down to paragraphs, a few marks and links, so every other Markdown construct (headings, bullet/ordered lists, task lists, blockquotes, code blocks, horizontal rules) was silently discarded when content was loaded.

In `MarkdownEditor` this destroyed stored data: simply opening a form with such content was enough to corrupt the value passed back through `onChange`, without a single keystroke. In `MarkdownView` the same constructs were flattened to plain text.

0.10.0 opens the schema so all of those constructs **round-trip untouched**. There is **no API change** — `MarkdownEditor` and `MarkdownView` keep the exact same props. What the editor toolbar lets a user *create* is unchanged (still Bold, Italic, Strikethrough, Link); task lists render but cannot be created.

The consumer-visible effect is that content which used to appear as flat text now renders as real structure. This is the intended fix, but it means blocks that were previously invisible-as-structure now take up their natural space.

This file is a self-contained prompt for an agent (Claude Code, etc.) to run inside a consumer repo. Paste it verbatim.

---

## Prompt

You are working inside an app that depends on `@open-elements/ui`. Goal: upgrade to `^0.10.0`. There is **no prop or import migration** — the change is behavioral, plus a documentation/visual verification pass.

### What changed in 0.10.0

- **`MarkdownEditor` no longer corrupts content on open.** Loading a value containing headings, lists, task lists, blockquotes, code blocks or horizontal rules no longer strips those constructs and no longer fires a spurious `onChange`. If your app worked around this bug (e.g. by re-saving on load, or by pre-stripping Markdown before passing it in), remove that workaround.
- **`MarkdownView` now renders structure.** Content that previously appeared as a single flat paragraph now renders as headings, lists, task lists (as checkboxes), blockquotes, code blocks and rules.
- **Task lists render but stay non-creatable.** `- [x]` / `- [ ]` items display as checkboxes and survive editing. There is no toolbar button, keyboard shortcut, or `[ ] ` input rule to create one — this is deliberate.
- **Checkboxes in `MarkdownView` are read-only.** They reflect the stored state; clicking them does nothing (interactive toggling arrives in a later release).
- **Dependency manifest.** Internally the library dropped a duplicate `@tiptap/extension-link` and declared `@tiptap/extension-list`. These are the library's own `dependencies`, resolved transitively — you do **not** manage them in your app.

### Steps

1. **Find the consumer's frontend `package.json`** (repo root or under `frontend/`). Confirm `@open-elements/ui` is listed, then bump it to `^0.10.0` and run:

```bash
pnpm install
```

2. **Confirm the library sources are in your Tailwind content globs.** The newly rendered blocks and the task-list styling rely on utility classes (`prose prose-sm`, `list-none`, `pl-0`, `flex`, …) that Tailwind must see in the library source. If you already render any `@open-elements/ui` component correctly, this is already the case — verify your `tailwind.config` `content` array includes the package, e.g.:

```js
content: ["./src/**/*.{ts,tsx}", "./node_modules/@open-elements/ui/src/**/*.{ts,tsx}"],
```

3. **Visually check every screen that uses `MarkdownView` or `MarkdownEditor`.** Content that was silently flattened will now render as structure. In compact contexts (detail panels, cards, table cells) headings and code blocks may look oversized. If so, add local `prose` overrides *in your app* — do not edit the library. Example:

```html
<div class="prose prose-sm prose-headings:text-base prose-headings:font-medium">
<MarkdownView content={value} />
</div>
```

4. **Remove any load-time corruption workarounds.** Search for code that pre-processes Markdown before handing it to `MarkdownEditor`, or that ignores the first `onChange` after mount. These existed to paper over the old bug and are now unnecessary.

5. **Verify.** All three must pass:

```bash
pnpm exec tsc --noEmit
pnpm test
pnpm build
```

6. **Commit** with a clear message:

```
chore(deps): upgrade @open-elements/ui to 0.10.0

MarkdownEditor no longer corrupts stored Markdown on open and
MarkdownView now renders full structure. Verified prose styling.
```

### Guard rails

- **Do not** change how you call `MarkdownEditor` / `MarkdownView` — their props are identical to 0.9.0.
- **Do not** add `@tiptap/*` packages to your app's dependencies to "fix" the upgrade. They are the library's transitive dependencies.
- **Do not** edit `@open-elements/ui` from the consumer side. `prose` overrides belong in your app's markup or stylesheet.
- **Do not** bundle unrelated dependency bumps into the same change.

### Don't do this

- Do not try to re-add a way to *create* task lists — their non-creatability is intentional in this release.
- Do not treat the new rendering as a regression and revert the bump; flattened text was the bug.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"@tiptap/core": "^3.22.0",
"@tiptap/react": "^3.22.0",
"@tiptap/starter-kit": "^3.22.0",
"@tiptap/extension-link": "^3.22.0",
"@tiptap/extension-list": "^3.22.0",
"@tiptap/extension-placeholder": "^3.22.0",
"tiptap-markdown": "^0.9.0"
},
Expand Down
15 changes: 14 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

142 changes: 142 additions & 0 deletions specs/001-markdown-schema-roundtrip/steps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Implementation Steps: Markdown schema round-trip

## Step 1: Shared extension factory and dependency changes

- [x] Add `@tiptap/extension-list` at `^3.22.0` to `dependencies` in `package.json`
- [x] Remove `@tiptap/extension-link` from `dependencies` in `package.json`
- [x] Run `pnpm install` so `@tiptap/extension-list` is hoisted as a direct dependency
- [x] Create `src/lib/markdown-extensions.ts` exporting `MarkdownExtensionsOptions` and `createMarkdownExtensions(options?)`
- [x] Open the schema: use `StarterKit.configure` without disabling `heading`, `codeBlock`, `blockquote`, `horizontalRule`, `bulletList`, `orderedList`, `listItem`
- [x] Set `underline: false` in the StarterKit config
- [x] Configure `link` **through** StarterKit (`openOnClick` from `options.openLinksOnClick`, default `false`; view adds `target`/`rel`)
- [x] Add `TaskList.extend({ addKeyboardShortcuts: () => ({}) })` configured with `HTMLAttributes: { class: "list-none pl-0" }`
- [x] Add `TaskItem.extend({ addInputRules: () => [] })` configured with `nested: true`, `HTMLAttributes: { class: "flex items-start gap-2" }`
- [x] Add `Placeholder.configure({ placeholder: options.placeholder ?? "" })` and `Markdown`

**Acceptance criteria:**
- [x] `pnpm typecheck` passes
- [x] `pnpm build` succeeds
- [x] The factory is the single source of extension configuration

**Related behaviors:** foundation for all scenarios

---

## Step 2: Refactor `MarkdownEditor` to use the factory

- [x] Replace the inline `extensions` array in `markdown-editor.tsx` with `createMarkdownExtensions({ placeholder, openLinksOnClick: false })`
- [x] Remove now-unused imports (`StarterKit`, `Link`, `Placeholder`, `Markdown`)
- [x] Leave `MarkdownEditorProps`, the toolbar, `onUpdate`, and the sync `useEffect` unchanged

**Acceptance criteria:**
- [x] `pnpm typecheck` and `pnpm build` pass
- [x] Toolbar still renders Bold, Italic, Strikethrough, Link (and Unlink in a link)

**Related behaviors:** Opening without editing leaves the value untouched; A real edit still reports the full document; An externally changed value replaces the document; The toolbar is unchanged

---

## Step 3: Refactor `MarkdownView` to use the factory

- [x] Replace the inline `extensions` array in `markdown-view.tsx` with `createMarkdownExtensions({ openLinksOnClick: true })`
- [x] Remove now-unused imports (`StarterKit`, `Link`, `Markdown`)
- [x] Leave `MarkdownViewProps`, `editable: false`, and the sync `useEffect` unchanged

**Acceptance criteria:**
- [x] `pnpm typecheck` and `pnpm build` pass

**Related behaviors:** Structural Markdown renders as structure; Task lists render as checkboxes reflecting their state; Clicking a checkbox has no effect; Checkboxes are not decorated with a list bullet

---

## Step 4: Round-trip tests against a real `Editor`

- [x] Create `src/lib/__tests__/markdown-extensions.test.ts`
- [x] Instantiate `new Editor({ extensions: createMarkdownExtensions(), content: input })` and assert `editor.storage.markdown.getMarkdown() === input`
- [x] Drive it from a table of Markdown inputs covering: task list, bullet list, ordered list, H1–H6, blockquote + fenced code block with language + `---`, nested task list, marks inside blocks (bold + link), mixed list, trailing-paragraph guard (doc ending in code block / hr), empty string, unchecked-only task list, plain text
- [x] If a trailing blank line appears, disable `trailingNode` in the factory and re-run

**Acceptance criteria:**
- [x] `pnpm test` passes for all round-trip cases

**Related behaviors:** Task list survives unchanged; Bullet list survives unchanged; Ordered list survives unchanged; Headings survive at every level; Blockquote, code block and horizontal rule survive; Nested task lists survive; Marks inside blocks survive; A mixed list keeps both kinds of items; The document does not grow a trailing paragraph; Empty content produces empty Markdown; An unchecked-only task list round-trips; Content that is already plain text is unaffected

---

## Step 5: Editor behaviour tests (real editor, no React mock)

- [x] Add tests that build a real `Editor` from the factory to verify creation paths are closed:
- [x] `Mod-Shift-9` creates no task list
- [x] typing `[ ] ` leaves literal text and creates no task item
- [x] underline command is unavailable (`editor.can().toggleUnderline()` is false / command absent)
- [x] Add editing tests for an existing task list: `Enter` splits into a new unchecked item (serializes to two lines); `Shift-Tab` lifts a nested item (reduced indentation)
- [x] Rewrite `markdown-editor.test.tsx` so the "no corruption on open" behaviours are testable: render a real `MarkdownEditor`, spy on `onChange`, assert it is not called after mount; assert a typed edit reports the full document; assert an external `value` change replaces the document
- [x] Keep a toolbar test asserting exactly Bold, Italic, Strikethrough, Link (and Unlink only inside a link)

**Acceptance criteria:**
- [x] `pnpm test` passes

**Related behaviors:** The keyboard shortcut does nothing; The input rule does nothing; Underline cannot be applied; Enter splits a task item; Shift-Tab lifts a nested item; Opening without editing leaves the value untouched; A real edit still reports the full document; An externally changed value replaces the document; The toolbar is unchanged

---

## Step 6: View rendering tests (real editor, no React mock)

- [x] Rewrite `markdown-view.test.tsx` to render a real `MarkdownView`
- [x] Assert structural Markdown renders as `h1`/`ul`/`blockquote` elements, not plain paragraphs
- [x] Assert `- [x] Done\n- [ ] Open` renders two checkbox inputs, first checked, second unchecked
- [x] Assert clicking a checkbox leaves its state unchanged and fires no callback
- [x] Assert the task list carries the `list-none pl-0` classes (bullet suppression)

**Acceptance criteria:**
- [x] `pnpm test` passes

**Related behaviors:** Structural Markdown renders as structure; Task lists render as checkboxes reflecting their state; Clicking a checkbox has no effect; Checkboxes are not decorated with a list bullet

---

## Step 7: Documentation

- [x] Create `docs/upgrade-to-0.10.md` documenting the rendering change (previously flattened constructs now render as structure) and the removal of `@tiptap/extension-link` from the public dependency set
- [x] Update `CLAUDE.md` Project Context (Features / Tech Stack / Structure / Architecture) if the markdown components or the new `markdown-extensions` module warrant it
- [x] Update `README.md` if user-facing behaviour or the dependency list is documented there

**Acceptance criteria:**
- [x] `pnpm build`, `pnpm test`, `pnpm lint`, `pnpm typecheck` all pass
- [x] Docs reflect the change

**Related behaviors:** none (documentation)

---

## Behavior Coverage

| Scenario | Layer | Covered in Step |
|----------|-------|-----------------|
| Task list survives unchanged | Frontend | 4 |
| Bullet list survives unchanged | Frontend | 4 |
| Ordered list survives unchanged | Frontend | 4 |
| Headings survive at every level | Frontend | 4 |
| Blockquote, code block and horizontal rule survive | Frontend | 4 |
| Nested task lists survive | Frontend | 4 |
| Marks inside blocks survive | Frontend | 4 |
| A mixed list keeps both kinds of items | Frontend | 4 |
| The document does not grow a trailing paragraph | Frontend | 4 |
| Opening without editing leaves the value untouched | Frontend | 5 |
| A real edit still reports the full document | Frontend | 5 |
| An externally changed value replaces the document | Frontend | 5 |
| Structural Markdown renders as structure | Frontend | 6 |
| Task lists render as checkboxes reflecting their state | Frontend | 6 |
| Clicking a checkbox has no effect | Frontend | 6 |
| Checkboxes are not decorated with a list bullet | Frontend | 6 |
| The keyboard shortcut does nothing | Frontend | 5 |
| The input rule does nothing | Frontend | 5 |
| The toolbar is unchanged | Frontend | 5 |
| Enter splits a task item | Frontend | 5 |
| Shift-Tab lifts a nested item | Frontend | 5 |
| Underline cannot be applied | Frontend | 5 |
| Empty content produces empty Markdown | Frontend | 4 |
| An unchecked-only task list round-trips | Frontend | 4 |
| Content that is already plain text is unaffected | Frontend | 4 |

Every scenario is a frontend/library scenario and is assigned to a step. No backend layer exists.
2 changes: 1 addition & 1 deletion specs/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

| ID | Spec-Folder | Name | Areas | Description | GitHub Issue | Status |
|-----|-------------|------|-------|-------------|--------------|--------|
| 001 | 001-markdown-schema-roundtrip | Markdown schema round-trip | frontend, api, testing | Stop destroying unsupported Markdown in MarkdownEditor/MarkdownView by teaching the schema everything Markdown can express | — | open |
| 001 | 001-markdown-schema-roundtrip | Markdown schema round-trip | frontend, api, testing | Stop destroying unsupported Markdown in MarkdownEditor/MarkdownView by teaching the schema everything Markdown can express | — | done |
| 002 | 002-markdown-toolbar-actions | Markdown toolbar actions | frontend, api | Compose the MarkdownEditor toolbar per usage via an explicit action allowlist | — | open |
| 003 | 003-markdown-view-checkboxes | Markdown view checkboxes | frontend, api | Tick task list checkboxes directly in MarkdownView with optimistic update and rollback | — | open |
Loading
Loading