Procházet zdrojové kódy

feat: sidebar nav editor (wip)

NGPixel před 1 rokem
rodič
revize
269040ed7f

+ 19 - 0
server/locales/en.json

@@ -1683,6 +1683,25 @@
   "history.restore.confirmText": "Are you sure you want to restore this page content as it was on {date}? This version will be copied on top of the current history. As such, newer versions will still be preserved.",
   "history.restore.confirmTitle": "Restore page version?",
   "history.restore.success": "Page version restored succesfully!",
+  "navEdit.editMenuItems": "Edit Menu Items",
+  "navEdit.header": "Header",
+  "navEdit.icon": "Icon",
+  "navEdit.iconHint": "Icon to display to the left of the menu item.",
+  "navEdit.label": "Label",
+  "navEdit.labelHint": "Text to display on the menu item.",
+  "navEdit.link": "Link",
+  "navEdit.nestItem": "Nest Item",
+  "navEdit.openInNewWindow": "Open in New Window",
+  "navEdit.openInNewWindowHint": "Whether the link should open in a new window / tab.",
+  "navEdit.separator": "Separator",
+  "navEdit.target": "Target",
+  "navEdit.targetHint": "Target path or external link to point to.",
+  "navEdit.title": "Edit Navigation",
+  "navEdit.unnestItem": "Unnest Item",
+  "navEdit.visibility": "Visibility",
+  "navEdit.visibilityAll": "Everyone",
+  "navEdit.visibilityHint": "Whether to show the menu item to everyone or just selected groups.",
+  "navEdit.visibilityLimited": "Selected Groups",
   "pageDeleteDialog.confirm": "Are you sure you want to delete the page {name}?",
   "pageDeleteDialog.deleteSuccess": "Page deleted successfully.",
   "pageDeleteDialog.pageId": "Page ID {id}",

+ 27 - 2
ux/package-lock.json

@@ -84,6 +84,8 @@
         "quasar": "2.12.1",
         "slugify": "1.6.6",
         "socket.io-client": "4.7.1",
+        "sortablejs": "1.15.0",
+        "sortablejs-vue3": "1.2.9",
         "tabulator-tables": "5.5.0",
         "tippy.js": "6.3.7",
         "twemoji": "14.0.2",
@@ -6920,8 +6922,26 @@
       }
     },
     "node_modules/sortablejs": {
-      "version": "1.14.0",
-      "license": "MIT"
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz",
+      "integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w=="
+    },
+    "node_modules/sortablejs-vue3": {
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/sortablejs-vue3/-/sortablejs-vue3-1.2.9.tgz",
+      "integrity": "sha512-l0IIBdu+nRIwC2+KOkiavXw5vRfsn6MIPVSVSf7ItBevcuRZ4mVzC7dgnr/Hs/VPH2Q+nF2PYP3FsrnrG+7qCw==",
+      "dependencies": {
+        "sortablejs": "^1.15.0",
+        "vue": "^3.2.37"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://github.com/sponsors/MaxLeiter/"
+      },
+      "peerDependencies": {
+        "sortablejs": "^1.15.0",
+        "vue": "^3.2.25"
+      }
     },
     "node_modules/source-map": {
       "version": "0.6.1",
@@ -7731,6 +7751,11 @@
         "vue": "^3.0.1"
       }
     },
