@liminis/editor 0.1.0 → 0.2.0
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.
- package/README.md +126 -14
- package/dist/annotations/types.d.ts +16 -0
- package/dist/app/App.d.ts +1 -1
- package/dist/app/App.js +3 -3
- package/dist/app/editor/AnchorScrollPlugin.js +2 -34
- package/dist/app/editor/AnnotationSurface.js +2 -1
- package/dist/app/editor/CorrectionPanelPlugin.js +10 -10
- package/dist/app/editor/DocumentOutline.d.ts +17 -0
- package/dist/app/editor/DocumentOutline.js +32 -0
- package/dist/app/editor/DragHandlePlugin.js +1 -1
- package/dist/app/editor/Editor.d.ts +10 -1
- package/dist/app/editor/Editor.js +3 -2
- package/dist/app/editor/OutlinePlugin.d.ts +14 -0
- package/dist/app/editor/OutlinePlugin.js +133 -0
- package/dist/app/editor/SelectionContextMenuPlugin.js +4 -4
- package/dist/app/editor/annotation-marks.d.ts +20 -0
- package/dist/app/editor/annotation-marks.js +40 -0
- package/dist/app/editor/documentOutlineHandle.d.ts +87 -0
- package/dist/app/editor/documentOutlineHandle.js +86 -0
- package/dist/app/editor/nodes/C4Component.js +6 -6
- package/dist/app/editor/nodes/DiagramContextMenu.js +5 -5
- package/dist/app/editor/scrollContainer.d.ts +18 -0
- package/dist/app/editor/scrollContainer.js +47 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +6 -0
- package/dist/markdown/stringify.js +65 -0
- package/dist/styles.css +342 -221
- package/docs/decisions/adr-078.md +37 -2
- package/docs/decisions/adr-085.md +166 -0
- package/docs/decisions/adr-086.md +147 -0
- package/docs/decisions/adr-087.md +216 -0
- package/docs/decisions/adr-088.md +136 -0
- package/docs/decisions/adr-089.md +121 -0
- package/docs/editor-api.md +53 -1
- package/package.json +23 -46
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# ADR-088: The Document Outline Widget Stays Host-Seam-Free and Is Wired Through a Shared Controller Object, Not a Ref
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-08-18
|
|
4
|
+
**Status:** Accepted
|
|
5
|
+
**Supersedes:** none
|
|
6
|
+
**Amends:** none
|
|
7
|
+
**Issue:** #69 (verveguy/liminis-editor)
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
`liminis-app`'s table-of-contents widget — heading list, click-to-scroll,
|
|
12
|
+
scroll-spy — lived entirely in the app, duplicated its click-to-scroll logic
|
|
13
|
+
across two call sites, and hardcoded two different scroll-container ids
|
|
14
|
+
between them (`editor-panel-scroll-container` in one, `editor-scroll-container`
|
|
15
|
+
in the other). Extracting it into `@liminis/editor` as a considered export
|
|
16
|
+
(FR-001) raised two design questions the issue's own research flagged as
|
|
17
|
+
needing a deliberate answer rather than an implicit one:
|
|
18
|
+
|
|
19
|
+
1. **FR-006** requires the widget not hardcode a host DOM id or assume a
|
|
20
|
+
specific scroll container, and says the scroll container/element
|
|
21
|
+
positions "MUST reach it through the editor-host seam (or be owned by the
|
|
22
|
+
package)". `AnchorScrollPlugin` — an existing plugin that scrolls the
|
|
23
|
+
editor to an anchor-linked heading — already solves exactly this problem
|
|
24
|
+
package-locally: it tries the two known host ids, then walks DOM
|
|
25
|
+
ancestors for the nearest actually-scrolling one, with no
|
|
26
|
+
`EditorHostServices` involvement at all. Whether the outline widget should
|
|
27
|
+
reuse that precedent or instead earn `EditorHostServices` a new
|
|
28
|
+
scroll-container/element-rect member was open.
|
|
29
|
+
|
|
30
|
+
2. `<Editor>` renders a fixed internal plugin list with no slot a consumer
|
|
31
|
+
can render into, and FR-008 requires the widget's placement to be entirely
|
|
32
|
+
the consumer's decision — so the rendered list cannot simply be "a child
|
|
33
|
+
of `<Editor>`" the way `AnchorScrollPlugin` is. It has to be a separate,
|
|
34
|
+
freely-placed component, while the data it renders and the scroll-to
|
|
35
|
+
action it triggers live inside the Lexical tree `<Editor>` owns. How those
|
|
36
|
+
two pieces communicate — a component mounted inside `LexicalComposer`
|
|
37
|
+
feeding data out, and a component mounted anywhere reading it back in —
|
|
38
|
+
was undecided. The package's one existing precedent for "read the live
|
|
39
|
+
editor from outside `<Editor>`", `annotationEditorHandleRef`, is a
|
|
40
|
+
`MutableRefObject` populated after mount and read imperatively later. That
|
|
41
|
+
doesn't fit here: the outline component has to *reactively* re-render on
|
|
42
|
+
every scroll and edit, and mutating a ref's `.current` does not notify a
|
|
43
|
+
sibling component holding the same ref.
|
|
44
|
+
|
|
45
|
+
## Decision
|
|
46
|
+
|
|
47
|
+
**No new `EditorHostServices` member.** The outline widget's scroll-spy and
|
|
48
|
+
scroll-to-heading logic reuse `AnchorScrollPlugin`'s container-discovery
|
|
49
|
+
logic directly — extracted, as a pure refactor, into a shared
|
|
50
|
+
`src/app/editor/scrollContainer.ts` (`scrollContainerFor()`,
|
|
51
|
+
`scrollElementIntoView()`) rather than duplicated. FR-006's own text permits
|
|
52
|
+
this explicitly ("... or be owned by the package"), and the only named
|
|
53
|
+
beneficiary of a shared host-seam capability for scroll-container/element-rect
|
|
54
|
+
data — Zusammen issue #126, margin comments aligned to text — is not yet
|
|
55
|
+
planned. Building a generalized host-seam primitive now, for a consumer that
|
|
56
|
+
doesn't exist yet, would be speculative and untested; `scrollContainer.ts` is
|
|
57
|
+
deliberately factored out so that whenever #126 does start, it has a single,
|
|
58
|
+
already-proven extraction point to build from instead of a second
|
|
59
|
+
copy-pasted implementation.
|
|
60
|
+
|
|
61
|
+
**A shared controller object connects `<Editor>` to the outline component,
|
|
62
|
+
not a ref.** `createDocumentOutlineHandle()` returns a `DocumentOutlineHandle`
|
|
63
|
+
— a React external store (`subscribe`/`getSnapshot`, consumed with
|
|
64
|
+
`useSyncExternalStore`) plus an imperative `scrollToHeading(index)`. The
|
|
65
|
+
consumer creates one per editor instance and passes the *same* object to both
|
|
66
|
+
`<Editor documentOutlineHandle={handle}>` and
|
|
67
|
+
`<DocumentOutline handle={handle}>`. Internally, an `OutlinePlugin` — mounted
|
|
68
|
+
inside `<Editor>`'s `LexicalComposer` tree only when `documentOutlineHandle`
|
|
69
|
+
is supplied, so an outline-less consumer pays nothing for it — walks
|
|
70
|
+
`HeadingNode`s on every `registerUpdateListener` firing to populate entries,
|
|
71
|
+
runs an rAF-throttled scroll-spy scoped to `editor.getRootElement()`'s
|
|
72
|
+
`.editor-heading-hN` elements, and implements `scrollToHeading` by reusing
|
|
73
|
+
`scrollElementIntoView`. `<DocumentOutline>` never talks to Lexical directly;
|
|
74
|
+
it only reads the handle.
|
|
75
|
+
|
|
76
|
+
Because the handle is created per call site rather than shared globally, an
|
|
77
|
+
outline for one editor instance can never aggregate headings from a second,
|
|
78
|
+
simultaneously-mounted editor — the correctness gap `EditorColumn`'s current
|
|
79
|
+
`document.querySelectorAll`-based implementation has today, closed as a
|
|
80
|
+
side effect of this shape rather than as a separate fix.
|
|
81
|
+
|
|
82
|
+
Entries are derived from **live Lexical state**
|
|
83
|
+
(`HeadingNode.getTextContent()`), not by re-parsing the markdown string:
|
|
84
|
+
`getTextContent()` already reduces inline content — including inline code —
|
|
85
|
+
to plain text, matching FR-002 without a second parse pass, and the same
|
|
86
|
+
update listener that populates entries already satisfies FR-005 (the
|
|
87
|
+
outline stays current) with no extra staleness tracking.
|
|
88
|
+
|
|
89
|
+
## Consequences
|
|
90
|
+
|
|
91
|
+
**Good:**
|
|
92
|
+
|
|
93
|
+
- FR-006 is satisfied with zero new, untested API surface. `AnchorScrollPlugin`
|
|
94
|
+
already demonstrated the DOM-ancestor-walk approach works and is exercised
|
|
95
|
+
by `host-plugins.test.tsx`; the outline widget inherits that coverage
|
|
96
|
+
rather than duplicating the risk with a second implementation.
|
|
97
|
+
- `scrollContainer.ts` is a real extraction, not a footnote: both
|
|
98
|
+
`AnchorScrollPlugin` and `OutlinePlugin` call the same functions, so a fix
|
|
99
|
+
to the container-discovery heuristic fixes both at once, and Zusammen #126
|
|
100
|
+
has a concrete starting point instead of a green-field design.
|
|
101
|
+
- The handle-sharing pattern gives `<DocumentOutline>` true placement freedom
|
|
102
|
+
(FR-008) — it can render anywhere, including outside `<Editor>`'s own
|
|
103
|
+
subtree — while still reacting live to scroll and edits, which a
|
|
104
|
+
`MutableRefObject` cannot do for a sibling reader.
|
|
105
|
+
- Multi-editor correctness (no cross-instance heading aggregation) falls out
|
|
106
|
+
of the design for free, rather than requiring a scoping fix bolted on
|
|
107
|
+
afterward.
|
|
108
|
+
|
|
109
|
+
**Bad / accepted:**
|
|
110
|
+
|
|
111
|
+
- The DOM-ancestor-walk heuristic — "nearest ancestor with `overflow-y:
|
|
112
|
+
auto|scroll` and real overflow" — is inherited, not newly assessed; an
|
|
113
|
+
unusual host layout could still pick the wrong container. This was already
|
|
114
|
+
an accepted risk for `AnchorScrollPlugin` before this issue.
|
|
115
|
+
- The handle-sharing shape (`documentOutlineHandle` prop shared between
|
|
116
|
+
`<Editor>` and a sibling component) is new API surface with no prior
|
|
117
|
+
precedent in this package — annotations use a ref instead — so it is the
|
|
118
|
+
first place a consumer has to learn this particular pattern.
|
|
119
|
+
|
|
120
|
+
**Neutral:**
|
|
121
|
+
|
|
122
|
+
- Extending `EditorHostServices` for scroll-container/element-rect data was
|
|
123
|
+
considered and explicitly deferred, not rejected outright — see Zusammen
|
|
124
|
+
#126. If and when that issue lands, `scrollContainer.ts`'s functions are
|
|
125
|
+
the natural seed for whatever host-seam capability it needs, rather than a
|
|
126
|
+
redesign from scratch.
|
|
127
|
+
|
|
128
|
+
## References
|
|
129
|
+
|
|
130
|
+
- Issue #69 (this decision)
|
|
131
|
+
- `src/app/editor/AnchorScrollPlugin.tsx` / `src/app/editor/scrollContainer.ts` — the extracted, shared container-discovery logic
|
|
132
|
+
- `src/app/editor/documentOutlineHandle.ts` — the `DocumentOutlineHandle` external-store shape
|
|
133
|
+
- `src/app/editor/OutlinePlugin.tsx` — the internal plugin that feeds the handle
|
|
134
|
+
- `src/app/editor/DocumentOutline.tsx` — the exported, freely-placed component that reads it
|
|
135
|
+
- `docs/decisions/adr-075.md` — the persistence-boundary seam and "safe default for every host-service member" rule this decision stays consistent with by adding no new member
|
|
136
|
+
- Zusammen issue #126 — the not-yet-planned margin-comment-positioning capability that would want the same scroll-container/element-rect primitive, named as the deferred beneficiary above
|
|
@@ -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
|
package/docs/editor-api.md
CHANGED
|
@@ -56,9 +56,61 @@ 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
|
+
|
|
62
114
|
## The host seam
|
|
63
115
|
|
|
64
116
|
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.
|
|
3
|
+
"version": "0.2.0",
|
|
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,67 +18,43 @@
|
|
|
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
|
-
"
|
|
23
|
-
"
|
|
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.",
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
24
29
|
"exports": {
|
|
25
30
|
".": {
|
|
26
|
-
"types": "./
|
|
27
|
-
"default": "./
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"default": "./dist/index.js"
|
|
28
33
|
},
|
|
29
34
|
"./markdown": {
|
|
30
|
-
"types": "./
|
|
31
|
-
"default": "./
|
|
35
|
+
"types": "./dist/markdown.d.ts",
|
|
36
|
+
"default": "./dist/markdown.js"
|
|
32
37
|
},
|
|
33
38
|
"./annotations": {
|
|
34
|
-
"types": "./
|
|
35
|
-
"default": "./
|
|
39
|
+
"types": "./dist/annotations.d.ts",
|
|
40
|
+
"default": "./dist/annotations.js"
|
|
36
41
|
},
|
|
37
42
|
"./headless": {
|
|
38
|
-
"types": "./
|
|
39
|
-
"default": "./
|
|
43
|
+
"types": "./dist/headless.d.ts",
|
|
44
|
+
"default": "./dist/headless.js"
|
|
40
45
|
},
|
|
41
46
|
"./contract": {
|
|
42
|
-
"types": "./
|
|
43
|
-
"default": "./
|
|
47
|
+
"types": "./dist/contract.d.ts",
|
|
48
|
+
"default": "./dist/contract.js"
|
|
44
49
|
},
|
|
45
50
|
"./nodes": {
|
|
46
|
-
"types": "./
|
|
47
|
-
"default": "./
|
|
51
|
+
"types": "./dist/nodes.d.ts",
|
|
52
|
+
"default": "./dist/nodes.js"
|
|
48
53
|
},
|
|
49
|
-
"./styles.css": "./
|
|
54
|
+
"./styles.css": "./dist/styles.css"
|
|
50
55
|
},
|
|
51
56
|
"publishConfig": {
|
|
52
|
-
"access": "public"
|
|
53
|
-
"main": "./dist/index.js",
|
|
54
|
-
"types": "./dist/index.d.ts",
|
|
55
|
-
"exports": {
|
|
56
|
-
".": {
|
|
57
|
-
"types": "./dist/index.d.ts",
|
|
58
|
-
"default": "./dist/index.js"
|
|
59
|
-
},
|
|
60
|
-
"./markdown": {
|
|
61
|
-
"types": "./dist/markdown.d.ts",
|
|
62
|
-
"default": "./dist/markdown.js"
|
|
63
|
-
},
|
|
64
|
-
"./annotations": {
|
|
65
|
-
"types": "./dist/annotations.d.ts",
|
|
66
|
-
"default": "./dist/annotations.js"
|
|
67
|
-
},
|
|
68
|
-
"./headless": {
|
|
69
|
-
"types": "./dist/headless.d.ts",
|
|
70
|
-
"default": "./dist/headless.js"
|
|
71
|
-
},
|
|
72
|
-
"./contract": {
|
|
73
|
-
"types": "./dist/contract.d.ts",
|
|
74
|
-
"default": "./dist/contract.js"
|
|
75
|
-
},
|
|
76
|
-
"./nodes": {
|
|
77
|
-
"types": "./dist/nodes.d.ts",
|
|
78
|
-
"default": "./dist/nodes.js"
|
|
79
|
-
},
|
|
80
|
-
"./styles.css": "./dist/styles.css"
|
|
81
|
-
}
|
|
57
|
+
"access": "public"
|
|
82
58
|
},
|
|
83
59
|
"files": [
|
|
84
60
|
"dist",
|
|
@@ -98,6 +74,7 @@
|
|
|
98
74
|
"test:watch": "vitest",
|
|
99
75
|
"test:coverage": "vitest run --coverage",
|
|
100
76
|
"verify:package": "node scripts/verify-package.mjs",
|
|
77
|
+
"docs:theming": "node scripts/generate-theming-docs.mjs",
|
|
101
78
|
"demo": "node scripts/run-demo.mjs",
|
|
102
79
|
"build:examples": "node scripts/build-examples.mjs",
|
|
103
80
|
"build:site": "node scripts/build-site.mjs"
|