@fusion.dev/helix 0.172.0 → 0.173.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,19 @@ const DEFAULT_SPLIT = 50;
13
13
  const DEFAULT_PANEL = 160;
14
14
  const PANEL_MAX = 0.8;
15
15
  const PANE_KEYS = { css: 'styles', html: 'source', js: 'script' };
16
- const isPaneName = (name) => typeof name === 'string' && Object.hasOwn(PANE_KEYS, name);
16
+ const MODULE = /\.m?js$/;
17
+ const MANIFEST = 'package.json';
18
+ const isPaneName = (name) => typeof name === 'string' &&
19
+ (Object.hasOwn(PANE_KEYS, name) || MODULE.test(name) || name === MANIFEST);
20
+ const dependenciesOf = (text) => {
21
+ try {
22
+ const { dependencies } = JSON.parse(text);
23
+ return { ...dependencies };
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ };
17
29
  const nameOf = (pane) => {
18
30
  const { name } = pane;
19
31
  return typeof name === 'string' ? name : (pane.getAttribute('name') ?? '');
@@ -30,11 +42,15 @@ let HelixSandbox = class HelixSandbox extends HelixElement {
30
42
  this.defaultSplit = DEFAULT_SPLIT;
31
43
  this.filesLabel = 'Files';
32
44
  this.lead = 'source';
45
+ this.modifiedLabel = 'Modified';
33
46
  this.panelCollapsedSize = 0;
34
47
  this.panelLabel = 'Resize panel';
35
48
  this.panelSize = DEFAULT_PANEL;
36
49
  this.previewDelay = 300;
37
50
  this.edits = new Map();
51
+ this.given = new Map();
52
+ this.texts = new Map();
53
+ this.drawnModified = new Map();
38
54
  this.resting = new DelayController(this, { renders: false });
39
55
  this.runEdits = () => {
40
56
  for (const [name, value] of this.edits) {
@@ -65,7 +81,7 @@ let HelixSandbox = class HelixSandbox extends HelixElement {
65
81
  event.preventDefault();
66
82
  this.chooseFile(pane);
67
83
  void this.updateComplete.then(() => {
68
- this.renderRoot.querySelector(`[part="file"][aria-selected="true"]`)?.focus();
84
+ this.renderRoot.querySelector(`[part~="file"][aria-selected="true"]`)?.focus();
69
85
  });
70
86
  };
71
87
  this.bodyHeight = 0;
@@ -81,13 +97,22 @@ let HelixSandbox = class HelixSandbox extends HelixElement {
81
97
  const { value } = event.detail;
82
98
  const pane = event.composedPath()[0];
83
99
  const name = pane?.name;
84
- if (!isPaneName(name)) {
85
- return;
86
- }
100
+ const source = this.sources.find((each) => each === pane || each.contains(pane));
87
101
  queueMicrotask(() => {
88
102
  if (event.defaultPrevented) {
89
103
  return;
90
104
  }
105
+ if (source) {
106
+ this.texts.set(source, value);
107
+ const modified = this.modifiedOf(source);
108
+ if (this.drawnModified.get(source) !== modified) {
109
+ this.drawnModified.set(source, modified);
110
+ this.requestUpdate();
111
+ }
112
+ }
113
+ if (!isPaneName(name)) {
114
+ return;
115
+ }
91
116
  this.edits.set(name, value);
92
117
  if (this.previewDelay > 0) {
93
118
  this.resting.start(this.previewDelay, this.runEdits);
@@ -99,10 +124,12 @@ let HelixSandbox = class HelixSandbox extends HelixElement {
99
124
  };
100
125
  this.seed = () => {
101
126
  this.readFiles();
102
- const slot = this.renderRoot.querySelector('slot[name="source"]');
103
- for (const pane of slot?.assignedElements({ flatten: true }) ?? []) {
127
+ for (const pane of this.sources) {
104
128
  const { name, value } = pane;
105
129
  const text = typeof value === 'string' ? value : pane.getAttribute('value');
130
+ if (text !== null && !this.given.has(pane)) {
131
+ this.given.set(pane, text);
132
+ }
106
133
  if (isPaneName(name) && text) {
107
134
  this.write(name, text, true);
108
135
  }
@@ -166,12 +193,42 @@ let HelixSandbox = class HelixSandbox extends HelixElement {
166
193
  };
167
194
  }
168
195
  static { this.words = 'helix-sandbox'; }
196
+ get sources() {
197
+ const slot = this.renderRoot.querySelector('slot[name="source"]');
198
+ return slot?.assignedElements({ flatten: true }) ?? [];
199
+ }
200
+ modifiedOf(pane) {
201
+ const { modified } = pane;
202
+ if (typeof modified === 'boolean') {
203
+ return modified;
204
+ }
205
+ return this.texts.has(pane) && this.texts.get(pane) !== this.given.get(pane);
206
+ }
207
+ reset() {
208
+ for (const pane of this.sources) {
209
+ const { reset } = pane;
210
+ const text = this.given.get(pane);
211
+ const name = nameOf(pane);
212
+ if (typeof reset === 'function') {
213
+ reset.call(pane);
214
+ }
215
+ else if (text !== undefined && this.modifiedOf(pane)) {
216
+ ;
217
+ pane.value = text;
218
+ this.texts.delete(pane);
219
+ if (isPaneName(name)) {
220
+ this.write(name, text, false);
221
+ }
222
+ }
223
+ }
224
+ this.drawnModified.clear();
225
+ this.requestUpdate();
226
+ }
169
227
  get shownFile() {
170
228
  return this.files.find((pane) => nameOf(pane) === this.file) ?? this.files[0];
171
229
  }
172
230
  readFiles() {
173
- const slot = this.renderRoot.querySelector('slot[name="source"]');
174
- const panes = slot?.assignedElements({ flatten: true }) ?? [];
231
+ const panes = this.sources;
175
232
  const files = panes.length > 1 ? panes : [];
176
233
  if (files.length !== this.files.length || files.some((pane, at) => pane !== this.files[at])) {
177
234
  this.files = files;
@@ -192,10 +249,13 @@ let HelixSandbox = class HelixSandbox extends HelixElement {
192
249
  return html `<div part="files" role="tablist" aria-label=${this.filesLabel} @keydown=${this.handleFileKey}>
193
250
  ${this.files.map((pane, at) => {
194
251
  const selected = pane === shown;
195
- return html `<button part="file" id=${`file-${at}`} role="tab" type="button"
196
- aria-controls="files-panel" aria-selected=${selected ? 'true' : 'false'}
252
+ const modified = this.modifiedOf(pane);
253
+ return html `<button part="file${modified ? ' file-modified' : ''}" id=${`file-${at}`}
254
+ role="tab" type="button" aria-controls="files-panel"
255
+ aria-describedby=${modified ? 'modified' : nothing} aria-selected=${selected ? 'true' : 'false'}
197
256
  tabindex=${selected ? '0' : '-1'} @click=${() => this.chooseFile(pane)}>${fileOf(pane)}</button>`;
198
257
  })}
258
+ <span id="modified" hidden>${this.modifiedLabel}</span>
199
259
  </div>`;
200
260
  }
201
261
  showFile() {
@@ -240,14 +300,24 @@ let HelixSandbox = class HelixSandbox extends HelixElement {
240
300
  if (!(target instanceof Node)) {
241
301
  return false;
242
302
  }
243
- const slot = this.renderRoot.querySelector('slot[name="source"]');
244
- return (slot?.assignedElements({ flatten: true }) ?? []).some((pane) => pane === target || pane.contains(target));
303
+ return this.sources.some((pane) => pane === target || pane.contains(target));
245
304
  }
246
305
  write(name, value, seeding) {
306
+ const dependencies = name === MANIFEST ? dependenciesOf(value) : undefined;
247
307
  for (const preview of this.previews) {
248
- const key = PANE_KEYS[name];
249
- if (!seeding || !preview[key]) {
250
- preview[key] = value;
308
+ if (Object.hasOwn(PANE_KEYS, name)) {
309
+ const key = PANE_KEYS[name];
310
+ if (!seeding || !preview[key]) {
311
+ preview[key] = value;
312
+ }
313
+ }
314
+ else if (dependencies) {
315
+ if (!seeding || !preview.dependencies) {
316
+ preview.dependencies = dependencies;
317
+ }
318
+ }
319
+ else if (MODULE.test(name) && (!seeding || preview.modules?.[name] === undefined)) {
320
+ preview.modules = { ...preview.modules, [name]: value };
251
321
  }
252
322
  }
253
323
  }
@@ -394,9 +464,10 @@ let HelixSandbox = class HelixSandbox extends HelixElement {
394
464
  if (changed.has('files') || changed.has('file')) {
395
465
  this.showFile();
396
466
  }
467
+ this.toggleState('modified', this.sources.some((pane) => this.modifiedOf(pane)));
397
468
  if (this.files.length > 0) {
398
469
  auditName(this, () => {
399
- const tabs = [...this.renderRoot.querySelectorAll('[part="file"]')];
470
+ const tabs = [...this.renderRoot.querySelectorAll('[part~="file"]')];
400
471
  return tabs.find((tab) => !tab.textContent?.trim()) ?? tabs[0];
401
472
  }, 'Give each source pane a `label` or a `name`, which its file tab shows.');
402
473
  }
@@ -459,6 +530,10 @@ __decorate([
459
530
  __decorate([
460
531
  property()
461
532
  ], HelixSandbox.prototype, "lead", void 0);
533
+ __decorate([
534
+ words,
535
+ property({ attribute: 'modified-label' })
536
+ ], HelixSandbox.prototype, "modifiedLabel", void 0);
462
537
  __decorate([
463
538
  property()
464
539
  ], HelixSandbox.prototype, "orientation", void 0);
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The editing commands of the source pane, as functions from the source and
3
+ * the selection to the change they make. Kept apart from the element so each
4
+ * can be read and tested on text alone.
5
+ */
6
+ /** A change to the source: `text` in place of what is between `from` and `to`, and the selection after it. */
7
+ export interface SourceEdit {
8
+ /** Where the replaced text starts. */
9
+ from: number;
10
+ /** The selection after the change, its start and its end. */
11
+ selection: readonly [number, number];
12
+ /** What replaces the text between `from` and `to`. Empty where the change only moves the caret. */
13
+ text: string;
14
+ /** Where the replaced text ends. */
15
+ to: number;
16
+ }
17
+ /** The comment markers of each syntax: a line marker, or the two around a block. */
18
+ declare const COMMENTS: {
19
+ readonly css: readonly ['/*', '*/'];
20
+ readonly html: readonly ['<!--', '-->'];
21
+ readonly js: readonly ['//'];
22
+ };
23
+ /** A syntax the comment command knows. */
24
+ export type CommentSyntax = keyof typeof COMMENTS;
25
+ /**
26
+ * The syntax of a pane, read from its `name`: `html`, `css` and `js`, or the
27
+ * extension of a file name such as `utils.js`. `ts`, `jsx`, `tsx`, `mjs` and
28
+ * `cjs` are `js`. Anything else has no comments.
29
+ */
30
+ export declare const syntaxOf: (name: string) => CommentSyntax | undefined;
31
+ /** Indents each line a selection touches by `unit`, leaving blank lines as they are. */
32
+ export declare const indentLines: (source: string, from: number, to: number, unit?: string) => SourceEdit | undefined;
33
+ /** Takes one level of indentation, two spaces or a tab, off each line a selection touches. */
34
+ export declare const outdentLines: (source: string, from: number, to: number) => SourceEdit | undefined;
35
+ /**
36
+ * What Enter does: a new line with the indentation of the line it breaks, one
37
+ * level deeper after an opening bracket, and with the closing bracket on a
38
+ * line of its own where the caret is between a pair. `undefined` for a line
39
+ * at the margin with no bracket before the caret, which takes the platform's
40
+ * own Enter.
41
+ */
42
+ export declare const breakLine: (source: string, from: number, to: number, unit?: string) => SourceEdit | undefined;
43
+ /**
44
+ * Comments out the lines a selection touches, or uncomments them where they
45
+ * already are a comment.
46
+ *
47
+ * `js` comments each line with `//` at the indentation the lines share. `css`
48
+ * and `html`, which have no line comment, put one block comment around the
49
+ * text of the lines.
50
+ */
51
+ export declare const toggleComment: (source: string, from: number, to: number, syntax: CommentSyntax) => SourceEdit | undefined;
52
+ /**
53
+ * What typing `key` does where brackets close: a bracket or quote typed with
54
+ * text selected goes around the text, one typed before a space or the end of
55
+ * the line closes as well, and a closing one typed before the same character
56
+ * steps over it. `undefined` where the key types as usual.
57
+ *
58
+ * A quote after a letter or a digit is an apostrophe and does not close.
59
+ */
60
+ export declare const closeBracket: (source: string, from: number, to: number, key: string) => SourceEdit | undefined;
61
+ /**
62
+ * What Backspace does between a bracket or quote and the character that
63
+ * closes it, where brackets close: it deletes both.
64
+ */
65
+ export declare const deletePair: (source: string, from: number, to: number) => SourceEdit | undefined;
66
+ /** A bracket beside the caret, and the bracket that pairs with it where there is one. */
67
+ export interface BracketMatch {
68
+ /** Where the bracket beside the caret is. */
69
+ at: number;
70
+ /** Where the bracket that pairs with it is, or `undefined` where none does. */
71
+ match: number | undefined;
72
+ }
73
+ /**
74
+ * The bracket beside the caret and its pair: the one before the caret first,
75
+ * then the one after it. `skip` leaves out positions inside strings and
76
+ * comments, where a highlighter has said which those are.
77
+ */
78
+ export declare const matchBracket: (source: string, at: number, skip?: (index: number) => boolean) => BracketMatch | undefined;
79
+ /** The completions for the word before the caret, and where that word starts. */
80
+ export interface Completions {
81
+ /** Where the word being completed starts. */
82
+ from: number;
83
+ /** The words that complete it, in alphabetical order. */
84
+ words: string[];
85
+ }
86
+ /**
87
+ * The completions for the word before the caret: every longer word in the
88
+ * source or in `extra` that starts with it, whatever its case. `undefined`
89
+ * where the caret follows no word or nothing completes it.
90
+ */
91
+ export declare const suggest: (source: string, at: number, extra?: readonly string[]) => Completions | undefined;
92
+ /** The parts of a custom elements manifest that `manifestWords` reads. */
93
+ export interface ManifestLike {
94
+ modules?: readonly {
95
+ declarations?: readonly {
96
+ attributes?: readonly {
97
+ name: string;
98
+ }[];
99
+ cssProperties?: readonly {
100
+ name: string;
101
+ }[];
102
+ tagName?: string;
103
+ }[];
104
+ }[];
105
+ }
106
+ /**
107
+ * The tag names, attribute names and CSS custom properties a custom elements
108
+ * manifest lists, for a source pane's `completions`. Each name is listed once.
109
+ */
110
+ export declare const manifestWords: (manifest: ManifestLike) => string[];
111
+ export {};
@@ -0,0 +1,185 @@
1
+ const PAIRS = {
2
+ "'": "'",
3
+ '"': '"',
4
+ '(': ')',
5
+ '[': ']',
6
+ '{': '}',
7
+ '`': '`',
8
+ };
9
+ const OPENERS = '([{';
10
+ const CLOSERS = ')]}';
11
+ const BEFORE = /^(?:$|[\s)\]}>;:,"'`])/;
12
+ const COMMENTS = {
13
+ css: ['/*', '*/'],
14
+ html: ['<!--', '-->'],
15
+ js: ['//'],
16
+ };
17
+ export const syntaxOf = (name) => {
18
+ const extension = name.slice(name.lastIndexOf('.') + 1);
19
+ if (extension === 'css' || extension === 'html') {
20
+ return extension;
21
+ }
22
+ return /^[cm]?[jt]sx?$/.test(extension) ? 'js' : undefined;
23
+ };
24
+ const linesOf = (source, from, to) => {
25
+ const start = source.lastIndexOf('\n', from - 1) + 1;
26
+ const last = to > from && source[to - 1] === '\n' ? to - 1 : to;
27
+ const end = source.indexOf('\n', last);
28
+ return [start, end < 0 ? source.length : end];
29
+ };
30
+ const clamp = (at, low, high) => Math.min(high, Math.max(low, at));
31
+ const eachLine = (source, from, to, change) => {
32
+ const [start, end] = linesOf(source, from, to);
33
+ const before = source.slice(start, end).split('\n');
34
+ const after = before.map(change);
35
+ const text = after.join('\n');
36
+ if (text === source.slice(start, end)) {
37
+ return undefined;
38
+ }
39
+ const moved = from + (after[0] ?? '').length - (before[0] ?? '').length;
40
+ const head = from === start && from !== to ? from : clamp(moved, start, start + text.length);
41
+ const tail = from === to ? head : Math.max(head, to + text.length - (end - start));
42
+ return { from: start, selection: [head, tail], text, to: end };
43
+ };
44
+ export const indentLines = (source, from, to, unit = ' ') => eachLine(source, from, to, (line) => (line.trim() === '' ? line : `${unit}${line}`));
45
+ export const outdentLines = (source, from, to) => eachLine(source, from, to, (line) => line.replace(/^(?: {1,2}|\t)/, ''));
46
+ export const breakLine = (source, from, to, unit = ' ') => {
47
+ const start = source.lastIndexOf('\n', from - 1) + 1;
48
+ const indent = /^[ \t]*/.exec(source.slice(start, from))?.[0] ?? '';
49
+ const opens = OPENERS.indexOf(source[from - 1] ?? '\n');
50
+ if (opens < 0 && indent === '') {
51
+ return undefined;
52
+ }
53
+ const line = `\n${indent}${opens < 0 ? '' : unit}`;
54
+ const text = opens >= 0 && source[to] === CLOSERS[opens] ? `${line}\n${indent}` : line;
55
+ return { from, selection: [from + line.length, from + line.length], text, to };
56
+ };
57
+ const literal = (text) => text.replace(/[*/]/g, '\\$&');
58
+ export const toggleComment = (source, from, to, syntax) => {
59
+ const [open, close] = COMMENTS[syntax];
60
+ const [start, end] = linesOf(source, from, to);
61
+ const block = source.slice(start, end);
62
+ if (close === undefined) {
63
+ const filled = block.split('\n').filter((line) => line.trim() !== '');
64
+ const on = filled.length > 0 && filled.every((line) => line.trimStart().startsWith(open));
65
+ const indent = Math.min(...filled.map((line) => line.length - line.trimStart().length));
66
+ const marker = new RegExp(`^(\\s*)${literal(open)} ?`);
67
+ return eachLine(source, from, to, (line) => {
68
+ if (line.trim() === '') {
69
+ return line;
70
+ }
71
+ return on
72
+ ? line.replace(marker, '$1')
73
+ : `${line.slice(0, indent)}${open} ${line.slice(indent)}`;
74
+ });
75
+ }
76
+ const lead = block.length - block.trimStart().length;
77
+ const body = block.trim();
78
+ const trail = block.slice(lead + body.length);
79
+ const commented = body.length >= open.length + close.length && body.startsWith(open) && body.endsWith(close);
80
+ const inner = commented
81
+ ? body
82
+ .slice(open.length, body.length - close.length)
83
+ .replace(/^ /, '')
84
+ .replace(/ $/, '')
85
+ : `${open} ${body} ${close}`;
86
+ const text = `${block.slice(0, lead)}${inner}${trail}`;
87
+ const shift = commented ? -(open.length + (body[open.length] === ' ' ? 1 : 0)) : open.length + 1;
88
+ const head = clamp(from >= start + lead ? from + shift : from, start, start + text.length);
89
+ const tail = from === to ? head : Math.max(head, to + text.length - block.length);
90
+ return { from: start, selection: [head, tail], text, to: end };
91
+ };
92
+ export const closeBracket = (source, from, to, key) => {
93
+ const closer = PAIRS[key];
94
+ const next = source.slice(to);
95
+ if (from === to && (CLOSERS.includes(key) || closer === key) && next.startsWith(key)) {
96
+ return { from, selection: [from + 1, from + 1], text: '', to };
97
+ }
98
+ if (closer === undefined) {
99
+ return undefined;
100
+ }
101
+ if (from !== to) {
102
+ return {
103
+ from,
104
+ selection: [from + 1, to + 1],
105
+ text: `${key}${source.slice(from, to)}${closer}`,
106
+ to,
107
+ };
108
+ }
109
+ if (!BEFORE.test(next) || (closer === key && /[\w'"`]$/.test(source.slice(0, from)))) {
110
+ return undefined;
111
+ }
112
+ return { from, selection: [from + 1, from + 1], text: `${key}${closer}`, to };
113
+ };
114
+ export const deletePair = (source, from, to) => {
115
+ const opener = source[from - 1];
116
+ if (from !== to || opener === undefined || PAIRS[opener] !== source[from]) {
117
+ return undefined;
118
+ }
119
+ return { from: from - 1, selection: [from - 1, from - 1], text: '', to: from + 1 };
120
+ };
121
+ const REACH = 10_000;
122
+ export const matchBracket = (source, at, skip = () => false) => {
123
+ for (const index of [at - 1, at]) {
124
+ const bracket = source[index];
125
+ if (bracket === undefined || skip(index)) {
126
+ continue;
127
+ }
128
+ const opens = OPENERS.indexOf(bracket);
129
+ const closes = CLOSERS.indexOf(bracket);
130
+ if (opens < 0 && closes < 0) {
131
+ continue;
132
+ }
133
+ const step = opens < 0 ? -1 : 1;
134
+ const other = opens < 0 ? OPENERS[closes] : CLOSERS[opens];
135
+ let depth = 0;
136
+ for (let look = index; look >= 0 && look < source.length && Math.abs(look - index) < REACH; look += step) {
137
+ if (skip(look)) {
138
+ continue;
139
+ }
140
+ if (source[look] === bracket) {
141
+ depth += 1;
142
+ }
143
+ else if (source[look] === other) {
144
+ depth -= 1;
145
+ if (depth === 0) {
146
+ return { at: index, match: look };
147
+ }
148
+ }
149
+ }
150
+ return { at: index, match: undefined };
151
+ }
152
+ return undefined;
153
+ };
154
+ const OFFERED = 50;
155
+ const WORDS = /[\w$-]*[A-Za-z_$][\w$-]*/g;
156
+ export const suggest = (source, at, extra = []) => {
157
+ const word = /[\w$-]*$/.exec(source.slice(Math.max(0, at - 100), at))?.[0] ?? '';
158
+ if (!/[A-Za-z_$]/.test(word)) {
159
+ return undefined;
160
+ }
161
+ const start = word.toLowerCase();
162
+ const pool = new Set([...extra, ...(source.match(WORDS) ?? [])]);
163
+ const words = [...pool]
164
+ .filter((each) => each.length > word.length && each.toLowerCase().startsWith(start))
165
+ .sort((a, b) => a.localeCompare(b))
166
+ .slice(0, OFFERED);
167
+ return words.length > 0 ? { from: at - word.length, words } : undefined;
168
+ };
169
+ export const manifestWords = (manifest) => {
170
+ const names = new Set();
171
+ for (const module of manifest.modules ?? []) {
172
+ for (const declaration of module.declarations ?? []) {
173
+ if (declaration.tagName) {
174
+ names.add(declaration.tagName);
175
+ }
176
+ for (const { name } of [
177
+ ...(declaration.attributes ?? []),
178
+ ...(declaration.cssProperties ?? []),
179
+ ]) {
180
+ names.add(name);
181
+ }
182
+ }
183
+ }
184
+ return [...names];
185
+ };
@@ -1,4 +1,5 @@
1
1
  import { createSheet } from '@fusion.dev/helix-core';
2
2
  export const styles = createSheet(`:host {display: block;block-size: 100%;}[part="highlight"],
3
3
  [part="source"] {border: none;color: var(--helix-color-foreground-muted);font-family: var(--helix-font-mono);font-size: max(var(--helix-text-field-min), var(--helix-text-xs));inline-size: 100%;line-height: var(--helix-leading-normal);min-block-size: var(--sandbox-source-min-block-size, 12rem);padding: var(--helix-space-sm);tab-size: 2;}[part="source"] {background-color: var(--sandbox-source-background-color, var(--helix-color-background));block-size: 100%;display: block;resize: none;}[part="source"]:focus-visible {background-color: var(--helix-color-surface);outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: calc(var(--helix-size-focus-offset) * -1);}[part="field"] {background-color: var(--sandbox-source-background-color, var(--helix-color-background));block-size: 100%;display: grid;grid-template-rows: minmax(max-content, auto);overflow: auto;}[part="field"]:focus-within {background-color: var(--helix-color-surface);}[part="field"]:has(> [part="source"]:focus-visible) {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: calc(var(--helix-size-focus-offset) * -1);}[part="field"] > [part] {grid-area: 1 / 1;letter-spacing: normal;margin: 0;overflow-wrap: break-word;white-space: pre-wrap;}[part="field"] > [part="source"],
4
- [part="field"] > [part="source"]:focus-visible {-webkit-text-fill-color: transparent;align-self: stretch;background-color: transparent;block-size: auto;caret-color: var(--helix-color-foreground);color: transparent;outline: none;overflow: hidden;}:host([line-numbers]) [part="field"] > [part] {padding-inline-start: calc(var(--helix-space-sm) * 3 + var(--_sandbox-source-gutter, 1ch));}:host([line-numbers]) [part="highlight"] {counter-reset: line;}[part="line"] {counter-increment: line;display: block;min-block-size: 1lh;position: relative;}[part="line"]::before {color: var(--sandbox-source-line-number-color, var(--helix-color-foreground-subtle));content: counter(line);inset-inline-end: calc(100% + var(--helix-space-sm) * 2);position: absolute;text-align: end;}[data-kind="callable"] {color: var(--sandbox-source-callable-color, var(--helix-color-syntax-callable));}[data-kind="comment"] {color: var(--sandbox-source-comment-color, var(--helix-color-syntax-comment));}[data-kind="constant"] {color: var(--sandbox-source-constant-color, var(--helix-color-syntax-constant));}[data-kind="invalid"] {color: var(--sandbox-source-invalid-color, var(--helix-color-syntax-invalid));}[data-kind="keyword"] {color: var(--sandbox-source-keyword-color, var(--helix-color-syntax-keyword));}[data-kind="name"] {color: var(--sandbox-source-name-color, var(--helix-color-syntax-name));}[data-kind="string"] {color: var(--sandbox-source-string-color, var(--helix-color-syntax-string));}[data-kind="tag"] {color: var(--sandbox-source-tag-color, var(--helix-color-syntax-tag));}`);
4
+ [part="field"] > [part="source"]:focus-visible {-webkit-text-fill-color: transparent;align-self: stretch;background-color: transparent;block-size: auto;caret-color: var(--helix-color-foreground);color: transparent;outline: none;overflow: hidden;}:host([line-numbers]) [part="field"] > [part] {padding-inline-start: calc(var(--helix-space-sm) * 3 + var(--_sandbox-source-gutter, 1ch));}:host([line-numbers]) [part="highlight"] {counter-reset: line;}[part="line"] {counter-increment: line;display: block;min-block-size: 1lh;position: relative;}[part="line"]::before {color: var(--sandbox-source-line-number-color, var(--helix-color-foreground-subtle));content: counter(line);inset-inline-end: calc(100% + var(--helix-space-sm) * 2);position: absolute;text-align: end;}[data-kind="callable"] {color: var(--sandbox-source-callable-color, var(--helix-color-syntax-callable));}[data-kind="comment"] {color: var(--sandbox-source-comment-color, var(--helix-color-syntax-comment));}[data-kind="constant"] {color: var(--sandbox-source-constant-color, var(--helix-color-syntax-constant));}[data-kind="invalid"] {color: var(--sandbox-source-invalid-color, var(--helix-color-syntax-invalid));}[data-kind="keyword"] {color: var(--sandbox-source-keyword-color, var(--helix-color-syntax-keyword));}[data-kind="name"] {color: var(--sandbox-source-name-color, var(--helix-color-syntax-name));}[data-kind="string"] {color: var(--sandbox-source-string-color, var(--helix-color-syntax-string));}[data-kind="tag"] {color: var(--sandbox-source-tag-color, var(--helix-color-syntax-tag));}[part="field"]:focus-within [part~="bracket"] {background-color: var(--sandbox-source-bracket-color, var(--helix-color-accent-subtle));border-radius: var(--helix-radii-sm);}[part="field"]:focus-within [part~="bracket-unmatched"] {background-color: transparent;color: var(--sandbox-source-invalid-color, var(--helix-color-syntax-invalid));}[part="field"] {position: relative;}[part="completions"] {background-color: var(--helix-color-surface-overlay);border: var(--sandbox-source-completions-border-width, var(--helix-border-floating)) solid
5
+ var(--helix-color-border);border-radius: var(--helix-radii-sm);box-shadow: var(--sandbox-source-completions-shadow, var(--helix-shadow-floating));font-family: var(--helix-font-mono);font-size: max(var(--helix-text-field-min), var(--helix-text-xs));max-block-size: 12rem;min-inline-size: 8rem;overflow-y: auto;padding: var(--helix-space-2xs);position: absolute;z-index: 1;}[part~="completion"] {border-radius: var(--helix-radii-sm);color: var(--helix-color-foreground);cursor: pointer;padding: var(--helix-space-2xs) var(--helix-space-xs);white-space: nowrap;}[part~="completion-active"] {background-color: var(--helix-color-accent-surface);}`);