@nocturnium/svelte-ide 1.1.0 → 1.2.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.
Files changed (33) hide show
  1. package/dist/components/editor/CognitiveLoadMeter.svelte +27 -0
  2. package/dist/components/editor/ComplexityHeatLayer.svelte +157 -0
  3. package/dist/components/editor/ComplexityHeatLayer.svelte.d.ts +24 -0
  4. package/dist/components/editor/ComplexityLayer.svelte +345 -110
  5. package/dist/components/editor/ComplexityLayer.svelte.d.ts +20 -0
  6. package/dist/components/editor/ConflictZoneLayer.svelte +22 -15
  7. package/dist/components/editor/CustomEditor.svelte +81 -1
  8. package/dist/components/editor/CustomEditor.svelte.d.ts +3 -1
  9. package/dist/components/editor/EchoCursorLayer.svelte +60 -0
  10. package/dist/components/editor/PluginPreviewSandbox.svelte +43 -9
  11. package/dist/components/editor/PluginPreviewSandbox.svelte.d.ts +4 -4
  12. package/dist/components/editor/core/complexity-analyzer.d.ts +31 -0
  13. package/dist/components/editor/core/complexity-analyzer.js +479 -29
  14. package/dist/components/editor/core/conflict-predictor.d.ts +32 -0
  15. package/dist/components/editor/core/conflict-predictor.js +55 -0
  16. package/dist/components/editor/core/crdt-binding.d.ts +4 -0
  17. package/dist/components/editor/core/crdt-binding.js +34 -9
  18. package/dist/components/editor/core/echo-cursor.d.ts +18 -1
  19. package/dist/components/editor/core/echo-cursor.js +117 -6
  20. package/dist/components/editor/core/extract-function.d.ts +27 -0
  21. package/dist/components/editor/core/extract-function.js +865 -0
  22. package/dist/components/editor/core/index.d.ts +1 -0
  23. package/dist/components/editor/core/index.js +1 -0
  24. package/dist/components/editor/core/state.d.ts +38 -5
  25. package/dist/components/editor/core/state.js +175 -98
  26. package/dist/components/editor/core/timeline.js +6 -1
  27. package/dist/components/editor/editor-find.js +15 -3
  28. package/dist/components/editor/theme.d.ts +8 -0
  29. package/dist/components/editor/theme.js +52 -0
  30. package/dist/services/lsp-client.d.ts +3 -0
  31. package/dist/services/lsp-client.js +86 -14
  32. package/dist/styles/theme.css +4 -1
  33. package/package.json +1 -1
@@ -7,7 +7,6 @@
7
7
  * Hover over gutter indicators for details.
8
8
  */
9
9
 
10
- import { SvelteMap } from 'svelte/reactivity';
11
10
  import type { ComplexityMetrics, ComplexityRegion } from './core/complexity-analyzer';
12
11
 
13
12
  interface Props {
@@ -17,68 +16,42 @@
17
16
  lineHeight: number;
18
17
  /** Gutter width in pixels */
19
18
  gutterWidth?: number;
19
+ /**
20
+ * Full height of the scrollable content in pixels. The layer must span the
21
+ * whole document (not just one viewport) or indicators below the first
22
+ * screenful get clipped — which previously hid every high-complexity region
23
+ * that sat past the initial view.
24
+ */
25
+ totalHeight?: number;
26
+ /** Estimated full scrollable content width in pixels */
27
+ contentWidth?: number;
20
28
  /** Minimum score to show indicators (default: 50) */
21
29
  minScore?: number;
22
30
  /** Whether indicators are enabled */
23
31
  enabled?: boolean;
32
+ /** Region key to briefly emphasize after jump-to-hottest */
33
+ flashRegionKey?: string;
34
+ /** Maps a raw document line to its rendered visual row */
35
+ lineToVisualRow?: (line: number) => number;
24
36
  }
25
37
 
26
- let { metrics, lineHeight, gutterWidth = 50, minScore = 50, enabled = true }: Props = $props();
38
+ let {
39
+ metrics,
40
+ lineHeight,
41
+ gutterWidth = 50,
42
+ totalHeight = 0,
43
+ contentWidth = 0,
44
+ minScore = 50,
45
+ enabled = true,
46
+ flashRegionKey = '',
47
+ lineToVisualRow = (line: number) => line
48
+ }: Props = $props();
27
49
 
28
50
  // Filter regions that exceed the threshold
29
51
  let highlightedRegions = $derived(
30
52
  enabled && metrics ? metrics.regions.filter((r) => r.score >= minScore) : []
31
53
  );
32
54
 
33
- // Expand regions to individual line indicators for gutter
34
- let lineIndicators = $derived.by(() => {
35
- if (!enabled || !metrics) return [];
36
-
37
- const indicators: Array<{
38
- line: number;
39
- score: number;
40
- region: ComplexityRegion;
41
- isStart: boolean;
42
- isEnd: boolean;
43
- }> = [];
44
-
45
- for (const region of highlightedRegions) {
46
- for (let line = region.startLine; line <= region.endLine; line++) {
47
- indicators.push({
48
- line,
49
- score: region.score,
50
- region,
51
- isStart: line === region.startLine,
52
- isEnd: line === region.endLine
53
- });
54
- }
55
- }
56
-
57
- return indicators;
58
- });
59
-
60
- // Group by line (in case of overlapping regions, take highest score)
61
- let lineScores = $derived.by(() => {
62
- const scores = new SvelteMap<
63
- number,
64
- { score: number; region: ComplexityRegion; isStart: boolean; isEnd: boolean }
65
- >();
66
-
67
- for (const indicator of lineIndicators) {
68
- const existing = scores.get(indicator.line);
69
- if (!existing || indicator.score > existing.score) {
70
- scores.set(indicator.line, {
71
- score: indicator.score,
72
- region: indicator.region,
73
- isStart: indicator.isStart,
74
- isEnd: indicator.isEnd
75
- });
76
- }
77
- }
78
-
79
- return scores;
80
- });
81
-
82
55
  /**
83
56
  * Get the color based on score level
84
57
  */
@@ -87,7 +60,8 @@
87
60
  // medium otherwise (lines below the minScore threshold aren't shown).
88
61
  if (score >= 85) return 'var(--ide-error)'; // Critical
89
62
  if (score >= 70) return 'var(--ide-warning)'; // High
90
- return 'var(--ide-info)'; // Medium
63
+ if (score >= 50) return 'var(--ide-info)'; // Medium
64
+ return 'var(--ide-success)'; // Low
91
65
  }
92
66
 
93
67
  /**
@@ -95,19 +69,64 @@
95
69
  */
96
70
  function getOpacity(score: number): number {
97
71
  const normalized = Math.min(1, Math.max(0, (score - minScore) / (100 - minScore)));
98
- return 0.4 + normalized * 0.6;
72
+ return 0.65 + normalized * 0.35;
99
73
  }
100
74
 
101
75
  let hoveredRegion = $state<ComplexityRegion | null>(null);
102
- let tooltipPosition = $state({ top: 0, left: 0 });
76
+ let tooltipPosition = $state({ top: 0, left: 0, right: 0 });
77
+ let tooltipAnchor = $state<'left' | 'right'>('right');
78
+ let visibleContributions = $derived(
79
+ hoveredRegion
80
+ ? [...hoveredRegion.contributions].sort((a, b) => a.line - b.line).slice(0, 8)
81
+ : []
82
+ );
83
+ let hiddenContributionCount = $derived(
84
+ hoveredRegion
85
+ ? Math.max(0, hoveredRegion.contributions.length - visibleContributions.length)
86
+ : 0
87
+ );
88
+
89
+ function getRegionKey(region: ComplexityRegion): string {
90
+ return `${region.startLine}:${region.endLine}:${region.name ?? region.type}:${region.score}`;
91
+ }
92
+
93
+ function getLevelLabel(score: number): string {
94
+ if (score >= 85) return 'Critical';
95
+ if (score >= 70) return 'High';
96
+ if (score >= 50) return 'Medium';
97
+ return 'Low';
98
+ }
103
99
 
104
- function handleMouseEnter(region: ComplexityRegion, event: MouseEvent) {
100
+ function getRegionTop(region: ComplexityRegion): number {
101
+ return lineToVisualRow(region.startLine) * lineHeight;
102
+ }
103
+
104
+ function getRegionHeight(region: ComplexityRegion): number {
105
+ const startRow = lineToVisualRow(region.startLine);
106
+ const endRow = Math.max(startRow, lineToVisualRow(region.endLine));
107
+ return Math.max(lineHeight, (endRow - startRow + 1) * lineHeight);
108
+ }
109
+
110
+ function getContributionLabel(contribution: ComplexityRegion['contributions'][number]): string {
111
+ return contribution.kind || contribution.reason;
112
+ }
113
+
114
+ function handleMouseEnter(
115
+ region: ComplexityRegion,
116
+ event: MouseEvent,
117
+ anchor: 'left' | 'right' = 'right'
118
+ ) {
105
119
  hoveredRegion = region;
106
- const rect = (event.target as HTMLElement).getBoundingClientRect();
107
- tooltipPosition = {
108
- top: rect.top + window.scrollY,
109
- left: rect.right + 8
110
- };
120
+ const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
121
+ tooltipAnchor = anchor;
122
+ // The gutter spine opens the tooltip to its right (over the code); the score
123
+ // badge sits at the right edge, so it opens to its left. Anchor the left case
124
+ // by `right` (not a translateX) so the box grows leftward cleanly — translateX
125
+ // could subpixel-clip the first glyph of the region name.
126
+ tooltipPosition =
127
+ anchor === 'right'
128
+ ? { top: rect.top, left: rect.right + 8, right: 0 }
129
+ : { top: rect.top, left: 0, right: window.innerWidth - rect.left + 8 };
111
130
  }
112
131
 
113
132
  function handleMouseLeave() {
@@ -115,27 +134,45 @@
115
134
  }
116
135
  </script>
117
136
 
118
- {#if enabled && lineScores.size > 0}
119
- <div class="complexity-gutter" aria-hidden="true" style="width: {gutterWidth}px;">
120
- {#each [...lineScores.entries()] as [line, data] (line)}
137
+ {#if enabled && highlightedRegions.length > 0}
138
+ <div
139
+ class="complexity-gutter"
140
+ aria-hidden="true"
141
+ style="width: {contentWidth ? `${contentWidth}px` : '100%'};{totalHeight
142
+ ? ` height: ${totalHeight}px;`
143
+ : ''} --editor-gutter-width: {gutterWidth}px;"
144
+ >
145
+ {#each highlightedRegions as region (getRegionKey(region))}
146
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
121
147
  <div
122
- class="complexity-gutter__indicator"
123
- class:complexity-gutter__indicator--start={data.isStart}
124
- class:complexity-gutter__indicator--end={data.isEnd}
148
+ class="complexity-gutter__spine"
149
+ class:complexity-gutter__spine--critical={region.score >= 85}
150
+ class:complexity-gutter__spine--flash={flashRegionKey === getRegionKey(region)}
125
151
  style="
126
- top: {line * lineHeight}px;
127
- height: {lineHeight}px;
128
- --indicator-color: {getColor(data.score)};
129
- --indicator-opacity: {getOpacity(data.score)};
152
+ top: {getRegionTop(region)}px;
153
+ height: {getRegionHeight(region)}px;
154
+ --indicator-color: {getColor(region.score)};
155
+ --indicator-opacity: {getOpacity(region.score)};
156
+ --indicator-glow: {region.score >= 85 ? 18 : region.score >= 70 ? 14 : 9}px;
130
157
  "
131
- onmouseenter={(e) => handleMouseEnter(data.region, e)}
158
+ onmouseenter={(e) => handleMouseEnter(region, e)}
159
+ onmouseleave={handleMouseLeave}
160
+ ></div>
161
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
162
+ <div
163
+ class="complexity-gutter__score"
164
+ class:complexity-gutter__score--critical={region.score >= 85}
165
+ class:complexity-gutter__score--flash={flashRegionKey === getRegionKey(region)}
166
+ style="
167
+ top: {getRegionTop(region) + 3}px;
168
+ right: 18px;
169
+ --indicator-color: {getColor(region.score)};
170
+ --indicator-opacity: {getOpacity(region.score)};
171
+ "
172
+ onmouseenter={(e) => handleMouseEnter(region, e, 'left')}
132
173
  onmouseleave={handleMouseLeave}
133
- role="button"
134
- tabindex="-1"
135
174
  >
136
- {#if data.isStart}
137
- <span class="complexity-gutter__score">{data.score}</span>
138
- {/if}
175
+ {region.score}
139
176
  </div>
140
177
  {/each}
141
178
  </div>
@@ -144,19 +181,40 @@
144
181
  {#if hoveredRegion}
145
182
  <div
146
183
  class="complexity-tooltip"
147
- style="top: {tooltipPosition.top}px; left: {tooltipPosition.left}px;"
184
+ style={tooltipAnchor === 'left'
185
+ ? `top: ${tooltipPosition.top}px; right: ${tooltipPosition.right}px;`
186
+ : `top: ${tooltipPosition.top}px; left: ${tooltipPosition.left}px;`}
148
187
  >
149
188
  <div class="complexity-tooltip__header">
150
- <span class="complexity-tooltip__badge" style="background: {getColor(hoveredRegion.score)}">
151
- {hoveredRegion.score}
152
- </span>
153
- <span class="complexity-tooltip__title">
154
- {hoveredRegion.name || hoveredRegion.type}
189
+ <span class="complexity-tooltip__badge" style="color: {getColor(hoveredRegion.score)}">
190
+ {hoveredRegion.cognitiveComplexity}
155
191
  </span>
192
+ <div class="complexity-tooltip__heading">
193
+ <span class="complexity-tooltip__title">
194
+ {getLevelLabel(hoveredRegion.score)} Cognitive Complexity
195
+ </span>
196
+ <span class="complexity-tooltip__source">SonarSource Cognitive Complexity metric</span>
197
+ </div>
156
198
  </div>
157
199
  <div class="complexity-tooltip__lines">
158
- Lines {hoveredRegion.startLine + 1} - {hoveredRegion.endLine + 1}
200
+ {hoveredRegion.name || hoveredRegion.type} · Lines {hoveredRegion.startLine + 1} - {hoveredRegion.endLine +
201
+ 1}
202
+ · Score {hoveredRegion.score}
159
203
  </div>
204
+ {#if visibleContributions.length > 0}
205
+ <ul class="complexity-tooltip__contributions">
206
+ {#each visibleContributions as contribution, index (`${contribution.line}:${contribution.kind}:${index}`)}
207
+ <li>
208
+ <span>line {contribution.line + 1}</span>
209
+ <strong>+{contribution.increment}</strong>
210
+ <span>{getContributionLabel(contribution)} (nesting {contribution.nesting})</span>
211
+ </li>
212
+ {/each}
213
+ {#if hiddenContributionCount > 0}
214
+ <li class="complexity-tooltip__more">+{hiddenContributionCount} more</li>
215
+ {/if}
216
+ </ul>
217
+ {/if}
160
218
  {#if hoveredRegion.suggestion}
161
219
  <div class="complexity-tooltip__suggestion">
162
220
  {hoveredRegion.suggestion}
@@ -176,57 +234,195 @@
176
234
  position: absolute;
177
235
  top: 0;
178
236
  left: 0;
237
+ /* Height is set inline to the full content height so indicators below the
238
+ first viewport are not clipped. Fall back to the viewport when unknown. */
179
239
  bottom: 0;
180
240
  pointer-events: none;
181
241
  z-index: 5;
182
- overflow: hidden;
242
+ overflow: visible;
183
243
  }
184
244
 
185
- .complexity-gutter__indicator {
245
+ .complexity-gutter__spine {
186
246
  position: absolute;
187
- right: 2px;
247
+ left: calc(var(--editor-gutter-width, 50px) - 3px);
188
248
  width: 4px;
189
249
  pointer-events: auto;
190
250
  cursor: help;
191
251
  background: var(--indicator-color);
192
252
  opacity: var(--indicator-opacity);
253
+ border-radius: 999px;
254
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--indicator-color) 35%, transparent);
255
+ isolation: isolate;
193
256
  transition:
194
257
  opacity 0.15s ease,
195
- width 0.15s ease;
258
+ width 0.15s ease,
259
+ box-shadow 0.15s ease,
260
+ transform 0.15s ease;
261
+ }
262
+
263
+ .complexity-gutter__spine::before {
264
+ content: '';
265
+ position: absolute;
266
+ inset: -2px -1px;
267
+ border-radius: inherit;
268
+ background: var(--indicator-color);
269
+ filter: blur(6px);
270
+ opacity: 0.55;
271
+ transform: scaleX(1);
272
+ transform-origin: center;
273
+ z-index: -1;
196
274
  }
197
275
 
198
- .complexity-gutter__indicator:hover {
276
+ .complexity-gutter__spine:hover {
199
277
  opacity: 1;
200
278
  width: 6px;
279
+ transform: translateX(-1px);
201
280
  }
202
281
 
203
- .complexity-gutter__indicator--start {
204
- border-radius: 2px 2px 0 0;
282
+ .complexity-gutter__spine--critical {
283
+ animation: complexity-spine-pulse 2.4s ease-in-out infinite;
205
284
  }
206
285
 
207
- .complexity-gutter__indicator--end {
208
- border-radius: 0 0 2px 2px;
286
+ .complexity-gutter__spine--critical::before {
287
+ animation: complexity-spine-bloom-pulse 2.4s ease-in-out infinite;
209
288
  }
210
289
 
211
- .complexity-gutter__indicator--start.complexity-gutter__indicator--end {
212
- border-radius: 2px;
290
+ .complexity-gutter__spine--flash {
291
+ animation: complexity-spine-flash 0.9s ease-out 1;
292
+ }
293
+
294
+ .complexity-gutter__spine--flash::before {
295
+ animation: complexity-spine-bloom-flash 0.9s ease-out 1;
213
296
  }
214
297
 
215
298
  .complexity-gutter__score {
216
299
  position: absolute;
217
- right: 8px;
218
- top: 50%;
219
- transform: translateY(-50%);
220
- font-size: 9px;
221
- font-weight: 700;
300
+ display: inline-flex;
301
+ align-items: center;
302
+ justify-content: center;
303
+ min-width: 44px;
304
+ height: 30px;
305
+ padding: 0 10px;
306
+ border: 1px solid color-mix(in srgb, var(--indicator-color) 65%, transparent);
307
+ border-radius: 999px;
308
+ background: color-mix(in srgb, var(--indicator-color) 18%, var(--ide-bg-primary));
309
+ box-shadow:
310
+ 0 0 18px color-mix(in srgb, var(--indicator-color) 32%, transparent),
311
+ inset 0 1px 0 color-mix(in srgb, #fff 12%, transparent);
222
312
  color: var(--indicator-color);
223
- opacity: 0;
224
- transition: opacity 0.15s ease;
225
- pointer-events: none;
313
+ font-size: 22px;
314
+ font-weight: 800;
315
+ font-variant-numeric: tabular-nums;
316
+ line-height: 1;
317
+ text-shadow: 0 0 12px color-mix(in srgb, var(--indicator-color) 48%, transparent);
318
+ opacity: calc(0.76 + (var(--indicator-opacity) - 0.65) * 0.7);
319
+ transform: scale(1);
320
+ transform-origin: center;
321
+ /* The badge is a primary hover target for the explain-on-hover tooltip,
322
+ not only the 4px spine. */
323
+ pointer-events: auto;
324
+ cursor: help;
325
+ box-sizing: border-box;
226
326
  }
227
327
 
228
- .complexity-gutter__indicator:hover .complexity-gutter__score {
229
- opacity: 1;
328
+ .complexity-gutter__score--critical {
329
+ animation: complexity-score-glow-pulse 2.4s ease-in-out infinite;
330
+ }
331
+
332
+ .complexity-gutter__score--flash {
333
+ animation: complexity-score-arrival-pop 0.35s ease-out 1;
334
+ }
335
+
336
+ @keyframes complexity-spine-pulse {
337
+ 0%,
338
+ 100% {
339
+ transform: scaleX(1);
340
+ }
341
+ 50% {
342
+ transform: scaleX(1.12);
343
+ }
344
+ }
345
+
346
+ @keyframes complexity-spine-bloom-pulse {
347
+ 0%,
348
+ 100% {
349
+ filter: blur(6px);
350
+ opacity: 0.55;
351
+ transform: scaleX(1);
352
+ }
353
+ 50% {
354
+ filter: blur(10px);
355
+ opacity: 0.78;
356
+ transform: scaleX(1.35);
357
+ }
358
+ }
359
+
360
+ @keyframes complexity-score-glow-pulse {
361
+ 0%,
362
+ 100% {
363
+ box-shadow:
364
+ 0 0 18px color-mix(in srgb, var(--indicator-color) 32%, transparent),
365
+ inset 0 1px 0 color-mix(in srgb, #fff 12%, transparent);
366
+ transform: scale(1);
367
+ }
368
+ 50% {
369
+ box-shadow:
370
+ 0 0 28px color-mix(in srgb, var(--indicator-color) 52%, transparent),
371
+ 0 0 44px color-mix(in srgb, var(--indicator-color) 28%, transparent),
372
+ inset 0 1px 0 color-mix(in srgb, #fff 16%, transparent);
373
+ transform: scale(1.04);
374
+ }
375
+ }
376
+
377
+ @keyframes complexity-score-arrival-pop {
378
+ 0% {
379
+ transform: scale(1.15);
380
+ box-shadow:
381
+ 0 0 30px color-mix(in srgb, var(--indicator-color) 62%, transparent),
382
+ 0 0 58px color-mix(in srgb, var(--indicator-color) 36%, transparent),
383
+ inset 0 1px 0 color-mix(in srgb, #fff 18%, transparent);
384
+ }
385
+ 100% {
386
+ transform: scale(1);
387
+ }
388
+ }
389
+
390
+ @keyframes complexity-spine-flash {
391
+ 0% {
392
+ opacity: 1;
393
+ width: 8px;
394
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--indicator-color) 70%, transparent);
395
+ transform: translateX(-2px);
396
+ }
397
+ 100% {
398
+ opacity: var(--indicator-opacity);
399
+ width: 4px;
400
+ transform: translateX(0);
401
+ }
402
+ }
403
+
404
+ @keyframes complexity-spine-bloom-flash {
405
+ 0% {
406
+ filter: blur(12px);
407
+ opacity: 0.9;
408
+ transform: scaleX(1.45);
409
+ }
410
+ 100% {
411
+ filter: blur(6px);
412
+ opacity: 0.55;
413
+ transform: scaleX(1);
414
+ }
415
+ }
416
+
417
+ @media (prefers-reduced-motion: reduce) {
418
+ .complexity-gutter__spine--critical,
419
+ .complexity-gutter__spine--critical::before,
420
+ .complexity-gutter__spine--flash,
421
+ .complexity-gutter__spine--flash::before,
422
+ .complexity-gutter__score--critical,
423
+ .complexity-gutter__score--flash {
424
+ animation: none;
425
+ }
230
426
  }
231
427
 
232
428
  /* Tooltip */
@@ -234,7 +430,7 @@
234
430
  position: fixed;
235
431
  z-index: 1000;
236
432
  min-width: 200px;
237
- max-width: 300px;
433
+ max-width: 360px;
238
434
  padding: 12px;
239
435
  background: var(--ide-bg-secondary, #1a2744);
240
436
  border: 1px solid var(--ide-border, #a8c5d9);
@@ -255,12 +451,18 @@
255
451
  display: flex;
256
452
  align-items: center;
257
453
  justify-content: center;
258
- width: 28px;
259
- height: 28px;
260
- border-radius: 50%;
261
- font-size: 11px;
454
+ min-width: 38px;
455
+ height: 38px;
456
+ font-size: 24px;
262
457
  font-weight: 700;
263
- color: #fff;
458
+ color: var(--indicator-color);
459
+ font-variant-numeric: tabular-nums;
460
+ }
461
+
462
+ .complexity-tooltip__heading {
463
+ display: flex;
464
+ flex-direction: column;
465
+ gap: 2px;
264
466
  }
265
467
 
266
468
  .complexity-tooltip__title {
@@ -268,12 +470,45 @@
268
470
  color: var(--ide-text-primary, #f4f1e0);
269
471
  }
270
472
 
473
+ .complexity-tooltip__source {
474
+ font-size: 10px;
475
+ color: var(--ide-text-muted, #a8c5d9);
476
+ }
477
+
271
478
  .complexity-tooltip__lines {
272
479
  color: var(--ide-text-muted, #a8c5d9);
273
480
  font-size: 11px;
274
481
  margin-bottom: 8px;
275
482
  }
276
483
 
484
+ .complexity-tooltip__contributions {
485
+ list-style: none;
486
+ margin: 0 0 8px;
487
+ padding: 0;
488
+ display: grid;
489
+ gap: 3px;
490
+ }
491
+
492
+ .complexity-tooltip__contributions li {
493
+ display: grid;
494
+ grid-template-columns: 52px 34px minmax(0, 1fr);
495
+ gap: 6px;
496
+ align-items: baseline;
497
+ font-size: 11px;
498
+ color: var(--ide-text-secondary, #a8c5d9);
499
+ }
500
+
501
+ .complexity-tooltip__contributions strong {
502
+ color: var(--ide-text-primary, #f4f1e0);
503
+ font-variant-numeric: tabular-nums;
504
+ }
505
+
506
+ .complexity-tooltip__contributions .complexity-tooltip__more {
507
+ display: block;
508
+ color: var(--ide-text-muted, #a8c5d9);
509
+ font-style: italic;
510
+ }
511
+
277
512
  .complexity-tooltip__suggestion {
278
513
  padding: 8px;
279
514
  background: rgba(255, 255, 255, 0.05);
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Complexity Layer
3
+ *
4
+ * Shows complexity indicators in the gutter area only.
5
+ * No background highlighting - keeps editing area clean.
6
+ * Hover over gutter indicators for details.
7
+ */
1
8
  import type { ComplexityMetrics } from './core/complexity-analyzer';
2
9
  interface Props {
3
10
  /** Complexity metrics for the current document */
@@ -6,10 +13,23 @@ interface Props {
6
13
  lineHeight: number;
7
14
  /** Gutter width in pixels */
8
15
  gutterWidth?: number;
16
+ /**
17
+ * Full height of the scrollable content in pixels. The layer must span the
18
+ * whole document (not just one viewport) or indicators below the first
19
+ * screenful get clipped — which previously hid every high-complexity region
20
+ * that sat past the initial view.
21
+ */
22
+ totalHeight?: number;
23
+ /** Estimated full scrollable content width in pixels */
24
+ contentWidth?: number;
9
25
  /** Minimum score to show indicators (default: 50) */
10
26
  minScore?: number;
11
27
  /** Whether indicators are enabled */
12
28
  enabled?: boolean;
29
+ /** Region key to briefly emphasize after jump-to-hottest */
30
+ flashRegionKey?: string;
31
+ /** Maps a raw document line to its rendered visual row */
32
+ lineToVisualRow?: (line: number) => number;
13
33
  }
14
34
  declare const ComplexityLayer: import("svelte").Component<Props, {}, "">;
15
35
  type ComplexityLayer = ReturnType<typeof ComplexityLayer>;
@@ -40,17 +40,20 @@
40
40
  * Get color for severity level
41
41
  */
42
42
  function getSeverityColor(severity: ConflictZone['severity']): string {
43
+ // Route through the shared --ide-* band tokens so conflict severity stays
44
+ // in lockstep with the complexity band system (one source of truth) and
45
+ // avoids the repo's phantom hardcoded-hex footgun.
43
46
  switch (severity) {
44
47
  case 'critical':
45
- return '#ef4444';
48
+ return 'var(--ide-error)';
46
49
  case 'high':
47
- return '#f59e0b';
50
+ return 'var(--ide-warning)';
48
51
  case 'medium':
49
- return '#eab308';
52
+ return 'var(--ide-info)';
50
53
  case 'low':
51
- return '#22c55e';
54
+ return 'var(--ide-success)';
52
55
  default:
53
- return '#6b7280';
56
+ return 'var(--ide-text-muted)';
54
57
  }
55
58
  }
56
59
 
@@ -243,21 +246,25 @@
243
246
  .conflict-zone {
244
247
  position: absolute;
245
248
  right: 0;
249
+ /* Tint by the zone's own severity color (was hardcoded red for every
250
+ severity, so low/medium zones still washed red). */
246
251
  background: linear-gradient(
247
252
  90deg,
248
- rgba(var(--zone-color-rgb, 239, 68, 68), var(--zone-opacity)),
253
+ color-mix(in srgb, var(--zone-color) calc(var(--zone-opacity) * 100%), transparent),
249
254
  transparent
250
255
  );
251
- background-color: rgba(239, 68, 68, var(--zone-opacity));
252
- border-left: 3px solid rgba(239, 68, 68, var(--border-opacity));
253
- border-left-color: var(--zone-color);
256
+ border-left: 3px solid var(--zone-color);
254
257
  pointer-events: auto;
255
258
  cursor: pointer;
256
- transition: background-color 0.2s ease;
259
+ transition: background 0.2s ease;
257
260
  }
258
261
 
259
262
  .conflict-zone:hover {
260
- background-color: rgba(239, 68, 68, calc(var(--zone-opacity) + 0.1));
263
+ background: linear-gradient(
264
+ 90deg,
265
+ color-mix(in srgb, var(--zone-color) calc((var(--zone-opacity) + 0.1) * 100%), transparent),
266
+ transparent
267
+ );
261
268
  }
262
269
 
263
270
  .conflict-zone--critical {
@@ -417,17 +424,17 @@
417
424
  display: inline-block;
418
425
  padding: 1px 4px;
419
426
  margin-left: 4px;
420
- background: rgba(168, 85, 247, 0.3);
427
+ background: color-mix(in srgb, var(--ide-ai-assistant) 30%, transparent);
421
428
  border-radius: 3px;
422
429
  font-size: 9px;
423
- color: #a855f7;
430
+ color: var(--ide-ai-assistant);
424
431
  }
425
432
 
426
433
  .conflict-tooltip__suggestion {
427
434
  padding: 8px;
428
- background: rgba(255, 200, 0, 0.1);
435
+ background: color-mix(in srgb, var(--ide-warning) 12%, transparent);
429
436
  border-radius: 4px;
430
- border-left: 2px solid #eab308;
437
+ border-left: 2px solid var(--ide-warning);
431
438
  color: var(--ide-text-secondary, #a8c5d9);
432
439
  font-size: 11px;
433
440
  line-height: 1.4;