@nocturnium/svelte-ide 1.3.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/README.md +26 -4
- 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/CustomEditor.svelte +34 -0
- package/dist/components/editor/CustomEditor.svelte.d.ts +5 -1
- 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/extract-variable.d.ts +48 -0
- package/dist/components/editor/core/extract-variable.js +457 -0
- package/dist/components/editor/core/index.d.ts +2 -0
- package/dist/components/editor/core/index.js +2 -0
- package/dist/components/editor/core/organize-imports.d.ts +38 -0
- package/dist/components/editor/core/organize-imports.js +249 -0
- 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
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { resolveLanguage } from '../tokenizer';
|
|
2
|
+
const SUPPORTED_LANGUAGES = new Set(['javascript', 'typescript', 'jsx', 'tsx']);
|
|
3
|
+
const IDENT = '[A-Za-z_$][\\w$]*';
|
|
4
|
+
/**
|
|
5
|
+
* Plan an "organize imports" rewrite of the leading import block: sort the
|
|
6
|
+
* statements by module path, sort the named specifiers within each, and drop
|
|
7
|
+
* exact-duplicate statements. PURE — no editor mutation.
|
|
8
|
+
*
|
|
9
|
+
* Safe-or-refuse: this build only touches a contiguous run of single-line
|
|
10
|
+
* `import … from '…'` statements at the top of the file. It refuses (rather than
|
|
11
|
+
* risk changing behavior) on side-effect imports, multi-line imports, comments
|
|
12
|
+
* interleaved in the block, or any statement it cannot fully parse.
|
|
13
|
+
*/
|
|
14
|
+
export function planOrganizeImports(input) {
|
|
15
|
+
try {
|
|
16
|
+
return planOrganizeImportsUnsafe(input);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return { ok: false, reason: 'Could not safely parse the imports; leaving them untouched.' };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function planOrganizeImportsUnsafe(input) {
|
|
23
|
+
const language = resolveLanguage(input.language);
|
|
24
|
+
if (!SUPPORTED_LANGUAGES.has(language)) {
|
|
25
|
+
return { ok: false, reason: 'Organize imports supports JavaScript/TypeScript only.' };
|
|
26
|
+
}
|
|
27
|
+
const lines = input.lines.map((line) => line.text);
|
|
28
|
+
// Find the first import statement; everything above it (license headers,
|
|
29
|
+
// blank lines, leading comments) is left untouched.
|
|
30
|
+
const firstImport = lines.findIndex((text) => /^\s*import\b/.test(text));
|
|
31
|
+
if (firstImport === -1) {
|
|
32
|
+
return { ok: false, reason: 'No imports to organize.' };
|
|
33
|
+
}
|
|
34
|
+
// Walk the contiguous leading run: import lines and blank lines. A comment
|
|
35
|
+
// ENDS the run unless another import follows it — only a comment BETWEEN two
|
|
36
|
+
// imports is the unsupported "interleaved" case. A trailing body comment (a
|
|
37
|
+
// section header, the first JSDoc) is just the end of the block and must not
|
|
38
|
+
// make organize refuse.
|
|
39
|
+
let lastImport = firstImport;
|
|
40
|
+
const importLineIndices = [];
|
|
41
|
+
let sawCommentInRun = false;
|
|
42
|
+
let commentBetweenImports = false;
|
|
43
|
+
for (let i = firstImport; i < lines.length; i++) {
|
|
44
|
+
const text = lines[i];
|
|
45
|
+
if (text.trim().length === 0)
|
|
46
|
+
continue; // blank lines tolerated inside the run
|
|
47
|
+
if (isCommentLine(text)) {
|
|
48
|
+
sawCommentInRun = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (!/^\s*import\b/.test(text))
|
|
52
|
+
break; // a code line ends the run
|
|
53
|
+
if (sawCommentInRun)
|
|
54
|
+
commentBetweenImports = true; // a comment preceded THIS import
|
|
55
|
+
importLineIndices.push(i);
|
|
56
|
+
lastImport = i;
|
|
57
|
+
}
|
|
58
|
+
if (commentBetweenImports) {
|
|
59
|
+
return { ok: false, reason: 'Comments between imports are not supported yet.' };
|
|
60
|
+
}
|
|
61
|
+
const indent = lines[firstImport].match(/^[\t ]*/)?.[0] ?? '';
|
|
62
|
+
const parsed = [];
|
|
63
|
+
for (const i of importLineIndices) {
|
|
64
|
+
const text = lines[i];
|
|
65
|
+
if (!hasBalancedBraces(text)) {
|
|
66
|
+
return { ok: false, reason: 'Multi-line imports are not supported yet.' };
|
|
67
|
+
}
|
|
68
|
+
if (isSideEffectImport(text)) {
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
reason: 'Side-effect imports present; not reordering to preserve evaluation order.'
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const imp = parseImport(text);
|
|
75
|
+
if (!imp) {
|
|
76
|
+
return { ok: false, reason: 'Could not safely parse the imports; leaving them untouched.' };
|
|
77
|
+
}
|
|
78
|
+
parsed.push(imp);
|
|
79
|
+
}
|
|
80
|
+
const emitted = parsed.map((imp) => `${indent}${emitImport(imp)}`);
|
|
81
|
+
const deduped = dedupe([...emitted.keys()]
|
|
82
|
+
.sort((a, b) => compareImport(parsed[a], parsed[b], emitted[a], emitted[b]))
|
|
83
|
+
.map((index) => emitted[index]));
|
|
84
|
+
const newText = deduped.join('\n');
|
|
85
|
+
const originalText = lines.slice(firstImport, lastImport + 1).join('\n');
|
|
86
|
+
if (newText === originalText) {
|
|
87
|
+
return { ok: false, reason: 'Imports are already organized.' };
|
|
88
|
+
}
|
|
89
|
+
return { ok: true, startLine: firstImport, endLine: lastImport, newText };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Organize the editor's leading import block as a SINGLE undo step. On refusal
|
|
93
|
+
* the editor is untouched and the reason is returned.
|
|
94
|
+
*/
|
|
95
|
+
export function organizeImportsAt(editor) {
|
|
96
|
+
const plan = planOrganizeImports({
|
|
97
|
+
lines: editor.lines.map((line) => ({ text: line.text })),
|
|
98
|
+
language: editor.language
|
|
99
|
+
});
|
|
100
|
+
if (!plan.ok)
|
|
101
|
+
return plan;
|
|
102
|
+
const endCol = editor.getLine(plan.endLine)?.text.length ?? 0;
|
|
103
|
+
editor.transact((tx) => {
|
|
104
|
+
tx.delete({ line: plan.startLine, column: 0 }, { line: plan.endLine, column: endCol });
|
|
105
|
+
tx.insert({ line: plan.startLine, column: 0 }, plan.newText);
|
|
106
|
+
});
|
|
107
|
+
const lastLineLength = plan.newText.split('\n').at(-1)?.length ?? 0;
|
|
108
|
+
const lastLine = plan.startLine + plan.newText.split('\n').length - 1;
|
|
109
|
+
editor.setCursor({ line: lastLine, column: lastLineLength });
|
|
110
|
+
return { ok: true };
|
|
111
|
+
}
|
|
112
|
+
function isCommentLine(text) {
|
|
113
|
+
const t = text.trim();
|
|
114
|
+
return t.startsWith('//') || t.startsWith('/*') || t.startsWith('*');
|
|
115
|
+
}
|
|
116
|
+
function hasBalancedBraces(text) {
|
|
117
|
+
let brace = 0;
|
|
118
|
+
for (const ch of text) {
|
|
119
|
+
if (ch === '{')
|
|
120
|
+
brace++;
|
|
121
|
+
else if (ch === '}')
|
|
122
|
+
brace--;
|
|
123
|
+
if (brace < 0)
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
return brace === 0;
|
|
127
|
+
}
|
|
128
|
+
function isSideEffectImport(text) {
|
|
129
|
+
return new RegExp(`^\\s*import\\s*(['"])[^'"]+\\1\\s*;?\\s*$`).test(text);
|
|
130
|
+
}
|
|
131
|
+
function parseImport(text) {
|
|
132
|
+
const trimmed = text.trim().replace(/;\s*$/, '');
|
|
133
|
+
const sourceMatch = trimmed.match(/\bfrom\s*(['"])([^'"]+)\1$/);
|
|
134
|
+
if (!sourceMatch)
|
|
135
|
+
return null;
|
|
136
|
+
const quote = sourceMatch[1];
|
|
137
|
+
const source = sourceMatch[2];
|
|
138
|
+
let clause = trimmed.slice('import'.length, trimmed.length - sourceMatch[0].length).trim();
|
|
139
|
+
if (clause.length === 0)
|
|
140
|
+
return null; // `import from 'x'` is invalid
|
|
141
|
+
// `import type { … }` / `import type Foo from …` / `import type * as ns` is
|
|
142
|
+
// type-only. But `import type from 'x'` and `import type, { a }` are DEFAULT
|
|
143
|
+
// imports named `type`, so only treat it as type-only when `type` is followed
|
|
144
|
+
// by a real clause start (`{`, `*`, or an identifier) — NOT a comma.
|
|
145
|
+
let typeOnly = false;
|
|
146
|
+
if (/^type\s+[{*A-Za-z_$]/.test(clause)) {
|
|
147
|
+
typeOnly = true;
|
|
148
|
+
clause = clause.replace(/^type\s+/, '').trim();
|
|
149
|
+
}
|
|
150
|
+
let named;
|
|
151
|
+
const braceMatch = clause.match(/\{([^}]*)\}/);
|
|
152
|
+
if (braceMatch) {
|
|
153
|
+
const parsedNamed = parseNamed(braceMatch[1]);
|
|
154
|
+
if (!parsedNamed)
|
|
155
|
+
return null;
|
|
156
|
+
named = parsedNamed;
|
|
157
|
+
const at = braceMatch.index ?? 0;
|
|
158
|
+
clause = (clause.slice(0, at) + clause.slice(at + braceMatch[0].length)).trim();
|
|
159
|
+
clause = clause.replace(/^,|,$/g, '').trim();
|
|
160
|
+
}
|
|
161
|
+
let defaultImport;
|
|
162
|
+
let namespace;
|
|
163
|
+
const remainders = clause
|
|
164
|
+
.split(',')
|
|
165
|
+
.map((part) => part.trim())
|
|
166
|
+
.filter(Boolean);
|
|
167
|
+
for (const part of remainders) {
|
|
168
|
+
const nsMatch = part.match(new RegExp(`^\\*\\s+as\\s+(${IDENT})$`));
|
|
169
|
+
if (nsMatch) {
|
|
170
|
+
if (namespace)
|
|
171
|
+
return null; // two namespace clauses is invalid
|
|
172
|
+
namespace = nsMatch[1];
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (new RegExp(`^${IDENT}$`).test(part)) {
|
|
176
|
+
if (defaultImport)
|
|
177
|
+
return null; // `import A, B from …` is invalid JS
|
|
178
|
+
defaultImport = part;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
return null; // unrecognized clause fragment
|
|
182
|
+
}
|
|
183
|
+
if (!defaultImport && !namespace && !named)
|
|
184
|
+
return null;
|
|
185
|
+
// A namespace import cannot be combined with a named clause (`* as ns, { a }`
|
|
186
|
+
// and `d, * as ns, { a }` are SyntaxErrors). Refuse rather than re-emit them.
|
|
187
|
+
if (namespace && named)
|
|
188
|
+
return null;
|
|
189
|
+
return { source, quote, typeOnly, defaultImport, namespace, named };
|
|
190
|
+
}
|
|
191
|
+
function parseNamed(inner) {
|
|
192
|
+
const specs = [];
|
|
193
|
+
for (const raw of inner.split(',')) {
|
|
194
|
+
const spec = raw.trim();
|
|
195
|
+
if (spec.length === 0)
|
|
196
|
+
continue;
|
|
197
|
+
let body = spec;
|
|
198
|
+
let typeOnly = false;
|
|
199
|
+
const typeMatch = body.match(new RegExp(`^type\\s+(.*)$`));
|
|
200
|
+
if (typeMatch) {
|
|
201
|
+
typeOnly = true;
|
|
202
|
+
body = typeMatch[1].trim();
|
|
203
|
+
}
|
|
204
|
+
const aliasMatch = body.match(new RegExp(`^(${IDENT})\\s+as\\s+(${IDENT})$`));
|
|
205
|
+
if (aliasMatch) {
|
|
206
|
+
specs.push({ name: aliasMatch[1], alias: aliasMatch[2], typeOnly });
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (new RegExp(`^${IDENT}$`).test(body)) {
|
|
210
|
+
specs.push({ name: body, typeOnly });
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
return specs;
|
|
216
|
+
}
|
|
217
|
+
function emitImport(imp) {
|
|
218
|
+
const parts = [];
|
|
219
|
+
if (imp.defaultImport)
|
|
220
|
+
parts.push(imp.defaultImport);
|
|
221
|
+
if (imp.namespace)
|
|
222
|
+
parts.push(`* as ${imp.namespace}`);
|
|
223
|
+
if (imp.named) {
|
|
224
|
+
const specs = [...imp.named]
|
|
225
|
+
.sort((a, b) => compareName(a.name, b.name))
|
|
226
|
+
.map((n) => `${n.typeOnly ? 'type ' : ''}${n.name}${n.alias ? ` as ${n.alias}` : ''}`);
|
|
227
|
+
// An import with ONLY an empty `{}` is preserved as `{}` (valid, rare).
|
|
228
|
+
parts.push(`{ ${specs.join(', ')} }`);
|
|
229
|
+
}
|
|
230
|
+
const clause = parts.join(', ');
|
|
231
|
+
return `import ${imp.typeOnly ? 'type ' : ''}${clause} from ${imp.quote}${imp.source}${imp.quote};`;
|
|
232
|
+
}
|
|
233
|
+
function compareName(a, b) {
|
|
234
|
+
return a.toLowerCase().localeCompare(b.toLowerCase()) || a.localeCompare(b);
|
|
235
|
+
}
|
|
236
|
+
function compareImport(a, b, aLine, bLine) {
|
|
237
|
+
return compareName(a.source, b.source) || aLine.localeCompare(bLine);
|
|
238
|
+
}
|
|
239
|
+
function dedupe(sortedLines) {
|
|
240
|
+
const seen = new Set();
|
|
241
|
+
const out = [];
|
|
242
|
+
for (const line of sortedLines) {
|
|
243
|
+
if (seen.has(line))
|
|
244
|
+
continue;
|
|
245
|
+
seen.add(line);
|
|
246
|
+
out.push(line);
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
@@ -18,7 +18,7 @@ const JS_SNIPPETS = [
|
|
|
18
18
|
prefix: 'log',
|
|
19
19
|
body: 'console.log($1);$0',
|
|
20
20
|
description: 'Log to console',
|
|
21
|
-
languages: ['javascript', 'typescript'
|
|
21
|
+
languages: ['javascript', 'typescript'],
|
|
22
22
|
category: 'Debug',
|
|
23
23
|
isUserDefined: false,
|
|
24
24
|
usageCount: 0,
|
|
@@ -42,7 +42,7 @@ const JS_SNIPPETS = [
|
|
|
42
42
|
prefix: 'af',
|
|
43
43
|
body: 'const ${1:name} = (${2:params}) => {\n\t$0\n};',
|
|
44
44
|
description: 'Arrow function',
|
|
45
|
-
languages: ['javascript', 'typescript'
|
|
45
|
+
languages: ['javascript', 'typescript'],
|
|
46
46
|
category: 'Functions',
|
|
47
47
|
isUserDefined: false,
|
|
48
48
|
usageCount: 0,
|
|
@@ -126,7 +126,7 @@ const JS_SNIPPETS = [
|
|
|
126
126
|
prefix: 'imp',
|
|
127
127
|
body: "import { $2 } from '$1';$0",
|
|
128
128
|
description: 'Named import',
|
|
129
|
-
languages: ['javascript', 'typescript'
|
|
129
|
+
languages: ['javascript', 'typescript'],
|
|
130
130
|
category: 'Modules',
|
|
131
131
|
isUserDefined: false,
|
|
132
132
|
usageCount: 0,
|
|
@@ -68,7 +68,9 @@
|
|
|
68
68
|
{#if installed}
|
|
69
69
|
<Button variant="danger" size="xs" onclick={onUninstall}>Uninstall</Button>
|
|
70
70
|
{:else if plugin.status === 'deployed'}
|
|
71
|
-
<Button variant="primary" size="xs" onclick={onInstall}>
|
|
71
|
+
<Button variant="primary" size="xs" class="plugin-card__install" onclick={onInstall}>
|
|
72
|
+
Install
|
|
73
|
+
</Button>
|
|
72
74
|
{/if}
|
|
73
75
|
</div>
|
|
74
76
|
</div>
|
|
@@ -141,4 +143,22 @@
|
|
|
141
143
|
display: flex;
|
|
142
144
|
align-items: flex-start;
|
|
143
145
|
}
|
|
146
|
+
|
|
147
|
+
/*
|
|
148
|
+
* Install CTA: keep the embedded panel in ONE accent story (wave-blue),
|
|
149
|
+
* instead of the ember-orange `primary` fill competing with the surrounding
|
|
150
|
+
* blue chrome. White-on-blue uses --ide-interactive-strong (deep ocean) for
|
|
151
|
+
* AA-safe contrast (~5.3:1), not the lighter wave (~3.4:1, fails AA). The
|
|
152
|
+
* accent stays BLUE per the owner brand decision.
|
|
153
|
+
*/
|
|
154
|
+
.plugin-card__actions :global(.ide-button.plugin-card__install) {
|
|
155
|
+
background: var(--ide-interactive-strong);
|
|
156
|
+
color: #fff;
|
|
157
|
+
border-color: var(--ide-interactive-strong);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.plugin-card__actions :global(.ide-button.plugin-card__install:hover:not(:disabled)) {
|
|
161
|
+
background: color-mix(in srgb, var(--ide-interactive-strong) 88%, white);
|
|
162
|
+
border-color: color-mix(in srgb, var(--ide-interactive-strong) 88%, white);
|
|
163
|
+
}
|
|
144
164
|
</style>
|
|
@@ -54,7 +54,13 @@
|
|
|
54
54
|
<Badge variant="success" size="sm" dot>Live</Badge>
|
|
55
55
|
{/if}
|
|
56
56
|
</div>
|
|
57
|
-
<Button
|
|
57
|
+
<Button
|
|
58
|
+
variant="ghost"
|
|
59
|
+
size="xs"
|
|
60
|
+
title="Demo only — the create-proposal flow is illustrative on this static host"
|
|
61
|
+
aria-label="New plugin proposal (demo only)"
|
|
62
|
+
onclick={() => (showCreateForm = true)}
|
|
63
|
+
>
|
|
58
64
|
{#snippet icon()}<Icon name="plus" size={14} />{/snippet}
|
|
59
65
|
New
|
|
60
66
|
</Button>
|
|
@@ -110,7 +116,9 @@
|
|
|
110
116
|
{:else if activeTab === 'installed'}
|
|
111
117
|
{#if installedPlugins.length === 0}
|
|
112
118
|
<div class="plugin-panel__empty">
|
|
113
|
-
|
|
119
|
+
<!-- Static container glyph, not the radial `plugin` spoke (which mirrors
|
|
120
|
+
the `loading` glyph and reads as a spinner over this empty copy). -->
|
|
121
|
+
<Icon name="folder" size={32} />
|
|
114
122
|
<p>No plugins installed</p>
|
|
115
123
|
<Button variant="secondary" size="sm" onclick={() => (activeTab = 'available')}>
|
|
116
124
|
Browse Available
|
|
@@ -162,7 +170,13 @@
|
|
|
162
170
|
<div class="plugin-panel__empty">
|
|
163
171
|
<Icon name="file" size={32} />
|
|
164
172
|
<p>No proposals yet</p>
|
|
165
|
-
<Button
|
|
173
|
+
<Button
|
|
174
|
+
variant="primary"
|
|
175
|
+
size="sm"
|
|
176
|
+
title="Demo only — the create-proposal flow is illustrative on this static host"
|
|
177
|
+
aria-label="Create plugin proposal (demo only)"
|
|
178
|
+
onclick={() => (showCreateForm = true)}
|
|
179
|
+
>
|
|
166
180
|
Create Plugin
|
|
167
181
|
</Button>
|
|
168
182
|
</div>
|
package/dist/styles/theme.css
CHANGED
|
@@ -64,12 +64,19 @@
|
|
|
64
64
|
--ide-interactive-focus: var(--color-nocturnium-aurora-blue);
|
|
65
65
|
--ide-interactive-muted: color-mix(in srgb, var(--ide-interactive) 70%, transparent);
|
|
66
66
|
--ide-interactive-rgb: 74, 141, 183;
|
|
67
|
+
/* Deeper blue for FILLED active/selected states paired with white text:
|
|
68
|
+
white on --ide-interactive (#4a8db7) is only ~3.4:1, below WCAG AA; white on
|
|
69
|
+
this ocean blue (#2d5a7b) is ~5.3:1. Use for active pills/toggles/tabs. */
|
|
70
|
+
--ide-interactive-strong: var(--color-nocturnium-ocean);
|
|
67
71
|
|
|
68
72
|
/* Primary action — the signature warm ember accent, shared as a single
|
|
69
73
|
source of truth by primary buttons and badges so "primary" reads the
|
|
70
74
|
same everywhere. */
|
|
71
75
|
--ide-primary: var(--ide-interactive-active);
|
|
72
|
-
|
|
76
|
+
/* Primary hover is a controlled lift of the SAME ember (one deliberate primary),
|
|
77
|
+
not a jump to the separate flame hue — keeps the action color cohesive and
|
|
78
|
+
keeps dark-ink (--ide-text-inverse) label contrast comfortably above AA. */
|
|
79
|
+
--ide-primary-hover: color-mix(in srgb, var(--ide-primary) 85%, white);
|
|
73
80
|
|
|
74
81
|
/* IDE Accent Colors */
|
|
75
82
|
--ide-accent: var(--color-nocturnium-wave);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocturnium/svelte-ide",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
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",
|