@liminis/editor 0.2.1 → 0.3.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.
@@ -0,0 +1,152 @@
1
+ # ADR-92: The Lexical Peer Range Bumps Forward by a Narrow Single Caret, Never a Wide Band
2
+
3
+ **Date:** 2026-08-20
4
+ **Status:** Accepted
5
+ **Supersedes:** none
6
+ **Amends:** none
7
+ **Issue:** #92 (verveguy/liminis-editor)
8
+
9
+ ## Context
10
+
11
+ Every published version of `@liminis/editor`, including `0.2.2`, declared its
12
+ Lexical peer range as `^0.44.0` across all twelve Lexical family packages
13
+ (`lexical` and eleven `@lexical/*` scoped packages). Lexical itself moved on
14
+ to `0.49.0`. Because Lexical is a `0.x` package, `^0.44.0` resolves under
15
+ semver to `>=0.44.0 <0.45.0` — it admits patch releases of 0.44 only, and
16
+ excludes every 0.45+ release regardless of whether that particular release
17
+ was actually breaking. The practical effect: this package pinned every host
18
+ application to a Lexical line five minors behind, with no published version
19
+ offering a path forward.
20
+
21
+ This surfaced while refreshing `liminis-app`'s dependencies
22
+ (verveguy/liminis#1026): the Lexical family was bumped 0.44.0 → 0.49.0 and
23
+ tested in isolation there. It passed `liminis-app`'s own typecheck, lint,
24
+ build, and 3275-test suite — and was still wrong, because `liminis-app` would
25
+ have shipped in violation of this package's declared peer contract. That
26
+ app's suite does not exercise the editor deeply enough for a green run to be
27
+ evidence of Lexical compatibility; the only suite that can answer that
28
+ question is this package's own.
29
+
30
+ The peer-range shape was an open question, not a foregone conclusion: widen
31
+ the range to admit 0.44 through 0.49 (matching the issue's own suggested
32
+ example), or narrow it to `^0.49.0` and drop 0.44–0.48 support outright.
33
+
34
+ ## Decision
35
+
36
+ **The twelve Lexical peer ranges move to `^0.49.0` — a narrow single-caret
37
+ bump, not a wide multi-version band.** This is a deliberate, permanent
38
+ policy choice about how this package treats Lexical peer ranges going
39
+ forward, not a one-time bump: every future Lexical minor this package adopts
40
+ gets the same treatment — the range moves forward by exactly one caret,
41
+ paired with running the full suite against the new version, rather than
42
+ widening to claim a span of untested minors as compatible.
43
+
44
+ ### Why narrow, not wide
45
+
46
+ **Widening a peer range later is patch-safe; narrowing one is breaking.**
47
+ Starting narrow keeps both options open — if a wide band is ever wanted, it
48
+ can be added without breaking anyone. Starting wide commits the package to a
49
+ claim it can only walk back by breaking whoever relied on the wide end.
50
+
51
+ **This package's existing range philosophy for `react` does not transfer to
52
+ Lexical.** The `//peerDependencies` manifest comment already documents that
53
+ ranges are "the compatibility contract, not the tested matrix," and that
54
+ narrowing `react`'s range to match its `devDependency` version "would only
55
+ reject working consumers." That reasoning is sound for React specifically
56
+ *because* React's untested span sits inside a **stable major** — a
57
+ regression there is a bug in React, not a permitted outcome of its
58
+ versioning scheme. Lexical is `0.x`, where semver explicitly permits every
59
+ minor release to be breaking. A range like `>=0.44.0 <0.50.0` would assert
60
+ compatibility with six minors, five of them never run against this
61
+ package's own suite — a materially different, and unjustified, claim.
62
+
63
+ **No known consumer wants Lexical 0.44.** Both known consumers of this
64
+ package (`liminis-app` and the package's own test/build pipeline) are moving
65
+ *toward* 0.49, not sitting on 0.44 and wanting to stay there. The wide
66
+ band's benefit — "don't reject a working consumer on an old version" — is
67
+ close to hypothetical here, while its cost — asserting untested compatibility
68
+ across four intermediate minors known to contain breaking changes (see
69
+ Consequences) — is real.
70
+
71
+ A wide band remains available later, but only backed by CI matrix testing
72
+ across the band's endpoints — that is separate work, out of scope for this
73
+ decision.
74
+
75
+ ### What was verified before this decision was acted on
76
+
77
+ The twelve packages were bumped to `0.49.0` in both `peerDependencies` and
78
+ `devDependencies` (kept in lockstep, per this package's existing pairing
79
+ between the tested version and the declared floor), and the full
80
+ verification chain — `pnpm typecheck`, `pnpm lint`, `pnpm build`,
81
+ `pnpm test` (1591 tests), and `pnpm verify:package` (the packed-tarball
82
+ install-and-typecheck check against a fresh consumer) — ran clean with **no
83
+ source changes required**, despite Lexical's own changelog listing several
84
+ breaking changes in the 0.45–0.49 window (a `config()` protocol rework for
85
+ node classes in 0.49.0; `insertNodes` linebreak-handling changes and a
86
+ deprecated-export removal in 0.46.0; selection/backspace/reconcile changes
87
+ in 0.45.0). This package's sixteen custom Lexical node classes all define
88
+ explicit `getType`/`clone`/`importJSON` statics rather than relying on
89
+ auto-synthesis, which structurally avoided the highest-risk part of the
90
+ 0.49.0 change; the rest of the changelog's items apparently do not intersect
91
+ this package's actual usage patterns, since the suite exercises them and
92
+ passed unmodified.
93
+
94
+ ## Consequences
95
+
96
+ **Positive:**
97
+
98
+ - Consumers can now install this package alongside the current Lexical
99
+ release; the multi-minor stall the issue described cannot recur until
100
+ Lexical ships another minor this package hasn't yet adopted.
101
+ - The Lexical peer range's compatibility claim is now backed by an actual CI
102
+ run against the exact version it names — peer and devDependency are both
103
+ `0.49.0`, so no untested span exists in it. (React's own range still has
104
+ an intentionally untested lower half — peer `^19.2.0` vs. devDependency
105
+ `^19.2.5` — per the `//peerDependencies` note; that is unchanged by this
106
+ decision and follows the opposite, stable-major reasoning explained
107
+ above.)
108
+ - The policy is written down (`//lexicalPeerPolicy` in `package.json`, this
109
+ ADR, and the README's install section), so the next Lexical minor is a
110
+ known, bounded task rather than a rediscovery of this issue's reasoning.
111
+
112
+ **Negative:**
113
+
114
+ - **This is a breaking change.** `^0.44.0` and `^0.49.0` are non-overlapping
115
+ ranges; any consumer still on Lexical 0.44.0–0.48.x will see an
116
+ unmet-peer-dependency warning on installing the next release and must
117
+ upgrade its Lexical family to 0.49.0 first. That is the intended effect of
118
+ narrowing, not a regression.
119
+ - **The stall this issue describes will recur at every future Lexical
120
+ minor**, by design: this package's range never gets ahead of what it has
121
+ actually tested. The mitigation is that each recurrence is now a known,
122
+ bounded task (bump twelve versions, run the suite, adjust the range) with
123
+ a written rationale, rather than an accumulating five-minor gap discovered
124
+ incidentally by a downstream consumer's dependency refresh.
125
+
126
+ **Neutral:**
127
+
128
+ - `react`/`react-dom` peer ranges and all non-Lexical dependencies are
129
+ unaffected; this decision only concerns the Lexical family, and does not
130
+ reopen or alter the existing React range philosophy.
131
+ - No Lexical-API-floor guard test (the kind `tests/package-manifest-contract.test.ts`
132
+ already has for React) was added. That guard exists for React because its
133
+ range makes an untested-lower-half claim that needs policing; the narrow
134
+ Lexical range makes no such claim, so there is nothing analogous to guard
135
+ against.
136
+
137
+ ## References
138
+
139
+ - Issue #92 (this decision), verveguy/liminis#1026 (the originating
140
+ `liminis-app` dependency refresh that surfaced the stall)
141
+ - `package.json` — the `//peerDependencies` and `//lexicalPeerPolicy`
142
+ manifest comments; the twelve `peerDependencies`/`devDependencies` entries
143
+ - `README.md` — "Install" section, the Lexical `pnpm add` block and the
144
+ peer-range policy callout
145
+ - `CHANGELOG.md` — the `## Unreleased` / `### Breaking changes` entry
146
+ recording this bump
147
+ - `docs/decisions/adr-078.md`, `docs/decisions/adr-079.md` — prior ADRs on
148
+ consumer-facing package surface, cited as precedent for treating this as
149
+ ADR-worthy
150
+ - Related: #78 (a required member made 0.2.0 type-level breaking), #79
151
+ (token definitions documented as public API) — prior instances of
152
+ consumer-facing surface needing explicit treatment
@@ -0,0 +1,288 @@
1
+ # ADR-93: `--liminis-editor-*` Names Are Defined as Aliases of Their Legacy Names
2
+
3
+ **Date:** 2026-08-20
4
+ **Status:** Accepted
5
+ **Supersedes:** none
6
+ **Amends:** none
7
+ **Issue:** #93 (verveguy/liminis-editor)
8
+
9
+ ## Context
10
+
11
+ ADR-087 (#51) renamed every *consumption* site to `--liminis-editor-*`,
12
+ deliberately leaving every *definition* site (`:root`, `.dark`, the
13
+ `@media print` block) declaring the old, pre-`0.2.0` names. ADR-092 (#79)
14
+ built a checked-in baseline guarding those definitions, and explicitly
15
+ deferred a third item: whether the package should ever *define*
16
+ `--liminis-editor-*` names directly, with the legacy names kept as an
17
+ alias layer underneath.
18
+
19
+ That gap is not theoretical. Zusammen, a downstream host, reads this
20
+ package's definitions directly — `var(--vscode-background)`,
21
+ `var(--vscode-border)`, etc. — mapping its own design-system tokens onto
22
+ them rather than overriding them
23
+ ([verveguy/zusammen#129](https://github.com/verveguy/zusammen/issues/129)).
24
+ It wants to migrate those reads onto `--liminis-editor-*` instead, but
25
+ before this change there was nothing to migrate to: every row in the
26
+ README's token table read "Has a default: No (inline fallback only)."
27
+
28
+ **The naive fix is the trap ADR-087 already reasoned through once, now on
29
+ the definition side.** Adding `--liminis-editor-foreground: <default>;` to
30
+ the package's own `:root` unconditionally would resolve every one of the
31
+ 250 consumption chains (`var(--liminis-editor-foreground, var(--vscode-foreground))`)
32
+ at the new name before ever reaching the old one — silently disabling the
33
+ ADR-087 fallback layer. A host like `liminis-app`, which supplies only
34
+ `--color-primary`/`--color-muted-foreground` (its own Tailwind `@theme`
35
+ tokens, coincidentally the fallback names `C4Component.tsx` already uses),
36
+ would stop tracking its own overrides with no error and no failing test —
37
+ exactly the failure mode ADR-092 exists to guard against.
38
+
39
+ Two further, non-obvious problems surfaced during implementation, not
40
+ anticipated by the plan:
41
+
42
+ 1. **A definition-side alias's own `var()` reference is textually
43
+ indistinguishable from a consumption site.** `scripts/lib/theming-tokens.mjs`'s
44
+ `consumptionSitesIn()` scans raw text for depth-0 `var(--x` occurrences
45
+ with no awareness of whether the enclosing statement is a custom-property
46
+ *declaration* (`--liminis-editor-foreground: var(--vscode-foreground);`)
47
+ or an ordinary property/JS-style value (`color: var(--liminis-editor-foreground, ...)`).
48
+ Adding the alias block as originally planned would have made every one
49
+ of the ~40 flat-aliased legacy names register as a newly "consumed"
50
+ token, growing the README table and `TOKEN_DESCRIPTIONS` maintenance
51
+ surface for tokens no host-facing code actually reads — a regression
52
+ against this issue's own explicit Out of Scope ("does not add or remove
53
+ rows" from the consumed/documented table).
54
+ 2. **A CSS block comment containing a literal `*/` substring inside its
55
+ text terminates early.** An early draft of the new `:root` block's
56
+ header comment read `--vscode-*/--slashmd-*/--checkbox-*` — the `*/`
57
+ embedded in `--vscode-*/--slashmd-*` closed the comment there, both for
58
+ real CSS parsers and for this repo's own `stripCssComments()`, leaving
59
+ the remainder of the intended comment as live (invalid) CSS text. This
60
+ would have shipped a real authoring bug, not just a test artifact.
61
+
62
+ ## Decision
63
+
64
+ **Legacy names keep carrying the real defaults. Each `--liminis-editor-*`
65
+ name becomes a new declaration that reads its legacy name via `var()`** —
66
+ `--liminis-editor-foreground: var(--vscode-foreground);` — added in a new
67
+ `:root`/`.dark` block placed immediately after the package's existing
68
+ `:root`/`.dark` blocks in `src/styles.css`. This is the direction the
69
+ spec's Background illustrated and Research confirmed by inspecting this
70
+ file's cascade structure directly: a CSS custom property resolves against
71
+ the *element's own* cascade at the point of use, not at declaration time,
72
+ so a single `:root`-level alias re-resolves correctly through `.dark`'s and
73
+ the `@media print` block's overrides of the *legacy* name it points to —
74
+ no per-block alias duplication is needed, and neither `.dark` nor the print
75
+ block needed to change for the ~40 tokens with a real existing declaration.
76
+
77
+ **All 59 renamed tokens (`PREVIOUS_NAME`'s full set) get a real declaration
78
+ in this pass, not only the two the originating issue named.** FR-001 says
79
+ "every token it currently consumes"; inspection of every consumption site
80
+ found 19 tokens with no existing `:root`/`.dark` legacy declaration to
81
+ alias to, not 2, split into three groups:
82
+
83
+ - **2 tokens** (`--vscode-input-bg`, `--vscode-foreground-muted`) resolve
84
+ today through an existing *nested* fallback at their own consumption
85
+ site. Their alias preserves that chain rather than flattening it:
86
+ `--liminis-editor-input-bg: var(--vscode-input-bg, var(--vscode-code-bg));`.
87
+ - **13 tokens** have no CSS custom property backing anywhere — only a
88
+ literal inline fallback at each consumption site (e.g.
89
+ `var(--vscode-focus-border, #007acc)`). Their alias reproduces that
90
+ literal as the alias's own fallback:
91
+ `--liminis-editor-focus-border: var(--vscode-focus-border, #007acc);` —
92
+ a host supplying nothing sees the same literal as before (FR-002); a
93
+ host supplying the legacy name still wins (FR-003). 6 of these 13 vary by
94
+ theme via a JS `isDark`/`dark` ternary at their consumption sites
95
+ (`button-background`, all 5 `menu-*` tokens); those 6 get a matching
96
+ `.dark` override in the new block, mirroring the same light/dark split
97
+ their consumption sites already express in JS.
98
+ - **4 tokens** (`--color-primary`, `--color-primary-100`,
99
+ `--color-muted-foreground`, `--color-muted-100`) are `liminis-app`'s own
100
+ Tailwind `@theme` brand tokens, for which the package only ever supplied
101
+ a literal final fallback, never asserted ownership. They are aliased
102
+ identically to the 13 above — the mechanism doesn't newly assert
103
+ ownership, it makes the existing fallback literal reachable under the new
104
+ name too.
105
+
106
+ **Two of the 17 no-backing tokens have a real cross-site literal
107
+ inconsistency, discovered by direct inspection (one — `--vscode-errorForeground`
108
+ — was already flagged by Specify; the second — `--vscode-toolbar-hoverBackground`
109
+ — was not):**
110
+
111
+ | Token | Sites | Canonical value chosen |
112
+ |---|---|---|
113
+ | `--vscode-errorForeground` | `#f14c4c` (2 sites: `.editor-link-broken`, `:hover`) vs. `#f44336` (2 sites: `.block-delete-button:hover .block-delete-icon`, `.search-close-button:hover`) | `#f14c4c` — tie broken toward the more semantically central "broken link" indicator over two hover-only chrome details |
114
+ | `--vscode-toolbar-hoverBackground` | `rgba(128, 128, 128, 0.15)` (2 sites) vs. `rgba(128, 128, 128, 0.1)` (1 site, `.drag-handle:hover .drag-handle-icon`) | `rgba(128, 128, 128, 0.15)` — majority (2 of 3) |
115
+
116
+ Once a token has a single real definition, it necessarily wins over every
117
+ site's own inline fallback (fallback only applies when the property is
118
+ genuinely unset) — there is no mechanism that both defines the token
119
+ (FR-001) and preserves two different resolved values at different sites.
120
+ **This unifies 3 sites' rendered color to the canonical value, a small,
121
+ real, intentional visual change**, called out here per this repository's
122
+ convention of recording what a decision costs. The three affected call
123
+ sites (`src/styles.css:1148`, `:1188`, `:1704`) each carry a one-line
124
+ comment noting they are superseded by the new canonical default. The
125
+ spec's Out of Scope section explicitly permits this as a side effect of
126
+ choosing FR-001's value, not as a design goal in its own right.
127
+
128
+ **Consumption sites are left untouched.** All 224 `styles.css` sites and 26
129
+ `.tsx` sites keep their existing `var(--liminis-editor-x, var(--old-name-x, ...))`
130
+ chains. Simplifying them to a single `var(--liminis-editor-x)` call was
131
+ identified as optional cleanup (spec Assumptions) but would strip every
132
+ renamed token's `immediateFallback`, breaking `resolvesToPreviousName()`
133
+ (FR-011's existing drift guard) for all 59 tokens unless that guard were
134
+ also redesigned to check the *definition* site's alias target instead —
135
+ a materially larger, coordinated change deferred out of this issue's scope.
136
+ The now-redundant nested legacy fallback at each site is provably dead code
137
+ once its definition-side alias exists, left in place rather than removed.
138
+
139
+ **`scripts/lib/theming-tokens.mjs`'s `consumptionSitesIn()` gains a
140
+ declaration-value check.** A new helper,
141
+ `isCustomPropertyDeclarationValue(text, varStartIndex)`, walks back from a
142
+ depth-0 `var(` to the nearest statement boundary (`;`, `{`, or `}`) and
143
+ tests whether everything between that boundary and the `var(` is exactly
144
+ `--name:` — the shape only a custom-property declaration's value position
145
+ can have (an ordinary CSS property name never starts with `--`). Each
146
+ site now carries an `isDeclarationValue` flag; `consumedTokens()`'s Pass 1
147
+ skips depth-0 sites where it is true. This keeps the "consumed" inventory —
148
+ and therefore the README table's row set and the `TOKEN_DESCRIPTIONS`
149
+ maintenance surface — exactly as it was before this issue, satisfying its
150
+ Out of Scope constraint, while still letting `defaultedTokens()` (a
151
+ separate, unchanged function that just scans for any `--name:` declaration)
152
+ pick up all 59 new aliases as newly "defined."
153
+
154
+ **No other change to `theming-tokens.mjs` or its `.d.mts`.**
155
+ `isCustomPropertyDeclarationValue` is internal, not exported;
156
+ `defaultedTokens()` needed no change to recognize the new aliases; the
157
+ ADR-092 baseline guard, `resolvesWithoutHost()`, and `resolvesToPreviousName()`
158
+ all continue to operate over the same "consumed"/"defined" definitions they
159
+ already had, now simply seeing a larger defined set.
160
+
161
+ ## Consequences
162
+
163
+ **Good:**
164
+
165
+ - A host can now read `--liminis-editor-x` directly and get a real value in
166
+ every override state — the entire point of this issue and the
167
+ prerequisite for zusammen#129's read migration (FR-001, SC-001, SC-006).
168
+ - A host supplying only a legacy name **at `:root`/`document.documentElement`**
169
+ is unaffected: the alias's `var()` re-resolves through that name's own
170
+ `.dark`/`@media print` overrides at the point of use, verified directly
171
+ against this file's actual cascade structure rather than assumed from the
172
+ general CSS-custom-property theory alone (FR-002, FR-003, User Story 2).
173
+ This is the standard integration point and the one this package's own
174
+ e2e coverage exercises. It does not extend to a legacy override scoped to
175
+ an arbitrary descendant element — see Bad/accepted below.
176
+ - The ADR-092 baseline guard now protects all 59 new alias declarations the
177
+ same way it already protected the legacy ones — a future accidental
178
+ deletion of an alias fails `pnpm test`, naming the token (FR-005, US4).
179
+ - The declaration-value fix to `consumptionSitesIn()` is general: it holds
180
+ for any future alias declaration of this shape without further
181
+ special-casing, not just the 59 added here.
182
+ - The premature-comment-close bug was caught by the same regenerated test
183
+ suite that caught the consumption-site pollution, before either shipped —
184
+ the CI guards this issue extends are what surfaced both problems.
185
+
186
+ **Bad / accepted:**
187
+
188
+ - **The `:root`-only alias does not track a legacy-name override scoped to
189
+ a descendant element below `:root`** (e.g. a per-widget wrapper div a
190
+ host applies its own theme vars to, rather than `document.documentElement`
191
+ itself) — found during PR review (CodeRabbit), confirmed empirically in
192
+ a running Electron app, not merely reasoned about. A custom property
193
+ inherits its parent's already-*computed* value, not the `var()`
194
+ expression that produced it, so the alias's `var(--legacy-x)` is fixed
195
+ using `:root`'s own value of `--legacy-x` and does not re-evaluate for a
196
+ change made further down the tree. Two alternatives were tried and
197
+ rejected, each empirically, in a running app:
198
+ - **Re-declaring the alias on every element (`*`)** does fix the
199
+ descendant case, but then an element's own `*`-matched declaration
200
+ shadows whatever `--liminis-editor-x` value it would otherwise have
201
+ inherited from an ancestor — including a host's own direct
202
+ `--liminis-editor-x` override set on that ancestor. This broke User
203
+ Story 3 (a directly-set `--liminis-editor-*` name must win), which is
204
+ higher-confidence/already-tested behavior — confirmed by a real,
205
+ reproducible test failure, not a theoretical concern.
206
+ - **Scoping the redeclaration to the package's own root wrapper class**
207
+ (`.editor-app-root`) reduces but does not eliminate that same
208
+ shadowing (still breaks a `--liminis-editor-*` override set above
209
+ `.editor-app-root`), and is additionally unreliable on its own terms:
210
+ not every integration renders that wrapper — this repo's own
211
+ `examples/electron` shell mounts a lower-level component without it,
212
+ so the alias would go undeclared there, confirmed by inspecting the
213
+ actual DOM in a running instance.
214
+ - No mechanism was found that recomputes a legacy-name change made
215
+ anywhere below `:root` (User Story 2) while also respecting a
216
+ `--liminis-editor-*` override set on any ancestor of the read element
217
+ (User Story 3) — CSS custom-property inheritance passes down an
218
+ already-resolved value, not a live formula, so a property can prefer
219
+ "inherit if my ancestor set one" or "always recompute from a
220
+ dependency," but not both conditionally per-element without a
221
+ mechanism this package doesn't have (e.g. `@property` plus JS
222
+ reconciliation, well beyond this issue's scope). **The `:root`-only
223
+ design is kept, and this scope limitation is accepted and documented**
224
+ here and in `src/styles.css`'s alias-block comment, per this
225
+ repository's convention of recording what a decision costs rather than
226
+ silently narrowing FR-002/FR-003's stated guarantee. Extending it to
227
+ arbitrary DOM scope, if a specific host needs it, is deferred to a
228
+ follow-up issue — it is a materially larger design problem than this
229
+ ADR anticipated.
230
+ - Three consumption sites (`--vscode-errorForeground` at 2 sites,
231
+ `--vscode-toolbar-hoverBackground` at 1 site) render a different,
232
+ intentionally-unified color than before this change. A host relying on
233
+ the specific pre-existing `#f44336`/`0.1`-alpha rendering at exactly
234
+ those sites will see a small visual shift. This is an unavoidable
235
+ consequence of giving these two tokens a single real definition (FR-001)
236
+ when their own consumption sites already disagreed with each other —
237
+ there is no mechanism that satisfies FR-001 without picking one value.
238
+ - The checked-in baseline grows from 41 to 100 entries in one change. Per
239
+ ADR-092's own design the guard is one-directional (`missing` fails,
240
+ `added` never does), so this is a mechanical `pnpm docs:theming-baseline`
241
+ diff, not a CI risk — but it is a large diff for a human reviewer to
242
+ read line-by-line.
243
+ - Every one of the 250 existing consumption sites keeps a nested legacy
244
+ fallback that is now provably redundant dead code (the definition-side
245
+ alias means the fallback branch can never be reached in practice). Removing
246
+ it was explicitly deferred (see Decision) rather than done here, so this
247
+ verbosity persists in `styles.css` and the five `.tsx` files until a
248
+ future change picks it up.
249
+ - `consumptionSitesIn()`'s declaration-value detection is a text-boundary
250
+ heuristic (nearest `;`/`{`/`}` before the `var(`), not a real CSS parser.
251
+ It is correct for every declaration in this codebase today (all
252
+ single-line), but a future multi-line custom-property declaration split
253
+ across a statement boundary could evade it.
254
+
255
+ **Neutral:**
256
+
257
+ - The `@media print` block needed no changes: its `:root, .dark { ... }`
258
+ selector already overrides every legacy name it touches, and each new
259
+ alias's `var()` reference re-resolves through that override the same way
260
+ it re-resolves through `.dark`'s.
261
+ - `liminis-app`'s own `--color-primary`/`--color-muted-foreground` overrides
262
+ are unaffected either way — the 4 brand-token aliases only make their
263
+ existing fallback literal reachable under the new name too, without
264
+ changing what either name resolves to when the host supplies its usual
265
+ overrides (SC-003).
266
+
267
+ ## References
268
+
269
+ - Issue #93 (this decision); Issue #79 / ADR-092's "Not decided here" —
270
+ this decision's direct origin (Item 3)
271
+ - `docs/decisions/adr-087.md` — the consumption-side rename and fallback
272
+ design this decision extends to the definition side, and the trap
273
+ ("that alternative is broken...") this decision's alias direction avoids
274
+ repeating on the definition side
275
+ - `docs/decisions/adr-092.md` — the checked-in baseline guard this
276
+ decision's new declarations are added to
277
+ - `src/styles.css` — the new `:root`/`.dark` alias block (after the
278
+ existing `.dark` block); the three superseded-literal comments at the
279
+ drag-handle-hover, block-delete-hover, and search-close-hover sites
280
+ - `scripts/lib/theming-tokens.mjs` — `isCustomPropertyDeclarationValue()`,
281
+ `consumptionSitesIn()`'s new `isDeclarationValue` field, and
282
+ `consumedTokens()`'s Pass 1 filter
283
+ - `scripts/lib/theming-defined-tokens-baseline.json` — regenerated via
284
+ `pnpm docs:theming-baseline`, 41 → 100 entries
285
+ - `README.md`, "Theming: CSS custom properties" — regenerated table and
286
+ hand-rewritten prose describing the alias mechanism
287
+ - [verveguy/zusammen#129](https://github.com/verveguy/zusammen/issues/129)
288
+ — the downstream migration this decision unblocks
@@ -95,6 +95,7 @@ function MyEditorWithOutline() {
95
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
96
  | `<DocumentOutline>`'s `handle` | `DocumentOutlineHandle` | The same object passed to `<Editor>`. |
97
97
  | `<DocumentOutline>`'s `className` | `string` | Additional class on the outer `<nav>`, for your own placement/layout. |
98
+ | `<DocumentOutline>`'s `onEntrySelect` | `(entry: OutlineEntry) => void` | Optional. Fires with the clicked entry in addition to `handle.scrollToHeading`. Mainly useful on the markdown-derived path (below), where `scrollToHeading` no-ops. |
98
99
 
99
100
  `createDocumentOutlineHandle()` returns the shared controller: a React
100
101
  external store (`subscribe`/`getSnapshot`, so `<DocumentOutline>` re-renders
@@ -138,6 +139,76 @@ function MyAppWithOutline() {
138
139
  }
139
140
  ```
140
141
 
142
+ ### Raw-mode / markdown-derived entries (issue #84)
143
+
144
+ The Lexical path above requires a mounted `<Editor>`. A host with a raw
145
+ markdown mode — no `<Editor>`, no Lexical tree — can still drive the same
146
+ `<DocumentOutline>` component by feeding the handle from markdown text
147
+ directly, via `handle.publishFromMarkdown` and `handle.setActiveLine`:
148
+
149
+ ```tsx
150
+ import { useEffect, useState } from 'react'
151
+ import { DocumentOutline, createDocumentOutlineHandle } from '@liminis/editor'
152
+
153
+ function MyRawModeWithOutline({ markdown }: { markdown: string }) {
154
+ const [outlineHandle] = useState(() => createDocumentOutlineHandle())
155
+
156
+ // Re-derive entries whenever the raw markdown text changes.
157
+ useEffect(() => {
158
+ outlineHandle.publishFromMarkdown(markdown)
159
+ }, [outlineHandle, markdown])
160
+
161
+ return (
162
+ <div style={{ display: 'flex' }}>
163
+ <MyRawMarkdownEditor
164
+ markdown={markdown}
165
+ // Tell the outline which source line is at the top of your own
166
+ // (non-Lexical) editor's viewport as the reader scrolls, so it can
167
+ // resolve and highlight the active entry (FR-006).
168
+ onFirstVisibleLineChange={(line) => outlineHandle.setActiveLine(line)}
169
+ />
170
+ <aside>
171
+ <DocumentOutline
172
+ handle={outlineHandle}
173
+ // `handle.scrollToHeading` no-ops on this path (no Lexical editor
174
+ // to scroll) — use `entry.line` against your own editor instead.
175
+ onEntrySelect={(entry) => myRawEditorRef.current?.scrollToLine(entry.line)}
176
+ />
177
+ </aside>
178
+ </div>
179
+ )
180
+ }
181
+ ```
182
+
183
+ - `handle.publishFromMarkdown(markdown)` parses the markdown, derives H1–H5
184
+ entries using the same detection and text-extraction rules as the Lexical
185
+ path (order, nesting, inline formatting including inline code reduced to
186
+ plain text), and publishes them. Call it whenever your raw markdown text
187
+ changes.
188
+ - Each markdown-derived entry additionally carries a 1-based `line` —
189
+ matching mdast's `position.start.line` — since a raw-mode host has no
190
+ Lexical tree to scroll instead. `<DocumentOutline>`'s own click handling
191
+ calls `handle.scrollToHeading(entry.index)`, which is a no-op on this path
192
+ since there's no Lexical editor to scroll; pass `onEntrySelect` to receive
193
+ the clicked entry and scroll your own editor via `entry.line`, as shown
194
+ above.
195
+ - `handle.setActiveLine(line)` resolves which entry's heading line the
196
+ supplied line falls within or after (and before the next heading's line),
197
+ and publishes it as the active entry — the raw-mode equivalent of the
198
+ Lexical path's scroll-spy. Computing which line is "at the top" as the
199
+ reader scrolls is your responsibility, same as it is for a raw-mode host's
200
+ own scroll tracking; pass `null` before the first scroll for "no active
201
+ entry yet."
202
+ - One handle is fed by exactly one path at a time — the Lexical path (via
203
+ `<Editor documentOutlineHandle>`/`OutlinePlugin`) and the markdown path
204
+ (via `publishFromMarkdown`/`setActiveLine`) are independent and not
205
+ reconciled. Don't mount `<Editor documentOutlineHandle={outlineHandle}>`
206
+ and call `publishFromMarkdown` on the same handle at once.
207
+ - `deriveOutlineFromMarkdown(markdown)` and
208
+ `resolveActiveOutlineIndex(entries, line)` are also exported standalone,
209
+ for a host that wants the pure derivation without going through the
210
+ handle at all.
211
+
141
212
  ## The host seam
142
213
 
143
214
  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.2.1",
3
+ "version": "0.3.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",
@@ -75,24 +75,26 @@
75
75
  "test:coverage": "vitest run --coverage",
76
76
  "verify:package": "node scripts/verify-package.mjs",
77
77
  "docs:theming": "node scripts/generate-theming-docs.mjs",
78
+ "docs:theming-baseline": "node scripts/update-theming-baseline.mjs",
78
79
  "demo": "node scripts/run-demo.mjs",
79
80
  "build:examples": "node scripts/build-examples.mjs",
80
81
  "build:site": "node scripts/build-site.mjs"
81
82
  },
82
- "//peerDependencies": "Ranges are the compatibility contract, not the tested matrix. CI resolves one point version per range (currently react 19.2.x latest and lexical 0.44.x latest), so every caret range here has an untested lower half \u2014 narrowing `react` to the devDependency's `^19.2.5` would not change that, it would only reject working consumers. The react floor is not a claim, it is enforced: `tests/package-manifest-contract.test.ts` (\"imports no React API newer than the declared 19.2.0 peer floor\") scans every source file for post-19.0 named imports \u2014 `useEffectEvent`, `Activity`, `cacheSignal`, `ViewTransition`, `captureOwnerStack` \u2014 and fails if one appears. Reaching for a newer API therefore breaks CI until you raise the floor here too, which is the pairing this note would otherwise only assert. Raise a floor when the code actually starts needing a later API, not to mirror whatever CI happened to install.",
83
+ "//peerDependencies": "Ranges are the compatibility contract, not the tested matrix. CI resolves one point version per range (currently react 19.2.x latest and lexical 0.49.x latest), so every caret range here has an untested lower half \u2014 narrowing `react` to the devDependency's `^19.2.5` would not change that, it would only reject working consumers. The react floor is not a claim, it is enforced: `tests/package-manifest-contract.test.ts` (\"imports no React API newer than the declared 19.2.0 peer floor\") scans every source file for post-19.0 named imports \u2014 `useEffectEvent`, `Activity`, `cacheSignal`, `ViewTransition`, `captureOwnerStack` \u2014 and fails if one appears. Reaching for a newer API therefore breaks CI until you raise the floor here too, which is the pairing this note would otherwise only assert. Raise a floor when the code actually starts needing a later API, not to mirror whatever CI happened to install. This reasoning does not extend to the Lexical ranges below \u2014 see `//lexicalPeerPolicy`.",
84
+ "//lexicalPeerPolicy": "The twelve Lexical ranges are a single-caret bump per Lexical minor actually adopted and tested here, not a wide multi-version band. `react`'s untested lower half (above) is safe because a break there would be a bug in a stable major; Lexical is `0.x`, where every minor is permitted to break, so a wide band would claim compatibility with untested, potentially-breaking releases. Widening a range later is patch-safe; narrowing one is breaking \u2014 so this stays narrow by default and moves forward only when the code is actually run against the new minor. See docs/decisions/adr-92-lexical-peer-range-policy.md.",
83
85
  "peerDependencies": {
84
- "@lexical/code": "^0.44.0",
85
- "@lexical/code-prism": "^0.44.0",
86
- "@lexical/link": "^0.44.0",
87
- "@lexical/list": "^0.44.0",
88
- "@lexical/mark": "^0.44.0",
89
- "@lexical/markdown": "^0.44.0",
90
- "@lexical/react": "^0.44.0",
91
- "@lexical/rich-text": "^0.44.0",
92
- "@lexical/selection": "^0.44.0",
93
- "@lexical/table": "^0.44.0",
94
- "@lexical/utils": "^0.44.0",
95
- "lexical": "^0.44.0",
86
+ "@lexical/code": "^0.49.0",
87
+ "@lexical/code-prism": "^0.49.0",
88
+ "@lexical/link": "^0.49.0",
89
+ "@lexical/list": "^0.49.0",
90
+ "@lexical/mark": "^0.49.0",
91
+ "@lexical/markdown": "^0.49.0",
92
+ "@lexical/react": "^0.49.0",
93
+ "@lexical/rich-text": "^0.49.0",
94
+ "@lexical/selection": "^0.49.0",
95
+ "@lexical/table": "^0.49.0",
96
+ "@lexical/utils": "^0.49.0",
97
+ "lexical": "^0.49.0",
96
98
  "react": "^19.2.0",
97
99
  "react-dom": "^19.2.0"
98
100
  },
@@ -121,17 +123,17 @@
121
123
  },
122
124
  "devDependencies": {
123
125
  "@eslint/js": "^10.0.1",
124
- "@lexical/code": "^0.44.0",
125
- "@lexical/code-prism": "^0.44.0",
126
- "@lexical/link": "^0.44.0",
127
- "@lexical/list": "^0.44.0",
128
- "@lexical/mark": "^0.44.0",
129
- "@lexical/markdown": "^0.44.0",
130
- "@lexical/react": "^0.44.0",
131
- "@lexical/rich-text": "^0.44.0",
132
- "@lexical/selection": "^0.44.0",
133
- "@lexical/table": "^0.44.0",
134
- "@lexical/utils": "^0.44.0",
126
+ "@lexical/code": "^0.49.0",
127
+ "@lexical/code-prism": "^0.49.0",
128
+ "@lexical/link": "^0.49.0",
129
+ "@lexical/list": "^0.49.0",
130
+ "@lexical/mark": "^0.49.0",
131
+ "@lexical/markdown": "^0.49.0",
132
+ "@lexical/react": "^0.49.0",
133
+ "@lexical/rich-text": "^0.49.0",
134
+ "@lexical/selection": "^0.49.0",
135
+ "@lexical/table": "^0.49.0",
136
+ "@lexical/utils": "^0.49.0",
135
137
  "@testing-library/react": "^16.3.2",
136
138
  "@types/mdast": "^4.0.4",
137
139
  "@types/node": "^25.6.0",
@@ -145,7 +147,7 @@
145
147
  "eslint-plugin-react-refresh": "^0.5.2",
146
148
  "globals": "^17.5.0",
147
149
  "happy-dom": "^20.9.0",
148
- "lexical": "^0.44.0",
150
+ "lexical": "^0.49.0",
149
151
  "react": "^19.2.5",
150
152
  "react-dom": "^19.2.5",
151
153
  "tsc-alias": "^1.9.1",