@hanzo/ui 8.27.82 → 8.27.84

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.
Files changed (45) hide show
  1. package/dist/backends/gui/code-editor.cjs +80 -84
  2. package/dist/backends/gui/code-editor.d.ts +8 -6
  3. package/dist/backends/gui/code-editor.d.ts.map +1 -1
  4. package/dist/backends/gui/code-editor.js +83 -87
  5. package/dist/backends/gui/code-editor.js.map +1 -1
  6. package/dist/backends/gui/grip.cjs +132 -0
  7. package/dist/backends/gui/grip.d.ts +28 -0
  8. package/dist/backends/gui/grip.d.ts.map +1 -0
  9. package/dist/backends/gui/grip.js +96 -0
  10. package/dist/backends/gui/grip.js.map +1 -0
  11. package/dist/backends/gui/json-tree.cjs +110 -15
  12. package/dist/backends/gui/json-tree.d.ts +43 -9
  13. package/dist/backends/gui/json-tree.d.ts.map +1 -1
  14. package/dist/backends/gui/json-tree.js +110 -16
  15. package/dist/backends/gui/json-tree.js.map +1 -1
  16. package/dist/backends/gui/resizable.cjs +12 -3
  17. package/dist/backends/gui/resizable.d.ts +4 -2
  18. package/dist/backends/gui/resizable.d.ts.map +1 -1
  19. package/dist/backends/gui/resizable.js +12 -3
  20. package/dist/backends/gui/resizable.js.map +1 -1
  21. package/dist/code-theme.cjs +44 -0
  22. package/dist/code-theme.d.ts +46 -0
  23. package/dist/code-theme.d.ts.map +1 -0
  24. package/dist/code-theme.js +42 -0
  25. package/dist/code-theme.js.map +1 -0
  26. package/dist/core/index.cjs +4 -1
  27. package/dist/core/index.d.ts +1 -0
  28. package/dist/core/index.d.ts.map +1 -1
  29. package/dist/core/index.js +1 -0
  30. package/dist/core/index.js.map +1 -1
  31. package/dist/gallery.html +250 -250
  32. package/dist/gui-config.cjs +3 -0
  33. package/dist/gui-config.d.ts +218 -209
  34. package/dist/gui-config.d.ts.map +1 -1
  35. package/dist/gui-config.js +3 -0
  36. package/dist/gui-config.js.map +1 -1
  37. package/dist/index.cjs +8 -1
  38. package/dist/index.d.ts +1 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +5 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/index.node.cjs +8 -1
  43. package/dist/index.node.js +5 -0
  44. package/dist/styles.css +124 -124
  45. package/package.json +1 -1
@@ -50,16 +50,18 @@ const jsx_runtime_1 = require("react/jsx-runtime");
50
50
  *
51
51
  * JSON. `language="json"` colours the syntax and checks the text on every
52
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
53
+ * failing character and names line, column and reason in the footer. Every
54
+ * object and array that spans lines folds from a marker in the gutter, and a
55
+ * folded one reads `{…}` or `[…]` with how many keys or items it hides. With
54
56
  * `allowText`, text that does not open an object or an array is plain text and
55
57
  * passes. Format (or Shift-Alt-F) re-indents valid JSON by two spaces. Other
56
58
  * languages are edited as plain text: the package carries one grammar.
57
59
  *
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.
60
+ * Colour. The frame's colours are theme rungs over design tokens — the ink, the
61
+ * hairline, the selection and the error state — and the syntax is the code
62
+ * theme (`code-theme.ts`: Dracula on dark, GitHub Light on light), each token a
63
+ * theme key of its own. Both follow the page into light or dark with nothing to
64
+ * configure.
63
65
  */
64
66
  const gui_1 = require("@hanzo/gui");
65
67
  const lucide_icons_2_1 = require("@hanzogui/lucide-icons-2");
@@ -71,6 +73,7 @@ const view_1 = require("@codemirror/view");
71
73
  const highlight_1 = require("@lezer/highlight");
72
74
  const React = __importStar(require("react"));
73
75
  const button_1 = require("./button.cjs");
76
+ const grip_1 = require("./grip.cjs");
74
77
  const json_1 = require("./json.cjs");
75
78
  const select_1 = require("./select.cjs");
76
79
  const slot_1 = require("./slot.cjs");
@@ -130,10 +133,6 @@ const LABEL = {
130
133
  };
131
134
  /** Line height as a multiple of the font size. */
132
135
  const LEADING = 1.6;
133
- /** How far one ArrowUp / ArrowDown on the footer moves the height. */
134
- const STEP = 24;
135
- /** The separator's stated maximum, in px: the drag itself has none. */
136
- const RESIZE_CEILING = 4096;
137
136
  const MONO = "var(--font-mono, 'Zen Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace)";
