@coldsmirk/inkstone-react 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @coldsmirk/inkstone-react
2
2
 
3
- Drop-in React code editors with the integration chores solved once: a preconfigured Monaco **`<MonacoEditor>`** (offline workers, Shiki catppuccin themes, dark switching, opt-in zh-CN UI) and a controlled CodeMirror 6 **`<CodeMirrorEditor>`** (catppuccin themes, restyled chrome, MiniJinja support, sizing). Drop in a component; the plumbing is already wired.
3
+ Drop-in React code editors with the integration chores solved once: a preconfigured Monaco **`<MonacoEditor>`** (offline workers, Shiki catppuccin themes, dark switching, opt-in zh-CN UI) and a controlled CodeMirror 6 **`<CodeMirrorEditor>`** (catppuccin themes, restyled chrome, MiniJinja support, sizing) — plus **`<MonacoDiffEditor>`** and **`<CodeMirrorDiffEditor>`**, the same assemblies around each engine's side-by-side diff view. Drop in a component; the plumbing is already wired.
4
4
 
5
5
  Part of [inkstone](https://github.com/coldsmirk/inkstone) — this package composes [`@coldsmirk/inkstone-core`](https://www.npmjs.com/package/@coldsmirk/inkstone-core) (shared Shiki highlighter), [`@coldsmirk/inkstone-monaco`](https://www.npmjs.com/package/@coldsmirk/inkstone-monaco) (offline Monaco host), and [`@coldsmirk/inkstone-codemirror`](https://www.npmjs.com/package/@coldsmirk/inkstone-codemirror) (controlled CM6 host + MiniJinja). See the [repository README](https://github.com/coldsmirk/inkstone#readme) for the full guide; the highlights below are what you touch daily.
6
6
 
@@ -11,12 +11,14 @@ pnpm add @coldsmirk/inkstone-react monaco-editor react \
11
11
  @codemirror/state @codemirror/view @codemirror/language @codemirror/autocomplete @codemirror/commands @codemirror/search
12
12
  ```
13
13
 
14
- `monaco-editor`, `react`, and the `@codemirror/*` packages are **peer dependencies** — your app owns the single copy of each (`@codemirror/state` breaks at runtime if two copies load). Node.js >= 22 for build / SSR hosts.
14
+ `monaco-editor`, `react`, and the `@codemirror/*` packages are **peer dependencies** — your app owns the single copy of each (`@codemirror/state` breaks at runtime if two copies load). Node.js >= 24 for build / SSR hosts.
15
+
16
+ Import from the engine-specific entry points below so a CodeMirror-only build never traverses or emits Monaco (and vice versa). The aggregate `@coldsmirk/inkstone-react` entry remains available for existing consumers; `@coldsmirk/inkstone-react/shiki` exposes the standalone highlighter hook.
15
17
 
16
18
  ## `<MonacoEditor>`
17
19
 
18
20
  ```tsx
19
- import { MonacoEditor } from "@coldsmirk/inkstone-react";
21
+ import { MonacoEditor } from "@coldsmirk/inkstone-react/monaco";
20
22
 
21
23
  <MonacoEditor
22
24
  language="javascript"
@@ -39,7 +41,7 @@ Never `import "monaco-editor"` statically anywhere in the app — it locks the U
39
41
  ## `<CodeMirrorEditor>`
40
42
 
41
43
  ```tsx
42
- import { CodeMirrorEditor } from "@coldsmirk/inkstone-react";
44
+ import { CodeMirrorEditor } from "@coldsmirk/inkstone-react/codemirror";
43
45
 
44
46
  <CodeMirrorEditor
45
47
  language="json"
@@ -54,11 +56,31 @@ A controlled CM6 editor: create-once view, external value changes reconcile with
54
56
 
55
57
  - **`language`** lazily imports the matching grammar as its own chunk — the official Lezer parsers plus the `@codemirror/legacy-modes` catalog (`shell`, `dockerfile`, `ini` / `env`, `toml`, `nginx`, …). Includes the MiniJinja family — `minijinja` standalone or `minijinja-<host>` mixed variants (`minijinja-yaml`, `-json`, `-html`, `-sql`, `-shell`, `-dockerfile`, …).
56
58
  - **`context`** — pass `{ schema }` (JSON Schema) or `{ sample }` (representative value) and MiniJinja completion suggests the render context's variables with member access. Memoize/hoist it — a fresh identity each render re-normalizes.
59
+ - **`sqlSchema`** — pass table/column metadata to the Lezer-backed SQL languages (`sql`, `mysql`, `pgsql`, `sqlite`, `mssql`, `mariadb`, `plsql`, `cassandra`) and their `minijinja-*` variants for dialect-aware table, alias, and column completion. Updates in place; legacy stream-mode SQL languages do not consume schemas.
57
60
  - **`search`** (opt-in) adds the standard search keymap and panel; `locale="zh-cn"` localizes it per editor.
58
61
  - **Metrics & layout** — `fontSize` 14 and `lineHeight` 1.6 by default; `height` / `width` size the editor; `lineWrapping`, `showLineNumbers`, `folding` are opt-in.
62
+ - **Accessibility** — the textbox defaults to `ariaLabel="Editor content"`; pass a workflow-specific `ariaLabel` when needed. Focus presentation belongs to the surrounding application chrome.
59
63
  - **Extensions** — `extensions` is create-once; `dynamicExtensions` reconfigures live. The catppuccin theme sits at low precedence, so a caller theme in `dynamicExtensions` overrides it.
60
64
 
61
- `useShikiHighlighter` and the `codeMirrorLanguages` / `monacoLanguages` / `normalizeContext` / `searchPhrasesZhCn` helpers are re-exported for hosts that build custom surfaces next to the components.
65
+ `useShikiHighlighter` is exported from `@coldsmirk/inkstone-react/shiki`; `codeMirrorLanguages` / `normalizeContext` / `searchPhrasesZhCn` live on the CodeMirror entry, and `monacoLanguages` lives on the Monaco entry. The aggregate root re-exports them for compatibility.
66
+
67
+ ## `<MonacoDiffEditor>` / `<CodeMirrorDiffEditor>`
68
+
69
+ ```tsx
70
+ import { MonacoDiffEditor } from "@coldsmirk/inkstone-react/monaco";
71
+ import { CodeMirrorDiffEditor } from "@coldsmirk/inkstone-react/codemirror";
72
+
73
+ <MonacoDiffEditor language="typescript" dark={dark} original={before} modified={after} onChange={setAfter} />;
74
+
75
+ <CodeMirrorDiffEditor language="json" dark={dark} original={before} modified={after} onChange={setAfter} />;
76
+ ```
77
+
78
+ The pair speaks the same core contract on both engines: the **original** side (left) is read-only and follows its prop; the **modified** side is the controlled, editable document behind `modified` / `onChange` — set `readOnly` for a pure diff viewer. External changes to either prop reconcile into the live view as one minimal replace — no rebuild, off the undo stack, carets and selections outside the changed region kept — and prop reconciliations never echo back through `onChange`. Shared toggles: `collapseUnchanged` folds long unchanged stretches behind expandable bars, `lineWrapping`, and `showLineNumbers` (on by default — a diff reads by line reference).
79
+
80
+ Each component keeps its engine's conventions from the single-editor sibling:
81
+
82
+ - **`<MonacoDiffEditor>`** — the `<MonacoEditor>` assembly (offline host, Shiki themes, `locale`, `loading` / `failure`, `hostOptions`) around `monaco.editor.createDiffEditor`. `options` takes raw `IDiffEditorConstructionOptions` over the same `DEFAULT_MONACO_OPTIONS` baseline — e.g. `renderSideBySide: false` for the inline view.
83
+ - **`<CodeMirrorDiffEditor>`** — `@codemirror/merge`'s `MergeView` behind `<CodeMirrorEditor>`'s create-once discipline, with the catppuccin flavors extended to the diff chrome (red deletions / green insertions, themed collapse bars and revert controls). `revertControls` adds per-chunk arrows copying the original chunk into the modified document (flows through `onChange`); `language`, `extensions`, and `dynamicExtensions` apply to both sides; `onMount` hands you the `MergeView`.
62
84
 
63
85
  ## License
64
86
 
@@ -0,0 +1,604 @@
1
+ import { n as useControlledReconcileSignal, r as CODE_FONT_FAMILY, t as useLatest } from "./use-latest-BhihMdOp.js";
2
+ import { flavors } from "@catppuccin/palette";
3
+ import { EditorState, Prec } from "@codemirror/state";
4
+ import { EditorView, lineNumbers } from "@codemirror/view";
5
+ import { ControlledEditorHost, ControlledMergeHost, codeMirrorLanguages, documentExtensions, loadLanguage, normalizeContext, normalizeContext as normalizeContext$1, searchPhrasesZhCn, searchPhrasesZhCn as searchPhrasesZhCn$1, setMinijinjaContext, setSqlSchema } from "@coldsmirk/inkstone-codemirror";
6
+ import { useEffect, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
7
+ import { catppuccinLatte, catppuccinMacchiato } from "@catppuccin/codemirror";
8
+ import { jsx } from "react/jsx-runtime";
9
+ import { indentUnit } from "@codemirror/language";
10
+ //#region src/codemirror-theme.ts
11
+ function toCssSize(value) {
12
+ if (value === void 0) return;
13
+ return typeof value === "number" ? `${value}px` : value;
14
+ }
15
+ function alphaOf(color, alphaValue) {
16
+ return `rgba(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, ${alphaValue})`;
17
+ }
18
+ function completionIconMask(body) {
19
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="#000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">${body}</svg>`;
20
+ return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
21
+ }
22
+ const COMPLETION_ICON_FALLBACK = completionIconMask("<circle cx=\"8\" cy=\"8\" r=\"2.5\"/>");
23
+ const COMPLETION_ICONS = {
24
+ keyword: completionIconMask("<path d=\"M5.5 5 2.5 8l3 3\"/><path d=\"m10.5 5 3 3-3 3\"/>"),
25
+ function: completionIconMask("<path d=\"M10.8 3.4c-2-.4-2.9.7-3.2 2.5L6.4 12c-.3 1.7-1.2 2.2-2.6 1.8\"/><path d=\"M4.6 7.5h5\"/>"),
26
+ method: completionIconMask("<path d=\"M10.8 3.4c-2-.4-2.9.7-3.2 2.5L6.4 12c-.3 1.7-1.2 2.2-2.6 1.8\"/><path d=\"M4.6 7.5h5\"/>"),
27
+ variable: completionIconMask("<path d=\"M4 5c2.2 0 5.3 6 8 6\"/><path d=\"M12 5c-2.7 0-5.8 6-8 6\"/>"),
28
+ constant: completionIconMask("<path d=\"M3.5 5.2h9\"/><path d=\"M6.2 5.2v6.3\"/><path d=\"M10.2 5.2v4.4c0 1.3.6 1.9 1.7 1.9\"/>"),
29
+ property: completionIconMask("<circle cx=\"4.9\" cy=\"11.1\" r=\"1.8\"/><circle cx=\"11.1\" cy=\"4.9\" r=\"1.8\"/><path d=\"M6.4 9.6l3.2-3.2\"/>"),
30
+ class: completionIconMask("<rect x=\"3.5\" y=\"3.5\" width=\"9\" height=\"9\" rx=\"2.2\"/>"),
31
+ interface: completionIconMask("<circle cx=\"8\" cy=\"8\" r=\"4.5\"/>"),
32
+ type: completionIconMask("<path d=\"M4 4.5h8\"/><path d=\"M8 4.5V12\"/>"),
33
+ enum: completionIconMask("<path d=\"M6.5 4.5h6\"/><path d=\"M6.5 8h6\"/><path d=\"M6.5 11.5h6\"/><path d=\"M3.4 4.5h.01\"/><path d=\"M3.4 8h.01\"/><path d=\"M3.4 11.5h.01\"/>"),
34
+ namespace: completionIconMask("<path d=\"M6.2 3.5c-1.3 0-1.9.6-1.9 1.9v1.2c0 .9-.4 1.4-1.3 1.4.9 0 1.3.5 1.3 1.4v1.2c0 1.3.6 1.9 1.9 1.9\"/><path d=\"M9.8 3.5c1.3 0 1.9.6 1.9 1.9v1.2c0 .9.4 1.4 1.3 1.4-.9 0-1.3.5-1.3 1.4v1.2c0 1.3-.6 1.9-1.9 1.9\"/>"),
35
+ text: completionIconMask("<path d=\"M3.5 5h9\"/><path d=\"M3.5 8h9\"/><path d=\"M3.5 11h5.5\"/>")
36
+ };
37
+ function completionIconRules() {
38
+ const rules = {};
39
+ for (const [kind, mask] of Object.entries(COMPLETION_ICONS)) rules[`.cm-completionIcon-${kind}:after`] = {
40
+ maskImage: mask,
41
+ WebkitMaskImage: mask
42
+ };
43
+ return rules;
44
+ }
45
+ function chromeTheme(dark) {
46
+ const { colors } = dark ? flavors.macchiato : flavors.latte;
47
+ const hairline = `1px solid ${alphaOf(colors.overlay0, .35)}`;
48
+ const frame = {
49
+ border: hairline,
50
+ borderRadius: "8px",
51
+ boxShadow: dark ? "0 8px 24px rgba(0, 0, 0, 0.5)" : "0 8px 24px rgba(0, 0, 0, 0.12)",
52
+ color: colors.text.hex
53
+ };
54
+ return EditorView.theme({
55
+ "&": { colorScheme: dark ? "dark" : "light" },
56
+ ".cm-scroller": { fontFamily: CODE_FONT_FAMILY },
57
+ ".cm-tooltip": frame,
58
+ ".cm-tooltip.cm-tooltip-autocomplete": {
59
+ ...frame,
60
+ "& > ul": {
61
+ borderRadius: "7px",
62
+ padding: "4px",
63
+ maxHeight: "16em"
64
+ },
65
+ "& > ul > li": {
66
+ display: "flex",
67
+ alignItems: "center",
68
+ padding: "3px 8px",
69
+ borderRadius: "5px",
70
+ lineHeight: "1.45"
71
+ }
72
+ },
73
+ ".cm-completionLabel": {
74
+ minWidth: "0",
75
+ overflow: "hidden",
76
+ textOverflow: "ellipsis"
77
+ },
78
+ ".cm-completionMatchedText": {
79
+ textDecoration: "none",
80
+ fontWeight: "600",
81
+ color: colors.blue.hex
82
+ },
83
+ ".cm-completionDetail": {
84
+ marginLeft: "auto",
85
+ paddingLeft: "12px",
86
+ fontStyle: "normal",
87
+ fontSize: "85%",
88
+ color: colors.overlay1.hex
89
+ },
90
+ ".cm-completionIcon": {
91
+ width: "14px",
92
+ paddingRight: "8px",
93
+ opacity: "1",
94
+ color: colors.overlay1.hex
95
+ },
96
+ ".cm-completionIcon:after": {
97
+ content: "''",
98
+ display: "block",
99
+ width: "12px",
100
+ height: "12px",
101
+ backgroundColor: "currentColor",
102
+ maskImage: COMPLETION_ICON_FALLBACK,
103
+ WebkitMaskImage: COMPLETION_ICON_FALLBACK,
104
+ maskRepeat: "no-repeat",
105
+ WebkitMaskRepeat: "no-repeat",
106
+ maskPosition: "center",
107
+ WebkitMaskPosition: "center",
108
+ maskSize: "contain",
109
+ WebkitMaskSize: "contain"
110
+ },
111
+ ...completionIconRules(),
112
+ ".cm-completionIcon-keyword": { color: colors.mauve.hex },
113
+ ".cm-completionIcon-function, .cm-completionIcon-method": { color: colors.blue.hex },
114
+ ".cm-completionIcon-variable": { color: colors.peach.hex },
115
+ ".cm-completionIcon-constant": { color: colors.sky.hex },
116
+ ".cm-completionIcon-property": { color: colors.teal.hex },
117
+ ".cm-completionIcon-namespace": { color: colors.sapphire.hex },
118
+ ".cm-completionIcon-text": { color: colors.overlay1.hex },
119
+ ".cm-completionIcon-class, .cm-completionIcon-type, .cm-completionIcon-interface, .cm-completionIcon-enum": { color: colors.yellow.hex },
120
+ ".cm-tooltip.cm-completionInfo": {
121
+ ...frame,
122
+ padding: "8px 10px",
123
+ maxWidth: "26em"
124
+ },
125
+ ".cm-tooltip-lint": {
126
+ padding: "4px",
127
+ maxWidth: "26em",
128
+ overflowWrap: "anywhere"
129
+ },
130
+ ".cm-diagnostic": {
131
+ margin: "0",
132
+ padding: "3px 8px",
133
+ borderLeftWidth: "3px",
134
+ borderRadius: "5px",
135
+ lineHeight: "1.45"
136
+ },
137
+ ".cm-diagnostic-error": { borderLeftColor: colors.red.hex },
138
+ ".cm-diagnostic-warning": { borderLeftColor: colors.yellow.hex },
139
+ ".cm-diagnostic-info": { borderLeftColor: colors.overlay1.hex },
140
+ ".cm-diagnostic-hint": { borderLeftColor: colors.blue.hex },
141
+ ".cm-diagnosticAction": {
142
+ padding: "2px 6px",
143
+ backgroundColor: colors.surface1.hex,
144
+ color: colors.text.hex,
145
+ borderRadius: "4px"
146
+ },
147
+ ".cm-diagnosticSource": {
148
+ fontSize: "85%",
149
+ color: colors.overlay1.hex,
150
+ opacity: "1"
151
+ },
152
+ "&.cm-focused": { outline: "none" },
153
+ ".cm-activeLine": { backgroundColor: alphaOf(colors.overlay0, .12) },
154
+ ".cm-activeLineGutter": { backgroundColor: alphaOf(colors.overlay0, .12) },
155
+ "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground": { backgroundColor: alphaOf(colors.blue, dark ? .35 : .25) },
156
+ ".cm-selectionBackground": { backgroundColor: alphaOf(colors.blue, dark ? .2 : .14) },
157
+ ".cm-content ::selection": { backgroundColor: alphaOf(colors.blue, dark ? .35 : .25) },
158
+ ".cm-scroller, .cm-tooltip-autocomplete > ul, .cm-tooltip.cm-completionInfo": {
159
+ scrollbarWidth: "thin",
160
+ scrollbarColor: `${alphaOf(colors.overlay0, .55)} transparent`
161
+ },
162
+ ".cm-panels.cm-panels-top": { borderBottom: hairline },
163
+ ".cm-panels.cm-panels-bottom": { borderTop: hairline },
164
+ ".cm-panel.cm-search, .cm-panel.cm-gotoLine": {
165
+ position: "relative",
166
+ display: "flex",
167
+ flexWrap: "wrap",
168
+ alignItems: "center",
169
+ columnGap: "8px",
170
+ rowGap: "8px",
171
+ margin: "0",
172
+ padding: "10px 44px 10px 14px"
173
+ },
174
+ ".cm-panel.cm-search br": {
175
+ flexBasis: "100%",
176
+ height: "0",
177
+ margin: "0",
178
+ border: "none"
179
+ },
180
+ ".cm-panel.cm-search label, .cm-panel.cm-gotoLine label": {
181
+ display: "inline-flex",
182
+ alignItems: "center",
183
+ gap: "6px",
184
+ margin: "0",
185
+ fontSize: "12.5px",
186
+ color: colors.subtext0.hex,
187
+ cursor: "pointer",
188
+ userSelect: "none"
189
+ },
190
+ ".cm-panel.cm-search input[type=checkbox]": {
191
+ margin: "0",
192
+ width: "14px",
193
+ height: "14px",
194
+ accentColor: colors.blue.hex
195
+ },
196
+ ".cm-panel.cm-search button[name=close]": {
197
+ position: "absolute",
198
+ top: "10px",
199
+ right: "10px",
200
+ boxSizing: "border-box",
201
+ width: "28px",
202
+ height: "28px",
203
+ padding: "0",
204
+ display: "inline-flex",
205
+ alignItems: "center",
206
+ justifyContent: "center",
207
+ fontSize: "16px",
208
+ lineHeight: "1",
209
+ color: colors.overlay1.hex,
210
+ background: "transparent",
211
+ border: "none",
212
+ borderRadius: "6px",
213
+ cursor: "pointer"
214
+ },
215
+ ".cm-panel.cm-search button[name=close]:hover": {
216
+ backgroundColor: colors.surface0.hex,
217
+ color: colors.text.hex
218
+ },
219
+ ".cm-panel .cm-textfield": {
220
+ boxSizing: "border-box",
221
+ height: "28px",
222
+ width: "240px",
223
+ maxWidth: "100%",
224
+ margin: "0",
225
+ padding: "0 10px",
226
+ fontSize: "12.5px",
227
+ color: colors.text.hex,
228
+ backgroundColor: colors.base.hex,
229
+ border: hairline,
230
+ borderRadius: "6px"
231
+ },
232
+ ".cm-panel .cm-textfield:focus": {
233
+ outline: "none",
234
+ borderColor: alphaOf(colors.blue, .6),
235
+ boxShadow: `0 0 0 2px ${alphaOf(colors.blue, .2)}`
236
+ },
237
+ ".cm-panel .cm-button": {
238
+ boxSizing: "border-box",
239
+ height: "28px",
240
+ margin: "0",
241
+ padding: "0 12px",
242
+ fontSize: "12.5px",
243
+ color: colors.text.hex,
244
+ backgroundImage: "none",
245
+ backgroundColor: colors.surface0.hex,
246
+ border: hairline,
247
+ borderRadius: "6px",
248
+ cursor: "pointer"
249
+ },
250
+ ".cm-panel .cm-button:hover": { backgroundColor: colors.surface1.hex },
251
+ ".cm-panel .cm-button:active": {
252
+ backgroundImage: "none",
253
+ backgroundColor: colors.surface1.hex
254
+ }
255
+ }, { dark });
256
+ }
257
+ function defaultThemeExtensions(dark, height, width, fontSize, lineHeight) {
258
+ const extensions = [Prec.low(chromeTheme(dark)), Prec.low(dark ? catppuccinMacchiato : catppuccinLatte)];
259
+ const cssHeight = toCssSize(height);
260
+ const cssWidth = toCssSize(width);
261
+ if (cssHeight !== void 0 || cssWidth !== void 0 || fontSize !== void 0 || lineHeight !== void 0) {
262
+ const root = {};
263
+ if (cssHeight !== void 0) root.height = cssHeight;
264
+ if (cssWidth !== void 0) root.width = cssWidth;
265
+ if (fontSize !== void 0) root.fontSize = `${fontSize}px`;
266
+ const spec = { "&": root };
267
+ const scroller = {};
268
+ if (cssHeight !== void 0) scroller.overflow = "auto";
269
+ if (lineHeight !== void 0) scroller.lineHeight = lineHeight < 8 ? String(lineHeight) : `${lineHeight}px`;
270
+ if (Object.keys(scroller).length > 0) spec[".cm-scroller"] = scroller;
271
+ extensions.push(EditorView.theme(spec));
272
+ }
273
+ return extensions;
274
+ }
275
+ //#endregion
276
+ //#region src/codemirror-diff-editor.tsx
277
+ function mergeTheme(dark) {
278
+ const { colors } = dark ? flavors.macchiato : flavors.latte;
279
+ const removed = colors.red;
280
+ const added = colors.green;
281
+ const underline = (color) => `linear-gradient(${alphaOf(color, .5)}, ${alphaOf(color, .5)}) bottom/100% 2px no-repeat`;
282
+ return EditorView.theme({
283
+ "&.cm-merge-a .cm-changedLine": { backgroundColor: alphaOf(removed, dark ? .14 : .09) },
284
+ "&.cm-merge-b .cm-changedLine": { backgroundColor: alphaOf(added, dark ? .14 : .09) },
285
+ "&.cm-merge-a .cm-changedText": { background: underline(removed) },
286
+ "&.cm-merge-b .cm-changedText": { background: underline(added) },
287
+ "&.cm-merge-a .cm-changedLineGutter": { background: removed.hex },
288
+ "&.cm-merge-b .cm-changedLineGutter": { background: added.hex },
289
+ ".cm-collapsedLines": {
290
+ color: colors.subtext0.hex,
291
+ background: `linear-gradient(to bottom, transparent 0, ${colors.surface0.hex} 30%, ${colors.surface0.hex} 70%, transparent 100%)`
292
+ }
293
+ }, { dark });
294
+ }
295
+ const DIFF_CHROME_STYLE_SELECTOR = "style[data-inkstone=\"codemirror-diff\"]";
296
+ function installDiffChrome(root) {
297
+ if (root.querySelector(DIFF_CHROME_STYLE_SELECTOR)) return;
298
+ const style = (root.nodeType === 9 ? root : root.host.ownerDocument).createElement("style");
299
+ style.dataset.inkstone = "codemirror-diff";
300
+ style.textContent = [
301
+ "[data-inkstone-diff] > .cm-mergeView { height: 100%; }",
302
+ "[data-inkstone-diff] .cm-merge-revert button { color: inherit; border-radius: 4px; }",
303
+ "[data-inkstone-diff] .cm-merge-revert button:hover { background-color: color-mix(in srgb, currentColor 15%, transparent); }"
304
+ ].join("\n");
305
+ if (root.nodeType === 9) root.head.append(style);
306
+ else root.append(style);
307
+ }
308
+ function editorRoot(container) {
309
+ const root = container.getRootNode();
310
+ if (root.nodeType === 9 || root.nodeType === 11 && "host" in root) return root;
311
+ return container.ownerDocument;
312
+ }
313
+ function eventElement(event) {
314
+ const target = event.target;
315
+ return target?.nodeType === 1 ? target : target?.parentElement ?? null;
316
+ }
317
+ function installDiffControlKeyboardSupport(container) {
318
+ const enhanceControls = () => {
319
+ for (const control of container.querySelectorAll(":scope .cm-collapsedLines")) {
320
+ control.setAttribute("role", "button");
321
+ control.tabIndex = 0;
322
+ }
323
+ for (const button of container.querySelectorAll(":scope .cm-merge-revert button")) button.type = "button";
324
+ };
325
+ const handleClick = (event) => {
326
+ if (event.detail !== 0) return;
327
+ const button = eventElement(event)?.closest(".cm-merge-revert button");
328
+ if (!button || !container.contains(button)) return;
329
+ event.preventDefault();
330
+ const MouseEventConstructor = container.ownerDocument.defaultView?.MouseEvent ?? MouseEvent;
331
+ button.dispatchEvent(new MouseEventConstructor("mousedown", {
332
+ bubbles: true,
333
+ button: 0,
334
+ cancelable: true
335
+ }));
336
+ };
337
+ const handleKeyDown = (event) => {
338
+ if (event.key !== "Enter" && event.key !== " ") return;
339
+ const control = eventElement(event)?.closest(".cm-collapsedLines");
340
+ if (!control || !container.contains(control)) return;
341
+ event.preventDefault();
342
+ event.stopPropagation();
343
+ control.click();
344
+ };
345
+ enhanceControls();
346
+ const observer = new MutationObserver(enhanceControls);
347
+ observer.observe(container, {
348
+ childList: true,
349
+ subtree: true
350
+ });
351
+ container.addEventListener("click", handleClick);
352
+ container.addEventListener("keydown", handleKeyDown, { capture: true });
353
+ return () => {
354
+ observer.disconnect();
355
+ container.removeEventListener("click", handleClick);
356
+ container.removeEventListener("keydown", handleKeyDown, { capture: true });
357
+ };
358
+ }
359
+ function CodeMirrorDiffEditor({ original, modified, onChange, language, readOnly = false, revertControls = false, collapseUnchanged = false, lineWrapping = false, showLineNumbers = true, extensions, dynamicExtensions, dark = false, height, width, fontSize = 14, lineHeight = 1.6, className, originalAriaLabel = "Original content", modifiedAriaLabel = "Modified content", onMount }) {
360
+ const containerRef = useRef(null);
361
+ const hostRef = useRef(null);
362
+ const modifiedTransitionRef = useRef(false);
363
+ const [reconcileRevision, requestControlledReconcile] = useControlledReconcileSignal();
364
+ const onChangeRef = useLatest(onChange);
365
+ const onMountRef = useLatest(onMount);
366
+ const [loadedLanguage, setLoadedLanguage] = useState(null);
367
+ const languageExtension = useMemo(() => loadedLanguage && loadedLanguage.id === language ? [loadedLanguage.extension] : [], [language, loadedLanguage]);
368
+ const activeRevertControls = revertControls && !readOnly;
369
+ useEffect(() => {
370
+ if (!language) return;
371
+ let cancelled = false;
372
+ loadLanguage(language).then((extension) => {
373
+ if (!cancelled) setLoadedLanguage({
374
+ id: language,
375
+ extension
376
+ });
377
+ }).catch((error) => {
378
+ console.error("inkstone: failed to load CodeMirror language", language, error);
379
+ if (!cancelled) setLoadedLanguage(null);
380
+ });
381
+ return () => {
382
+ cancelled = true;
383
+ };
384
+ }, [language]);
385
+ const dynamic = useMemo(() => [
386
+ ...defaultThemeExtensions(dark, void 0, void 0, fontSize, lineHeight),
387
+ mergeTheme(dark),
388
+ ...showLineNumbers ? [lineNumbers()] : [],
389
+ ...lineWrapping ? [EditorView.lineWrapping] : [],
390
+ ...readOnly ? [EditorState.readOnly.of(true), EditorView.editable.of(false)] : [],
391
+ ...languageExtension,
392
+ ...dynamicExtensions ?? []
393
+ ], [
394
+ dark,
395
+ fontSize,
396
+ lineHeight,
397
+ showLineNumbers,
398
+ lineWrapping,
399
+ readOnly,
400
+ languageExtension,
401
+ dynamicExtensions
402
+ ]);
403
+ useEffect(() => {
404
+ const container = containerRef.current;
405
+ if (!container) return;
406
+ const root = editorRoot(container);
407
+ installDiffChrome(root);
408
+ const host = new ControlledMergeHost({
409
+ parent: container,
410
+ root,
411
+ original,
412
+ modified,
413
+ onModifiedChange: (next) => {
414
+ if (modifiedTransitionRef.current) return;
415
+ onChangeRef.current?.(next);
416
+ requestControlledReconcile();
417
+ },
418
+ extensions: [...documentExtensions(), ...extensions ?? []],
419
+ originalExtensions: [
420
+ EditorState.readOnly.of(true),
421
+ EditorView.editable.of(false),
422
+ EditorView.contentAttributes.of({ "aria-label": originalAriaLabel })
423
+ ],
424
+ modifiedExtensions: [EditorView.contentAttributes.of({ "aria-label": modifiedAriaLabel })],
425
+ dynamicExtensions: dynamic,
426
+ revertControls: activeRevertControls ? "a-to-b" : void 0,
427
+ collapseUnchanged: collapseUnchanged ? {} : void 0
428
+ });
429
+ hostRef.current = host;
430
+ const removeDiffControlKeyboardSupport = installDiffControlKeyboardSupport(container);
431
+ onMountRef.current?.(host.view);
432
+ return () => {
433
+ removeDiffControlKeyboardSupport();
434
+ host.destroy();
435
+ hostRef.current = null;
436
+ };
437
+ }, []);
438
+ useInsertionEffect(() => {
439
+ const host = hostRef.current;
440
+ modifiedTransitionRef.current = host !== null && host.view.b.state.doc.toString() !== modified;
441
+ }, [modified, reconcileRevision]);
442
+ useLayoutEffect(() => {
443
+ hostRef.current?.setOriginal(original);
444
+ }, [original]);
445
+ useLayoutEffect(() => {
446
+ try {
447
+ hostRef.current?.setModified(modified);
448
+ } finally {
449
+ modifiedTransitionRef.current = false;
450
+ }
451
+ }, [modified, reconcileRevision]);
452
+ useEffect(() => {
453
+ hostRef.current?.reconfigure(dynamic);
454
+ }, [dynamic]);
455
+ useEffect(() => {
456
+ hostRef.current?.view.reconfigure({
457
+ revertControls: activeRevertControls ? "a-to-b" : void 0,
458
+ collapseUnchanged: collapseUnchanged ? {} : void 0
459
+ });
460
+ }, [activeRevertControls, collapseUnchanged]);
461
+ return /* @__PURE__ */ jsx("div", {
462
+ ref: containerRef,
463
+ className,
464
+ "data-inkstone-diff": "",
465
+ style: {
466
+ height: toCssSize(height),
467
+ width: toCssSize(width),
468
+ color: (dark ? flavors.macchiato : flavors.latte).colors.overlay1.hex
469
+ }
470
+ });
471
+ }
472
+ //#endregion
473
+ //#region src/codemirror-editor.tsx
474
+ function CodeMirrorEditor({ value, onChange, language, context, sqlSchema, placeholder, lineWrapping = false, showLineNumbers = false, folding = false, spellcheck = false, search = false, extensions, dynamicExtensions, dark = false, locale = "en", height, width, fontSize = 14, lineHeight = 1.6, tabSize, className, onMount, onFocus, onBlur, ariaLabel = "Editor content" }) {
475
+ const containerRef = useRef(null);
476
+ const hostRef = useRef(null);
477
+ const valueTransitionRef = useRef(false);
478
+ const [reconcileRevision, requestControlledReconcile] = useControlledReconcileSignal();
479
+ const onChangeRef = useLatest(onChange);
480
+ const onMountRef = useLatest(onMount);
481
+ const onFocusRef = useLatest(onFocus);
482
+ const onBlurRef = useLatest(onBlur);
483
+ const lastContextKeyRef = useRef(null);
484
+ const lastSqlSchemaKeyRef = useRef(null);
485
+ const [loadedLanguage, setLoadedLanguage] = useState(null);
486
+ const languageExtension = useMemo(() => loadedLanguage && loadedLanguage.id === language ? [loadedLanguage.extension] : [], [language, loadedLanguage]);
487
+ useEffect(() => {
488
+ if (!language) return;
489
+ let cancelled = false;
490
+ loadLanguage(language).then((extension) => {
491
+ if (!cancelled) setLoadedLanguage({
492
+ id: language,
493
+ extension
494
+ });
495
+ }).catch((error) => {
496
+ console.error("inkstone: failed to load CodeMirror language", language, error);
497
+ if (!cancelled) setLoadedLanguage(null);
498
+ });
499
+ return () => {
500
+ cancelled = true;
501
+ };
502
+ }, [language]);
503
+ const dynamic = useMemo(() => [
504
+ ...defaultThemeExtensions(dark, height, width, fontSize, lineHeight),
505
+ ...tabSize === void 0 ? [] : [EditorState.tabSize.of(tabSize), indentUnit.of(" ".repeat(tabSize))],
506
+ ...locale === "zh-cn" ? [searchPhrasesZhCn] : [],
507
+ ...languageExtension,
508
+ EditorView.contentAttributes.of({ "aria-label": ariaLabel }),
509
+ ...dynamicExtensions ?? []
510
+ ], [
511
+ dark,
512
+ height,
513
+ width,
514
+ fontSize,
515
+ lineHeight,
516
+ tabSize,
517
+ locale,
518
+ languageExtension,
519
+ ariaLabel,
520
+ dynamicExtensions
521
+ ]);
522
+ const contextSchema = useMemo(() => context ? normalizeContext(context) : null, [context]);
523
+ useEffect(() => {
524
+ const container = containerRef.current;
525
+ if (!container) return;
526
+ const host = new ControlledEditorHost({
527
+ parent: container,
528
+ doc: value,
529
+ onChange: (next) => {
530
+ if (valueTransitionRef.current) return;
531
+ onChangeRef.current?.(next);
532
+ requestControlledReconcile();
533
+ },
534
+ extensions: [
535
+ ...documentExtensions({
536
+ placeholder,
537
+ lineWrapping,
538
+ showLineNumbers,
539
+ folding,
540
+ spellcheck,
541
+ search
542
+ }),
543
+ EditorView.updateListener.of((update) => {
544
+ if (update.focusChanged) if (update.view.hasFocus) onFocusRef.current?.();
545
+ else onBlurRef.current?.();
546
+ }),
547
+ ...extensions ?? []
548
+ ],
549
+ dynamicExtensions: dynamic
550
+ });
551
+ hostRef.current = host;
552
+ onMountRef.current?.(host.view);
553
+ return () => {
554
+ host.destroy();
555
+ hostRef.current = null;
556
+ };
557
+ }, []);
558
+ useInsertionEffect(() => {
559
+ const host = hostRef.current;
560
+ valueTransitionRef.current = host !== null && host.view.state.doc.toString() !== value;
561
+ }, [reconcileRevision, value]);
562
+ useLayoutEffect(() => {
563
+ try {
564
+ hostRef.current?.setValue(value);
565
+ } finally {
566
+ valueTransitionRef.current = false;
567
+ }
568
+ }, [reconcileRevision, value]);
569
+ useEffect(() => {
570
+ hostRef.current?.reconfigure(dynamic);
571
+ }, [dynamic]);
572
+ useEffect(() => {
573
+ const host = hostRef.current;
574
+ if (!host) return;
575
+ const key = contextSchema ? JSON.stringify(contextSchema) : "";
576
+ const last = lastContextKeyRef.current;
577
+ if (last?.host === host && last.key === key) return;
578
+ lastContextKeyRef.current = {
579
+ host,
580
+ key
581
+ };
582
+ if (key === "" && last?.host !== host) return;
583
+ host.view.dispatch({ effects: setMinijinjaContext.of(contextSchema) });
584
+ }, [contextSchema]);
585
+ useEffect(() => {
586
+ const host = hostRef.current;
587
+ if (!host) return;
588
+ const key = sqlSchema ? JSON.stringify(sqlSchema) : "";
589
+ const last = lastSqlSchemaKeyRef.current;
590
+ if (last?.host === host && last.key === key) return;
591
+ lastSqlSchemaKeyRef.current = {
592
+ host,
593
+ key
594
+ };
595
+ if (key === "" && last?.host !== host) return;
596
+ host.view.dispatch({ effects: setSqlSchema.of(sqlSchema ?? null) });
597
+ }, [sqlSchema]);
598
+ return /* @__PURE__ */ jsx("div", {
599
+ ref: containerRef,
600
+ className
601
+ });
602
+ }
603
+ //#endregion
604
+ export { CodeMirrorDiffEditor as a, CodeMirrorEditor as i, normalizeContext$1 as n, searchPhrasesZhCn$1 as r, codeMirrorLanguages as t };