@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
package/dist/styles.css
CHANGED
|
@@ -107,6 +107,135 @@
|
|
|
107
107
|
--slashmd-callout-caution-border: oklch(0.63 0.24 27);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/* ========================================
|
|
111
|
+
PUBLIC ALIASES — --liminis-editor-* names (ADR-93)
|
|
112
|
+
|
|
113
|
+
The package's own :root/.dark declarations above keep carrying the real
|
|
114
|
+
defaults, under the legacy names, exactly as before. Every alias below
|
|
115
|
+
only reads a legacy name via var() — it never declares an independent
|
|
116
|
+
value — so a host overriding only the legacy name still wins: the alias
|
|
117
|
+
re-resolves through .dark and the @media print block's overrides of that
|
|
118
|
+
legacy name, since a var() reference resolves per-element at the point of
|
|
119
|
+
use, not at the point this declaration is parsed. This lets a host read
|
|
120
|
+
--liminis-editor-* directly (verveguy/zusammen#129) without disabling the
|
|
121
|
+
ADR-087 fallback layer that a host supplying only the legacy name relies
|
|
122
|
+
on.
|
|
123
|
+
|
|
124
|
+
Scope of the "legacy name still wins" guarantee: this re-resolution is
|
|
125
|
+
correct for a legacy override applied at (or that plainly inherits down
|
|
126
|
+
to) :root/document.documentElement — the standard integration point, and
|
|
127
|
+
the only one covered by this package's own e2e coverage. It does NOT
|
|
128
|
+
extend to a legacy override scoped to an arbitrary DESCENDANT element
|
|
129
|
+
between :root and the editor's own DOM (e.g. a per-widget wrapper div a
|
|
130
|
+
host might apply its own theme vars to): a custom property inherits its
|
|
131
|
+
parent's already-*computed* value, not the var() expression, so this
|
|
132
|
+
:root-declared alias is fixed at parse time using :root's own value and
|
|
133
|
+
won't pick up a change made lower in the tree. Re-declaring the alias on
|
|
134
|
+
every element (`*`) was tried and rejected: it fixes the descendant case
|
|
135
|
+
but then shadows a --liminis-editor-* override a host sets on an
|
|
136
|
+
ancestor of the element being read, breaking the higher-priority
|
|
137
|
+
guarantee (User Story 3) that a directly-set --liminis-editor-* name
|
|
138
|
+
always wins — confirmed by test, not assumed. Scoping the redeclaration
|
|
139
|
+
to a known package wrapper class (e.g. .editor-app-root) was also tried
|
|
140
|
+
and rejected: not every integration renders that wrapper (this repo's
|
|
141
|
+
own examples/electron shell doesn't), so the alias would go undefined
|
|
142
|
+
there. See ADR-93 for the full trade-off and verveguy/liminis-editor#93's
|
|
143
|
+
PR review thread for the CSS-inheritance mechanics that rule out a
|
|
144
|
+
`:root`-only or `*`-only fix satisfying both scenarios at once.
|
|
145
|
+
======================================== */
|
|
146
|
+
:root {
|
|
147
|
+
/* Aliases for tokens with an existing --vscode-, --slashmd- or --checkbox- default */
|
|
148
|
+
--liminis-editor-background: var(--vscode-background);
|
|
149
|
+
--liminis-editor-bold-color: var(--slashmd-bold-color);
|
|
150
|
+
--liminis-editor-border: var(--vscode-border);
|
|
151
|
+
--liminis-editor-callout-caution-bg: var(--slashmd-callout-caution-bg);
|
|
152
|
+
--liminis-editor-callout-caution-border: var(--slashmd-callout-caution-border);
|
|
153
|
+
--liminis-editor-callout-important-bg: var(--slashmd-callout-important-bg);
|
|
154
|
+
--liminis-editor-callout-important-border: var(--slashmd-callout-important-border);
|
|
155
|
+
--liminis-editor-callout-note-bg: var(--slashmd-callout-note-bg);
|
|
156
|
+
--liminis-editor-callout-note-border: var(--slashmd-callout-note-border);
|
|
157
|
+
--liminis-editor-callout-tip-bg: var(--slashmd-callout-tip-bg);
|
|
158
|
+
--liminis-editor-callout-tip-border: var(--slashmd-callout-tip-border);
|
|
159
|
+
--liminis-editor-callout-warning-bg: var(--slashmd-callout-warning-bg);
|
|
160
|
+
--liminis-editor-callout-warning-border: var(--slashmd-callout-warning-border);
|
|
161
|
+
--liminis-editor-checkbox-border: var(--checkbox-border);
|
|
162
|
+
--liminis-editor-code-bg: var(--vscode-code-bg);
|
|
163
|
+
--liminis-editor-external-link: var(--vscode-external-link);
|
|
164
|
+
--liminis-editor-font-family: var(--vscode-font-family);
|
|
165
|
+
--liminis-editor-font-size: var(--vscode-font-size);
|
|
166
|
+
--liminis-editor-foreground: var(--vscode-foreground);
|
|
167
|
+
--liminis-editor-h1-color: var(--slashmd-h1-color);
|
|
168
|
+
--liminis-editor-h1-indent: var(--slashmd-h1-indent);
|
|
169
|
+
--liminis-editor-h2-color: var(--slashmd-h2-color);
|
|
170
|
+
--liminis-editor-h2-indent: var(--slashmd-h2-indent);
|
|
171
|
+
--liminis-editor-h3-color: var(--slashmd-h3-color);
|
|
172
|
+
--liminis-editor-h3-indent: var(--slashmd-h3-indent);
|
|
173
|
+
--liminis-editor-h4-color: var(--slashmd-h4-color);
|
|
174
|
+
--liminis-editor-h4-indent: var(--slashmd-h4-indent);
|
|
175
|
+
--liminis-editor-h5-color: var(--slashmd-h5-color);
|
|
176
|
+
--liminis-editor-h5-indent: var(--slashmd-h5-indent);
|
|
177
|
+
--liminis-editor-italic-color: var(--slashmd-italic-color);
|
|
178
|
+
--liminis-editor-link: var(--vscode-link);
|
|
179
|
+
--liminis-editor-selection: var(--vscode-selection);
|
|
180
|
+
--liminis-editor-token-comment: var(--slashmd-token-comment);
|
|
181
|
+
--liminis-editor-token-function: var(--slashmd-token-function);
|
|
182
|
+
--liminis-editor-token-keyword: var(--slashmd-token-keyword);
|
|
183
|
+
--liminis-editor-token-operator: var(--slashmd-token-operator);
|
|
184
|
+
--liminis-editor-token-property: var(--slashmd-token-property);
|
|
185
|
+
--liminis-editor-token-punctuation: var(--slashmd-token-punctuation);
|
|
186
|
+
--liminis-editor-token-selector: var(--slashmd-token-selector);
|
|
187
|
+
--liminis-editor-token-variable: var(--slashmd-token-variable);
|
|
188
|
+
|
|
189
|
+
/* Aliases preserving an existing nested consumption-site fallback chain —
|
|
190
|
+
these two legacy names have no --vscode-* declaration of their own, only
|
|
191
|
+
an existing fallback to a second legacy name at their consumption site. */
|
|
192
|
+
--liminis-editor-input-bg: var(--vscode-input-bg, var(--vscode-code-bg));
|
|
193
|
+
--liminis-editor-foreground-muted: var(--vscode-foreground-muted, var(--vscode-border));
|
|
194
|
+
|
|
195
|
+
/* Aliases for tokens with no CSS custom property backing anywhere — only an
|
|
196
|
+
inline literal fallback at their consumption site(s). The literal below
|
|
197
|
+
reproduces that existing light-mode fallback so a host supplying nothing
|
|
198
|
+
sees no change (FR-002); a host setting the legacy name still wins
|
|
199
|
+
(FR-003). Where the same token's literal differs by site (errorForeground,
|
|
200
|
+
toolbar-hoverBackground), a single canonical value is chosen here — see
|
|
201
|
+
ADR-93 for the two accepted visual deltas this causes at three sites. */
|
|
202
|
+
--liminis-editor-button-background: var(--vscode-button-background, #007acc);
|
|
203
|
+
--liminis-editor-button-foreground: var(--vscode-button-foreground, #ffffff);
|
|
204
|
+
--liminis-editor-menu-background: var(--vscode-menu-background, #ffffff);
|
|
205
|
+
--liminis-editor-menu-border: var(--vscode-menu-border, #d4d4d4);
|
|
206
|
+
--liminis-editor-menu-foreground: var(--vscode-menu-foreground, #333333);
|
|
207
|
+
--liminis-editor-menu-selectionBackground: var(--vscode-menu-selectionBackground, #e8e8e8);
|
|
208
|
+
--liminis-editor-menu-separatorBackground: var(--vscode-menu-separatorBackground, #d4d4d4);
|
|
209
|
+
--liminis-editor-toolbar-hoverBackground: var(--vscode-toolbar-hoverBackground, rgba(128, 128, 128, 0.15));
|
|
210
|
+
--liminis-editor-notificationsInfoIcon-foreground: var(--vscode-notificationsInfoIcon-foreground, #3794ff);
|
|
211
|
+
--liminis-editor-inputValidation-errorBackground: var(--vscode-inputValidation-errorBackground, rgba(255, 0, 0, 0.15));
|
|
212
|
+
--liminis-editor-errorForeground: var(--vscode-errorForeground, #f14c4c);
|
|
213
|
+
--liminis-editor-focus-border: var(--vscode-focus-border, #007acc);
|
|
214
|
+
--liminis-editor-focusBorder: var(--vscode-focusBorder, #007acc);
|
|
215
|
+
|
|
216
|
+
/* Aliases for liminis-app's own Tailwind @theme brand tokens — the package
|
|
217
|
+
only ever supplied a literal final fallback for these, never asserted
|
|
218
|
+
ownership; this alias makes that existing fallback reachable under the
|
|
219
|
+
new name too, without changing what it resolves to. */
|
|
220
|
+
--liminis-editor-primary: var(--color-primary, #3b82f6);
|
|
221
|
+
--liminis-editor-primary-100: var(--color-primary-100, rgba(59, 130, 246, 0.1));
|
|
222
|
+
--liminis-editor-muted-foreground: var(--color-muted-foreground, #6b7280);
|
|
223
|
+
--liminis-editor-muted-100: var(--color-muted-100, rgba(128, 128, 128, 0.1));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.dark {
|
|
227
|
+
/* Dark-mode literal overrides for the subset of the no-CSS-backing group
|
|
228
|
+
above whose consumption sites select a different literal in dark mode
|
|
229
|
+
(via a JS `isDark`/`dark` check reading the same .dark class this
|
|
230
|
+
selector keys off) rather than a single literal used in both themes. */
|
|
231
|
+
--liminis-editor-button-background: var(--vscode-button-background, #0e639c);
|
|
232
|
+
--liminis-editor-menu-background: var(--vscode-menu-background, #252526);
|
|
233
|
+
--liminis-editor-menu-border: var(--vscode-menu-border, #454545);
|
|
234
|
+
--liminis-editor-menu-foreground: var(--vscode-menu-foreground, #cccccc);
|
|
235
|
+
--liminis-editor-menu-selectionBackground: var(--vscode-menu-selectionBackground, #094771);
|
|
236
|
+
--liminis-editor-menu-separatorBackground: var(--vscode-menu-separatorBackground, #454545);
|
|
237
|
+
}
|
|
238
|
+
|
|
110
239
|
* {
|
|
111
240
|
box-sizing: border-box;
|
|
112
241
|
}
|
|
@@ -1038,6 +1167,7 @@ body {
|
|
|
1038
1167
|
}
|
|
1039
1168
|
|
|
1040
1169
|
.drag-handle:hover .drag-handle-icon {
|
|
1170
|
+
/* Superseded by --liminis-editor-toolbar-hoverBackground's canonical 0.15-alpha default (ADR-93) */
|
|
1041
1171
|
background: var(--liminis-editor-toolbar-hoverBackground, var(--vscode-toolbar-hoverBackground, rgba(128, 128, 128, 0.1)));
|
|
1042
1172
|
}
|
|
1043
1173
|
|
|
@@ -1077,6 +1207,7 @@ body {
|
|
|
1077
1207
|
|
|
1078
1208
|
.block-delete-button:hover .block-delete-icon {
|
|
1079
1209
|
background: var(--liminis-editor-inputValidation-errorBackground, var(--vscode-inputValidation-errorBackground, rgba(255, 0, 0, 0.15)));
|
|
1210
|
+
/* Superseded by --liminis-editor-errorForeground's canonical #f14c4c default (ADR-93) */
|
|
1080
1211
|
color: var(--liminis-editor-errorForeground, var(--vscode-errorForeground, #f44336));
|
|
1081
1212
|
}
|
|
1082
1213
|
|
|
@@ -1592,6 +1723,7 @@ body {
|
|
|
1592
1723
|
}
|
|
1593
1724
|
|
|
1594
1725
|
.search-close-button:hover {
|
|
1726
|
+
/* Superseded by --liminis-editor-errorForeground's canonical #f14c4c default (ADR-93) */
|
|
1595
1727
|
color: var(--liminis-editor-errorForeground, var(--vscode-errorForeground, #f44336));
|
|
1596
1728
|
}
|
|
1597
1729
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# ADR-091: `DocumentOutlineHandle` Gets Two Independent, Non-Reconciled Entry Sources
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-08-19
|
|
4
|
+
**Status:** Accepted
|
|
5
|
+
**Supersedes:** none
|
|
6
|
+
**Amends:** none
|
|
7
|
+
**Issue:** #84 (verveguy/liminis-editor)
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
Issue #69 extracted `DocumentOutline` from liminis-app's `TableOfContents.tsx`
|
|
12
|
+
and deliberately rebuilt it on **live Lexical state**: `OutlinePlugin` walks
|
|
13
|
+
`HeadingNode`s on every editor update and feeds the shared
|
|
14
|
+
`DocumentOutlineHandle`. That decision avoided a second markdown parse and got
|
|
15
|
+
#69's "the outline updates as headings change" requirement for free — but it
|
|
16
|
+
also meant an outline is only possible where a Lexical `<Editor>` is mounted.
|
|
17
|
+
|
|
18
|
+
The pre-extraction `TableOfContents.tsx` had no such limitation: it derived
|
|
19
|
+
entries from **markdown**, using this package's own `parseMarkdown`/
|
|
20
|
+
`isHeading`/`isText`/`isInlineCode`, and carried each heading's mdast
|
|
21
|
+
`position.start.line` — a 1-based source line its raw-mode variant used to
|
|
22
|
+
scroll a non-Lexical editor directly. That capability did not survive
|
|
23
|
+
extraction, which is what stalled liminis-app's raw markdown mode
|
|
24
|
+
(verveguy/liminis#1022) from adopting `DocumentOutline` at all.
|
|
25
|
+
|
|
26
|
+
Three questions needed a considered answer:
|
|
27
|
+
|
|
28
|
+
1. **Does markdown replace the Lexical derivation, or sit alongside it?**
|
|
29
|
+
Unifying on markdown for both paths would give up #69's "no second parse"
|
|
30
|
+
property for the live-Lexical/WYSIWYG path, and risked introducing subtle
|
|
31
|
+
Lexical-state/markdown divergence bugs in a path that already works.
|
|
32
|
+
2. **How does markdown reach the handle?** `OutlineHandleImpl`'s `publish`/
|
|
33
|
+
`connect`/`disconnect` are reachable today only via a brand-gated cast
|
|
34
|
+
(`handle as OutlineHandleImpl`) that only `OutlinePlugin` — always
|
|
35
|
+
constructed from `createDocumentOutlineHandle()` — can perform. A raw-mode
|
|
36
|
+
host holds a plain `DocumentOutlineHandle` and cannot replicate that cast.
|
|
37
|
+
3. **Should the two paths be reconciled when both are somehow available on
|
|
38
|
+
one handle at once?** (E.g., a host that mounts both a Lexical editor and
|
|
39
|
+
also calls the markdown methods on the same handle.)
|
|
40
|
+
|
|
41
|
+
## Decision
|
|
42
|
+
|
|
43
|
+
**`DocumentOutlineHandle` gains two new public methods —
|
|
44
|
+
`publishFromMarkdown(markdown)` and `setActiveLine(line)` — that are a second,
|
|
45
|
+
independent entry source feeding the same `publish`/`subscribe`/`getSnapshot`
|
|
46
|
+
machinery `OutlinePlugin` already uses.** `DocumentOutline.tsx` renders
|
|
47
|
+
correctly regardless of which path produced its snapshot, with no branching
|
|
48
|
+
on the source — its only change is an optional `onEntrySelect` prop, fired
|
|
49
|
+
alongside the existing `scrollToHeading` call, since that call is a no-op on
|
|
50
|
+
the markdown path (no Lexical editor to scroll) and a raw-mode host needs
|
|
51
|
+
some way to react to a click without re-implementing the entry list itself
|
|
52
|
+
(see "Bad / accepted" below). `OutlinePlugin.tsx` needs zero changes: the
|
|
53
|
+
Lexical path keeps working exactly as it did in 0.2.1.
|
|
54
|
+
|
|
55
|
+
**The two paths are parallel and deliberately not reconciled.** A handle is
|
|
56
|
+
fed by one path or the other — `<Editor documentOutlineHandle>`/
|
|
57
|
+
`OutlinePlugin`, or `publishFromMarkdown`/`setActiveLine` — matching how the
|
|
58
|
+
pre-extraction app never ran both on the same instance either. Mounting both
|
|
59
|
+
on one handle at once is unsupported and produces whichever `publish()` call
|
|
60
|
+
happened most recently, the same "last write wins" behavior `publish()`
|
|
61
|
+
already has for any two callers.
|
|
62
|
+
|
|
63
|
+
**`publishFromMarkdown` and `setActiveLine` are split into two methods, not
|
|
64
|
+
one combined call**, because content changes and scroll events fire
|
|
65
|
+
independently and at different rates in a raw-mode host, which owns its own
|
|
66
|
+
scroll tracking (there is no package-side equivalent of `OutlinePlugin`'s
|
|
67
|
+
rAF-throttled viewport observer, since the package does not own a raw-mode
|
|
68
|
+
host's editor or its scroll container). `OutlineHandleImpl` stores the
|
|
69
|
+
last-derived markdown entries and last active line internally so either
|
|
70
|
+
method can be called alone.
|
|
71
|
+
|
|
72
|
+
**The pure derivation functions (`deriveOutlineFromMarkdown`,
|
|
73
|
+
`resolveActiveOutlineIndex`) live in a sibling file
|
|
74
|
+
(`documentOutlineMarkdown.ts`), not inlined into `documentOutlineHandle.ts`,
|
|
75
|
+
and are exported standalone from the package root** alongside the handle
|
|
76
|
+
methods that compose them — independently unit-testable with no handle/
|
|
77
|
+
publish machinery involved, and usable by a host that wants the derivation
|
|
78
|
+
without the handle's mutable-state class at all.
|
|
79
|
+
|
|
80
|
+
**`OutlineEntry` gains an optional `line?: number`**, populated only on
|
|
81
|
+
markdown-derived entries; the Lexical path leaves it unset since live Lexical
|
|
82
|
+
state has no source-line mapping to supply. `snapshotsEqual` compares it, so
|
|
83
|
+
a line-only change (same text/level, new line) still notifies subscribers.
|
|
84
|
+
|
|
85
|
+
## Consequences
|
|
86
|
+
|
|
87
|
+
**Good:**
|
|
88
|
+
|
|
89
|
+
- A raw-mode host with zero Lexical editors mounted can produce a populated,
|
|
90
|
+
navigable outline by calling two methods on a handle it already holds, and
|
|
91
|
+
render it with the shared `<DocumentOutline>` component — including
|
|
92
|
+
click-to-navigate via `onEntrySelect`, since `scrollToHeading` alone
|
|
93
|
+
no-ops on this path — satisfying #84's FR-008/SC-004 ("adopt without
|
|
94
|
+
reimplementing... logic") without liminis#1022 forking outline/
|
|
95
|
+
heading-derivation *or* list-rendering code.
|
|
96
|
+
- The existing Lexical/WYSIWYG path is untouched at the source level:
|
|
97
|
+
`OutlinePlugin.tsx` has no diff, and #69's existing test suites pass
|
|
98
|
+
unmodified, directly verifying FR-005/US3 ("WYSIWYG behavior is
|
|
99
|
+
unaffected").
|
|
100
|
+
- `DocumentOutline.tsx` has no branching on which path produced its
|
|
101
|
+
snapshot — the two entry sources are invisible to it — and its one
|
|
102
|
+
addition (`onEntrySelect`) is additive and optional, so existing WYSIWYG
|
|
103
|
+
consumers that don't pass it see no behavior change.
|
|
104
|
+
- The standalone-exported pure functions give a host an escape hatch if it
|
|
105
|
+
ever wants derivation without the handle/publish machinery (e.g. a
|
|
106
|
+
server-side outline preview), without that being the primary API shape.
|
|
107
|
+
|
|
108
|
+
**Bad / accepted:**
|
|
109
|
+
|
|
110
|
+
- **Text extraction is now implemented twice**: Lexical's built-in
|
|
111
|
+
`getTextContent()` on the WYSIWYG path, and a hand-written recursive mdast
|
|
112
|
+
walk (`extractText` in `documentOutlineMarkdown.ts`) on the markdown path.
|
|
113
|
+
A future inline mdast node type could silently produce different text on
|
|
114
|
+
the two paths with nothing to catch it beyond the shared fixture the two
|
|
115
|
+
paths' test suites both happen to exercise (`OutlinePlugin.test.tsx`'s and
|
|
116
|
+
`documentOutlineMarkdown.test.ts`'s equivalent inline-code case). No shared
|
|
117
|
+
test harness enforces parity mechanically; considered out of scope for this
|
|
118
|
+
issue.
|
|
119
|
+
- **The two paths are never reconciled**, so a host that (incorrectly) drives
|
|
120
|
+
both at once on the same handle gets undefined-in-practice "whichever
|
|
121
|
+
`publish()` ran last" behavior rather than an explicit error. Accepted
|
|
122
|
+
because detecting and rejecting that misuse would add complexity for a
|
|
123
|
+
configuration nothing in the spec calls for and the pre-extraction app
|
|
124
|
+
never did either.
|
|
125
|
+
- **Empty-title headings are included on the markdown path**, diverging from
|
|
126
|
+
the pre-extraction `TableOfContents.tsx`'s `if (!title) continue`. This
|
|
127
|
+
matches FR-002's literal requirement ("same rules as the existing Lexical
|
|
128
|
+
path", which does not skip them) over parity with the old app behavior —
|
|
129
|
+
liminis#1022 needs to be aware of this when it adopts the markdown path.
|
|
130
|
+
|
|
131
|
+
**Neutral:**
|
|
132
|
+
|
|
133
|
+
- No new export subpath was added; `deriveOutlineFromMarkdown`/
|
|
134
|
+
`resolveActiveOutlineIndex` are exported from the same root `.` entry as
|
|
135
|
+
the rest of the outline surface, not from `./markdown`'s Lexical-free
|
|
136
|
+
barrel. ADR-075 draws that barrel's isolation boundary around avoiding
|
|
137
|
+
Lexical/MathJax/Mermaid/C4 for a consumer that only parses markdown; this
|
|
138
|
+
issue's FR-008 requires only that liminis#1022 can adopt the capability
|
|
139
|
+
without forking logic, not that it be reachable without the root barrel's
|
|
140
|
+
full import graph. Revisit only if that assumption proves wrong during
|
|
141
|
+
#1022's own implementation.
|
|
142
|
+
|
|
143
|
+
## References
|
|
144
|
+
|
|
145
|
+
- Issue #84 (this decision)
|
|
146
|
+
- `src/app/editor/documentOutlineHandle.ts` — `OutlineEntry.line`,
|
|
147
|
+
`publishFromMarkdown`, `setActiveLine`, updated `snapshotsEqual`
|
|
148
|
+
- `src/app/editor/documentOutlineMarkdown.ts` — `deriveOutlineFromMarkdown`,
|
|
149
|
+
`resolveActiveOutlineIndex`
|
|
150
|
+
- `src/app/editor/OutlinePlugin.tsx` — the unchanged Lexical/WYSIWYG path
|
|
151
|
+
- `src/app/editor/DocumentOutline.tsx` — the `onEntrySelect` prop, its one
|
|
152
|
+
change, added so a raw-mode host can react to clicks
|
|
153
|
+
- `docs/editor-api.md` — "Raw-mode / markdown-derived entries" section
|
|
154
|
+
- `docs/decisions/adr-075.md` — the package-boundary/export-subpath decision
|
|
155
|
+
this ADR's "Neutral" section revisits
|
|
156
|
+
- `specs/69-extract-the-table-of/spec.md`, `docs/decisions/adr-088.md` —
|
|
157
|
+
issue #69's own decision to derive from live Lexical state, whose
|
|
158
|
+
WYSIWYG-only assumption this issue lifts without replacing it
|
|
159
|
+
- verveguy/liminis#1022 (external repo) — the consumer this decision unblocks
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# ADR-092: A Checked-In Baseline Guards `styles.css`'s Defined Tokens Against Silent Rename or Removal
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-08-20
|
|
4
|
+
**Status:** Accepted
|
|
5
|
+
**Supersedes:** none
|
|
6
|
+
**Amends:** none
|
|
7
|
+
**Issue:** #79 (verveguy/liminis-editor)
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
`tests/theming-contract.test.ts` (ADR-085) already guards the *consumed*
|
|
12
|
+
token set — every `var(--x)` reference under `src/` must be documented in
|
|
13
|
+
the README's generated table, resolve without a host, carry a description,
|
|
14
|
+
and (per ADR-087) preserve a fallback to its pre-`0.2.0` name where renamed.
|
|
15
|
+
None of those four checks say anything about a token that is *defined*
|
|
16
|
+
(declared with a value in `styles.css`) but never itself consumed by
|
|
17
|
+
`src/` — the `--vscode-*`, `--slashmd-*`, `--color-*` and `--checkbox-*`
|
|
18
|
+
fallback targets `0.2.0` (#51) left in place specifically so a host still
|
|
19
|
+
supplying only the old name keeps working.
|
|
20
|
+
|
|
21
|
+
Zusammen, a downstream host, reads several of those definitions directly —
|
|
22
|
+
`var(--vscode-background)`, `var(--vscode-border)`, etc. — mapping its own
|
|
23
|
+
design-system tokens onto them, not overriding them. Nothing about that is
|
|
24
|
+
visible from inside this package: `src/` never references those names via
|
|
25
|
+
`var()`, so the existing consumed-token guard has nothing to check. `0.2.0`
|
|
26
|
+
happened to leave every defined token's name untouched (0 removed, 0 added,
|
|
27
|
+
comparing the published `0.1.1` and `0.2.0` packages) — but that was a
|
|
28
|
+
property of what `0.2.0` chose to do, not a guarantee enforced anywhere. A
|
|
29
|
+
future rename that touched definition sites as well as consumption sites
|
|
30
|
+
would drop Zusammen's palette silently: an unresolved CSS custom property
|
|
31
|
+
does not error, the declaration is just gone.
|
|
32
|
+
|
|
33
|
+
This is a genuinely different axis from every existing guard in this repo.
|
|
34
|
+
Two questions needed a considered answer:
|
|
35
|
+
|
|
36
|
+
1. **What should CI compare the live defined set against?** Every existing
|
|
37
|
+
contract test (`theming-contract`, `adr-citations`,
|
|
38
|
+
`package-manifest-contract`) computes both sides of its comparison
|
|
39
|
+
dynamically from current source — there is no prior example in this repo
|
|
40
|
+
of a checked-in snapshot a maintainer edits by hand as a deliberate,
|
|
41
|
+
reviewable act. A dynamic self-comparison can't work here: the whole
|
|
42
|
+
point is to catch names *disappearing* between one state and the next,
|
|
43
|
+
which requires a reference point independent of "whatever `styles.css`
|
|
44
|
+
currently says."
|
|
45
|
+
2. **Should an addition to the defined set ever fail CI?** The issue's
|
|
46
|
+
FR-006 and SC-002 are explicit that it must not — only a *disappearance*
|
|
47
|
+
of a previously-baselined name is a failure. (Acceptance Scenario 4's
|
|
48
|
+
prose reads, in isolation, as if new tokens need the baseline updated
|
|
49
|
+
before CI passes at all; FR-006/SC-002 are the more explicit,
|
|
50
|
+
cross-referencing pair on this exact point, so this decision follows
|
|
51
|
+
them: the guard is one-directional.)
|
|
52
|
+
|
|
53
|
+
## Decision
|
|
54
|
+
|
|
55
|
+
**A new checked-in baseline, `scripts/lib/theming-defined-tokens-baseline.json`,
|
|
56
|
+
records the flat, sorted array of every token name `defaultedTokens()`
|
|
57
|
+
(already existing, unchanged) finds declared in `src/styles.css`.**
|
|
58
|
+
`diffDefinedTokenBaseline(current, baseline)`, added to
|
|
59
|
+
`scripts/lib/theming-tokens.mjs` alongside the other pure extraction/compare
|
|
60
|
+
functions, returns `{ missing, added }` — both sorted, both computed by
|
|
61
|
+
name, never by count. `tests/theming-contract.test.ts` fails only when
|
|
62
|
+
`missing` is non-empty, and names the specific token(s) in the failure
|
|
63
|
+
message.
|
|
64
|
+
|
|
65
|
+
**The comparison is one-directional: `missing` fails CI, `added` never
|
|
66
|
+
does.** A token added to `styles.css` without a baseline update is not
|
|
67
|
+
itself an error — it's simply not yet protected by this guard until someone
|
|
68
|
+
runs the updater. This is deliberate (FR-006), not an oversight: the
|
|
69
|
+
baseline's job is to catch *disappearance*, not to force every addition
|
|
70
|
+
through an extra step before it can ship.
|
|
71
|
+
|
|
72
|
+
**A same-count rename still fails.** Because the diff is by name, a
|
|
73
|
+
declaration renamed in the same change (old name deleted, new name added,
|
|
74
|
+
total count unchanged) produces one entry in `missing` and one in `added` —
|
|
75
|
+
never a silent no-op. This reproduces, and would have caught, the exact
|
|
76
|
+
scenario the issue's Background describes: `0.2.0` was 42 defined tokens
|
|
77
|
+
before and 42 after, which a count-only check would have called clean.
|
|
78
|
+
|
|
79
|
+
**`scripts/update-theming-baseline.mjs`, a new script mirroring
|
|
80
|
+
`generate-theming-docs.mjs`'s shape, is the update mechanism**, exposed as
|
|
81
|
+
`pnpm docs:theming-baseline`. It reads `defaultedTokens('src/styles.css')`
|
|
82
|
+
and writes the sorted JSON array. A maintainer making a deliberate rename or
|
|
83
|
+
removal runs it in the same change that edits `styles.css`; that diff is
|
|
84
|
+
then a normal part of PR review, distinguishing "I meant to do this" from
|
|
85
|
+
"this disappeared by accident." This matches the repo's existing
|
|
86
|
+
`docs:theming` precedent rather than asking for a bare hand-edit of JSON.
|
|
87
|
+
|
|
88
|
+
**The guard evaluates `src/styles.css`, not a built `dist/`.** `pnpm test`
|
|
89
|
+
alone (no prior build) is what a local run and this guard's own CI job
|
|
90
|
+
invoke; `dist/` is gitignored and does not exist without a build step
|
|
91
|
+
first. `scripts/copy-assets.mjs` makes `dist/styles.css` a byte-for-byte
|
|
92
|
+
copy of `src/styles.css` with no transform, so evaluating the guard against
|
|
93
|
+
`src/` is representative of what ships (FR-008) without requiring a build
|
|
94
|
+
at test time — the same choice the pre-existing consumed-token guard
|
|
95
|
+
already made.
|
|
96
|
+
|
|
97
|
+
**The baseline lives in `scripts/lib/`, not a fixtures directory.** This
|
|
98
|
+
repo has no checked-in test-fixtures directory; the existing mutation tests
|
|
99
|
+
build throwaway fixtures under `os.tmpdir()`. The baseline is not test
|
|
100
|
+
scaffolding — it's permanent project data describing the current package
|
|
101
|
+
surface, the same category as `PREVIOUS_NAME` and `TOKEN_DESCRIPTIONS` in
|
|
102
|
+
the same module, so it's colocated with them.
|
|
103
|
+
|
|
104
|
+
## Consequences
|
|
105
|
+
|
|
106
|
+
**Good:**
|
|
107
|
+
|
|
108
|
+
- Removing or renaming a defined token without updating the baseline now
|
|
109
|
+
fails CI, naming the specific token(s) — the exact defence the issue's
|
|
110
|
+
Background describes as missing, closing the gap that let `0.2.0`'s
|
|
111
|
+
rename through by coincidence rather than by guarantee.
|
|
112
|
+
- The name-vs-count distinction is exercised directly by a mutation test
|
|
113
|
+
(`flags a same-count rename, not masked by an unchanged total`), so the
|
|
114
|
+
guard's core guarantee is demonstrated, not merely asserted.
|
|
115
|
+
- Adding a new token is a one-line, low-friction baseline update via
|
|
116
|
+
`pnpm docs:theming-baseline` — never a blocker on its own, per FR-006.
|
|
117
|
+
- The README now states, in the paragraph a first-time reader reaches
|
|
118
|
+
before the token table, that defined tokens (including pre-`0.2.0`
|
|
119
|
+
fallback names) are public API a host may read directly, and that
|
|
120
|
+
renaming or removing one is a breaking change under the existing
|
|
121
|
+
`0.x` versioning policy — answering SC-004 without requiring this ADR or
|
|
122
|
+
the issue as prerequisite reading.
|
|
123
|
+
|
|
124
|
+
**Bad / accepted:**
|
|
125
|
+
|
|
126
|
+
- The baseline is a plain hand-maintained (script-updated) JSON array with
|
|
127
|
+
no schema enforcement beyond `diffDefinedTokenBaseline`'s runtime
|
|
128
|
+
comparison — a malformed or hand-edited-incorrectly baseline would only
|
|
129
|
+
surface as an unexpected `missing`/`added` result at test time, not a
|
|
130
|
+
separate validation error. Accepted: the array is trivial enough (one
|
|
131
|
+
string per line) that this class of mistake is unlikely and, if it
|
|
132
|
+
happens, self-corrects on the next `pnpm docs:theming-baseline` run.
|
|
133
|
+
- Because the guard is one-directional by design, a token added to
|
|
134
|
+
`styles.css` and never baselined is not protected — if it is later
|
|
135
|
+
removed, that removal also goes uncaught (it was never in `missing`'s
|
|
136
|
+
input set to begin with). This is an accepted consequence of FR-006's
|
|
137
|
+
explicit choice, not a bug to "fix" by making the guard bidirectional;
|
|
138
|
+
a maintainer who wants a newly-added token protected needs to run the
|
|
139
|
+
updater, same as documenting it in the README table already requires a
|
|
140
|
+
separate `pnpm docs:theming` run today.
|
|
141
|
+
- `theming-tokens.d.mts` is hand-maintained with no automated sync check
|
|
142
|
+
against `theming-tokens.mjs`'s actual exports; forgetting to add
|
|
143
|
+
`diffDefinedTokenBaseline`'s declaration there would only surface as a
|
|
144
|
+
`pnpm typecheck` failure. Accepted as the same pre-existing tradeoff
|
|
145
|
+
ADR-085 already lives with for every other exported function in this
|
|
146
|
+
module.
|
|
147
|
+
|
|
148
|
+
**Neutral:**
|
|
149
|
+
|
|
150
|
+
- This ADR does not change which tokens are currently defined, consumed,
|
|
151
|
+
or documented, and does not decide whether `--liminis-editor-*` names
|
|
152
|
+
should eventually be defined natively with the legacy prefixes as an
|
|
153
|
+
alias layer (issue #79's Item 3) — that remains a follow-up candidate
|
|
154
|
+
issue, independent of this guard. See "Not decided here" below.
|
|
155
|
+
|
|
156
|
+
## Not decided here
|
|
157
|
+
|
|
158
|
+
Issue #79 named three things to do and ranked "Item 2 is the substance."
|
|
159
|
+
This decision delivers Item 1 (the README states the contract) and Item 2
|
|
160
|
+
(this baseline guard). **Item 3 — whether the package should eventually
|
|
161
|
+
*define* `--liminis-editor-*` names directly, with `--vscode-*`/
|
|
162
|
+
`--slashmd-*`/`--color-*`/`--checkbox-*` kept as an alias layer — is left
|
|
163
|
+
open.** It is a design decision (how an alias layer would interact with the
|
|
164
|
+
existing fallback chain from ADR-087), not a guard, and resolving it well is
|
|
165
|
+
independent of stating today's contract and protecting it against
|
|
166
|
+
shrinkage.
|
|
167
|
+
|
|
168
|
+
This gap is not just theoretical follow-up work: **a downstream consumer is
|
|
169
|
+
already blocked on it.** Zusammen currently reads this package's legacy
|
|
170
|
+
definitions directly (`var(--vscode-background)`, `var(--vscode-border)`,
|
|
171
|
+
etc., per this issue's Background) and wants to migrate those reads onto the
|
|
172
|
+
`--liminis-editor-*` vocabulary instead — but there is nothing to migrate
|
|
173
|
+
to, because this package defines zero `--liminis-editor-*` names today (see
|
|
174
|
+
"Has a default: No" for every row in the README's token table). That
|
|
175
|
+
migration is tracked at
|
|
176
|
+
[verveguy/zusammen#129](https://github.com/verveguy/zusammen/issues/129) and
|
|
177
|
+
cannot proceed until Item 3 is decided. Whoever picks up Item 3 should treat
|
|
178
|
+
that issue as a concrete acceptance case, not just this ADR's own
|
|
179
|
+
description of the gap.
|
|
180
|
+
|
|
181
|
+
## References
|
|
182
|
+
|
|
183
|
+
- Issue #79 (this decision)
|
|
184
|
+
- `scripts/lib/theming-tokens.mjs` — `defaultedTokens()` (unchanged, already
|
|
185
|
+
computed the "defined" set) and the new `diffDefinedTokenBaseline()`
|
|
186
|
+
- `scripts/lib/theming-defined-tokens-baseline.json` — the checked-in
|
|
187
|
+
baseline
|
|
188
|
+
- `scripts/update-theming-baseline.mjs` — the update mechanism
|
|
189
|
+
(`pnpm docs:theming-baseline`)
|
|
190
|
+
- `tests/theming-contract.test.ts` — the new "defined tokens are a public
|
|
191
|
+
API surface" `describe` block and its mutation tests
|
|
192
|
+
- `scripts/copy-assets.mjs` — establishes `dist/styles.css` as a
|
|
193
|
+
byte-for-byte copy of `src/styles.css`, the basis for FR-008
|
|
194
|
+
- `docs/decisions/adr-085.md` — the consumed/documented guard and the
|
|
195
|
+
shared-module, mutation-testing conventions this decision follows
|
|
196
|
+
- `docs/decisions/adr-087.md` — the `--liminis-editor-*` rename this
|
|
197
|
+
decision's guard would have caught had it also touched definition sites
|
|
198
|
+
- Issue #52 — the prior silent-drop incident that motivated the
|
|
199
|
+
resolves-without-host guard this new guard sits alongside
|
|
200
|
+
- [verveguy/zusammen#129](https://github.com/verveguy/zusammen/issues/129) —
|
|
201
|
+
the downstream migration blocked on issue #79's Item 3, deferred by this
|
|
202
|
+
decision (see "Not decided here")
|