desktop: components/apps/text-editor — code editor with toolbar, cursor position, dark theme
This commit is contained in:
parent
7496fbeced
commit
283e53d1dc
@ -0,0 +1,23 @@
|
|||||||
|
<div class="text-editor">
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<button class="toolbar-btn" (click)="newFile()" title="New">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14"><path d="M1 1h12v12H1z" fill="none" stroke="currentColor" stroke-width="1.2"/><line x1="7" y1="4" x2="7" y2="10" stroke="currentColor" stroke-width="1.2"/><line x1="4" y1="7" x2="10" y2="7" stroke="currentColor" stroke-width="1.2"/></svg>
|
||||||
|
New
|
||||||
|
</button>
|
||||||
|
<div class="toolbar-separator"></div>
|
||||||
|
<span class="file-name">{{ fileName() }}</span>
|
||||||
|
<span class="spacer"></span>
|
||||||
|
<span class="cursor-info">Ln {{ cursorLine() }}, Col {{ cursorCol() }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Editor area -->
|
||||||
|
<textarea
|
||||||
|
class="editor-area"
|
||||||
|
[value]="content()"
|
||||||
|
placeholder="Start typing…"
|
||||||
|
spellcheck="false"
|
||||||
|
(input)="onInput($event)"
|
||||||
|
(keyup)="onKeyUp($event)"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-editor {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 8px;
|
||||||
|
background: #2d2d2d;
|
||||||
|
border-bottom: 1px solid #404040;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
height: 26px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-separator {
|
||||||
|
width: 1px;
|
||||||
|
height: 20px;
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-info {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #888;
|
||||||
|
font-family: 'Consolas', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-area {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
resize: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
tab-size: 2;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
import { Component, signal } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-text-editor',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule],
|
||||||
|
templateUrl: './text-editor.component.html',
|
||||||
|
styleUrl: './text-editor.component.scss',
|
||||||
|
})
|
||||||
|
export class TextEditorComponent {
|
||||||
|
readonly content = signal('');
|
||||||
|
readonly fileName = signal('untitled.txt');
|
||||||
|
readonly showSaveDialog = signal(false);
|
||||||
|
|
||||||
|
/** Cursor position. */
|
||||||
|
readonly cursorLine = signal(1);
|
||||||
|
readonly cursorCol = signal(1);
|
||||||
|
|
||||||
|
onInput(event: Event): void {
|
||||||
|
const textarea = event.target as HTMLTextAreaElement;
|
||||||
|
this.content.set(textarea.value);
|
||||||
|
this.updateCursorPosition(textarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyUp(event: Event): void {
|
||||||
|
this.updateCursorPosition(event.target as HTMLTextAreaElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateCursorPosition(textarea: HTMLTextAreaElement): void {
|
||||||
|
const val = textarea.value.substring(0, textarea.selectionStart);
|
||||||
|
const lines = val.split('\n');
|
||||||
|
this.cursorLine.set(lines.length);
|
||||||
|
this.cursorCol.set(lines[lines.length - 1].length + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
newFile(): void {
|
||||||
|
this.content.set('');
|
||||||
|
this.fileName.set('untitled.txt');
|
||||||
|
}
|
||||||
|
|
||||||
|
saveFile(): void {
|
||||||
|
// In a real implementation this would save to the server's virtual filesystem.
|
||||||
|
this.showSaveDialog.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user