@hanzo/ui 8.27.57 → 8.27.59
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/dist/backends/gui/code-editor.cjs +374 -71
- package/dist/backends/gui/code-editor.d.ts +46 -25
- package/dist/backends/gui/code-editor.d.ts.map +1 -1
- package/dist/backends/gui/code-editor.js +376 -73
- package/dist/backends/gui/code-editor.js.map +1 -1
- package/dist/backends/gui/index.cjs +9 -4
- package/dist/backends/gui/index.d.ts +3 -1
- package/dist/backends/gui/index.d.ts.map +1 -1
- package/dist/backends/gui/index.js +2 -0
- package/dist/backends/gui/index.js.map +1 -1
- package/dist/backends/gui/json-tree.cjs +135 -0
- package/dist/backends/gui/json-tree.d.ts +26 -0
- package/dist/backends/gui/json-tree.d.ts.map +1 -0
- package/dist/backends/gui/json-tree.js +100 -0
- package/dist/backends/gui/json-tree.js.map +1 -0
- package/dist/backends/gui/json.cjs +183 -0
- package/dist/backends/gui/json.d.ts +41 -0
- package/dist/backends/gui/json.d.ts.map +1 -0
- package/dist/backends/gui/json.js +180 -0
- package/dist/backends/gui/json.js.map +1 -0
- package/dist/gallery.html +7 -5
- package/dist/index.cjs +8 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.node.cjs +8 -4
- package/dist/index.node.js +1 -1
- package/dist/primitives/JsonTree.cjs +8 -0
- package/dist/primitives/JsonTree.d.ts +2 -0
- package/dist/primitives/JsonTree.d.ts.map +1 -0
- package/dist/primitives/JsonTree.js +5 -0
- package/dist/primitives/JsonTree.js.map +1 -0
- package/dist/primitives/Picker.cjs +8 -0
- package/dist/primitives/Picker.d.ts +2 -0
- package/dist/primitives/Picker.d.ts.map +1 -0
- package/dist/primitives/Picker.js +5 -0
- package/dist/primitives/Picker.js.map +1 -0
- package/dist/primitives/checkJson.cjs +8 -0
- package/dist/primitives/checkJson.d.ts +2 -0
- package/dist/primitives/checkJson.d.ts.map +1 -0
- package/dist/primitives/checkJson.js +5 -0
- package/dist/primitives/checkJson.js.map +1 -0
- package/dist/primitives/formatJson.cjs +8 -0
- package/dist/primitives/formatJson.d.ts +2 -0
- package/dist/primitives/formatJson.d.ts.map +1 -0
- package/dist/primitives/formatJson.js +5 -0
- package/dist/primitives/formatJson.js.map +1 -0
- package/dist/product/limits.cjs +10 -1
- package/dist/product/limits.d.ts.map +1 -1
- package/dist/product/limits.js +10 -1
- package/dist/product/limits.js.map +1 -1
- package/dist/styles.css +4 -2
- package/package.json +7 -1
|
@@ -37,26 +37,41 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
37
37
|
exports.CodeEditor = CodeEditor;
|
|
38
38
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
39
39
|
/**
|
|
40
|
-
* CodeEditor — a
|
|
41
|
-
* language menu
|
|
42
|
-
*
|
|
40
|
+
* CodeEditor — a CodeMirror 6 editor in a gui frame: a toolbar with the
|
|
41
|
+
* language (a menu, or a label), Format for JSON and Copy; the editor with a
|
|
42
|
+
* line-number gutter; and a footer that reports whether JSON parses and is the
|
|
43
|
+
* handle that resizes the editor.
|
|
43
44
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
45
|
+
* Height. With no `height` the editor is as tall as its text, from `minHeight`
|
|
46
|
+
* up to `maxHeight`, and scrolls past that. Dragging the footer (or ArrowUp /
|
|
47
|
+
* ArrowDown on it) sets a height of the reader's own, past the cap if they
|
|
48
|
+
* want; a double-click hands the height back to the text. A `height` fixes it,
|
|
49
|
+
* and `'100%'` fills a frame that is itself sized (a flex child).
|
|
49
50
|
*
|
|
50
|
-
* `
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
51
|
+
* JSON. `language="json"` colours the syntax and checks the text on every
|
|
52
|
+
* change with `checkJson`: a broken document tints the failing line, marks the
|
|
53
|
+
* failing character and names line, column and reason in the footer. With
|
|
54
|
+
* `allowText`, text that does not open an object or an array is plain text and
|
|
55
|
+
* passes. Format (or Shift-Alt-F) re-indents valid JSON by two spaces. Other
|
|
56
|
+
* languages are edited as plain text: the package carries one grammar.
|
|
57
|
+
*
|
|
58
|
+
* Colour. Every colour is a theme rung over a design token — the ink, the
|
|
59
|
+
* hairline, the selection and the error state — so the editor follows the page
|
|
60
|
+
* into light or dark with nothing to configure. Hanzo is monochrome, so the syntax is ranked
|
|
61
|
+
* by ink and weight, not by hue: property names in full ink, strings one rank
|
|
62
|
+
* down, punctuation at the rank of a hint. Hue is spent only on an error.
|
|
54
63
|
*/
|
|
55
64
|
const gui_1 = require("@hanzo/gui");
|
|
56
65
|
const lucide_icons_2_1 = require("@hanzogui/lucide-icons-2");
|
|
66
|
+
const commands_1 = require("@codemirror/commands");
|
|
67
|
+
const lang_json_1 = require("@codemirror/lang-json");
|
|
68
|
+
const language_1 = require("@codemirror/language");
|
|
69
|
+
const state_1 = require("@codemirror/state");
|
|
70
|
+
const view_1 = require("@codemirror/view");
|
|
71
|
+
const highlight_1 = require("@lezer/highlight");
|
|
57
72
|
const React = __importStar(require("react"));
|
|
58
73
|
const button_1 = require("./button.cjs");
|
|
59
|
-
const
|
|
74
|
+
const json_1 = require("./json.cjs");
|
|
60
75
|
const select_1 = require("./select.cjs");
|
|
61
76
|
const slot_1 = require("./slot.cjs");
|
|
62
77
|
const toaster_1 = require("./toaster.cjs");
|
|
@@ -113,53 +128,332 @@ const LABEL = {
|
|
|
113
128
|
shell: 'Shell',
|
|
114
129
|
plaintext: 'Plain Text',
|
|
115
130
|
};
|
|
116
|
-
/** The two named palettes; `'auto'` paints none of this. */
|
|
117
|
-
const PALETTE = {
|
|
118
|
-
light: { bg: '#ffffff', fg: '#1f2328', border: '#d0d7de', head: '#f6f8fa', gutter: '#8c959f' },
|
|
119
|
-
dark: { bg: '#1e1e1e', fg: '#d4d4d4', border: '#3c3c3c', head: '#252526', gutter: '#858585' },
|
|
120
|
-
};
|
|
121
|
-
/** What Tab inserts. */
|
|
122
|
-
const INDENT = ' ';
|
|
123
131
|
/** Line height as a multiple of the font size. */
|
|
124
|
-
const LEADING = 1.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
132
|
+
const LEADING = 1.6;
|
|
133
|
+
/** How far one ArrowUp / ArrowDown on the footer moves the height. */
|
|
134
|
+
const STEP = 24;
|
|
135
|
+
const MONO = "var(--font-mono, 'Zen Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace)";
|
|
136
|
+
/**
|
|
137
|
+
* The editor's surface. Every colour is one of the theme's named rungs — gui emits
|
|
138
|
+
* each as a custom property on the theme class, so it inverts with the page — and
|
|
139
|
+
* those rungs read @hanzo/design's tokens (`--ink` is `--foreground`, `--edge` is
|
|
140
|
+
* `--border`, `--bad` is `--state-error`). The design ladder is NOT read directly:
|
|
141
|
+
* on a host that mounts design's sheet without its light selector, `--text-primary`
|
|
142
|
+
* stays the dark theme's white on a light page.
|
|
143
|
+
*/
|
|
144
|
+
const FRAME = /* @__PURE__ */ view_1.EditorView.theme({
|
|
145
|
+
'&': { color: 'var(--ink)', backgroundColor: 'transparent' },
|
|
146
|
+
'&.cm-focused': { outline: 'none' },
|
|
147
|
+
// The scroller fills what the editor is given and no more: grown with its text up to the
|
|
148
|
+
// editor's max-height, then scrolling; stretched to a fixed or dragged height. CodeMirror's
|
|
149
|
+
// own `height: 100%` resolves against an auto-height editor as its min-height, which pinned
|
|
150
|
+
// a growing editor at its floor.
|
|
151
|
+
'.cm-scroller': {
|
|
152
|
+
fontFamily: MONO,
|
|
153
|
+
lineHeight: String(LEADING),
|
|
154
|
+
overflow: 'auto',
|
|
155
|
+
flex: '1 1 auto',
|
|
156
|
+
height: 'auto',
|
|
157
|
+
minHeight: '0',
|
|
158
|
+
},
|
|
159
|
+
'.cm-content': { padding: '8px 0', caretColor: 'var(--ink)' },
|
|
160
|
+
'.cm-line': { padding: '0 12px' },
|
|
161
|
+
'.cm-gutters': {
|
|
162
|
+
backgroundColor: 'transparent',
|
|
163
|
+
color: 'var(--dim)',
|
|
164
|
+
border: 'none',
|
|
165
|
+
borderRight: '1px solid var(--edge)',
|
|
166
|
+
},
|
|
167
|
+
'.cm-lineNumbers .cm-gutterElement': { padding: '0 10px 0 12px', minWidth: '28px' },
|
|
168
|
+
'.cm-activeLine': { backgroundColor: 'var(--panel)' },
|
|
169
|
+
'.cm-activeLineGutter': { backgroundColor: 'transparent', color: 'var(--quiet)' },
|
|
170
|
+
'.cm-cursor, .cm-dropCursor': { borderLeftColor: 'var(--ink)' },
|
|
171
|
+
'.cm-selectionBackground, &.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, ::selection': {
|
|
172
|
+
backgroundColor: 'var(--raised)',
|
|
173
|
+
},
|
|
174
|
+
'.cm-matchingBracket, &.cm-focused .cm-matchingBracket': {
|
|
175
|
+
backgroundColor: 'var(--raised)',
|
|
176
|
+
color: 'inherit',
|
|
177
|
+
outline: 'none',
|
|
178
|
+
},
|
|
179
|
+
'.cm-nonmatchingBracket, &.cm-focused .cm-nonmatchingBracket': { backgroundColor: 'transparent' },
|
|
180
|
+
'.cm-placeholder': { color: 'var(--dim)' },
|
|
181
|
+
'.cm-fault': { backgroundColor: 'var(--state-error-bg, rgb(239 68 68 / .1))' },
|
|
182
|
+
'.cm-gutterElement.cm-fault': { color: 'var(--bad)' },
|
|
183
|
+
'.cm-fault-at': {
|
|
184
|
+
textDecoration: 'underline wavy var(--bad)',
|
|
185
|
+
textDecorationSkipInk: 'none',
|
|
186
|
+
textUnderlineOffset: '3px',
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
/** The syntax, ranked by ink: names in full ink, strings a rank down, punctuation at a hint. */
|
|
190
|
+
const INK = /* @__PURE__ */ language_1.HighlightStyle.define([
|
|
191
|
+
{ tag: highlight_1.tags.propertyName, color: 'var(--ink)', fontWeight: '500' },
|
|
192
|
+
{ tag: highlight_1.tags.string, color: 'var(--soft)' },
|
|
193
|
+
{ tag: [highlight_1.tags.number, highlight_1.tags.bool, highlight_1.tags.null], color: 'var(--ink)' },
|
|
194
|
+
{ tag: [highlight_1.tags.punctuation, highlight_1.tags.separator, highlight_1.tags.brace, highlight_1.tags.squareBracket], color: 'var(--dim)' },
|
|
195
|
+
]);
|
|
196
|
+
/** How much deeper than its line a wrapped continuation starts, in characters. */
|
|
197
|
+
const HANG = 2;
|
|
198
|
+
/** A line's own indentation in characters, a tab counted as two. */
|
|
199
|
+
const indentOf = (text) => {
|
|
200
|
+
let n = 0;
|
|
201
|
+
for (const c of text) {
|
|
202
|
+
if (c === ' ')
|
|
203
|
+
n += 1;
|
|
204
|
+
else if (c === '\t')
|
|
205
|
+
n += 2;
|
|
206
|
+
else
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
return n;
|
|
210
|
+
};
|
|
211
|
+
const hangs = new Map();
|
|
212
|
+
const hangFor = (n) => {
|
|
213
|
+
let d = hangs.get(n);
|
|
214
|
+
if (!d) {
|
|
215
|
+
d = view_1.Decoration.line({ attributes: { style: `padding-left: calc(${n}ch + 12px); text-indent: -${n}ch` } });
|
|
216
|
+
hangs.set(n, d);
|
|
217
|
+
}
|
|
218
|
+
return d;
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* Wrapped lines hang under their own text rather than restarting at the margin, so a
|
|
222
|
+
* long string inside nested JSON still reads as belonging to its key. A line's padding
|
|
223
|
+
* is its indentation plus the hang, and a negative text-indent of the same width puts
|
|
224
|
+
* its first row back where it was.
|
|
225
|
+
*/
|
|
226
|
+
const hang = /* @__PURE__ */ view_1.ViewPlugin.fromClass(class {
|
|
227
|
+
decorations;
|
|
228
|
+
constructor(view) {
|
|
229
|
+
this.decorations = this.build(view);
|
|
230
|
+
}
|
|
231
|
+
update(u) {
|
|
232
|
+
if (u.docChanged || u.viewportChanged)
|
|
233
|
+
this.decorations = this.build(u.view);
|
|
234
|
+
}
|
|
235
|
+
build(view) {
|
|
236
|
+
const out = new state_1.RangeSetBuilder();
|
|
237
|
+
for (const { from, to } of view.visibleRanges) {
|
|
238
|
+
for (let pos = from; pos <= to;) {
|
|
239
|
+
const line = view.state.doc.lineAt(pos);
|
|
240
|
+
out.add(line.from, line.from, hangFor(indentOf(line.text) + HANG));
|
|
241
|
+
pos = line.to + 1;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return out.finish();
|
|
245
|
+
}
|
|
246
|
+
}, { decorations: (v) => v.decorations });
|
|
247
|
+
/** Marks a programmatic edit, so a caller's own `value` is never echoed back to `onChange`. */
|
|
248
|
+
const External = /* @__PURE__ */ state_1.Annotation.define();
|
|
249
|
+
class Fault extends view_1.GutterMarker {
|
|
250
|
+
elementClass = 'cm-fault';
|
|
251
|
+
}
|
|
252
|
+
const FAULT_GUTTER = /* @__PURE__ */ new Fault();
|
|
253
|
+
const FAULT_LINE = /* @__PURE__ */ view_1.Decoration.line({ class: 'cm-fault' });
|
|
254
|
+
const FAULT_AT = /* @__PURE__ */ view_1.Decoration.mark({ class: 'cm-fault-at' });
|
|
255
|
+
function judge(state, text) {
|
|
256
|
+
const check = (0, json_1.checkJson)(state.doc.toString(), { text });
|
|
257
|
+
if (check.kind !== 'error')
|
|
258
|
+
return { check, marks: view_1.Decoration.none, gutter: state_1.RangeSet.empty };
|
|
259
|
+
const at = Math.min(check.at, state.doc.length);
|
|
260
|
+
const line = state.doc.lineAt(at);
|
|
261
|
+
const end = Math.min(at + 1, line.to);
|
|
262
|
+
const ranges = [FAULT_LINE.range(line.from), ...(end > at ? [FAULT_AT.range(at, end)] : [])];
|
|
263
|
+
return { check, marks: view_1.Decoration.set(ranges, true), gutter: state_1.RangeSet.of([FAULT_GUTTER.range(line.from)]) };
|
|
264
|
+
}
|
|
265
|
+
/** The JSON verdict as editor state: recomputed on every edit, drawn as decorations. */
|
|
266
|
+
const verdict = (text) => state_1.StateField.define({
|
|
267
|
+
create: (state) => judge(state, text),
|
|
268
|
+
update: (value, tr) => (tr.docChanged ? judge(tr.state, text) : value),
|
|
269
|
+
provide: (field) => [
|
|
270
|
+
view_1.EditorView.decorations.from(field, (v) => v.marks),
|
|
271
|
+
view_1.gutterLineClass.from(field, (v) => v.gutter),
|
|
272
|
+
],
|
|
273
|
+
});
|
|
274
|
+
const px = (v) => (typeof v === 'number' ? `${v}px` : v);
|
|
275
|
+
function CodeEditor({ value, defaultValue = '', language = 'javascript', height, minHeight = 96, maxHeight = 480, resizable = height === undefined, allowText = false, onChange, onCheck, onMount, readOnly = false, lineNumbers = true, wordWrap = 'on', fontSize = 14, placeholder, showCopyButton = true, showFormatButton, showLanguageSelector = true, availableLanguages = LANGUAGES, label = 'Code', ...props }) {
|
|
128
276
|
const [selected, setSelected] = React.useState(language);
|
|
277
|
+
const [text, setText] = React.useState(value ?? defaultValue);
|
|
278
|
+
const [check, setCheck] = React.useState(null);
|
|
129
279
|
const [copied, setCopied] = React.useState(false);
|
|
130
|
-
const [
|
|
131
|
-
const
|
|
280
|
+
const [dragged, setDragged] = React.useState(null);
|
|
281
|
+
const host = React.useRef(null);
|
|
282
|
+
const grip = React.useRef(null);
|
|
283
|
+
const view = React.useRef(null);
|
|
132
284
|
const timer = React.useRef(undefined);
|
|
133
|
-
React.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
285
|
+
const listeners = React.useRef({ onChange, onCheck });
|
|
286
|
+
listeners.current = { onChange, onCheck };
|
|
287
|
+
const compartments = React.useMemo(() => ({ language: new state_1.Compartment(), gutter: new state_1.Compartment(), wrap: new state_1.Compartment(), edit: new state_1.Compartment() }), []);
|
|
288
|
+
const isJson = selected === 'json';
|
|
289
|
+
const field = React.useMemo(() => verdict(allowText), [allowText]);
|
|
290
|
+
const judged = React.useRef({ field });
|
|
291
|
+
judged.current.field = field;
|
|
292
|
+
const grammar = () => (isJson ? [(0, lang_json_1.json)(), field] : []);
|
|
293
|
+
const gutter = () => (lineNumbers ? [(0, view_1.lineNumbers)(), (0, view_1.highlightActiveLineGutter)()] : []);
|
|
294
|
+
const wrap = () => (wordWrap === 'off' ? [] : [view_1.EditorView.lineWrapping, hang]);
|
|
295
|
+
const edit = () => [state_1.EditorState.readOnly.of(readOnly), view_1.EditorView.editable.of(!readOnly)];
|
|
296
|
+
/** Report the JSON verdict when it changed (an edit, or a new grammar). */
|
|
297
|
+
const report = (state) => {
|
|
298
|
+
const v = state.field(judged.current.field, false);
|
|
299
|
+
if (v === judged.current.seen)
|
|
300
|
+
return;
|
|
301
|
+
judged.current.seen = v;
|
|
302
|
+
setCheck(v ? v.check : null);
|
|
303
|
+
if (v)
|
|
304
|
+
listeners.current.onCheck?.(v.check);
|
|
305
|
+
};
|
|
306
|
+
const format = React.useCallback(() => {
|
|
307
|
+
const v = view.current;
|
|
308
|
+
if (!v || v.state.readOnly)
|
|
309
|
+
return false;
|
|
310
|
+
const current = v.state.doc.toString();
|
|
311
|
+
const next = (0, json_1.formatJson)(current);
|
|
312
|
+
if (next === null || next === current)
|
|
313
|
+
return false;
|
|
314
|
+
v.dispatch({ changes: { from: 0, to: current.length, insert: next } });
|
|
315
|
+
return true;
|
|
137
316
|
}, []);
|
|
138
|
-
// Bound directly rather than through the gui field's `onScroll` prop: native
|
|
139
|
-
// `scroll` does not bubble, and a listener attached this way fires however
|
|
140
|
-
// the offset changed — a real drag or a test stating it outright.
|
|
141
317
|
React.useEffect(() => {
|
|
142
|
-
const
|
|
143
|
-
if (!
|
|
318
|
+
const parent = host.current;
|
|
319
|
+
if (!parent)
|
|
144
320
|
return;
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
321
|
+
const listen = view_1.EditorView.updateListener.of((update) => {
|
|
322
|
+
if (update.docChanged && !update.transactions.some((tr) => tr.annotation(External))) {
|
|
323
|
+
const next = update.state.doc.toString();
|
|
324
|
+
setText(next);
|
|
325
|
+
listeners.current.onChange?.(next);
|
|
326
|
+
}
|
|
327
|
+
report(update.state);
|
|
328
|
+
});
|
|
329
|
+
const editor = new view_1.EditorView({
|
|
330
|
+
parent,
|
|
331
|
+
state: state_1.EditorState.create({
|
|
332
|
+
doc: value ?? defaultValue,
|
|
333
|
+
extensions: [
|
|
334
|
+
compartments.gutter.of(gutter()),
|
|
335
|
+
(0, commands_1.history)(),
|
|
336
|
+
(0, view_1.drawSelection)(),
|
|
337
|
+
(0, language_1.indentOnInput)(),
|
|
338
|
+
(0, language_1.bracketMatching)(),
|
|
339
|
+
(0, view_1.highlightActiveLine)(),
|
|
340
|
+
(0, language_1.syntaxHighlighting)(INK),
|
|
341
|
+
view_1.keymap.of([
|
|
342
|
+
{ key: 'Shift-Alt-f', run: format },
|
|
343
|
+
commands_1.indentWithTab,
|
|
344
|
+
...commands_1.defaultKeymap,
|
|
345
|
+
...commands_1.historyKeymap,
|
|
346
|
+
{
|
|
347
|
+
key: 'Escape',
|
|
348
|
+
run: (v) => {
|
|
349
|
+
v.contentDOM.blur();
|
|
350
|
+
return true;
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
]),
|
|
354
|
+
compartments.language.of(grammar()),
|
|
355
|
+
compartments.wrap.of(wrap()),
|
|
356
|
+
compartments.edit.of(edit()),
|
|
357
|
+
placeholder ? (0, view_1.placeholder)(placeholder) : [],
|
|
358
|
+
view_1.EditorView.contentAttributes.of({ 'aria-label': label, 'data-slot': 'code-editor-content' }),
|
|
359
|
+
FRAME,
|
|
360
|
+
listen,
|
|
361
|
+
],
|
|
362
|
+
}),
|
|
363
|
+
});
|
|
364
|
+
view.current = editor;
|
|
365
|
+
report(editor.state);
|
|
366
|
+
onMount?.(editor);
|
|
367
|
+
return () => {
|
|
368
|
+
editor.destroy();
|
|
369
|
+
view.current = null;
|
|
370
|
+
clearTimeout(timer.current);
|
|
371
|
+
};
|
|
148
372
|
}, []);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
373
|
+
// Props that reshape the editor reach it as a reconfiguration, not a remount,
|
|
374
|
+
// so the caret, the selection and the undo history survive.
|
|
375
|
+
React.useEffect(() => {
|
|
376
|
+
view.current?.dispatch({
|
|
377
|
+
effects: [
|
|
378
|
+
compartments.language.reconfigure(grammar()),
|
|
379
|
+
compartments.gutter.reconfigure(gutter()),
|
|
380
|
+
compartments.wrap.reconfigure(wrap()),
|
|
381
|
+
compartments.edit.reconfigure(edit()),
|
|
382
|
+
],
|
|
383
|
+
});
|
|
384
|
+
}, [isJson, field, lineNumbers, wordWrap, readOnly]);
|
|
385
|
+
// A controlled value that moved out from under the editor replaces the text.
|
|
386
|
+
React.useEffect(() => {
|
|
387
|
+
const v = view.current;
|
|
388
|
+
if (!v || value === undefined)
|
|
158
389
|
return;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
390
|
+
const current = v.state.doc.toString();
|
|
391
|
+
if (value === current)
|
|
392
|
+
return;
|
|
393
|
+
v.dispatch({ changes: { from: 0, to: current.length, insert: value }, annotations: External.of(true) });
|
|
394
|
+
setText(value);
|
|
395
|
+
}, [value]);
|
|
396
|
+
React.useEffect(() => {
|
|
397
|
+
const dom = view.current?.dom;
|
|
398
|
+
if (!dom)
|
|
399
|
+
return;
|
|
400
|
+
dom.style.fontSize = `${fontSize}px`;
|
|
401
|
+
const fixed = dragged !== null ? `${dragged}px` : height !== undefined ? px(height) : '';
|
|
402
|
+
dom.style.height = fixed;
|
|
403
|
+
dom.style.minHeight = fixed ? '' : `${minHeight}px`;
|
|
404
|
+
dom.style.maxHeight = fixed ? '' : `${maxHeight}px`;
|
|
405
|
+
view.current?.requestMeasure();
|
|
406
|
+
}, [dragged, height, minHeight, maxHeight, fontSize]);
|
|
407
|
+
React.useEffect(() => {
|
|
408
|
+
const el = grip.current;
|
|
409
|
+
if (!el || !resizable)
|
|
410
|
+
return;
|
|
411
|
+
let pointer = -1;
|
|
412
|
+
let startY = 0;
|
|
413
|
+
let startH = 0;
|
|
414
|
+
const current = () => view.current?.dom.getBoundingClientRect().height ?? minHeight;
|
|
415
|
+
const down = (e) => {
|
|
416
|
+
if (e.button !== 0)
|
|
417
|
+
return;
|
|
418
|
+
e.preventDefault();
|
|
419
|
+
pointer = e.pointerId;
|
|
420
|
+
startY = e.clientY;
|
|
421
|
+
startH = current();
|
|
422
|
+
el.setPointerCapture?.(pointer);
|
|
423
|
+
};
|
|
424
|
+
const move = (e) => {
|
|
425
|
+
if (e.pointerId !== pointer)
|
|
426
|
+
return;
|
|
427
|
+
setDragged(Math.max(minHeight, Math.round(startH + e.clientY - startY)));
|
|
428
|
+
};
|
|
429
|
+
const up = (e) => {
|
|
430
|
+
if (e.pointerId !== pointer)
|
|
431
|
+
return;
|
|
432
|
+
el.releasePointerCapture?.(pointer);
|
|
433
|
+
pointer = -1;
|
|
434
|
+
};
|
|
435
|
+
const key = (e) => {
|
|
436
|
+
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown')
|
|
437
|
+
return;
|
|
438
|
+
e.preventDefault();
|
|
439
|
+
setDragged(Math.max(minHeight, Math.round(current() + (e.key === 'ArrowDown' ? STEP : -STEP))));
|
|
440
|
+
};
|
|
441
|
+
const reset = () => setDragged(null);
|
|
442
|
+
el.addEventListener('pointerdown', down);
|
|
443
|
+
el.addEventListener('pointermove', move);
|
|
444
|
+
el.addEventListener('pointerup', up);
|
|
445
|
+
el.addEventListener('pointercancel', up);
|
|
446
|
+
el.addEventListener('keydown', key);
|
|
447
|
+
el.addEventListener('dblclick', reset);
|
|
448
|
+
return () => {
|
|
449
|
+
el.removeEventListener('pointerdown', down);
|
|
450
|
+
el.removeEventListener('pointermove', move);
|
|
451
|
+
el.removeEventListener('pointerup', up);
|
|
452
|
+
el.removeEventListener('pointercancel', up);
|
|
453
|
+
el.removeEventListener('keydown', key);
|
|
454
|
+
el.removeEventListener('dblclick', reset);
|
|
455
|
+
};
|
|
456
|
+
}, [resizable, minHeight]);
|
|
163
457
|
const copy = React.useCallback(async () => {
|
|
164
458
|
try {
|
|
165
459
|
await navigator.clipboard.writeText(text);
|
|
@@ -172,24 +466,33 @@ function CodeEditor({ value, defaultValue = '', language = 'javascript', height
|
|
|
172
466
|
toaster_1.toast.error('Failed to copy code');
|
|
173
467
|
}
|
|
174
468
|
}, [text]);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
469
|
+
// A percentage height fills a frame that is itself sized; the body has to flex for it to
|
|
470
|
+
// resolve. Any other height is the editor's own, so the body is just as tall as the editor.
|
|
471
|
+
const fills = typeof height === 'string' && height.endsWith('%');
|
|
472
|
+
const formattable = showFormatButton ?? (isJson && !readOnly);
|
|
473
|
+
const toolbar = showLanguageSelector || showCopyButton || formattable;
|
|
474
|
+
const footer = resizable || (isJson && check !== null);
|
|
475
|
+
return ((0, jsx_runtime_1.jsxs)(gui_1.YStack, { ...(0, slot_1.slot)('code-editor'), borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", bg: "$background", focusWithinStyle: { borderColor: '$outlineColor' }, ...props, children: [toolbar && ((0, jsx_runtime_1.jsxs)(gui_1.XStack, { ...(0, slot_1.slot)('code-editor-toolbar'), items: "center", justify: "space-between", flexWrap: "wrap", gap: "$2", borderBottomWidth: 1, borderColor: "$borderColor", bg: "$panel", px: "$2", py: "$1", children: [showLanguageSelector ? ((0, jsx_runtime_1.jsxs)(select_1.Select, { value: selected, onValueChange: setSelected, children: [(0, jsx_runtime_1.jsx)(select_1.SelectTrigger, { ...(0, slot_1.slot)('code-editor-language-trigger'), width: "auto", minHeight: 32, borderWidth: 0, bg: "transparent", gap: "$1.5", px: "$2", children: (0, jsx_runtime_1.jsx)(gui_1.SizableText, { size: "$1", fontFamily: "$mono", children: LABEL[selected] ?? selected }) }), (0, jsx_runtime_1.jsx)(select_1.SelectContent, { ...(0, slot_1.slot)('code-editor-language-content'), children: availableLanguages.map((lang) => ((0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: lang, children: LABEL[lang] ?? lang }, lang))) })] })) : ((0, jsx_runtime_1.jsx)(gui_1.SizableText, { ...(0, slot_1.slot)('code-editor-language'), size: "$1", fontFamily: "$mono", color: "$soft", px: "$2", children: LABEL[selected] ?? selected })), (0, jsx_runtime_1.jsxs)(gui_1.XStack, { items: "center", justify: "flex-end", flexWrap: "wrap", gap: "$1", shrink: 1, minW: 0, children: [formattable && ((0, jsx_runtime_1.jsxs)(button_1.Button, { ...(0, slot_1.slot)('code-editor-format-button'), variant: "ghost", size: "sm", disabled: check?.kind !== 'json', onClick: format, children: [(0, jsx_runtime_1.jsx)(lucide_icons_2_1.AlignLeft, { size: 14 }), "Format"] })), showCopyButton && ((0, jsx_runtime_1.jsxs)(button_1.Button, { ...(0, slot_1.slot)('code-editor-copy-button'), variant: "ghost", size: "sm", disabled: !text, onClick: copy, children: [copied ? (0, jsx_runtime_1.jsx)(lucide_icons_2_1.Check, { size: 14 }) : (0, jsx_runtime_1.jsx)(lucide_icons_2_1.Copy, { size: 14 }), copied ? 'Copied!' : 'Copy'] }))] })] })), (0, jsx_runtime_1.jsx)(gui_1.YStack, { ref: host, ...(0, slot_1.slot)('code-editor-body'), ...(fills && { flex: 1, minH: 0 }) }), footer && ((0, jsx_runtime_1.jsxs)(gui_1.XStack, { ref: grip, ...(0, slot_1.slot)('code-editor-footer'), ...(resizable &&
|
|
476
|
+
{
|
|
477
|
+
role: 'separator',
|
|
478
|
+
'aria-orientation': 'horizontal',
|
|
479
|
+
'aria-label': `Resize ${label}`,
|
|
480
|
+
'aria-valuemin': minHeight,
|
|
481
|
+
'aria-valuenow': dragged ?? undefined,
|
|
482
|
+
tabIndex: 0,
|
|
483
|
+
}), cursor: resizable ? 'row-resize' : undefined, focusVisibleStyle: { bg: '$hover' }, items: "center", justify: "space-between", gap: "$2", minH: 28, px: "$3", py: "$1", borderTopWidth: 1, borderColor: "$borderColor", bg: "$panel", select: "none", children: [(0, jsx_runtime_1.jsx)(Status, { check: isJson ? check : null }), resizable && (0, jsx_runtime_1.jsx)(lucide_icons_2_1.GripHorizontal, { size: 14, color: "$soft" })] }))] }));
|
|
484
|
+
}
|
|
485
|
+
/** The footer's JSON verdict, announced politely when it changes. */
|
|
486
|
+
function Status({ check }) {
|
|
487
|
+
const bad = check?.kind === 'error';
|
|
488
|
+
const words = check === null
|
|
489
|
+
? ''
|
|
490
|
+
: check.kind === 'error'
|
|
491
|
+
? `Line ${check.line}, column ${check.column}: ${check.message}`
|
|
492
|
+
: check.kind === 'text'
|
|
493
|
+
? 'Plain text'
|
|
494
|
+
: check.kind === 'empty'
|
|
495
|
+
? 'Empty'
|
|
496
|
+
: 'Valid JSON';
|
|
497
|
+
return ((0, jsx_runtime_1.jsxs)(gui_1.XStack, { ...(0, slot_1.slot)('code-editor-status'), "aria-live": "polite", items: "center", gap: "$1.5", flex: 1, minW: 0, children: [bad ? ((0, jsx_runtime_1.jsx)(lucide_icons_2_1.CircleAlert, { size: 14, color: "$bad" })) : check?.kind === 'text' ? ((0, jsx_runtime_1.jsx)(lucide_icons_2_1.Type, { size: 14, color: "$soft" })) : check?.kind === 'json' ? ((0, jsx_runtime_1.jsx)(lucide_icons_2_1.CircleCheck, { size: 14, color: "$soft" })) : null, (0, jsx_runtime_1.jsx)(gui_1.SizableText, { ...(0, slot_1.slot)('code-editor-status-text'), "data-kind": check?.kind, size: "$1", fontFamily: "$mono", color: bad ? '$bad' : '$soft', numberOfLines: 2, children: words })] }));
|
|
195
498
|
}
|
|
@@ -1,49 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CodeEditor — a
|
|
3
|
-
* language menu
|
|
4
|
-
*
|
|
2
|
+
* CodeEditor — a CodeMirror 6 editor in a gui frame: a toolbar with the
|
|
3
|
+
* language (a menu, or a label), Format for JSON and Copy; the editor with a
|
|
4
|
+
* line-number gutter; and a footer that reports whether JSON parses and is the
|
|
5
|
+
* handle that resizes the editor.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Height. With no `height` the editor is as tall as its text, from `minHeight`
|
|
8
|
+
* up to `maxHeight`, and scrolls past that. Dragging the footer (or ArrowUp /
|
|
9
|
+
* ArrowDown on it) sets a height of the reader's own, past the cap if they
|
|
10
|
+
* want; a double-click hands the height back to the text. A `height` fixes it,
|
|
11
|
+
* and `'100%'` fills a frame that is itself sized (a flex child).
|
|
11
12
|
*
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* JSON. `language="json"` colours the syntax and checks the text on every
|
|
14
|
+
* change with `checkJson`: a broken document tints the failing line, marks the
|
|
15
|
+
* failing character and names line, column and reason in the footer. With
|
|
16
|
+
* `allowText`, text that does not open an object or an array is plain text and
|
|
17
|
+
* passes. Format (or Shift-Alt-F) re-indents valid JSON by two spaces. Other
|
|
18
|
+
* languages are edited as plain text: the package carries one grammar.
|
|
19
|
+
*
|
|
20
|
+
* Colour. Every colour is a theme rung over a design token — the ink, the
|
|
21
|
+
* hairline, the selection and the error state — so the editor follows the page
|
|
22
|
+
* into light or dark with nothing to configure. Hanzo is monochrome, so the syntax is ranked
|
|
23
|
+
* by ink and weight, not by hue: property names in full ink, strings one rank
|
|
24
|
+
* down, punctuation at the rank of a hint. Hue is spent only on an error.
|
|
16
25
|
*/
|
|
17
26
|
import { type YStackProps } from '@hanzo/gui';
|
|
27
|
+
import { EditorView } from '@codemirror/view';
|
|
18
28
|
import * as React from 'react';
|
|
19
|
-
|
|
29
|
+
import { type JsonCheck } from './json.js';
|
|
20
30
|
export type CodeEditorWordWrap = 'on' | 'off' | 'wordWrapColumn' | 'bounded';
|
|
21
|
-
export interface CodeEditorProps extends Omit<YStackProps, 'children' | 'height' | '
|
|
22
|
-
/** A controlled value; omit it and set `defaultValue` to let the
|
|
31
|
+
export interface CodeEditorProps extends Omit<YStackProps, 'children' | 'height' | 'minHeight' | 'maxHeight' | 'onChange'> {
|
|
32
|
+
/** A controlled value; omit it and set `defaultValue` to let the editor own its text. */
|
|
23
33
|
value?: string;
|
|
24
34
|
defaultValue?: string;
|
|
25
35
|
language?: string;
|
|
26
|
-
/**
|
|
36
|
+
/** A fixed height. Omit it and the editor follows its text between `minHeight` and `maxHeight`. */
|
|
27
37
|
height?: string | number;
|
|
28
|
-
|
|
38
|
+
/** The shortest the editor gets when it follows its text, in px. */
|
|
39
|
+
minHeight?: number;
|
|
40
|
+
/** The tallest it grows on its own, in px; past it the editor scrolls. */
|
|
41
|
+
maxHeight?: number;
|
|
42
|
+
/** Whether the footer resizes the editor. Defaults to true unless `height` is set. */
|
|
43
|
+
resizable?: boolean;
|
|
44
|
+
/** JSON only: text that does not open an object or an array is plain text, not an error. */
|
|
45
|
+
allowText?: boolean;
|
|
29
46
|
onChange?: (value: string) => void;
|
|
30
|
-
/**
|
|
31
|
-
|
|
47
|
+
/** JSON only: the verdict on the text, after mount and after every edit. */
|
|
48
|
+
onCheck?: (check: JsonCheck) => void;
|
|
49
|
+
/** Called once, after mount, with the CodeMirror view. */
|
|
50
|
+
onMount?: (view: EditorView) => void;
|
|
32
51
|
readOnly?: boolean;
|
|
33
52
|
lineNumbers?: boolean;
|
|
34
|
-
/** Accepted and ignored: a plain text field has no surface to draw a minimap on. */
|
|
35
|
-
minimap?: boolean;
|
|
36
53
|
wordWrap?: CodeEditorWordWrap;
|
|
37
54
|
fontSize?: number;
|
|
55
|
+
/** Shown while the editor is empty. */
|
|
56
|
+
placeholder?: string;
|
|
38
57
|
showCopyButton?: boolean;
|
|
58
|
+
/** Defaults to true for editable JSON. */
|
|
59
|
+
showFormatButton?: boolean;
|
|
39
60
|
showLanguageSelector?: boolean;
|
|
40
61
|
availableLanguages?: readonly string[];
|
|
41
62
|
/**
|
|
42
|
-
* The
|
|
43
|
-
*
|
|
44
|
-
*
|
|
63
|
+
* The editor's accessible name. An editing surface with none is announced
|
|
64
|
+
* with nothing to say what it holds; a file's path is the usual answer.
|
|
65
|
+
* Defaults to "Code".
|
|
45
66
|
*/
|
|
46
67
|
label?: string;
|
|
47
68
|
}
|
|
48
|
-
export declare function CodeEditor({ value, defaultValue, language, height,
|
|
69
|
+
export declare function CodeEditor({ value, defaultValue, language, height, minHeight, maxHeight, resizable, allowText, onChange, onCheck, onMount, readOnly, lineNumbers, wordWrap, fontSize, placeholder, showCopyButton, showFormatButton, showLanguageSelector, availableLanguages, label, ...props }: CodeEditorProps): React.JSX.Element;
|
|
49
70
|
//# sourceMappingURL=code-editor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"code-editor.d.ts","sourceRoot":"","sources":["../../../src/backends/gui/code-editor.tsx"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"code-editor.d.ts","sourceRoot":"","sources":["../../../src/backends/gui/code-editor.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAA+B,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AAM1E,OAAO,EAEL,UAAU,EAYX,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,OAAO,EAAyB,KAAK,SAAS,EAAE,MAAM,QAAQ,CAAA;AAK9D,MAAM,MAAM,kBAAkB,GAAG,IAAI,GAAG,KAAK,GAAG,gBAAgB,GAAG,SAAS,CAAA;AAsN5E,MAAM,WAAW,eACf,SAAQ,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,QAAQ,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,CAAC;IACzF,yFAAyF;IACzF,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mGAAmG;IACnG,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACxB,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sFAAsF;IACtF,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,4EAA4E;IAC5E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAA;IACpC,0DAA0D;IAC1D,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAA;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,QAAQ,CAAC,EAAE,kBAAkB,CAAA;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,0CAA0C;IAC1C,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,kBAAkB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACtC;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,YAAiB,EACjB,QAAuB,EACvB,MAAM,EACN,SAAc,EACd,SAAe,EACf,SAAgC,EAChC,SAAiB,EACjB,QAAQ,EACR,OAAO,EACP,OAAO,EACP,QAAgB,EAChB,WAAkB,EAClB,QAAe,EACf,QAAa,EACb,WAAW,EACX,cAAqB,EACrB,gBAAgB,EAChB,oBAA2B,EAC3B,kBAA8B,EAC9B,KAAc,EACd,GAAG,KAAK,EACT,EAAE,eAAe,qBA2TjB"}
|