@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.
- package/README.md +127 -72
- package/dist/app/editor/DocumentOutline.d.ts +11 -2
- package/dist/app/editor/DocumentOutline.js +5 -2
- package/dist/app/editor/Editor.js +17 -5
- package/dist/app/editor/documentOutlineHandle.d.ts +28 -0
- package/dist/app/editor/documentOutlineHandle.js +16 -1
- package/dist/app/editor/documentOutlineMarkdown.d.ts +35 -0
- package/dist/app/editor/documentOutlineMarkdown.js +82 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -1
- package/dist/styles.css +132 -0
- package/docs/decisions/adr-091.md +159 -0
- package/docs/decisions/adr-092.md +202 -0
- package/docs/decisions/adr-92-lexical-peer-range-policy.md +152 -0
- package/docs/decisions/adr-93-liminis-editor-defined-aliases.md +288 -0
- package/docs/editor-api.md +71 -0
- package/package.json +28 -26
|
@@ -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
|
package/docs/editor-api.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
85
|
-
"@lexical/code-prism": "^0.
|
|
86
|
-
"@lexical/link": "^0.
|
|
87
|
-
"@lexical/list": "^0.
|
|
88
|
-
"@lexical/mark": "^0.
|
|
89
|
-
"@lexical/markdown": "^0.
|
|
90
|
-
"@lexical/react": "^0.
|
|
91
|
-
"@lexical/rich-text": "^0.
|
|
92
|
-
"@lexical/selection": "^0.
|
|
93
|
-
"@lexical/table": "^0.
|
|
94
|
-
"@lexical/utils": "^0.
|
|
95
|
-
"lexical": "^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.
|
|
125
|
-
"@lexical/code-prism": "^0.
|
|
126
|
-
"@lexical/link": "^0.
|
|
127
|
-
"@lexical/list": "^0.
|
|
128
|
-
"@lexical/mark": "^0.
|
|
129
|
-
"@lexical/markdown": "^0.
|
|
130
|
-
"@lexical/react": "^0.
|
|
131
|
-
"@lexical/rich-text": "^0.
|
|
132
|
-
"@lexical/selection": "^0.
|
|
133
|
-
"@lexical/table": "^0.
|
|
134
|
-
"@lexical/utils": "^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.
|
|
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",
|