浏览代码

feat: welcome overlay + editor improvements

NGPixel 2 年之前
父节点
当前提交
d2a18eca3c

+ 2 - 0
.devcontainer/app-init.sh

@@ -9,6 +9,8 @@ git config oh-my-zsh.hide-info 1
 echo "Waiting for DB container to come online..."
 echo "Waiting for DB container to come online..."
 /usr/local/bin/wait-for localhost:5432 -- echo "DB ready"
 /usr/local/bin/wait-for localhost:5432 -- echo "DB ready"
 
 
+npm install -g npm-check-updates
+
 echo "Installing dependencies..."
 echo "Installing dependencies..."
 cd server
 cd server
 npm install
 npm install

+ 5 - 1
.vscode/settings.json

@@ -13,5 +13,9 @@
   "i18n-ally.localesPaths": [
   "i18n-ally.localesPaths": [
     "ux/src/i18n/locales"
     "ux/src/i18n/locales"
   ],
   ],
-  "i18n-ally.keystyle": "flat"
+  "i18n-ally.keystyle": "flat",
+  "i18n-ally.sortKeys": true,
+  "i18n-ally.enabledFrameworks": [
+    "vue"
+  ]
 }
 }

+ 1 - 1
server/package.json

@@ -198,7 +198,7 @@
     },
     },
     "ext": "js,json,graphql,gql",
     "ext": "js,json,graphql,gql",
     "watch": [
     "watch": [
-      "./"
+      "server"
     ]
     ]
   }
   }
 }
 }

文件差异内容过多而无法显示
+ 344 - 272
ux/package-lock.json


+ 47 - 46
ux/package.json

@@ -11,41 +11,42 @@
     "lint": "eslint --ext .js,.vue ./"
     "lint": "eslint --ext .js,.vue ./"
   },
   },
   "dependencies": {
   "dependencies": {
-    "@apollo/client": "3.7.7",
+    "@apollo/client": "3.7.11",
     "@lezer/common": "1.0.2",
     "@lezer/common": "1.0.2",
-    "@quasar/extras": "1.15.10",
-    "@tiptap/core": "2.0.0-beta.212",
-    "@tiptap/extension-code-block": "2.0.0-beta.212",
-    "@tiptap/extension-code-block-lowlight": "2.0.0-beta.212",
-    "@tiptap/extension-color": "2.0.0-beta.212",
-    "@tiptap/extension-dropcursor": "2.0.0-beta.212",
-    "@tiptap/extension-font-family": "2.0.0-beta.212",
-    "@tiptap/extension-gapcursor": "2.0.0-beta.212",
-    "@tiptap/extension-hard-break": "2.0.0-beta.212",
-    "@tiptap/extension-highlight": "2.0.0-beta.212",
-    "@tiptap/extension-history": "2.0.0-beta.212",
-    "@tiptap/extension-image": "2.0.0-beta.212",
-    "@tiptap/extension-mention": "2.0.0-beta.212",
-    "@tiptap/extension-placeholder": "2.0.0-beta.212",
-    "@tiptap/extension-table": "2.0.0-beta.212",
-    "@tiptap/extension-table-cell": "2.0.0-beta.212",
-    "@tiptap/extension-table-header": "2.0.0-beta.212",
-    "@tiptap/extension-table-row": "2.0.0-beta.212",
-    "@tiptap/extension-task-item": "2.0.0-beta.212",
-    "@tiptap/extension-task-list": "2.0.0-beta.212",
-    "@tiptap/extension-text-align": "2.0.0-beta.212",
-    "@tiptap/extension-text-style": "2.0.0-beta.212",
-    "@tiptap/extension-typography": "2.0.0-beta.212",
-    "@tiptap/pm": "2.0.0-beta.212",
-    "@tiptap/starter-kit": "2.0.0-beta.212",
-    "@tiptap/vue-3": "2.0.0-beta.212",
+    "@mdi/font": "7.2.96",
+    "@quasar/extras": "1.16.1",
+    "@tiptap/core": "2.0.1",
+    "@tiptap/extension-code-block": "2.0.1",
+    "@tiptap/extension-code-block-lowlight": "2.0.1",
+    "@tiptap/extension-color": "2.0.1",
+    "@tiptap/extension-dropcursor": "2.0.1",
+    "@tiptap/extension-font-family": "2.0.1",
+    "@tiptap/extension-gapcursor": "2.0.1",
+    "@tiptap/extension-hard-break": "2.0.1",
+    "@tiptap/extension-highlight": "2.0.1",
+    "@tiptap/extension-history": "2.0.1",
+    "@tiptap/extension-image": "2.0.1",
+    "@tiptap/extension-mention": "2.0.1",
+    "@tiptap/extension-placeholder": "2.0.1",
+    "@tiptap/extension-table": "2.0.1",
+    "@tiptap/extension-table-cell": "2.0.1",
+    "@tiptap/extension-table-header": "2.0.1",
+    "@tiptap/extension-table-row": "2.0.1",
+    "@tiptap/extension-task-item": "2.0.1",
+    "@tiptap/extension-task-list": "2.0.1",
+    "@tiptap/extension-text-align": "2.0.1",
+    "@tiptap/extension-text-style": "2.0.1",
+    "@tiptap/extension-typography": "2.0.1",
+    "@tiptap/pm": "2.0.1",
+    "@tiptap/starter-kit": "2.0.1",
+    "@tiptap/vue-3": "2.0.1",
     "apollo-upload-client": "17.0.0",
     "apollo-upload-client": "17.0.0",
-    "browser-fs-access": "0.31.2",
+    "browser-fs-access": "0.33.0",
     "clipboard": "2.0.11",
     "clipboard": "2.0.11",
     "codemirror": "5.65.11",
     "codemirror": "5.65.11",
     "codemirror-asciidoc": "1.0.4",
     "codemirror-asciidoc": "1.0.4",
     "dependency-graph": "0.11.0",
     "dependency-graph": "0.11.0",
-    "filesize": "10.0.6",
+    "filesize": "10.0.7",
     "filesize-parser": "1.5.0",
     "filesize-parser": "1.5.0",
     "fuse.js": "6.6.2",
     "fuse.js": "6.6.2",
     "graphql": "16.6.0",
     "graphql": "16.6.0",
@@ -54,45 +55,45 @@
     "jwt-decode": "3.1.2",
     "jwt-decode": "3.1.2",
     "lodash-es": "4.17.21",
     "lodash-es": "4.17.21",
     "lowlight": "2.8.1",
     "lowlight": "2.8.1",
-    "luxon": "3.2.1",
-    "pinia": "2.0.30",
-    "prosemirror-commands": "1.5.0",
+    "luxon": "3.3.0",
+    "pinia": "2.0.33",
+    "prosemirror-commands": "1.5.1",
     "prosemirror-history": "1.3.0",
     "prosemirror-history": "1.3.0",
-    "prosemirror-keymap": "1.2.0",
+    "prosemirror-keymap": "1.2.1",
     "prosemirror-model": "1.19.0",
     "prosemirror-model": "1.19.0",
     "prosemirror-schema-list": "1.2.2",
     "prosemirror-schema-list": "1.2.2",
     "prosemirror-state": "1.4.2",
     "prosemirror-state": "1.4.2",
     "prosemirror-transform": "1.7.1",
     "prosemirror-transform": "1.7.1",
-    "prosemirror-view": "1.30.1",
+    "prosemirror-view": "1.30.2",
     "pug": "3.0.2",
     "pug": "3.0.2",
-    "quasar": "2.11.5",
-    "slugify": "1.6.5",
-    "socket.io-client": "4.5.4",
+    "quasar": "2.11.9",
+    "slugify": "1.6.6",
+    "socket.io-client": "4.6.1",
     "tabulator-tables": "5.4.4",
     "tabulator-tables": "5.4.4",
     "tippy.js": "6.3.7",
     "tippy.js": "6.3.7",
     "uuid": "9.0.0",
     "uuid": "9.0.0",
-    "v-network-graph": "0.8.1",
+    "v-network-graph": "0.9.1",
     "vue": "3.2.47",
     "vue": "3.2.47",
     "vue-i18n": "9.2.2",
     "vue-i18n": "9.2.2",
     "vue-router": "4.1.6",
     "vue-router": "4.1.6",
-    "vue3-otp-input": "0.3.6",
+    "vue3-otp-input": "0.4.1",
     "vuedraggable": "4.1.0",
     "vuedraggable": "4.1.0",
     "xterm": "5.1.0",
     "xterm": "5.1.0",
     "zxcvbn": "4.4.2"
     "zxcvbn": "4.4.2"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@intlify/unplugin-vue-i18n": "0.8.1",
-    "@quasar/app-vite": "1.2.0",
-    "@types/lodash": "4.14.191",
-    "@volar/vue-language-plugin-pug": "1.0.24",
+    "@intlify/unplugin-vue-i18n": "0.10.0",
+    "@quasar/app-vite": "1.2.1",
+    "@types/lodash": "4.14.192",
+    "@volar/vue-language-plugin-pug": "1.2.0",
     "autoprefixer": "10.4.14",
     "autoprefixer": "10.4.14",
     "browserlist": "latest",
     "browserlist": "latest",
-    "eslint": "8.33.0",
+    "eslint": "8.37.0",
     "eslint-config-standard": "17.0.0",
     "eslint-config-standard": "17.0.0",
     "eslint-plugin-import": "2.27.5",
     "eslint-plugin-import": "2.27.5",
-    "eslint-plugin-n": "15.6.1",
+    "eslint-plugin-n": "15.7.0",
     "eslint-plugin-promise": "6.1.1",
     "eslint-plugin-promise": "6.1.1",
-    "eslint-plugin-vue": "9.9.0"
+    "eslint-plugin-vue": "9.10.0"
   },
   },
   "engines": {
   "engines": {
     "node": ">= 18.0",
     "node": ">= 18.0",

+ 1 - 1
ux/quasar.config.js

@@ -50,7 +50,7 @@ module.exports = configure(function (/* ctx */) {
     extras: [
     extras: [
       // 'ionicons-v4',
       // 'ionicons-v4',
       // 'mdi-v5',
       // 'mdi-v5',
-      'mdi-v7',
+      // 'mdi-v7',
       // 'fontawesome-v6',
       // 'fontawesome-v6',
       // 'eva-icons',
       // 'eva-icons',
       // 'themify',
       // 'themify',

+ 2 - 0
ux/src/App.vue

@@ -10,6 +10,8 @@ import { useSiteStore } from 'src/stores/site'
 import { useUserStore } from 'src/stores/user'
 import { useUserStore } from 'src/stores/user'
 import { setCssVar, useQuasar } from 'quasar'
 import { setCssVar, useQuasar } from 'quasar'
 
 
+import '@mdi/font/css/materialdesignicons.css'
+
 /* global siteConfig */
 /* global siteConfig */
 
 
 // QUASAR
 // QUASAR

+ 175 - 15
ux/src/components/EditorMarkdown.vue

@@ -31,19 +31,37 @@
         @click='insertTable'
         @click='insertTable'
         )
         )
         q-tooltip(anchor='center right' self='center left') {{ t('editor.markup.insertTable') }}
         q-tooltip(anchor='center right' self='center left') {{ t('editor.markup.insertTable') }}
+      q-btn(
+        icon='mdi-tab-plus'
+        padding='sm sm'
+        flat
+        )
+        q-tooltip(anchor='center right' self='center left') {{ t('editor.markup.insertTabset') }}
       q-btn(
       q-btn(
         icon='mdi-chart-multiline'
         icon='mdi-chart-multiline'
         padding='sm sm'
         padding='sm sm'
         flat
         flat
         )
         )
         q-tooltip(anchor='center right' self='center left') {{ t('editor.markup.insertDiagram') }}
         q-tooltip(anchor='center right' self='center left') {{ t('editor.markup.insertDiagram') }}
+      q-btn(
+        icon='mdi-book-plus'
+        padding='sm sm'
+        flat
+        )
+        q-tooltip(anchor='center right' self='center left') {{ t('editor.markup.insertFootnote') }}
+      q-btn(
+        icon='mdi-cookie-plus'
+        padding='sm sm'
+        flat
+        )
+        q-tooltip(anchor='center right' self='center left') {{ t('editor.markup.insertEmoji') }}
       q-btn(
       q-btn(
         icon='mdi-line-scan'
         icon='mdi-line-scan'
         padding='sm sm'
         padding='sm sm'
         flat
         flat
         @click='insertHorizontalBar'
         @click='insertHorizontalBar'
         )
         )
-        q-tooltip(anchor='center right' self='center left') {{ t('editor.markup.horizontalBar') }}
+        q-tooltip(anchor='center right' self='center left') {{ t('editor.markup.insertHorizontalBar') }}
       q-space
       q-space
       span.editor-markdown-type Markdown
       span.editor-markdown-type Markdown
     .editor-markdown-mid
     .editor-markdown-mid
@@ -55,18 +73,21 @@
           icon='mdi-format-bold'
           icon='mdi-format-bold'
           padding='xs sm'
           padding='xs sm'
           flat
           flat
+          @click='toggleMarkup({ start: `**` })'
           )
           )
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.bold') }}
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.bold') }}
         q-btn(
         q-btn(
           icon='mdi-format-italic'
           icon='mdi-format-italic'
           padding='xs sm'
           padding='xs sm'
           flat
           flat
+          @click='toggleMarkup({ start: `*` })'
           )
           )
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.italic') }}
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.italic') }}
         q-btn(
         q-btn(
           icon='mdi-format-strikethrough'
           icon='mdi-format-strikethrough'
           padding='xs sm'
           padding='xs sm'
           flat
           flat
+          @click='toggleMarkup({ start: `~~` })'
           )
           )
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.strikethrough') }}
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.strikethrough') }}
         q-btn(
         q-btn(
@@ -75,16 +96,28 @@
           flat
           flat
           )
           )
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.header') }}
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.header') }}
+          q-menu(auto-close)
+            q-list(separator)
+              q-item(
+                v-for='lvl in 6'
+                clickable
+                @click='setHeaderLine(lvl)'
+                )
+                q-item-section(side)
+                  q-icon(:name='`mdi-format-header-` + lvl')
+                q-item-section {{ t('editor.markup.headerLevel', { level: lvl }) }}
         q-btn(
         q-btn(
           icon='mdi-format-subscript'
           icon='mdi-format-subscript'
           padding='xs sm'
           padding='xs sm'
           flat
           flat
+          @click='toggleMarkup({ start: `~` })'
           )
           )
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.subscript') }}
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.subscript') }}
         q-btn(
         q-btn(
           icon='mdi-format-superscript'
           icon='mdi-format-superscript'
           padding='xs sm'
           padding='xs sm'
           flat
           flat
+          @click='toggleMarkup({ start: `^` })'
           )
           )
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.superscript') }}
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.superscript') }}
         q-btn(
         q-btn(
@@ -92,29 +125,71 @@
           padding='xs sm'
           padding='xs sm'
           flat
           flat
           )
           )
-          q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.blockquote') }}
+          q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.blockquoteAdmonitions') }}
+          q-menu(auto-close)
+            q-list(separator)
+              q-item(clickable, @click='insertBeforeEachLine({ content: `> `})')
+                q-item-section(side)
+                  q-icon(name='mdi-format-quote-close')
+                q-item-section {{ t('editor.markup.blockquote') }}
+              q-item(clickable, @click='insertBeforeEachLine({ content: `> `, after: `{.is-info}`})')
+                q-item-section(side)
+                  q-icon(name='mdi-information-box', color='blue-7')
+                q-item-section {{ t('editor.markup.admonitionInfo') }}
+              q-item(clickable, @click='insertBeforeEachLine({ content: `> `, after: `{.is-success}`})')
+                q-item-section(side)
+                  q-icon(name='mdi-check-circle', color='positive')
+                q-item-section {{ t('editor.markup.admonitionSuccess') }}
+              q-item(clickable, @click='insertBeforeEachLine({ content: `> `, after: `{.is-warning}`})')
+                q-item-section(side)
+                  q-icon(name='mdi-alert-box', color='orange')
+                q-item-section {{ t('editor.markup.admonitionWarning') }}
+              q-item(clickable, @click='insertBeforeEachLine({ content: `> `, after: `{.is-danger}`})')
+                q-item-section(side)
+                  q-icon(name='mdi-close-box', color='negative')
+                q-item-section {{ t('editor.markup.admonitionDanger') }}
         q-btn(
         q-btn(
           icon='mdi-format-list-bulleted'
           icon='mdi-format-list-bulleted'
           padding='xs sm'
           padding='xs sm'
           flat
           flat
+          @click='insertBeforeEachLine({ content: `- `})'
           )
           )
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.unorderedList') }}
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.unorderedList') }}
         q-btn(
         q-btn(
           icon='mdi-format-list-numbered'
           icon='mdi-format-list-numbered'
           padding='xs sm'
           padding='xs sm'
           flat
           flat
+          @click='insertBeforeEachLine({ content: `1. `})'
           )
           )
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.orderedList') }}
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.orderedList') }}
+        q-btn(
+          icon='mdi-format-list-checks'
+          padding='xs sm'
+          flat
+          )
+          q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.taskList') }}
+          q-menu(auto-close)
+            q-list(separator)
+              q-item(clickable, @click='insertBeforeEachLine({ content: `- [ ] `})')
+                q-item-section(side)
+                  q-icon(name='mdi-checkbox-blank-outline')
+                q-item-section {{ t('editor.markup.taskListUnchecked') }}
+              q-item(clickable, @click='insertBeforeEachLine({ content: `- [x] `})')
+                q-item-section(side)
+                  q-icon(name='mdi-checkbox-outline')
+                q-item-section {{ t('editor.markup.taskListChecked') }}
         q-btn(
         q-btn(
           icon='mdi-code-tags'
           icon='mdi-code-tags'
           padding='xs sm'
           padding='xs sm'
           flat
           flat
+          @click='toggleMarkup({ start: "`" })'
           )
           )
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.inlineCode') }}
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.inlineCode') }}
         q-btn(
         q-btn(
           icon='mdi-keyboard-variant'
           icon='mdi-keyboard-variant'
           padding='xs sm'
           padding='xs sm'
           flat
           flat
+          @click='toggleMarkup({ start: `<kbd>`, end: `</kbd>` })'
           )
           )
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.keyboardKey') }}
           q-tooltip(anchor='top middle' self='bottom middle') {{ t('editor.markup.keyboardKey') }}
         q-btn(
         q-btn(
@@ -161,6 +236,7 @@
 import { reactive, ref, shallowRef, nextTick, onBeforeMount, onMounted, watch } from 'vue'
 import { reactive, ref, shallowRef, nextTick, onBeforeMount, onMounted, watch } from 'vue'
 import { useMeta, useQuasar, setCssVar } from 'quasar'
 import { useMeta, useQuasar, setCssVar } from 'quasar'
 import { useI18n } from 'vue-i18n'
 import { useI18n } from 'vue-i18n'
+import { get, flatten, last, times, startsWith } from 'lodash-es'
 
 
 import { useEditorStore } from 'src/stores/editor'
 import { useEditorStore } from 'src/stores/editor'
 import { useSiteStore } from 'src/stores/site'
 import { useSiteStore } from 'src/stores/site'
@@ -206,7 +282,7 @@ const state = reactive({
   content: '',
   content: '',
   previewShown: true,
   previewShown: true,
   previewHTML: '',
   previewHTML: '',
-  previewScrollSync: false
+  previewScrollSync: true
 })
 })
 
 
 // Platform detection
 // Platform detection
@@ -226,6 +302,34 @@ function insertTable () {
   })
   })
 }
 }
 
 
+/**
+ * Set current line as header
+ */
+function setHeaderLine (lvl) {
+  const curLine = cm.value.doc.getCursor('head').line
+  let lineContent = cm.value.doc.getLine(curLine)
+  const lineLength = lineContent.length
+  if (startsWith(lineContent, '#')) {
+    lineContent = lineContent.replace(/^(#+ )/, '')
+  }
+  lineContent = times(lvl, n => '#').join('') + ' ' + lineContent
+  cm.value.doc.replaceRange(lineContent, { line: curLine, ch: 0 }, { line: curLine, ch: lineLength })
+}
+
+/**
+ * Get the header lever of the current line
+ */
+function getHeaderLevel (cm) {
+  const curLine = cm.doc.getCursor('head').line
+  const lineContent = cm.doc.getLine(curLine)
+  let lvl = 0
+  const result = lineContent.match(/^(#+) /)
+  if (result) {
+    lvl = get(result, '[1]', '').length
+  }
+  return lvl
+}
+
 /**
 /**
  * Insert content at cursor
  * Insert content at cursor
  */
  */
@@ -243,10 +347,55 @@ function insertAfter ({ content, newLine }) {
   cm.value.doc.replaceRange(newLine ? `\n${content}\n` : content, { line: curLine, ch: lineLength + 1 })
   cm.value.doc.replaceRange(newLine ? `\n${content}\n` : content, { line: curLine, ch: lineLength + 1 })
 }
 }
 
 
+/**
+ * Insert content before current line
+ */
+function insertBeforeEachLine ({ content, after }) {
+  let lines = []
+  if (!cm.value.doc.somethingSelected()) {
+    lines.push(cm.value.doc.getCursor('head').line)
+  } else {
+    lines = flatten(cm.value.doc.listSelections().map(sl => {
+      const range = Math.abs(sl.anchor.line - sl.head.line) + 1
+      const lowestLine = (sl.anchor.line > sl.head.line) ? sl.head.line : sl.anchor.line
+      return times(range, l => l + lowestLine)
+    }))
+  }
+  lines.forEach(ln => {
+    let lineContent = cm.value.doc.getLine(ln)
+    const lineLength = lineContent.length
+    if (startsWith(lineContent, content)) {
+      lineContent = lineContent.substring(content.length)
+    }
+    cm.value.doc.replaceRange(content + lineContent, { line: ln, ch: 0 }, { line: ln, ch: lineLength })
+  })
+  if (after) {
+    const lastLine = last(lines)
+    cm.value.doc.replaceRange(`\n${after}\n`, { line: lastLine, ch: cm.value.doc.getLine(lastLine).length + 1 })
+  }
+}
+
+/**
+ * Insert an Horizontal Bar
+ */
 function insertHorizontalBar () {
 function insertHorizontalBar () {
   insertAfter({ content: '---', newLine: true })
   insertAfter({ content: '---', newLine: true })
 }
 }
 
 
+/**
+ * Toggle Markup at selection
+ */
+function toggleMarkup ({ start, end }) {
+  if (!end) { end = start }
+  if (!cm.value.doc.somethingSelected()) {
+    return $q.notify({
+      type: 'negative',
+      message: t('editor.markup.noSelectionError')
+    })
+  }
+  cm.value.doc.replaceSelections(cm.value.doc.getSelections().map(s => start + s + end))
+}
+
 // MOUNTED
 // MOUNTED
 
 
 onMounted(async () => {
 onMounted(async () => {
@@ -300,23 +449,23 @@ onMounted(async () => {
       return false
       return false
     },
     },
     [`${CtrlKey}-B`] (c) {
     [`${CtrlKey}-B`] (c) {
-      // toggleMarkup({ start: '**' })
+      toggleMarkup({ start: '**' })
       return false
       return false
     },
     },
     [`${CtrlKey}-I`] (c) {
     [`${CtrlKey}-I`] (c) {
-      // toggleMarkup({ start: '*' })
+      toggleMarkup({ start: '*' })
       return false
       return false
     },
     },
     [`${CtrlKey}-Alt-Right`] (c) {
     [`${CtrlKey}-Alt-Right`] (c) {
-      // let lvl = getHeaderLevel(c)
-      // if (lvl >= 6) { lvl = 5 }
-      // setHeaderLine(lvl + 1)
+      let lvl = getHeaderLevel(c)
+      if (lvl >= 6) { lvl = 5 }
+      setHeaderLine(lvl + 1)
       return false
       return false
     },
     },
     [`${CtrlKey}-Alt-Left`] (c) {
     [`${CtrlKey}-Alt-Left`] (c) {
-      // let lvl = getHeaderLevel(c)
-      // if (lvl <= 1) { lvl = 2 }
-      // setHeaderLine(lvl - 1)
+      let lvl = getHeaderLevel(c)
+      if (lvl <= 1) { lvl = 2 }
+      setHeaderLine(lvl - 1)
       return false
       return false
     }
     }
   }
   }
@@ -336,6 +485,7 @@ onMounted(async () => {
   // this.processContent(this.$store.get('editor/content'))
   // this.processContent(this.$store.get('editor/content'))
   nextTick(() => {
   nextTick(() => {
     cm.value.refresh()
     cm.value.refresh()
+    cm.value.focus()
   })
   })
 
 
   // this.$root.$on('editorInsert', opts => {
   // this.$root.$on('editorInsert', opts => {
@@ -408,16 +558,19 @@ $editor-height-mobile: calc(100vh - 112px - 16px);
     text-orientation: mixed;
     text-orientation: mixed;
     padding-bottom: 1rem;
     padding-bottom: 1rem;
     color: rgba(255,255,255, .4);
     color: rgba(255,255,255, .4);
+    font-weight: 500;
   }
   }
   &-preview {
   &-preview {
     flex: 1 1 50%;
     flex: 1 1 50%;
-    background-color: $grey-2;
     position: relative;
     position: relative;
     height: $editor-height;
     height: $editor-height;
     overflow: hidden;
     overflow: hidden;
 
 
-    @at-root .theme--dark & {
-      background-color: $grey-9;
+    @at-root .body--light & {
+      background-color: $grey-2;
+    }
+    @at-root .body--dark & {
+      background-color: $dark-4;
     }
     }
     // @include until($tablet) {
     // @include until($tablet) {
     //   display: none;
     //   display: none;
@@ -434,12 +587,19 @@ $editor-height-mobile: calc(100vh - 112px - 16px);
       max-width: 0;
       max-width: 0;
     }
     }
     &-toolbar {
     &-toolbar {
-      background-color: $grey-3;
       color: $grey-8;
       color: $grey-8;
       height: 32px;
       height: 32px;
       display: flex;
       display: flex;
       align-items: center;
       align-items: center;
       padding: 0 1rem;
       padding: 0 1rem;
+
+      @at-root .body--light & {
+        background-color: $grey-3;
+      }
+      @at-root .body--dark & {
+        background-color: $dark-2;
+        color: $grey-6;
+      }
     }
     }
     &-content {
     &-content {
       height: $editor-height;
       height: $editor-height;

+ 4 - 0
ux/src/components/MainOverlayDialog.vue

@@ -26,6 +26,10 @@ const overlays = {
   TableEditor: defineAsyncComponent({
   TableEditor: defineAsyncComponent({
     loader: () => import('./TableEditorOverlay.vue'),
     loader: () => import('./TableEditorOverlay.vue'),
     loadingComponent: LoadingGeneric
     loadingComponent: LoadingGeneric
+  }),
+  Welcome: defineAsyncComponent({
+    loader: () => import('./WelcomeOverlay.vue'),
+    loadingComponent: LoadingGeneric
   })
   })
 }
 }
 
 

+ 210 - 0
ux/src/components/PageActionsCol.vue

@@ -0,0 +1,210 @@
+<template lang="pug">
+.page-actions.column.items-stretch.order-last(:class='editorStore.isActive ? `is-editor` : ``')
+  q-btn.q-py-md(
+    flat
+    icon='las la-pen-nib'
+    :color='editorStore.isActive ? `white` : `deep-orange-9`'
+    aria-label='Page Properties'
+    @click='togglePageProperties'
+    )
+    q-tooltip(anchor='center left' self='center right') Page Properties
+  q-btn.q-py-md(
+    flat
+    icon='las la-project-diagram'
+    :color='editorStore.isActive ? `white` : `deep-orange-9`'
+    aria-label='Page Data'
+    @click='togglePageData'
+    disable
+    v-if='flagsStore.experimental'
+    )
+    q-tooltip(anchor='center left' self='center right') Page Data
+  template(v-if='!(editorStore.isActive && editorStore.mode === `create`)')
+    q-separator.q-my-sm(inset)
+    q-btn.q-py-sm(
+      flat
+      icon='las la-ellipsis-h'
+      :color='editorStore.isActive ? `deep-orange-2` : `grey`'
+      aria-label='Page Actions'
+      )
+      q-tooltip(anchor='center left' self='center right') Page Actions
+      q-menu(
+        anchor='top left'
+        self='top right'
+        auto-close
+        transition-show='jump-left'
+        )
+        q-list(padding, style='min-width: 225px;')
+          q-item(clickable)
+            q-item-section.items-center(avatar)
+              q-icon(color='deep-orange-9', name='las la-history', size='sm')
+            q-item-section
+              q-item-label View History
+          q-item(clickable)
+            q-item-section.items-center(avatar)
+              q-icon(color='deep-orange-9', name='las la-code', size='sm')
+            q-item-section
+              q-item-label View Source
+          q-item(clickable)
+            q-item-section.items-center(avatar)
+              q-icon(color='deep-orange-9', name='las la-atom', size='sm')
+            q-item-section
+              q-item-label Convert Page
+          q-item(clickable)
+            q-item-section.items-center(avatar)
+              q-icon(color='deep-orange-9', name='las la-magic', size='sm')
+            q-item-section
+              q-item-label Re-render Page
+          q-item(clickable)
+            q-item-section.items-center(avatar)
+              q-icon(color='deep-orange-9', name='las la-sun', size='sm')
+            q-item-section
+              q-item-label View Backlinks
+  q-space
+  template(v-if='!(editorStore.isActive && editorStore.mode === `create`)')
+    q-btn.q-py-sm(
+      flat
+      icon='las la-copy'
+      :color='editorStore.isActive ? `deep-orange-2` : `grey`'
+      aria-label='Duplicate Page'
+      @click='duplicatePage'
+      )
+      q-tooltip(anchor='center left' self='center right') Duplicate Page
+    q-btn.q-py-sm(
+      flat
+      icon='las la-share'
+      :color='editorStore.isActive ? `deep-orange-2` : `grey`'
+      aria-label='Rename / Move Page'
+      @click='renamePage'
+      )
+      q-tooltip(anchor='center left' self='center right') Rename / Move Page
+    q-btn.q-py-sm(
+      flat
+      icon='las la-trash'
+      :color='editorStore.isActive ? `deep-orange-2` : `grey`'
+      aria-label='Delete Page'
+      @click='deletePage'
+      :class='editorStore.isActive ? `q-pb-md` : ``'
+      )
+      q-tooltip(anchor='center left' self='center right') Delete Page
+  span.page-actions-mode(v-else) {{ t('common.actions.newPage') }}
+</template>
+
+<script setup>
+import { useQuasar } from 'quasar'
+import { computed, defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { useI18n } from 'vue-i18n'
+
+import { useEditorStore } from 'src/stores/editor'
+import { useFlagsStore } from 'src/stores/flags'
+import { usePageStore } from 'src/stores/page'
+import { useSiteStore } from 'src/stores/site'
+
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const editorStore = useEditorStore()
+const flagsStore = useFlagsStore()
+const pageStore = usePageStore()
+const siteStore = useSiteStore()
+
+// ROUTER
+
+const router = useRouter()
+const route = useRoute()
+
+// I18N
+
+const { t } = useI18n()
+
+// METHODS
+
+function togglePageProperties () {
+  siteStore.$patch({
+    sideDialogComponent: 'PagePropertiesDialog',
+    sideDialogShown: true
+  })
+}
+
+function togglePageData () {
+  siteStore.$patch({
+    sideDialogComponent: 'PageDataDialog',
+    sideDialogShown: true
+  })
+}
+
+function duplicatePage () {
+  $q.dialog({
+    component: defineAsyncComponent(() => import('../components/TreeBrowserDialog.vue')),
+    componentProps: {
+      mode: 'duplicatePage',
+      folderPath: '',
+      itemId: pageStore.id,
+      itemTitle: pageStore.title,
+      itemFileName: pageStore.path
+    }
+  }).onOk(() => {
+    // TODO: change route to new location
+  })
+}
+
+function renamePage () {
+  $q.dialog({
+    component: defineAsyncComponent(() => import('../components/TreeBrowserDialog.vue')),
+    componentProps: {
+      mode: 'renamePage',
+      folderPath: '',
+      itemId: pageStore.id,
+      itemTitle: pageStore.title,
+      itemFileName: pageStore.path
+    }
+  }).onOk(() => {
+    // TODO: change route to new location
+  })
+}
+
+function deletePage () {
+  $q.dialog({
+    component: defineAsyncComponent(() => import('../components/PageDeleteDialog.vue')),
+    componentProps: {
+      pageId: pageStore.id,
+      pageName: pageStore.title
+    }
+  }).onOk(() => {
+    router.replace('/')
+  })
+}
+</script>
+
+<style lang="scss">
+.page-actions {
+  flex: 0 0 56px;
+
+  @at-root .body--light & {
+    background-color: $grey-3;
+  }
+  @at-root .body--dark & {
+    background-color: $dark-4;
+  }
+
+  &.is-editor {
+    @at-root .body--light & {
+      background-color: $deep-orange-9;
+    }
+    @at-root .body--dark & {
+      background-color: $deep-orange-9;
+    }
+  }
+
+  &-mode {
+    writing-mode: vertical-rl;
+    text-orientation: mixed;
+    padding: 1.75rem 1rem 1.75rem 0;
+    color: $deep-orange-3;
+    font-weight: 500;
+  }
+}
+</style>

+ 128 - 0
ux/src/components/SideDialog.vue

@@ -0,0 +1,128 @@
+<template lang="pug">
+q-dialog(
+  v-model='siteStore.sideDialogShown'
+  position='right'
+  full-height
+  transition-show='jump-left'
+  transition-hide='jump-right'
+  class='floating-sidepanel'
+  no-shake
+  )
+  component(:is='sideDialogs[siteStore.sideDialogComponent]')
+</template>
+
+<script setup>
+import { useQuasar } from 'quasar'
+import { computed, defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { useI18n } from 'vue-i18n'
+
+import { useEditorStore } from 'src/stores/editor'
+import { useFlagsStore } from 'src/stores/flags'
+import { usePageStore } from 'src/stores/page'
+import { useSiteStore } from 'src/stores/site'
+
+// COMPONENTS
+
+import LoadingGeneric from 'src/components/LoadingGeneric.vue'
+
+const sideDialogs = {
+  PageDataDialog: defineAsyncComponent({
+    loader: () => import('src/components/PageDataDialog.vue'),
+    loadingComponent: LoadingGeneric
+  }),
+  PagePropertiesDialog: defineAsyncComponent({
+    loader: () => import('src/components/PagePropertiesDialog.vue'),
+    loadingComponent: LoadingGeneric
+  })
+}
+
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const editorStore = useEditorStore()
+const flagsStore = useFlagsStore()
+const pageStore = usePageStore()
+const siteStore = useSiteStore()
+
+// ROUTER
+
+const router = useRouter()
+const route = useRoute()
+
+// I18N
+
+const { t } = useI18n()
+
+// DATA
+
+const state = reactive({
+  showSideDialog: false,
+  sideDialogComponent: null,
+  showGlobalDialog: false,
+  globalDialogComponent: null,
+  showTagsEditBtn: false,
+  tagEditMode: false,
+  tocExpanded: ['h1-0', 'h1-1'],
+  tocSelected: [],
+  currentRating: 3
+})
+</script>
+
+<style lang="scss">
+.floating-sidepanel {
+  .q-dialog__inner {
+    right: 24px;
+
+    .q-card {
+      border-radius: 4px !important;
+      min-width: 450px;
+
+      .q-card__section {
+        border-radius: 0;
+      }
+    }
+  }
+
+  .alt-card {
+    @at-root .body--light & {
+      background-color: $grey-2;
+      border-top: 1px solid $grey-4;
+      box-shadow: inset 0 1px 0 0 #FFF, inset 0 -1px 0 0 #FFF;
+      border-bottom: 1px solid $grey-4;
+    }
+    @at-root .body--dark & {
+      background-color: $dark-4;
+      border-top: 1px solid lighten($dark-3, 8%);
+      box-shadow: inset 0 1px 0 0 $dark-6, inset 0 -1px 0 0 $dark-6;
+      border-bottom: 1px solid lighten($dark-3, 8%);
+    }
+  }
+
+  &-quickaccess {
+    width: 40px;
+    border-radius: 4px !important;
+    background-color: rgba(0,0,0,.75);
+    backdrop-filter: blur(5px);
+    color: #FFF;
+    position: fixed;
+    right: 486px;
+    top: 74px;
+    z-index: -1;
+    display: flex;
+    flex-direction: column;
+    box-shadow: 0 0 5px 0 rgba(0,0,0,.5) !important;
+
+    @at-root .q-transition--jump-left-enter-active & {
+      display: none !important;
+    }
+
+    @at-root .q-transition--jump-right-leave-active & {
+      display: none !important;
+    }
+  }
+}
+</style>

+ 173 - 0
ux/src/components/WelcomeOverlay.vue

@@ -0,0 +1,173 @@
+<template lang='pug'>
+.welcome
+  .welcome-bg
+  .welcome-content
+    .welcome-logo
+      img(src='/_assets/logo-wikijs.svg')
+    .welcome-title {{t('welcome.title')}}
+    .welcome-subtitle {{t('welcome.subtitle')}}
+    .welcome-actions
+      q-btn(
+        push
+        color='primary'
+        :label='t(`welcome.createHome`)'
+        icon='las la-plus'
+        no-caps
+        )
+        q-menu.translucent-menu(
+          auto-close
+          anchor='top left'
+          self='bottom left'
+          )
+          q-list(padding)
+            q-item(
+              clickable
+              @click='createHomePage(`wysiwyg`)'
+              v-if='siteStore.editors.wysiwyg'
+              )
+              blueprint-icon(icon='google-presentation')
+              q-item-section.q-pr-sm Using the Visual Editor
+              q-item-section(side): q-icon(name='mdi-chevron-right')
+            q-item(
+              clickable
+              @click='createHomePage(`markdown`)'
+              v-if='siteStore.editors.markdown'
+              )
+              blueprint-icon(icon='markdown')
+              q-item-section.q-pr-sm Using the Markdown Editor
+              q-item-section(side): q-icon(name='mdi-chevron-right')
+            q-item(
+              clickable
+              @click='createHomePage(`asciidoc`)'
+              v-if='siteStore.editors.asciidoc'
+              )
+              blueprint-icon(icon='asciidoc')
+              q-item-section.q-pr-sm Using the AsciiDoc Editor
+              q-item-section(side): q-icon(name='mdi-chevron-right')
+      q-btn(
+        push
+        color='primary'
+        :label='t(`welcome.admin`)'
+        icon='las la-cog'
+        no-caps
+        @click='loadAdmin'
+      )
+
+</template>
+
+<script setup>
+import { useI18n } from 'vue-i18n'
+import { useRouter } from 'vue-router'
+import { useMeta, useQuasar } from 'quasar'
+
+import { useSiteStore } from 'src/stores/site'
+import { usePageStore } from 'src/stores/page'
+
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const pageStore = usePageStore()
+const siteStore = useSiteStore()
+
+// ROUTER
+
+const router = useRouter()
+
+// I18N
+
+const { t } = useI18n()
+
+// META
+
+useMeta({
+  title: t('welcome.title')
+})
+
+// METHODS
+
+function createHomePage (editor) {
+  siteStore.overlay = ''
+  pageStore.pageCreate({ editor, locale: 'en', path: '' })
+}
+
+function loadAdmin () {
+  siteStore.overlay = ''
+  router.push('/_admin')
+}
+
+</script>
+
+<style lang="scss">
+  .welcome {
+    background: #FFF radial-gradient(ellipse, #FFF, #DDD);
+    color: $grey-9;
+    height: 100vh;
+    border: 10px solid #EEE;
+    border-radius: 25px !important;
+
+    &-bg {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      width: 320px;
+      height: 320px;
+      background: linear-gradient(0, #FFF 50%, $blue-5 50%);
+      border-radius: 50%;
+      filter: blur(100px);
+      transform: translate(-50%, -55%);
+    }
+
+    &-content {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      width: 90vw;
+    }
+
+    &-logo {
+      user-select: none;
+
+      > img {
+        height: 200px;
+        user-select: none;
+      }
+    }
+
+    &-title {
+      font-size: 4rem;
+      font-weight: 500;
+      line-height: 4rem;
+      text-align: center;
+
+      @media (max-width: $breakpoint-md-max) {
+        font-size: 2.5rem;
+        line-height: 2.5rem;
+      }
+    }
+
+    &-subtitle {
+      font-size: 1.2rem;
+      font-weight: 500;
+      color: $blue-7;
+      line-height: 1.2rem;
+      margin-top: 1rem;
+    }
+
+    &-actions {
+      margin-top: 2rem;
+      text-align: center;
+
+      > .q-btn {
+        margin: 0 5px 5px 5px;
+      }
+    }
+  }
+</style>

+ 1 - 0
ux/src/css/app.scss

@@ -202,6 +202,7 @@ body::-webkit-scrollbar-thumb {
   @at-root .body--dark & {
   @at-root .body--dark & {
     background-color: rgba($dark,.7);
     background-color: rgba($dark,.7);
   }
   }
+
   backdrop-filter: blur(10px);
   backdrop-filter: blur(10px);
 
 
   > .q-card {
   > .q-card {

+ 90 - 77
ux/src/i18n/locales/en.json

@@ -55,9 +55,9 @@
   "admin.api.revoke": "Revoke",
   "admin.api.revoke": "Revoke",
   "admin.api.revokeConfirm": "Revoke API Key?",
   "admin.api.revokeConfirm": "Revoke API Key?",
   "admin.api.revokeConfirmText": "Are you sure you want to revoke key {name}? This action cannot be undone!",
   "admin.api.revokeConfirmText": "Are you sure you want to revoke key {name}? This action cannot be undone!",
+  "admin.api.revokeSuccess": "The key has been revoked successfully.",
   "admin.api.revoked": "Revoked",
   "admin.api.revoked": "Revoked",
   "admin.api.revokedHint": "This key has been revoked and can no longer be used.",
   "admin.api.revokedHint": "This key has been revoked and can no longer be used.",
-  "admin.api.revokeSuccess": "The key has been revoked successfully.",
   "admin.api.subtitle": "Manage keys to access the API",
   "admin.api.subtitle": "Manage keys to access the API",
   "admin.api.title": "API Access",
   "admin.api.title": "API Access",
   "admin.api.toggleStateDisabledSuccess": "API has been disabled successfully.",
   "admin.api.toggleStateDisabledSuccess": "API has been disabled successfully.",
@@ -138,21 +138,51 @@
   "admin.editors.channelDescription": "Create discussion channels to collaborate in real-time with your team.",
   "admin.editors.channelDescription": "Create discussion channels to collaborate in real-time with your team.",
   "admin.editors.channelName": "Discussion Channels",
   "admin.editors.channelName": "Discussion Channels",
   "admin.editors.configuration": "Configuration",
   "admin.editors.configuration": "Configuration",
+  "admin.editors.markdown.allowHTML": "Allow HTML",
+  "admin.editors.markdown.allowHTMLHint": "Allow HTML tags in content.",
+  "admin.editors.markdown.general": "General",
+  "admin.editors.markdown.kroki": "Kroki",
+  "admin.editors.markdown.krokiHint": "Enable Kroki Diagrams Parser",
+  "admin.editors.markdown.krokiServerUrl": "Kroki Server URL",
+  "admin.editors.markdown.krokiServerUrlHint": "URL to the Kroki server used for image generation.",
+  "admin.editors.markdown.latexEngine": "LaTeX Engine",
+  "admin.editors.markdown.latexEngineHint": "Which engine to use to process TeX/LaTeX expressions.",
+  "admin.editors.markdown.lineBreaks": "Auto Line Breaks",
+  "admin.editors.markdown.lineBreaksHint": "Automatically add linebreaks within paragraphs.",
+  "admin.editors.markdown.linkify": "Auto-linking",
+  "admin.editors.markdown.linkifyHint": "Automatically convert URLs into clickable links.",
+  "admin.editors.markdown.multimdTable": "MultiMarkdown Table",
+  "admin.editors.markdown.multimdTableHint": "Enable support for MultiMarkdown Table features.",
+  "admin.editors.markdown.plantuml": "PlantUML",
+  "admin.editors.markdown.plantumlHint": "Enable PlantUML Parser",
+  "admin.editors.markdown.plantumlServerUrl": "PlantUML Server URL",
+  "admin.editors.markdown.plantumlServerUrlHint": "URL to the PlantUML server used for image generation.",
+  "admin.editors.markdown.quotes": "Quotes Style",
+  "admin.editors.markdown.quotesHint": "When typographer is enabled. Double + single quotes replacement pairs. e.g. «»„“ for Russian, „“‚‘ for German, etc.",
+  "admin.editors.markdown.saveSuccess": "Markdown editor configuration saved successfully.",
+  "admin.editors.markdown.tabWidth": "Code Block Tab Width",
+  "admin.editors.markdown.tabWidthHint": "Amount of spaces for each tab in code blocks.",
+  "admin.editors.markdown.typographer": "Typographer",
+  "admin.editors.markdown.typographerHint": "Enable some language-neutral replacement + quotes beautification.",
+  "admin.editors.markdown.underline": "Underline Emphasis",
+  "admin.editors.markdown.underlineHint": "Enable text underlining by using _underline_ syntax.",
   "admin.editors.markdownDescription": "Use the Markdown syntax to write content. Includes real-time preview and code completion features.",
   "admin.editors.markdownDescription": "Use the Markdown syntax to write content. Includes real-time preview and code completion features.",
   "admin.editors.markdownName": "Markdown Editor",
   "admin.editors.markdownName": "Markdown Editor",
   "admin.editors.redirectDescription": "Create redirections to other pages / external links.",
   "admin.editors.redirectDescription": "Create redirections to other pages / external links.",
   "admin.editors.redirectName": "Redirection",
   "admin.editors.redirectName": "Redirection",
+  "admin.editors.saveSuccess": "Editors state saved successfully.",
   "admin.editors.subtitle": "Manage editors and their configuration",
   "admin.editors.subtitle": "Manage editors and their configuration",
   "admin.editors.title": "Editors",
   "admin.editors.title": "Editors",
+  "admin.editors.useRenderingPipeline": "Uses the rendering pipeline.",
   "admin.editors.wysiwygDescription": "A visual WYSIWYG editor. The recommended editor for non-technical users.",
   "admin.editors.wysiwygDescription": "A visual WYSIWYG editor. The recommended editor for non-technical users.",
   "admin.editors.wysiwygName": "Visual Editor",
   "admin.editors.wysiwygName": "Visual Editor",
   "admin.extensions.incompatible": "not compatible",
   "admin.extensions.incompatible": "not compatible",
   "admin.extensions.install": "Install",
   "admin.extensions.install": "Install",
-  "admin.extensions.installed": "Installed",
   "admin.extensions.installFailed": "Failed to install extension.",
   "admin.extensions.installFailed": "Failed to install extension.",
+  "admin.extensions.installSuccess": "Extension installed successfully.",
+  "admin.extensions.installed": "Installed",
   "admin.extensions.installing": "Installing extension...",
   "admin.extensions.installing": "Installing extension...",
   "admin.extensions.installingHint": "This may take a while depending on your server.",
   "admin.extensions.installingHint": "This may take a while depending on your server.",
-  "admin.extensions.installSuccess": "Extension installed successfully.",
   "admin.extensions.instructions": "Instructions",
   "admin.extensions.instructions": "Instructions",
   "admin.extensions.instructionsHint": "Must be installed manually",
   "admin.extensions.instructionsHint": "Must be installed manually",
   "admin.extensions.reinstall": "Reinstall",
   "admin.extensions.reinstall": "Reinstall",
@@ -188,7 +218,6 @@
   "admin.general.contentLicenseHint": "License shown in the footer of all content pages.",
   "admin.general.contentLicenseHint": "License shown in the footer of all content pages.",
   "admin.general.defaultDateFormat": "Default Date Format",
   "admin.general.defaultDateFormat": "Default Date Format",
   "admin.general.defaultDateFormatHint": "The default date format for new users.",
   "admin.general.defaultDateFormatHint": "The default date format for new users.",
-  "admin.general.defaults": "Site Defaults",
   "admin.general.defaultTimeFormat": "Default Time Format",
   "admin.general.defaultTimeFormat": "Default Time Format",
   "admin.general.defaultTimeFormat12h": "12 hour",
   "admin.general.defaultTimeFormat12h": "12 hour",
   "admin.general.defaultTimeFormat24h": "24 hour",
   "admin.general.defaultTimeFormat24h": "24 hour",
@@ -197,6 +226,7 @@
   "admin.general.defaultTimezoneHint": "The default timezone for new users.",
   "admin.general.defaultTimezoneHint": "The default timezone for new users.",
   "admin.general.defaultTocDepth": "Default ToC Depth",
   "admin.general.defaultTocDepth": "Default ToC Depth",
   "admin.general.defaultTocDepthHint": "The default minimum and maximum header levels to show in the table of contents.",
   "admin.general.defaultTocDepthHint": "The default minimum and maximum header levels to show in the table of contents.",
+  "admin.general.defaults": "Site Defaults",
   "admin.general.displaySiteTitle": "Display Site Title",
   "admin.general.displaySiteTitle": "Display Site Title",
   "admin.general.displaySiteTitleHint": "Should the site title be displayed next to the logo? If your logo isn't square and contain your brand name, turn this option off.",
   "admin.general.displaySiteTitleHint": "Should the site title be displayed next to the logo? If your logo isn't square and contain your brand name, turn this option off.",
   "admin.general.favicon": "Favicon",
   "admin.general.favicon": "Favicon",
@@ -235,11 +265,11 @@
   "admin.general.siteHostnameHint": "Hostname this site should respond to. Set * for catch-all / fallback domain.",
   "admin.general.siteHostnameHint": "Hostname this site should respond to. Set * for catch-all / fallback domain.",
   "admin.general.siteHostnameInvalid": "Invalid Hostname",
   "admin.general.siteHostnameInvalid": "Invalid Hostname",
   "admin.general.siteInfo": "Site Info",
   "admin.general.siteInfo": "Site Info",
-  "admin.general.sitemap": "Allow Sitemap",
-  "admin.general.sitemapHint": "Make a sitemap.xml available to search engines with all pages accessible to guests.",
   "admin.general.siteTitle": "Site Title",
   "admin.general.siteTitle": "Site Title",
   "admin.general.siteTitleHint": "Displayed in the top bar and appended to all pages meta title.",
   "admin.general.siteTitleHint": "Displayed in the top bar and appended to all pages meta title.",
   "admin.general.siteTitleInvalidChars": "Site Title contains invalid characters.",
   "admin.general.siteTitleInvalidChars": "Site Title contains invalid characters.",
+  "admin.general.sitemap": "Allow Sitemap",
+  "admin.general.sitemapHint": "Make a sitemap.xml available to search engines with all pages accessible to guests.",
   "admin.general.subtitle": "Main settings of your wiki",
   "admin.general.subtitle": "Main settings of your wiki",
   "admin.general.title": "General",
   "admin.general.title": "General",
   "admin.general.uploadClear": "Clear",
   "admin.general.uploadClear": "Clear",
@@ -251,9 +281,9 @@
   "admin.general.uploadLogo": "Upload Logo",
   "admin.general.uploadLogo": "Upload Logo",
   "admin.general.uploadNormalizeFilename": "Normalize Filenames",
   "admin.general.uploadNormalizeFilename": "Normalize Filenames",
   "admin.general.uploadNormalizeFilenameHint": "Automatically transform filenames to a standard URL-friendly format.",
   "admin.general.uploadNormalizeFilenameHint": "Automatically transform filenames to a standard URL-friendly format.",
-  "admin.general.uploads": "Uploads",
   "admin.general.uploadSizeHint": "An image of {size} pixels is recommended for best results.",
   "admin.general.uploadSizeHint": "An image of {size} pixels is recommended for best results.",
   "admin.general.uploadTypesHint": "{typeList} or {lastType} files only",
   "admin.general.uploadTypesHint": "{typeList} or {lastType} files only",
+  "admin.general.uploads": "Uploads",
   "admin.general.urlHandling": "URL Handling",
   "admin.general.urlHandling": "URL Handling",
   "admin.groups.assignUser": "Assign User",
   "admin.groups.assignUser": "Assign User",
   "admin.groups.authBehaviors": "Authentication Behaviors",
   "admin.groups.authBehaviors": "Authentication Behaviors",
@@ -297,10 +327,10 @@
   "admin.groups.ruleMatchStart": "Path Starts With...",
   "admin.groups.ruleMatchStart": "Path Starts With...",
   "admin.groups.ruleMatchTag": "Has Any Tag...",
   "admin.groups.ruleMatchTag": "Has Any Tag...",
   "admin.groups.ruleMatchTagAll": "Has All Tags...",
   "admin.groups.ruleMatchTagAll": "Has All Tags...",
-  "admin.groups.rules": "Rules",
   "admin.groups.ruleSites": "Site(s)",
   "admin.groups.ruleSites": "Site(s)",
-  "admin.groups.rulesNone": "This group doesn't have any rules yet.",
   "admin.groups.ruleUntitled": "Untitled Rule",
   "admin.groups.ruleUntitled": "Untitled Rule",
+  "admin.groups.rules": "Rules",
+  "admin.groups.rulesNone": "This group doesn't have any rules yet.",
   "admin.groups.selectedLocales": "Any Locale | {locale} locale only | {count} locales selected",
   "admin.groups.selectedLocales": "Any Locale | {locale} locale only | {count} locales selected",
   "admin.groups.selectedSites": "Any Site | 1 site selected | {count} sites selected",
   "admin.groups.selectedSites": "Any Site | 1 site selected | {count} sites selected",
   "admin.groups.subtitle": "Manage user groups and permissions",
   "admin.groups.subtitle": "Manage user groups and permissions",
@@ -378,10 +408,10 @@
   "admin.mail.dkimUse": "Use DKIM",
   "admin.mail.dkimUse": "Use DKIM",
   "admin.mail.dkimUseHint": "Should DKIM be used when sending emails.",
   "admin.mail.dkimUseHint": "Should DKIM be used when sending emails.",
   "admin.mail.saveSuccess": "Configuration saved successfully.",
   "admin.mail.saveSuccess": "Configuration saved successfully.",
+  "admin.mail.sendTestSuccess": "A test email was sent successfully.",
   "admin.mail.sender": "Sender",
   "admin.mail.sender": "Sender",
   "admin.mail.senderEmail": "Sender Email",
   "admin.mail.senderEmail": "Sender Email",
   "admin.mail.senderName": "Sender Name",
   "admin.mail.senderName": "Sender Name",
-  "admin.mail.sendTestSuccess": "A test email was sent successfully.",
   "admin.mail.smtp": "SMTP Settings",
   "admin.mail.smtp": "SMTP Settings",
   "admin.mail.smtpHost": "Host",
   "admin.mail.smtpHost": "Host",
   "admin.mail.smtpHostHint": "Hostname or IP address of the SMTP server.",
   "admin.mail.smtpHostHint": "Hostname or IP address of the SMTP server.",
@@ -399,8 +429,8 @@
   "admin.mail.smtpVerifySSLHint": "Some hosts requires SSL certificate checking to be disabled. Leave enabled for proper security.",
   "admin.mail.smtpVerifySSLHint": "Some hosts requires SSL certificate checking to be disabled. Leave enabled for proper security.",
   "admin.mail.subtitle": "Configure mail settings",
   "admin.mail.subtitle": "Configure mail settings",
   "admin.mail.templateResetPwd": "Password Reset Email",
   "admin.mail.templateResetPwd": "Password Reset Email",
-  "admin.mail.templates": "Mail Templates",
   "admin.mail.templateWelcome": "Welcome Email",
   "admin.mail.templateWelcome": "Welcome Email",
+  "admin.mail.templates": "Mail Templates",
   "admin.mail.test": "Send a test email",
   "admin.mail.test": "Send a test email",
   "admin.mail.testHint": "Send a test email to ensure your SMTP configuration is working.",
   "admin.mail.testHint": "Send a test email to ensure your SMTP configuration is working.",
   "admin.mail.testRecipient": "Recipient Email Address",
   "admin.mail.testRecipient": "Recipient Email Address",
@@ -597,10 +627,10 @@
   "admin.storage.assetDirectAccess": "Direct Access",
   "admin.storage.assetDirectAccess": "Direct Access",
   "admin.storage.assetDirectAccessHint": "Assets are accessed directly by the user using a secure / signed link. When enabled, takes priority over file streaming.",
   "admin.storage.assetDirectAccessHint": "Assets are accessed directly by the user using a secure / signed link. When enabled, takes priority over file streaming.",
   "admin.storage.assetDirectAccessNotSupported": "Not supported by this storage target.",
   "admin.storage.assetDirectAccessNotSupported": "Not supported by this storage target.",
-  "admin.storage.assetsOnly": "Assets Only",
   "admin.storage.assetStreaming": "File Streaming",
   "admin.storage.assetStreaming": "File Streaming",
   "admin.storage.assetStreamingHint": "Assets will be streamed from the storage target, through the server, to the user.",
   "admin.storage.assetStreamingHint": "Assets will be streamed from the storage target, through the server, to the user.",
   "admin.storage.assetStreamingNotSupported": "Not supported by this storage target.",
   "admin.storage.assetStreamingNotSupported": "Not supported by this storage target.",
+  "admin.storage.assetsOnly": "Assets Only",
   "admin.storage.cancelSetup": "Cancel",
   "admin.storage.cancelSetup": "Cancel",
   "admin.storage.config": "Configuration",
   "admin.storage.config": "Configuration",
   "admin.storage.contentTypeDocuments": "Documents",
   "admin.storage.contentTypeDocuments": "Documents",
@@ -678,21 +708,21 @@
   "admin.storage.sync": "Synchronization",
   "admin.storage.sync": "Synchronization",
   "admin.storage.syncDirBi": "Bi-directional",
   "admin.storage.syncDirBi": "Bi-directional",
   "admin.storage.syncDirBiHint": "In bi-directional mode, content is first pulled from the storage target. Any newer content overwrites local content. New content since last sync is then pushed to the storage target, overwriting any content on target if present.",
   "admin.storage.syncDirBiHint": "In bi-directional mode, content is first pulled from the storage target. Any newer content overwrites local content. New content since last sync is then pushed to the storage target, overwriting any content on target if present.",
-  "admin.storage.syncDirection": "Sync Direction",
-  "admin.storage.syncDirectionSubtitle": "Choose how content synchronization is handled for this storage target.",
   "admin.storage.syncDirPull": "Pull from target",
   "admin.storage.syncDirPull": "Pull from target",
   "admin.storage.syncDirPullHint": "Content is always pulled from the storage target, overwriting any local content which already exists. This choice is usually reserved for single-use content import. Caution with this option as any local content will always be overwritten!",
   "admin.storage.syncDirPullHint": "Content is always pulled from the storage target, overwriting any local content which already exists. This choice is usually reserved for single-use content import. Caution with this option as any local content will always be overwritten!",
   "admin.storage.syncDirPush": "Push to target",
   "admin.storage.syncDirPush": "Push to target",
   "admin.storage.syncDirPushHint": "Content is always pushed to the storage target, overwriting any existing content. This is safest choice for backup scenarios.",
   "admin.storage.syncDirPushHint": "Content is always pushed to the storage target, overwriting any existing content. This is safest choice for backup scenarios.",
+  "admin.storage.syncDirection": "Sync Direction",
+  "admin.storage.syncDirectionSubtitle": "Choose how content synchronization is handled for this storage target.",
   "admin.storage.syncSchedule": "Sync Schedule",
   "admin.storage.syncSchedule": "Sync Schedule",
   "admin.storage.syncScheduleCurrent": "Currently set to every {schedule}.",
   "admin.storage.syncScheduleCurrent": "Currently set to every {schedule}.",
   "admin.storage.syncScheduleDefault": "The default is every {schedule}.",
   "admin.storage.syncScheduleDefault": "The default is every {schedule}.",
   "admin.storage.syncScheduleHint": "For performance reasons, this storage target synchronize changes on an interval-based schedule, instead of on every change. Define at which interval should the synchronization occur.",
   "admin.storage.syncScheduleHint": "For performance reasons, this storage target synchronize changes on an interval-based schedule, instead of on every change. Define at which interval should the synchronization occur.",
   "admin.storage.targetConfig": "Target Configuration",
   "admin.storage.targetConfig": "Target Configuration",
-  "admin.storage.targets": "Targets",
   "admin.storage.targetState": "This storage target is {state}",
   "admin.storage.targetState": "This storage target is {state}",
   "admin.storage.targetStateActive": "active",
   "admin.storage.targetStateActive": "active",
   "admin.storage.targetStateInactive": "inactive",
   "admin.storage.targetStateInactive": "inactive",
+  "admin.storage.targets": "Targets",
   "admin.storage.title": "Storage",
   "admin.storage.title": "Storage",
   "admin.storage.uninstall": "Uninstall",
   "admin.storage.uninstall": "Uninstall",
   "admin.storage.unsupported": "Unsupported",
   "admin.storage.unsupported": "Unsupported",
@@ -707,8 +737,8 @@
   "admin.system.browser": "Browser",
   "admin.system.browser": "Browser",
   "admin.system.browserHint": "The browser name and version.",
   "admin.system.browserHint": "The browser name and version.",
   "admin.system.checkForUpdates": "Check",
   "admin.system.checkForUpdates": "Check",
-  "admin.system.checkingForUpdates": "Checking for Updates...",
   "admin.system.checkUpdate": "Check / Upgrade",
   "admin.system.checkUpdate": "Check / Upgrade",
+  "admin.system.checkingForUpdates": "Checking for Updates...",
   "admin.system.client": "Client",
   "admin.system.client": "Client",
   "admin.system.clientCookies": "Cookies Support",
   "admin.system.clientCookies": "Cookies Support",
   "admin.system.clientCookiesHint": "Whether cookies are enabled on this browser.",
   "admin.system.clientCookiesHint": "Whether cookies are enabled on this browser.",
@@ -768,8 +798,8 @@
   "admin.terminal.clear": "Clear",
   "admin.terminal.clear": "Clear",
   "admin.terminal.command": "Command",
   "admin.terminal.command": "Command",
   "admin.terminal.connect": "Connect",
   "admin.terminal.connect": "Connect",
-  "admin.terminal.connected": "Connected.",
   "admin.terminal.connectError": "Connection Error:",
   "admin.terminal.connectError": "Connection Error:",
+  "admin.terminal.connected": "Connected.",
   "admin.terminal.connecting": "Connecting to server...",
   "admin.terminal.connecting": "Connecting to server...",
   "admin.terminal.disconnect": "Disconnect",
   "admin.terminal.disconnect": "Disconnect",
   "admin.terminal.disconnected": "Disconnected.",
   "admin.terminal.disconnected": "Disconnected.",
@@ -799,10 +829,10 @@
   "admin.theme.downloadName": "Name",
   "admin.theme.downloadName": "Name",
   "admin.theme.downloadThemes": "Download Themes",
   "admin.theme.downloadThemes": "Download Themes",
   "admin.theme.fonts": "Fonts",
   "admin.theme.fonts": "Fonts",
-  "admin.theme.headerColor": "Header Color",
-  "admin.theme.headerColorHint": "The background color for the site top header. Does not apply to the administration area.",
   "admin.theme.headHtmlInjection": "Head HTML Injection",
   "admin.theme.headHtmlInjection": "Head HTML Injection",
   "admin.theme.headHtmlInjectionHint": "HTML code to be injected just before the closing head tag. Usually for script tags.",
   "admin.theme.headHtmlInjectionHint": "HTML code to be injected just before the closing head tag. Usually for script tags.",
+  "admin.theme.headerColor": "Header Color",
+  "admin.theme.headerColorHint": "The background color for the site top header. Does not apply to the administration area.",
   "admin.theme.iconset": "Icon Set",
   "admin.theme.iconset": "Icon Set",
   "admin.theme.iconsetHint": "Set of icons to use for the sidebar navigation.",
   "admin.theme.iconsetHint": "Set of icons to use for the sidebar navigation.",
   "admin.theme.layout": "Layout",
   "admin.theme.layout": "Layout",
@@ -834,9 +864,9 @@
   "admin.users.appearance": "Site Appearance",
   "admin.users.appearance": "Site Appearance",
   "admin.users.assignGroup": "Assign Group",
   "admin.users.assignGroup": "Assign Group",
   "admin.users.auth": "Authentication",
   "admin.users.auth": "Authentication",
-  "admin.users.authentication": "Authentication",
   "admin.users.authProvider": "Provider",
   "admin.users.authProvider": "Provider",
   "admin.users.authProviderId": "Provider Id",
   "admin.users.authProviderId": "Provider Id",
+  "admin.users.authentication": "Authentication",
   "admin.users.ban": "Ban User",
   "admin.users.ban": "Ban User",
   "admin.users.banHint": "Block the user from signing in and invalidate any active sessions.",
   "admin.users.banHint": "Block the user from signing in and invalidate any active sessions.",
   "admin.users.banned": "Banned",
   "admin.users.banned": "Banned",
@@ -844,10 +874,10 @@
   "admin.users.changePassword": "Change Password",
   "admin.users.changePassword": "Change Password",
   "admin.users.changePasswordHint": "Change the user password. Note that the current password cannot be recovered.",
   "admin.users.changePasswordHint": "Change the user password. Note that the current password cannot be recovered.",
   "admin.users.create": "Create User",
   "admin.users.create": "Create User",
-  "admin.users.createdAt": "Created {date}",
   "admin.users.createInvalidData": "Cannot create user as some fields are invalid or missing.",
   "admin.users.createInvalidData": "Cannot create user as some fields are invalid or missing.",
   "admin.users.createKeepOpened": "Keep dialog opened after create",
   "admin.users.createKeepOpened": "Keep dialog opened after create",
   "admin.users.createSuccess": "User created successfully!",
   "admin.users.createSuccess": "User created successfully!",
+  "admin.users.createdAt": "Created {date}",
   "admin.users.darkMode": "Dark Mode",
   "admin.users.darkMode": "Dark Mode",
   "admin.users.darkModeHint": "Display the user interface using dark mode.",
   "admin.users.darkModeHint": "Display the user interface using dark mode.",
   "admin.users.dateFormat": "Date Format",
   "admin.users.dateFormat": "Date Format",
@@ -868,8 +898,8 @@
   "admin.users.groupAlreadyAssigned": "User is already assigned to this group.",
   "admin.users.groupAlreadyAssigned": "User is already assigned to this group.",
   "admin.users.groupAssign": "Assign",
   "admin.users.groupAssign": "Assign",
   "admin.users.groupAssignNotice": "Note that you cannot assign users to the Administrators or Guests groups from this panel.",
   "admin.users.groupAssignNotice": "Note that you cannot assign users to the Administrators or Guests groups from this panel.",
-  "admin.users.groups": "Groups",
   "admin.users.groupSelected": "Assign to {group}",
   "admin.users.groupSelected": "Assign to {group}",
+  "admin.users.groups": "Groups",
   "admin.users.groupsMissing": "You must assign the user to at least 1 group.",
   "admin.users.groupsMissing": "You must assign the user to at least 1 group.",
   "admin.users.groupsSelected": "Assign to {count} groups",
   "admin.users.groupsSelected": "Assign to {count} groups",
   "admin.users.id": "ID",
   "admin.users.id": "ID",
@@ -1015,13 +1045,13 @@
   "admin.webhooks.eventNewComment": "Post a new comment",
   "admin.webhooks.eventNewComment": "Post a new comment",
   "admin.webhooks.eventRenameAsset": "Rename / move an asset",
   "admin.webhooks.eventRenameAsset": "Rename / move an asset",
   "admin.webhooks.eventRenamePage": "Rename / move a page",
   "admin.webhooks.eventRenamePage": "Rename / move a page",
-  "admin.webhooks.events": "Events",
-  "admin.webhooks.eventsMissing": "You must select at least 1 event.",
-  "admin.webhooks.eventsSelected": "No event selected | 1 event selected | {count} events selected",
   "admin.webhooks.eventUploadAsset": "Upload a new asset",
   "admin.webhooks.eventUploadAsset": "Upload a new asset",
   "admin.webhooks.eventUserJoin": "Create / register a new user",
   "admin.webhooks.eventUserJoin": "Create / register a new user",
   "admin.webhooks.eventUserLogin": "User logins",
   "admin.webhooks.eventUserLogin": "User logins",
   "admin.webhooks.eventUserLogout": "User logouts",
   "admin.webhooks.eventUserLogout": "User logouts",
+  "admin.webhooks.events": "Events",
+  "admin.webhooks.eventsMissing": "You must select at least 1 event.",
+  "admin.webhooks.eventsSelected": "No event selected | 1 event selected | {count} events selected",
   "admin.webhooks.includeContent": "Include Content",
   "admin.webhooks.includeContent": "Include Content",
   "admin.webhooks.includeContentHint": "Should the payload include content (e.g. the full page body). Make sure that your remote endpoint can accept large payloads!",
   "admin.webhooks.includeContentHint": "Should the payload include content (e.g. the full page body). Make sure that your remote endpoint can accept large payloads!",
   "admin.webhooks.includeMetadata": "Include Metadata",
   "admin.webhooks.includeMetadata": "Include Metadata",
@@ -1072,8 +1102,8 @@
   "auth.errors.missingUsername": "Username is missing.",
   "auth.errors.missingUsername": "Username is missing.",
   "auth.errors.missingVerifyPassword": "Password Verification is missing.",
   "auth.errors.missingVerifyPassword": "Password Verification is missing.",
   "auth.errors.notYetAuthorized": "You have not been authorized to login to this site yet.",
   "auth.errors.notYetAuthorized": "You have not been authorized to login to this site yet.",
-  "auth.errors.passwordsNotMatch": "Passwords do not match.",
   "auth.errors.passwordTooShort": "Password is too short.",
   "auth.errors.passwordTooShort": "Password is too short.",
+  "auth.errors.passwordsNotMatch": "Passwords do not match.",
   "auth.errors.register": "One or more fields are invalid.",
   "auth.errors.register": "One or more fields are invalid.",
   "auth.errors.tfaMissing": "Missing or incomplete security code.",
   "auth.errors.tfaMissing": "Missing or incomplete security code.",
   "auth.errors.tooManyAttempts": "Too many attempts!",
   "auth.errors.tooManyAttempts": "Too many attempts!",
@@ -1109,10 +1139,10 @@
   "auth.passwordTooShort": "Password is too short.",
   "auth.passwordTooShort": "Password is too short.",
   "auth.pleaseWait": "Please wait",
   "auth.pleaseWait": "Please wait",
   "auth.registerCheckEmail": "Check your emails to activate your account.",
   "auth.registerCheckEmail": "Check your emails to activate your account.",
-  "auth.registering": "Creating account...",
   "auth.registerSubTitle": "Fill-in the form below to create an account.",
   "auth.registerSubTitle": "Fill-in the form below to create an account.",
   "auth.registerSuccess": "Account created successfully!",
   "auth.registerSuccess": "Account created successfully!",
   "auth.registerTitle": "Create an account",
   "auth.registerTitle": "Create an account",
+  "auth.registering": "Creating account...",
   "auth.selectAuthProvider": "Sign in with",
   "auth.selectAuthProvider": "Sign in with",
   "auth.sendResetPassword": "Reset Password",
   "auth.sendResetPassword": "Reset Password",
   "auth.signingIn": "Signing In...",
   "auth.signingIn": "Signing In...",
@@ -1153,6 +1183,7 @@
   "common.actions.moveTo": "Move To",
   "common.actions.moveTo": "Move To",
   "common.actions.new": "New",
   "common.actions.new": "New",
   "common.actions.newFolder": "New Folder",
   "common.actions.newFolder": "New Folder",
+  "common.actions.newPage": "New Page",
   "common.actions.ok": "OK",
   "common.actions.ok": "OK",
   "common.actions.optimize": "Optimize",
   "common.actions.optimize": "Optimize",
   "common.actions.page": "Page",
   "common.actions.page": "Page",
@@ -1188,8 +1219,8 @@
   "common.comments.newPlaceholder": "Write a new comment...",
   "common.comments.newPlaceholder": "Write a new comment...",
   "common.comments.none": "No comments yet.",
   "common.comments.none": "No comments yet.",
   "common.comments.postComment": "Post Comment",
   "common.comments.postComment": "Post Comment",
-  "common.comments.postingAs": "Posting as {name}",
   "common.comments.postSuccess": "New comment posted successfully.",
   "common.comments.postSuccess": "New comment posted successfully.",
+  "common.comments.postingAs": "Posting as {name}",
   "common.comments.sdTitle": "Talk",
   "common.comments.sdTitle": "Talk",
   "common.comments.title": "Comments",
   "common.comments.title": "Comments",
   "common.comments.updateComment": "Update Comment",
   "common.comments.updateComment": "Update Comment",
@@ -1336,8 +1367,8 @@
   "editor.assets.folderNameNamingRulesLink": "naming rules",
   "editor.assets.folderNameNamingRulesLink": "naming rules",
   "editor.assets.headerActions": "Actions",
   "editor.assets.headerActions": "Actions",
   "editor.assets.headerAdded": "Added",
   "editor.assets.headerAdded": "Added",
-  "editor.assets.headerFilename": "Filename",
   "editor.assets.headerFileSize": "File Size",
   "editor.assets.headerFileSize": "File Size",
+  "editor.assets.headerFilename": "Filename",
   "editor.assets.headerId": "ID",
   "editor.assets.headerId": "ID",
   "editor.assets.headerType": "Type",
   "editor.assets.headerType": "Type",
   "editor.assets.imageAlign": "Image Alignment",
   "editor.assets.imageAlign": "Image Alignment",
@@ -1375,22 +1406,33 @@
   "editor.conflict.whatToDo": "What do you want to do?",
   "editor.conflict.whatToDo": "What do you want to do?",
   "editor.conflict.whatToDoLocal": "Use your current local version and ignore the latest changes.",
   "editor.conflict.whatToDoLocal": "Use your current local version and ignore the latest changes.",
   "editor.conflict.whatToDoRemote": "Use the remote version (latest) and discard your changes.",
   "editor.conflict.whatToDoRemote": "Use the remote version (latest) and discard your changes.",
+  "editor.markup.admonitionDanger": "Danger / Important Admonition",
+  "editor.markup.admonitionInfo": "Info / Note Admonition",
+  "editor.markup.admonitionSuccess": "Tip / Success Admonition",
+  "editor.markup.admonitionWarning": "Warning Admonition",
   "editor.markup.blockquote": "Blockquote",
   "editor.markup.blockquote": "Blockquote",
+  "editor.markup.blockquoteAdmonitions": "Blockquote / Admonition",
   "editor.markup.blockquoteError": "Error Blockquote",
   "editor.markup.blockquoteError": "Error Blockquote",
   "editor.markup.blockquoteInfo": "Info Blockquote",
   "editor.markup.blockquoteInfo": "Info Blockquote",
   "editor.markup.blockquoteSuccess": "Success Blockquote",
   "editor.markup.blockquoteSuccess": "Success Blockquote",
   "editor.markup.blockquoteWarning": "Warning Blockquote",
   "editor.markup.blockquoteWarning": "Warning Blockquote",
   "editor.markup.bold": "Bold",
   "editor.markup.bold": "Bold",
   "editor.markup.distractionFreeMode": "Distraction Free Mode",
   "editor.markup.distractionFreeMode": "Distraction Free Mode",
+  "editor.markup.header": "Header",
+  "editor.markup.headerLevel": "Header {level}",
   "editor.markup.heading": "Heading {level}",
   "editor.markup.heading": "Heading {level}",
-  "editor.markup.horizontalBar": "Horizontal Bar",
   "editor.markup.inlineCode": "Inline Code",
   "editor.markup.inlineCode": "Inline Code",
   "editor.markup.insertAssets": "Insert Assets",
   "editor.markup.insertAssets": "Insert Assets",
   "editor.markup.insertBlock": "Insert Block",
   "editor.markup.insertBlock": "Insert Block",
   "editor.markup.insertCodeBlock": "Insert Code Block",
   "editor.markup.insertCodeBlock": "Insert Code Block",
   "editor.markup.insertDiagram": "Insert Diagram",
   "editor.markup.insertDiagram": "Insert Diagram",
+  "editor.markup.insertEmoji": "Insert Emoji",
+  "editor.markup.insertFootnote": "Insert Footnote",
+  "editor.markup.insertHorizontalBar": "Insert Horizontal Bar",
   "editor.markup.insertLink": "Insert Link",
   "editor.markup.insertLink": "Insert Link",
   "editor.markup.insertMathExpression": "Insert Math Expression",
   "editor.markup.insertMathExpression": "Insert Math Expression",
+  "editor.markup.insertTable": "Insert Table",
+  "editor.markup.insertTabset": "Insert Tabset",
   "editor.markup.insertVideoAudio": "Insert Video / Audio",
   "editor.markup.insertVideoAudio": "Insert Video / Audio",
   "editor.markup.italic": "Italic",
   "editor.markup.italic": "Italic",
   "editor.markup.keyboardKey": "Keyboard Key",
   "editor.markup.keyboardKey": "Keyboard Key",
@@ -1401,6 +1443,9 @@
   "editor.markup.subscript": "Subscript",
   "editor.markup.subscript": "Subscript",
   "editor.markup.superscript": "Superscript",
   "editor.markup.superscript": "Superscript",
   "editor.markup.tableHelper": "Table Helper",
   "editor.markup.tableHelper": "Table Helper",
+  "editor.markup.taskList": "Task List",
+  "editor.markup.taskListChecked": "Checked List Item",
+  "editor.markup.taskListUnchecked": "Unchecked List Item",
   "editor.markup.togglePreviewPane": "Hide / Show Preview Pane",
   "editor.markup.togglePreviewPane": "Hide / Show Preview Pane",
   "editor.markup.toggleSpellcheck": "Toggle Spellcheck",
   "editor.markup.toggleSpellcheck": "Toggle Spellcheck",
   "editor.markup.unorderedList": "Unordered List",
   "editor.markup.unorderedList": "Unordered List",
@@ -1464,9 +1509,9 @@
   "editor.props.pageProperties": "Page Properties",
   "editor.props.pageProperties": "Page Properties",
   "editor.props.password": "Password",
   "editor.props.password": "Password",
   "editor.props.passwordHint": "The page must be published and the user must have read access rights.",
   "editor.props.passwordHint": "The page must be published and the user must have read access rights.",
+  "editor.props.publishState": "Publishing State",
   "editor.props.published": "Published",
   "editor.props.published": "Published",
   "editor.props.publishedHint": "Visible to all users with read access.",
   "editor.props.publishedHint": "Visible to all users with read access.",
-  "editor.props.publishState": "Publishing State",
   "editor.props.relationAdd": "Add Relation...",
   "editor.props.relationAdd": "Add Relation...",
   "editor.props.relationAddHint": "Add links to other pages in the footer (e.g. as part of a series of articles)",
   "editor.props.relationAddHint": "Add links to other pages in the footer (e.g. as part of a series of articles)",
   "editor.props.relations": "Relations",
   "editor.props.relations": "Relations",
@@ -1486,6 +1531,7 @@
   "editor.props.title": "Title",
   "editor.props.title": "Title",
   "editor.props.tocMinMaxDepth": "Min/Max Depth",
   "editor.props.tocMinMaxDepth": "Min/Max Depth",
   "editor.props.visibility": "Visibility",
   "editor.props.visibility": "Visibility",
+  "editor.renderPreview": "Render Preview",
   "editor.save.createSuccess": "Page created successfully.",
   "editor.save.createSuccess": "Page created successfully.",
   "editor.save.error": "An error occurred while creating the page",
   "editor.save.error": "An error occurred while creating the page",
   "editor.save.pleaseWait": "Please wait...",
   "editor.save.pleaseWait": "Please wait...",
@@ -1495,13 +1541,16 @@
   "editor.select.cannotChange": "This cannot be changed once the page is created.",
   "editor.select.cannotChange": "This cannot be changed once the page is created.",
   "editor.select.customView": "or create a custom view?",
   "editor.select.customView": "or create a custom view?",
   "editor.select.title": "Which editor do you want to use for this page?",
   "editor.select.title": "Which editor do you want to use for this page?",
+  "editor.tableEditor.title": "Table Editor",
+  "editor.togglePreviewPane": "Toggle Preview Pane",
+  "editor.toggleScrollSync": "Toggle Scroll Sync",
   "editor.unsaved.body": "You have unsaved changes. Are you sure you want to leave the editor and discard any modifications you made since the last save?",
   "editor.unsaved.body": "You have unsaved changes. Are you sure you want to leave the editor and discard any modifications you made since the last save?",
   "editor.unsaved.title": "Discard Unsaved Changes?",
   "editor.unsaved.title": "Discard Unsaved Changes?",
   "editor.unsavedWarning": "You have unsaved edits. Are you sure you want to leave the editor?",
   "editor.unsavedWarning": "You have unsaved edits. Are you sure you want to leave the editor?",
   "fileman.7zFileType": "7zip Archive",
   "fileman.7zFileType": "7zip Archive",
   "fileman.aacFileType": "AAC Audio File",
   "fileman.aacFileType": "AAC Audio File",
-  "fileman.aifFileType": "AIF Audio File",
   "fileman.aiFileType": "Adobe Illustrator Document",
   "fileman.aiFileType": "Adobe Illustrator Document",
+  "fileman.aifFileType": "AIF Audio File",
   "fileman.apkFileType": "Android Package",
   "fileman.apkFileType": "Android Package",
   "fileman.aviFileType": "AVI Video File",
   "fileman.aviFileType": "AVI Video File",
   "fileman.binFileType": "Binary File",
   "fileman.binFileType": "Binary File",
@@ -1594,6 +1643,7 @@
   "pageSaveDialog.displayModePath": "Browse Using Paths",
   "pageSaveDialog.displayModePath": "Browse Using Paths",
   "pageSaveDialog.displayModeTitle": "Browse Using Titles",
   "pageSaveDialog.displayModeTitle": "Browse Using Titles",
   "pageSaveDialog.title": "Save As...",
   "pageSaveDialog.title": "Save As...",
+  "profile.accessibility": "Accessibility",
   "profile.activity": "Activity",
   "profile.activity": "Activity",
   "profile.appearance": "Site Appearance",
   "profile.appearance": "Site Appearance",
   "profile.appearanceDark": "Dark",
   "profile.appearanceDark": "Dark",
@@ -1613,6 +1663,12 @@
   "profile.avatarUploadHint": "For best results, use a 180x180 image of type JPG or PNG.",
   "profile.avatarUploadHint": "For best results, use a 180x180 image of type JPG or PNG.",
   "profile.avatarUploadSuccess": "Profile picture uploaded successfully.",
   "profile.avatarUploadSuccess": "Profile picture uploaded successfully.",
   "profile.avatarUploadTitle": "Upload your user profile picture.",
   "profile.avatarUploadTitle": "Upload your user profile picture.",
+  "profile.cvd": "Color Vision Deficiency",
+  "profile.cvdDeuteranopia": "Deuteranopia",
+  "profile.cvdHint": "Alter the color scheme of certain UI elements to account for certain color vision dificiencies.",
+  "profile.cvdNone": "None",
+  "profile.cvdProtanopia": "Protanopia",
+  "profile.cvdTritanopia": "Tritanopia",
   "profile.darkMode": "Dark Mode",
   "profile.darkMode": "Dark Mode",
   "profile.darkModeHint": "Change the appareance of the site to a dark theme.",
   "profile.darkModeHint": "Change the appareance of the site to a dark theme.",
   "profile.dateFormat": "Date Format",
   "profile.dateFormat": "Date Format",
@@ -1664,8 +1720,8 @@
   "tags.noResults": "Couldn't find any page with the selected tags.",
   "tags.noResults": "Couldn't find any page with the selected tags.",
   "tags.noResultsWithFilter": "Couldn't find any page matching the current filtering options.",
   "tags.noResultsWithFilter": "Couldn't find any page matching the current filtering options.",
   "tags.orderBy": "Order By",
   "tags.orderBy": "Order By",
-  "tags.orderByField.creationDate": "Creation Date",
   "tags.orderByField.ID": "ID",
   "tags.orderByField.ID": "ID",
+  "tags.orderByField.creationDate": "Creation Date",
   "tags.orderByField.lastModified": "Last Modified",
   "tags.orderByField.lastModified": "Last Modified",
   "tags.orderByField.path": "Path",
   "tags.orderByField.path": "Path",
   "tags.orderByField.title": "Title",
   "tags.orderByField.title": "Title",
@@ -1677,48 +1733,5 @@
   "welcome.admin": "Administration Area",
   "welcome.admin": "Administration Area",
   "welcome.createHome": "Create the homepage",
   "welcome.createHome": "Create the homepage",
   "welcome.subtitle": "Let's get started...",
   "welcome.subtitle": "Let's get started...",
-  "welcome.title": "Welcome to Wiki.js!",
-  "admin.editors.useRenderingPipeline": "Uses the rendering pipeline.",
-  "admin.editors.markdown.allowHTML": "Allow HTML",
-  "admin.editors.markdown.allowHTMLHint": "Allow HTML tags in content.",
-  "admin.editors.markdown.linkify": "Auto-linking",
-  "admin.editors.markdown.linkifyHint": "Automatically convert URLs into clickable links.",
-  "admin.editors.markdown.lineBreaks": "Auto Line Breaks",
-  "admin.editors.markdown.lineBreaksHint": "Automatically add linebreaks within paragraphs.",
-  "admin.editors.markdown.typographer": "Typographer",
-  "admin.editors.markdown.typographerHint": "Enable some language-neutral replacement + quotes beautification.",
-  "admin.editors.markdown.underline": "Underline Emphasis",
-  "admin.editors.markdown.underlineHint": "Enable text underlining by using _underline_ syntax.",
-  "admin.editors.markdown.tabWidth": "Code Block Tab Width",
-  "admin.editors.markdown.tabWidthHint": "Amount of spaces for each tab in code blocks.",
-  "admin.editors.markdown.latexEngine": "LaTeX Engine",
-  "admin.editors.markdown.latexEngineHint": "Which engine to use to process TeX/LaTeX expressions.",
-  "admin.editors.markdown.general": "General",
-  "admin.editors.markdown.plantuml": "PlantUML",
-  "admin.editors.markdown.kroki": "Kroki",
-  "admin.editors.markdown.multimdTable": "MultiMarkdown Table",
-  "admin.editors.markdown.multimdTableHint": "Enable support for MultiMarkdown Table features.",
-  "admin.editors.markdown.plantumlHint": "Enable PlantUML Parser",
-  "admin.editors.markdown.krokiHint": "Enable Kroki Diagrams Parser",
-  "admin.editors.markdown.krokiServerUrl": "Kroki Server URL",
-  "admin.editors.markdown.krokiServerUrlHint": "URL to the Kroki server used for image generation.",
-  "admin.editors.markdown.plantumlServerUrl": "PlantUML Server URL",
-  "admin.editors.markdown.plantumlServerUrlHint": "URL to the PlantUML server used for image generation.",
-  "admin.editors.markdown.quotes": "Quotes Style",
-  "admin.editors.markdown.quotesHint": "When typographer is enabled. Double + single quotes replacement pairs. e.g. «»„“ for Russian, „“‚‘ for German, etc.",
-  "admin.editors.saveSuccess": "Editors state saved successfully.",
-  "admin.editors.markdown.saveSuccess": "Markdown editor configuration saved successfully.",
-  "editor.markup.insertTable": "Insert Table",
-  "editor.markup.header": "Header",
-  "profile.cvdHint": "Alter the color scheme of certain UI elements to account for certain color vision dificiencies.",
-  "profile.cvd": "Color Vision Deficiency",
-  "profile.cvdNone": "None",
-  "profile.cvdProtanopia": "Protanopia",
-  "profile.cvdTritanopia": "Tritanopia",
-  "profile.cvdDeuteranopia": "Deuteranopia",
-  "profile.accessibility": "Accessibility",
-  "editor.toggleScrollSync": "Toggle Scroll Sync",
-  "editor.togglePreviewPane": "Toggle Preview Pane",
-  "editor.renderPreview": "Render Preview",
-  "editor.tableEditor.title": "Table Editor"
+  "welcome.title": "Welcome to Wiki.js!"
 }
 }

+ 15 - 227
ux/src/pages/Index.vue

@@ -149,105 +149,13 @@ q-page.column
               icon='las la-thumbs-up'
               icon='las la-thumbs-up'
               color='secondary'
               color='secondary'
             )
             )
-    .page-actions.column.items-stretch.order-last
-      q-btn.q-py-md(
-        flat
-        icon='las la-pen-nib'
-        color='deep-orange-9'
-        aria-label='Page Properties'
-        @click='togglePageProperties'
-        )
-        q-tooltip(anchor='center left' self='center right') Page Properties
-      q-btn.q-py-md(
-        flat
-        icon='las la-project-diagram'
-        color='deep-orange-9'
-        aria-label='Page Data'
-        @click='togglePageData'
-        disable
-        v-if='flagsStore.experimental'
-        )
-        q-tooltip(anchor='center left' self='center right') Page Data
-      q-separator.q-my-sm(inset)
-      q-btn.q-py-sm(
-        flat
-        icon='las la-ellipsis-h'
-        color='grey'
-        aria-label='Page Actions'
-        )
-        q-tooltip(anchor='center left' self='center right') Page Actions
-        q-menu(
-          anchor='top left'
-          self='top right'
-          auto-close
-          transition-show='jump-left'
-          )
-          q-list(padding, style='min-width: 225px;')
-            q-item(clickable)
-              q-item-section.items-center(avatar)
-                q-icon(color='deep-orange-9', name='las la-history', size='sm')
-              q-item-section
-                q-item-label View History
-            q-item(clickable)
-              q-item-section.items-center(avatar)
-                q-icon(color='deep-orange-9', name='las la-code', size='sm')
-              q-item-section
-                q-item-label View Source
-            q-item(clickable)
-              q-item-section.items-center(avatar)
-                q-icon(color='deep-orange-9', name='las la-atom', size='sm')
-              q-item-section
-                q-item-label Convert Page
-            q-item(clickable)
-              q-item-section.items-center(avatar)
-                q-icon(color='deep-orange-9', name='las la-magic', size='sm')
-              q-item-section
-                q-item-label Re-render Page
-            q-item(clickable)
-              q-item-section.items-center(avatar)
-                q-icon(color='deep-orange-9', name='las la-sun', size='sm')
-              q-item-section
-                q-item-label View Backlinks
-      q-space
-      q-btn.q-py-sm(
-        flat
-        icon='las la-copy'
-        color='grey'
-        aria-label='Duplicate Page'
-        @click='duplicatePage'
-        )
-        q-tooltip(anchor='center left' self='center right') Duplicate Page
-      q-btn.q-py-sm(
-        flat
-        icon='las la-share'
-        color='grey'
-        aria-label='Rename / Move Page'
-        @click='renamePage'
-        )
-        q-tooltip(anchor='center left' self='center right') Rename / Move Page
-      q-btn.q-py-sm(
-        flat
-        icon='las la-trash'
-        color='grey'
-        aria-label='Delete Page'
-        @click='deletePage'
-        )
-        q-tooltip(anchor='center left' self='center right') Delete Page
-
-  q-dialog(
-    v-model='state.showSideDialog'
-    position='right'
-    full-height
-    transition-show='jump-left'
-    transition-hide='jump-right'
-    class='floating-sidepanel'
-    no-shake
-    )
-    component(:is='sideDialogs[state.sideDialogComponent]')
+    page-actions-col
+
+  side-dialog
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { useMeta, useQuasar, setCssVar } from 'quasar'
+import { useMeta, useQuasar } from 'quasar'
 import { computed, defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue'
 import { computed, defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue'
 import { useRouter, useRoute } from 'vue-router'
 import { useRouter, useRoute } from 'vue-router'
 import { useI18n } from 'vue-i18n'
 import { useI18n } from 'vue-i18n'
@@ -261,19 +169,10 @@ import { useSiteStore } from 'src/stores/site'
 // COMPONENTS
 // COMPONENTS
 
 
 import LoadingGeneric from 'src/components/LoadingGeneric.vue'
 import LoadingGeneric from 'src/components/LoadingGeneric.vue'
+import PageActionsCol from 'src/components/PageActionsCol.vue'
 import PageHeader from 'src/components/PageHeader.vue'
 import PageHeader from 'src/components/PageHeader.vue'
-import PageTags from '../components/PageTags.vue'
-
-const sideDialogs = {
-  PageDataDialog: defineAsyncComponent({
-    loader: () => import('../components/PageDataDialog.vue'),
-    loadingComponent: LoadingGeneric
-  }),
-  PagePropertiesDialog: defineAsyncComponent({
-    loader: () => import('../components/PagePropertiesDialog.vue'),
-    loadingComponent: LoadingGeneric
-  })
-}
+import PageTags from 'src/components/PageTags.vue'
+import SideDialog from 'src/components/SideDialog.vue'
 
 
 const editorComponents = {
 const editorComponents = {
   markdown: defineAsyncComponent({
   markdown: defineAsyncComponent({
@@ -364,10 +263,14 @@ watch(() => route.path, async (newValue) => {
     await pageStore.pageLoad({ path: newValue })
     await pageStore.pageLoad({ path: newValue })
   } catch (err) {
   } catch (err) {
     if (err.message === 'ERR_PAGE_NOT_FOUND') {
     if (err.message === 'ERR_PAGE_NOT_FOUND') {
-      $q.notify({
-        type: 'negative',
-        message: 'This page does not exist (yet)!'
-      })
+      if (newValue === '/') {
+        siteStore.overlay = 'Welcome'
+      } else {
+        $q.notify({
+          type: 'negative',
+          message: 'This page does not exist (yet)!'
+        })
+      }
     } else {
     } else {
       $q.notify({
       $q.notify({
         type: 'negative',
         type: 'negative',
@@ -382,58 +285,6 @@ watch(() => pageStore.tocDepth, () => { refreshTocExpanded() })
 
 
 // METHODS
 // METHODS
 
 
-function togglePageProperties () {
-  state.sideDialogComponent = 'PagePropertiesDialog'
-  state.showSideDialog = true
-}
-
-function togglePageData () {
-  state.sideDialogComponent = 'PageDataDialog'
-  state.showSideDialog = true
-}
-
-function duplicatePage () {
-  $q.dialog({
-    component: defineAsyncComponent(() => import('../components/TreeBrowserDialog.vue')),
-    componentProps: {
-      mode: 'duplicatePage',
-      folderPath: '',
-      itemId: pageStore.id,
-      itemTitle: pageStore.title,
-      itemFileName: pageStore.path
-    }
-  }).onOk(() => {
-    // TODO: change route to new location
-  })
-}
-
-function renamePage () {
-  $q.dialog({
-    component: defineAsyncComponent(() => import('../components/TreeBrowserDialog.vue')),
-    componentProps: {
-      mode: 'renamePage',
-      folderPath: '',
-      itemId: pageStore.id,
-      itemTitle: pageStore.title,
-      itemFileName: pageStore.path
-    }
-  }).onOk(() => {
-    // TODO: change route to new location
-  })
-}
-
-function deletePage () {
-  $q.dialog({
-    component: defineAsyncComponent(() => import('../components/PageDeleteDialog.vue')),
-    componentProps: {
-      pageId: pageStore.id,
-      pageName: pageStore.title
-    }
-  }).onOk(() => {
-    router.replace('/')
-  })
-}
-
 function refreshTocExpanded (baseToc, lvl) {
 function refreshTocExpanded (baseToc, lvl) {
   const toExpand = []
   const toExpand = []
   let isRootNode = false
   let isRootNode = false
@@ -538,16 +389,6 @@ function refreshTocExpanded (baseToc, lvl) {
     }
     }
   }
   }
 }
 }
-.page-actions {
-  flex: 0 0 56px;
-
-  @at-root .body--light & {
-    background-color: $grey-3;
-  }
-  @at-root .body--dark & {
-    background-color: $dark-4;
-  }
-}
 
 
 .floating-syncpanel {
 .floating-syncpanel {
   .q-dialog__inner {
   .q-dialog__inner {
@@ -570,59 +411,6 @@ function refreshTocExpanded (baseToc, lvl) {
   }
   }
 }
 }
 
 
-.floating-sidepanel {
-  .q-dialog__inner {
-    right: 24px;
-
-    .q-card {
-      border-radius: 4px !important;
-      min-width: 450px;
-
-      .q-card__section {
-        border-radius: 0;
-      }
-    }
-  }
-
-  .alt-card {
-    @at-root .body--light & {
-      background-color: $grey-2;
-      border-top: 1px solid $grey-4;
-      box-shadow: inset 0 1px 0 0 #FFF, inset 0 -1px 0 0 #FFF;
-      border-bottom: 1px solid $grey-4;
-    }
-    @at-root .body--dark & {
-      background-color: $dark-4;
-      border-top: 1px solid lighten($dark-3, 8%);
-      box-shadow: inset 0 1px 0 0 $dark-6, inset 0 -1px 0 0 $dark-6;
-      border-bottom: 1px solid lighten($dark-3, 8%);
-    }
-  }
-
-  &-quickaccess {
-    width: 40px;
-    border-radius: 4px !important;
-    background-color: rgba(0,0,0,.75);
-    backdrop-filter: blur(5px);
-    color: #FFF;
-    position: fixed;
-    right: 486px;
-    top: 74px;
-    z-index: -1;
-    display: flex;
-    flex-direction: column;
-    box-shadow: 0 0 5px 0 rgba(0,0,0,.5) !important;
-
-    @at-root .q-transition--jump-left-enter-active & {
-      display: none !important;
-    }
-
-    @at-root .q-transition--jump-right-leave-active & {
-      display: none !important;
-    }
-  }
-}
-
 .q-card {
 .q-card {
   @at-root .body--light & {
   @at-root .body--light & {
     background-color: #FFF;
     background-color: #FFF;

+ 0 - 113
ux/src/pages/Welcome.vue

@@ -1,113 +0,0 @@
-<template lang='pug'>
-.welcome
-  .welcome-bg
-  .welcome-content
-    .welcome-logo
-      img(src='/_assets/logo-wikijs.svg')
-    .welcome-title {{t('welcome.title')}}
-    .welcome-subtitle {{t('welcome.subtitle')}}
-    .welcome-actions
-      q-btn(
-        push
-        color='primary'
-        :label='t(`welcome.createHome`)'
-        icon='las la-plus'
-        no-caps
-        href='/_edit'
-      )
-      q-btn(
-        push
-        color='primary'
-        :label='t(`welcome.admin`)'
-        icon='las la-cog'
-        no-caps
-        to='/_admin'
-      )
-
-</template>
-
-<script setup>
-import { useI18n } from 'vue-i18n'
-import { useMeta } from 'quasar'
-
-// I18N
-
-const { t } = useI18n()
-
-// META
-
-useMeta({
-  title: t('welcome.title')
-})
-
-</script>
-
-<style lang="scss">
-  .welcome {
-    background: $dark-6 radial-gradient(ellipse, $dark-4, $dark-6);
-    color: #FFF;
-    height: 100vh;
-
-    &-bg {
-      position: absolute;
-      top: 50%;
-      left: 50%;
-      width: 320px;
-      height: 320px;
-      background: linear-gradient(0, $purple-6 50%, $blue-9 50%);
-      border-radius: 50%;
-      filter: blur(80px);
-      transform: translate(-50%, -50%);
-    }
-
-    &-content {
-      position: absolute;
-      top: 50%;
-      left: 50%;
-      transform: translate(-50%, -50%);
-      display: flex;
-      flex-direction: column;
-      justify-content: center;
-      align-items: center;
-      width: 90vw;
-    }
-
-    &-logo {
-      user-select: none;
-
-      > img {
-        height: 200px;
-        user-select: none;
-      }
-    }
-
-    &-title {
-      font-size: 4rem;
-      font-weight: 500;
-      line-height: 4rem;
-      text-align: center;
-
-      @media (max-width: $breakpoint-md-max) {
-        font-size: 2.5rem;
-        line-height: 2.5rem;
-      }
-    }
-
-    &-subtitle {
-      font-size: 1.2rem;
-      font-weight: 500;
-      color: $purple-2;
-      line-height: 1.2rem;
-      margin-top: 1rem;
-    }
-
-    &-actions {
-      margin-top: 2rem;
-      text-align: center;
-
-      > .q-btn {
-        margin: 0 5px 5px 5px;
-      }
-    }
-  }
-</style>

+ 2 - 6
ux/src/router/routes.js

@@ -4,8 +4,8 @@ const routes = [
     path: '/',
     path: '/',
     component: () => import('../layouts/MainLayout.vue'),
     component: () => import('../layouts/MainLayout.vue'),
     children: [
     children: [
-      { path: '', component: () => import('../pages/Index.vue') }
-      // { path: 'n/:editor?', component: () => import('../pages/Index.vue') }
+      { path: '', component: () => import('../pages/Index.vue') },
+      { path: '_create/:editor?', component: () => import('../pages/Index.vue') }
     ]
     ]
   },
   },
   {
   {
@@ -61,10 +61,6 @@ const routes = [
       { path: 'flags', component: () => import('pages/AdminFlags.vue') }
       { path: 'flags', component: () => import('pages/AdminFlags.vue') }
     ]
     ]
   },
   },
-  {
-    path: '/_welcome',
-    component: () => import('pages/Welcome.vue')
-  },
   {
   {
     path: '/_error/:action?',
     path: '/_error/:action?',
     component: () => import('pages/ErrorGeneric.vue')
     component: () => import('pages/ErrorGeneric.vue')

+ 2 - 2
ux/src/stores/page.js

@@ -191,7 +191,7 @@ export const usePageStore = defineStore('page', {
       // -> Page Data
       // -> Page Data
       this.id = 0
       this.id = 0
       this.locale = locale || this.locale
       this.locale = locale || this.locale
-      if (path) {
+      if (path || path === '') {
         this.path = path
         this.path = path
       } else {
       } else {
         this.path = this.path.length < 2 ? 'new-page' : `${this.path}/new-page`
         this.path = this.path.length < 2 ? 'new-page' : `${this.path}/new-page`
@@ -199,7 +199,7 @@ export const usePageStore = defineStore('page', {
       this.title = ''
       this.title = ''
       this.description = ''
       this.description = ''
       this.icon = 'las la-file-alt'
       this.icon = 'las la-file-alt'
-      this.isPublished = false
+      this.publishState = 'published'
       this.relations = []
       this.relations = []
       this.tags = []
       this.tags = []
 
 

+ 2 - 0
ux/src/stores/site.js

@@ -60,6 +60,8 @@ export const useSiteStore = defineStore('site', {
       width: '9px',
       width: '9px',
       opacity: 1
       opacity: 1
     },
     },
+    sideDialogShown: false,
+    sideDialogComponent: '',
     docsBase: 'https://next.js.wiki/docs'
     docsBase: 'https://next.js.wiki/docs'
   }),
   }),
   getters: {
   getters: {

部分文件因为文件数量过多而无法显示