Skip to content
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ 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
- **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)

## Usage
Expand Down
89 changes: 89 additions & 0 deletions docs/upgrade-to-0.11.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Upgrade prompt: `@open-elements/ui` 0.10.x → 0.11.0 (breaking)

`@open-elements/ui` 0.11.0 makes the `MarkdownEditor` toolbar configurable per usage and **changes its default**. This is a **breaking change** for authoring, not for stored content.

Until now every `MarkdownEditor` showed the same four buttons (Bold, Italic, Strikethrough, Link). 0.11.0 adds a `toolbar` prop — an ordered allowlist of actions — and the default drops to `["bold", "italic"]`. Every existing usage therefore **loses the Strikethrough and Link buttons** unless it declares them.

Nothing about stored content changes: links, strikethrough and every other construct still render and still round-trip exactly as in 0.10.0. Only the authoring affordance (which buttons appear) is affected. The schema is untouched — this prop governs the toolbar, not what the document can hold.

```ts
export type MarkdownToolbarAction =
| "bold" | "italic" | "strike" | "code" | "link"
| "h1" | "h2" | "h3"
| "bulletList" | "orderedList" | "taskList"
| "blockquote" | "codeBlock" | "horizontalRule";

// New optional prop; defaults to ["bold", "italic"].
<MarkdownEditor value={v} onChange={setV} toolbar={["bold", "italic", "strike", "link"]} />
```

Two smaller changes ride along:

- Task-list creation is gated by the toolbar. Where `"taskList"` is **not** in the allowlist, the `Mod-Shift-9` shortcut and the `[ ] ` input rule do nothing, so a checklist cannot be created by any means. Stored checklists still render and stay editable. All other actions are gated at the button only — `Mod-b` and `**bold**` keep working even where the Bold button is absent.
- Every toolbar button now carries an `aria-label` (and `aria-pressed` for active state), so icon-only buttons are announced by screen readers.

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.11.0`. This is a **breaking** change: the `MarkdownEditor` toolbar default changed, so each usage must declare the actions it needs.

### What changed in 0.11.0

- **New `toolbar` prop** on `MarkdownEditor`: `readonly toolbar?: readonly MarkdownToolbarAction[]`. It is an ordered allowlist — the buttons render in array order, duplicates collapse, and an empty array renders no toolbar at all.
- **The default dropped** from Bold/Italic/Strikethrough/Link to `["bold", "italic"]`. Any usage that relied on the old default now shows only Bold and Italic.
- **`taskList` creation is gated by the toolbar**; other actions are gated at the button only.
- **No stored-content change.** Links, strikethrough, headings, lists, task lists, etc. still render and round-trip. This is purely about which buttons appear.
- **No change to `MarkdownView`.**

### Steps

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

```bash
pnpm install
```

2. **Find every `MarkdownEditor` usage:**

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

3. **For each usage, decide the toolbar explicitly.** Do not blindly restore the old four buttons — this upgrade is the moment to make each field honest about what it offers. Guidance:
- A **tag / label description** field: keep it minimal, e.g. `toolbar={["bold", "italic"]}` (the new default — you can omit the prop) or `toolbar={[]}` for plain text.
- A **rich note / task description** field: declare what it needs, e.g. `toolbar={["bold", "italic", "strike", "link", "h2", "h3", "bulletList", "orderedList", "taskList"]}`.
- If a field previously relied on the Link or Strikethrough buttons, add `"link"` / `"strike"` back **explicitly** where they belong.

4. **If a field needs checkboxes**, include `"taskList"` — otherwise users cannot create task lists there (stored ones still render and stay editable regardless).

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

Declare an explicit toolbar on each MarkdownEditor usage; the default
dropped to ["bold", "italic"] in 0.11.0.
```

### Guard rails

- **Do not** add a global wrapper that re-injects the old four-button default everywhere — that defeats the point of the change. Decide per field.
- **Do not** try to gate marks (Bold/Italic/…) beyond hiding their buttons; only `taskList` creation is fully gated, by design.
- **Do not** touch `MarkdownView` usages — its props are unchanged.
- **Do not** treat missing links/strikethrough in stored content as data loss — they still render; only the button was removed.

