@nocturnium/svelte-ide 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/components/ai/AIPanel.svelte +6 -1
  2. package/dist/components/ai/AIToolCallDisplay.svelte +1 -1
  3. package/dist/components/core/Avatar.svelte +14 -14
  4. package/dist/components/core/Badge.svelte +2 -2
  5. package/dist/components/core/Button.svelte +2 -2
  6. package/dist/components/editor/AIFocusLayer.svelte +3 -3
  7. package/dist/components/editor/CognitiveLoadMeter.svelte +15 -15
  8. package/dist/components/editor/CommandPalette.svelte +17 -17
  9. package/dist/components/editor/ComplexityLayer.svelte +11 -9
  10. package/dist/components/editor/ConflictZoneLayer.svelte +8 -8
  11. package/dist/components/editor/CustomEditor.svelte +34 -12
  12. package/dist/components/editor/CustomEditor.svelte.d.ts +5 -1
  13. package/dist/components/editor/FileIcon.svelte +2 -1
  14. package/dist/components/editor/GitBlameLayer.svelte +8 -8
  15. package/dist/components/editor/InlineDiffLayer.svelte +12 -12
  16. package/dist/components/editor/PluginPreviewSandbox.svelte +21 -21
  17. package/dist/components/editor/SnippetPalette.svelte +21 -21
  18. package/dist/components/editor/StructureMap.svelte +47 -30
  19. package/dist/components/editor/TimelineScrubber.svelte +11 -11
  20. package/dist/components/editor/core/complexity-analyzer.d.ts +4 -0
  21. package/dist/components/editor/core/complexity-analyzer.js +70 -12
  22. package/dist/components/editor/core/diagnostics.js +4 -4
  23. package/dist/components/editor/core/semantic-analyzer.js +13 -1
  24. package/dist/components/editor/core/structure-layout.d.ts +31 -0
  25. package/dist/components/editor/core/structure-layout.js +64 -0
  26. package/dist/styles/theme.css +24 -0
  27. package/package.json +1 -1
