@liminis/editor 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +126 -14
  2. package/dist/annotations/types.d.ts +16 -0
  3. package/dist/app/App.d.ts +10 -2
  4. package/dist/app/App.js +3 -3
  5. package/dist/app/editor/AnchorScrollPlugin.js +2 -34
  6. package/dist/app/editor/AnnotationSurface.js +2 -1
  7. package/dist/app/editor/CorrectionPanelPlugin.js +10 -10
  8. package/dist/app/editor/DocumentOutline.d.ts +17 -0
  9. package/dist/app/editor/DocumentOutline.js +32 -0
  10. package/dist/app/editor/DragHandlePlugin.js +1 -1
  11. package/dist/app/editor/Editor.d.ts +10 -1
  12. package/dist/app/editor/Editor.js +3 -2
  13. package/dist/app/editor/OutlinePlugin.d.ts +14 -0
  14. package/dist/app/editor/OutlinePlugin.js +133 -0
  15. package/dist/app/editor/SelectionContextMenuPlugin.js +4 -4
  16. package/dist/app/editor/annotation-marks.d.ts +20 -0
  17. package/dist/app/editor/annotation-marks.js +40 -0
  18. package/dist/app/editor/documentOutlineHandle.d.ts +87 -0
  19. package/dist/app/editor/documentOutlineHandle.js +86 -0
  20. package/dist/app/editor/nodes/C4Component.js +6 -6
  21. package/dist/app/editor/nodes/DiagramContextMenu.js +5 -5
  22. package/dist/app/editor/scrollContainer.d.ts +18 -0
  23. package/dist/app/editor/scrollContainer.js +47 -0
  24. package/dist/index.d.ts +4 -0
  25. package/dist/index.js +6 -0
  26. package/dist/markdown/stringify.js +65 -0
  27. package/dist/styles.css +342 -221
  28. package/docs/decisions/adr-085.md +166 -0
  29. package/docs/decisions/adr-086.md +147 -0
  30. package/docs/decisions/adr-087.md +216 -0
  31. package/docs/decisions/adr-088.md +136 -0
  32. package/docs/decisions/adr-089.md +121 -0
  33. package/docs/decisions/adr-090.md +141 -0
  34. package/docs/editor-api.md +80 -1
  35. package/package.json +6 -1
