diff --git a/frontend/package.json b/frontend/package.json
index 6061686..4e2dd9c 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -18,8 +18,11 @@
"node": ">=18.0.0"
},
"dependencies": {
+ "@codemirror/language-data": "^6.5.2",
+ "@codemirror/theme-one-dark": "^6.1.3",
"@imengyu/vue3-context-menu": "^1.5.3",
"@vueuse/core": "^14.1.0",
+ "codemirror": "^6.0.2",
"esbuild": "^0.27.2",
"lodash": "^4.17.23",
"lodash-es": "^4.17.23",
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 71afd77..32ea297 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -131,7 +131,9 @@ const globalShortcutHandler = (event: KeyboardEvent) => {
const fileExplorer = store.fileExplorer as any
if (!fileExplorer) return
const c = fileExplorer.isCursor()
- const input = ['INPUT', 'TEXTAREA'].includes((event.target as HTMLElement).tagName)
+ const target = event.target as HTMLElement
+ const input =
+ ['INPUT', 'TEXTAREA'].includes(target.tagName) || !!target.closest('.cm-editor')
const keyup = event.type === 'keyup'
// Always clear repeat timer on arrow keyup, even if focus moved to input
diff --git a/frontend/src/assets/svg/create-file.svg b/frontend/src/assets/svg/create-file.svg
index 3486c99..f9ed1a5 100644
--- a/frontend/src/assets/svg/create-file.svg
+++ b/frontend/src/assets/svg/create-file.svg
@@ -1 +1,3 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/frontend/src/components/FileExplorer.vue b/frontend/src/components/FileExplorer.vue
index 227f26a..986ea05 100644
--- a/frontend/src/components/FileExplorer.vue
+++ b/frontend/src/components/FileExplorer.vue
@@ -12,10 +12,10 @@
-
+
|
-
+
|
@@ -206,8 +206,20 @@ const rename = async (doc: Doc, newName: string) => {
}
}
defineExpose({
+ newFile() {
+ const now = Math.floor(Date.now() / 1000)
+ editing.value = new Doc({
+ loc: loc.value,
+ key: 'new',
+ name: 'New File.txt',
+ dir: false,
+ mtime: now,
+ size: 0,
+ allocated: 0
+ })
+ store.cursor = editing.value.key
+ },
newFolder() {
- console.log('New folder')
const now = Math.floor(Date.now() / 1000)
editing.value = new Doc({
loc: loc.value,
@@ -350,19 +362,36 @@ onUnmounted(() => {
keyboardFollowScroll.cancel()
clearInterval(modifiedTimer)
})
-const mkdir = async (doc: Doc, name: string) => {
+const editRoute = (path: string) =>
+ '/edit/' +
+ path
+ .split('/')
+ .map(part => encodeURIComponent(part))
+ .join('/')
+
+const createItem = async (doc: Doc, name: string) => {
doc.name = name
doc.key = crypto.randomUUID()
store.addGhost(doc)
editing.value = null
const path = doc.loc ? `${doc.loc}/${name}` : name
try {
- const res = await apiFetch(filesUrl(path), { method: 'MKCOL' })
+ const res = doc.dir
+ ? await apiFetch(filesUrl(path), { method: 'MKCOL' })
+ : await apiFetch(filesUrl(path), {
+ method: 'PUT',
+ body: '',
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' }
+ })
if (!res.ok) throw new Error(await parseErrorMessage(res))
- router.push(doc.urlrouter)
+ if (doc.dir) {
+ router.push(doc.urlrouter)
+ } else {
+ router.push(editRoute(path))
+ }
} catch (err) {
- console.error('Mkdir failed', err)
- store.showToast(err instanceof Error ? err.message : 'Mkdir failed')
+ console.error('Create failed', err)
+ store.showToast(err instanceof Error ? err.message : 'Create failed')
}
}
const showFolderBreadcrumb = (i: number) => {
diff --git a/frontend/src/components/Gallery.vue b/frontend/src/components/Gallery.vue
index 3ca587e..abd11fa 100644
--- a/frontend/src/components/Gallery.vue
+++ b/frontend/src/components/Gallery.vue
@@ -1,6 +1,6 @@
-
+
{
}
defineExpose({
+ newFile() {
+ const now = Math.floor(Date.now() / 1000)
+ editing.value = new Doc({
+ loc: loc.value,
+ key: 'new',
+ name: 'New File.txt',
+ dir: false,
+ mtime: now,
+ size: 0,
+ allocated: 0
+ })
+ store.cursor = editing.value.key
+ },
newFolder() {
const now = Math.floor(Date.now() / 1000)
editing.value = new Doc({
@@ -420,19 +433,36 @@ onUnmounted(() => {
// Re-seed aspect ratios whenever docs update (e.g., ar patch from server)
watch(() => props.documents, seedFromDocs)
-const mkdir = async (doc: Doc, name: string) => {
+const editRoute = (path: string) =>
+ '/edit/' +
+ path
+ .split('/')
+ .map(part => encodeURIComponent(part))
+ .join('/')
+
+const createItem = async (doc: Doc, name: string) => {
doc.name = name
doc.key = crypto.randomUUID()
store.addGhost(doc)
editing.value = null
const path = doc.loc ? `${doc.loc}/${name}` : name
try {
- const res = await apiFetch(filesUrl(path), { method: 'MKCOL' })
+ const res = doc.dir
+ ? await apiFetch(filesUrl(path), { method: 'MKCOL' })
+ : await apiFetch(filesUrl(path), {
+ method: 'PUT',
+ body: '',
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' }
+ })
if (!res.ok) throw new Error(await parseErrorMessage(res))
- router.push(doc.urlrouter)
+ if (doc.dir) {
+ router.push(doc.urlrouter)
+ } else {
+ router.push(editRoute(path))
+ }
} catch (err) {
- console.error('Mkdir failed', err)
- store.showToast(err instanceof Error ? err.message : 'Mkdir failed')
+ console.error('Create failed', err)
+ store.showToast(err instanceof Error ? err.message : 'Create failed')
}
}
const showFolderBreadcrumb = (i: number) => {
diff --git a/frontend/src/components/HeaderMain.vue b/frontend/src/components/HeaderMain.vue
index 6b7eab5..88e1d91 100644
--- a/frontend/src/components/HeaderMain.vue
+++ b/frontend/src/components/HeaderMain.vue
@@ -2,6 +2,11 @@
@@ -133,6 +175,7 @@ onUnmounted(() => {
height: 100%;
background: #1a1a1a;
color: #ddd;
+ text-align: left;
}
.editor-body {
flex: 1;
@@ -140,25 +183,40 @@ onUnmounted(() => {
display: flex;
flex-direction: column;
}
-.editor-body textarea {
+.editor-host {
flex: 1;
- width: 100%;
- resize: none;
+ min-height: 0;
+}
+.editor-host :deep(.cm-editor) {
+ flex: 1;
+ height: 100%;
border: none;
outline: none;
- padding: 1rem;
+}
+.editor-host :deep(.cm-scroller) {
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
font-size: 0.875rem;
line-height: 1.5;
- background: #1a1a1a;
- color: #ddd;
- white-space: pre;
- overflow-wrap: normal;
- overflow-x: auto;
+ text-align: left;
}
-.editor-body textarea::selection {
- background: var(--accent-color, #007bff);
- color: #000;
+.editor-host :deep(.cm-content) {
+ padding: 1rem;
+ text-align: left;
+}
+.editor-host :deep(.cm-selectionBackground) {
+ background: var(--soft-color, #146) !important;
+}
+.editor-host :deep(.cm-focused .cm-selectionBackground) {
+ background: var(--soft-color, #146) !important;
+}
+.editor-host :deep(.cm-content ::selection) {
+ background: var(--soft-color, #146);
+}
+.editor-host :deep(.cm-content, .cm-gutter) {
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
+}
+.editor-host :deep(.cm-line, .cm-gutters, .cm-gutterElement) {
+ text-align: left;
}
.status {
padding: 2rem;