@codecanvas-ide/engine 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,101 @@
1
1
  // src/parse.ts
2
+ import ts3 from "typescript";
3
+
4
+ // src/react.ts
5
+ import ts2 from "typescript";
6
+
7
+ // src/syntax.ts
2
8
  import ts from "typescript";
9
+ function unwrap(e) {
10
+ while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isSatisfiesExpression(e) || ts.isTypeAssertionExpression(e)) e = e.expression;
11
+ return e;
12
+ }
13
+
14
+ // src/react.ts
15
+ var REACT_MODULE = /^(react|react-dom|react-native)(\/|$)/;
16
+ function fileLanguageOf(path, sf) {
17
+ const script = /\.[cm]?jsx?$/.test(path) ? "js" : "ts";
18
+ const hasJsxExtension = /\.[jt]sx$/.test(path);
19
+ const mayHaveJsx = script === "js" && /<[A-Za-z>]/.test(sf.text);
20
+ const hasJsx = hasJsxExtension || mayHaveJsx && containsJsx(sf);
21
+ const isReact = hasJsx || importsReact(sf);
22
+ return { script, hasJsx, isReact, label: languageLabel(script, hasJsx, isReact) };
23
+ }
24
+ function languageLabel(script, hasJsx, isReact) {
25
+ if (hasJsx) return script === "js" ? "React JSX" : "React TSX";
26
+ if (isReact) return script === "js" ? "React JS" : "React TS";
27
+ return script === "js" ? "JS" : "TS";
28
+ }
29
+ function isJsx(node) {
30
+ return ts2.isJsxElement(node) || ts2.isJsxSelfClosingElement(node) || ts2.isJsxFragment(node);
31
+ }
32
+ function containsJsx(node) {
33
+ return isJsx(node) || !!ts2.forEachChild(node, (child) => containsJsx(child) || void 0);
34
+ }
35
+ function importsReact(sf) {
36
+ return sf.statements.some((statement) => {
37
+ const isImportOrReExport = ts2.isImportDeclaration(statement) || ts2.isExportDeclaration(statement);
38
+ if (isImportOrReExport) return !!statement.moduleSpecifier && ts2.isStringLiteral(statement.moduleSpecifier) && REACT_MODULE.test(statement.moduleSpecifier.text);
39
+ if (ts2.isImportEqualsDeclaration(statement)) return ts2.isExternalModuleReference(statement.moduleReference) && isReactModuleName(statement.moduleReference.expression);
40
+ if (ts2.isVariableStatement(statement)) return statement.declarationList.declarations.some((declaration) => !!declaration.initializer && isRequireOfReact(declaration.initializer));
41
+ return false;
42
+ });
43
+ }
44
+ function isRequireOfReact(expression) {
45
+ if (ts2.isPropertyAccessExpression(expression)) return isRequireOfReact(expression.expression);
46
+ const isRequireCall = ts2.isCallExpression(expression) && ts2.isIdentifier(expression.expression) && expression.expression.text === "require";
47
+ return isRequireCall && expression.arguments.length === 1 && isReactModuleName(expression.arguments[0]);
48
+ }
49
+ function isReactModuleName(expression) {
50
+ return ts2.isStringLiteral(expression) && REACT_MODULE.test(expression.text);
51
+ }
52
+ var COMPONENT_WRAPPERS = /* @__PURE__ */ new Set(["memo", "forwardRef", "React.memo", "React.forwardRef"]);
53
+ var COMPONENT_BASE_CLASSES = /* @__PURE__ */ new Set(["Component", "PureComponent", "React.Component", "React.PureComponent"]);
54
+ var isComponentName = (name) => /^[A-Z]/.test(name);
55
+ function returnsJsx(fn) {
56
+ if (!fn.body) return false;
57
+ if (!ts2.isBlock(fn.body)) return isJsxValue(fn.body);
58
+ const hasJsxReturn = (node) => {
59
+ if (ts2.isReturnStatement(node)) return !!node.expression && isJsxValue(node.expression);
60
+ if (ts2.isFunctionLike(node) || ts2.isClassLike(node)) return false;
61
+ return !!ts2.forEachChild(node, (child) => hasJsxReturn(child) || void 0);
62
+ };
63
+ return hasJsxReturn(fn.body);
64
+ }
65
+ function isJsxValue(expression) {
66
+ const value = unwrap(expression);
67
+ if (isJsx(value)) return true;
68
+ if (ts2.isConditionalExpression(value)) return isJsxValue(value.whenTrue) || isJsxValue(value.whenFalse);
69
+ if (ts2.isBinaryExpression(value)) return isJsxValue(value.left) || isJsxValue(value.right);
70
+ return false;
71
+ }
72
+ function componentFunctionOf(initializer) {
73
+ const value = unwrap(initializer);
74
+ if (ts2.isArrowFunction(value) || ts2.isFunctionExpression(value)) return returnsJsx(value) ? value : void 0;
75
+ const isWrapperCall = ts2.isCallExpression(value) && value.arguments.length > 0 && COMPONENT_WRAPPERS.has(value.expression.getText());
76
+ if (!isWrapperCall) return void 0;
77
+ const wrapped = unwrap(value.arguments[0]);
78
+ if (ts2.isArrowFunction(wrapped) || ts2.isFunctionExpression(wrapped)) return wrapped;
79
+ return componentFunctionOf(wrapped);
80
+ }
81
+ function isComponentClass(cls) {
82
+ const extendsClause = cls.heritageClauses?.find((clause) => clause.token === ts2.SyntaxKind.ExtendsKeyword);
83
+ const baseClass = extendsClause?.types[0]?.expression.getText();
84
+ return !!baseClass && COMPONENT_BASE_CLASSES.has(baseClass);
85
+ }
86
+ var STATE_HOOKS = /* @__PURE__ */ new Set(["useState", "useReducer", "React.useState", "React.useReducer"]);
87
+ function stateHookOf(declaration) {
88
+ const call = declaration.initializer && unwrap(declaration.initializer);
89
+ const isStateHookCall = !!call && ts2.isCallExpression(call) && STATE_HOOKS.has(call.expression.getText());
90
+ if (!isStateHookCall) return void 0;
91
+ if (ts2.isIdentifier(declaration.name)) return { call, value: declaration };
92
+ if (!ts2.isArrayBindingPattern(declaration.name)) return void 0;
93
+ const [value, setter] = declaration.name.elements.map((element) => ts2.isBindingElement(element) && ts2.isIdentifier(element.name) ? element : void 0);
94
+ return value || setter ? { call, value, setter } : void 0;
95
+ }
96
+ var isHookName = (name) => /^use[A-Z0-9]/.test(name);
97
+
98
+ // src/parse.ts
3
99
  var PREVIEW_LINES = 6;
4
100
  var CONTAINER_PREVIEW_LINES = 3;
5
101
  var PREVIEW_WIDTH = 64;
@@ -15,11 +111,12 @@ function parseStructure(path, text, prev) {
15
111
  let sf;
16
112
  if (old && !old.reused) {
17
113
  old.reused = true;
18
- sf = ts.updateSourceFile(old.sf, text, changeRange(prev.text, text));
114
+ sf = ts3.updateSourceFile(old.sf, text, changeRange(prev.text, text));
19
115
  } else {
20
- sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, kind);
116
+ sf = ts3.createSourceFile(path, text, ts3.ScriptTarget.Latest, true, kind);
21
117
  }
22
- const b = new Builder(sf, text);
118
+ const language = fileLanguageOf(path, sf);
119
+ const b = new Builder(sf, text, language.isReact);
23
120
  b.collectStatements(sf.statements, null, 0);
24
121
  const syntax = sf.parseDiagnostics ?? [];
25
122
  const model = {
@@ -31,7 +128,8 @@ function parseStructure(path, text, prev) {
31
128
  syntaxErrors: syntax.length,
32
129
  firstErrorLine: -1,
33
130
  diagnostics: [],
34
- indentUnit: detectIndentUnit(text)
131
+ indentUnit: detectIndentUnit(text),
132
+ language
35
133
  };
36
134
  model.diagnostics = syntax.map((d) => toCodeDiagnostic(model, sf, d, "syntax"));
37
135
  model.firstErrorLine = model.diagnostics[0]?.line ?? -1;
@@ -54,9 +152,9 @@ function toCodeDiagnostic(model, sf, d, source) {
54
152
  start,
55
153
  length: d.length ?? 0,
56
154
  line: sf.getLineAndCharacterOfPosition(Math.min(start, sf.text.length)).line + 1,
57
- message: ts.flattenDiagnosticMessageText(d.messageText, "\n"),
155
+ message: ts3.flattenDiagnosticMessageText(d.messageText, "\n"),
58
156
  source,
59
- severity: d.category === ts.DiagnosticCategory.Error ? "error" : "warning",
157
+ severity: d.category === ts3.DiagnosticCategory.Error ? "error" : "warning",
60
158
  blockId: blockAt(model, start)?.id ?? null
61
159
  };
62
160
  }
@@ -77,29 +175,29 @@ function changeRange(a, b) {
77
175
  while (s < n && a.charCodeAt(s) === b.charCodeAt(s)) s++;
78
176
  let e = 0;
79
177
  while (e < n - s && a.charCodeAt(a.length - 1 - e) === b.charCodeAt(b.length - 1 - e)) e++;
80
- return ts.createTextChangeRange(ts.createTextSpan(s, a.length - e - s), b.length - e - s);
178
+ return ts3.createTextChangeRange(ts3.createTextSpan(s, a.length - e - s), b.length - e - s);
81
179
  }
82
180
  function commonJsExportedFunction(s) {
83
- if (!ts.isExpressionStatement(s) || !ts.isBinaryExpression(s.expression) || s.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken) return null;
181
+ if (!ts3.isExpressionStatement(s) || !ts3.isBinaryExpression(s.expression) || s.expression.operatorToken.kind !== ts3.SyntaxKind.EqualsToken) return null;
84
182
  const { left, right } = s.expression;
85
183
  const fn = unwrap(right);
86
- const isFunction = ts.isFunctionExpression(fn) || ts.isArrowFunction(fn);
87
- if (!isFunction || !ts.isPropertyAccessExpression(left) || !ts.isIdentifier(left.name)) return null;
184
+ const isFunction = ts3.isFunctionExpression(fn) || ts3.isArrowFunction(fn);
185
+ if (!isFunction || !ts3.isPropertyAccessExpression(left) || !ts3.isIdentifier(left.name)) return null;
88
186
  const target = left.expression.getText();
89
187
  const isExportsObject = target === "exports" || target === "module.exports";
90
188
  return isExportsObject ? { assignment: s.expression, exportedName: left.name, fn } : null;
91
189
  }
92
190
  function scriptKindOf(path) {
93
- if (/\.[cm]?jsx?$/.test(path)) return path.endsWith("x") ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
94
- return path.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
191
+ if (/\.[cm]?jsx?$/.test(path)) return path.endsWith("x") ? ts3.ScriptKind.JSX : ts3.ScriptKind.JS;
192
+ return path.endsWith("x") ? ts3.ScriptKind.TSX : ts3.ScriptKind.TS;
95
193
  }
