Skip to content

Commit

Permalink
feat(admin): Monaco, Resize panels, improved explorer (#509)
Browse files Browse the repository at this point in the history
* feat(admin): merge frontmatter and content

* chore: init monaco

* chore: fix lint

* feat: monaco dark mode

* feat(admin): resizable panels, close #484

* style: improve explorer

* chore: enabled wordWrap for monaco

* style: panel style

* style(admin): explorer scoll

* fix(admin): monaco build
  • Loading branch information
antfu authored Jun 24, 2021
1 parent 656f0cc commit 37a772e
Show file tree
Hide file tree
Showing 19 changed files with 406 additions and 260 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"dev": "ADMIN_DEV=true nuxt dev docs",
"dev:nuxtjs": "nuxt dev nuxtjs.org",
"dev:admin": "vite --config src/admin/app/vite.config.ts",
"build:admin": "vite build --config src/admin/app/vite.config.ts",
"generate": "nuxt generate --force-build docs",
"generate:nuxtjs": "nuxt generate --force-build nuxtjs.org",
"start": "nuxt start docs",
Expand Down Expand Up @@ -131,9 +132,12 @@
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-nuxt": "^2.0.0",
"eslint-plugin-prettier": "^3.4.0",
"monaco-editor": "^0.25.2",
"prettier": "^2.3.1",
"siroc": "^0.9.3",
"splitpanes": "^3.0.4",
"standard-version": "^9.3.0",
"vite": "^2.3.8",
"vite-plugin-components": "^0.11.2",
"vite-plugin-icons": "^0.6.3"
},
Expand Down
File renamed without changes.
27 changes: 8 additions & 19 deletions src/admin/api/functions/content.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { promises as fs } from 'fs'
import { join, extname } from 'path'
import matter from 'gray-matter'
import { createError, Middleware, useBody } from 'h3'
import dirTree from 'directory-tree'
import { FileData, File } from '../../type'
import { normalizeFiles, r } from '../utils'

interface Body {
data: any
content: string
}