138
137
  /**
139
138
  * The editor's surface. Every colour is one of the theme's named rungs — gui emits
@@ -180,6 +179,18 @@ const FRAME = /* @__PURE__ */ view_1.EditorView.theme({
180
179
  },
181
180
  '.cm-nonmatchingBracket, &.cm-focused .cm-nonmatchingBracket': { backgroundColor: 'transparent' },
182
181
  '.cm-placeholder': { color: 'var(--dim)' },
182
+ '.cm-foldGutter .cm-gutterElement': { padding: '0 4px', color: 'var(--dim)', cursor: 'pointer' },
183
+ '.cm-foldGutter .cm-gutterElement:hover': { color: 'var(--ink)' },
184
+ '.cm-foldPlaceholder': {
185
+ backgroundColor: 'var(--raised)',
186
+ border: '1px solid var(--edge)',
187
+ borderRadius: '4px',
188
+ color: 'var(--codePunctuation)',
189
+ padding: '0 4px',
190
+ margin: '0 2px',
191
+ cursor: 'pointer',
192
+ },
193
+ '.cm-foldCount': { color: 'var(--codeComment)' },
183
194
  '.cm-fault': { backgroundColor: 'var(--state-error-bg, rgb(239 68 68 / .1))' },
184
195
  '.cm-gutterElement.cm-fault': { color: 'var(--bad)' },
185
196
  '.cm-fault-at': {
@@ -188,13 +199,55 @@ const FRAME = /* @__PURE__ */ view_1.EditorView.theme({
188
199
  textUnderlineOffset: '3px',
189
200
  },
190
201
  });
191
- /** The syntax, ranked by ink: names in full ink, strings a rank down, punctuation at a hint. */
192
- const INK = /* @__PURE__ */ language_1.HighlightStyle.define([
193
- { tag: highlight_1.tags.propertyName, color: 'var(--ink)', fontWeight: '500' },
194
- { tag: highlight_1.tags.string, color: 'var(--soft)' },
195
- { tag: [highlight_1.tags.number, highlight_1.tags.bool, highlight_1.tags.null], color: 'var(--ink)' },
196
- { tag: [highlight_1.tags.punctuation, highlight_1.tags.separator, highlight_1.tags.brace, highlight_1.tags.squareBracket], color: 'var(--dim)' },
202
+ /** The syntax in the code theme's colours: one theme key per kind of token, from `code-theme.ts`. */
203
+ const SYNTAX = /* @__PURE__ */ language_1.HighlightStyle.define([
204
+ { tag: highlight_1.tags.propertyName, color: 'var(--codeKey)' },
205
+ { tag: highlight_1.tags.string, color: 'var(--codeString)' },
206
+ { tag: highlight_1.tags.number, color: 'var(--codeNumber)' },
207
+ { tag: highlight_1.tags.bool, color: 'var(--codeBoolean)' },
208
+ { tag: highlight_1.tags.null, color: 'var(--codeNull)' },
209
+ { tag: [highlight_1.tags.punctuation, highlight_1.tags.separator, highlight_1.tags.brace, highlight_1.tags.squareBracket], color: 'var(--codePunctuation)' },
210
+ { tag: highlight_1.tags.comment, color: 'var(--codeComment)' },
211
+ { tag: highlight_1.tags.keyword, color: 'var(--codeKeyword)' },
197
212
  ]);
213
+ /**
214
+ * JSON folds from its opening bracket through its closing one, so a folded block
215
+ * is drawn whole as `{…}` or `[…]` and has room to say what it holds. lang-json
216
+ * folds inside the brackets, which leaves them standing around an ellipsis.
217
+ */
218
+ const JSON_GRAMMAR = /* @__PURE__ */ new language_1.LanguageSupport(lang_json_1.jsonLanguage.configure({
219
+ props: [language_1.foldNodeProp.add({ 'Object Array': (node) => ({ from: node.from, to: node.to }) })],
220
+ }));
221
+ const words = (n, one) => `${n} ${one}${n === 1 ? '' : 's'}`;
222
+ /** What a folded range holds: its brackets and how many keys or items sit inside. */
223
+ function measureFold(state, range) {
224
+ let node = (0, language_1.syntaxTree)(state).resolveInner(range.from, 1);
225
+ while (node.parent && node.name !== 'Object' && node.name !== 'Array')
226
+ node = node.parent;
227
+ if (node.name === 'Object')
228
+ return { open: '{', close: '}', count: words(node.getChildren('Property').length, 'key') };
229
+ let n = 0;
230
+ for (let c = node.firstChild; c; c = c.nextSibling)
231
+ if (!/^[[\],]$/.test(c.name) && !c.type.isError)
232
+ n += 1;
233
+ return { open: '[', close: ']', count: words(n, 'item') };
234
+ }
235
+ const FOLDING = /* @__PURE__ */ (0, language_1.codeFolding)({
236
+ preparePlaceholder: measureFold,
237
+ placeholderDOM: (_view, onclick, fold) => {
238
+ const el = document.createElement('span');
239
+ el.className = 'cm-foldPlaceholder';
240
+ el.title = 'Unfold';
241
+ el.setAttribute('aria-label', `Folded, ${fold.count}`);
242
+ el.textContent = `${fold.open}…${fold.close}`;
243
+ const count = document.createElement('span');
244
+ count.className = 'cm-foldCount';
245
+ count.textContent = ` ${fold.count}`;
246
+ el.append(count);
247
+ el.onclick = onclick;
248
+ return el;
249
+ },
250
+ });
198
251
  /** How much deeper than its line a wrapped continuation starts, in characters. */
199
252
  const HANG = 2;
200
253
  /** A line's own indentation in characters, a tab counted at the editor's tab width. */
@@ -279,13 +332,16 @@ function CodeEditor({ value, defaultValue = '', language = 'javascript', height,
279
332
  const [text, setText] = React.useState(value ?? defaultValue);
280
333
  const [check, setCheck] = React.useState(null);
281
334
  const [copied, setCopied] = React.useState(false);
282
- const [dragged, setDragged] = React.useState(null);
283
335
  const host = React.useRef(null);
284
- const grip = React.useRef(null);
285
- const knob = React.useRef(null);
286
336
  /** Every text reported to `onChange` and not yet seen back as `value`. */
287
337
  const sent = React.useRef([]);
288
338
  const view = React.useRef(null);
339
+ const sized = (0, grip_1.useGrip)({
340
+ enabled: resizable,
341
+ min: minHeight,
342
+ measure: () => view.current?.dom.getBoundingClientRect().height ?? minHeight,
343
+ });
344
+ const dragged = sized.height;
289
345
  const timer = React.useRef(undefined);
290
346
  const listeners = React.useRef({ onChange, onCheck });
291
347
  listeners.current = { onChange, onCheck };
@@ -294,7 +350,7 @@ function CodeEditor({ value, defaultValue = '', language = 'javascript', height,
294
350
  const field = React.useMemo(() => verdict(allowText), [allowText]);
295
351
  const judged = React.useRef({ field });
296
352
  judged.current.field = field;
297
- const grammar = () => (isJson ? [(0, lang_json_1.json)(), field] : []);
353
+ const grammar = () => (isJson ? [JSON_GRAMMAR, field, FOLDING, (0, language_1.foldGutter)()] : []);
298
354
  const gutter = () => (lineNumbers ? [(0, view_1.lineNumbers)(), (0, view_1.highlightActiveLineGutter)()] : []);
299
355
  const wrap = () => (wordWrap === 'off' ? [] : [view_1.EditorView.lineWrapping, hang]);
300
356
  const edit = () => [state_1.EditorState.readOnly.of(readOnly), view_1.EditorView.editable.of(!readOnly)];
@@ -344,10 +400,11 @@ function CodeEditor({ value, defaultValue = '', language = 'javascript', height,
344
400
  (0, language_1.indentOnInput)(),
345
401
  (0, language_1.bracketMatching)(),
346
402
  (0, view_1.highlightActiveLine)(),
347
- (0, language_1.syntaxHighlighting)(INK),
403
+ (0, language_1.syntaxHighlighting)(SYNTAX),
348
404
  view_1.keymap.of([
349
405
  { key: 'Shift-Alt-f', run: format },
350
406
  commands_1.indentWithTab,
407
+ ...language_1.foldKeymap,
351
408
  ...commands_1.defaultKeymap,
352
409
  ...commands_1.historyKeymap,
353
410
  {
@@ -420,57 +477,6 @@ function CodeEditor({ value, defaultValue = '', language = 'javascript', height,
420
477
  dom.style.maxHeight = fixed ? '' : `${maxHeight}px`;
421
478
  view.current?.requestMeasure();
422
479
  }, [dragged, height, minHeight, maxHeight, fontSize]);
423
- React.useEffect(() => {
424
- const el = grip.current;
425
- const handle = knob.current;
426
- if (!el || !handle || !resizable)
427
- return;
428
- let pointer = -1;
429
- let startY = 0;
430
- let startH = 0;
431
- const current = () => view.current?.dom.getBoundingClientRect().height ?? minHeight;
432
- const down = (e) => {
433
- if (e.button !== 0)
434
- return;
435
- e.preventDefault();
436
- pointer = e.pointerId;
437
- startY = e.clientY;
438
- startH = current();
439
- el.setPointerCapture?.(pointer);
440
- };
441
- const move = (e) => {
442
- if (e.pointerId !== pointer)
443
- return;
444
- setDragged(Math.max(minHeight, Math.round(startH + e.clientY - startY)));
445
- };
446
- const up = (e) => {
447
- if (e.pointerId !== pointer)
448
- return;
449
- el.releasePointerCapture?.(pointer);
450
- pointer = -1;
451
- };
452
- const key = (e) => {
453
- if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown')
454
- return;
455
- e.preventDefault();
456
- setDragged(Math.max(minHeight, Math.round(current() + (e.key === 'ArrowDown' ? STEP : -STEP))));
457
- };
458
- const reset = () => setDragged(null);
459
- el.addEventListener('pointerdown', down);
460
- el.addEventListener('pointermove', move);
461
- el.addEventListener('pointerup', up);
462
- el.addEventListener('pointercancel', up);
463
- el.addEventListener('dblclick', reset);
464
- handle.addEventListener('keydown', key);
465
- return () => {
466
- el.removeEventListener('pointerdown', down);
467
- el.removeEventListener('pointermove', move);
468
- el.removeEventListener('pointerup', up);
469
- el.removeEventListener('pointercancel', up);
470
- el.removeEventListener('dblclick', reset);
471
- handle.removeEventListener('keydown', key);
472
- };
473
- }, [resizable, minHeight]);
474
480
  const copy = React.useCallback(async () => {
475
481
  try {
476
482
  await navigator.clipboard.writeText(text);
@@ -489,19 +495,9 @@ function CodeEditor({ value, defaultValue = '', language = 'javascript', height,
489
495
  const formattable = showFormatButton ?? (isJson && !readOnly);
490
496
  const toolbar = showLanguageSelector || showCopyButton || formattable;
491
497
  const footer = resizable || (isJson && check !== null);
492
- 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'), cursor: resizable ? 'row-resize' : undefined, items: "center", justify: "space-between", gap: "$2", minH: 28, px: "$3", py: "$1", borderTopWidth: 1, borderColor: "$borderColor", bg: "$panel", select: "none",
498
+ 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: sized.grip, ...(0, slot_1.slot)('code-editor-footer'), cursor: resizable ? 'row-resize' : undefined, items: "center", justify: "space-between", gap: "$2", minH: 28, px: "$3", py: "$1", borderTopWidth: 1, borderColor: "$borderColor", bg: "$panel", select: "none",
493
499
  // A touch drag would otherwise scroll the page and cancel the pointer.
494
- style: resizable ? { touchAction: 'none' } : undefined, children: [(0, jsx_runtime_1.jsx)(Status, { check: isJson ? check : null }), resizable && ((0, jsx_runtime_1.jsx)(gui_1.XStack, { ref: knob, ...(0, slot_1.slot)('code-editor-resize'), ...{
495
- role: 'separator',
496
- 'aria-orientation': 'horizontal',
497
- 'aria-label': `Resize ${label}`,
498
- 'aria-valuemin': minHeight,
499
- // A drag has no ceiling; this stands in for "as tall as you like".
500
- 'aria-valuemax': RESIZE_CEILING,
501
- 'aria-valuenow': dragged ?? minHeight,
502
- 'aria-valuetext': dragged === null ? 'Fits its text' : `${dragged} pixels`,
503
- tabIndex: 0,
504
- }, rounded: "$2", p: "$1", focusVisibleStyle: { bg: '$hover' }, children: (0, jsx_runtime_1.jsx)(lucide_icons_2_1.GripHorizontal, { size: 14, color: "$soft" }) }))] }))] }));
500
+ style: resizable ? { touchAction: 'none' } : undefined, children: [(0, jsx_runtime_1.jsx)(Status, { check: isJson ? check : null }), resizable && ((0, jsx_runtime_1.jsx)(grip_1.Grip, { knob: sized.knob, name: "code-editor-resize", label: label, min: minHeight, height: dragged }))] }))] }));
505
501
  }
506
502
  /** The footer's JSON verdict, announced politely when it changes. */
507
503
  function Status({ check }) {
@@ -12,16 +12,18 @@
12
12
  *
13
13
  * JSON. `language="json"` colours the syntax and checks the text on every
14
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
15
+ * failing character and names line, column and reason in the footer. Every
16
+ * object and array that spans lines folds from a marker in the gutter, and a
17
+ * folded one reads `{…}` or `[…]` with how many keys or items it hides. With
16
18
  * `allowText`, text that does not open an object or an array is plain text and
17
19
  * passes. Format (or Shift-Alt-F) re-indents valid JSON by two spaces. Other
18
20
  * languages are edited as plain text: the package carries one grammar.
19
21
  *
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.
22
+ * Colour. The frame's colours are theme rungs over design tokens — the ink, the
23
+ * hairline, the selection and the error state — and the syntax is the code
24
+ * theme (`code-theme.ts`: Dracula on dark, GitHub Light on light), each token a
25
+ * theme key of its own. Both follow the page into light or dark with nothing to
26
+ * configure.
25
27
  */
26
28
  import { type YStackProps } from '@hanzo/gui';
27
29
  import { EditorView } from '@codemirror/view';
@@ -1 +1 @@
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;AAwN5E,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,qBAyVjB"}
1
+ {"version":3,"file":"code-editor.d.ts","sourceRoot":"","sources":["../../../src/backends/gui/code-editor.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EAA+B,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AAiB1E,OAAO,EAEL,UAAU,EAYX,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,OAAO,EAAyB,KAAK,SAAS,EAAE,MAAM,QAAQ,CAAA;AAK9D,MAAM,MAAM,kBAAkB,GAAG,IAAI,GAAG,KAAK,GAAG,gBAAgB,GAAG,SAAS,CAAA;AA4Q5E,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,qBA2RjB"}
@@ -14,27 +14,30 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
14
  *
15
15
  * JSON. `language="json"` colours the syntax and checks the text on every
16
16
  * change with `checkJson`: a broken document tints the failing line, marks the
17
- * failing character and names line, column and reason in the footer. With
17
+ * failing character and names line, column and reason in the footer. Every
18
+ * object and array that spans lines folds from a marker in the gutter, and a
19
+ * folded one reads `{…}` or `[…]` with how many keys or items it hides. With
18
20
  * `allowText`, text that does not open an object or an array is plain text and
19
21
  * passes. Format (or Shift-Alt-F) re-indents valid JSON by two spaces. Other
20
22
  * languages are edited as plain text: the package carries one grammar.
21
23
  *
22
- * Colour. Every colour is a theme rung over a design token — the ink, the
23
- * hairline, the selection and the error state — so the editor follows the page
24
- * into light or dark with nothing to configure. Hanzo is monochrome, so the syntax is ranked
25
- * by ink and weight, not by hue: property names in full ink, strings one rank
26
- * down, punctuation at the rank of a hint. Hue is spent only on an error.
24
+ * Colour. The frame's colours are theme rungs over design tokens — the ink, the
25
+ * hairline, the selection and the error state — and the syntax is the code
26
+ * theme (`code-theme.ts`: Dracula on dark, GitHub Light on light), each token a
27
+ * theme key of its own. Both follow the page into light or dark with nothing to
28
+ * configure.
27
29
  */
28
30
  import { SizableText, XStack, YStack } from '@hanzo/gui';
29
- import { AlignLeft, Check, CircleAlert, CircleCheck, Copy, GripHorizontal, Type } from '@hanzogui/lucide-icons-2';
31
+ import { AlignLeft, Check, CircleAlert, CircleCheck, Copy, Type } from '@hanzogui/lucide-icons-2';
30
32
  import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
31
- import { json } from '@codemirror/lang-json';
32
- import { HighlightStyle, bracketMatching, indentOnInput, syntaxHighlighting } from '@codemirror/language';
33
+ import { jsonLanguage } from '@codemirror/lang-json';
34
+ import { HighlightStyle, LanguageSupport, bracketMatching, codeFolding, foldGutter, foldKeymap, foldNodeProp, indentOnInput, syntaxHighlighting, syntaxTree, } from '@codemirror/language';
33
35
  import { Annotation, Compartment, EditorState, RangeSet, RangeSetBuilder, StateField } from '@codemirror/state';
34
36
  import { Decoration, EditorView, GutterMarker, drawSelection, gutterLineClass, highlightActiveLine, highlightActiveLineGutter, keymap, lineNumbers as numbering, placeholder as hint, ViewPlugin, } from '@codemirror/view';
35
37
  import { tags } from '@lezer/highlight';
36
38
  import * as React from 'react';
37
39
  import { Button } from './button.js';
40
+ import { Grip, useGrip } from './grip.js';
38
41
  import { checkJson, formatJson } from './json.js';
39
42
  import { Select, SelectContent, SelectItem, SelectTrigger } from './select.js';
40
43
  import { slot } from './slot.js';
@@ -94,10 +97,6 @@ const LABEL = {
94
97
  };
95
98
  /** Line height as a multiple of the font size. */
96
99
  const LEADING = 1.6;
97
- /** How far one ArrowUp / ArrowDown on the footer moves the height. */
98
- const STEP = 24;
99
- /** The separator's stated maximum, in px: the drag itself has none. */
100
- const RESIZE_CEILING = 4096;
101
100
  const MONO = "var(--font-mono, 'Zen Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace)";
102
101
  /**
103
102
  * The editor's surface. Every colour is one of the theme's named rungs — gui emits
@@ -144,6 +143,18 @@ const FRAME = /* @__PURE__ */ EditorView.theme({
144
143
  },
145
144
  '.cm-nonmatchingBracket, &.cm-focused .cm-nonmatchingBracket': { backgroundColor: 'transparent' },
146
145
  '.cm-placeholder': { color: 'var(--dim)' },
146
+ '.cm-foldGutter .cm-gutterElement': { padding: '0 4px', color: 'var(--dim)', cursor: 'pointer' },
147
+ '.cm-foldGutter .cm-gutterElement:hover': { color: 'var(--ink)' },
148
+ '.cm-foldPlaceholder': {
149
+ backgroundColor: 'var(--raised)',
150
+ border: '1px solid var(--edge)',
151
+ borderRadius: '4px',
152
+ color: 'var(--codePunctuation)',
153
+ padding: '0 4px',
154
+ margin: '0 2px',
155
+ cursor: 'pointer',
156
+ },
157
+ '.cm-foldCount': { color: 'var(--codeComment)' },
147
158
  '.cm-fault': { backgroundColor: 'var(--state-error-bg, rgb(239 68 68 / .1))' },
148
159
  '.cm-gutterElement.cm-fault': { color: 'var(--bad)' },
149
160
  '.cm-fault-at': {
@@ -152,13 +163,55 @@ const FRAME = /* @__PURE__ */ EditorView.theme({
152
163
  textUnderlineOffset: '3px',
153
164
  },
154
165
  });
155
- /** The syntax, ranked by ink: names in full ink, strings a rank down, punctuation at a hint. */
156
- const INK = /* @__PURE__ */ HighlightStyle.define([
157
- { tag: tags.propertyName, color: 'var(--ink)', fontWeight: '500' },
158
- { tag: tags.string, color: 'var(--soft)' },
159
- { tag: [tags.number, tags.bool, tags.null], color: 'var(--ink)' },
160
- { tag: [tags.punctuation, tags.separator, tags.brace, tags.squareBracket], color: 'var(--dim)' },
166
+ /** The syntax in the code theme's colours: one theme key per kind of token, from `code-theme.ts`. */
167
+ const SYNTAX = /* @__PURE__ */ HighlightStyle.define([
168
+ { tag: tags.propertyName, color: 'var(--codeKey)' },
169
+ { tag: tags.string, color: 'var(--codeString)' },
170
+ { tag: tags.number, color: 'var(--codeNumber)' },
171
+ { tag: tags.bool, color: 'var(--codeBoolean)' },
172
+ { tag: tags.null, color: 'var(--codeNull)' },
173
+ { tag: [tags.punctuation, tags.separator, tags.brace, tags.squareBracket], color: 'var(--codePunctuation)' },
174
+ { tag: tags.comment, color: 'var(--codeComment)' },
175
+ { tag: tags.keyword, color: 'var(--codeKeyword)' },
161
176
  ]);
177
+ /**
178
+ * JSON folds from its opening bracket through its closing one, so a folded block
179
+ * is drawn whole as `{…}` or `[…]` and has room to say what it holds. lang-json
180
+ * folds inside the brackets, which leaves them standing around an ellipsis.
181
+ */
182
+ const JSON_GRAMMAR = /* @__PURE__ */ new LanguageSupport(jsonLanguage.configure({
183
+ props: [foldNodeProp.add({ 'Object Array': (node) => ({ from: node.from, to: node.to }) })],
184
+ }));
185
+ const words = (n, one) => `${n} ${one}${n === 1 ? '' : 's'}`;
186
+ /** What a folded range holds: its brackets and how many keys or items sit inside. */
187
+ function measureFold(state, range) {
188
+ let node = syntaxTree(state).resolveInner(range.from, 1);
189
+ while (node.parent && node.name !== 'Object' && node.name !== 'Array')
190
+ node = node.parent;
191
+ if (node.name === 'Object')
192
+ return { open: '{', close: '}', count: words(node.getChildren('Property').length, 'key') };
193
+ let n = 0;
194
+ for (let c = node.firstChild; c; c = c.nextSibling)
195
+ if (!/^[[\],]$/.test(c.name) && !c.type.isError)
196
+ n += 1;
197
+ return { open: '[', close: ']', count: words(n, 'item') };
198
+ }
199
+ const FOLDING = /* @__PURE__ */ codeFolding({
200
+ preparePlaceholder: measureFold,
201
+ placeholderDOM: (_view, onclick, fold) => {
202
+ const el = document.createElement('span');
203
+ el.className = 'cm-foldPlaceholder';
204
+ el.title = 'Unfold';
205
+ el.setAttribute('aria-label', `Folded, ${fold.count}`);
206
+ el.textContent = `${fold.open}…${fold.close}`;
207
+ const count = document.createElement('span');
208
+ count.className = 'cm-foldCount';
209
+ count.textContent = ` ${fold.count}`;
210
+ el.append(count);
211
+ el.onclick = onclick;
212
+ return el;
213
+ },
214
+ });
162
215
  /** How much deeper than its line a wrapped continuation starts, in characters. */
163
216
  const HANG = 2;
164
217
  /** A line's own indentation in characters, a tab counted at the editor's tab width. */
@@ -243,13 +296,16 @@ export function CodeEditor({ value, defaultValue = '', language = 'javascript',
243
296
  const [text, setText] = React.useState(value ?? defaultValue);
244
297
  const [check, setCheck] = React.useState(null);
245
298
  const [copied, setCopied] = React.useState(false);
246
- const [dragged, setDragged] = React.useState(null);
247
299
  const host = React.useRef(null);
248
- const grip = React.useRef(null);
249
- const knob = React.useRef(null);
250
300
  /** Every text reported to `onChange` and not yet seen back as `value`. */
251
301
  const sent = React.useRef([]);
252
302
  const view = React.useRef(null);
303
+ const sized = useGrip({
304
+ enabled: resizable,
305
+ min: minHeight,
306
+ measure: () => view.current?.dom.getBoundingClientRect().height ?? minHeight,
307
+ });
308
+ const dragged = sized.height;
253
309
  const timer = React.useRef(undefined);
254
310
  const listeners = React.useRef({ onChange, onCheck });
255
311
  listeners.current = { onChange, onCheck };
@@ -258,7 +314,7 @@ export function CodeEditor({ value, defaultValue = '', language = 'javascript',
258
314
  const field = React.useMemo(() => verdict(allowText), [allowText]);
259
315
  const judged = React.useRef({ field });
260
316
  judged.current.field = field;
261
- const grammar = () => (isJson ? [json(), field] : []);
317
+ const grammar = () => (isJson ? [JSON_GRAMMAR, field, FOLDING, foldGutter()] : []);
262
318
  const gutter = () => (lineNumbers ? [numbering(), highlightActiveLineGutter()] : []);
263
319
  const wrap = () => (wordWrap === 'off' ? [] : [EditorView.lineWrapping, hang]);
264
320
  const edit = () => [EditorState.readOnly.of(readOnly), EditorView.editable.of(!readOnly)];
@@ -308,10 +364,11 @@ export function CodeEditor({ value, defaultValue = '', language = 'javascript',
308
364
  indentOnInput(),
309
365
  bracketMatching(),
310
366
  highlightActiveLine(),
311
- syntaxHighlighting(INK),
367
+ syntaxHighlighting(SYNTAX),
312
368
  keymap.of([
313
369
  { key: 'Shift-Alt-f', run: format },
314
370
  indentWithTab,
371
+ ...foldKeymap,
315
372
  ...defaultKeymap,
316
373
  ...historyKeymap,
317
374
  {
@@ -384,57 +441,6 @@ export function CodeEditor({ value, defaultValue = '', language = 'javascript',
384
441
  dom.style.maxHeight = fixed ? '' : `${maxHeight}px`;
385
442
  view.current?.requestMeasure();
386
443
  }, [dragged, height, minHeight, maxHeight, fontSize]);
387
- React.useEffect(() => {
388
- const el = grip.current;
389
- const handle = knob.current;
390
- if (!el || !handle || !resizable)
391
- return;
392
- let pointer = -1;
393
- let startY = 0;
394
- let startH = 0;
395
- const current = () => view.current?.dom.getBoundingClientRect().height ?? minHeight;
396
- const down = (e) => {
397
- if (e.button !== 0)
398
- return;
399
- e.preventDefault();
400
- pointer = e.pointerId;
401
- startY = e.clientY;
402
- startH = current();
403
- el.setPointerCapture?.(pointer);
404
- };
405
- const move = (e) => {
406
- if (e.pointerId !== pointer)
407
- return;
408
- setDragged(Math.max(minHeight, Math.round(startH + e.clientY - startY)));
409
- };
410
- const up = (e) => {
411
- if (e.pointerId !== pointer)
412
- return;
413
- el.releasePointerCapture?.(pointer);
414
- pointer = -1;
415
- };
416
- const key = (e) => {
417
- if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown')
418
- return;
419
- e.preventDefault();
420
- setDragged(Math.max(minHeight, Math.round(current() + (e.key === 'ArrowDown' ? STEP : -STEP))));
421
- };
422
- const reset = () => setDragged(null);
423
- el.addEventListener('pointerdown', down);
424
- el.addEventListener('pointermove', move);
425
- el.addEventListener('pointerup', up);
426
- el.addEventListener('pointercancel', up);
427
- el.addEventListener('dblclick', reset);
428
- handle.addEventListener('keydown', key);
429
- return () => {
430
- el.removeEventListener('pointerdown', down);
431
- el.removeEventListener('pointermove', move);
432
- el.removeEventListener('pointerup', up);
433
- el.removeEventListener('pointercancel', up);
434
- el.removeEventListener('dblclick', reset);
435
- handle.removeEventListener('keydown', key);
436
- };
437
- }, [resizable, minHeight]);
438
444
  const copy = React.useCallback(async () => {
439
445
  try {
440
446
  await navigator.clipboard.writeText(text);
@@ -453,19 +459,9 @@ export function CodeEditor({ value, defaultValue = '', language = 'javascript',
453
459
  const formattable = showFormatButton ?? (isJson && !readOnly);
454
460
  const toolbar = showLanguageSelector || showCopyButton || formattable;
455
461
  const footer = resizable || (isJson && check !== null);
456
- return (_jsxs(YStack, { ...slot('code-editor'), borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", bg: "$background", focusWithinStyle: { borderColor: '$outlineColor' }, ...props, children: [toolbar && (_jsxs(XStack, { ...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 ? (_jsxs(Select, { value: selected, onValueChange: setSelected, children: [_jsx(SelectTrigger, { ...slot('code-editor-language-trigger'), width: "auto", minHeight: 32, borderWidth: 0, bg: "transparent", gap: "$1.5", px: "$2", children: _jsx(SizableText, { size: "$1", fontFamily: "$mono", children: LABEL[selected] ?? selected }) }), _jsx(SelectContent, { ...slot('code-editor-language-content'), children: availableLanguages.map((lang) => (_jsx(SelectItem, { value: lang, children: LABEL[lang] ?? lang }, lang))) })] })) : (_jsx(SizableText, { ...slot('code-editor-language'), size: "$1", fontFamily: "$mono", color: "$soft", px: "$2", children: LABEL[selected] ?? selected })), _jsxs(XStack, { items: "center", justify: "flex-end", flexWrap: "wrap", gap: "$1", shrink: 1, minW: 0, children: [formattable && (_jsxs(Button, { ...slot('code-editor-format-button'), variant: "ghost", size: "sm", disabled: check?.kind !== 'json', onClick: format, children: [_jsx(AlignLeft, { size: 14 }), "Format"] })), showCopyButton && (_jsxs(Button, { ...slot('code-editor-copy-button'), variant: "ghost", size: "sm", disabled: !text, onClick: copy, children: [copied ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }), copied ? 'Copied!' : 'Copy'] }))] })] })), _jsx(YStack, { ref: host, ...slot('code-editor-body'), ...(fills && { flex: 1, minH: 0 }) }), footer && (_jsxs(XStack, { ref: grip, ...slot('code-editor-footer'), cursor: resizable ? 'row-resize' : undefined, items: "center", justify: "space-between", gap: "$2", minH: 28, px: "$3", py: "$1", borderTopWidth: 1, borderColor: "$borderColor", bg: "$panel", select: "none",
462
+ return (_jsxs(YStack, { ...slot('code-editor'), borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", bg: "$background", focusWithinStyle: { borderColor: '$outlineColor' }, ...props, children: [toolbar && (_jsxs(XStack, { ...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 ? (_jsxs(Select, { value: selected, onValueChange: setSelected, children: [_jsx(SelectTrigger, { ...slot('code-editor-language-trigger'), width: "auto", minHeight: 32, borderWidth: 0, bg: "transparent", gap: "$1.5", px: "$2", children: _jsx(SizableText, { size: "$1", fontFamily: "$mono", children: LABEL[selected] ?? selected }) }), _jsx(SelectContent, { ...slot('code-editor-language-content'), children: availableLanguages.map((lang) => (_jsx(SelectItem, { value: lang, children: LABEL[lang] ?? lang }, lang))) })] })) : (_jsx(SizableText, { ...slot('code-editor-language'), size: "$1", fontFamily: "$mono", color: "$soft", px: "$2", children: LABEL[selected] ?? selected })), _jsxs(XStack, { items: "center", justify: "flex-end", flexWrap: "wrap", gap: "$1", shrink: 1, minW: 0, children: [formattable && (_jsxs(Button, { ...slot('code-editor-format-button'), variant: "ghost", size: "sm", disabled: check?.kind !== 'json', onClick: format, children: [_jsx(AlignLeft, { size: 14 }), "Format"] })), showCopyButton && (_jsxs(Button, { ...slot('code-editor-copy-button'), variant: "ghost", size: "sm", disabled: !text, onClick: copy, children: [copied ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }), copied ? 'Copied!' : 'Copy'] }))] })] })), _jsx(YStack, { ref: host, ...slot('code-editor-body'), ...(fills && { flex: 1, minH: 0 }) }), footer && (_jsxs(XStack, { ref: sized.grip, ...slot('code-editor-footer'), cursor: resizable ? 'row-resize' : undefined, items: "center", justify: "space-between", gap: "$2", minH: 28, px: "$3", py: "$1", borderTopWidth: 1, borderColor: "$borderColor", bg: "$panel", select: "none",
457
463
  // A touch drag would otherwise scroll the page and cancel the pointer.
458
- style: resizable ? { touchAction: 'none' } : undefined, children: [_jsx(Status, { check: isJson ? check : null }), resizable && (_jsx(XStack, { ref: knob, ...slot('code-editor-resize'), ...{
459
- role: 'separator',
460
- 'aria-orientation': 'horizontal',
461
- 'aria-label': `Resize ${label}`,
462
- 'aria-valuemin': minHeight,
463
- // A drag has no ceiling; this stands in for "as tall as you like".
464
- 'aria-valuemax': RESIZE_CEILING,
465
- 'aria-valuenow': dragged ?? minHeight,
466
- 'aria-valuetext': dragged === null ? 'Fits its text' : `${dragged} pixels`,
467
- tabIndex: 0,
468
- }, rounded: "$2", p: "$1", focusVisibleStyle: { bg: '$hover' }, children: _jsx(GripHorizontal, { size: 14, color: "$soft" }) }))] }))] }));
464
+ style: resizable ? { touchAction: 'none' } : undefined, children: [_jsx(Status, { check: isJson ? check : null }), resizable && (_jsx(Grip, { knob: sized.knob, name: "code-editor-resize", label: label, min: minHeight, height: dragged }))] }))] }));
469
465
  }
470
466
  /** The footer's JSON verdict, announced politely when it changes. */
471
467
  function Status({ check }) {