+    "node_modules/vuedraggable/node_modules/sortablejs": {
+      "version": "1.14.0",
+      "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
+      "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="
+    },
     "node_modules/wcwidth": {
       "version": "1.0.1",
       "dev": true,

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
ux/public/_assets/icons/fluent-sidebar-menu.svg


+ 1 - 0
ux/public/_assets/icons/ultraviolet-external-link.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 40 40" width="80px" height="80px"><path fill="#dff0fe" d="M1.5 5.5H34.5V38.5H1.5z"/><path fill="#4788c7" d="M34,6v32H2V6H34 M35,5H1v34h34V5L35,5z"/><path fill="#dff0fe" d="M32.667 11.555H35V19H32.667zM21 5H28.444V7.333H21z"/><path fill="#98ccfd" d="M18.707 16L28.707 6 24.207 1.5 38.5 1.5 38.5 15.793 34 11.293 24 21.293z"/><path fill="#4788c7" d="M38,2v12.586l-3.293-3.293L34,10.586l-0.707,0.707L24,20.586L19.414,16l9.293-9.293L29.414,6 l-0.707-0.707L25.414,2H38 M39,1H23l5,5L18,16l6,6l10-10l5,5V1L39,1z"/><path fill="#4788c7" d="M34.5 21L34.5 21c.275 0 .5-.225.5-.5v-2c0-.275-.225-.5-.5-.5l0 0c-.275 0-.5.225-.5.5v2C34 20.775 34.225 21 34.5 21zM19.5 6h2C21.775 6 22 5.775 22 5.5v0C22 5.225 21.775 5 21.5 5h-2C19.225 5 19 5.225 19 5.5v0C19 5.775 19.225 6 19.5 6z"/></svg>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
ux/public/_assets/icons/ultraviolet-spring.svg


+ 1 - 0
ux/public/_assets/icons/ultraviolet-typography.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 40 40" width="80px" height="80px"><path fill="#dff0fe" d="M28.249,35.5l-2.061-7H13.812l-2.061,7H5.702l10.721-31h7.405l10.475,31H28.249z M15.283,23.5 h9.434L20.51,9.213h-1.022L15.283,23.5z"/><path fill="#4788c7" d="M23.47,5l10.137,30h-4.984l-1.849-6.282L26.562,28h-0.748H14.185h-0.748l-0.211,0.718L11.377,35 H6.404L16.779,5H23.47 M14.615,24h1.337h8.097h1.337l-0.378-1.282L21.096,9.43l-0.211-0.718h-0.748h-0.274h-0.748L18.903,9.43 l-3.911,13.287L14.615,24 M24.187,4h-8.12L5,36h7.125l2.06-7h11.629l2.061,7H35L24.187,4L24.187,4z M15.951,23l3.911-13.287h0.274 L24.048,23H15.951L15.951,23z"/></svg>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
ux/public/_assets/icons/ultraviolet-user-groups.svg


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

@@ -27,6 +27,10 @@ const overlays = {
     loader: () => import('./FileManager.vue'),
     loadingComponent: LoadingGeneric
   }),
