@semantic-components/code 0.63.0 → 0.64.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/esm2022/index.js +2 -0
- package/esm2022/index.js.map +1 -0
- package/esm2022/lib/components/code-editor/code-editor-content.js +435 -0
- package/esm2022/lib/components/code-editor/code-editor-content.js.map +1 -0
- package/esm2022/lib/components/code-editor/code-editor-copy-button.js +90 -0
- package/esm2022/lib/components/code-editor/code-editor-copy-button.js.map +1 -0
- package/esm2022/lib/components/code-editor/code-editor-footer.js +27 -0
- package/esm2022/lib/components/code-editor/code-editor-footer.js.map +1 -0
- package/esm2022/lib/components/code-editor/code-editor-header.js +27 -0
- package/esm2022/lib/components/code-editor/code-editor-header.js.map +1 -0
- package/esm2022/lib/components/code-editor/code-editor-label.js +27 -0
- package/esm2022/lib/components/code-editor/code-editor-label.js.map +1 -0
- package/esm2022/lib/components/code-editor/code-editor.js +27 -0
- package/esm2022/lib/components/code-editor/code-editor.js.map +1 -0
- package/esm2022/lib/components/code-editor/index.js +7 -0
- package/esm2022/lib/components/code-editor/index.js.map +1 -0
- package/esm2022/lib/components/code-viewer/code-viewer-content.js +70 -0
- package/esm2022/lib/components/code-viewer/code-viewer-content.js.map +1 -0
- package/esm2022/lib/components/code-viewer/code-viewer-header.js +27 -0
- package/esm2022/lib/components/code-viewer/code-viewer-header.js.map +1 -0
- package/esm2022/lib/components/code-viewer/code-viewer-label.js +27 -0
- package/esm2022/lib/components/code-viewer/code-viewer-label.js.map +1 -0
- package/esm2022/lib/components/code-viewer/code-viewer.js +27 -0
- package/esm2022/lib/components/code-viewer/code-viewer.js.map +1 -0
- package/esm2022/lib/components/code-viewer/index.js +5 -0
- package/esm2022/lib/components/code-viewer/index.js.map +1 -0
- package/esm2022/lib/components/index.js +3 -0
- package/esm2022/lib/components/index.js.map +1 -0
- package/esm2022/semantic-components-code.js +5 -0
- package/esm2022/semantic-components-code.js.map +1 -0
- package/lib/components/code-editor/code-editor-content.d.ts +57 -0
- package/lib/components/code-editor/code-editor-copy-button.d.ts +11 -0
- package/lib/components/code-editor/code-editor-footer.d.ts +7 -0
- package/lib/components/code-editor/code-editor-header.d.ts +7 -0
- package/lib/components/code-editor/code-editor-label.d.ts +7 -0
- package/lib/components/code-editor/code-editor.d.ts +7 -0
- package/lib/components/code-viewer/code-viewer-content.d.ts +17 -0
- package/lib/components/code-viewer/code-viewer-header.d.ts +7 -0
- package/lib/components/code-viewer/code-viewer-label.d.ts +7 -0
- package/lib/components/code-viewer/code-viewer.d.ts +7 -0
- package/package.json +15 -3
- package/semantic-components-code.d.ts +5 -0
- package/eslint.config.mjs +0 -48
- package/ng-package.json +0 -8
- package/project.json +0 -28
- package/src/lib/components/code-editor/README.md +0 -368
- package/src/lib/components/code-editor/code-editor-content.ts +0 -507
- package/src/lib/components/code-editor/code-editor-copy-button.ts +0 -77
- package/src/lib/components/code-editor/code-editor-footer.ts +0 -31
- package/src/lib/components/code-editor/code-editor-header.ts +0 -31
- package/src/lib/components/code-editor/code-editor-label.ts +0 -28
- package/src/lib/components/code-editor/code-editor.ts +0 -31
- package/src/lib/components/code-viewer/README.md +0 -178
- package/src/lib/components/code-viewer/code-viewer-content.ts +0 -96
- package/src/lib/components/code-viewer/code-viewer-header.ts +0 -31
- package/src/lib/components/code-viewer/code-viewer-label.ts +0 -28
- package/src/lib/components/code-viewer/code-viewer.ts +0 -28
- package/tsconfig.json +0 -28
- package/tsconfig.lib.json +0 -12
- package/tsconfig.lib.prod.json +0 -7
- /package/{src/index.ts → index.d.ts} +0 -0
- /package/{src/lib/components/code-editor/index.ts → lib/components/code-editor/index.d.ts} +0 -0
- /package/{src/lib/components/code-viewer/index.ts → lib/components/code-viewer/index.d.ts} +0 -0
- /package/{src/lib/components/index.ts → lib/components/index.d.ts} +0 -0
|
@@ -1,507 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ChangeDetectionStrategy,
|
|
3
|
-
Component,
|
|
4
|
-
computed,
|
|
5
|
-
effect,
|
|
6
|
-
ElementRef,
|
|
7
|
-
inject,
|
|
8
|
-
input,
|
|
9
|
-
model,
|
|
10
|
-
output,
|
|
11
|
-
signal,
|
|
12
|
-
viewChild,
|
|
13
|
-
ViewEncapsulation,
|
|
14
|
-
} from '@angular/core';
|
|
15
|
-
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
|
16
|
-
import { codeToHtml } from 'shiki';
|
|
17
|
-
import { cn } from '@semantic-components/ui';
|
|
18
|
-
|
|
19
|
-
export type ScCodeEditorLanguage =
|
|
20
|
-
| 'angular-ts'
|
|
21
|
-
| 'typescript'
|
|
22
|
-
| 'javascript'
|
|
23
|
-
| 'html'
|
|
24
|
-
| 'css'
|
|
25
|
-
| 'json'
|
|
26
|
-
| 'python'
|
|
27
|
-
| 'bash'
|
|
28
|
-
| 'shell'
|
|
29
|
-
| 'markdown'
|
|
30
|
-
| 'yaml'
|
|
31
|
-
| 'sql'
|
|
32
|
-
| 'go'
|
|
33
|
-
| 'rust'
|
|
34
|
-
| 'java'
|
|
35
|
-
| 'plaintext';
|
|
36
|
-
|
|
37
|
-
const EXTENSION_MAP: Record<string, ScCodeEditorLanguage> = {
|
|
38
|
-
js: 'javascript',
|
|
39
|
-
mjs: 'javascript',
|
|
40
|
-
cjs: 'javascript',
|
|
41
|
-
jsx: 'javascript',
|
|
42
|
-
ts: 'typescript',
|
|
43
|
-
mts: 'typescript',
|
|
44
|
-
cts: 'typescript',
|
|
45
|
-
tsx: 'typescript',
|
|
46
|
-
html: 'html',
|
|
47
|
-
htm: 'html',
|
|
48
|
-
css: 'css',
|
|
49
|
-
scss: 'css',
|
|
50
|
-
sass: 'css',
|
|
51
|
-
less: 'css',
|
|
52
|
-
json: 'json',
|
|
53
|
-
py: 'python',
|
|
54
|
-
pyw: 'python',
|
|
55
|
-
sh: 'bash',
|
|
56
|
-
bash: 'bash',
|
|
57
|
-
zsh: 'shell',
|
|
58
|
-
sql: 'sql',
|
|
59
|
-
md: 'markdown',
|
|
60
|
-
markdown: 'markdown',
|
|
61
|
-
yaml: 'yaml',
|
|
62
|
-
yml: 'yaml',
|
|
63
|
-
go: 'go',
|
|
64
|
-
rs: 'rust',
|
|
65
|
-
java: 'java',
|
|
66
|
-
txt: 'plaintext',
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
export function detectLanguage(
|
|
70
|
-
code: string,
|
|
71
|
-
filename?: string,
|
|
72
|
-
): ScCodeEditorLanguage {
|
|
73
|
-
if (filename) {
|
|
74
|
-
const ext = filename.split('.').pop()?.toLowerCase();
|
|
75
|
-
if (ext && EXTENSION_MAP[ext]) {
|
|
76
|
-
return EXTENSION_MAP[ext];
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Try to detect from content
|
|
81
|
-
if (
|
|
82
|
-
code.includes('<!DOCTYPE') ||
|
|
83
|
-
code.includes('<html') ||
|
|
84
|
-
/<\w+[^>]*>/.test(code)
|
|
85
|
-
) {
|
|
86
|
-
return 'html';
|
|
87
|
-
}
|
|
88
|
-
if (/^\s*\{[\s\S]*\}\s*$/.test(code) || /^\s*\[[\s\S]*\]\s*$/.test(code)) {
|
|
89
|
-
try {
|
|
90
|
-
JSON.parse(code);
|
|
91
|
-
return 'json';
|
|
92
|
-
} catch {
|
|
93
|
-
// Not JSON
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (/^(import|export|const|let|var|function|class)\s/.test(code)) {
|
|
97
|
-
if (
|
|
98
|
-
/:\s*(string|number|boolean|any|void|never)\b/.test(code) ||
|
|
99
|
-
/interface\s+\w+/.test(code)
|
|
100
|
-
) {
|
|
101
|
-
return 'typescript';
|
|
102
|
-
}
|
|
103
|
-
return 'javascript';
|
|
104
|
-
}
|
|
105
|
-
if (
|
|
106
|
-
/^(def|class|import|from|if __name__)\s/.test(code) ||
|
|
107
|
-
/:\s*$/.test(code.split('\n')[0])
|
|
108
|
-
) {
|
|
109
|
-
return 'python';
|
|
110
|
-
}
|
|
111
|
-
if (/^(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)\s/i.test(code)) {
|
|
112
|
-
return 'sql';
|
|
113
|
-
}
|
|
114
|
-
if (/^#\s/.test(code) || /^\*\*.*\*\*$/.test(code)) {
|
|
115
|
-
return 'markdown';
|
|
116
|
-
}
|
|
117
|
-
if (/^#!/.test(code)) {
|
|
118
|
-
return 'bash';
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return 'plaintext';
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
@Component({
|
|
125
|
-
selector: 'div[sc-code-editor-content]',
|
|
126
|
-
template: `
|
|
127
|
-
<!-- Line numbers -->
|
|
128
|
-
@if (showLineNumbers()) {
|
|
129
|
-
<div
|
|
130
|
-
class="sc-code-editor-content__line-numbers flex-shrink-0 select-none border-r border-border py-3 pl-3 pr-3 text-right"
|
|
131
|
-
[style.min-width.ch]="lineNumberWidth()"
|
|
132
|
-
aria-hidden="true"
|
|
133
|
-
>
|
|
134
|
-
@for (line of lines(); track $index) {
|
|
135
|
-
<div
|
|
136
|
-
class="font-mono text-sm leading-relaxed"
|
|
137
|
-
[class.sc-code-editor-content__line-numbers--active]="
|
|
138
|
-
activeLine() === $index + 1
|
|
139
|
-
"
|
|
140
|
-
>
|
|
141
|
-
{{ $index + 1 }}
|
|
142
|
-
</div>
|
|
143
|
-
}
|
|
144
|
-
</div>
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
<!-- Code area -->
|
|
148
|
-
<div class="relative min-w-0 flex-1">
|
|
149
|
-
<!-- Highlighted code (display layer) -->
|
|
150
|
-
<div
|
|
151
|
-
class="pointer-events-none absolute inset-0 overflow-hidden"
|
|
152
|
-
[class.word-wrap-enabled]="wordWrap()"
|
|
153
|
-
aria-hidden="true"
|
|
154
|
-
>
|
|
155
|
-
@if (highlightedHtml()) {
|
|
156
|
-
<div [innerHTML]="highlightedHtml()"></div>
|
|
157
|
-
} @else {
|
|
158
|
-
<pre
|
|
159
|
-
class="m-0 p-3 font-mono text-sm leading-relaxed"
|
|
160
|
-
><code>{{ displayCode() }}</code></pre>
|
|
161
|
-
}
|
|
162
|
-
</div>
|
|
163
|
-
|
|
164
|
-
<!-- Textarea (input layer) -->
|
|
165
|
-
<textarea
|
|
166
|
-
#textarea
|
|
167
|
-
[value]="value()"
|
|
168
|
-
(input)="onInput($event)"
|
|
169
|
-
(keydown)="onKeydown($event)"
|
|
170
|
-
(scroll)="onScroll($event)"
|
|
171
|
-
(focus)="onFocus()"
|
|
172
|
-
(blur)="onBlur()"
|
|
173
|
-
(click)="updateActiveLine($event)"
|
|
174
|
-
(keyup)="updateActiveLine($event)"
|
|
175
|
-
[disabled]="disabled()"
|
|
176
|
-
[readonly]="readonly()"
|
|
177
|
-
[placeholder]="placeholder()"
|
|
178
|
-
[attr.aria-label]="ariaLabel() || 'Code editor'"
|
|
179
|
-
[attr.aria-describedby]="ariaDescribedby()"
|
|
180
|
-
autocomplete="off"
|
|
181
|
-
autocorrect="off"
|
|
182
|
-
autocapitalize="off"
|
|
183
|
-
spellcheck="false"
|
|
184
|
-
[class]="textareaClass()"
|
|
185
|
-
[style]="textareaStyle()"
|
|
186
|
-
></textarea>
|
|
187
|
-
</div>
|
|
188
|
-
`,
|
|
189
|
-
styles: `
|
|
190
|
-
.sc-code-editor-content__line-numbers {
|
|
191
|
-
color: oklch(from var(--muted-foreground) l c h / 0.5);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
.sc-code-editor-content__line-numbers--active {
|
|
195
|
-
color: var(--foreground);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/* Transparent background for editor overlay effect */
|
|
199
|
-
[sc-code-editor-content] pre.shiki,
|
|
200
|
-
[sc-code-editor-content] pre.shiki span {
|
|
201
|
-
background-color: transparent !important;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/* Word wrap support */
|
|
205
|
-
[sc-code-editor-content] .word-wrap-enabled pre.shiki {
|
|
206
|
-
white-space: pre-wrap;
|
|
207
|
-
word-break: break-all;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/* Caret color */
|
|
211
|
-
[sc-code-editor-content] textarea {
|
|
212
|
-
caret-color: var(--primary);
|
|
213
|
-
}
|
|
214
|
-
`,
|
|
215
|
-
host: {
|
|
216
|
-
'data-slot': 'code-editor-content',
|
|
217
|
-
'[class]': 'wrapperClass()',
|
|
218
|
-
'(click)': 'focusTextarea()',
|
|
219
|
-
},
|
|
220
|
-
encapsulation: ViewEncapsulation.None,
|
|
221
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
222
|
-
})
|
|
223
|
-
export class ScCodeEditorContent {
|
|
224
|
-
// Two-way binding for value
|
|
225
|
-
readonly value = model<string>('');
|
|
226
|
-
|
|
227
|
-
// Inputs
|
|
228
|
-
readonly language = input<ScCodeEditorLanguage>('plaintext');
|
|
229
|
-
readonly filename = input<string>('');
|
|
230
|
-
readonly placeholder = input<string>('');
|
|
231
|
-
readonly disabled = input(false);
|
|
232
|
-
readonly readonly = input(false);
|
|
233
|
-
readonly showLineNumbers = input(true);
|
|
234
|
-
readonly tabSize = input(2);
|
|
235
|
-
readonly insertSpaces = input(true);
|
|
236
|
-
readonly wordWrap = input(false);
|
|
237
|
-
readonly autoDetectLanguage = input(false);
|
|
238
|
-
readonly ariaLabel = input<string>('');
|
|
239
|
-
readonly ariaDescribedby = input<string>('');
|
|
240
|
-
readonly classInput = input<string>('', { alias: 'class' });
|
|
241
|
-
|
|
242
|
-
// Outputs
|
|
243
|
-
readonly valueChange = output<string>();
|
|
244
|
-
readonly languageDetected = output<ScCodeEditorLanguage>();
|
|
245
|
-
readonly cursorChange = output<{ line: number; column: number }>();
|
|
246
|
-
|
|
247
|
-
// Internal state
|
|
248
|
-
readonly activeLine = signal(1);
|
|
249
|
-
readonly activeColumn = signal(1);
|
|
250
|
-
readonly isFocused = signal(false);
|
|
251
|
-
protected readonly highlightedHtml = signal<SafeHtml | null>(null);
|
|
252
|
-
private scrollTop = 0;
|
|
253
|
-
private scrollLeft = 0;
|
|
254
|
-
|
|
255
|
-
private readonly sanitizer = inject(DomSanitizer);
|
|
256
|
-
private readonly textarea =
|
|
257
|
-
viewChild.required<ElementRef<HTMLTextAreaElement>>('textarea');
|
|
258
|
-
|
|
259
|
-
protected readonly lines = computed(() => {
|
|
260
|
-
const code = this.value() || '';
|
|
261
|
-
return code.split('\n');
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
protected readonly lineNumberWidth = computed(() => {
|
|
265
|
-
return Math.max(2, String(this.lines().length).length + 1);
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
protected readonly displayCode = computed(() => {
|
|
269
|
-
const code = this.value() || '';
|
|
270
|
-
return code.endsWith('\n') ? code : code + '\n';
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
protected readonly effectiveLanguage = computed(() => {
|
|
274
|
-
if (this.autoDetectLanguage() && this.value()) {
|
|
275
|
-
return detectLanguage(this.value(), this.filename());
|
|
276
|
-
}
|
|
277
|
-
return this.language();
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
protected readonly wrapperClass = computed(() =>
|
|
281
|
-
cn('relative flex overflow-hidden', this.classInput()),
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
protected readonly textareaClass = computed(() =>
|
|
285
|
-
cn(
|
|
286
|
-
'relative m-0 w-full resize-none p-3',
|
|
287
|
-
'border-none bg-transparent text-transparent caret-current outline-none',
|
|
288
|
-
'font-mono text-sm leading-relaxed',
|
|
289
|
-
this.disabled() && 'cursor-not-allowed',
|
|
290
|
-
),
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
protected readonly textareaStyle = computed(() => ({
|
|
294
|
-
whiteSpace: this.wordWrap() ? 'pre-wrap' : 'pre',
|
|
295
|
-
wordBreak: this.wordWrap() ? 'break-all' : 'normal',
|
|
296
|
-
overflowWrap: this.wordWrap() ? 'break-word' : 'normal',
|
|
297
|
-
}));
|
|
298
|
-
|
|
299
|
-
constructor() {
|
|
300
|
-
// Effect to trigger Shiki highlighting
|
|
301
|
-
effect(() => {
|
|
302
|
-
const code = this.displayCode();
|
|
303
|
-
const lang = this.effectiveLanguage();
|
|
304
|
-
|
|
305
|
-
this.highlight(code, lang);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
// Effect for language detection notification
|
|
309
|
-
effect(() => {
|
|
310
|
-
if (this.autoDetectLanguage() && this.value()) {
|
|
311
|
-
const detected = detectLanguage(this.value(), this.filename());
|
|
312
|
-
this.languageDetected.emit(detected);
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
private async highlight(
|
|
318
|
-
code: string,
|
|
319
|
-
lang: ScCodeEditorLanguage,
|
|
320
|
-
): Promise<void> {
|
|
321
|
-
try {
|
|
322
|
-
const html = await codeToHtml(code, {
|
|
323
|
-
lang,
|
|
324
|
-
themes: {
|
|
325
|
-
light: 'github-light',
|
|
326
|
-
dark: 'github-dark',
|
|
327
|
-
},
|
|
328
|
-
defaultColor: false,
|
|
329
|
-
});
|
|
330
|
-
this.highlightedHtml.set(this.sanitizer.bypassSecurityTrustHtml(html));
|
|
331
|
-
} catch {
|
|
332
|
-
this.highlightedHtml.set(null);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
focusTextarea(): void {
|
|
337
|
-
if (!this.disabled() && !this.readonly()) {
|
|
338
|
-
this.textarea().nativeElement.focus();
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
protected onInput(event: Event): void {
|
|
343
|
-
const target = event.target as HTMLTextAreaElement;
|
|
344
|
-
this.value.set(target.value);
|
|
345
|
-
this.valueChange.emit(target.value);
|
|
346
|
-
this.updateCursorPosition(target);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
protected onKeydown(event: KeyboardEvent): void {
|
|
350
|
-
const target = event.target as HTMLTextAreaElement;
|
|
351
|
-
|
|
352
|
-
// Handle Tab key
|
|
353
|
-
if (event.key === 'Tab') {
|
|
354
|
-
event.preventDefault();
|
|
355
|
-
const start = target.selectionStart;
|
|
356
|
-
const end = target.selectionEnd;
|
|
357
|
-
const value = target.value;
|
|
358
|
-
|
|
359
|
-
const indent = this.insertSpaces() ? ' '.repeat(this.tabSize()) : '\t';
|
|
360
|
-
|
|
361
|
-
if (event.shiftKey) {
|
|
362
|
-
// Outdent
|
|
363
|
-
const lineStart = value.lastIndexOf('\n', start - 1) + 1;
|
|
364
|
-
const lineContent = value.slice(lineStart, start);
|
|
365
|
-
const indentMatch = lineContent.match(/^(\t| {1,})/);
|
|
366
|
-
|
|
367
|
-
if (indentMatch) {
|
|
368
|
-
const removeLength = Math.min(
|
|
369
|
-
indentMatch[1].length,
|
|
370
|
-
this.insertSpaces() ? this.tabSize() : 1,
|
|
371
|
-
);
|
|
372
|
-
const newValue =
|
|
373
|
-
value.slice(0, lineStart) + value.slice(lineStart + removeLength);
|
|
374
|
-
|
|
375
|
-
this.value.set(newValue);
|
|
376
|
-
this.valueChange.emit(newValue);
|
|
377
|
-
|
|
378
|
-
// Restore cursor position
|
|
379
|
-
setTimeout(() => {
|
|
380
|
-
target.selectionStart = target.selectionEnd = start - removeLength;
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
} else {
|
|
384
|
-
// Indent
|
|
385
|
-
const newValue = value.slice(0, start) + indent + value.slice(end);
|
|
386
|
-
this.value.set(newValue);
|
|
387
|
-
this.valueChange.emit(newValue);
|
|
388
|
-
|
|
389
|
-
// Move cursor after indent
|
|
390
|
-
setTimeout(() => {
|
|
391
|
-
target.selectionStart = target.selectionEnd = start + indent.length;
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Handle Enter for auto-indent
|
|
397
|
-
if (event.key === 'Enter' && !event.shiftKey) {
|
|
398
|
-
const start = target.selectionStart;
|
|
399
|
-
const value = target.value;
|
|
400
|
-
|
|
401
|
-
// Find the current line's indentation
|
|
402
|
-
const lineStart = value.lastIndexOf('\n', start - 1) + 1;
|
|
403
|
-
const lineContent = value.slice(lineStart, start);
|
|
404
|
-
const indentMatch = lineContent.match(/^(\s*)/);
|
|
405
|
-
const currentIndent = indentMatch ? indentMatch[1] : '';
|
|
406
|
-
|
|
407
|
-
// Check if we need extra indent (after { or :)
|
|
408
|
-
const charBefore = value[start - 1];
|
|
409
|
-
const extraIndent =
|
|
410
|
-
charBefore === '{' || charBefore === ':' || charBefore === '['
|
|
411
|
-
? this.insertSpaces()
|
|
412
|
-
? ' '.repeat(this.tabSize())
|
|
413
|
-
: '\t'
|
|
414
|
-
: '';
|
|
415
|
-
|
|
416
|
-
event.preventDefault();
|
|
417
|
-
const newValue =
|
|
418
|
-
value.slice(0, start) +
|
|
419
|
-
'\n' +
|
|
420
|
-
currentIndent +
|
|
421
|
-
extraIndent +
|
|
422
|
-
value.slice(start);
|
|
423
|
-
|
|
424
|
-
this.value.set(newValue);
|
|
425
|
-
this.valueChange.emit(newValue);
|
|
426
|
-
|
|
427
|
-
setTimeout(() => {
|
|
428
|
-
const newPos = start + 1 + currentIndent.length + extraIndent.length;
|
|
429
|
-
target.selectionStart = target.selectionEnd = newPos;
|
|
430
|
-
this.updateCursorPosition(target);
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Handle closing brackets
|
|
435
|
-
if (event.key === '}' || event.key === ']' || event.key === ')') {
|
|
436
|
-
const start = target.selectionStart;
|
|
437
|
-
const value = target.value;
|
|
438
|
-
const lineStart = value.lastIndexOf('\n', start - 1) + 1;
|
|
439
|
-
const beforeCursor = value.slice(lineStart, start);
|
|
440
|
-
|
|
441
|
-
// If line is only whitespace, reduce indent
|
|
442
|
-
if (/^\s+$/.test(beforeCursor)) {
|
|
443
|
-
const reduceBy = this.insertSpaces() ? this.tabSize() : 1;
|
|
444
|
-
const newIndent = beforeCursor.slice(
|
|
445
|
-
0,
|
|
446
|
-
Math.max(0, beforeCursor.length - reduceBy),
|
|
447
|
-
);
|
|
448
|
-
|
|
449
|
-
event.preventDefault();
|
|
450
|
-
const newValue =
|
|
451
|
-
value.slice(0, lineStart) +
|
|
452
|
-
newIndent +
|
|
453
|
-
event.key +
|
|
454
|
-
value.slice(start);
|
|
455
|
-
|
|
456
|
-
this.value.set(newValue);
|
|
457
|
-
this.valueChange.emit(newValue);
|
|
458
|
-
|
|
459
|
-
setTimeout(() => {
|
|
460
|
-
target.selectionStart = target.selectionEnd =
|
|
461
|
-
lineStart + newIndent.length + 1;
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
protected onScroll(event: Event): void {
|
|
468
|
-
const target = event.target as HTMLTextAreaElement;
|
|
469
|
-
this.scrollTop = target.scrollTop;
|
|
470
|
-
this.scrollLeft = target.scrollLeft;
|
|
471
|
-
|
|
472
|
-
// Sync scroll with the display layer
|
|
473
|
-
const displayLayer = target.previousElementSibling as HTMLElement;
|
|
474
|
-
if (displayLayer) {
|
|
475
|
-
displayLayer.scrollTop = this.scrollTop;
|
|
476
|
-
displayLayer.scrollLeft = this.scrollLeft;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
protected onFocus(): void {
|
|
481
|
-
this.isFocused.set(true);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
protected onBlur(): void {
|
|
485
|
-
this.isFocused.set(false);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
protected updateActiveLine(event: Event): void {
|
|
489
|
-
const target = event.target as HTMLTextAreaElement;
|
|
490
|
-
this.updateCursorPosition(target);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
private updateCursorPosition(textarea: HTMLTextAreaElement): void {
|
|
494
|
-
const value = textarea.value;
|
|
495
|
-
const pos = textarea.selectionStart;
|
|
496
|
-
|
|
497
|
-
// Calculate line and column
|
|
498
|
-
const textBeforeCursor = value.slice(0, pos);
|
|
499
|
-
const lines = textBeforeCursor.split('\n');
|
|
500
|
-
const line = lines.length;
|
|
501
|
-
const column = lines[lines.length - 1].length + 1;
|
|
502
|
-
|
|
503
|
-
this.activeLine.set(line);
|
|
504
|
-
this.activeColumn.set(column);
|
|
505
|
-
this.cursorChange.emit({ line, column });
|
|
506
|
-
}
|
|
507
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ChangeDetectionStrategy,
|
|
3
|
-
Component,
|
|
4
|
-
computed,
|
|
5
|
-
input,
|
|
6
|
-
signal,
|
|
7
|
-
ViewEncapsulation,
|
|
8
|
-
} from '@angular/core';
|
|
9
|
-
import { cn } from '@semantic-components/ui';
|
|
10
|
-
|
|
11
|
-
@Component({
|
|
12
|
-
selector: 'button[sc-code-editor-copy-button]',
|
|
13
|
-
template: `
|
|
14
|
-
@if (copied()) {
|
|
15
|
-
<svg
|
|
16
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
17
|
-
viewBox="0 0 24 24"
|
|
18
|
-
fill="none"
|
|
19
|
-
stroke="currentColor"
|
|
20
|
-
stroke-width="2"
|
|
21
|
-
class="size-4"
|
|
22
|
-
>
|
|
23
|
-
<polyline points="20 6 9 17 4 12" />
|
|
24
|
-
</svg>
|
|
25
|
-
} @else {
|
|
26
|
-
<svg
|
|
27
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
28
|
-
viewBox="0 0 24 24"
|
|
29
|
-
fill="none"
|
|
30
|
-
stroke="currentColor"
|
|
31
|
-
stroke-width="2"
|
|
32
|
-
class="size-4"
|
|
33
|
-
>
|
|
34
|
-
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
|
|
35
|
-
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
|
36
|
-
</svg>
|
|
37
|
-
}
|
|
38
|
-
`,
|
|
39
|
-
host: {
|
|
40
|
-
'data-slot': 'code-editor-copy-button',
|
|
41
|
-
'[class]': 'class()',
|
|
42
|
-
'[attr.aria-label]': 'ariaLabel()',
|
|
43
|
-
type: 'button',
|
|
44
|
-
'(click)': 'copyCode($event)',
|
|
45
|
-
},
|
|
46
|
-
encapsulation: ViewEncapsulation.None,
|
|
47
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
48
|
-
})
|
|
49
|
-
export class ScCodeEditorCopyButton {
|
|
50
|
-
readonly code = input.required<string>();
|
|
51
|
-
readonly classInput = input<string>('', { alias: 'class' });
|
|
52
|
-
|
|
53
|
-
readonly copied = signal(false);
|
|
54
|
-
|
|
55
|
-
protected readonly class = computed(() =>
|
|
56
|
-
cn(
|
|
57
|
-
'rounded p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground',
|
|
58
|
-
this.classInput(),
|
|
59
|
-
),
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
protected readonly ariaLabel = computed(() =>
|
|
63
|
-
this.copied() ? 'Copied!' : 'Copy code',
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
protected async copyCode(event: Event): Promise<void> {
|
|
67
|
-
event.stopPropagation();
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
await navigator.clipboard.writeText(this.code());
|
|
71
|
-
this.copied.set(true);
|
|
72
|
-
setTimeout(() => this.copied.set(false), 2000);
|
|
73
|
-
} catch (err) {
|
|
74
|
-
console.error('Failed to copy code:', err);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ChangeDetectionStrategy,
|
|
3
|
-
Component,
|
|
4
|
-
computed,
|
|
5
|
-
input,
|
|
6
|
-
ViewEncapsulation,
|
|
7
|
-
} from '@angular/core';
|
|
8
|
-
import { cn } from '@semantic-components/ui';
|
|
9
|
-
|
|
10
|
-
@Component({
|
|
11
|
-
selector: 'div[sc-code-editor-footer]',
|
|
12
|
-
template: `
|
|
13
|
-
<ng-content />
|
|
14
|
-
`,
|
|
15
|
-
host: {
|
|
16
|
-
'data-slot': 'code-editor-footer',
|
|
17
|
-
'[class]': 'class()',
|
|
18
|
-
},
|
|
19
|
-
encapsulation: ViewEncapsulation.None,
|
|
20
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
21
|
-
})
|
|
22
|
-
export class ScCodeEditorFooter {
|
|
23
|
-
readonly classInput = input<string>('', { alias: 'class' });
|
|
24
|
-
|
|
25
|
-
protected readonly class = computed(() =>
|
|
26
|
-
cn(
|
|
27
|
-
'flex items-center justify-between border-t border-border bg-background/50 px-3 py-1.5 text-xs text-muted-foreground',
|
|
28
|
-
this.classInput(),
|
|
29
|
-
),
|
|
30
|
-
);
|
|
31
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ChangeDetectionStrategy,
|
|
3
|
-
Component,
|
|
4
|
-
computed,
|
|
5
|
-
input,
|
|
6
|
-
ViewEncapsulation,
|
|
7
|
-
} from '@angular/core';
|
|
8
|
-
import { cn } from '@semantic-components/ui';
|
|
9
|
-
|
|
10
|
-
@Component({
|
|
11
|
-
selector: 'div[sc-code-editor-header]',
|
|
12
|
-
template: `
|
|
13
|
-
<ng-content />
|
|
14
|
-
`,
|
|
15
|
-
host: {
|
|
16
|
-
'data-slot': 'code-editor-header',
|
|
17
|
-
'[class]': 'class()',
|
|
18
|
-
},
|
|
19
|
-
encapsulation: ViewEncapsulation.None,
|
|
20
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
21
|
-
})
|
|
22
|
-
export class ScCodeEditorHeader {
|
|
23
|
-
readonly classInput = input<string>('', { alias: 'class' });
|
|
24
|
-
|
|
25
|
-
protected readonly class = computed(() =>
|
|
26
|
-
cn(
|
|
27
|
-
'flex items-center justify-between border-b border-border bg-background/50 px-3 py-2',
|
|
28
|
-
this.classInput(),
|
|
29
|
-
),
|
|
30
|
-
);
|
|
31
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ChangeDetectionStrategy,
|
|
3
|
-
Component,
|
|
4
|
-
computed,
|
|
5
|
-
input,
|
|
6
|
-
ViewEncapsulation,
|
|
7
|
-
} from '@angular/core';
|
|
8
|
-
import { cn } from '@semantic-components/ui';
|
|
9
|
-
|
|
10
|
-
@Component({
|
|
11
|
-
selector: 'span[sc-code-editor-label]',
|
|
12
|
-
template: `
|
|
13
|
-
<ng-content />
|
|
14
|
-
`,
|
|
15
|
-
host: {
|
|
16
|
-
'data-slot': 'code-editor-label',
|
|
17
|
-
'[class]': 'class()',
|
|
18
|
-
},
|
|
19
|
-
encapsulation: ViewEncapsulation.None,
|
|
20
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
21
|
-
})
|
|
22
|
-
export class ScCodeEditorLabel {
|
|
23
|
-
readonly classInput = input<string>('', { alias: 'class' });
|
|
24
|
-
|
|
25
|
-
protected readonly class = computed(() =>
|
|
26
|
-
cn('text-xs font-medium text-muted-foreground', this.classInput()),
|
|
27
|
-
);
|
|
28
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ChangeDetectionStrategy,
|
|
3
|
-
Component,
|
|
4
|
-
computed,
|
|
5
|
-
input,
|
|
6
|
-
ViewEncapsulation,
|
|
7
|
-
} from '@angular/core';
|
|
8
|
-
import { cn } from '@semantic-components/ui';
|
|
9
|
-
|
|
10
|
-
@Component({
|
|
11
|
-
selector: 'div[sc-code-editor]',
|
|
12
|
-
template: `
|
|
13
|
-
<ng-content />
|
|
14
|
-
`,
|
|
15
|
-
host: {
|
|
16
|
-
'data-slot': 'code-editor',
|
|
17
|
-
'[class]': 'class()',
|
|
18
|
-
},
|
|
19
|
-
encapsulation: ViewEncapsulation.None,
|
|
20
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
21
|
-
})
|
|
22
|
-
export class ScCodeEditor {
|
|
23
|
-
readonly classInput = input<string>('', { alias: 'class' });
|
|
24
|
-
|
|
25
|
-
protected readonly class = computed(() =>
|
|
26
|
-
cn(
|
|
27
|
-
'overflow-hidden rounded-lg border border-border bg-muted focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2',
|
|
28
|
-
this.classInput(),
|
|
29
|
-
),
|
|
30
|
-
);
|
|
31
|
-
}
|