@@ -0,0 +1,121 @@
1
+ # ADR-089: Existing-Mark Geometry (`getMarkRects`) Lives on `AnnotationEditorHandle`, Is Viewport-Relative, and Is Pull-Only
2
+
3
+ **Date:** 2026-08-19
4
+ **Status:** Accepted
5
+ **Supersedes:** none
6
+ **Amends:** none
7
+ **Issue:** #73 (verveguy/liminis-editor)
8
+
9
+ ## Context
10
+
11
+ ADR-088 (issue #69, the Document Outline widget) considered and declined to
12
+ add a new `EditorHostServices` member for scroll-container/element-rect data,
13
+ naming Zusammen [#126](https://github.com/verveguy/zusammen/issues/126) —
14
+ "margin comments aligned to text" — as the not-yet-planned consumer that
15
+ would want the same positional primitive, and deliberately deferred the
16
+ question until #126 "does start." That is now: #126 needs a host to look up
17
+ where an *existing* annotation's mark currently renders, so it can position a
18
+ margin note against the annotated text. Today `AnnotationEditorHandle`
19
+ exposes `removeMarksForAnnotation` and `collectLiveAnchorSnapshots`
20
+ (document-offset ranges, not screen geometry) for live marks, and
21
+ `AnnotationCreateEvent.rect` delivers a one-shot `DOMRect` only at the moment
22
+ an annotation is created — nothing lets a host ask "where is annotation X
23
+ *right now*."
24
+
25
+ Three questions were explicitly left open by the issue's spec for this
26
+ decision:
27
+
28
+ 1. **Surface placement** — extend `AnnotationEditorHandle` (mechanics-side,
29
+ matching its two existing members) or add a new `EditorHostServices`
30
+ member (host-seam-side, the option ADR-088 deferred)?
31
+ 2. **Coordinate space** — raw viewport-relative `getBoundingClientRect()`, or
32
+ rects adjusted relative to the editor's scroll container?
33
+ 3. **Staleness model** — a pull-based snapshot the host re-requests, or a
34
+ subscription/observer the package pushes updates through?
35
+
36
+ ## Decision
37
+
38
+ **`getMarkRects(annotationIds?)` is added to `AnnotationEditorHandle`, not to
39
+ `EditorHostServices`.** It keeps every live-mark operation — remove, snapshot,
40
+ and now geometry — on the one seam a host already holds a reference to for
41
+ annotations, rather than splitting positional queries onto a second surface
42
+ for no functional reason. This does not reopen ADR-088: that decision was
43
+ about the *outline widget's* scroll-container discovery (`AnchorScrollPlugin`
44
+ / `scrollContainer.ts`), which stays exactly as it is. `getMarkRects` needs
45
+ none of that machinery — it resolves annotation ids to `MarkNode` elements
46
+ (already solved, and already tested, by `markElementsByAnnotationId`) and
47
+ reads each element's own `getBoundingClientRect()`.
48
+
49
+ **Rects are raw, viewport-relative `getBoundingClientRect()` output — no
50
+ scroll-container adjustment.** This matches the only existing precedent for
51
+ rect delivery in this package, `AnnotationCreateEvent.rect`
52
+ (`AnnotationPlugin.tsx`), which is also unadjusted. A host that wants
53
+ scroll-container-relative coordinates can subtract that container's own
54
+ `getBoundingClientRect()` origin itself — exactly what
55
+ `AnchorScrollPlugin.tsx` already does internally for its own purposes — so
56
+ nothing is lost by not building that adjustment into the package.
57
+
58
+ **`getMarkRects` is pull-only: a fresh snapshot computed at call time, with no
59
+ subscription or observer.** `getBoundingClientRect()` is inherently a live
60
+ query, so a pull-based method trivially satisfies "no caching, no staleness"
61
+ with zero extra machinery. ADR-088 declined to add a host-seam member for
62
+ positional data at all and deferred to "when #126 starts" — it did not
63
+ direct that the eventual mechanism be a subscription, only that the simpler
64
+ pull-based need (this issue's actual scope) be resolved once a real consumer
65
+ existed. Building a push/subscribe model now, before any consumer has asked
66
+ for continuous updates rather than an on-demand read, would be the same kind
67
+ of speculative, untested surface ADR-088 itself avoided.
68
+
69
+ Built directly on `markElementsByAnnotationId(editor, ids)`: for a multi-block
70
+ annotation (several sibling `MarkNode`s sharing one id), the result is one
71
+ `DOMRect` per constituent element, in document order — the host, not the
72
+ package, decides how to combine them. An id with no currently live mark is
73
+ simply absent from the result map, never a thrown error. Two ids can report
74
+ an identical rect when their marks share one `MarkNode` (overlapping
75
+ annotations) — expected, and documented on the method itself so a host does
76
+ not mistake it for a bug.
77
+
78
+ ## Consequences
79
+
80
+ **Good:**
81
+
82
+ - Zero new architectural surface: `AnnotationEditorHandle` gains a third
83
+ closure over an already-existing, already-tested helper
84
+ (`markElementsByAnnotationId`), the same pattern its two existing members
85
+ already follow.
86
+ - A host that wants scroll-container-relative geometry can derive it trivially
87
+ from the container's own rect; the package is not left guessing which of a
88
+ host's several possible scroll containers is the relevant one.
89
+ - Resolves ADR-088's explicit deferral: #126 now has a concrete, shipped
90
+ primitive rather than a "wait until it's needed" placeholder.
91
+ - No subscription lifecycle (registration, teardown, re-render scheduling) to
92
+ design, test, or maintain until a consumer actually needs push updates.
93
+
94
+ **Bad / accepted:**
95
+
96
+ - A host that wants to keep UI glued to a mark across every scroll/resize/edit
97
+ must poll `getMarkRects` itself (e.g. on a `ResizeObserver`/scroll listener
98
+ it owns) — the package does no observing on the host's behalf. Accepted as
99
+ the deliberately smaller, already-demonstrated scope; a push model can be
100
+ layered on later without displacing this pull-based method.
101
+ - Viewport-relative coordinates mean a host embedding the editor inside a
102
+ transformed or nested scroll container must do its own translation to a
103
+ meaningful local space — no worse than the precedent
104
+ (`AnnotationCreateEvent.rect`) already accepts.
105
+
106
+ **Neutral:**
107
+
108
+ - If a genuinely continuous-update consumer emerges later, it can be added as
109
+ a second, additive API (e.g. a subscribe variant) without touching
110
+ `getMarkRects`'s existing shape or forcing every caller to opt into observer
111
+ bookkeeping they don't need.
112
+
113
+ ## References
114
+
115
+ - Issue #73 (this decision)
116
+ - `src/app/editor/annotation-marks.ts` — `getMarkRects`, built on `markElementsByAnnotationId`
117
+ - `src/annotations/types.ts` — `AnnotationEditorHandle`, where `getMarkRects` joins `removeMarksForAnnotation` and `collectLiveAnchorSnapshots`
118
+ - `src/app/editor/AnnotationSurface.tsx` — `AnnotationEditorHandlePlugin`, where the closure is wired
119
+ - `src/app/editor/AnnotationPlugin.tsx` — the existing one-shot `AnnotationCreateEvent.rect`, the precedent for raw viewport-relative rects
120
+ - `docs/decisions/adr-088.md` — the deferred host-seam-vs-package-local question this decision resolves for positional annotation data specifically (ADR-088's own scroll-container-discovery decision is untouched)
121
+ - Zusammen issue [#126](https://github.com/verveguy/zusammen/issues/126) — the consumer this capability unblocks
@@ -0,0 +1,141 @@
1
+ # ADR-090: A Text-Based Contract Test Guards `App`'s Forwarding of `EditorProps`, Checked Against JSX-Attribute Reachability
2
+
3
+ **Date:** 2026-08-19
4
+ **Status:** Accepted
5
+ **Supersedes:** none
6
+ **Amends:** none
7
+ **Issue:** #80 (verveguy/liminis-editor)
8
+
9
+ ## Context
10
+
11
+ Most consumers of `@liminis/editor` embed the package via `<App>`, not
12
+ `<Editor>` directly — Zusammen, the primary downstream consumer, mounts
13
+ `App`. `App` wraps `<Editor>` and forwards a curated, hand-maintained subset
14
+ of `EditorProps` as `AppProps`, passing each one straight through in its
15
+ render body's `<Editor .../>` call.
16
+
17
+ Issue #69 added `documentOutlineHandle` to `EditorProps`. It did not add the
18
+ same prop to `AppProps`, so `<App>` consumers had no way to reach the
19
+ document outline — a capability exported from the package yet structurally
20
+ unreachable through the component most consumers actually use. Nothing in
21
+ the code made this omission visible; it was discovered the way this class of
22
+ bug always is, one prop at a time, by a consumer hitting the gap.
23
+
24
+ Because `App`'s forwarding list is maintained by hand with no mechanical
25
+ link back to `EditorProps`, the same gap can recur for any future member
26
+ added to `EditorProps`. This decision is about what closes that class of
27
+ bug, not just the one prop: what the guard actually checks, and how it is
28
+ implemented.
29
+
30
+ Two questions needed a considered answer:
31
+
32
+ 1. **What does "forwarded" mean for the guard to check?** `AppProps`
33
+ declaring a member with a matching name is necessary but not sufficient —
34
+ a member could be added to the `AppProps` interface and never wired into
35
+ the `<Editor .../>` JSX call in `App`'s render body, leaving it exactly as
36
+ unreachable as `documentOutlineHandle` was, while satisfying a check that
37
+ only inspects the type declaration.
38
+ 2. **How should the guard extract that information?** The repo already has
39
+ an established contract-test convention (`tests/theming-contract.test.ts`,
40
+ `tests/package-manifest-contract.test.ts`), both of which parse source
41
+ files as text with targeted regexes rather than using a compiler API.
42
+ `typescript` is a devDependency (`^6.0.3`) but is not used
43
+ programmatically anywhere in the repo.
44
+
45
+ ## Decision
46
+
47
+ **The guard checks JSX-attribute presence on the `<Editor .../>` element
48
+ inside `App.tsx`, not `AppProps`-interface-member presence.**
49
+ `scripts/lib/editor-app-prop-forwarding.mjs` extracts every top-level member
50
+ name from `EditorProps` (`src/app/editor/Editor.tsx`) and every
51
+ `attr={expr}` attribute name on the `<Editor .../>` JSX element in `App`'s
52
+ render body (`src/app/App.tsx`). `tests/editor-app-forwarding-contract.test.ts`
53
+ asserts every `EditorProps` member is one of those attribute names, or is
54
+ listed in `EDITOR_INTERNAL_PROPS` (in the same lib module) with a stated
55
+ reason. This directly encodes "forwarded" as "actually reaches the Editor
56
+ element," which is strictly stronger than, and implies, "declared on
57
+ `AppProps`."
58
+
59
+ **Some `EditorProps` members are legitimately App-internal and belong in the
60
+ allowlist, not the forwarding list.** `App` has two operating modes —
61
+ IPC-driven (the default, used by the Electron host) and inline
62
+ (`content`/`onChange` props, no IPC) — and several members are sourced from
63
+ `App`'s own IPC or bookkeeping state in both modes rather than being a value
64
+ a caller supplies: `contentVersion`, `cursorToRestoreRef`, `onCursorChange`,
65
+ `assetBaseUri`, `documentDirUri`, `imagePathResolution`. A seventh,
66
+ `initialContent`, is not internal in the same sense — it is reachable via
67
+ `AppProps.content`, which is then multiplexed with IPC-sourced state before
68
+ reaching `<Editor>` as `initialContent` — but it fails a same-name match, so
69
+ it is recorded in the same allowlist with a reason distinguishing "renamed
70
+ pass-through" from "genuinely internal." The guard's job is presence and
71
+ classification, not semantic tracking of renames, so both cases are handled
72
+ by the one allowlist mechanism rather than by building rename-detection
73
+ logic for a single instance.
74
+
75
+ **The guard is a text/regex-based contract test, matching the two existing
76
+ precedents, not a TypeScript-compiler-API-based one.** Both `EditorProps`
77
+ and `AppProps` are simple flat property lists today — every member is
78
+ declared on a single line, even where its type contains nested braces or
79
+ generics (`annotationEditorHandleRef?: MutableRefObject<AnnotationEditorHandle
80
+ | null>`, `scrollToAnnotation?: { id: string; nonce: number } | null`). A
81
+ brace-depth-tracking line scanner only needs to establish "am I at interface
82
+ top level," not parse the nested type, which is sufficient for both files as
83
+ they stand. Introducing `typescript`'s AST/type-checker API programmatically
84
+ for the first time in this repo would be more robust against future
85
+ syntactic variation (a multi-line declaration, a spread in the JSX call) but
86
+ disproportionate to interfaces this simple, and would set a new pattern
87
+ inconsistent with `theming-contract.test.ts` and
88
+ `package-manifest-contract.test.ts`.
89
+
90
+ ## Consequences
91
+
92
+ **Good:**
93
+
94
+ - Adding a new `EditorProps` member without forwarding it on `<Editor .../>`
95
+ in `App.tsx`, and without an `EDITOR_INTERNAL_PROPS` entry, now fails CI
96
+ with a message naming the unclassified member — the exact recurrence this
97
+ issue exists to prevent.
98
+ - Checking JSX-attribute reachability rather than interface-member
99
+ declaration closes the specific loophole a weaker guard would leave open:
100
+ a prop added to the `AppProps` type but never wired into the render body.
101
+ - No new tooling dependency: the guard follows the same regex/text-parsing
102
+ shape as the two existing contract tests, so a maintainer who already
103
+ understands `theming-contract.test.ts` can read this one the same way.
104
+ - `EDITOR_INTERNAL_PROPS`'s reasons are colocated with the guard itself
105
+ (`scripts/lib/editor-app-prop-forwarding.mjs`), discoverable by reading the
106
+ guard rather than requiring a separate document.
107
+
108
+ **Bad / accepted:**
109
+
110
+ - The extractor assumes every interface member and every JSX attribute is
111
+ declared on a single line, and that the `<Editor .../>` call uses
112
+ `attr={expr}` syntax throughout (no `{...props}` spread). Both hold for
113
+ every current member and for `App.tsx`'s current render body; a future
114
+ multi-line declaration or a spread refactor would need the extractor
115
+ updated, and would silently under- or over-count until then. Accepted as
116
+ the same tradeoff `theming-contract.test.ts` already accepts for CSS custom
117
+ properties.
118
+ - `EDITOR_INTERNAL_PROPS` is a plain allowlist a contributor must remember to
119
+ update — the guard fails loudly if they forget, but nothing prevents an
120
+ entry from being added for the wrong reason (a prop that could have been
121
+ forwarded, allowlisted instead to make CI pass). The guard checks presence
122
+ and classification, not whether the classification is *correct* — that
123
+ remains a code-review concern, matching the spec's own stated edge case
124
+ that behavioral/type equivalence is out of the guard's scope.
125
+
126
+ **Neutral:**
127
+
128
+ - If `EditorProps` or `AppProps` later grow member declarations complex
129
+ enough that the single-line assumption breaks down, that is a signal to
130
+ revisit the AST-based approach this decision declined — not a reason to
131
+ build it preemptively now.
132
+
133
+ ## References
134
+
135
+ - Issue #80 (this decision)
136
+ - `scripts/lib/editor-app-prop-forwarding.mjs` — the extraction logic and `EDITOR_INTERNAL_PROPS` allowlist
137
+ - `tests/editor-app-forwarding-contract.test.ts` — the contract test itself
138
+ - `src/app/App.tsx` — `AppProps`, and the `<Editor .../>` render-body call the guard reads
139
+ - `src/app/editor/Editor.tsx` — `EditorProps`, including `documentOutlineHandle` (added by #69, the prop this issue forwards)
140
+ - `tests/theming-contract.test.ts`, `tests/package-manifest-contract.test.ts` — the existing regex/text-based contract-test precedent this guard follows
141
+ - `docs/decisions/adr-088.md` — issue #69's own decision record, the origin of `documentOutlineHandle`
@@ -56,9 +56,88 @@ All optional; supplying `annotationKinds` is what turns the mechanism on. See
56
56
  | `scrollToAnnotation` | `{ id: string; nonce: number } \| null` | Host-driven scroll signal. Bump `nonce` to re-scroll to the same id. |
57
57
  | `onCreateAnnotation` | `(event: AnnotationCreateEvent) => void` | Fires when a user creates one via a kind's create affordance. |
58
58
  | `onActivateAnnotation` | `(id: string) => void` | Fires when a marker is activated. Your app opens its own panel. |
59
- | `annotationEditorHandleRef` | `MutableRefObject<AnnotationEditorHandle \| null>` | Imperative handle for the mounted editor's live marks. |
59
+ | `annotationEditorHandleRef` | `MutableRefObject<AnnotationEditorHandle \| null>` | Imperative handle for the mounted editor's live marks: `removeMarksForAnnotation`, `collectLiveAnchorSnapshots`, and `getMarkRects(annotationIds?)` — current on-screen `DOMRect[]` per annotation id (one rect per constituent mark, omitted ids means "every live annotation"), for a host positioning UI against annotated text. |
60
60
  | `annotationLogger` | `{ warn(message, ...args): void }` | Injected, so the package never imports a host logger. |
61
61
 
62
+ ## Document outline
63
+
64
+ A live, navigable "on this page" heading list (issue #69) — `<Editor>` owns
65
+ the Lexical document and its heading DOM, so the package derives entries and
66
+ implements scroll-to-heading; you decide whether, where, and how the outline
67
+ is shown.
68
+
69
+ ```tsx
70
+ import { useState } from 'react'
71
+ import { Editor, DocumentOutline, createDocumentOutlineHandle } from '@liminis/editor'
72
+
73
+ function MyEditorWithOutline() {
74
+ // Create once per editor instance — the same object connects the two
75
+ // components. A second editor on the same page needs its own handle.
76
+ const [outlineHandle] = useState(() => createDocumentOutlineHandle())
77
+
78
+ return (
79
+ <div style={{ display: 'flex' }}>
80
+ <Editor
81
+ initialContent={markdown}
82
+ onChange={setMarkdown}
83
+ documentOutlineHandle={outlineHandle}
84
+ />
85
+ <aside>
86
+ <DocumentOutline handle={outlineHandle} />
87
+ </aside>
88
+ </div>
89
+ )
90
+ }
91
+ ```
92
+
93
+ | Prop | Type | Notes |
94
+ |---|---|---|
95
+ | `<Editor>`'s `documentOutlineHandle` | `DocumentOutlineHandle` | Connects this editor instance to a `<DocumentOutline>`. Omit it and no outline plugin mounts at all — an outline-less consumer pays nothing for it. |
96
+ | `<DocumentOutline>`'s `handle` | `DocumentOutlineHandle` | The same object passed to `<Editor>`. |
97
+ | `<DocumentOutline>`'s `className` | `string` | Additional class on the outer `<nav>`, for your own placement/layout. |
98
+
99
+ `createDocumentOutlineHandle()` returns the shared controller: a React
100
+ external store (`subscribe`/`getSnapshot`, so `<DocumentOutline>` re-renders
101
+ live as the reader scrolls and the document changes) plus an imperative
102
+ `scrollToHeading(index)`. You never call its methods directly — `<Editor>`
103
+ feeds it, `<DocumentOutline>` reads it.
104
+
105
+ - Entries are H1–H5 headings in document order, indented by level, with
106
+ inline formatting (including inline code) reduced to plain text. Identity
107
+ is by position, not text, so duplicate headings are handled correctly.
108
+ - The active entry — the heading at the top of the viewport — is tracked as
109
+ the reader scrolls and kept visible within the outline itself.
110
+ - `<DocumentOutline>` renders nothing when the document has no headings.
111
+ - Visibility, placement, width-gating, and any collapse/persistence are
112
+ entirely up to you — `<DocumentOutline>` imposes no layout policy.
113
+
114
+ Most consumers embed this package via `<App>`, not `<Editor>` directly —
115
+ `<App>` forwards `documentOutlineHandle` straight through to its inner
116
+ `<Editor>`, so the same handle wires up an outline there too:
117
+
118
+ ```tsx
119
+ import { useState } from 'react'
120
+ import { App, DocumentOutline, createDocumentOutlineHandle } from '@liminis/editor'
121
+
122
+ function MyAppWithOutline() {
123
+ const [markdown, setMarkdown] = useState('# Introduction')
124
+ const [outlineHandle] = useState(() => createDocumentOutlineHandle())
125
+
126
+ return (
127
+ <div style={{ display: 'flex' }}>
128
+ <App
129
+ content={markdown}
130
+ onChange={setMarkdown}
131
+ documentOutlineHandle={outlineHandle}
132
+ />
133
+ <aside>
134
+ <DocumentOutline handle={outlineHandle} />
135
+ </aside>
136
+ </div>
137
+ )
138
+ }
139
+ ```
140
+
62
141
  ## The host seam
63
142
 
64
143
  The package boundary is drawn at **persistence**: text ranges, marks, rendering
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liminis/editor",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "//publishing": "Publishing is deliberate, never incidental. `private: true` was this package's guard until verveguy/liminis-editor#39 took the publish decision; it is gone because that decision was taken, not because it was tidied away. The guard is now `prepublishOnly` -> scripts/guard-publish.mjs, which refuses unless LIMINIS_ALLOW_PUBLISH=1 is set explicitly. That variable is set at step scope in .github/workflows/publish.yml and nowhere else, so a release is the only path that publishes. Note that `npm publish --dry-run` does NOT report a private package as blocked (npm 10.8.2), which is why the guard is a script rather than a flag.",
5
5
  "license": "MIT",
6
6
  "description": "Lexical-based markdown WYSIWYG editor with mdast round-trip and a host-injection seam",
@@ -18,6 +18,10 @@
18
18
  ],
19
19
  "//packageManager": "Not cosmetic. This field used to live in the monorepo root manifest and did not travel with the package, so `pnpm/action-setup` had no version to read and CI failed before installing anything. It is also what pins the pnpm that resolves the lockfile.",
20
20
  "packageManager": "pnpm@10.33.0",
21
+ "//engines": "A support statement, not a technical floor. Nothing in the dependency graph needs it — the highest `engines.node` among runtime dependencies is `>= 14.6` (yaml), and 18 of 21 declare none at all. It says which runtimes this package is maintained against: Node 20 reached end of life on 24 March 2026, and 22 and 24 are Active LTS. CI and the release both run 24, so 22 is supported by intent rather than by test — see the note in .github/workflows/ci.yml. Consumers get a warning rather than a hard failure unless they set engine-strict.",
22
+ "engines": {
23
+ "node": ">=22"
24
+ },
21
25
  "type": "module",
22
26
  "//entrypoints": "These point at dist/ here, in the checked-in manifest, and must stay that way. They lived under `publishConfig` until 0.1.1 so that in-workspace consumers could resolve raw TypeScript, and that silently shipped a broken 0.1.0: `publishConfig` manifest-field overrides (main/types/exports) are a pnpm and yarn feature, and npm honours `publishConfig` only for config values like access, registry and tag. `npm publish` therefore published src/ paths while `files` shipped only dist/, so every entry point resolved to a file that was not in the tarball. What is published is now exactly what is written here, and scripts/verify-package.mjs packs with the same client that publishes so the two cannot diverge again. See docs/decisions/adr-078.md.",
23
27
  "main": "./dist/index.js",
@@ -70,6 +74,7 @@
70
74
  "test:watch": "vitest",
71
75
  "test:coverage": "vitest run --coverage",
72
76
  "verify:package": "node scripts/verify-package.mjs",
77
+ "docs:theming": "node scripts/generate-theming-docs.mjs",
73
78
  "demo": "node scripts/run-demo.mjs",
74
79
  "build:examples": "node scripts/build-examples.mjs",
75
80
  "build:site": "node scripts/build-site.mjs"