export default <Middleware>async function contentHandler(req) {
const url = req.url

Expand All @@ -23,13 +18,11 @@ export default <Middleware>async function contentHandler(req) {
try {
const path = join(r('content'), url)
const file = await fs.readFile(path, 'utf-8')
const { content, data } = matter(file)

return {
return <File>{
path: path.replace(r('content'), ''),
extension: extname(path),
data,
content
raw: file
}
} catch (err) {
return createError({
Expand All @@ -41,24 +34,20 @@ export default <Middleware>async function contentHandler(req) {

// Update changes
if (req.method === 'PUT') {
const { data, content } = await useBody<Body>(req)

if (!data || !content) {
const { raw } = await useBody<FileData>(req)
if (raw == null) {
return createError({
statusCode: 400,
statusMessage: 'data and content keys are required'
statusMessage: '"raw" key is required'
})
}

const path = join(r('content'), url)

try {
// @ts-ignore
await fs.stat(path, 'utf-8')

const file = matter.stringify(content, data)

await fs.writeFile(path, file)
// await fs.stat(path, 'utf-8')
await fs.writeFile(path, raw)

return { ok: true }
} catch (err) {
Expand Down
13 changes: 1 addition & 12 deletions src/admin/app/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,9 @@
<Component :is="Component" />
</KeepAlive>
</RouterView>

<Preview />
</main>
</template>

<script lang="ts">
import { defineComponent } from 'vue3'
<script setup lang="ts">
import AppHeader from './components/AppHeader.vue'
import Preview from './components/Preview.vue'
export default defineComponent({
components: {
AppHeader,
Preview
}
})
</script>
87 changes: 25 additions & 62 deletions src/admin/app/components/Editor.vue
Original file line number Diff line number Diff line change
@@ -1,72 +1,35 @@
<template>
<textarea v-model="frontmatter" class="h-24 w-full font-mono px-4 py-2 d-border border-b outline-none text-sm" />
<textarea v-model="content" class="w-full h-full font-mono px-4 py-2 outline-none text-sm" />
<Monaco :value="raw" language="markdown" @change="update" />
</template>

<script lang="ts">
import YAML from 'js-yaml'
import { GrayMatterFile } from 'gray-matter'
import { defineComponent, computed, ref, watch, PropType } from 'vue3'
<script setup lang="ts">
import type { PropType } from 'vue3'
import { ref, watch, defineProps } from 'vue3'
import type { File } from '../../type'
import { useApi } from '../plugins/api'
import Monaco from './Monaco.vue'
type PermissiveGrayMatterFile = GrayMatterFile<any> & { file: any; path: any }
export default defineComponent({
props: {
file: {
type: Object as PropType<PermissiveGrayMatterFile>,
required: true
}
},
setup(props) {
const api = useApi()
// Sync local data when file changes
watch(
() => props.file,
newVal => {
data.value = newVal.data
content.value = newVal.content
}
)
// Local data
const data = ref(props.file.data)
const content = ref(props.file.content)
// Stringified reference for frontmatter text-area
const frontmatter = computed({
get() {
return YAML.dump(data.value, null, 2)
},
set(value: string) {
try {
data.value = YAML.parse(value)
} catch (e) {
// New value is not a valid YAML string.
// Do nothing and wait for the next valid YAML input.
}
}
})
const props = defineProps({
file: {
type: Object as PropType<File>,
required: true
}
})
// API update on change on data or content
watch([data, content], () => {
api.put(`/content${props.file.path}`, {
data: data.value,
content: content.value
})
})
const api = useApi()
const raw = ref(props.file.raw)
return {
frontmatter,
content
}
// Sync local data when file changes
watch(
() => props.file,
newVal => {
raw.value = newVal.raw
}
})
</script>
)
<style lang="postcss" scoped>
textarea {
@apply whitespace-nowrap overflow-scroll bg-transparent;
function update(content) {
api.put(`/content${props.file.path}`, {
raw: content
})
}
</style>
</script>
29 changes: 17 additions & 12 deletions src/admin/app/components/FilesTree.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
<template>
<ul>
<li
v-for="file of files"
:key="file.path"
class="rounded"
:class="isCurrent(file) ? 'bg-gray-400 bg-opacity-20' : 'opacity-75'"
>
<li v-for="file of files" :key="file.path">
<div
v-if="!isHidden(file)"
class="
Expand All @@ -18,40 +13,50 @@
text-sm
leading-5
rounded
overflow-hidden
select-none
hover:bg-gray-400 hover:bg-opacity-15
"
:class="isCurrent(file) ? 'bg-gray-400 bg-opacity-20' : 'opacity-75'"
@click="open(file)"
>
<FilesTreeIcon :file="file" />
<span>{{ filename(file.name) }}</span>
<TreeToggler :file="file" />
<FilesTreeIcon class="w-8 min-w-8" :file="file" />
<div class="whitespace-nowrap overflow-ellipsis">{{ filename(file.name) }}</div>
</div>
<FilesTree
v-if="isDir(file) && file.isOpen"
:files="file.children"
:current-file="currentFile"
:is-root="false"
class="pl-2"
class="pl-3"
@open="open"
/>
</li>
</ul>
</template>

<script>
<script lang="ts">
import { defineComponent, provide } from 'vue3'
import type { PropType } from 'vue3'
import type { File } from '../../type'
import { isImage } from '../utils'
import FilesTreeIcon from './FilesTreeIcon.vue'
import TreeToggler from './TreeToggler.vue'
export default defineComponent({
name: 'FilesTree',
components: { FilesTreeIcon },
components: {
FilesTreeIcon,
TreeToggler
},
props: {
isRoot: {
type: Boolean,
default: true
},
files: {
type: Array,
type: Array as PropType<File[]>,
default: () => []
},
currentFile: {
Expand Down
104 changes: 18 additions & 86 deletions src/admin/app/components/FilesTreeIcon.vue
Original file line number Diff line number Diff line change
@@ -1,93 +1,25 @@
<template>
<svg
v-if="isDir(file) && file.isOpen"
class="mr-1 h-4 w-4 text-gray-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>

<svg
v-else-if="isDir(file)"
class="mr-1 h-4 w-4 text-gray-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>

<span v-else class="mr-1 inline-block h-4 w-4" />

<svg
v-if="isDir(file)"
class="mr-1 h-5 w-5 text-blue-400"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" />
</svg>

<svg
v-else-if="isImage(file)"
class="mr-1 h-5 w-5 text-gray-500"
:class="{ 'text-gray-600': isCurrent(file) }"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>

<svg
v-else
class="mr-1 h-5 w-5 text-gray-500"
:class="{ 'text-gray-600': isCurrent(file) }"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
<vscode-icons:default-folder-opened v-if="isDir && file.isOpen" />
<vscode-icons:default-folder v-else-if="isDir" />
<mdi:language-markdown v-else-if="isMarkdown" />
<vscode-icons:file-type-vue v-else-if="isVue" />
<heroicons-outline:photograph v-else-if="isImage" />
<heroicons-outline:document v-else />
</template>

<script lang="ts">
import { defineComponent, inject } from 'vue3'
export default defineComponent({
props: {
file: {
type: Object,
required: true
}
},
setup() {
const { isDir, isCurrent, isImage, hasOneDir, open } = inject('tree-helpers')
<script setup lang="ts">
import { defineProps, computed } from 'vue3'
import { isImage as isFileImage } from '../utils'
return {
isDir,
isCurrent,
isImage,
hasOneDir,
open
}
const props = defineProps({
file: {
type: Object,
required: true
}
})
const isDir = computed(() => props.file.type === 'directory')
const isImage = computed(() => isFileImage(props.file))
const isMarkdown = computed(() => props.file.extension === '.md')
const isVue = computed(() => props.file.extension === '.vue')
</script>
Loading

1 comment on commit 37a772e

@vercel
Copy link

@vercel vercel bot commented on 37a772e Jun 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.