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: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ This package contains extracted UI components from the Open CRM frontend, design
- **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; toolbar actions are configurable per usage via the `toolbar` prop
- **MarkdownView** — Read-only Markdown renderer with structural output (headings, lists, task lists, blockquotes, code)
- **MarkdownView** — Read-only Markdown renderer with structural output (headings, lists, task lists, blockquotes, code); task-list checkboxes become interactive via an optional `onChange` (optimistic update with rollback)

## Usage

Expand Down
9 changes: 9 additions & 0 deletions docs/TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# TODO

## Bullet list adjacent to a task list grows a phantom empty task item

When Markdown places a bullet list directly before a task list (`- one\n- two\n\n- [ ] a`), the schema round-trip inserts a spurious empty task item (`- [ ] `) between them. Separating the two lists with a paragraph avoids it. This breaks the byte-identical round-trip guarantee from spec `001-markdown-schema-roundtrip` for that specific adjacency.

**Context:** Surfaced while writing the "only the clicked item changes" test for spec `003-markdown-view-checkboxes`; the test was adjusted to separate the lists. Root cause is in the `tiptap-markdown` / markdown-it parse of adjacent bullet+task lists, not in spec 003, so it was left for a dedicated fix. Not yet reproduced in a spec 001 round-trip test.

**Prerequisite:** none — can be investigated against `createMarkdownExtensions` directly.
95 changes: 95 additions & 0 deletions docs/upgrade-to-0.12.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Upgrade prompt: `@open-elements/ui` 0.11.x → 0.12.0 (additive)

`@open-elements/ui` 0.12.0 makes `MarkdownView` task-list checkboxes **interactive**. This is an **additive, non-breaking** change: without the new prop, `MarkdownView` behaves exactly as in 0.11.0 (checkboxes render read-only and a click reverts).

`MarkdownView` gains an optional `onChange`:

```ts
export interface MarkdownViewProps {
readonly content: string;
/**
* Called when the reader toggles a task list checkbox, with the complete
* updated Markdown. Omit to render checkboxes as read-only.
*/
readonly onChange?: (markdown: string) => void | Promise<void>;
}
```

There are three levels of engagement:

| You pass | Behaviour |
|----------|-----------|
| nothing | Checkboxes render; a click reverts. Unchanged from 0.11.0. |
| `(md) => void` | The click applies immediately and `onChange` fires with the full Markdown. No busy state, no rollback. |
| `(md) => Promise<void>` | The click applies immediately; every checkbox is disabled and muted until the Promise settles; on rejection the document reverts to the last confirmed Markdown. |

Only one save can be in flight at a time — clicks during a pending save are ignored — so concurrent saves for the same field are impossible by construction. `MarkdownView` stays read-only for text; only checkboxes are interactive.

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.12.0`. This is **additive** — no existing usage needs to change. Optionally, make read-only checklists tickable where it makes sense.

### What changed in 0.12.0

- **`MarkdownView` gained an optional `onChange`.** When provided, ticking a task-list checkbox applies immediately, reports the complete updated Markdown, and — if `onChange` returns a Promise — disables all checkboxes until it settles and rolls back on rejection.
- **No change without the prop.** Omitting `onChange` is identical to 0.11.0.
- **No `MarkdownEditor` change.**

### Steps

1. **Find the consumer's frontend `package.json`**, bump `@open-elements/ui` to `^0.12.0`, and run:

```bash
pnpm install
```

2. **Decide where interactive checklists belong.** Find `MarkdownView` usages:

```bash
grep -rn "MarkdownView" src app components 2>/dev/null
```

For a detail view where the reader should be able to tick items, wire `onChange` to your existing save path — the string it hands you is the same full Markdown your editor saves:

```tsx
<MarkdownView
content={task.notes}
onChange={(md) => saveNotes(task.id, md)} // return the Promise to get busy-state + rollback
/>
```

3. **Return the Promise** from your save call if you want the built-in busy state and automatic rollback on failure. Return nothing (`void`) if your save is fire-and-forget.

4. **Do not pass `onChange` where the user lacks write access.** `MarkdownView` performs no authorization; it only serializes and reports. The consumer validates and authorizes the write.

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.12.0

Optionally wire MarkdownView onChange to make task-list checkboxes tickable.
```

### Guard rails

- **Do not** build your own busy/disabled state around `MarkdownView` when you return a Promise — the view already disables its checkboxes while the save is pending.
- **Do not** try to derive "which item changed" yourself — `onChange` already hands you the complete updated Markdown; save it as-is.
- **Do not** pass `onChange` to a read-only audience; omit it to keep checkboxes non-interactive.
- **Do not** expect text to become editable — only checkboxes are interactive; `MarkdownView` is still not an editor.

### Don't do this

- Do not render your own progress spinner *inside* the checklist expecting the library to place it — the library owns only the disabled state; render saving UI where it belongs on your page.
- Do not bundle unrelated dependency bumps into the same change.
101 changes: 101 additions & 0 deletions specs/003-markdown-view-checkboxes/steps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Implementation Steps: Markdown view checkboxes

## Step 1: Optional `onChange` on `MarkdownViewProps`

- [x] Add `readonly onChange?: (markdown: string) => void | Promise<void>` to `MarkdownViewProps`
- [x] Document the three engagement levels (omitted / void / Promise)

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

**Related behaviors:** all (type foundation)

---

## Step 2: Factory support for `onReadOnlyChecked`

- [x] Add `readonly onReadOnlyChecked?: (node, checked) => boolean` to `MarkdownExtensionsOptions`
- [x] Pass it through to `TaskItem.configure`
- [x] Derive the ProseMirror `Node` type without adding a `@tiptap/pm` dependency

**Acceptance criteria:**
- [x] `pnpm typecheck` and `pnpm build` pass
- [x] Specs 001/002 tests still pass unchanged

**Related behaviors:** foundation for interactive checkboxes

---

## Step 3: Interactive `MarkdownView`

- [x] Apply the toggle in a component-owned transaction (resolve the toggled item's position by DOM order — the node handed to `onReadOnlyChecked` goes stale — then `setNodeMarkup`)
- [x] Report the full serialized Markdown via `onChange`
- [x] Track a component-wide `busy` flag (ref + state); disable every checkbox and mute it while a Promise is in flight
- [x] Swallow clicks (return `false`) while busy or when no `onChange` is provided
- [x] Roll back to the last confirmed Markdown on rejection; keep the last confirmed baseline on resolution
- [x] Guard the sync effect (only `setContent` when `content` differs from the serialized document); a genuinely new `content` becomes the new baseline
- [x] Keep `editable: false`

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

**Related behaviors:** all

---

## Step 4: Behaviour tests

- [x] Extend `src/components/__tests__/markdown-view.test.tsx`, driving real checkbox `change` events
- [x] Without `onChange`: click reverts, document unchanged; checkboxes reflect stored state
- [x] With `onChange`: ticking/unticking reports the full Markdown; only the clicked (incl. nested) item changes
- [x] Optimistic: the checkbox flips before an unsettled Promise resolves
- [x] Busy: a pending save disables/mutes all checkboxes; a second click is ignored and does not call `onChange` again; resolving re-enables; a void return leaves no busy state
- [x] Rollback: a rejected save reverts the document and re-enables; a rejection after a prior success reverts only the failed change
- [x] `content` prop: an echo is a no-op; a genuinely different value replaces; a value arriving during a pending save wins over the rollback
- [x] Edge: no task list → `onChange` never called; empty content → no checkbox; text stays non-editable

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

**Related behaviors:** all 20 scenarios

---

## Step 5: Documentation

- [x] Create `docs/upgrade-to-0.12.md` (additive) — new optional `onChange` on `MarkdownView`, the three engagement levels, and the security note that consumers own validation/authorization
- [x] Update `README.md` MarkdownView entry to mention interactive checkboxes via `onChange`

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

**Related behaviors:** none (documentation)

---

## Behavior Coverage

| Scenario | Layer | Covered in Step |
|----------|-------|-----------------|
| Clicking a checkbox reverts it (no onChange) | Frontend | 4 |
| Checkboxes still reflect their stored state | Frontend | 4 |
| Ticking reports the full updated Markdown | Frontend | 4 |
| Unticking reports the full updated Markdown | Frontend | 4 |
| The checkbox flips immediately | Frontend | 4 |
| Only the clicked item changes | Frontend | 4 |
| Ticking a nested item changes only that item | Frontend | 4 |
| A pending save disables all checkboxes | Frontend | 4 |
| A click during a pending save is ignored | Frontend | 4 |
| Resolving re-enables interaction | Frontend | 4 |
| A void return means no busy state | Frontend | 4 |
| A rejected save reverts the document | Frontend | 4 |
| A rejected save re-enables interaction | Frontend | 4 |
| A rejection after a successful save reverts only the failed change | Frontend | 4 |
| An echoed `content` does not rebuild the document | Frontend | 4 |
| A genuinely different `content` replaces the document | Frontend | 4 |
| A `content` change during a pending save wins | Frontend | 4 |
| A view without task lists is unaffected | Frontend | 4 |
| Empty content renders nothing interactive | Frontend | 4 |
| Text remains non-editable | Frontend | 4 |

Every scenario is assigned. All are component-level and covered in Step 4.
2 changes: 1 addition & 1 deletion specs/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
|-----|-------------|------|-------|-------------|--------------|--------|
| 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 | — | done |
| 003 | 003-markdown-view-checkboxes | Markdown view checkboxes | frontend, api | Tick task list checkboxes directly in MarkdownView with optimistic update and rollback | — | open |
| 003 | 003-markdown-view-checkboxes | Markdown view checkboxes | frontend, api | Tick task list checkboxes directly in MarkdownView with optimistic update and rollback | — | done |
Loading
Loading