+  NavEdit: defineAsyncComponent({
+    loader: () => import('./NavEditOverlay.vue'),
+    loadingComponent: LoadingGeneric
+  }),
   TableEditor: defineAsyncComponent({
     loader: () => import('./TableEditorOverlay.vue'),
     loadingComponent: LoadingGeneric

+ 88 - 0
ux/src/components/NavEditMenu.vue

@@ -0,0 +1,88 @@
+<template lang="pug">
+q-card(style='min-width: 350px')
+  q-card-section.card-header
+    q-icon(name='img:/_assets/icons/fluent-sidebar-menu.svg', left, size='sm')
+    span {{t(`navEdit.title`)}}
+  q-card-section
+    q-btn.full-width(
+      unelevated
+      icon='mdi-playlist-edit'
+      color='deep-orange-9'
+      :label='t(`navEdit.editMenuItems`)'
+      @click='startEditing'
+    )
+  q-separator(inset)
+  q-card-section.q-pb-none.text-body2 Mode
+  q-list(padding)
+    q-item(tag='label')
+      q-item-section(side)
+        q-radio(v-model='state.mode', val='inherit')
+      q-item-section
+        q-item-label Inherit
+        q-item-label(caption) Use the menu items and settings from the parent path.
+    q-item(tag='label')
+      q-item-section(side)
+        q-radio(v-model='state.mode', val='starting')
+      q-item-section
+        q-item-label Override Current + Descendants
+        q-item-label(caption) Set menu items and settings for this path and all children.
+    q-item(tag='label')
+      q-item-section(side)
+        q-radio(v-model='state.mode', val='exact')
+      q-item-section
+        q-item-label Override Current Only
+        q-item-label(caption) Set menu items and settings only for this path.
+  q-card-actions.card-actions
+    q-space
+    q-btn.acrylic-btn(
+      flat
+      :label='t(`common.actions.cancel`)'
+      color='grey'
+      padding='xs md'
+      @click='props.menuHideHandler'
+      )
+    q-btn(
+      unelevated
+      :label='t(`common.actions.save`)'
+      color='positive'
+      padding='xs md'
+      )
+</template>
+
+<script setup>
+import { onMounted, reactive, ref, watch } from 'vue'
+import { useI18n } from 'vue-i18n'
+
+import { useSiteStore } from 'src/stores/site'
+
+// PROPS
+
+const props = defineProps({
+  menuHideHandler: {
+    type: Function,
+    default: () => ({})
+  }
+})
+
+// STORES
+
+const siteStore = useSiteStore()
+
+// I18N
+
+const { t } = useI18n()
+
+// DATA
+
+const state = reactive({
+  mode: 'inherit'
+})
+
+// METHODS
+
+function startEditing () {
+  siteStore.$patch({ overlay: 'NavEdit' })
+  props.menuHideHandler()
+}
+
+</script>

+ 518 - 0
ux/src/components/NavEditOverlay.vue

@@ -0,0 +1,518 @@
+<template lang="pug">
+q-layout(view='hHh lpR fFf', container)
+  q-header.card-header.q-px-md.q-py-sm
+    q-icon(name='img:/_assets/icons/fluent-sidebar-menu.svg', left, size='md')
+    span {{t(`navEdit.editMenuItems`)}}
+    q-space
+    q-btn.q-mr-sm(
+      flat
+      rounded
+      color='white'
+      :aria-label='t(`common.actions.viewDocs`)'
+      icon='las la-question-circle'
+      :href='siteStore.docsBase + `/admin/editors/markdown`'
+      target='_blank'
+      type='a'
+    )
+    q-btn-group(push)
+      q-btn(
+        push
+        color='white'
+        text-color='grey-7'
+        :label='t(`common.actions.cancel`)'
+        :aria-label='t(`common.actions.cancel`)'
+        icon='las la-times'
+        @click='close'
+      )
+      q-btn(
+        push
+        color='positive'
+        text-color='white'
+        :label='t(`common.actions.save`)'
+        :aria-label='t(`common.actions.save`)'
+        icon='las la-check'
+        :disabled='state.loading > 0'
+      )
+
+  q-drawer.bg-dark-6(:model-value='true', :width='295', dark)
+    q-scroll-area.nav-edit(
+      :thumb-style='thumbStyle'
+      :bar-style='barStyle'
+      )
+      sortable(
+        class='q-list q-list--dense q-list--dark nav-edit-list'
+        :list='state.items'
+        item-key='id'
+        :options='sortableOptions'
+        )
+        template(#item='{element}')
+          .nav-edit-item.nav-edit-item-header(
+            v-if='element.type === `header`'
+            :class='state.selected === element.id ? `is-active` : ``'
+            @click='setItem(element)'
+            )
+            q-item-label.text-caption(
+              header
+              ) {{ element.label }}
+            q-space
+            q-item-section(side)
+              q-icon.handle(name='mdi-drag-horizontal', size='sm')
+          q-item.nav-edit-item.nav-edit-item-link(
+            v-else-if='element.type === `link`'
+            :class='{ "is-active": state.selected === element.id, "is-nested": element.isNested }'
+            @click='setItem(element)'
+            clickable
+            )
+            q-item-section(side)
+              q-icon(:name='element.icon', color='white')
+            q-item-section.text-wordbreak-all {{ element.label }}
+            q-item-section(side)
+              q-icon.handle(name='mdi-drag-horizontal', size='sm')
+          .nav-edit-item.nav-edit-item-separator(
+            v-else
+            :class='state.selected === element.id ? `is-active` : ``'
+            @click='setItem(element)'
+            )
+            q-separator(
+              dark
+              inset
+              style='flex: 1; margin-top: 11px;'
+              )
+            q-item-section(side)
+              q-icon.handle(name='mdi-drag-horizontal', size='sm')
+
+      .q-pa-md
+        q-btn.full-width.acrylic-btn(
+          flat
+          color='positive'
+          :label='t(`common.actions.add`)'
+          :aria-label='t(`common.actions.add`)'
+          icon='las la-plus-circle'
+          )
+          q-menu(fit, :offset='[0, 10]')
+            q-list(separator)
+              q-item(clickable)
+                q-item-section(side)
+                  q-icon(name='las la-heading')
+                q-item-section
+                  q-item-label Header
+              q-item(clickable)
+                q-item-section(side)
+                  q-icon(name='las la-link')
+                q-item-section
+                  q-item-label {{t('navEdit.link')}}
+              q-item(clickable)
+                q-item-section(side)
+                  q-icon(name='las la-minus')
+                q-item-section
+                  q-item-label Separator
+              q-item(clickable, style='border-top-width: 5px;')
+                q-item-section(side)
+                  q-icon(name='mdi-import')
+                q-item-section
+                  q-item-label Copy from...
+
+  q-page-container
+    q-page.q-pa-md
+      template(v-if='state.current.type === `header`')
+        q-card.q-pb-sm
+          q-card-section
+            .text-subtitle1 {{t('navEdit.header')}}
+          q-item
+            blueprint-icon(icon='typography')
+            q-item-section
+              q-item-label {{t(`navEdit.label`)}}
+              q-item-label(caption) {{t(`navEdit.labelHint`)}}
+            q-item-section
+              q-input(
+                outlined
+                v-model='state.current.label'
+                dense
+                hide-bottom-space
+                :aria-label='t(`navEdit.label`)'
+                )
+        q-card.q-pa-md.q-mt-md.flex
+          q-space
+          q-btn.acrylic-btn(
+            flat
+            :label='t(`common.actions.delete`)'
+            color='negative'
+            padding='xs md'
+            @click=''
+          )
+
+      template(v-if='state.current.type === `link`')
+        q-card.q-pb-sm
+          q-card-section
+            .text-subtitle1 {{t('navEdit.link')}}
+          q-item
+            blueprint-icon(icon='typography')
+            q-item-section
+              q-item-label {{t(`navEdit.label`)}}
+              q-item-label(caption) {{t(`navEdit.labelHint`)}}
+            q-item-section
+              q-input(
+                outlined
+                v-model='state.current.label'
+                dense
+                hide-bottom-space
+                :aria-label='t(`navEdit.label`)'
+                )
+          q-separator.q-my-sm(inset)
+          q-item
+            blueprint-icon(icon='spring')
+            q-item-section
+              q-item-label {{t(`navEdit.icon`)}}
+              q-item-label(caption) {{t(`navEdit.iconHint`)}}
+            q-item-section
+              q-input(
+                outlined
+                v-model='state.current.icon'
+                dense
+                :aria-label='t(`navEdit.icon`)'
+                )
+                template(#append)
+                  q-icon.cursor-pointer(
+                    name='las la-icons'
+                    color='primary'
+                  )
+          q-separator.q-my-sm(inset)
+          q-item
+            blueprint-icon(icon='link')
+            q-item-section
+              q-item-label {{t(`navEdit.target`)}}
+              q-item-label(caption) {{t(`navEdit.targetHint`)}}
+            q-item-section
+              q-input(
+                outlined
+                v-model='state.current.target'
+                dense
+                hide-bottom-space
+                :aria-label='t(`navEdit.target`)'
+                )
+          q-separator.q-my-sm(inset)
+          q-item(tag='label')
+            blueprint-icon(icon='external-link')
+            q-item-section
+              q-item-label {{t(`navEdit.openInNewWindow`)}}
+              q-item-label(caption) {{t(`navEdit.openInNewWindowHint`)}}
+            q-item-section(avatar)
+              q-toggle(
+                v-model='state.current.openInNewWindow'
+                color='primary'
+                checked-icon='las la-check'
+                unchecked-icon='las la-times'
+                :aria-label='t(`navEdit.openInNewWindow`)'
+                )
+          q-separator.q-my-sm(inset)
+          q-item
+            blueprint-icon(icon='user-groups')
+            q-item-section
+              q-item-label {{t(`navEdit.visibility`)}}
+              q-item-label(caption) {{t(`navEdit.visibilityHint`)}}
+            q-item-section(avatar)
+              q-btn-toggle(
+                v-model='state.current.visibilityLimited'
+                push
+                glossy
+                no-caps
+                toggle-color='primary'
+                :options='visibilityOptions'
+              )
+          q-item(v-if='state.current.visibilityLimited')
+            q-item-section
+            q-item-section
+              q-select(
+                outlined
+                v-model='state.current.visibility'
+                :options='state.groups'
+                option-value='value'
+                option-label='label'
+                emit-value
+                map-options
+                dense
+                options-dense
+                :virtual-scroll-slice-size='1000'
+                :aria-label='t(`admin.general.uploadConflictBehavior`)'
+                )
+
+        q-card.q-pa-md.q-mt-md.flex
+          q-btn.acrylic-btn(
+            v-if='state.current.isNested'
+            flat
+            :label='t(`navEdit.unnestItem`)'
+            icon='mdi-format-indent-decrease'
+            color='teal'
+            padding='xs md'
+            @click='state.current.isNested = false'
+          )
+          q-btn.acrylic-btn(
+            v-else
+            flat
+            :label='t(`navEdit.nestItem`)'
+            icon='mdi-format-indent-increase'
+            color='teal'
+            padding='xs md'
+            @click='state.current.isNested = true'
+          )
+          q-space
+          q-btn.acrylic-btn(
+            flat
+            :label='t(`common.actions.delete`)'
+            color='negative'
+            padding='xs md'
+            @click=''
+          )
+
+      template(v-if='state.current.type === `separator`')
+        q-card
+          q-card-section
+            .text-subtitle1 {{t('navEdit.separator')}}
+        q-card.q-pa-md.q-mt-md.flex
+          q-space
+          q-btn.acrylic-btn(
+            flat
+            :label='t(`common.actions.delete`)'
+            color='negative'
+            padding='xs md'
+            @click=''
+          )
+
+</template>
+
+<script setup>
+import { useI18n } from 'vue-i18n'
+import { useQuasar } from 'quasar'
+import { onMounted, reactive, ref } from 'vue'
+import gql from 'graphql-tag'
+import { cloneDeep } from 'lodash-es'
+
+import { useSiteStore } from 'src/stores/site'
+
+import { Sortable } from 'sortablejs-vue3'
+
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const siteStore = useSiteStore()
+
+// I18N
+
+const { t } = useI18n()
+
+// DATA
+
+const state = reactive({
+  loading: 0,
+  selected: '3',
+  items: [
+    {
+      id: '1',
+      type: 'header',
+      label: 'General'
+    },
+    {
+      id: '2',
+      type: 'link',
+      label: 'Dogs',
+      icon: 'las la-dog'
+    },
+    {
+      id: '3',
+      type: 'link',
+      label: 'Cats',
+      icon: 'las la-cat'
+    },
+    {
+      id: '4',
+      type: 'separator'
+    },
+    {
+      id: '5',
+      type: 'header',
+      label: 'User Guide'
+    },
+    {
+      id: '6',
+      type: 'link',
+      label: 'Editing Pages',
+      icon: 'las la-file-alt'
+    },
+    {
+      id: '7',
+      type: 'link',
+      label: 'Permissions',
+      icon: 'las la-key',
+      isNested: true
+    },
+    {
+      id: '8',
+      type: 'link',
+      label: 'Supersuperlongtitleveryveryversupersupersupersupersuper long word',
+      icon: 'las la-key'
+    },
+    {
+      id: '9',
+      type: 'link',
+      label: 'Users',
+      icon: 'las la-users'
+    },
+    {
+      id: '10',
+      type: 'link',
+      label: 'Locales',
+      icon: 'las la-globe'
+    }
+  ],
+  current: {
+    label: '',
+    icon: '',
+    target: '/',
+    openInNewWindow: false,
+    visibility: [],
+    visibilityLimited: false,
+    isNested: false
+  },
+  groups: []
+})
+
+const sortableOptions = {
+  handle: '.handle',
+  animation: 150
+}
+
+const visibilityOptions = [
+  { value: false, label: t('navEdit.visibilityAll') },
+  { value: true, label: t('navEdit.visibilityLimited') }
+]
+
+const thumbStyle = {
+  right: '2px',
+  borderRadius: '5px',
+  backgroundColor: '#FFF',
+  width: '5px',
+  opacity: 0.5
+}
+const barStyle = {
+  backgroundColor: '#000',
+  width: '9px',
+  opacity: 0.1
+}
+
+// METHODS
+
+function setItem (item) {
+  state.selected = item.id
+  state.current = item
+}
+
+function close () {
+  siteStore.$patch({ overlay: '' })
+}
+
+onMounted(() => {
+
+})
+</script>
+
+<style lang="scss" scoped>
+.nav-edit {
+  height: 100%;
+
+  .handle {
+    cursor: grab;
+  }
+}
+
+.nav-edit-item {
+  position: relative;
+  &.is-active {
+    background-color: $blue-8;
+  }
+
+  &.sortable-chosen {
+    background-color: $blue-5;
+  }
+}
+
+.nav-edit-item-header {
+  display: flex;
+  cursor: pointer;
+}
+.nav-edit-item-link {
+  &.is-nested {
+    border-left: 10px solid $dark-1;
+    background-color: $dark-4;
+    &.is-active {
+      background-color: $primary;
+    }
+
+    & + div:not(.is-nested) {
+      &::before {
+        content: '';
+        display: 'block';
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 10px;
+        height: 10px;
+        border-style: solid;
+        border-color: $dark-1 transparent transparent $dark-1;
+        border-width: 10px 10px 10px 0;
+      }
+    }
+  }
+
+  &:not(.is-nested) + &.is-nested {
+    &::before {
+      content: '';
+      display: 'block';
+      position: absolute;
+      top: -10px;
+      left: -10px;
+      width: 10px;
+      height: 10px;
+      border-style: solid;
+      border-color: transparent transparent $dark-1 $dark-1;
+      border-width: 0 10px 10px 0;
+    }
+  }
+}
+.nav-edit-item-separator {
+  display: flex;
+  cursor: pointer;
+}
+
+.nav-edit-item-header, .nav-edit-item-separator {
+  & + .nav-edit-item-link.is-nested {
+    background-color: $negative !important;
+    border-left-color: darken($negative, 10%) !important;
+
+    & + div:not(.is-nested) {
+      &::before {
+        display: none !important;
+      }
+    }
+  }
+}
+
+.nav-edit-list {
+  .nav-edit-item-separator + .nav-edit-item-header > .q-item__label {
+    padding-top: 8px;
+  }
+
+  .is-nested:first-child {
+    background-color: $negative !important;
+    border-left-color: darken($negative, 10%) !important;
+
+    & + div:not(.is-nested) {
+      &::before {
+        display: none !important;
+      }
+    }
+  }
+}
+</style>

+ 79 - 0
ux/src/components/NavSidebar.vue

@@ -0,0 +1,79 @@
+<template lang="pug">
+q-scroll-area.sidebar-nav(
+  :thumb-style='thumbStyle'
+  :bar-style='barStyle'
+  )
+  q-list(
+    clickable
+    dense
+    dark
+    )
+    q-item-label.text-blue-2.text-caption(header) Header
+    q-item(to='/install')
+      q-item-section(side)
+        q-icon(name='las la-dog', color='white')
+      q-item-section Link 1
+    q-item(to='/install')
+      q-item-section(side)
+        q-icon(name='las la-cat', color='white')
+      q-item-section Link 2
+    q-separator.q-my-sm(dark)
+    q-item(to='/install')
+      q-item-section(side)
+        q-icon(name='mdi-fruit-grapes', color='white')
+      q-item-section.text-wordbreak-all Link 3
+</template>
+
+<script setup>
+import { useQuasar } from 'quasar'
+import { computed, onMounted, reactive, ref, watch } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { useI18n } from 'vue-i18n'
+
+import { useSiteStore } from 'src/stores/site'
+
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const siteStore = useSiteStore()
+
+// ROUTER
+
+const router = useRouter()
+const route = useRoute()
+
+// I18N
+
+const { t } = useI18n()
+
+// DATA
+
+const thumbStyle = {
+  right: '2px',
+  borderRadius: '5px',
+  backgroundColor: '#FFF',
+  width: '5px',
+  opacity: 0.5
+}
+const barStyle = {
+  backgroundColor: '#000',
+  width: '9px',
+  opacity: 0.1
+}
+</script>
+
+<style lang="scss">
+.sidebar-nav {
+  border-top: 1px solid rgba(255,255,255,.15);
+  height: calc(100% - 38px - 24px);
+
+  .q-list {
+    .q-separator + .q-item__label {
+      padding-top: 12px;
+    }
+  }
+}
+</style>

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

@@ -36,6 +36,10 @@ body::-webkit-scrollbar-thumb {
   font-family: 'Roboto Mono', Consolas, "Liberation Mono", Courier, monospace;
 }
 
+.text-wordbreak-all {
+  word-break: break-all;
+}
+
 // ------------------------------------------------------------------
 // THEME COLORS
 // ------------------------------------------------------------------

+ 17 - 52
ux/src/layouts/MainLayout.vue

@@ -30,40 +30,27 @@ q-layout(view='hHh Lpr lff')
         aria-label='Browse'
         size='sm'
         )
-    q-scroll-area.sidebar-nav(
-      :thumb-style='thumbStyle'
-      :bar-style='barStyle'
-      )
-      q-list(
-        clickable
-        dense
-        dark
-        )
-        q-item-label.text-blue-2.text-caption(header) Header
-        q-item(to='/install')
-          q-item-section(side)
-            q-icon(name='las la-dog', color='white')
-          q-item-section Link 1
-        q-item(to='/install')
-          q-item-section(side)
-            q-icon(name='las la-cat', color='white')
-          q-item-section Link 2
-        q-separator.q-my-sm(dark)
-        q-item(to='/install')
-          q-item-section(side)
-            q-icon(name='mdi-fruit-grapes', color='white')
-          q-item-section Link 3
-    q-bar.bg-blue-9.text-white(dense, v-if='flagsStore.experimental && userStore.authenticated')
+    nav-sidebar
+    q-bar.bg-blue-9.text-white(dense, v-if='userStore.authenticated')
       q-btn.col(
         icon='las la-dharmachakra'
-        label='History'
+        label='Edit Nav'
         flat
-      )
+        )
+        q-menu(
+          ref='navEditMenu'
+          anchor='top left'
+          self='bottom left'
+          :offset='[0, 10]'
+          )
+          nav-edit-menu(:menu-hide-handler='navEditMenu.hide')
+
       q-separator(vertical)
       q-btn.col(
         icon='las la-bookmark'
         label='Bookmarks'
         flat
+        disabled
       )
   q-page-container
     router-view
@@ -99,6 +86,8 @@ import { useUserStore } from 'src/stores/user'
 import FooterNav from 'src/components/FooterNav.vue'
 import HeaderNav from 'src/components/HeaderNav.vue'
 import LocaleSelectorMenu from 'src/components/LocaleSelectorMenu.vue'
+import NavSidebar from 'src/components/NavSidebar.vue'
+import NavEditMenu from 'src/components/NavEditMenu.vue'
 import MainOverlayDialog from 'src/components/MainOverlayDialog.vue'
 
 // QUASAR
@@ -128,23 +117,9 @@ useMeta({
   titleTemplate: title => `${title} - ${siteStore.title}`
 })
 
-// DATA
+// REFS
 
-const leftDrawerOpen = ref(true)
-const search = ref('')
-
-const thumbStyle = {
-  right: '2px',
-  borderRadius: '5px',
-  backgroundColor: '#FFF',
-  width: '5px',
-  opacity: 0.5
-}
-const barStyle = {
-  backgroundColor: '#000',
-  width: '9px',
-  opacity: 0.1
-}
+const navEditMenu = ref(null)
 
 // COMPUTED
 
@@ -152,12 +127,6 @@ const isSidebarShown = computed(() => {
   return siteStore.showSideNav && !siteStore.sideNavIsDisabled && !(editorStore.isActive && editorStore.hideSideNav)
 })
 
-// METHODS
-
-function openFileManager () {
-  siteStore.openFileManager()
-}
-
 </script>
 
 <style lang="scss">
@@ -166,10 +135,6 @@ function openFileManager () {
   border-bottom: 1px solid rgba(0,0,0,.2);
   height: 38px;
 }
-.sidebar-nav {
-  border-top: 1px solid rgba(255,255,255,.15);
-  height: calc(100% - 38px - 24px);
-}
 
 body.body--dark {
   background-color: $dark-6;

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

@@ -64,7 +64,8 @@ export const useSiteStore = defineStore('site', {
     },
     sideDialogShown: false,
     sideDialogComponent: '',
-    docsBase: 'https://next.js.wiki/docs'
+    docsBase: 'https://next.js.wiki/docs',
+    nav: {}
   }),
   getters: {
     overlayIsShown: (state) => Boolean(state.overlay),

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů