@nocturnium/svelte-ide 1.4.0 → 1.5.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/components/ai/AIEditPreview.svelte +36 -6
- package/dist/components/ai/AIPanel.svelte +42 -14
- package/dist/components/core/Button.svelte +11 -4
- package/dist/components/core/Icon.svelte +19 -1
- package/dist/components/core/ResizeHandle.svelte +90 -6
- package/dist/components/core/ResizeHandle.svelte.d.ts +6 -0
- package/dist/components/core/Tooltip.svelte +13 -2
- package/dist/components/editor/EchoCursorLayer.svelte +4 -1
- package/dist/components/editor/GhostBracketLayer.svelte +17 -7
- package/dist/components/editor/GitBlameLayer.svelte +10 -3
- package/dist/components/editor/InlineDiagnosticsLayer.svelte +226 -13
- package/dist/components/editor/InlineDiagnosticsLayer.svelte.d.ts +7 -0
- package/dist/components/editor/InlineDiffLayer.svelte +8 -2
- package/dist/components/editor/PluginPreviewSandbox.svelte +10 -2
- package/dist/components/editor/ProblemsPanel.svelte +40 -5
- package/dist/components/editor/SnippetPalette.svelte +63 -20
- package/dist/components/editor/core/diagnostics.js +4 -1
- package/dist/components/editor/core/snippet-manager.js +3 -3
- package/dist/components/plugins/PluginCard.svelte +21 -1
- package/dist/components/plugins/PluginPanel.svelte +17 -3
- package/dist/styles/theme.css +8 -1
- package/package.json +1 -1
|
@@ -34,6 +34,18 @@
|
|
|
34
34
|
return 'default';
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
|
+
|
|
38
|
+
type DiffLineKind = 'added' | 'removed' | 'context';
|
|
39
|
+
|
|
40
|
+
const diffLines = $derived.by((): Array<{ kind: DiffLineKind; text: string }> => {
|
|
41
|
+
if (!session.diff) return [];
|
|
42
|
+
return session.diff.split('\n').map((text) => {
|
|
43
|
+
let kind: DiffLineKind = 'context';
|
|
44
|
+
if (text.startsWith('+') && !text.startsWith('+++')) kind = 'added';
|
|
45
|
+
else if (text.startsWith('-') && !text.startsWith('---')) kind = 'removed';
|
|
46
|
+
return { kind, text };
|
|
47
|
+
});
|
|
48
|
+
});
|
|
37
49
|
</script>
|
|
38
50
|
|
|
39
51
|
<div class="ai-edit-preview {className}">
|
|
@@ -52,7 +64,11 @@
|
|
|
52
64
|
|
|
53
65
|
{#if session.diff}
|
|
54
66
|
<div class="ai-edit-preview__diff">
|
|
55
|
-
|
|
67
|
+
{#each diffLines as line, i (i)}
|
|
68
|
+
<div class="ai-edit-preview__diff-line ai-edit-preview__diff-line--{line.kind}">
|
|
69
|
+
{line.text || ' '}
|
|
70
|
+
</div>
|
|
71
|
+
{/each}
|
|
56
72
|
</div>
|
|
57
73
|
{/if}
|
|
58
74
|
|
|
@@ -110,16 +126,30 @@
|
|
|
110
126
|
.ai-edit-preview__diff {
|
|
111
127
|
max-height: 300px;
|
|
112
128
|
overflow: auto;
|
|
129
|
+
padding: var(--ide-spacing-sm) 0;
|
|
113
130
|
background: var(--ide-bg-primary);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
.ai-edit-preview__diff pre {
|
|
117
|
-
margin: 0;
|
|
118
|
-
padding: var(--ide-spacing-md);
|
|
119
131
|
font-family: var(--ide-font-mono);
|
|
120
132
|
font-size: var(--ide-font-size-xs);
|
|
121
133
|
line-height: 1.6;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.ai-edit-preview__diff-line {
|
|
137
|
+
padding: 0 var(--ide-spacing-md);
|
|
138
|
+
/* keep the +/- gutter and code intact, wrapping long lines */
|
|
122
139
|
white-space: pre-wrap;
|
|
140
|
+
/* reserve the indent the colored rows claim with their left border,
|
|
141
|
+
so added/removed text doesn't shift relative to context lines */
|
|
142
|
+
border-left: 2px solid transparent;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.ai-edit-preview__diff-line--added {
|
|
146
|
+
background: color-mix(in srgb, var(--ide-success) 12%, transparent);
|
|
147
|
+
border-left-color: var(--ide-success);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.ai-edit-preview__diff-line--removed {
|
|
151
|
+
background: color-mix(in srgb, var(--ide-error) 12%, transparent);
|
|
152
|
+
border-left-color: var(--ide-error);
|
|
123
153
|
}
|
|
124
154
|
|
|
125
155
|
.ai-edit-preview__actions {
|
|
@@ -60,8 +60,25 @@
|
|
|
60
60
|
onMount(async () => {
|
|
61
61
|
await initPersistence();
|
|
62
62
|
persistedConversations = await loadConversations();
|
|
63
|
+
// Anchor the conversation to the START of the last message on mount so a
|
|
64
|
+
// seeded/long assistant message reads from its header, not a stale browser
|
|
65
|
+
// mid-content scroll offset (which looks like an abandoned position).
|
|
66
|
+
scrollToLastMessageStart();
|
|
63
67
|
});
|
|
64
68
|
|
|
69
|
+
// Bring the start of the most recent message into view (its header at the top
|
|
70
|
+
// of the viewport) rather than slamming to the very bottom of its body.
|
|
71
|
+
function scrollToLastMessageStart() {
|
|
72
|
+
if (!messagesContainer) return;
|
|
73
|
+
const messages = messagesContainer.querySelectorAll('.ai-message, [data-ai-message]');
|
|
74
|
+
const last = messages[messages.length - 1] as HTMLElement | undefined;
|
|
75
|
+
if (last) {
|
|
76
|
+
messagesContainer.scrollTop = Math.max(0, last.offsetTop - messagesContainer.offsetTop);
|
|
77
|
+
} else {
|
|
78
|
+
messagesContainer.scrollTop = 0;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
65
82
|
// Auto-scroll to bottom when new messages arrive
|
|
66
83
|
$effect(() => {
|
|
67
84
|
if (messagesContainer && getMessages().length > 0) {
|
|
@@ -201,6 +218,7 @@
|
|
|
201
218
|
size="xs"
|
|
202
219
|
onclick={() => (sidebarOpen = !sidebarOpen)}
|
|
203
220
|
title={sidebarOpen ? 'Hide sidebar' : 'Show sidebar'}
|
|
221
|
+
aria-label={sidebarOpen ? 'Hide conversation sidebar' : 'Show conversation sidebar'}
|
|
204
222
|
>
|
|
205
223
|
<Icon name={sidebarOpen ? 'panel-left-close' : 'panel-left'} size={14} />
|
|
206
224
|
</Button>
|
|
@@ -208,12 +226,16 @@
|
|
|
208
226
|
<Icon name="sparkles" size={16} />
|
|
209
227
|
<span>AI Assistant</span>
|
|
210
228
|
</div>
|
|
211
|
-
<span class="ai-panel__mock-badge" title="Demo mock - no real model"
|
|
212
|
-
>Demo mock - no real model</span
|
|
213
|
-
>
|
|
229
|
+
<span class="ai-panel__mock-badge" title="Demo mock - no real model">Demo mock</span>
|
|
214
230
|
</div>
|
|
215
231
|
<div class="ai-panel__actions">
|
|
216
|
-
<Button
|
|
232
|
+
<Button
|
|
233
|
+
variant="ghost"
|
|
234
|
+
size="xs"
|
|
235
|
+
onclick={handleNewConversation}
|
|
236
|
+
title="New conversation"
|
|
237
|
+
aria-label="New conversation"
|
|
238
|
+
>
|
|
217
239
|
<Icon name="plus" size={14} />
|
|
218
240
|
</Button>
|
|
219
241
|
</div>
|
|
@@ -271,7 +293,13 @@
|
|
|
271
293
|
<div class="ai-panel__error">
|
|
272
294
|
<Icon name="alert-circle" size={14} />
|
|
273
295
|
<span>{getError()}</span>
|
|
274
|
-
<Button
|
|
296
|
+
<Button
|
|
297
|
+
variant="ghost"
|
|
298
|
+
size="xs"
|
|
299
|
+
onclick={clearError}
|
|
300
|
+
title="Dismiss error"
|
|
301
|
+
aria-label="Dismiss error"
|
|
302
|
+
>
|
|
275
303
|
<Icon name="x" size={12} />
|
|
276
304
|
</Button>
|
|
277
305
|
</div>
|
|
@@ -283,7 +311,7 @@
|
|
|
283
311
|
bind:this={textareaEl}
|
|
284
312
|
bind:value={inputValue}
|
|
285
313
|
class="ai-panel__textarea"
|
|
286
|
-
placeholder="Ask AI anything...
|
|
314
|
+
placeholder="Ask AI anything..."
|
|
287
315
|
rows={1}
|
|
288
316
|
disabled={getIsStreaming() || isSubmitting}
|
|
289
317
|
onkeydown={handleKeydown}
|
|
@@ -356,9 +384,9 @@
|
|
|
356
384
|
.ai-panel__mock-badge {
|
|
357
385
|
display: inline-flex;
|
|
358
386
|
align-items: center;
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
387
|
+
/* Size to the label so it never clips its own text; the header-left flex
|
|
388
|
+
keeps it from squeezing the title. Full copy lives in the title tooltip. */
|
|
389
|
+
flex-shrink: 0;
|
|
362
390
|
padding: 0.125rem var(--ide-spacing-sm);
|
|
363
391
|
border: 1px solid color-mix(in srgb, var(--ide-accent) 40%, var(--ide-border));
|
|
364
392
|
border-radius: var(--ide-radius-full);
|
|
@@ -367,7 +395,6 @@
|
|
|
367
395
|
font-size: var(--ide-font-size-xs);
|
|
368
396
|
font-weight: 500;
|
|
369
397
|
line-height: var(--ide-line-height-normal);
|
|
370
|
-
text-overflow: ellipsis;
|
|
371
398
|
white-space: nowrap;
|
|
372
399
|
}
|
|
373
400
|
|
|
@@ -504,6 +531,11 @@
|
|
|
504
531
|
align-items: flex-end;
|
|
505
532
|
gap: var(--ide-spacing-sm);
|
|
506
533
|
padding: var(--ide-spacing-md);
|
|
534
|
+
/* Anchor the composer to the bottom of the panel's flex column with a real
|
|
535
|
+
bottom inset (incl. iOS safe-area) so it always sits above the host
|
|
536
|
+
status bar and its border never reads as bleeding off-screen. */
|
|
537
|
+
padding-bottom: calc(var(--ide-spacing-md) + env(safe-area-inset-bottom, 0px));
|
|
538
|
+
flex-shrink: 0;
|
|
507
539
|
border-top: 1px solid var(--ide-border);
|
|
508
540
|
background: var(--ide-bg-secondary);
|
|
509
541
|
}
|
|
@@ -564,10 +596,6 @@
|
|
|
564
596
|
.ai-panel__input {
|
|
565
597
|
padding: var(--ide-spacing-sm);
|
|
566
598
|
}
|
|
567
|
-
|
|
568
|
-
.ai-panel__mock-badge {
|
|
569
|
-
white-space: normal;
|
|
570
|
-
}
|
|
571
599
|
}
|
|
572
600
|
|
|
573
601
|
@media (max-width: 480px) {
|
|
@@ -74,8 +74,15 @@
|
|
|
74
74
|
outline-offset: 2px;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
/* Disabled/loading: dim the fill, not the label. Routing every variant
|
|
78
|
+
to a neutral tertiary surface with muted (still legible) text keeps the
|
|
79
|
+
label >=3:1 instead of multiplying the variant fill by opacity 0.5,
|
|
80
|
+
which crushed cream-on-orange/white-on-salmon below readable contrast. */
|
|
81
|
+
.ide-button.ide-button:disabled {
|
|
82
|
+
background: var(--ide-bg-tertiary);
|
|
83
|
+
color: var(--ide-text-secondary);
|
|
84
|
+
border-color: var(--ide-border);
|
|
85
|
+
opacity: 0.85;
|
|
79
86
|
cursor: not-allowed;
|
|
80
87
|
}
|
|
81
88
|
|
|
@@ -86,7 +93,7 @@
|
|
|
86
93
|
/* Variants */
|
|
87
94
|
.ide-button--primary {
|
|
88
95
|
background: var(--ide-primary);
|
|
89
|
-
color: var(--ide-text-
|
|
96
|
+
color: var(--ide-text-inverse);
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
.ide-button--primary:hover:not(:disabled) {
|
|
@@ -116,7 +123,7 @@
|
|
|
116
123
|
|
|
117
124
|
.ide-button--danger {
|
|
118
125
|
background: var(--ide-error);
|
|
119
|
-
color:
|
|
126
|
+
color: var(--ide-text-inverse);
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
.ide-button--danger:hover:not(:disabled) {
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
folder: 'M10 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8l-2-2z',
|
|
15
15
|
'folder-open':
|
|
16
16
|
'M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2v2M2 11h20l-2 8H4l-2-8z',
|
|
17
|
+
'file-code':
|
|
18
|
+
'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM14 2v6h6M10 13l-2 2 2 2M14 13l2 2-2 2',
|
|
17
19
|
|
|
18
20
|
// Actions
|
|
19
21
|
close: 'M18 6L6 18M6 6l12 12',
|
|
@@ -55,6 +57,11 @@
|
|
|
55
57
|
bot: 'M12 8V4H8M12 8a4 4 0 0 0-4 4v6a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-6a4 4 0 0 0-4-4zM9 13h.01M15 13h.01',
|
|
56
58
|
wand: 'M15 4V2M15 16v-2M8 9h2M20 9h2M17.8 11.8L19 13M17.8 6.2L19 5M12.2 11.8L11 13M12.2 6.2L11 5M12 12l-9 9',
|
|
57
59
|
message: 'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10z',
|
|
60
|
+
'message-circle':
|
|
61
|
+
'M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z',
|
|
62
|
+
send: 'M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z',
|
|
63
|
+
// loader: a continuous spinner ring (3/4 arc with a leading cap) — distinct from `loading`'s 8 discrete spokes
|
|
64
|
+
loader: 'M21 12a9 9 0 1 1-6.219-8.56',
|
|
58
65
|
|
|
59
66
|
// Status
|
|
60
67
|
info: 'M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zM12 16v-4M12 8h.01',
|
|
@@ -77,6 +84,8 @@
|
|
|
77
84
|
'git-commit': 'M12 16a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM1.05 12H8M16 12h6.95',
|
|
78
85
|
'git-merge':
|
|
79
86
|
'M18 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 21V9a9 9 0 0 0 9 9M6 21h12',
|
|
87
|
+
'git-compare':
|
|
88
|
+
'M18 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM13 6h3a2 2 0 0 1 2 2v7M11 18H8a2 2 0 0 1-2-2V9',
|
|
80
89
|
|
|
81
90
|
// Misc
|
|
82
91
|
play: 'M5 3l14 9-14 9V3z',
|
|
@@ -87,11 +96,20 @@
|
|
|
87
96
|
clock: 'M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zM12 6v6l4 2',
|
|
88
97
|
zap: 'M13 2L3 14h9l-1 8 10-12h-9l1-8z',
|
|
89
98
|
plugin:
|
|
90
|
-
'M12 2v6M12 18v4M4.93 4.93l4.24 4.24M14.83 14.83l4.24 4.24M2 12h6M18 12h4M4.93 19.07l4.24-4.24M14.83 9.17l4.24-4.24'
|
|
99
|
+
'M12 2v6M12 18v4M4.93 4.93l4.24 4.24M14.83 14.83l4.24 4.24M2 12h6M18 12h4M4.93 19.07l4.24-4.24M14.83 9.17l4.24-4.24',
|
|
100
|
+
database:
|
|
101
|
+
'M12 8c4.418 0 8-1.343 8-3s-3.582-3-8-3-8 1.343-8 3 3.582 3 8 3zM20 5v6c0 1.657-3.582 3-8 3s-8-1.343-8-3V5M20 11v6c0 1.657-3.582 3-8 3s-8-1.343-8-3v-6'
|
|
91
102
|
};
|
|
92
103
|
|
|
93
104
|
const path = $derived(icons[name] ?? icons.file);
|
|
94
105
|
const sizeValue = $derived(typeof size === 'number' ? `${size}px` : size);
|
|
106
|
+
|
|
107
|
+
// Dev-time guard: warn when a name misses the map so silent `file`-fallback drift is caught.
|
|
108
|
+
$effect(() => {
|
|
109
|
+
if (import.meta.env.DEV && !(name in icons)) {
|
|
110
|
+
console.warn(`[Icon] no glyph for name "${name}" — falling back to the generic "file" icon`);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
95
113
|
</script>
|
|
96
114
|
|
|
97
115
|
<svg
|
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
max?: number;
|
|
19
19
|
/** Current size in pixels */
|
|
20
20
|
size: number;
|
|
21
|
+
/** Step (px) for keyboard arrow adjustment (default 10; Shift = 5x) */
|
|
22
|
+
step?: number;
|
|
23
|
+
/** Size to snap to on double-click (defaults to the midpoint of min/max) */
|
|
24
|
+
defaultSize?: number;
|
|
25
|
+
/** Accessible name for the slider (e.g. "Resize left panel") */
|
|
26
|
+
ariaLabel?: string;
|
|
21
27
|
/** Callback when size changes */
|
|
22
28
|
onResize?: (size: number) => void;
|
|
23
29
|
/** Callback when drag starts */
|
|
@@ -34,6 +40,9 @@
|
|
|
34
40
|
min = 100,
|
|
35
41
|
max = 800,
|
|
36
42
|
size,
|
|
43
|
+
step = 10,
|
|
44
|
+
defaultSize,
|
|
45
|
+
ariaLabel,
|
|
37
46
|
onResize,
|
|
38
47
|
onResizeStart,
|
|
39
48
|
onResizeEnd,
|
|
@@ -102,9 +111,43 @@
|
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
function handleDoubleClick() {
|
|
105
|
-
// Reset to default
|
|
106
|
-
|
|
107
|
-
|
|
114
|
+
// Reset to the configured default (or the midpoint of min/max).
|
|
115
|
+
onResize?.(defaultSize ?? Math.round((min + max) / 2));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Keyboard operation for the slider role: arrows adjust the size, Home/End jump
|
|
119
|
+
// to min/max. Without this the role="slider" + aria-value* attributes promise
|
|
120
|
+
// an adjustable control that a keyboard/SR user cannot actually move.
|
|
121
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
122
|
+
const amount = e.shiftKey ? step * 5 : step;
|
|
123
|
+
let next: number;
|
|
124
|
+
switch (e.key) {
|
|
125
|
+
case 'ArrowRight':
|
|
126
|
+
case 'ArrowUp':
|
|
127
|
+
next = size + amount;
|
|
128
|
+
break;
|
|
129
|
+
case 'ArrowLeft':
|
|
130
|
+
case 'ArrowDown':
|
|
131
|
+
next = size - amount;
|
|
132
|
+
break;
|
|
133
|
+
case 'Home':
|
|
134
|
+
next = min;
|
|
135
|
+
break;
|
|
136
|
+
case 'End':
|
|
137
|
+
next = max;
|
|
138
|
+
break;
|
|
139
|
+
case 'Enter':
|
|
140
|
+
case ' ':
|
|
141
|
+
// Keyboard equivalent of double-click-to-reset: snap to the
|
|
142
|
+
// configured default (or the midpoint of min/max).
|
|
143
|
+
next = defaultSize ?? Math.round((min + max) / 2);
|
|
144
|
+
break;
|
|
145
|
+
default:
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
const clamped = Math.max(min, Math.min(max, next));
|
|
150
|
+
if (clamped !== size) onResize?.(clamped);
|
|
108
151
|
}
|
|
109
152
|
|
|
110
153
|
// Cleanup on destroy
|
|
@@ -123,7 +166,9 @@
|
|
|
123
166
|
class:resize-handle--dragging={isDragging}
|
|
124
167
|
onmousedown={handleMouseDown}
|
|
125
168
|
ondblclick={handleDoubleClick}
|
|
169
|
+
onkeydown={handleKeyDown}
|
|
126
170
|
role="slider"
|
|
171
|
+
aria-label={ariaLabel}
|
|
127
172
|
aria-orientation={direction}
|
|
128
173
|
aria-valuenow={size}
|
|
129
174
|
aria-valuemin={min}
|
|
@@ -131,6 +176,11 @@
|
|
|
131
176
|
tabindex={0}
|
|
132
177
|
>
|
|
133
178
|
<div class="resize-handle__indicator"></div>
|
|
179
|
+
<div class="resize-handle__grip" aria-hidden="true">
|
|
180
|
+
<span></span>
|
|
181
|
+
<span></span>
|
|
182
|
+
<span></span>
|
|
183
|
+
</div>
|
|
134
184
|
</div>
|
|
135
185
|
|
|
136
186
|
<style>
|
|
@@ -163,7 +213,9 @@
|
|
|
163
213
|
|
|
164
214
|
.resize-handle__indicator {
|
|
165
215
|
position: absolute;
|
|
166
|
-
|
|
216
|
+
/* Brighter resting hairline so the handle reads as a grab affordance
|
|
217
|
+
without leaning on the page's "Drag the edge" labels. */
|
|
218
|
+
background: color-mix(in srgb, var(--ide-border) 55%, var(--ide-text-secondary));
|
|
167
219
|
transition: background-color 0.15s ease;
|
|
168
220
|
}
|
|
169
221
|
|
|
@@ -184,17 +236,49 @@
|
|
|
184
236
|
}
|
|
185
237
|
|
|
186
238
|
.resize-handle:hover .resize-handle__indicator,
|
|
239
|
+
.resize-handle:focus-visible .resize-handle__indicator,
|
|
187
240
|
.resize-handle--dragging .resize-handle__indicator {
|
|
188
241
|
background: var(--ide-interactive, #4a8db7);
|
|
189
242
|
}
|
|
190
243
|
|
|
244
|
+
/* Centered grip motif (2-3 dots) so the resting handle reads as grabbable. */
|
|
245
|
+
.resize-handle__grip {
|
|
246
|
+
position: absolute;
|
|
247
|
+
top: 50%;
|
|
248
|
+
left: 50%;
|
|
249
|
+
display: flex;
|
|
250
|
+
gap: 3px;
|
|
251
|
+
transform: translate(-50%, -50%);
|
|
252
|
+
pointer-events: none;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.resize-handle__grip span {
|
|
256
|
+
display: block;
|
|
257
|
+
width: 2px;
|
|
258
|
+
height: 2px;
|
|
259
|
+
border-radius: 50%;
|
|
260
|
+
background: color-mix(in srgb, var(--ide-border) 40%, var(--ide-text-secondary));
|
|
261
|
+
transition: background-color 0.15s ease;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* Stack the dots along the drag axis (vertical handle = dots in a column). */
|
|
265
|
+
.resize-handle--vertical .resize-handle__grip {
|
|
266
|
+
flex-direction: column;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.resize-handle:hover .resize-handle__grip span,
|
|
270
|
+
.resize-handle:focus-visible .resize-handle__grip span,
|
|
271
|
+
.resize-handle--dragging .resize-handle__grip span {
|
|
272
|
+
background: var(--ide-interactive, #4a8db7);
|
|
273
|
+
}
|
|
274
|
+
|
|
191
275
|
/* Focus styles for keyboard accessibility */
|
|
192
276
|
.resize-handle:focus {
|
|
193
277
|
outline: none;
|
|
194
278
|
}
|
|
195
279
|
|
|
196
280
|
.resize-handle:focus-visible {
|
|
197
|
-
outline: 2px solid var(--ide-interactive, #4a8db7);
|
|
198
|
-
outline-offset:
|
|
281
|
+
outline: 2px solid var(--ide-interactive-focus, #4a8db7);
|
|
282
|
+
outline-offset: 2px;
|
|
199
283
|
}
|
|
200
284
|
</style>
|
|
@@ -9,6 +9,12 @@ interface Props {
|
|
|
9
9
|
max?: number;
|
|
10
10
|
/** Current size in pixels */
|
|
11
11
|
size: number;
|
|
12
|
+
/** Step (px) for keyboard arrow adjustment (default 10; Shift = 5x) */
|
|
13
|
+
step?: number;
|
|
14
|
+
/** Size to snap to on double-click (defaults to the midpoint of min/max) */
|
|
15
|
+
defaultSize?: number;
|
|
16
|
+
/** Accessible name for the slider (e.g. "Resize left panel") */
|
|
17
|
+
ariaLabel?: string;
|
|
12
18
|
/** Callback when size changes */
|
|
13
19
|
onResize?: (size: number) => void;
|
|
14
20
|
/** Callback when drag starts */
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
// Stable per-instance counter (SSR-safe — no Math.random/Date) so the bubble's
|
|
3
|
+
// id matches across server render and hydration for aria-describedby.
|
|
4
|
+
let tooltipCounter = 0;
|
|
5
|
+
</script>
|
|
6
|
+
|
|
1
7
|
<script lang="ts">
|
|
2
8
|
import type { Snippet } from 'svelte';
|
|
3
9
|
|
|
@@ -11,6 +17,7 @@
|
|
|
11
17
|
|
|
12
18
|
let { content, position = 'top', delay = 300, class: className = '', children }: Props = $props();
|
|
13
19
|
|
|
20
|
+
const tooltipId = `ide-tooltip-${++tooltipCounter}`;
|
|
14
21
|
let visible = $state(false);
|
|
15
22
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
16
23
|
|
|
@@ -29,17 +36,21 @@
|
|
|
29
36
|
}
|
|
30
37
|
</script>
|
|
31
38
|
|
|
39
|
+
<!-- Presentational hover/focus container: the real trigger is the wrapped child,
|
|
40
|
+
and the tooltip text is exposed via aria-describedby. It is deliberately NOT
|
|
41
|
+
given a role (a role="tooltip" here was the original bug). -->
|
|
42
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
32
43
|
<div
|
|
33
44
|
class="ide-tooltip-wrapper {className}"
|
|
34
45
|
onmouseenter={show}
|
|
35
46
|
onmouseleave={hide}
|
|
36
47
|
onfocus={show}
|
|
37
48
|
onblur={hide}
|
|
38
|
-
|
|
49
|
+
aria-describedby={visible && content ? tooltipId : undefined}
|
|
39
50
|
>
|
|
40
51
|
{@render children()}
|
|
41
52
|
{#if visible && content}
|
|
42
|
-
<div class="ide-tooltip ide-tooltip--{position}" role="tooltip">
|
|
53
|
+
<div id={tooltipId} class="ide-tooltip ide-tooltip--{position}" role="tooltip">
|
|
43
54
|
{content}
|
|
44
55
|
</div>
|
|
45
56
|
{/if}
|
|
@@ -400,7 +400,10 @@
|
|
|
400
400
|
|
|
401
401
|
/* Mode indicator */
|
|
402
402
|
.echo-mode-indicator {
|
|
403
|
-
|
|
403
|
+
/* Anchor to this layer's own editor (the .echo-cursor-layer fills it via
|
|
404
|
+
absolute inset:0), not the viewport — a fixed pill would float free of
|
|
405
|
+
the editor it reports on wherever the layer is embedded. */
|
|
406
|
+
position: absolute;
|
|
404
407
|
bottom: 16px;
|
|
405
408
|
right: 16px;
|
|
406
409
|
display: flex;
|
|
@@ -228,23 +228,31 @@
|
|
|
228
228
|
display: inline-block;
|
|
229
229
|
font-family: inherit;
|
|
230
230
|
font-size: inherit;
|
|
231
|
+
font-weight: 700;
|
|
231
232
|
color: var(--ghost-color);
|
|
232
|
-
opacity
|
|
233
|
+
/* Resting opacity ships legible (was 0.5) so the inferred brace reads
|
|
234
|
+
against body text without a page-level :global override. */
|
|
235
|
+
opacity: 0.85;
|
|
236
|
+
/* Faint highlight: a soft glow in the confidence color keeps the AA-ish
|
|
237
|
+
delta from surrounding code without recoloring the brace. */
|
|
238
|
+
text-shadow: 0 0 6px color-mix(in srgb, var(--ghost-color) 60%, transparent);
|
|
239
|
+
border-radius: 2px;
|
|
240
|
+
background: color-mix(in srgb, var(--ghost-color) 14%, transparent);
|
|
233
241
|
animation: ghost-pulse 2s ease-in-out infinite;
|
|
234
242
|
}
|
|
235
243
|
|
|
236
244
|
@keyframes ghost-pulse {
|
|
237
245
|
0%,
|
|
238
246
|
100% {
|
|
239
|
-
opacity: 0.
|
|
247
|
+
opacity: 0.85;
|
|
240
248
|
}
|
|
241
249
|
50% {
|
|
242
|
-
opacity: 0.
|
|
250
|
+
opacity: 0.65;
|
|
243
251
|
}
|
|
244
252
|
}
|
|
245
253
|
|
|
246
254
|
.ghost-bracket:hover .ghost-bracket__char {
|
|
247
|
-
opacity:
|
|
255
|
+
opacity: 1;
|
|
248
256
|
animation: none;
|
|
249
257
|
}
|
|
250
258
|
|
|
@@ -350,9 +358,11 @@
|
|
|
350
358
|
position: absolute;
|
|
351
359
|
bottom: 2px;
|
|
352
360
|
left: 0;
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
361
|
+
/* Span the brace and a short run of the offending line so the marker
|
|
362
|
+
reads as anchored to its cause, not a detached tick above the code. */
|
|
363
|
+
width: 6ch;
|
|
364
|
+
height: 0;
|
|
365
|
+
border-bottom: 2px dotted var(--mismatch-color);
|
|
356
366
|
border-radius: 1px;
|
|
357
367
|
}
|
|
358
368
|
|
|
@@ -174,7 +174,8 @@
|
|
|
174
174
|
onclick={() => handleClick(info)}
|
|
175
175
|
onkeydown={(e) => e.key === 'Enter' && handleClick(info)}
|
|
176
176
|
role="button"
|
|
177
|
-
tabindex={-1}
|
|
177
|
+
tabindex={onCommitClick ? 0 : -1}
|
|
178
|
+
aria-label={onCommitClick ? `View commit ${info.commitSha} by ${info.author}` : undefined}
|
|
178
179
|
>
|
|
179
180
|
<!-- Color indicator bar -->
|
|
180
181
|
<div class="git-blame-bar"></div>
|
|
@@ -240,7 +241,9 @@
|
|
|
240
241
|
<div class="tooltip-uncommitted-badge">Uncommitted changes</div>
|
|
241
242
|
{/if}
|
|
242
243
|
|
|
243
|
-
|
|
244
|
+
{#if onCommitClick}
|
|
245
|
+
<div class="tooltip-hint">Click to view full commit</div>
|
|
246
|
+
{/if}
|
|
244
247
|
</div>
|
|
245
248
|
{/if}
|
|
246
249
|
{/if}
|
|
@@ -295,11 +298,15 @@
|
|
|
295
298
|
cursor: pointer;
|
|
296
299
|
transition: background 0.1s ease;
|
|
297
300
|
overflow: hidden;
|
|
301
|
+
/* Recency tint ramp: in 'age' colorMode --blame-color runs green (recent)
|
|
302
|
+
-> gray (old), so a faint wash of it across the whole row makes the
|
|
303
|
+
green->gray gradient actually visible instead of a flat dark fill. */
|
|
304
|
+
background: color-mix(in srgb, var(--blame-color, #6b7280) 22%, transparent);
|
|
298
305
|
}
|
|
299
306
|
|
|
300
307
|
.git-blame-row:hover,
|
|
301
308
|
.git-blame-row--hovered {
|
|
302
|
-
background:
|
|
309
|
+
background: color-mix(in srgb, var(--blame-color, #6b7280) 34%, transparent);
|
|
303
310
|
}
|
|
304
311
|
|
|
305
312
|
.git-blame-row--uncommitted {
|