@@ -19,7 +19,11 @@ const THRESHOLDS = {
19
19
  * Weights for different complexity factors
20
20
  */
21
21
  const WEIGHTS = {
22
- nestingDepth: 15,
22
+ // Nesting is the dominant cognitive-load factor, but 15/level over-rated
23
+ // shallow code: a function with two nested `if`s landed at ~71 ("High").
24
+ // 12/level keeps deeply-nested code critical while letting genuinely medium
25
+ // code read as medium.
26
+ nestingDepth: 12,
23
27
  branchingFactor: 8,
24
28
  lineCount: 0.3,
25
29
  identifierCount: 0.2,
@@ -77,12 +81,15 @@ export class ComplexityAnalyzer {
77
81
  * Get complexity for a specific line
78
82
  */
79
83
  getLineComplexity(metrics, line) {
84
+ // A line can sit inside several nested regions; report the highest score
85
+ // so an inner low-complexity region never masks the hot function around it.
86
+ let best = 0;
80
87
  for (const region of metrics.regions) {
81
- if (line >= region.startLine && line <= region.endLine) {
82
- return region.score;
88
+ if (line >= region.startLine && line <= region.endLine && region.score > best) {
89
+ best = region.score;
83
90
  }
84
91
  }
85
- return 0;
92
+ return best;
86
93
  }
87
94
  /**
88
95
  * Check if a line is a hotspot
@@ -117,10 +124,21 @@ export class ComplexityAnalyzer {
117
124
  // Track blocks with their brace depth at push time
118
125
  const blockStack = [];
119
126
  let braceDepth = 0;
127
+ let pendingDef;
120
128
  for (let i = 0; i < lines.length; i++) {
121
129
  const text = lines[i].text;
122
130
  // Check for function/class definition
123
131
  const funcMatch = text.match(PATTERNS.functionDef);
132
+ const currentBlock = blockStack[blockStack.length - 1];
133
+ const defCandidate = this.getDefinitionCandidate(text, funcMatch, currentBlock?.type === 'class' && braceDepth === currentBlock.depth);
134
+ if (defCandidate) {
135
+ pendingDef = {
136
+ line: i,
137
+ type: defCandidate.type,
138
+ name: defCandidate.name,
139
+ depth: braceDepth
140
+ };
141
+ }
124
142
  // Process character by character to properly match braces
125
143
  // Skip braces inside strings and comments
126
144
  let inString = null;
@@ -150,13 +168,16 @@ export class ComplexityAnalyzer {
150
168
  if (c === '{') {
151
169
  braceDepth++;
152
170
  // If this is the opening brace of a function/class def, push it
153
- if (funcMatch && this.isDefOpeningBrace(text, ch, funcMatch)) {
171
+ if (pendingDef &&
172
+ braceDepth === pendingDef.depth + 1 &&
173
+ this.isDefOpeningBrace(text, ch, pendingDef.type)) {
154
174
  blockStack.push({
155
- line: i,
156
- type: funcMatch[5] ? 'class' : 'function',
157
- name: funcMatch[2] || funcMatch[3] || funcMatch[4] || funcMatch[5],
175
+ line: pendingDef.line,
176
+ type: pendingDef.type,
177
+ name: pendingDef.name,
158
178
  depth: braceDepth
159
179
  });
180
+ pendingDef = undefined;
160
181
  }
161
182
  else if (!funcMatch ||
162
183
  braceDepth > (blockStack.length > 0 ? blockStack[blockStack.length - 1].depth : 0) + 1) {
@@ -185,6 +206,9 @@ export class ComplexityAnalyzer {
185
206
  }
186
207
  }
187
208
  braceDepth = Math.max(0, braceDepth - 1);
209
+ if (pendingDef && braceDepth < pendingDef.depth) {
210
+ pendingDef = undefined;
211
+ }
188
212
  }
189
213
  }
190
214
  }
@@ -202,15 +226,49 @@ export class ComplexityAnalyzer {
202
226
  /**
203
227
  * Check if a `{` at position `ch` is the opening brace of a function/class definition
204
228
  */
205
- isDefOpeningBrace(text, ch, funcMatch) {
229
+ isDefOpeningBrace(text, ch, type) {
206
230
  // For class: `class Foo {` — the `{` follows the class name
207
- if (funcMatch[5]) {
231
+ if (type === 'class') {
208
232
  return true; // First `{` on a class line is the class body
209
233
  }
210
234
  // For functions: the `{` should follow the parameter list closing `)`
211
- // or follow `=>` for arrow functions
235
+ // with an optional TypeScript return type, or follow `=>` for arrow functions.
212
236
  const before = text.slice(0, ch).trimEnd();
213
- return before.endsWith(')') || before.endsWith('=>');
237
+ return /\)\s*(:\s*[^{};]+)?\s*$/.test(before) || before.endsWith('=>');
238
+ }
239
+ /**
240
+ * Extract a definition candidate before its body brace is seen.
241
+ */
242
+ getDefinitionCandidate(text, funcMatch, allowMethodStyle) {
243
+ if (funcMatch) {
244
+ // The arrow-assignment branch (`name = (`) also matches a parenthesised
245
+ // expression like `const x = (a - b) / c`, which is NOT a function.
246
+ // Only treat it as one when the line actually starts an arrow: it
247
+ // contains `=>`, or it has an unclosed `(` that opens a multi-line arrow
248
+ // signature. Otherwise the stale candidate gets attached to the next
249
+ // `if (...) {` block and a phantom region is reported.
250
+ const isArrowAssignment = !!funcMatch[3] && !funcMatch[2] && !funcMatch[4] && !funcMatch[5];
251
+ if (isArrowAssignment) {
252
+ const opens = (text.match(/\(/g) || []).length;
253
+ const closes = (text.match(/\)/g) || []).length;
254
+ if (!text.includes('=>') && opens <= closes) {
255
+ return undefined;
256
+ }
257
+ }
258
+ return {
259
+ type: funcMatch[5] ? 'class' : 'function',
260
+ name: funcMatch[2] || funcMatch[3] || funcMatch[4] || funcMatch[5]
261
+ };
262
+ }
263
+ if (!allowMethodStyle) {
264
+ return undefined;
265
+ }
266
+ const trimmed = text.trim();
267
+ const methodMatch = trimmed.match(/^(?:(?:public|private|protected|static|async|readonly|override)\s+)*(?!(?:if|else|for|while|do|switch|try|catch|finally|with|return|throw|new|typeof|void|delete|await|yield)\b)(\w+)\s*\(/);
268
+ if (methodMatch) {
269
+ return { type: 'function', name: methodMatch[1] };
270
+ }
271
+ return undefined;
214
272
  }
215
273
  /**
216
274
  * Analyze a specific region
@@ -235,13 +235,13 @@ export function getSeverityIcon(severity) {
235
235
  export function getSeverityColor(severity) {
236
236
  switch (severity) {
237
237
  case 'error':
238
- return '#f44747';
238
+ return 'var(--ide-error)';
239
239
  case 'warning':
240
- return '#ff8c00';
240
+ return 'var(--ide-warning)';
241
241
  case 'info':
242
- return '#3794ff';
242
+ return 'var(--ide-info)';
243
243
  case 'hint':
244
- return '#6c6c6c';
244
+ return 'var(--ide-text-muted)';
245
245
  }
246
246
  }
247
247
  /**
@@ -71,8 +71,9 @@ const JAVASCRIPT_PATTERNS = new Map([
71
71
  [
72
72
  'function',
73
73
  {
74
- startPattern: /^(export\s+)?(async\s+)?function\s+(\w+)|^const\s+(\w+)\s*=\s*(async\s*)?\([^)]*\)\s*=>/,
74
+ startPattern: /^(export\s+)?(async\s+)?function\s+(\w+)|^(?:export\s+)?const\s+(\w+)\s*=\s*(async\s*)?\([^)]*\)\s*(?::[^=]+)?=>/,
75
75
  blockBased: true,
76
+ expressionBody: true,
76
77
  getLabel: (m) => `Function: ${m[3] || m[4] || 'anonymous'}`
77
78
  }
78
79
  ],
@@ -298,6 +299,17 @@ export class SemanticAnalyzer {
298
299
  label: config.getLabel?.(match)
299
300
  });
300
301
  }
302
+ else if (match && config.expressionBody && text.includes('=>')) {
303
+ // Brace-less expression-bodied arrow (e.g. `export const f = () => x`):
304
+ // no block to track, so record it as a single-line region.
305
+ regions.push({
306
+ category,
307
+ startLine: i,
308
+ endLine: i,
309
+ confidence: 0.85,
310
+ label: config.getLabel?.(match)
311
+ });
312
+ }
301
313
  }
302
314
  }
303
315
  // Update depth
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Structure Map row layout
3
+ *
4
+ * The Structure Map places one labeled row per semantic region. Rows want to sit
5
+ * at a position proportional to their start line (so the map reads like a
6
+ * minimap), but every label needs a minimum pixel height to stay legible. With
7
+ * pure proportional placement, regions that start on the same line (e.g. the
8
+ * `exports` and `class` regions of `export class Foo`) or that cluster densely
9
+ * (a run of test hooks) overprint each other into illegible mush.
10
+ *
11
+ * `layoutStructureRows` resolves non-overlapping vertical offsets:
12
+ * - pass 1 (top-down): no row sits closer than `rowHeight` below the previous
13
+ * one, preserving proportional placement wherever there is room;
14
+ * - pass 2 (bottom-up): if the de-collided stack overruns the available height,
15
+ * pull rows back up so the last one ends exactly at `height`, keeping the
16
+ * minimum spacing intact.
17
+ *
18
+ * When there are genuinely more rows than can fit (`rows * rowHeight > height`)
19
+ * the stack is floored at the top and rows are placed sequentially; callers are
20
+ * expected to clip the overflow (`overflow: hidden`).
21
+ */
22
+ /**
23
+ * Resolve non-overlapping top offsets (in px) for structure-map rows.
24
+ *
25
+ * @param startLines 0-based start line of each row, in render order.
26
+ * @param totalLines Total line count the map represents.
27
+ * @param height Available height of the rendering area, in px.
28
+ * @param rowHeight Minimum legible height of a single row, in px.
29
+ * @returns Top offset in px for each input row, in the same order.
30
+ */
31
+ export declare function layoutStructureRows(startLines: readonly number[], totalLines: number, height: number, rowHeight: number): number[];
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Structure Map row layout
3
+ *
4
+ * The Structure Map places one labeled row per semantic region. Rows want to sit
5
+ * at a position proportional to their start line (so the map reads like a
6
+ * minimap), but every label needs a minimum pixel height to stay legible. With
7
+ * pure proportional placement, regions that start on the same line (e.g. the
8
+ * `exports` and `class` regions of `export class Foo`) or that cluster densely
9
+ * (a run of test hooks) overprint each other into illegible mush.
10
+ *
11
+ * `layoutStructureRows` resolves non-overlapping vertical offsets:
12
+ * - pass 1 (top-down): no row sits closer than `rowHeight` below the previous
13
+ * one, preserving proportional placement wherever there is room;
14
+ * - pass 2 (bottom-up): if the de-collided stack overruns the available height,
15
+ * pull rows back up so the last one ends exactly at `height`, keeping the
16
+ * minimum spacing intact.
17
+ *
18
+ * When there are genuinely more rows than can fit (`rows * rowHeight > height`)
19
+ * the stack is floored at the top and rows are placed sequentially; callers are
20
+ * expected to clip the overflow (`overflow: hidden`).
21
+ */
22
+ /**
23
+ * Resolve non-overlapping top offsets (in px) for structure-map rows.
24
+ *
25
+ * @param startLines 0-based start line of each row, in render order.
26
+ * @param totalLines Total line count the map represents.
27
+ * @param height Available height of the rendering area, in px.
28
+ * @param rowHeight Minimum legible height of a single row, in px.
29
+ * @returns Top offset in px for each input row, in the same order.
30
+ */
31
+ export function layoutStructureRows(startLines, totalLines, height, rowHeight) {
32
+ const n = startLines.length;
33
+ if (n === 0)
34
+ return [];
35
+ const span = Math.max(1, totalLines);
36
+ const tops = startLines.map((line) => (Math.max(0, line) / span) * height);
37
+ // Pass 1 — top-down: enforce the minimum gap, keeping proportional order.
38
+ for (let i = 1; i < n; i++) {
39
+ const min = tops[i - 1] + rowHeight;
40
+ if (tops[i] < min)
41
+ tops[i] = min;
42
+ }
43
+ // Pass 2 — bottom-up: if the stack overflows, slide it back into bounds.
44
+ if (height > 0 && tops[n - 1] + rowHeight > height) {
45
+ tops[n - 1] = height - rowHeight;
46
+ for (let i = n - 2; i >= 0; i--) {
47
+ const max = tops[i + 1] - rowHeight;
48
+ if (tops[i] > max)
49
+ tops[i] = max;
50
+ }
51
+ // More rows than fit: floor at the top and place sequentially. Some
52
+ // overlap is unavoidable here, but the result is deterministic and the
53
+ // caller clips it.
54
+ if (tops[0] < 0) {
55
+ tops[0] = 0;
56
+ for (let i = 1; i < n; i++) {
57
+ const min = tops[i - 1] + rowHeight;
58
+ if (tops[i] < min)
59
+ tops[i] = min;
60
+ }
61
+ }
62
+ }
63
+ return tops;
64
+ }
@@ -65,6 +65,12 @@
65
65
  --ide-interactive-muted: color-mix(in srgb, var(--ide-interactive) 70%, transparent);
66
66
  --ide-interactive-rgb: 74, 141, 183;
67
67
 
68
+ /* Primary action — the signature warm ember accent, shared as a single
69
+ source of truth by primary buttons and badges so "primary" reads the
70
+ same everywhere. */
71
+ --ide-primary: var(--ide-interactive-active);
72
+ --ide-primary-hover: var(--ide-interactive-hover);
73
+
68
74
  /* IDE Accent Colors */
69
75
  --ide-accent: var(--color-nocturnium-wave);
70
76
  --ide-accent-hover: var(--color-nocturnium-flame);
@@ -85,11 +91,14 @@
85
91
 
86
92
  /* Plugin Status Colors */
87
93
  --ide-plugin-draft: var(--ide-text-muted);
94
+ --ide-plugin-submitted: var(--ide-info);
88
95
  --ide-plugin-reviewing: var(--color-nocturnium-aurora-yellow);
89
96
  --ide-plugin-approved: var(--color-nocturnium-aurora-green);
97
+ --ide-plugin-testing: var(--color-nocturnium-teal);
90
98
  --ide-plugin-rejected: var(--ide-error);
91
99
  --ide-plugin-deploying: var(--color-nocturnium-aurora-blue);
92
100
  --ide-plugin-deployed: var(--color-nocturnium-aurora-green);
101
+ --ide-plugin-rolled_back: var(--color-nocturnium-flame);
93
102
 
94
103
  /* CRDT Collaboration Colors */
95
104
  --ide-collab-cursor-1: var(--color-nocturnium-aurora-blue);
@@ -397,6 +406,11 @@
397
406
  color: var(--ide-plugin-draft);
398
407
  }
399
408
 
409
+ .ide-plugin-status--submitted {
410
+ background-color: color-mix(in srgb, var(--ide-plugin-submitted) 20%, transparent);
411
+ color: var(--ide-plugin-submitted);
412
+ }
413
+
400
414
  .ide-plugin-status--reviewing {
401
415
  background-color: color-mix(in srgb, var(--ide-plugin-reviewing) 20%, transparent);
402
416
  color: var(--ide-plugin-reviewing);
@@ -407,6 +421,11 @@
407
421
  color: var(--ide-plugin-approved);
408
422
  }
409
423
 
424
+ .ide-plugin-status--testing {
425
+ background-color: color-mix(in srgb, var(--ide-plugin-testing) 20%, transparent);
426
+ color: var(--ide-plugin-testing);
427
+ }
428
+
410
429
  .ide-plugin-status--rejected {
411
430
  background-color: color-mix(in srgb, var(--ide-plugin-rejected) 20%, transparent);
412
431
  color: var(--ide-plugin-rejected);
@@ -422,6 +441,11 @@
422
441
  color: var(--ide-plugin-deployed);
423
442
  }
424
443
 
444
+ .ide-plugin-status--rolled_back {
445
+ background-color: color-mix(in srgb, var(--ide-plugin-rolled_back) 20%, transparent);
446
+ color: var(--ide-plugin-rolled_back);
447
+ }
448
+
425
449
  /* Agent Status Animations */
426
450
  @keyframes ide-agent-pulse {
427
451
  0%,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocturnium/svelte-ide",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Svelte 5 code editor and IDE building blocks — custom editor, syntax highlighting, code folding, multi-cursor, LSP client, and optional realtime collaboration.",
5
5
  "author": "Nocturnium & Jordan Dziat <hello@nocturnium.ai> (https://nocturnium.ai)",
6
6
  "license": "MIT",