96
194
  function createProgram(sf) {
97
195
  const options = {
98
196
  noLib: true,
99
197
  noResolve: true,
100
198
  allowJs: true,
101
- target: ts.ScriptTarget.Latest,
102
- jsx: ts.JsxEmit.Preserve,
199
+ target: ts3.ScriptTarget.Latest,
200
+ jsx: ts3.JsxEmit.Preserve,
103
201
  types: []
104
202
  };
105
203
  const host = {
@@ -115,7 +213,7 @@ function createProgram(sf) {
115
213
  readFile: () => void 0,
116
214
  getDirectories: () => []
117
215
  };
118
- return ts.createProgram({ rootNames: [sf.fileName], options, host });
216
+ return ts3.createProgram({ rootNames: [sf.fileName], options, host });
119
217
  }
120
218
  function detectIndentUnit(text) {
121
219
  const m = /\n(\t+|[ ]{2,8})\S/.exec(text);
@@ -123,12 +221,14 @@ function detectIndentUnit(text) {
123
221
  return m[1].startsWith(" ") ? " " : m[1].length >= 4 && m[1].length % 4 === 0 ? " " : " ";
124
222
  }
125
223
  var Builder = class {
126
- constructor(sf, text) {
224
+ constructor(sf, text, isReact) {
127
225
  this.sf = sf;
128
226
  this.text = text;
227
+ this.isReact = isReact;
129
228
  }
130
229
  sf;
131
230
  text;
231
+ isReact;
132
232
  roots = [];
133
233
  byId = /* @__PURE__ */ new Map();
134
234
  /**
@@ -140,6 +240,8 @@ var Builder = class {
140
240
  rangeToBlock = /* @__PURE__ */ new Map();
141
241
  /** Every name a usage could resolve to. Other identifiers skip the (slow) checker. */
142
242
  names = /* @__PURE__ */ new Set();
243
+ /** Setters of React state (`setCount`), keyed like `declToBlock`: using one writes the state. */
244
+ stateSetters = /* @__PURE__ */ new Set();
143
245
  // ---------- structure ----------
144
246
  collectStatements(stmts, parent, depth, declarationsOnly = false) {
145
247
  let group = [];
@@ -168,21 +270,23 @@ var Builder = class {
168
270
  };
169
271
  for (let i = 0; i < stmts.length; i++) {
170
272
  const s = stmts[i];
171
- if (ts.isFunctionDeclaration(s)) {
273
+ if (ts3.isFunctionDeclaration(s)) {
172
274
  flushGroup();
173
275
  const overloads = [s];
174
276
  while (!overloads[overloads.length - 1].body) {
175
277
  const next = stmts[i + 1];
176
- if (next && ts.isFunctionDeclaration(next) && next.name?.text === s.name?.text) {
278
+ if (next && ts3.isFunctionDeclaration(next) && next.name?.text === s.name?.text) {
177
279
  overloads.push(next);
178
280
  i++;
179
281
  } else break;
180
282
  }
181
- this.addFunctionLike(overloads, s.name?.text ?? "default", parent, depth);
182
- } else if (ts.isClassDeclaration(s)) {
283
+ const name = s.name?.text ?? "default";
284
+ const isComponent = this.isReact && isComponentName(name) && returnsJsx(overloads[overloads.length - 1]);
285
+ this.addFunctionLike(overloads, name, parent, depth, { kind: isComponent ? "component" : "function" });
286
+ } else if (ts3.isClassDeclaration(s)) {
183
287
  flushGroup();
184
288
  this.addClass(s, s, s.name?.text ?? "default", parent, depth);
185
- } else if (ts.isVariableStatement(s)) {
289
+ } else if (ts3.isVariableStatement(s)) {
186
290
  flushGroup();
187
291
  this.addVariableStatement(s, parent, depth, declarationsOnly);
188
292
  } else if (commonJsExportedFunction(s)) {
@@ -192,14 +296,14 @@ var Builder = class {
192
296
  this.addFunctionLike([fn], exportedName.text, parent, depth, { rangeNodes: [s], declNodes, extraTags: ["export"], nameNode: exportedName });
193
297
  } else if (declarationsOnly) {
194
298
  continue;
195
- } else if (ts.isImportDeclaration(s) || ts.isImportEqualsDeclaration(s)) {
299
+ } else if (ts3.isImportDeclaration(s) || ts3.isImportEqualsDeclaration(s)) {
196
300
  flushGroup();
197
301
  this.addImport(s, parent, depth);
198
- } else if (ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s) || ts.isEnumDeclaration(s)) {
302
+ } else if (ts3.isInterfaceDeclaration(s) || ts3.isTypeAliasDeclaration(s) || ts3.isEnumDeclaration(s)) {
199
303
  flushGroup();
200
- const keyword = ts.isInterfaceDeclaration(s) ? "interface" : ts.isEnumDeclaration(s) ? "enum" : "type";
304
+ const keyword = ts3.isInterfaceDeclaration(s) ? "interface" : ts3.isEnumDeclaration(s) ? "enum" : "type";
201
305
  const typeParams = "typeParameters" in s && s.typeParameters ? `<${s.typeParameters.map((t) => t.getText()).join(", ")}>` : "";
202
- const bodyStart = ts.isTypeAliasDeclaration(s) ? s.type.getStart(this.sf) : this.text.indexOf("{", s.name.end);
306
+ const bodyStart = ts3.isTypeAliasDeclaration(s) ? s.type.getStart(this.sf) : this.text.indexOf("{", s.name.end);
203
307
  this.add(
204
308
  {
205
309
  kind: "type",
@@ -217,7 +321,7 @@ var Builder = class {
217
321
  parent,
218
322
  depth
219
323
  );
220
- } else if (ts.isModuleDeclaration(s) && s.body && ts.isModuleBlock(s.body)) {
324
+ } else if (ts3.isModuleDeclaration(s) && s.body && ts3.isModuleBlock(s.body)) {
221
325
  flushGroup();
222
326
  const body = s.body;
223
327
  this.add(
@@ -247,13 +351,13 @@ var Builder = class {
247
351
  addImport(s, parent, depth) {
248
352
  let spec;
249
353
  let names = [];
250
- if (ts.isImportDeclaration(s)) {
354
+ if (ts3.isImportDeclaration(s)) {
251
355
  spec = s.moduleSpecifier.text;
252
356
  const clause = s.importClause;
253
357
  if (clause?.name) names.push(clause.name.text);
254
358
  const nb = clause?.namedBindings;
255
- if (nb && ts.isNamespaceImport(nb)) names.push("* as " + nb.name.text);
256
- if (nb && ts.isNamedImports(nb)) names.push(...nb.elements.map((e) => e.name.text));
359
+ if (nb && ts3.isNamespaceImport(nb)) names.push("* as " + nb.name.text);
360
+ if (nb && ts3.isNamedImports(nb)) names.push(...nb.elements.map((e) => e.name.text));
257
361
  if (!clause) names = ["(side effect)"];
258
362
  } else {
259
363
  spec = s.name.text;
@@ -267,7 +371,7 @@ var Builder = class {
267
371
  key: "import:" + spec,
268
372
  name: spec,
269
373
  detail: "",
270
- tags: ts.isImportDeclaration(s) && s.importClause?.isTypeOnly ? ["type"] : [],
374
+ tags: ts3.isImportDeclaration(s) && s.importClause?.isTypeOnly ? ["type"] : [],
271
375
  rangeNodes: [s],
272
376
  declNodes: [s],
273
377
  previewRange: null,
@@ -286,11 +390,11 @@ var Builder = class {
286
390
  let previewRange = null;
287
391
  let insert = null;
288
392
  let insertAt = -1;
289
- if (body && ts.isBlock(body)) {
393
+ if (body && ts3.isBlock(body)) {
290
394
  previewRange = [body.getStart(this.sf) + 1, body.end - 1];
291
395
  insert = "body";
292
396
  const last = body.statements[body.statements.length - 1];
293
- insertAt = last && ts.isReturnStatement(last) ? this.lineStartOf(last.getStart(this.sf)) : body.end - 1;
397
+ insertAt = last && ts3.isReturnStatement(last) ? this.lineStartOf(last.getStart(this.sf)) : body.end - 1;
294
398
  } else if (body) {
295
399
  previewRange = [body.getStart(this.sf), body.end];
296
400
  }
@@ -298,11 +402,12 @@ var Builder = class {
298
402
  const ret = sig.type ? ": " + sig.type.getText() : "";
299
403
  const detail = squash(`${typeParams}(${sig.parameters.map((p) => p.getText()).join(", ")})${ret}`);
300
404
  const tags = [...opts.extraTags ?? [], ...modifierTags(nodes[0])];
405
+ if (this.isReact && isHookName(name)) tags.push("hook");
301
406
  if (nodes.length > 1) tags.push(`${nodes.length - 1} overload${nodes.length > 2 ? "s" : ""}`);
302
407
  if (impl.asteriskToken) tags.push("generator");
303
408
  this.add(
304
409
  {
305
- kind: "function",
410
+ kind: opts.kind ?? "function",
306
411
  key: opts.key ?? name,
307
412
  name,
308
413
  detail,
@@ -313,7 +418,7 @@ var Builder = class {
313
418
  previewRange,
314
419
  insert,
315
420
  insertAt,
316
- children: body && ts.isBlock(body) ? (blk) => this.collectStatements(body.statements, blk, depth + 1, true) : void 0
421
+ children: body && ts3.isBlock(body) ? (blk) => this.collectStatements(body.statements, blk, depth + 1, true) : void 0
317
422
  },
318
423
  parent,
319
424
  depth
@@ -325,7 +430,7 @@ var Builder = class {
325
430
  const typeParams = cls.typeParameters?.length ? `<${cls.typeParameters.map((t) => t.getText()).join(", ")}>` : "";
326
431
  this.add(
327
432
  {
328
- kind: "class",
433
+ kind: this.isReact && isComponentClass(cls) ? "component" : "class",
329
434
  key: name,
330
435
  name,
331
436
  detail: squash(typeParams + (heritage ? " " + heritage : "")),
@@ -345,26 +450,26 @@ var Builder = class {
345
450
  collectMembers(members, parent, depth) {
346
451
  for (let i = 0; i < members.length; i++) {
347
452
  const m = members[i];
348
- if (ts.isMethodDeclaration(m) || ts.isConstructorDeclaration(m)) {
349
- const name = ts.isConstructorDeclaration(m) ? "constructor" : m.name.getText();
453
+ if (ts3.isMethodDeclaration(m) || ts3.isConstructorDeclaration(m)) {
454
+ const name = ts3.isConstructorDeclaration(m) ? "constructor" : m.name.getText();
350
455
  const overloads = [m];
351
456
  while (!overloads[overloads.length - 1].body) {
352
457
  const next = members[i + 1];
353
- const nextName = next && (ts.isConstructorDeclaration(next) ? "constructor" : ts.isMethodDeclaration(next) ? next.name.getText() : null);
458
+ const nextName = next && (ts3.isConstructorDeclaration(next) ? "constructor" : ts3.isMethodDeclaration(next) ? next.name.getText() : null);
354
459
  if (nextName === name) {
355
460
  overloads.push(next);
356
461
  i++;
357
462
  } else break;
358
463
  }
359
464
  this.addFunctionLike(overloads, name, parent, depth);
360
- } else if (ts.isGetAccessor(m) || ts.isSetAccessor(m)) {
465
+ } else if (ts3.isGetAccessor(m) || ts3.isSetAccessor(m)) {
361
466
  const name = m.name.getText();
362
- const word = ts.isGetAccessor(m) ? "get" : "set";
467
+ const word = ts3.isGetAccessor(m) ? "get" : "set";
363
468
  this.addFunctionLike([m], name, parent, depth, { key: `${word} ${name}`, extraTags: [word] });
364
- } else if (ts.isPropertyDeclaration(m)) {
469
+ } else if (ts3.isPropertyDeclaration(m)) {
365
470
  const name = m.name.getText();
366
471
  const init = m.initializer && unwrap(m.initializer);
367
- if (init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) {
472
+ if (init && (ts3.isArrowFunction(init) || ts3.isFunctionExpression(init))) {
368
473
  this.addFunctionLike([init], name, parent, depth, { rangeNodes: [m], declNodes: [m], extraTags: modifierTags(m), nameNode: m.name });
369
474
  } else {
370
475
  this.addVariable(m, m, name, m.type ? ": " + m.type.getText() : "", modifierTags(m), parent, depth, m.initializer);
@@ -374,25 +479,61 @@ var Builder = class {
374
479
  }
375
480
  addVariableStatement(s, parent, depth, functionsOnly) {
376
481
  const decls = s.declarationList.declarations;
377
- const keyword = s.declarationList.flags & ts.NodeFlags.Const ? "const" : s.declarationList.flags & ts.NodeFlags.Let ? "let" : "var";
482
+ const keyword = s.declarationList.flags & ts3.NodeFlags.Const ? "const" : s.declarationList.flags & ts3.NodeFlags.Let ? "let" : "var";
378
483
  for (const d of decls) {
379
484
  const rangeNode = decls.length === 1 ? s : d;
380
485
  const name = d.name.getText();
381
486
  const tags = [...modifierTags(s), keyword];
487
+ const stateHook = this.isReact ? stateHookOf(d) : void 0;
488
+ if (stateHook) {
489
+ this.addState(stateHook, rangeNode, tags, parent, depth);
490
+ continue;
491
+ }
382
492
  const init = d.initializer && unwrap(d.initializer);
383
- if (init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) {
493
+ const componentFunction = this.isReact && init && isComponentName(name) ? componentFunctionOf(init) : void 0;
494
+ if (componentFunction) {
495
+ this.addFunctionLike([componentFunction], name, parent, depth, { kind: "component", rangeNodes: [rangeNode], declNodes: [d], extraTags: tags, nameNode: d.name });
496
+ } else if (init && (ts3.isArrowFunction(init) || ts3.isFunctionExpression(init))) {
384
497
  this.addFunctionLike([init], name, parent, depth, { rangeNodes: [rangeNode], declNodes: [d], extraTags: tags, nameNode: d.name });
385
- } else if (init && ts.isClassExpression(init)) {
498
+ } else if (init && ts3.isClassExpression(init)) {
386
499
  this.addClass(init, rangeNode, name, parent, depth, [d, init], tags);
387
500
  } else if (!functionsOnly) {
388
501
  this.addVariable(d, rangeNode, name, d.type ? ": " + d.type.getText() : "", tags, parent, depth, d.initializer);
389
502
  }
390
503
  }
391
504
  }
505
+ /** `const [count, setCount] = useState(0)`: a state block named `count`; using `setCount` writes it. */
506
+ addState(hook, rangeNode, tags, parent, depth) {
507
+ const named = hook.value ?? hook.setter;
508
+ const setterName = hook.setter?.name.getText();
509
+ const [firstArg, ...otherArgs] = hook.call.arguments;
510
+ const args = firstArg ? truncate(squash(firstArg.getText()), 24) + (otherArgs.length ? ", \u2026" : "") : "";
511
+ if (hook.setter && setterName) {
512
+ this.stateSetters.add(nodeKey(hook.setter));
513
+ this.names.add(setterName);
514
+ }
515
+ this.add(
516
+ {
517
+ kind: "state",
518
+ key: named.name.getText(),
519
+ name: named.name.getText(),
520
+ detail: `${hook.call.expression.getText()}(${args})`,
521
+ tags: setterName && hook.value ? [...tags, setterName] : tags,
522
+ rangeNodes: [rangeNode],
523
+ declNodes: [hook.value, hook.setter].filter((declaration) => !!declaration),
524
+ nameNode: named.name,
525
+ previewRange: [hook.call.getStart(this.sf), hook.call.end],
526
+ insert: null,
527
+ insertAt: -1
528
+ },
529
+ parent,
530
+ depth
531
+ );
532
+ }
392
533
  addVariable(decl, rangeNode, name, type, tags, parent, depth, initializer) {
393
534
  const init = initializer && unwrap(initializer);
394
- const objectChildren = init && ts.isObjectLiteralExpression(init) ? init.properties.filter(
395
- (p) => ts.isMethodDeclaration(p) || ts.isPropertyAssignment(p) && (ts.isArrowFunction(unwrap(p.initializer)) || ts.isFunctionExpression(unwrap(p.initializer)))
535
+ const objectChildren = init && ts3.isObjectLiteralExpression(init) ? init.properties.filter(
536
+ (p) => ts3.isMethodDeclaration(p) || ts3.isPropertyAssignment(p) && (ts3.isArrowFunction(unwrap(p.initializer)) || ts3.isFunctionExpression(unwrap(p.initializer)))
396
537
  ) : [];
397
538
  this.add(
398
539
  {
@@ -410,7 +551,7 @@ var Builder = class {
410
551
  children: objectChildren.length ? (blk) => {
411
552
  for (const p of objectChildren) {
412
553
  const pname = p.name.getText();
413
- if (ts.isMethodDeclaration(p)) this.addFunctionLike([p], pname, blk, depth + 1);
554
+ if (ts3.isMethodDeclaration(p)) this.addFunctionLike([p], pname, blk, depth + 1);
414
555
  else {
415
556
  const fn = unwrap(p.initializer);
416
557
  this.addFunctionLike([fn], pname, blk, depth + 1, { rangeNodes: [p], declNodes: [p], nameNode: p.name });
@@ -472,7 +613,7 @@ var Builder = class {
472
613
  /** Include doc comments directly above the node (no blank line in between). */
473
614
  startWithComments(node) {
474
615
  let start = node.getStart(this.sf);
475
- const comments = ts.getLeadingCommentRanges(this.text, node.pos) ?? [];
616
+ const comments = ts3.getLeadingCommentRanges(this.text, node.pos) ?? [];
476
617
  for (let i = comments.length - 1; i >= 0; i--) {
477
618
  const gap = this.text.slice(comments[i].end, start);
478
619
  if (/\S/.test(gap) || (gap.match(/\n/g)?.length ?? 0) > 1) break;
@@ -500,8 +641,8 @@ var Builder = class {
500
641
  const visit = (node, current) => {
501
642
  const blk = this.rangeToBlock.get(nodeKey(node));
502
643
  if (blk) current = blk;
503
- if (ts.isIdentifier(node) && current) this.recordUsage(node, current, checker, byPair, otherFiles);
504
- ts.forEachChild(node, (c) => visit(c, current));
644
+ if (ts3.isIdentifier(node) && current) this.recordUsage(node, current, checker, byPair, otherFiles);
645
+ ts3.forEachChild(node, (c) => visit(c, current));
505
646
  };
506
647
  visit(sf, null);
507
648
  return [...byPair.values()];
@@ -510,29 +651,30 @@ var Builder = class {
510
651
  const isKnownName = this.names.has(id.text) || !!otherFiles?.projectNames.has(id.text);
511
652
  if (!isKnownName) return;
512
653
  const parent = id.parent;
513
- const isDeclarationName = !!parent && parent.name === id && !ts.isPropertyAccessExpression(parent) && !ts.isShorthandPropertyAssignment(parent);
654
+ const isDeclarationName = !!parent && parent.name === id && !ts3.isPropertyAccessExpression(parent) && !ts3.isShorthandPropertyAssignment(parent);
514
655
  if (isDeclarationName) return;
515
656
  let sym;
516
657
  try {
517
- sym = parent && ts.isShorthandPropertyAssignment(parent) ? checker.getShorthandAssignmentValueSymbol(parent) : checker.getSymbolAtLocation(id);
658
+ sym = parent && ts3.isShorthandPropertyAssignment(parent) ? checker.getShorthandAssignmentValueSymbol(parent) : checker.getSymbolAtLocation(id);
518
659
  } catch {
519
660
  return;
520
661
  }
521
662
  if (!sym?.declarations) return;
522
- const kind = usageKind(id);
663
+ let kind = usageKind(id);
523
664
  let to;
524
665
  let importBlockId;
525
666
  for (const d of sym.declarations) {
526
667
  to = this.declToBlock.get(nodeKey(d));
668
+ if (this.stateSetters.has(nodeKey(d))) kind = "write";
527
669
  if (!to) {
528
670
  let n = d;
529
- while (n && !ts.isImportDeclaration(n) && !ts.isImportEqualsDeclaration(n) && !ts.isSourceFile(n)) n = n.parent;
530
- if (n && !ts.isSourceFile(n)) importBlockId = to = this.declToBlock.get(nodeKey(n));
671
+ while (n && !ts3.isImportDeclaration(n) && !ts3.isImportEqualsDeclaration(n) && !ts3.isSourceFile(n)) n = n.parent;
672
+ if (n && !ts3.isSourceFile(n)) importBlockId = to = this.declToBlock.get(nodeKey(n));
531
673
  }
532
674
  if (to) break;
533
675
  }
534
676
  if (otherFiles) {
535
- const isImportedName = !!importBlockId && (sym.flags & ts.SymbolFlags.Alias) !== 0;
677
+ const isImportedName = !!importBlockId && (sym.flags & ts3.SymbolFlags.Alias) !== 0;
536
678
  const target = isImportedName ? safeAliasedSymbol(checker, sym) : to ? void 0 : sym;
537
679
  const declaredElsewhere = target?.declarations?.find((d) => d.getSourceFile() !== id.getSourceFile());
538
680
  const external = declaredElsewhere && otherFiles.blockInOtherFile(declaredElsewhere);
@@ -549,6 +691,7 @@ function addUsage(out, usage) {
549
691
  if (!existing) return void out.set(pairKey, usage);
550
692
  existing.count++;
551
693
  if (usage.kind === "call") existing.kind = "call";
694
+ else if (usage.kind === "write" && existing.kind === "read") existing.kind = "write";
552
695
  }
553
696
  function safeAliasedSymbol(checker, symbol) {
554
697
  try {
@@ -561,23 +704,26 @@ function nodeKey(node) {
561
704
  return `${node.kind}:${node.pos}:${node.end}`;
562
705
  }
563
706
  function usageKind(id) {
707
+ if (isJsxTagName(id)) return "render";
564
708
  let n = id;
565
- if (ts.isPropertyAccessExpression(n.parent) && n.parent.name === n) n = n.parent;
709
+ if (ts3.isPropertyAccessExpression(n.parent) && n.parent.name === n) n = n.parent;
566
710
  const p = n.parent;
567
- if ((ts.isCallExpression(p) || ts.isNewExpression(p)) && p.expression === n) return "call";
568
- if (ts.isTaggedTemplateExpression(p) && p.tag === n) return "call";
569
- if (ts.isJsxOpeningLikeElement(p) && p.tagName === n) return "call";
570
- if (ts.isBinaryExpression(p) && p.left === n && p.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && p.operatorToken.kind <= ts.SyntaxKind.LastAssignment) return "write";
571
- if ((ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p)) && (p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken)) return "write";
572
- if (ts.isTypeReferenceNode(p) || ts.isExpressionWithTypeArguments(p) || ts.isTypeQueryNode(p) || ts.isQualifiedName(p)) return "type";
711
+ if ((ts3.isCallExpression(p) || ts3.isNewExpression(p)) && p.expression === n) return "call";
712
+ if (ts3.isTaggedTemplateExpression(p) && p.tag === n) return "call";
713
+ if (ts3.isJsxOpeningLikeElement(p) && p.tagName === n) return "render";
714
+ if (ts3.isBinaryExpression(p) && p.left === n && p.operatorToken.kind >= ts3.SyntaxKind.FirstAssignment && p.operatorToken.kind <= ts3.SyntaxKind.LastAssignment) return "write";
715
+ if ((ts3.isPrefixUnaryExpression(p) || ts3.isPostfixUnaryExpression(p)) && (p.operator === ts3.SyntaxKind.PlusPlusToken || p.operator === ts3.SyntaxKind.MinusMinusToken)) return "write";
716
+ if (ts3.isTypeReferenceNode(p) || ts3.isExpressionWithTypeArguments(p) || ts3.isTypeQueryNode(p) || ts3.isQualifiedName(p)) return "type";
573
717
  return "read";
574
718
  }
575
- function unwrap(e) {
576
- while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isSatisfiesExpression(e) || ts.isTypeAssertionExpression(e)) e = e.expression;
577
- return e;
719
+ function isJsxTagName(id) {
720
+ let tag = id;
721
+ while (ts3.isPropertyAccessExpression(tag.parent) && tag.parent.expression === tag) tag = tag.parent;
722
+ const element = tag.parent;
723
+ return (ts3.isJsxOpeningLikeElement(element) || ts3.isJsxClosingElement(element)) && element.tagName === tag;
578
724
  }
579
725
  function modifierTags(n) {
580
- const mods = ts.canHaveModifiers(n) ? ts.getModifiers(n) ?? [] : [];
726
+ const mods = ts3.canHaveModifiers(n) ? ts3.getModifiers(n) ?? [] : [];
581
727
  return mods.map((m) => m.getText());
582
728
  }
583
729
  function squash(s) {
@@ -625,13 +771,13 @@ function makePreview(text, range, children, max) {
625
771
  }
626
772
 
627
773
  // src/project.ts
628
- import ts3 from "typescript";
774
+ import ts5 from "typescript";
629
775
 
630
776
  // src/workspace.ts
631
- import ts2 from "typescript";
777
+ import ts4 from "typescript";
632
778
  function parseConfig(config, projectPaths) {
633
779
  const configFileName = "/" + config.path;
634
- const json = ts2.parseConfigFileTextToJson(configFileName, config.text);
780
+ const json = ts4.parseConfigFileTextToJson(configFileName, config.text);
635
781
  if (json.error || !json.config) return null;
636
782
  const readConfig = (name) => name === configFileName ? config.text : config.extendedFiles?.[name.slice(1)];
637
783
  const host = {
@@ -641,7 +787,7 @@ function parseConfig(config, projectPaths) {
641
787
  readFile: readConfig
642
788
  };
643
789
  const folder = config.path.slice(0, config.path.lastIndexOf("/") + 1);
644
- const parsed = ts2.parseJsonConfigFileContent(json.config, host, "/" + folder, void 0, configFileName);
790
+ const parsed = ts4.parseJsonConfigFileContent(json.config, host, "/" + folder, void 0, configFileName);
645
791
  const included = new Set(parsed.fileNames.map((name) => name.slice(1)));
646
792
  return { path: config.path, folder, options: parsed.options, includes: (projectPath) => included.has(projectPath) };
647
793
  }
@@ -743,7 +889,7 @@ var Project = class {
743
889
  /** Language services by config path, created when a file of that config is first resolved. */
744
890
  services = /* @__PURE__ */ new Map();
745
891
  /** Shared by the services, so a file several programs include is parsed once. */
746
- documentRegistry = ts3.createDocumentRegistry(true, "/");
892
+ documentRegistry = ts5.createDocumentRegistry(true, "/");
747
893
  /** Changes when files are added or removed, or the settings change: where imports point may change then. */
748
894
  fileSetVersion = 0;
749
895
  /** Load the standard library later, e.g. once it has been downloaded. Type errors show from then on. */
@@ -777,11 +923,11 @@ var Project = class {
777
923
  compilerOptions(configOptions, packagePaths) {
778
924
  const paths = { ...packagePaths, ...configOptions.paths };
779
925
  return {
780
- target: ts3.ScriptTarget.ES2022,
781
- module: ts3.ModuleKind.ESNext,
782
- moduleResolution: ts3.ModuleResolutionKind.Bundler,
926
+ target: ts5.ScriptTarget.ES2022,
927
+ module: ts5.ModuleKind.ESNext,
928
+ moduleResolution: ts5.ModuleResolutionKind.Bundler,
783
929
  strict: true,
784
- jsx: ts3.JsxEmit.Preserve,
930
+ jsx: ts5.JsxEmit.Preserve,
785
931
  skipLibCheck: true,
786
932
  allowImportingTsExtensions: true,
787
933
  ...configOptions,
@@ -827,7 +973,7 @@ var Project = class {
827
973
  serviceOfConfig(configPath) {
828
974
  let service = this.services.get(configPath);
829
975
  if (!service) {
830
- service = ts3.createLanguageService(this.createHost(configPath), this.documentRegistry);
976
+ service = ts5.createLanguageService(this.createHost(configPath), this.documentRegistry);
831
977
  this.services.set(configPath, service);
832
978
  }
833
979
  return service;
@@ -996,7 +1142,7 @@ var Project = class {
996
1142
  const reExportedPaths = /* @__PURE__ */ new Set();
997
1143
  const host = this.moduleResolutionHost();
998
1144
  const resolve = (specifier) => {
999
- const result = ts3.resolveModuleName(specifier, toFileName(path), this.optionsOfFile(path), host).resolvedModule;
1145
+ const result = ts5.resolveModuleName(specifier, toFileName(path), this.optionsOfFile(path), host).resolvedModule;
1000
1146
  return result ? fromFileName(result.resolvedFileName) : null;
1001
1147
  };
1002
1148
  for (const block of file.model.roots) {
@@ -1050,7 +1196,7 @@ var Project = class {
1050
1196
  getScriptSnapshot: (name) => {
1051
1197
  if (name.startsWith(LIB_DIR)) {
1052
1198
  const text = this.libFiles[name.slice(LIB_DIR.length)];
1053
- return text === void 0 ? void 0 : ts3.ScriptSnapshot.fromString(text);
1199
+ return text === void 0 ? void 0 : ts5.ScriptSnapshot.fromString(text);
1054
1200
  }
1055
1201
  const path = fromFileName(name);
1056
1202
  const file = path ? this.files.get(path) : void 0;
@@ -1064,7 +1210,7 @@ var Project = class {
1064
1210
  const containingPath = fromFileName(containingFile);
1065
1211
  const options = this.optionsOfFile(containingPath);
1066
1212
  const host = this.moduleResolutionHost();
1067
- return moduleLiterals.map((literal) => ts3.resolveModuleName(literal.text, containingFile, options, host));
1213
+ return moduleLiterals.map((literal) => ts5.resolveModuleName(literal.text, containingFile, options, host));
1068
1214
  },
1069
1215
  getDefaultLibFileName: () => LIB_DIR + "lib.d.ts",
1070
1216
  fileExists: (name) => this.fileExists(name),
@@ -1100,7 +1246,7 @@ function changedRegion(before, after) {
1100
1246
  while (start < shorter && before.charCodeAt(start) === after.charCodeAt(start)) start++;
1101
1247
  let fromEnd = 0;
1102
1248
  while (fromEnd < shorter - start && before.charCodeAt(before.length - 1 - fromEnd) === after.charCodeAt(after.length - 1 - fromEnd)) fromEnd++;
1103
- return ts3.createTextChangeRange(ts3.createTextSpan(start, before.length - fromEnd - start), after.length - fromEnd - start);
1249
+ return ts5.createTextChangeRange(ts5.createTextSpan(start, before.length - fromEnd - start), after.length - fromEnd - start);
1104
1250
  }
1105
1251
  function toFileName(path) {
1106
1252
  return "/" + path;
@@ -1124,10 +1270,10 @@ function namesVisibleFrom(model, block) {
1124
1270
  return names;
1125
1271
  }
1126
1272
  function isValidIdentifier(name) {
1127
- const scanner = ts3.createScanner(ts3.ScriptTarget.Latest, true, ts3.LanguageVariant.Standard, name);
1273
+ const scanner = ts5.createScanner(ts5.ScriptTarget.Latest, true, ts5.LanguageVariant.Standard, name);
1128
1274
  const token = scanner.scan();
1129
1275
  const isWholeText = scanner.getTokenEnd() === name.length && name.length > 0;
1130
- const isNameToken = token === ts3.SyntaxKind.Identifier || token > ts3.SyntaxKind.LastReservedWord && token <= ts3.SyntaxKind.LastKeyword;
1276
+ const isNameToken = token === ts5.SyntaxKind.Identifier || token > ts5.SyntaxKind.LastReservedWord && token <= ts5.SyntaxKind.LastKeyword;
1131
1277
  return isWholeText && isNameToken;
1132
1278
  }
1133
1279
 
@@ -1257,7 +1403,7 @@ function connectProjectWorker(port) {
1257
1403
  }
1258
1404
 
1259
1405
  // src/edits.ts
1260
- import ts4 from "typescript";
1406
+ import ts6 from "typescript";
1261
1407
  function applyEdit(text, e) {
1262
1408
  return text.slice(0, e.start) + e.text + text.slice(e.end);
1263
1409
  }
@@ -1384,13 +1530,13 @@ function toTopLevel(code) {
1384
1530
  function renameDeclaration(code, inClass, name) {
1385
1531
  const prefix = inClass ? "class __cc__ {\n" : "";
1386
1532
  const src = prefix + code + (inClass ? "\n}" : "");
1387
- const sf = ts4.createSourceFile("snippet.ts", src, ts4.ScriptTarget.Latest, true);
1533
+ const sf = ts6.createSourceFile("snippet.ts", src, ts6.ScriptTarget.Latest, true);
1388
1534
  let target;
1389
1535
  const first = sf.statements[0];
1390
- if (inClass && first && ts4.isClassDeclaration(first)) target = first.members[0]?.name;
1391
- else if (first && (ts4.isFunctionDeclaration(first) || ts4.isClassDeclaration(first) || ts4.isInterfaceDeclaration(first) || ts4.isTypeAliasDeclaration(first) || ts4.isEnumDeclaration(first)))
1536
+ if (inClass && first && ts6.isClassDeclaration(first)) target = first.members[0]?.name;
1537
+ else if (first && (ts6.isFunctionDeclaration(first) || ts6.isClassDeclaration(first) || ts6.isInterfaceDeclaration(first) || ts6.isTypeAliasDeclaration(first) || ts6.isEnumDeclaration(first)))
1392
1538
  target = first.name;
1393
- else if (first && ts4.isVariableStatement(first)) target = first.declarationList.declarations[0]?.name;
1539
+ else if (first && ts6.isVariableStatement(first)) target = first.declarationList.declarations[0]?.name;
1394
1540
  if (!target) return code;
1395
1541
  const s = target.getStart(sf) - prefix.length;
1396
1542
  return code.slice(0, s) + name + code.slice(target.end - prefix.length);
@@ -1402,26 +1548,26 @@ function must(model, id) {
1402
1548
  }
1403
1549
  function clipFromText(code) {
1404
1550
  const text = code.replace(/\s+$/, "");
1405
- const sf = ts4.createSourceFile("clip.ts", text, ts4.ScriptTarget.Latest, true);
1551
+ const sf = ts6.createSourceFile("clip.ts", text, ts6.ScriptTarget.Latest, true);
1406
1552
  const first = sf.statements[0];
1407
1553
  const diags = sf.parseDiagnostics ?? [];
1408
1554
  if (first && !diags.length) {
1409
1555
  let name;
1410
1556
  let kind = "statement";
1411
- if (ts4.isFunctionDeclaration(first)) name = first.name?.text, kind = "function";
1412
- else if (ts4.isClassDeclaration(first)) name = first.name?.text, kind = "class";
1413
- else if (ts4.isVariableStatement(first)) name = first.declarationList.declarations[0]?.name.getText(), kind = "variable";
1414
- else if (ts4.isInterfaceDeclaration(first) || ts4.isTypeAliasDeclaration(first) || ts4.isEnumDeclaration(first)) name = first.name.text, kind = "type";
1557
+ if (ts6.isFunctionDeclaration(first)) name = first.name?.text, kind = "function";
1558
+ else if (ts6.isClassDeclaration(first)) name = first.name?.text, kind = "class";
1559
+ else if (ts6.isVariableStatement(first)) name = first.declarationList.declarations[0]?.name.getText(), kind = "variable";
1560
+ else if (ts6.isInterfaceDeclaration(first) || ts6.isTypeAliasDeclaration(first) || ts6.isEnumDeclaration(first)) name = first.name.text, kind = "type";
1415
1561
  if (name) return { code: text, name, kind, fromClass: false };
1416
1562
  }
1417
- const wrapped = ts4.createSourceFile("clip.ts", `class __cc__ {
1563
+ const wrapped = ts6.createSourceFile("clip.ts", `class __cc__ {
1418
1564
  ${text}
1419
- }`, ts4.ScriptTarget.Latest, true);
1565
+ }`, ts6.ScriptTarget.Latest, true);
1420
1566
  const cls = wrapped.statements[0];
1421
1567
  const wd = wrapped.parseDiagnostics ?? [];
1422
- if (cls && ts4.isClassDeclaration(cls) && !wd.length && cls.members[0]?.name) {
1568
+ if (cls && ts6.isClassDeclaration(cls) && !wd.length && cls.members[0]?.name) {
1423
1569
  const m = cls.members[0];
1424
- return { code: text, name: m.name.getText(wrapped), kind: ts4.isPropertyDeclaration(m) ? "variable" : "function", fromClass: true };
1570
+ return { code: text, name: m.name.getText(wrapped), kind: ts6.isPropertyDeclaration(m) ? "variable" : "function", fromClass: true };
1425
1571
  }
1426
1572
  return { code: text, name: "", kind: "statement", fromClass: false };
1427
1573
  }
@@ -1579,7 +1725,7 @@ function resolveConflict(text, hunk, choice) {
1579
1725
  var BAND_ORDER = ["data", "types", "main", "bottom"];
1580
1726
  var ZONE_RULES = [
1581
1727
  { zone: "left", description: "Imports on the left", matches: (block) => block.kind === "import" },
1582
- { zone: "data", description: "Constants and other data at the top", matches: (block) => block.kind === "variable" },
1728
+ { zone: "data", description: "Constants, other data and React state at the top", matches: (block) => block.kind === "variable" || block.kind === "state" },
1583
1729
  { zone: "types", description: "Then interfaces, types and enums", matches: (block) => block.kind === "type" },
1584
1730
  { zone: "bottom", description: "Code that runs when the file loads, at the bottom", matches: (block) => block.kind === "statement" },
1585
1731
  { zone: "main", description: "Functions, classes and the rest in between", matches: () => true }
@@ -1669,7 +1815,7 @@ var PLACEMENT_RULES = [
1669
1815
  ];
1670
1816
  var zoneOf = (block) => ZONE_RULES.find((rule) => rule.matches(block)).zone;
1671
1817
  var NEWSPAPER_DEPTH = 3;
1672
- var isFunctionLike = (block) => block.kind === "function";
1818
+ var isFunctionLike = (block) => block.kind === "function" || block.kind === "component";
1673
1819
  function accessedName(block, accessor) {
1674
1820
  if (block.kind !== "function") return void 0;
1675
1821
  if (block.tags.includes(accessor)) return block.name;
@@ -1823,6 +1969,7 @@ function orderByRelatedness(units, factsOf) {
1823
1969
  function siblingFacts(blocks, text) {
1824
1970
  const idsByName = /* @__PURE__ */ new Map();
1825
1971
  for (const block of blocks) idsByName.set(block.name, [...idsByName.get(block.name) ?? [], block.id]);
1972
+ const componentIds = new Set(blocks.filter((block) => block.kind === "component").map((block) => block.id));
1826
1973
  const facts = /* @__PURE__ */ new Map();
1827
1974
  for (const block of blocks) {
1828
1975
  const code = codeWithoutChildren(text, [block.start, block.end], block.children);
@@ -1832,9 +1979,11 @@ function siblingFacts(blocks, text) {
1832
1979
  const ids = idsByName.get(match[0])?.filter((id) => id !== block.id);
1833
1980
  if (!ids?.length) continue;
1834
1981
  const isCall = /^\s*\(/.test(code.slice(match.index + match[0].length, match.index + match[0].length + 40));
1982
+ const isJsxTag = /<\/?$/.test(code.slice(Math.max(0, match.index - 2), match.index));
1835
1983
  for (const id of ids) {
1836
1984
  uses.add(id);
1837
- if (isCall && !calls.includes(id)) calls.push(id);
1985
+ const isRender = isJsxTag && componentIds.has(id);
1986
+ if ((isCall || isRender) && !calls.includes(id)) calls.push(id);
1838
1987
  }
1839
1988
  }
1840
1989
  facts.set(block.id, { calls, uses });
@@ -1875,13 +2024,13 @@ function contains(arrangement, unit) {
1875
2024
 
1876
2025
  // src/layout.ts
1877
2026
  var M = {
1878
- headerH: 32,
1879
- headerCharW: 7.9,
1880
- // 13px monospace
1881
- previewLineH: 15,
1882
- previewCharW: 6.7,
1883
- // 11px monospace
1884
- previewPad: 10,
2027
+ headerH: 34,
2028
+ headerCharW: 7.6,
2029
+ // 12.5px monospace
2030
+ previewLineH: 19,
2031
+ previewCharW: 6.95,
2032
+ // 11.5px monospace
2033
+ previewPad: 16,
1885
2034
  pad: 14,
1886
2035
  // inner padding of containers
1887
2036
  gap: 28,
@@ -2149,98 +2298,222 @@ function setHTML(element, html) {
2149
2298
  const htmlPolicy = trustedHtmlPolicy();
2150
2299
  element.innerHTML = htmlPolicy ? htmlPolicy.createHTML(html) : html;
2151
2300
  }
2301
+ function esc(s) {
2302
+ return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
2303
+ }
2304
+
2305
+ // src/highlight.ts
2306
+ var KEYWORDS = /* @__PURE__ */ new Set([
2307
+ "import",
2308
+ "from",
2309
+ "export",
2310
+ "default",
2311
+ "const",
2312
+ "let",
2313
+ "var",
2314
+ "function",
2315
+ "return",
2316
+ "async",
2317
+ "await",
2318
+ "if",
2319
+ "else",
2320
+ "for",
2321
+ "while",
2322
+ "do",
2323
+ "switch",
2324
+ "case",
2325
+ "break",
2326
+ "continue",
2327
+ "new",
2328
+ "class",
2329
+ "extends",
2330
+ "implements",
2331
+ "interface",
2332
+ "type",
2333
+ "enum",
2334
+ "namespace",
2335
+ "this",
2336
+ "super",
2337
+ "throw",
2338
+ "try",
2339
+ "catch",
2340
+ "finally",
2341
+ "typeof",
2342
+ "instanceof",
2343
+ "in",
2344
+ "of",
2345
+ "yield",
2346
+ "void",
2347
+ "delete",
2348
+ "null",
2349
+ "undefined",
2350
+ "true",
2351
+ "false",
2352
+ "static",
2353
+ "public",
2354
+ "private",
2355
+ "protected",
2356
+ "readonly",
2357
+ "as",
2358
+ "satisfies",
2359
+ "get",
2360
+ "set"
2361
+ ]);
2362
+ var TOKEN = /(\/\*.*?\*\/|\/\/.*$|'(?:\\.|[^'\\])*'?|"(?:\\.|[^"\\])*"?|`(?:\\.|[^`\\])*`?|[A-Za-z_$][\w$]*|\d[\w.]*|\s+|.)/g;
2363
+ function highlightLine(line) {
2364
+ let html = "";
2365
+ for (const [token] of line.matchAll(TOKEN)) {
2366
+ const syntaxClass = classOf(token);
2367
+ html += syntaxClass ? `<span class="${syntaxClass}">${esc(token)}</span>` : esc(token);
2368
+ }
2369
+ return html;
2370
+ }
2371
+ function classOf(token) {
2372
+ if (token.startsWith("//") || token.startsWith("/*")) return "s-com";
2373
+ if (/^['"`]/.test(token)) return "s-str";
2374
+ if (/^\d/.test(token)) return "s-con";
2375
+ if (KEYWORDS.has(token)) return "s-kw";
2376
+ if (/^[A-Z][A-Z0-9_]{2,}$/.test(token)) return "s-con";
2377
+ if (/^[A-Z][a-z]\w*$/.test(token)) return "s-type";
2378
+ return void 0;
2379
+ }
2152
2380
 
2153
2381
  // src/styles.ts
2154
2382
  var CSS = (
2155
2383
  /* css */
2156
2384
  `
2157
2385
  .cc-root {
2158
- --cc-bg: #15171c;
2159
- --cc-dot: rgba(255,255,255,.07);
2160
- --cc-frame-bg: rgba(255,255,255,.025);
2161
- --cc-frame-border: rgba(255,255,255,.10);
2162
- --cc-text: #e8eaee;
2163
- --cc-muted: #9aa1ad;
2164
- --cc-faint: #c4cad4;
2165
- --cc-panel: #1f2229;
2166
- --cc-panel-border: rgba(255,255,255,.12);
2167
- --cc-accent: #7aa2ff;
2168
- --cc-danger: #ff6b6b;
2169
- --cc-shadow: 0 10px 30px rgba(0,0,0,.45);
2386
+ --cc-bg: #0c0d0f;
2387
+ --cc-dot: #25272c;
2388
+ --cc-frame-bg: rgba(255,255,255,.015);
2389
+ --cc-frame-border: #2e3036;
2390
+ --cc-node-bg: #16171a;
2391
+ --cc-node-border: #2b2d33;
2392
+ --cc-node-head-border: #24262b;
2393
+ --cc-text: #e6e7ea;
2394
+ --cc-strong: #f2f2f3;
2395
+ --cc-muted: #a9adb6;
2396
+ --cc-faint: #8b8f98;
2397
+ --cc-faintest: #6e727b;
2398
+ --cc-code: #c9ccd2;
2399
+ --cc-panel: #16171a;
2400
+ --cc-surface2: #1c1d21;
2401
+ --cc-raised: #2a2c31;
2402
+ --cc-panel-border: #26282d;
2403
+ --cc-accent: #e8c27a;
2404
+ --cc-ink: #e8c27a;
2405
+ --cc-on-accent: #1a1408;
2406
+ --cc-danger: #f2877d;
2407
+ --cc-shadow: 0 10px 30px rgba(0,0,0,.35);
2408
+ --cc-shadow-sm: 0 1px 0 rgba(0,0,0,.2);
2409
+ --cc-shadow-lg: 0 24px 60px rgba(0,0,0,.55);
2410
+ --cc-syn-kw: #c3a6f0;
2411
+ --cc-syn-str: #9fd3ec;
2412
+ --cc-syn-type: #b8c4ff;
2413
+ --cc-syn-com: #7d828c;
2414
+ --cc-syn-con: #7cc4e8;
2415
+ --cc-ui-font: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
2170
2416
 
2171
- --k-import: #6b7280; --k-import-head: #3b3f46; --k-import-bg: #25272c;
2172
- --k-class: #34b27a; --k-class-head: #1d6b4a; --k-class-bg: rgba(52,178,122,.08);
2173
- --k-function: #ec5fa5; --k-function-head: #8f2f62; --k-function-bg: rgba(236,95,165,.09);
2174
- --k-variable: #4b8ef7; --k-variable-head: #23508f; --k-variable-bg: rgba(75,142,247,.09);
2175
- --k-type: #d4a92a; --k-type-head: #6e5714; --k-type-bg: rgba(212,169,42,.08);
2176
- --k-namespace: #9b8cf8; --k-namespace-head: #4d4291; --k-namespace-bg: rgba(155,140,248,.08);
2177
- --k-statement: #7c8799; --k-statement-head: #39414f; --k-statement-bg: rgba(124,135,153,.08);
2417
+ --k-import: #b4b9c2;
2418
+ --k-variable: #7cc4e8;
2419
+ --k-function: #e8c27a;
2420
+ --k-class: #8fd0a5;
2421
+ --k-type: #b8b0f0;
2422
+ --k-component: #5fd1b5;
2423
+ --k-state: #f0a36a;
2424
+ --k-namespace: #9aa6bd;
2425
+ --k-statement: #8b8f98;
2178
2426
 
2179
2427
  position: relative; overflow: hidden; width: 100%; height: 100%;
2180
2428
  background-color: var(--cc-bg);
2181
- background-image: radial-gradient(var(--cc-dot) 1.2px, transparent 1.3px);
2429
+ background-image: radial-gradient(circle, var(--cc-dot) 1px, transparent 1.4px);
2182
2430
  color: var(--cc-text);
2183
- font: 13px/1.3 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
2431
+ font: 13px/1.3 var(--cc-ui-font);
2184
2432
  touch-action: none; user-select: none; -webkit-user-select: none; outline: none;
2185
2433
  -webkit-tap-highlight-color: transparent;
2186
2434
  }
2187
2435
  .cc-root[data-theme="light"] {
2188
- --cc-bg: #f6f7f9; --cc-dot: rgba(0,0,0,.09);
2189
- --cc-frame-bg: rgba(255,255,255,.7); --cc-frame-border: rgba(0,0,0,.12);
2190
- --cc-text: #1d2129; --cc-muted: #5d6573; --cc-faint: #39404c;
2191
- --cc-panel: #ffffff; --cc-panel-border: rgba(0,0,0,.12); --cc-accent: #3366e6;
2192
- --cc-shadow: 0 10px 30px rgba(20,30,50,.18);
2193
- --k-import: #6b7280; --k-import-head: #4b5058; --k-import-bg: #eceef1;
2194
- --k-class: #1f9963; --k-class-head: #1f9963; --k-class-bg: rgba(31,153,99,.07);
2195
- --k-function: #d63e8c; --k-function-head: #d63e8c; --k-function-bg: rgba(214,62,140,.07);
2196
- --k-variable: #2f6fe0; --k-variable-head: #2f6fe0; --k-variable-bg: rgba(47,111,224,.07);
2197
- --k-type: #b58a0c; --k-type-head: #a87f08; --k-type-bg: rgba(181,138,12,.08);
2198
- --k-namespace: #6f5ce6; --k-namespace-head: #6f5ce6; --k-namespace-bg: rgba(111,92,230,.07);
2199
- --k-statement: #64708a; --k-statement-head: #64708a; --k-statement-bg: rgba(100,112,138,.07);
2436
+ --cc-bg: #f3f3f1; --cc-dot: #d4d4cf;
2437
+ --cc-frame-bg: rgba(255,255,255,.5); --cc-frame-border: #cfcfc9;
2438
+ --cc-node-bg: #ffffff; --cc-node-border: #dadad4; --cc-node-head-border: #edede9;
2439
+ --cc-text: #1f2023; --cc-strong: #0f1012; --cc-muted: #4a4d55; --cc-faint: #5f626a; --cc-faintest: #8a8d94; --cc-code: #2e3036;
2440
+ --cc-panel: #ffffff; --cc-surface2: #f5f5f3; --cc-raised: #ececE8; --cc-panel-border: #e1e1dc;
2441
+ --cc-ink: #8a5d0e; --cc-danger: #b4362b;
2442
+ --cc-shadow: 0 8px 24px rgba(20,20,10,.08); --cc-shadow-sm: 0 1px 2px rgba(20,20,10,.06); --cc-shadow-lg: 0 24px 60px rgba(20,20,10,.18);
2443
+ --cc-syn-kw: #7a3fc0; --cc-syn-str: #1d6a92; --cc-syn-type: #3b4fb0; --cc-syn-com: #6f737b; --cc-syn-con: #1d6a92;
2444
+ --k-import: #555a63; --k-variable: #1d6a92; --k-function: #8a5d0e; --k-class: #2f7a4a; --k-type: #5a4bb0;
2445
+ --k-component: #11756a; --k-state: #a8521a; --k-namespace: #4f5b73; --k-statement: #5f626a;
2200
2446
  }
2201
2447
  .cc-world { position: absolute; left: 0; top: 0; transform-origin: 0 0; }
2202
2448
  .cc-root.cc-moving .cc-world { will-change: transform; }
2203
- .cc-frame { position: absolute; border: 1px solid var(--cc-frame-border); background: var(--cc-frame-bg); border-radius: 18px; }
2204
- .cc-frame-title { position: absolute; left: 20px; top: 12px; font: 600 15px/20px var(--cc-font, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); color: var(--cc-muted); white-space: nowrap; }
2449
+ .cc-root.cc-tool-pan, .cc-root.cc-tool-pan .cc-block { cursor: grab; }
2450
+ .cc-mono { font-family: var(--cc-font, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); }
2451
+ .cc-frame { position: absolute; border: 1px dashed var(--cc-frame-border); background: var(--cc-frame-bg); border-radius: 18px; }
2452
+ .cc-frame-title {
2453
+ position: absolute; left: 20px; top: 14px; display: flex; align-items: center; gap: 8px; white-space: nowrap;
2454
+ font: 12px/18px var(--cc-font, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
2455
+ }
2456
+ .cc-frame-folder { color: var(--cc-faint); }
2457
+ .cc-frame-file { color: var(--cc-text); font-weight: 500; }
2458
+ .cc-frame-count { padding: 2px 6px; border-radius: 5px; background: var(--cc-surface2); color: var(--cc-faint); font-size: 10.5px; line-height: 14px; }
2459
+ .cc-frame-count:empty { display: none; }
2205
2460
  .cc-empty { position: absolute; font-size: 15px; color: var(--cc-muted); white-space: nowrap; }
2206
2461
 
2207
2462
  .cc-block {
2208
- position: absolute; left: 0; top: 0; box-sizing: border-box; border-radius: 10px;
2209
- border: 1.5px solid var(--k); background: var(--kbg); overflow: hidden; contain: layout style;
2210
- cursor: default;
2211
- }
2212
- .cc-block.k-import { --k: var(--k-import); --kh: var(--k-import-head); --kbg: var(--k-import-bg); }
2213
- .cc-block.k-class { --k: var(--k-class); --kh: var(--k-class-head); --kbg: var(--k-class-bg); }
2214
- .cc-block.k-function { --k: var(--k-function); --kh: var(--k-function-head); --kbg: var(--k-function-bg); }
2215
- .cc-block.k-variable { --k: var(--k-variable); --kh: var(--k-variable-head); --kbg: var(--k-variable-bg); }
2216
- .cc-block.k-type { --k: var(--k-type); --kh: var(--k-type-head); --kbg: var(--k-type-bg); }
2217
- .cc-block.k-namespace { --k: var(--k-namespace); --kh: var(--k-namespace-head); --kbg: var(--k-namespace-bg); }
2218
- .cc-block.k-statement { --k: var(--k-statement); --kh: var(--k-statement-head); --kbg: var(--k-statement-bg); }
2219
- .cc-block.k-import, .cc-root:not([data-theme="light"]) .cc-block.k-import { border-color: #555a63; }
2463
+ --kglyph: color-mix(in srgb, var(--k) 16%, transparent);
2464
+ position: absolute; left: 0; top: 0; box-sizing: border-box; border-radius: 12px;
2465
+ border: 1px solid var(--cc-node-border); background: var(--cc-node-bg); overflow: hidden; contain: layout style;
2466
+ box-shadow: var(--cc-shadow-sm); cursor: default; transition: opacity 160ms ease, box-shadow 160ms ease;
2467
+ }
2468
+ .cc-root[data-theme="light"] .cc-block { --kglyph: color-mix(in srgb, var(--k) 10%, transparent); }
2469
+ .cc-block.k-import { --k: var(--k-import); }
2470
+ .cc-block.k-class { --k: var(--k-class); }
2471
+ .cc-block.k-function { --k: var(--k-function); }
2472
+ .cc-block.k-component { --k: var(--k-component); }
2473
+ .cc-block.k-state { --k: var(--k-state); }
2474
+ .cc-block.k-variable { --k: var(--k-variable); }
2475
+ .cc-block.k-type { --k: var(--k-type); }
2476
+ .cc-block.k-namespace { --k: var(--k-namespace); }
2477
+ .cc-block.k-statement { --k: var(--k-statement); }
2220
2478
 
2221
2479
  .cc-head {
2222
- display: flex; align-items: center; gap: 7px; height: 32px; padding: 0 10px; box-sizing: border-box;
2223
- background: var(--kh); color: #fff; white-space: nowrap; overflow: hidden;
2224
- font: 13px/32px var(--cc-font, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
2225
- }
2226
- .cc-kind { font-size: 10px; text-transform: uppercase; letter-spacing: .06em; opacity: .75; flex: none; }
2227
- .cc-name { font-weight: 700; flex: none; max-width: 70%; overflow: hidden; text-overflow: ellipsis; }
2228
- .cc-detail { opacity: .8; overflow: hidden; text-overflow: ellipsis; min-width: 0; flex: 1 1 auto; }
2229
- .cc-tags { font-size: 10px; opacity: .7; flex: none; }
2230
- .cc-exp { width: 6px; height: 6px; border-radius: 50%; background: #fff; opacity: .8; flex: none; }
2480
+ display: flex; align-items: center; gap: 8px; height: 34px; padding: 0 10px; box-sizing: border-box;
2481
+ border-bottom: 1px solid var(--cc-node-head-border); color: var(--cc-strong); white-space: nowrap; overflow: hidden;
2482
+ font: 12.5px/34px var(--cc-font, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
2483
+ }
2484
+ .cc-glyph {
2485
+ flex: none; width: 18px; min-width: 18px; height: 18px; padding: 0 2px; box-sizing: border-box; border-radius: 5px;
2486
+ background: var(--kglyph); color: var(--k); display: flex; align-items: center; justify-content: center;
2487
+ font: 600 10.5px/18px var(--cc-font, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); letter-spacing: -.04em;
2488
+ }
2489
+ .k-component > .cc-head .cc-glyph, .k-namespace > .cc-head .cc-glyph { width: auto; }
2490
+ .cc-name { font-weight: 600; flex: none; max-width: 70%; overflow: hidden; text-overflow: ellipsis; }
2491
+ .cc-detail { font-size: 11.5px; color: var(--cc-faint); overflow: hidden; text-overflow: ellipsis; min-width: 0; flex: 1 1 auto; }
2492
+ .cc-tag {
2493
+ flex: none; padding: 0 7px; border-radius: 6px; background: var(--kglyph); color: var(--k);
2494
+ font: 500 10.5px/18px var(--cc-ui-font);
2495
+ }
2496
+ .cc-exp { width: 5px; height: 5px; border-radius: 50%; background: var(--cc-faint); flex: none; }
2231
2497
  .cc-fold {
2232
2498
  flex: none; margin-left: auto; width: 22px; height: 22px; border: 0; border-radius: 6px; padding: 0;
2233
- background: rgba(255,255,255,.14); color: #fff; font-size: 12px; line-height: 22px; cursor: pointer;
2499
+ background: transparent; color: var(--cc-faint); font-size: 12px; line-height: 22px; cursor: pointer;
2234
2500
  }
2235
- .cc-fold:hover { background: rgba(255,255,255,.28); }
2501
+ .cc-fold:hover { background: var(--cc-raised); color: var(--cc-strong); }
2236
2502
  .cc-prev {
2237
- margin: 0; padding: 5px 12px 0; white-space: pre; overflow: hidden; color: var(--cc-faint); opacity: .5;
2238
- font: 11px/15px var(--cc-font, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
2239
- }
2240
- .cc-more { opacity: .7; font-style: italic; }
2241
- .cc-mini { position: relative; margin: 6px 12px 0; height: 34px; opacity: .45; }
2242
- .cc-mini > i { position: absolute; border-radius: 2px; border: 1px solid var(--cc-faint); background: rgba(255,255,255,.06); }
2503
+ margin: 0; padding: 7px 12px 9px; white-space: pre; overflow: hidden; color: var(--cc-code);
2504
+ font: 11.5px/19px var(--cc-font, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
2505
+ }
2506
+ .cc-prev .s-kw { color: var(--cc-syn-kw); }
2507
+ .cc-prev .s-str { color: var(--cc-syn-str); }
2508
+ .cc-prev .s-type { color: var(--cc-syn-type); }
2509
+ .cc-prev .s-com { color: var(--cc-syn-com); }
2510
+ .cc-prev .s-con { color: var(--cc-syn-con); }
2511
+ .k-import > .cc-prev { color: var(--cc-muted); }
2512
+ .cc-more { color: var(--cc-faintest); font-style: italic; }
2513
+ .cc-mini { position: relative; margin: 6px 12px 0; height: 34px; opacity: .55; }
2514
+ .cc-mini > i { position: absolute; border-radius: 2px; border: 1px solid var(--cc-faint); background: transparent; }
2243
2515
  .cc-mini > i.k-class { border-color: var(--k-class); } .cc-mini > i.k-function { border-color: var(--k-function); }
2516
+ .cc-mini > i.k-component { border-color: var(--k-component); } .cc-mini > i.k-state { border-color: var(--k-state); }
2244
2517
  .cc-mini > i.k-variable { border-color: var(--k-variable); } .cc-mini > i.k-type { border-color: var(--k-type); }
2245
2518
 
2246
2519
  .cc-err {
@@ -2248,9 +2521,10 @@ var CSS = (
2248
2521
  background: var(--cc-danger); color: #fff; font: 700 11px/18px ui-sans-serif, system-ui, sans-serif; text-align: center;
2249
2522
  }
2250
2523
  .cc-block.cc-has-error { border-color: var(--cc-danger); }
2524
+ .cc-err { color: #1a0d0b; }
2251
2525
  .cc-root.lod-2 .cc-err { min-width: 34px; height: 34px; border-radius: 17px; font-size: 22px; line-height: 34px; }
2252
2526
  .cc-rename {
2253
- font: inherit; font-weight: 700; color: #fff; background: rgba(0,0,0,.35); border: 1px solid var(--cc-accent);
2527
+ font: inherit; font-weight: 600; color: var(--cc-strong); background: var(--cc-raised); border: 1px solid var(--cc-ink);
2254
2528
  border-radius: 4px; padding: 0 4px; height: 22px; outline: none; user-select: text; -webkit-user-select: text; max-width: 100%;
2255
2529
  }
2256
2530
  .cc-rename.invalid { border-color: var(--cc-danger); }
@@ -2263,10 +2537,7 @@ var CSS = (
2263
2537
  font: 900 13px/18px ui-sans-serif, system-ui, sans-serif; text-align: center;
2264
2538
  }
2265
2539
  .cc-root.lod-2 .cc-conflict-chip { min-width: 34px; height: 34px; border-radius: 17px; font-size: 24px; line-height: 34px; }
2266
- .cc-block.cc-conflict-incoming {
2267
- --k: #f0883e; --kh: #8a4a17; --kbg: rgba(240, 136, 62, .08);
2268
- border-style: dashed; pointer-events: none; opacity: .9;
2269
- }
2540
+ .cc-block.cc-conflict-incoming { --k: #f0883e; border: 1px dashed #f0883e; pointer-events: none; opacity: .9; }
2270
2541
  .cc-diff-chip.incoming { background: #f0883e; }
2271
2542
  .cc-banner.cc-banner-conflicts { border-color: #f0883e; }
2272
2543
 
@@ -2281,10 +2552,9 @@ var CSS = (
2281
2552
  .cc-diff-chip.removed { background: var(--cc-danger); }
2282
2553
  .cc-root.lod-2 .cc-diff-chip { min-width: 34px; height: 34px; border-radius: 8px; font-size: 24px; line-height: 34px; }
2283
2554
  .cc-block.cc-diff-removed {
2284
- --k: var(--cc-danger); --kh: #9c2f2f; --kbg: rgba(255, 107, 107, .10);
2285
- border-style: dashed; pointer-events: none; opacity: .9;
2555
+ --k: var(--cc-danger); border: 1px dashed var(--cc-danger); background: color-mix(in srgb, var(--cc-danger) 8%, var(--cc-node-bg));
2556
+ pointer-events: none; opacity: .9;
2286
2557
  }
2287
- .cc-root[data-theme="light"] .cc-block.cc-diff-removed { --kh: #d64545; --kbg: rgba(214, 69, 69, .08); }
2288
2558
  .cc-prev.cc-diff-prev { opacity: .95; }
2289
2559
  .cc-diff-prev > span { display: block; margin: 0 -12px; padding: 0 12px; }
2290
2560
  .cc-diff-prev .cc-line-same { opacity: .5; }
@@ -2292,23 +2562,24 @@ var CSS = (
2292
2562
  .cc-diff-prev .cc-line-removed { background: rgba(248, 81, 73, .26); text-decoration: line-through; text-decoration-color: rgba(248, 81, 73, .7); }
2293
2563
  .cc-diff-summary { padding: 0 8px; font-size: 12px; color: var(--cc-muted); white-space: nowrap; }
2294
2564
 
2295
- .cc-block.cc-sel { box-shadow: 0 0 0 2px var(--cc-bg), 0 0 0 4px var(--k); }
2296
- .cc-block.cc-editing { box-shadow: 0 0 0 2px var(--cc-bg), 0 0 0 4px var(--cc-accent); }
2297
- .cc-block.cc-dragging { box-shadow: var(--cc-shadow); opacity: .92; z-index: 9999 !important; }
2298
- .cc-root.has-hl .cc-block.cc-linked { box-shadow: 0 0 0 2px var(--k); }
2565
+ .cc-block.cc-sel { box-shadow: 0 0 0 2px var(--cc-ink), var(--cc-shadow-lg); }
2566
+ .cc-block.cc-editing { box-shadow: 0 0 0 2px var(--cc-ink), var(--cc-shadow-lg); }
2567
+ .cc-block.cc-dragging { box-shadow: var(--cc-shadow-lg); opacity: .92; z-index: 9999 !important; }
2568
+ .cc-root.has-sel .cc-block:not(.cc-related) { opacity: .38; }
2299
2569
 
2300
2570
  .cc-edges { position: absolute; left: 0; top: 0; overflow: visible; pointer-events: none; }
2301
- .cc-edge { fill: none; stroke-width: 1.5; opacity: .22; }
2302
- .cc-edge.u-type { stroke-dasharray: 4 4; }
2303
- .cc-root.has-hl .cc-edge { opacity: .07; }
2304
- .cc-root.has-hl .cc-edge.hl { opacity: .95; stroke-width: 2.5; }
2571
+ .cc-edge { fill: none; stroke: var(--edge); stroke-width: 1.5; opacity: .5; }
2572
+ .cc-edge.to-import, .cc-edge.u-type { stroke-dasharray: 3 4; }
2573
+ .cc-root.has-hl .cc-edge { opacity: .18; }
2574
+ .cc-root.has-hl .cc-edge.hl { stroke: var(--cc-ink); opacity: .95; stroke-width: 2; }
2305
2575
 
2306
2576
  /* level of detail when zoomed out */
2307
- .cc-root.lod-1 .cc-tags { visibility: hidden; }
2577
+ .cc-root.lod-1 .cc-tag { visibility: hidden; }
2308
2578
  .cc-root.lod-2 .cc-prev, .cc-root.lod-2 .cc-detail, .cc-root.lod-2 .cc-mini { visibility: hidden; }
2309
2579
  .cc-root.lod-2 .cc-head { font-size: 26px; }
2310
- .cc-root.lod-2 .cc-kind, .cc-root.lod-2 .cc-fold { display: none; }
2580
+ .cc-root.lod-2 .cc-glyph, .cc-root.lod-2 .cc-fold, .cc-root.lod-2 .cc-tag { display: none; }
2311
2581
  .cc-root.lod-2 .cc-frame-title { font-size: 40px; line-height: 44px; top: -8px; }
2582
+ .cc-root.lod-2 .cc-frame-count { font-size: 28px; line-height: 36px; padding: 2px 12px; border-radius: 10px; }
2312
2583
 
2313
2584
  /* overlays (screen space) */
2314
2585
  .cc-banner {
@@ -2316,37 +2587,66 @@ var CSS = (
2316
2587
  padding: 8px 12px; border-radius: 10px; background: var(--cc-panel); border: 1px solid var(--cc-danger); box-shadow: var(--cc-shadow);
2317
2588
  font-size: 13px; max-width: calc(100% - 32px);
2318
2589
  }
2319
- .cc-banner[hidden], .cc-bar[hidden], .cc-refs[hidden] { display: none; }
2590
+ .cc-banner[hidden], .cc-bar[hidden], .cc-refs[hidden], .cc-selbar[hidden], .cc-menu[hidden] { display: none; }
2320
2591
  .cc-toast {
2321
2592
  position: absolute; left: 50%; bottom: 76px; transform: translateX(-50%); z-index: 30; padding: 8px 14px; border-radius: 10px;
2322
2593
  background: var(--cc-panel); border: 1px solid var(--cc-panel-border); box-shadow: var(--cc-shadow); font-size: 13px;
2323
2594
  transition: opacity .2s; pointer-events: none; max-width: calc(100% - 32px);
2324
2595
  }
2325
2596
  .cc-bar {
2326
- position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%); z-index: 20; display: flex; align-items: center; gap: 4px;
2327
- padding: 5px; border-radius: 12px; background: var(--cc-panel); border: 1px solid var(--cc-panel-border); box-shadow: var(--cc-shadow);
2597
+ position: absolute; left: 50%; bottom: 12px; transform: translateX(-50%); z-index: 20; display: flex; align-items: center; gap: 2px;
2598
+ padding: 4px; border-radius: 12px; background: var(--cc-panel); border: 1px solid var(--cc-panel-border); box-shadow: var(--cc-shadow);
2328
2599
  width: max-content; max-width: calc(100% - 24px); box-sizing: border-box; flex-wrap: wrap; justify-content: center;
2329
2600
  }
2330
- .cc-bar .cc-chip { padding: 0 10px 0 6px; font: 600 12px/30px var(--cc-font, "JetBrains Mono", ui-monospace, Menlo, monospace); white-space: nowrap; display: flex; align-items: center; gap: 6px; max-width: 220px; overflow: hidden; text-overflow: ellipsis; }
2331
- .cc-bar .cc-chip i { width: 10px; height: 10px; border-radius: 3px; background: var(--k); flex: none; }
2332
2601
  .cc-btn {
2333
- border: 0; background: transparent; color: var(--cc-text); font: 500 13px/30px inherit; font-family: inherit; padding: 0 10px; border-radius: 8px;
2334
- cursor: pointer; white-space: nowrap; min-height: 32px;
2335
- }
2336
- .cc-btn:hover { background: rgba(127,127,127,.16); }
2602
+ display: inline-flex; align-items: center; justify-content: center; border: 0; background: transparent; color: var(--cc-muted);
2603
+ font: 400 13px/32px var(--cc-ui-font); padding: 0 12px; border-radius: 8px; cursor: pointer; white-space: nowrap; height: 32px; box-sizing: border-box;
2604
+ }
2605
+ .cc-btn:hover { background: var(--cc-raised); color: var(--cc-strong); }
2606
+ .cc-btn:focus-visible { outline: 2px solid var(--cc-ink); outline-offset: -2px; }
2607
+ .cc-btn.strong { color: var(--cc-strong); font-weight: 500; }
2608
+ .cc-btn.square { padding: 0; min-width: 30px; }
2609
+ .cc-btn.on { background: var(--cc-raised); color: var(--cc-strong); }
2337
2610
  .cc-btn.danger { color: var(--cc-danger); }
2338
- .cc-btn.primary { background: var(--cc-accent); color: #fff; }
2339
- .cc-sep { width: 1px; height: 20px; background: var(--cc-panel-border); margin: 0 2px; flex: none; }
2611
+ .cc-btn.primary { background: var(--cc-accent); color: var(--cc-on-accent); font-weight: 600; }
2612
+ .cc-btn.primary:hover { filter: brightness(1.06); }
2613
+ .cc-zoom { min-width: 44px; text-align: center; font: 12px/32px var(--cc-font, "JetBrains Mono", ui-monospace, Menlo, monospace); color: var(--cc-muted); }
2614
+ .cc-language { padding: 0 8px; font: 500 11px/32px var(--cc-font, "JetBrains Mono", ui-monospace, Menlo, monospace); color: var(--cc-faint); white-space: nowrap; }
2615
+ .cc-sep { width: 1px; height: 20px; background: var(--cc-panel-border); margin: 0 4px; flex: none; }
2616
+ .cc-selbar {
2617
+ position: absolute; z-index: 19; display: flex; align-items: center; gap: 2px; padding: 3px; border-radius: 10px; white-space: nowrap;
2618
+ background: var(--cc-panel); border: 1px solid var(--cc-panel-border); box-shadow: var(--cc-shadow);
2619
+ }
2620
+ .cc-selbar .cc-btn { height: 26px; line-height: 26px; padding: 0 9px; border-radius: 7px; font-size: 12px; color: var(--cc-strong); }
2621
+ .cc-selbar .cc-btn.square { padding: 0; min-width: 26px; color: var(--cc-faint); }
2622
+ .cc-selbar .cc-btn.meta { color: var(--cc-faint); font-size: 11.5px; padding: 0 8px; }
2623
+ .cc-selbar .cc-btn.danger { color: var(--cc-danger); }
2624
+ .cc-selbar .cc-btn.primary { color: var(--cc-on-accent); }
2625
+ .cc-selbar .cc-sep { height: 16px; margin: 0 2px; }
2626
+ .cc-menu {
2627
+ position: absolute; z-index: 40; display: flex; flex-direction: column; min-width: 168px; padding: 4px; border-radius: 10px;
2628
+ background: var(--cc-panel); border: 1px solid var(--cc-panel-border); box-shadow: var(--cc-shadow-lg);
2629
+ }
2630
+ .cc-menu .cc-btn { justify-content: flex-start; height: 30px; line-height: 30px; padding: 0 10px; border-radius: 7px; font-size: 12.5px; color: var(--cc-text); }
2631
+ .cc-menu .cc-btn.danger { color: var(--cc-danger); }
2632
+ .cc-minimap {
2633
+ position: absolute; right: var(--cc-minimap-right, 12px); bottom: var(--cc-minimap-bottom, 12px); z-index: 18; width: 148px; height: 92px;
2634
+ box-sizing: border-box; border-radius: 10px; overflow: hidden; background: var(--cc-panel); border: 1px solid var(--cc-panel-border); cursor: pointer;
2635
+ }
2636
+ .cc-minimap-blocks { position: absolute; left: 10px; top: 13px; width: 128px; height: 66px; }
2637
+ .cc-minimap-blocks > i { position: absolute; border-radius: 2px; background: var(--k); opacity: .55; }
2638
+ .cc-minimap-view { position: absolute; left: 10px; top: 13px; box-sizing: border-box; border: 1.5px solid var(--cc-ink); border-radius: 6px; margin: 13px 0 0 10px; pointer-events: none; }
2639
+ @media (max-width: 640px) { .cc-minimap { display: none; } }
2340
2640
  .cc-refs {
2341
2641
  position: absolute; left: 50%; bottom: 64px; transform: translateX(-50%); z-index: 21; width: min(360px, calc(100% - 24px)); max-height: 45%;
2342
2642
  overflow: auto; border-radius: 12px; background: var(--cc-panel); border: 1px solid var(--cc-panel-border); box-shadow: var(--cc-shadow); padding: 6px;
2343
2643
  }
2344
- .cc-refs h4 { margin: 8px 8px 4px; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--cc-muted); }
2644
+ .cc-refs h4 { margin: 8px 8px 4px; font: 600 11px/16px var(--cc-ui-font); text-transform: uppercase; letter-spacing: .06em; color: var(--cc-faint); }
2345
2645
  .cc-refs button {
2346
2646
  display: flex; width: 100%; align-items: center; gap: 8px; border: 0; background: transparent; color: var(--cc-text); text-align: left;
2347
2647
  padding: 6px 8px; border-radius: 8px; cursor: pointer; font: 12px/18px var(--cc-font, "JetBrains Mono", ui-monospace, Menlo, monospace);
2348
2648
  }
2349
- .cc-refs button:hover { background: rgba(127,127,127,.16); }
2649
+ .cc-refs button:hover { background: var(--cc-raised); }
2350
2650
  .cc-refs button i { width: 10px; height: 10px; border-radius: 3px; background: var(--k); flex: none; }
2351
2651
  .cc-refs button span:last-child { margin-left: auto; color: var(--cc-muted); }
2352
2652
  .cc-refs button:disabled { cursor: default; opacity: .7; }
@@ -2385,11 +2685,24 @@ var KIND_LABEL = {
2385
2685
  import: "import",
2386
2686
  class: "class",
2387
2687
  function: "fn",
2688
+ component: "component",
2689
+ state: "state",
2388
2690
  variable: "var",
2389
2691
  type: "type",
2390
2692
  namespace: "ns",
2391
2693
  statement: "code"
2392
2694
  };
2695
+ var KIND_GLYPH = {
2696
+ import: "\u2193",
2697
+ class: "C",
2698
+ function: "\u0192",
2699
+ component: "\u27E8\u27E9",
2700
+ state: "\u25C6",
2701
+ variable: "=",
2702
+ type: "T",
2703
+ namespace: "{}",
2704
+ statement: "\u25B8"
2705
+ };
2393
2706
  var DIFF_CHIPS = {
2394
2707
  added: '<span class="cc-diff-chip added" title="Added">+</span>',
2395
2708
  changed: '<span class="cc-diff-chip changed" title="Changed">~</span>',
@@ -2398,6 +2711,9 @@ var DIFF_CHIPS = {
2398
2711
  containsChanges: "",
2399
2712
  unchanged: ""
2400
2713
  };
2714
+ var MINIMAP = { width: 128, height: 66 };
2715
+ var ICON_SELECT = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.4" aria-hidden="true"><path d="M2.5 1.5l9 4.5-4 1.2-1.4 4.3z"/></svg>';
2716
+ var ICON_PAN = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.4" aria-hidden="true"><path d="M7 1.5v11M1.5 7h11M7 1.5l-2 2M7 1.5l2 2M7 12.5l-2-2M7 12.5l2-2M1.5 7l2-2M1.5 7l2 2M12.5 7l-2-2M12.5 7l-2 2"/></svg>';
2401
2717
  var LABEL_TAGS = /* @__PURE__ */ new Set(["const", "let", "var", "interface", "type", "enum", "namespace", "get", "set"]);
2402
2718
  var MIN_SCALE = 0.04;
2403
2719
  var MAX_SCALE = 4;
@@ -2414,18 +2730,28 @@ var CodeCanvas = class {
2414
2730
  this.blocksEl = el("div", "cc-blocks");
2415
2731
  this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2416
2732
  this.svg.classList.add("cc-edges");
2417
- setHTML(this.svg, `<defs>${["import", "class", "function", "variable", "type", "namespace", "statement"].map(
2418
- (k) => `<marker id="cc-arrow-${k}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" fill="var(--k-${k})"/></marker>`
2419
- ).join("")}</defs>`);
2733
+ const dotMarker = (id, color) => `<marker id="${id}" viewBox="0 0 8 8" refX="4" refY="4" markerWidth="6" markerHeight="6" markerUnits="userSpaceOnUse"><circle cx="4" cy="4" r="3" fill="${color}"/></marker>`;
2734
+ setHTML(this.svg, `<defs>${Object.keys(KIND_GLYPH).map((k) => dotMarker(`cc-dot-${k}`, `var(--k-${k})`)).join("")}${dotMarker("cc-dot-hl", "var(--cc-ink)")}</defs>`);
2420
2735
  this.world.append(this.frameEl, this.blocksEl, this.svg);
2421
2736
  this.banner = el("div", "cc-banner");
2422
2737
  this.banner.hidden = true;
2423
2738
  this.toastEl = el("div", "cc-toast");
2424
2739
  this.toastEl.style.opacity = "0";
2425
- this.bar = el("div", "cc-bar");
2740
+ this.bar = el("div", "cc-bar cc-dock");
2741
+ this.selectionBar = el("div", "cc-selbar");
2742
+ this.selectionBar.hidden = true;
2743
+ this.menuEl = el("div", "cc-menu");
2744
+ this.menuEl.setAttribute("role", "menu");
2745
+ this.menuEl.hidden = true;
2746
+ this.zoomLabel = el("span", "cc-zoom");
2426
2747
  this.refsEl = el("div", "cc-refs");
2427
2748
  this.refsEl.hidden = true;
2428
- root.append(this.world, this.banner, this.refsEl, this.bar, this.toastEl);
2749
+ root.append(this.world, this.banner, this.refsEl, this.bar, this.selectionBar, this.menuEl, this.toastEl);
2750
+ if (opts.minimap ?? true) {
2751
+ this.minimapEl = el("div", "cc-minimap");
2752
+ this.minimapEl.setAttribute("aria-hidden", "true");
2753
+ root.append(this.minimapEl);
2754
+ }
2429
2755
  container.appendChild(root);
2430
2756
  this.bindEvents();
2431
2757
  this.ro = new ResizeObserver(() => this.fittedToView ? this.fit(false) : this.queueCamera());
@@ -2444,7 +2770,14 @@ var CodeCanvas = class {
2444
2770
  frameEl;
2445
2771
  blocksEl;
2446
2772
  svg;
2773
+ /** The dock at the bottom. */
2447
2774
  bar;
2775
+ selectionBar;
2776
+ menuEl;
2777
+ minimapEl = null;
2778
+ minimapScale = 1;
2779
+ zoomLabel;
2780
+ tool = "select";
2448
2781
  refsEl;
2449
2782
  banner;
2450
2783
  toastEl;
@@ -2806,7 +3139,9 @@ var CodeCanvas = class {
2806
3139
  if (!g || !view) return;
2807
3140
  const f = g.frame;
2808
3141
  Object.assign(this.frameEl.style, { transform: `translate(${f.x}px,${f.y}px)`, width: f.w + "px", height: f.h + "px" });
2809
- setHTML(this.frameEl, `<div class="cc-frame-title">${esc(this.path)}</div>${view.roots.length ? "" : `<div class="cc-empty" style="left:${M.filePad}px;top:${M.filePad + M.fileHeader}px">Empty file. Double-click (or double-tap) to create a function.</div>`}`);
3142
+ const folder = this.path.includes("/") ? this.path.slice(0, this.path.lastIndexOf("/")) + " /" : "";
3143
+ const fileName2 = this.path.slice(this.path.lastIndexOf("/") + 1);
3144
+ setHTML(this.frameEl, `<div class="cc-frame-title">${folder ? `<span class="cc-frame-folder">${esc(folder)}</span>` : ""}<span class="cc-frame-file">${esc(fileName2)}</span><span class="cc-frame-count"></span></div>${view.roots.length ? "" : `<div class="cc-empty" style="left:${M.filePad}px;top:${M.filePad + M.fileHeader}px">Empty file. Double-click (or double-tap) to create a function.</div>`}`);
2810
3145
  const seen = /* @__PURE__ */ new Set();
2811
3146
  const diff = this.currentDiff();
2812
3147
  for (const geo of g.order) {
@@ -2857,6 +3192,7 @@ var CodeCanvas = class {
2857
3192
  this.renderEdges();
2858
3193
  this.updateHighlight();
2859
3194
  this.updateErrorMarkers();
3195
+ this.renderMinimap();
2860
3196
  }
2861
3197
  /** Blocks only the incoming side of a conflict has, drawn dashed where they would go. */
2862
3198
  renderIncomingBlocks() {
@@ -2937,16 +3273,18 @@ var CodeCanvas = class {
2937
3273
  }
2938
3274
  blockHTML(b, geo, mini, change, changedLines = "", isConflicted = false) {
2939
3275
  const label = blockLabel(b);
2940
- const tags = b.tags.filter((t) => t !== label && t !== "export" && (b.kind === "import" || !LABEL_TAGS.has(t))).join(" ");
3276
+ const chips = b.tags.filter((t) => t !== label && t !== "export" && (b.kind === "import" || !LABEL_TAGS.has(t)));
2941
3277
  const exp = b.tags.includes("export") ? '<span class="cc-exp" title="exported"></span>' : "";
2942
3278
  const isRemoved = change === "removed" || change === "incoming";
2943
3279
  const fold = b.children.length && !isRemoved ? `<button class="cc-fold" title="${geo.collapsed ? "Expand" : "Collapse"}">${geo.collapsed ? "\u25B8" : "\u25BE"}</button>` : "";
2944
- const prev = changedLines ? `<pre class="cc-prev cc-diff-prev">${changedLines}</pre>` : b.preview.length ? `<pre class="cc-prev">${esc(b.preview.join("\n"))}${b.previewMore ? `
2945
- <span class="cc-more">\u2026 ${b.previewMore} more line${b.previewMore > 1 ? "s" : ""}</span>` : ""}</pre>` : "";
3280
+ const more = b.previewMore ? `
3281
+ <span class="cc-more">\u2026 ${b.previewMore} more line${b.previewMore > 1 ? "s" : ""}</span>` : "";
3282
+ const prev = changedLines ? `<pre class="cc-prev cc-diff-prev">${changedLines}</pre>` : b.preview.length ? `<pre class="cc-prev">${b.preview.map(highlightLine).join("\n")}${more}</pre>` : "";
2946
3283
  const diffChip = (change ? DIFF_CHIPS[change] : "") + (isConflicted ? '<span class="cc-conflict-chip" title="Merge conflict">!</span>' : "");
2947
- return `<div class="cc-head"><span class="cc-kind">${esc(label)}</span>${exp}<span class="cc-name">${esc(b.name)}</span>${diffChip}<span class="cc-detail">${esc(
3284
+ const chipsHTML = chips.map((chip) => `<span class="cc-tag">${esc(chip)}</span>`).join("");
3285
+ return `<div class="cc-head"><span class="cc-glyph" title="${esc(label)}">${KIND_GLYPH[b.kind]}</span><span class="cc-name">${esc(b.name)}</span>${exp}${diffChip}<span class="cc-detail">${esc(
2948
3286
  b.detail
2949
- )}</span>${tags ? `<span class="cc-tags">${esc(tags)}</span>` : ""}${fold}</div>${prev}${mini ? `<div class="cc-mini">${mini}</div>` : ""}`;
3287
+ )}</span>${chipsHTML}${fold}</div>${prev}${mini ? `<div class="cc-mini">${mini}</div>` : ""}`;
2950
3288
  }
2951
3289
  /** Faint miniature of a collapsed block's nested blocks. */
2952
3290
  miniMap(geo) {
@@ -2991,34 +3329,41 @@ var CodeCanvas = class {
2991
3329
  this.svg.appendChild(p);
2992
3330
  e = { key, from: w.from.id, to: w.to.id, el: p };
2993
3331
  }
2994
- e.el.setAttribute("class", `cc-edge u-${w.usage}`);
2995
- e.el.setAttribute("stroke", `var(--k-${w.kind})`);
2996
- e.el.setAttribute("marker-end", `url(#cc-arrow-${w.kind})`);
3332
+ e.el.setAttribute("class", `cc-edge u-${w.usage}${w.kind === "import" ? " to-import" : ""}`);
3333
+ e.el.style.setProperty("--edge", `var(--k-${w.kind})`);
3334
+ e.el.dataset.kind = w.kind;
2997
3335
  e.el.setAttribute("d", edgePath(w.from, w.to));
2998
3336
  next.push(e);
2999
3337
  }
3000
3338
  for (const e of old.values()) e.el.remove();
3001
3339
  this.edges = next;
3340
+ const counter = this.frameEl.querySelector(".cc-frame-count");
3341
+ if (counter) counter.textContent = `${plural(view.blocks.size, "node")} \xB7 ${plural(next.length, "link")}`;
3002
3342
  }
3003
3343
  updateHighlight() {
3004
3344
  const h = this.selected ?? this.hovered;
3005
3345
  const inH = (id) => !!h && (id === h || id.startsWith(h + "/"));
3346
+ const isAroundSelection = (id) => !!this.selected && (inH(id) || this.selected.startsWith(id + "/"));
3006
3347
  const linked = /* @__PURE__ */ new Set();
3007
3348
  for (const e of this.edges) {
3008
3349
  const on = inH(e.from) || inH(e.to);
3009
3350
  e.el.classList.toggle("hl", on);
3351
+ e.el.setAttribute("marker-end", on ? "url(#cc-dot-hl)" : `url(#cc-dot-${e.el.dataset.kind})`);
3010
3352
  if (on) {
3011
3353
  linked.add(e.from);
3012
3354
  linked.add(e.to);
3013
3355
  }
3014
3356
  }
3015
3357
  this.root.classList.toggle("has-hl", !!h);
3358
+ this.root.classList.toggle("has-sel", !!this.selected);
3016
3359
  for (const [id, { el: e }] of this.els) {
3017
3360
  e.classList.toggle("cc-sel", id === this.selected);
3018
3361
  e.classList.toggle("cc-linked", linked.has(id) && !inH(id));
3362
+ e.classList.toggle("cc-related", isAroundSelection(id) || linked.has(id));
3019
3363
  e.classList.toggle("cc-editing", !!this.editing && id === this.editing.id);
3020
3364
  }
3021
3365
  if (this.refsOpen) this.renderRefs();
3366
+ this.placeSelectionBar();
3022
3367
  }
3023
3368
  // ---------------------------------------------------------------- camera
3024
3369
  queueCamera() {
@@ -3032,7 +3377,7 @@ var CodeCanvas = class {
3032
3377
  applyCamera() {
3033
3378
  const { x, y, s } = this.cam;
3034
3379
  this.world.style.transform = `translate(${x}px,${y}px) scale(${s})`;
3035
- const grid = 24 * s;
3380
+ const grid = 22 * s;
3036
3381
  this.root.style.backgroundSize = s < 0.25 ? "0 0" : `${grid}px ${grid}px`;
3037
3382
  this.root.style.backgroundPosition = `${x}px ${y}px`;
3038
3383
  const lod = s < 0.3 ? 2 : s < 0.55 ? 1 : 0;
@@ -3044,6 +3389,9 @@ var CodeCanvas = class {
3044
3389
  this.root.classList.add("cc-moving");
3045
3390
  clearTimeout(this.movingTimer);
3046
3391
  this.movingTimer = window.setTimeout(() => this.root.classList.remove("cc-moving"), 160);
3392
+ this.zoomLabel.textContent = Math.round(s * 100) + "%";
3393
+ this.placeSelectionBar();
3394
+ this.placeMinimapView();
3047
3395
  this.opts.onCameraChange?.(s);
3048
3396
  }
3049
3397
  setCamera(c) {
@@ -3117,6 +3465,19 @@ var CodeCanvas = class {
3117
3465
  on(r, "pointercancel", (e) => this.onUp(e, true));
3118
3466
  on(r, "wheel", (e) => this.onWheel(e), { passive: false });
3119
3467
  on(r, "keydown", (e) => this.onKey(e));
3468
+ on(r, "contextmenu", (e) => {
3469
+ const id = e.target.closest?.(".cc-block")?.getAttribute("data-id");
3470
+ if (!id || this.isOverlay(e.target) || this.stale) return;
3471
+ e.preventDefault();
3472
+ this.openBlockMenu(id, this.local(e));
3473
+ });
3474
+ on(this.menuEl, "keydown", (e) => {
3475
+ if (e.key === "Escape") {
3476
+ e.stopPropagation();
3477
+ this.closeMenu();
3478
+ this.root.focus({ preventScroll: true });
3479
+ }
3480
+ });
3120
3481
  on(r, "pointerover", (e) => {
3121
3482
  if (e.pointerType !== "mouse" || this.gesture) return;
3122
3483
  const id = e.target.closest?.(".cc-block")?.getAttribute("data-id") ?? null;
@@ -3138,9 +3499,11 @@ var CodeCanvas = class {
3138
3499
  on(doc, "paste", (e) => this.onClipboard(e, "paste"));
3139
3500
  }
3140
3501
  isOverlay(t) {
3141
- return !!t?.closest?.(".cc-editor, .cc-bar, .cc-refs, .cc-banner, .cc-rename");
3502
+ return !!t?.closest?.(".cc-editor, .cc-bar, .cc-selbar, .cc-menu, .cc-minimap, .cc-refs, .cc-banner, .cc-rename");
3142
3503
  }
3143
3504
  onDown(e) {
3505
+ if (!e.target?.closest?.(".cc-menu")) this.closeMenu();
3506
+ if (e.target?.closest?.(".cc-minimap")) return this.moveToMinimapPoint(e);
3144
3507
  if (this.isOverlay(e.target)) return;
3145
3508
  if (e.pointerType === "mouse" && e.button !== 0 && e.button !== 1) return;
3146
3509
  this.root.focus({ preventScroll: true });
@@ -3154,7 +3517,7 @@ var CodeCanvas = class {
3154
3517
  }
3155
3518
  if (this.pointers.size > 2) return;
3156
3519
  const p = this.local(e);
3157
- if (e.button === 1) {
3520
+ if (e.button === 1 || this.tool === "pan") {
3158
3521
  this.gesture = { t: "pan", id: e.pointerId, sx: p.x, sy: p.y, cam: { ...this.cam } };
3159
3522
  return;
3160
3523
  }
@@ -3653,61 +4016,186 @@ var CodeCanvas = class {
3653
4016
  }
3654
4017
  // ---------------------------------------------------------------- toolbar & references
3655
4018
  renderBar() {
3656
- const bar = this.bar;
4019
+ this.renderDock();
4020
+ this.renderSelectionBar();
4021
+ this.renderRefs();
4022
+ }
4023
+ /** The dock at the bottom: tools, file actions, zoom and the language. */
4024
+ renderDock() {
4025
+ const dock = this.bar;
4026
+ dock.replaceChildren();
4027
+ dock.hidden = !!this.editing;
4028
+ const toolButton = (tool, label, icon) => {
4029
+ const toolBtn = iconButton(label, icon, () => this.setTool(tool));
4030
+ toolBtn.classList.toggle("on", this.tool === tool);
4031
+ toolBtn.setAttribute("aria-pressed", String(this.tool === tool));
4032
+ return toolBtn;
4033
+ };
4034
+ dock.append(toolButton("select", "Select", ICON_SELECT), toolButton("pan", "Pan", ICON_PAN), el("div", "cc-sep"));
4035
+ const firstErrorBlock = this.diagnostics.find((d) => d.blockId)?.blockId;
4036
+ if (firstErrorBlock) dock.append(button(plural(this.diagnostics.length, "error"), () => this.showErrorsOf(firstErrorBlock), "danger"));
4037
+ const diff = this.currentDiff();
4038
+ if (diff) dock.append(changesSummary(diff), el("div", "cc-sep"));
4039
+ dock.append(button("+ Function", () => this.createFunctionAtCenter(), "strong"));
4040
+ if (this.clip) dock.append(button("Paste", () => (this.lastPointer = null, this.paste())));
4041
+ dock.append(
4042
+ this.layoutBeforeArrange ? button("Undo arrange", () => this.undoArrange()) : button("Arrange", () => this.arrange()),
4043
+ button("Fit", () => this.fit()),
4044
+ el("div", "cc-sep"),
4045
+ button("\u2212", () => this.zoomBy(0.8), "square"),
4046
+ this.zoomLabel,
4047
+ button("+", () => this.zoomBy(1.25), "square"),
4048
+ ...this.languageLabelParts()
4049
+ );
4050
+ this.zoomLabel.title = "Zoom";
4051
+ }
4052
+ /** The bar above the selected block: what it uses and what uses it, and actions on it. */
4053
+ renderSelectionBar() {
4054
+ const bar = this.selectionBar;
3657
4055
  bar.replaceChildren();
3658
- bar.hidden = !!this.editing;
3659
4056
  const b = this.selected ? this.view?.blocks.get(this.selected) : null;
3660
- if (b) {
3661
- const chip = el("div", "cc-chip");
3662
- chip.style.setProperty("--k", `var(--k-${b.kind})`);
3663
- setHTML(chip, `<i></i>${esc(b.name)}`);
3664
- const refs = this.refsFor(b.id);
4057
+ bar.hidden = !b || !!this.editing;
4058
+ if (!b) return;
4059
+ const refs = this.refsFor(b.id);
4060
+ const usedBy = `used by ${refs.incoming.length}`;
4061
+ const refsToggle = button(refs.outgoing.length ? `${plural(refs.outgoing.length, "dep")} \xB7 ${usedBy}` : usedBy, () => this.toggleRefs(), "meta");
4062
+ refsToggle.classList.add("cc-refs-toggle");
4063
+ refsToggle.title = "References";
4064
+ refsToggle.setAttribute("aria-expanded", String(this.refsOpen));
4065
+ const close = button("\xD7", () => this.select(null), "square");
4066
+ close.setAttribute("aria-label", "Clear selection");
4067
+ if (this.conflicts) {
4068
+ const isConflicted = this.conflicts.hunksByBlock.has(b.id);
4069
+ const firstHunk = isConflicted ? this.conflicts.hunks[this.conflicts.hunksByBlock.get(b.id)[0]] : void 0;
4070
+ const keep = (label, choice, side) => {
4071
+ const keepButton = button(label, () => this.keepConflictSide(b.id, choice), choice === "current" ? "primary" : void 0);
4072
+ if (side) keepButton.title = side;
4073
+ return keepButton;
4074
+ };
4075
+ const keepButtons = firstHunk ? [keep("Keep current", "current", firstHunk.currentLabel), keep("Keep incoming", "incoming", firstHunk.incomingLabel), keep("Keep both", "both"), el("div", "cc-sep")] : [];
4076
+ bar.append(refsToggle, el("div", "cc-sep"), ...keepButtons, button("Edit", () => this.openEditor(b.id)), this.moreButton(b), close);
4077
+ } else {
3665
4078
  const errors = this.diagnosticsInside(b.id);
3666
- if (this.conflicts) {
3667
- const isConflicted = this.conflicts.hunksByBlock.has(b.id);
3668
- const firstHunk = isConflicted ? this.conflicts.hunks[this.conflicts.hunksByBlock.get(b.id)[0]] : void 0;
3669
- const keep = (label, choice, side) => {
3670
- const keepButton = button(label, () => this.keepConflictSide(b.id, choice), choice === "current" ? "primary" : void 0);
3671
- if (side) keepButton.title = side;
3672
- return keepButton;
3673
- };
3674
- bar.append(
3675
- chip,
3676
- el("div", "cc-sep"),
3677
- ...firstHunk ? [keep("Keep current", "current", firstHunk.currentLabel), keep("Keep incoming", "incoming", firstHunk.incomingLabel), keep("Keep both", "both"), el("div", "cc-sep")] : [],
3678
- button("Edit", () => this.openEditor(b.id)),
3679
- button("Copy", () => this.copy(b.id))
3680
- );
3681
- return this.renderRefs();
3682
- }
3683
4079
  bar.append(
3684
- chip,
4080
+ refsToggle,
3685
4081
  el("div", "cc-sep"),
3686
4082
  ...errors.length ? [button(plural(errors.length, "error"), () => this.toggleRefs(), "danger")] : [],
3687
- button("Edit", () => this.openEditor(b.id)),
4083
+ ...this.opts.onOpenCode ? [button("Open code", () => this.opts.onOpenCode(b))] : [],
4084
+ ...this.opts.onAsk ? [button("Ask", () => this.opts.onAsk(b))] : [],
3688
4085
  ...isRenamable(b) ? [button("Rename", () => this.startRename(b.id))] : [],
3689
- button(`Refs ${refs.incoming.length + refs.outgoing.length}`, () => this.toggleRefs()),
3690
- button("Copy", () => this.copy(b.id)),
3691
- ...b.kind !== "import" && b.kind !== "statement" ? [button("Duplicate", () => this.duplicate(b.id))] : [],
3692
- ...b.insert ? [button("+ Inside", () => this.createFunctionAtCenter())] : [],
3693
- button("Delete", () => this.deleteBlock(b.id), "danger")
3694
- );
3695
- } else {
3696
- const firstErrorBlock = this.diagnostics.find((d) => d.blockId)?.blockId;
3697
- if (firstErrorBlock) bar.append(button(plural(this.diagnostics.length, "error"), () => this.showErrorsOf(firstErrorBlock), "danger"));
3698
- const diff = this.currentDiff();
3699
- if (diff) bar.append(changesSummary(diff), el("div", "cc-sep"));
3700
- bar.append(button("+ Function", () => this.createFunctionAtCenter()));
3701
- if (this.clip) bar.append(button("Paste", () => (this.lastPointer = null, this.paste())));
3702
- bar.append(
3703
- this.layoutBeforeArrange ? button("Undo arrange", () => this.undoArrange()) : button("Arrange", () => this.arrange()),
3704
- el("div", "cc-sep"),
3705
- button("Fit", () => this.fit()),
3706
- button("\u2212", () => this.zoomBy(0.8)),
3707
- button("+", () => this.zoomBy(1.25))
4086
+ button("Delete", () => this.deleteBlock(b.id), "danger"),
4087
+ this.moreButton(b),
4088
+ close
3708
4089
  );
3709
4090
  }
3710
- this.renderRefs();
4091
+ this.placeSelectionBar();
4092
+ }
4093
+ moreButton(b) {
4094
+ const more = button("\u22EF", () => {
4095
+ const box = more.getBoundingClientRect();
4096
+ const rootBox = this.root.getBoundingClientRect();
4097
+ this.openBlockMenu(b.id, { x: box.left - rootBox.left, y: box.bottom - rootBox.top + 4 });
4098
+ }, "square");
4099
+ more.setAttribute("aria-label", "More actions");
4100
+ more.setAttribute("aria-haspopup", "menu");
4101
+ return more;
4102
+ }
4103
+ /** The block's actions as a menu, from ⋯ or a right-click. */
4104
+ openBlockMenu(id, at) {
4105
+ const b = this.view?.blocks.get(id);
4106
+ if (!b) return;
4107
+ if (this.selected !== id) this.select(id);
4108
+ const item = (label, action, variant) => button(label, () => (this.closeMenu(), action()), variant);
4109
+ const items = [
4110
+ item("Edit", () => this.openEditor(id)),
4111
+ ...this.opts.onOpenCode ? [item("Open code", () => this.opts.onOpenCode(b))] : [],
4112
+ ...this.opts.onAsk ? [item("Ask", () => this.opts.onAsk(b))] : [],
4113
+ ...isRenamable(b) && !this.conflicts ? [item("Rename", () => this.startRename(id))] : [],
4114
+ item("References", () => (this.refsOpen = true, this.renderBar())),
4115
+ item("Copy", () => this.copy(id)),
4116
+ ...b.kind !== "import" && b.kind !== "statement" && !this.conflicts ? [item("Duplicate", () => this.duplicate(id))] : [],
4117
+ ...b.insert && !this.conflicts ? [item("+ Inside", () => this.createFunctionAtCenter())] : [],
4118
+ ...!this.conflicts ? [item("Delete", () => this.deleteBlock(id), "danger")] : []
4119
+ ];
4120
+ const menu = this.menuEl;
4121
+ menu.replaceChildren(...items);
4122
+ menu.hidden = false;
4123
+ const left = clamp2(at.x, 8, Math.max(8, this.root.clientWidth - menu.offsetWidth - 8));
4124
+ const top = clamp2(at.y, 8, Math.max(8, this.root.clientHeight - menu.offsetHeight - 8));
4125
+ Object.assign(menu.style, { left: left + "px", top: top + "px" });
4126
+ items[0]?.focus({ preventScroll: true });
4127
+ }
4128
+ closeMenu() {
4129
+ this.menuEl.hidden = true;
4130
+ }
4131
+ /** Keep the selection bar right above the selected block (below it when there is no room). */
4132
+ placeSelectionBar() {
4133
+ const bar = this.selectionBar;
4134
+ const geo = this.selected ? this.geom?.geo.get(this.selected) : void 0;
4135
+ if (bar.hidden || !geo || geo.hidden) {
4136
+ bar.style.visibility = "hidden";
4137
+ return;
4138
+ }
4139
+ bar.style.visibility = "";
4140
+ const { x, y, s } = this.cam;
4141
+ const blockTop = geo.ay * s + y;
4142
+ const blockBottom = (geo.ay + geo.h) * s + y;
4143
+ const barHeight = bar.offsetHeight || 34;
4144
+ const fitsAbove = blockTop - barHeight - 8 >= 8;
4145
+ const left = clamp2(geo.ax * s + x, 8, Math.max(8, this.root.clientWidth - bar.offsetWidth - 8));
4146
+ bar.style.left = left + "px";
4147
+ bar.style.top = (fitsAbove ? blockTop - barHeight - 8 : blockBottom + 8) + "px";
4148
+ }
4149
+ setTool(tool) {
4150
+ this.tool = tool;
4151
+ this.root.classList.toggle("cc-tool-pan", tool === "pan");
4152
+ this.renderDock();
4153
+ }
4154
+ /** The whole file in small, in a corner; the visible part outlined. A click moves there. */
4155
+ renderMinimap() {
4156
+ const g = this.geom;
4157
+ const minimap = this.minimapEl;
4158
+ if (!minimap || !g) return;
4159
+ const f = g.frame;
4160
+ const scale = Math.min(MINIMAP.width / f.w, MINIMAP.height / f.h);
4161
+ this.minimapScale = scale;
4162
+ const bars = g.order.filter((geo) => !geo.hidden && geo.block.depth <= 1).map(
4163
+ (geo) => `<i style="--k:var(--k-${geo.block.kind});left:${((geo.ax - f.x) * scale).toFixed(1)}px;top:${((geo.ay - f.y) * scale).toFixed(1)}px;width:${Math.max(2, geo.w * scale).toFixed(1)}px;height:${Math.max(
4164
+ 2,
4165
+ geo.h * scale
4166
+ ).toFixed(1)}px"></i>`
4167
+ ).join("");
4168
+ setHTML(minimap, `<div class="cc-minimap-blocks">${bars}</div><span class="cc-minimap-view"></span>`);
4169
+ this.placeMinimapView();
4170
+ }
4171
+ placeMinimapView() {
4172
+ const g = this.geom;
4173
+ const view = this.minimapEl?.querySelector(".cc-minimap-view");
4174
+ if (!g || !view) return;
4175
+ const { x, y, s } = this.cam;
4176
+ const f = g.frame;
4177
+ const scale = this.minimapScale;
4178
+ const left = clamp2((-x / s - f.x) * scale, -2, MINIMAP.width);
4179
+ const top = clamp2((-y / s - f.y) * scale, -2, MINIMAP.height);
4180
+ const right = clamp2(((this.root.clientWidth - x) / s - f.x) * scale, left, MINIMAP.width + 2);
4181
+ const bottom = clamp2(((this.root.clientHeight - y) / s - f.y) * scale, top, MINIMAP.height + 2);
4182
+ Object.assign(view.style, { left: left + "px", top: top + "px", width: right - left + "px", height: bottom - top + "px" });
4183
+ }
4184
+ moveToMinimapPoint(e) {
4185
+ const g = this.geom;
4186
+ const inner = this.minimapEl?.querySelector(".cc-minimap-blocks");
4187
+ if (!g || !inner) return;
4188
+ const box = inner.getBoundingClientRect();
4189
+ const worldX = g.frame.x + (e.clientX - box.left) / this.minimapScale;
4190
+ const worldY = g.frame.y + (e.clientY - box.top) / this.minimapScale;
4191
+ const s = this.cam.s;
4192
+ this.moveCamera({ x: this.root.clientWidth / 2 - worldX * s, y: this.root.clientHeight / 2 - worldY * s, s }, true);
4193
+ }
4194
+ languageLabelParts() {
4195
+ if (!this.view) return [];
4196
+ const label = el("span", "cc-language");
4197
+ label.textContent = this.view.language.label;
4198
+ return [el("div", "cc-sep"), label];
3711
4199
  }
3712
4200
  toggleRefs() {
3713
4201
  this.refsOpen = !this.refsOpen;
@@ -3748,7 +4236,7 @@ var CodeCanvas = class {
3748
4236
  btn.style.setProperty("--k", `var(--k-${block?.kind ?? "statement"})`);
3749
4237
  const name = block && model ? displayName(model, block) : blockId;
3750
4238
  const label = isOtherFile ? `<span class="cc-file">${esc(fileName(path))} \u203A</span> ${esc(name)}` : esc(name);
3751
- setHTML(btn, `<i></i><span>${label}</span><span>${usage.kind}${usage.count > 1 ? " \xD7" + usage.count : ""}</span>`);
4239
+ setHTML(btn, `<i></i><span>${label}</span><span>${usage.kind === "render" ? "renders" : usage.kind}${usage.count > 1 ? " \xD7" + usage.count : ""}</span>`);
3752
4240
  btn.onclick = () => isOtherFile ? this.opts.onOpenBlock?.(path, blockId) : this.reveal(blockId);
3753
4241
  btn.disabled = isOtherFile && !this.opts.onOpenBlock;
3754
4242
  return btn;
@@ -3863,6 +4351,13 @@ function el(tag, cls) {
3863
4351
  if (cls) e.className = cls;
3864
4352
  return e;
3865
4353
  }
4354
+ function iconButton(label, icon, onClick) {
4355
+ const iconBtn = button("", onClick, "square");
4356
+ setHTML(iconBtn, icon);
4357
+ iconBtn.setAttribute("aria-label", label);
4358
+ iconBtn.title = label;
4359
+ return iconBtn;
4360
+ }
3866
4361
  function button(label, onClick, variant) {
3867
4362
  const b = keepsCanvasFocus(document.createElement("button"));
3868
4363
  b.className = "cc-btn" + (variant ? " " + variant : "");
@@ -3877,9 +4372,6 @@ function keepsCanvasFocus(element) {
3877
4372
  element.addEventListener("pointerdown", (e) => e.preventDefault());
3878
4373
  return element;
3879
4374
  }
3880
- function esc(s) {
3881
- return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
3882
- }
3883
4375
  function clamp2(v, lo, hi) {
3884
4376
  return Math.min(hi, Math.max(lo, v));
3885
4377
  }