From 4b271e96b707af0fdf2469cc58b7f501832d04c2 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Mon, 28 Sep 2026 23:32:19 -0700 Subject: [PATCH 1/2] chore: widen engine to ^0.9.0 (linkWidgets seam), 0.5.0 Engine 0.9.0 adds linkWidgets() and refreshLinkWidgets. They compose through the extensions prop with no wrapper code changes; a wrapper test guards that path and byte fidelity. pnpm-lock.yaml relocks once engine 0.9.0 is on npm. --- CHANGELOG.md | 11 +++++ README.md | 9 ++++ package.json | 6 +-- test/link-widgets.test.tsx | 84 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 test/link-widgets.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index aafe003..895a5ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this package will be documented in this file. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow [SemVer](https://semver.org) (pre-1.0: minors may break). +## [0.5.0] + +### Changed + +- `@plannotator/atomic-editor` peer range widened to `^0.8.0 || ^0.9.0`; the dev engine is + `^0.9.0`. Engine 0.9.0 adds `linkWidgets()` and `refreshLinkWidgets`, a host seam that draws a + single-line `[text](url)` link as the host's own widget (a chip, a mention) while the markdown + stays the link byte for byte. It composes through the `extensions` prop with no wrapper code + changes; `test/link-widgets.test.tsx` guards that path and the fidelity rule. Links inside table + cells keep the link look in engine 0.9.0. + ## [0.4.0] ### Added diff --git a/README.md b/README.md index 751a164..16912ea 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,15 @@ How it behaves: `slashCommands()` is a Notion-style insert menu on `/` at the start of a line; `selectionToolbar()` is a floating bold/italic/strike/code/link bar over selected text (works multi-line and inside table cells). Both are themeable via the `--atomic-editor-menu-*` CSS variables and documented in the [atomic-editor changelog](https://github.com/plannotator/atomic-editor/blob/main/CHANGELOG.md). + Engine 0.9.0 adds `linkWidgets()`, which draws a single-line `[text](url)` link as your own CM6 widget while the markdown stays the link. The engine decides when the widget yields to the raw source (caret inside or at an edge, focus, pointer-press freeze, diff changes); dispatch `refreshLinkWidgets` when your answer changes without a document edit: + + ```tsx + import { linkWidgets, refreshLinkWidgets } from "@plannotator/atomic-editor"; + + chipFor(url, text) })]} ... /> + // later: view.dispatch({ effects: refreshLinkWidgets.of(null) }) + ``` + - **`className` / `cardClassName`**: extra classes on the wrapper and inner card, for stacking, shadows, or padding your app needs. ## Theming diff --git a/package.json b/package.json index 855755b..c7adf0e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@plannotator/markdown-editor", - "version": "0.4.0", + "version": "0.5.0", "description": "Live-preview markdown editor for React (CodeMirror 6, atomic-editor). Raw markdown is the source of truth: rendering is decoration only, edits round-trip byte-identical.", "keywords": [ "codemirror", @@ -59,7 +59,7 @@ "@codemirror/legacy-modes": "^6.5.3", "@codemirror/state": "^6.7.0", "@codemirror/view": "^6.43.0", - "@plannotator/atomic-editor": "^0.8.0", + "@plannotator/atomic-editor": "^0.9.0", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", "happy-dom": "^20.0.0", @@ -78,7 +78,7 @@ "@codemirror/language": "^6.0.0", "@codemirror/legacy-modes": "^6.0.0", "@codemirror/state": "^6.0.0", - "@plannotator/atomic-editor": "^0.8.0", + "@plannotator/atomic-editor": "^0.8.0 || ^0.9.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, diff --git a/test/link-widgets.test.tsx b/test/link-widgets.test.tsx new file mode 100644 index 0000000..b60fad0 --- /dev/null +++ b/test/link-widgets.test.tsx @@ -0,0 +1,84 @@ +/** + * Engine 0.9.0's `linkWidgets()` seam composes through the wrapper's + * `extensions` prop: a host widget draws in place of a `[text](url)` link, + * the document bytes are untouched, and `refreshLinkWidgets` re-asks the + * host without a document change. The engine owns the reveal rules and + * tests them itself; this guards the wrapper path and the fidelity rule. + */ +import { describe, expect, test } from "vitest"; +import { createRoot } from "react-dom/client"; +import { act } from "react"; +import { EditorView, WidgetType } from "@codemirror/view"; +import { linkWidgets, refreshLinkWidgets } from "@plannotator/atomic-editor"; +import type { LinkWidgetLink } from "@plannotator/atomic-editor"; +import { MarkdownEditor } from "../src/MarkdownEditor.js"; +import type { MarkdownEditorHandle } from "../src/MarkdownEditor.js"; + +const DOC = + "# Decisions\n\nDecided: [Ship on Friday](dec://42) today.\n\nSee [docs](https://example.com).\n"; + +class ChipWidget extends WidgetType { + constructor(readonly text: string) { + super(); + } + + override eq(other: ChipWidget): boolean { + return other.text === this.text; + } + + override toDOM(): HTMLElement { + const span = document.createElement("span"); + span.className = "test-chip"; + span.textContent = this.text; + return span; + } +} + +describe("linkWidgets through the wrapper", () => { + test("draws the host widget, keeps the bytes, and re-asks on refresh", async () => { + const known = new Set(); + const match = (link: LinkWidgetLink) => + known.has(link.url) ? new ChipWidget(link.text) : null; + const host = document.createElement("div"); + host.style.width = "600px"; + host.style.height = "400px"; + document.body.appendChild(host); + const handleRef: { current: MarkdownEditorHandle | null } = { current: null }; + const root = createRoot(host); + await act(async () => { + root.render( + , + ); + }); + + // Nothing known yet: the engine's own link look. + expect(host.querySelector(".test-chip")).toBeNull(); + + known.add("dec://42"); + const editorDom = host.querySelector(".cm-editor"); + const view = editorDom ? EditorView.findFromDOM(editorDom) : null; + expect(view).not.toBeNull(); + await act(async () => { + view?.dispatch({ effects: refreshLinkWidgets.of(null) }); + }); + + const chips = host.querySelectorAll(".test-chip"); + expect(chips).toHaveLength(1); + expect(chips[0]?.textContent).toBe("Ship on Friday"); + expect(chips[0]?.closest(".cm-atomic-link")).toBeNull(); + // The unmatched link keeps the engine look. + expect(host.querySelector(".cm-atomic-link")?.textContent).toBe("docs"); + // The one inviolable rule: drawing a widget never touches the bytes. + expect(handleRef.current?.getMarkdown()).toBe(DOC); + + await act(async () => { + root.unmount(); + }); + host.remove(); + }); +}); From e92533e6252a39fbc717758946f0f75548e0d5af Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Tue, 29 Sep 2026 08:17:46 -0700 Subject: [PATCH 2/2] chore: lock @plannotator/atomic-editor 0.9.0 --- pnpm-lock.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21d305c..af3b4ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,8 +33,8 @@ importers: specifier: ^6.43.0 version: 6.43.4 '@plannotator/atomic-editor': - specifier: ^0.8.0 - version: 0.8.0(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.4)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.11)(@codemirror/lang-javascript@6.2.5)(@codemirror/lang-json@6.0.2)(@codemirror/lang-markdown@6.5.0)(@codemirror/lang-python@6.2.1)(@codemirror/lang-yaml@6.1.3)(@codemirror/language@6.12.4)(@codemirror/legacy-modes@6.5.3)(@codemirror/merge@6.12.2)(@codemirror/search@6.7.1)(@codemirror/state@6.7.0)(@codemirror/view@6.43.4)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/markdown@1.6.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.2) + specifier: ^0.9.0 + version: 0.9.0(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.4)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.11)(@codemirror/lang-javascript@6.2.5)(@codemirror/lang-json@6.0.2)(@codemirror/lang-markdown@6.5.0)(@codemirror/lang-python@6.2.1)(@codemirror/lang-yaml@6.1.3)(@codemirror/language@6.12.4)(@codemirror/legacy-modes@6.5.3)(@codemirror/merge@6.12.2)(@codemirror/search@6.7.1)(@codemirror/state@6.7.0)(@codemirror/view@6.43.4)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/markdown@1.6.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.2) '@types/react': specifier: ^19.2.0 version: 19.2.17 @@ -533,8 +533,8 @@ packages: cpu: [x64] os: [win32] - '@plannotator/atomic-editor@0.8.0': - resolution: {integrity: sha512-s4IaRrlFjluYvMVRS/XJjAaHVR5I/4yAxAFWlUHk74RE7W9BQO8lazWgYgNyRSYQtdlEZsJiPgzaDeOuocErWg==} + '@plannotator/atomic-editor@0.9.0': + resolution: {integrity: sha512-TWiv9zkMUPpsw5gIX0zWNM9xxS7LTqgs0FC5Dpg6kl+cushddaWzrRHO8Z7WaMfoyMvSg2kiQYiUvZMSxba+dQ==} engines: {node: '>=18'} peerDependencies: '@codemirror/autocomplete': ^6.0.0 @@ -1443,7 +1443,7 @@ snapshots: '@oxlint/binding-win32-x64-msvc@1.72.0': optional: true - '@plannotator/atomic-editor@0.8.0(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.4)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.11)(@codemirror/lang-javascript@6.2.5)(@codemirror/lang-json@6.0.2)(@codemirror/lang-markdown@6.5.0)(@codemirror/lang-python@6.2.1)(@codemirror/lang-yaml@6.1.3)(@codemirror/language@6.12.4)(@codemirror/legacy-modes@6.5.3)(@codemirror/merge@6.12.2)(@codemirror/search@6.7.1)(@codemirror/state@6.7.0)(@codemirror/view@6.43.4)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/markdown@1.6.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.2)': + '@plannotator/atomic-editor@0.9.0(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.4)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.11)(@codemirror/lang-javascript@6.2.5)(@codemirror/lang-json@6.0.2)(@codemirror/lang-markdown@6.5.0)(@codemirror/lang-python@6.2.1)(@codemirror/lang-yaml@6.1.3)(@codemirror/language@6.12.4)(@codemirror/legacy-modes@6.5.3)(@codemirror/merge@6.12.2)(@codemirror/search@6.7.1)(@codemirror/state@6.7.0)(@codemirror/view@6.43.4)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/markdown@1.6.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.2)': dependencies: '@codemirror/autocomplete': 6.20.3 '@codemirror/commands': 6.10.4