### Don't do this

- Do not pass `"unlink"` in `toolbar` — it is not an action; Unlink appears automatically next to Link when the cursor is inside a link.
- Do not bundle unrelated dependency bumps into the same change.
11 changes: 11 additions & 0 deletions specs/001-markdown-schema-roundtrip/behaviors.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,14 @@
- **Given** an editor built from `createMarkdownExtensions()`
- **When** it is loaded with a plain paragraph containing no Markdown syntax
- **Then** the serialized Markdown is byte-identical to the input

---

## Drift Log

### 2026-08-16 — Caused by spec `002-markdown-toolbar-actions`

- **Affected scenario:** The toolbar is unchanged
- **Original behavior:** The `MarkdownEditor` toolbar offered exactly Bold, Italic, Strikethrough and Link (plus Unlink inside a link), fixed for every usage.
- **Current behavior:** The toolbar is configured per usage via a `toolbar` allowlist prop, and its default dropped to `["bold", "italic"]`. Strikethrough and Link are no longer shown unless declared. Stored content is unaffected — links and strikethrough still render and round-trip.
- **Reason:** Spec 002 makes the toolbar composable per field; the fixed four-button toolbar from spec 001 was intentionally superseded (spec 001's design already noted this would be handled in spec 002).
131 changes: 131 additions & 0 deletions specs/002-markdown-toolbar-actions/steps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Implementation Steps: Markdown toolbar actions

## Step 1: Public type and prop

- [x] Add `MarkdownToolbarAction` union type to `src/types/index.ts` (14 actions per design)
- [x] Add `readonly toolbar?: readonly MarkdownToolbarAction[]` to `MarkdownEditorProps` (default documented as `["bold", "italic"]`)
- [x] Export `MarkdownToolbarAction` from `src/index.ts`
- [x] Leave `MarkdownViewProps` untouched

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

**Related behaviors:** Unlink cannot be declared on its own; MarkdownView is unaffected

---

## Step 2: Conditional task-list gate in the factory

- [x] Add `readonly allowedActions?: readonly MarkdownToolbarAction[]` to `MarkdownExtensionsOptions`
- [x] Compute `taskListAllowed = (allowedActions ?? []).includes("taskList")`
- [x] Keep the `tight` attribute on `TaskList` in both cases (round-trip must survive)
- [x] When not allowed: strip `addKeyboardShortcuts` on `TaskList` and `addInputRules` on `TaskItem` (as spec 001 did)
- [x] When allowed: keep the default `Mod-Shift-9` shortcut and `[ ] ` input rule
- [x] Keep `TaskItem` `nested: true` and the styling classes in both cases

**Acceptance criteria:**
- [x] `pnpm typecheck` and `pnpm build` pass
- [x] Spec 001 round-trip tests still pass unchanged

**Related behaviors:** Without `taskList`, the keyboard shortcut/input rule does nothing; With `taskList`, they work; Existing task lists remain editable/round-trip without the action

---

## Step 3: Toolbar rendering from the allowlist

- [x] Replace the hardcoded `Toolbar` with an `ACTIONS` record mapping each `MarkdownToolbarAction` to `{ icon, label, isActive, run }`
- [x] Render one button per action in array order, de-duplicated (first occurrence wins)
- [x] Give each button both `aria-label` and `title` with the same English string
- [x] `"link"` keeps the `window.prompt` flow and the contextual Unlink button
- [x] Render no toolbar element at all when the resolved action list is empty
- [x] Default the `toolbar` prop to `["bold", "italic"]` and pass it to the factory as `allowedActions`
- [x] Use lucide icons: Bold, Italic, Strikethrough, Code, Link, Heading1/2/3, List, ListOrdered, ListChecks, Quote, SquareCode, Minus

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

**Related behaviors:** all "Rendering the declared actions" and "Executing actions" scenarios; Every button has an accessible name

---

## Step 4: Factory / extension gate tests

- [x] Extend `src/lib/__tests__/markdown-extensions.test.ts`
- [x] Without `taskList`: `Mod-Shift-9` and `[ ] ` create nothing (reuse spec 001 helpers)
- [x] With `allowedActions: ["taskList"]`: `Mod-Shift-9` and `[ ] ` create a task item
- [x] With gating: an existing `- [x] Done` still splits on Enter and round-trips unchanged

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

**Related behaviors:** Task list creation gate (all five scenarios)

---

## Step 5: Component behaviour tests

- [x] Extend `src/components/__tests__/markdown-editor.test.tsx`
- [x] Rendering: exactly-declared; order; default (bold+italic only); empty array → no toolbar element; duplicate → one button
- [x] Executing: H2 transforms block and reports `## Title`; taskList wraps to `- [ ] Call Anna`; active state on bold; Link shows contextual Unlink that disappears on leaving
- [x] Marks gated at button only: `Mod-b` bolds without the Bold button (component); the typing `**bold**` variant is covered at the extension level in Step 4 (literal content cannot be injected through the component's markdown-parsed `value`)
- [x] Accessibility: with all 14 actions, every button has matching `aria-label` + `title` and is findable by accessible name
- [x] Rendering independent: with `toolbar={["bold"]}`, a loaded heading/blockquote/task list still render as structure

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

**Related behaviors:** Rendering; Executing; Marks are gated at the button only; Accessibility; Undeclared constructs still render

---

## Step 6: Type-level guarantees

- [x] Add a type test (e.g. `src/types/__tests__/markdown-toolbar-action.test-d.ts` or `@ts-expect-error` in a `.test.ts`) that `toolbar={["unlink"]}` does not typecheck
- [x] Assert `MarkdownViewProps` shape is unchanged (structural type assertion)

**Acceptance criteria:**
- [x] `pnpm typecheck` passes (the `@ts-expect-error` is satisfied)

**Related behaviors:** Unlink cannot be declared on its own; MarkdownView is unaffected

---

## Step 7: Documentation

- [x] Create `docs/upgrade-to-0.11.md` — breaking change: default toolbar drops to `["bold", "italic"]`; each existing usage must declare the actions it needs; content still renders/round-trips
- [x] Update `README.md` MarkdownEditor entry to mention the configurable `toolbar` prop

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

**Related behaviors:** none (documentation)

---

## Behavior Coverage

| Scenario | Layer | Covered in Step |
|----------|-------|-----------------|
| The toolbar renders exactly what was declared | Frontend | 5 |
| Order follows the array | Frontend | 5 |
| Omitting the prop yields the default | Frontend | 5 |
| An empty array renders no toolbar | Frontend | 5 |
| A duplicate entry renders once | Frontend | 5 |
| A block action transforms the current block | Frontend | 5 |
| A list action wraps the current block | Frontend | 5 |
| An active action is marked as active | Frontend | 5 |
| Link keeps its contextual Unlink button | Frontend | 5 |
| Unlink cannot be declared on its own | Type | 6 |
| Without `taskList`, the keyboard shortcut does nothing | Frontend | 4 |
| Without `taskList`, the input rule does nothing | Frontend | 4 |
| With `taskList`, the keyboard shortcut works | Frontend | 4 |
| With `taskList`, the input rule works | Frontend | 4 |
| Existing task lists remain editable without the action | Frontend | 4 |
| Existing task lists still round-trip without the action | Frontend | 4 |
| Bold stays reachable by shortcut without the button | Frontend | 5 (component) + 4 (extension) |
| Bold stays reachable by typing without the button | Frontend | 4 (extension) |
| Every button has an accessible name | Frontend | 5 |
| Undeclared constructs still render | Frontend | 5 |
| MarkdownView is unaffected | Type | 6 |

Every scenario is assigned. Type-level scenarios (Unlink, MarkdownView) are verified by the typecheck step.
2 changes: 1 addition & 1 deletion specs/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
| 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 | — | done |
| 002 | 002-markdown-toolbar-actions | Markdown toolbar actions | frontend, api | Compose the MarkdownEditor toolbar per usage via an explicit action allowlist | — | open |
| 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 |
Loading
Loading