瀏覽代碼

feat: accessibilty cvd option + editor page header

Nicolas Giard 2 年之前
父節點
當前提交
72eccb7a2a

+ 4 - 2
server/db/migrations/3.0.0.js

@@ -691,7 +691,8 @@ exports.up = async knex => {
         timezone: 'America/New_York',
         dateFormat: 'YYYY-MM-DD',
         timeFormat: '12h',
-        appearance: 'site'
+        appearance: 'site',
+        cvd: 'none'
       },
       localeCode: 'en'
     },
@@ -708,7 +709,8 @@ exports.up = async knex => {
         timezone: 'America/New_York',
         dateFormat: 'YYYY-MM-DD',
         timeFormat: '12h',
-        appearance: 'site'
+        appearance: 'site',
+        cvd: 'none'
       },
       localeCode: 'en'
     }

+ 2 - 1
server/graph/resolvers/user.js

@@ -229,7 +229,8 @@ module.exports = {
             timezone: args.timezone || usr.prefs.timezone,
             dateFormat: args.dateFormat ?? usr.prefs.dateFormat,
             timeFormat: args.timeFormat ?? usr.prefs.timeFormat,
-            appearance: args.appearance || usr.prefs.appearance
+            appearance: args.appearance || usr.prefs.appearance,
+            cvd: args.cvd || usr.prefs.cvd
           }
         })
 

+ 8 - 0
server/graph/schemas/user.graphql

@@ -72,6 +72,7 @@ extend type Mutation {
     dateFormat: String
     timeFormat: String
     appearance: UserSiteAppearance
+    cvd: UserCvdChoices
   ): DefaultResponse
 
   uploadUserAvatar(
@@ -154,6 +155,13 @@ enum UserSiteAppearance {
   dark
 }
 
+enum UserCvdChoices {
+  none
+  protanopia
+  deuteranopia
+  tritanopia
+}
+
 input UserUpdateInput {
   email: String
   name: String

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

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 40 40" width="80px" height="80px"><path fill="#fff" d="M20,32.5C10.238,32.5,2.002,21.886,0.616,20C2.002,18.115,10.245,7.5,20,7.5 c9.762,0,17.998,10.614,19.384,12.5C37.998,21.885,29.755,32.5,20,32.5z"/><path fill="#4788c7" d="M20,8c9.097,0,16.906,9.549,18.76,12C36.908,22.453,29.11,32,20,32c-9.097,0-16.906-9.549-18.76-12 C3.092,17.547,10.89,8,20,8 M20,7C8.954,7,0,20,0,20s8.954,13,20,13s20-13,20-13S31.046,7,20,7L20,7z"/><path fill="#98ccfd" d="M25.994 26.701C27.835 25.053 29 22.665 29 20c0-4.971-4.029-9-9-9-2.665 0-5.053 1.165-6.701 3.006L25.994 26.701zM8.166 12.873L25.98 30.687c.329-.139.654-.284.974-.44l-17.99-17.99C8.693 12.46 8.428 12.666 8.166 12.873zM6.019 14.726l16.932 16.932c.378-.082.751-.182 1.123-.291L6.757 14.05C6.501 14.276 6.261 14.501 6.019 14.726zM4.707 16c-.242.247-.468.486-.69.724l15.228 15.228C19.497 31.966 19.746 32 20 32c.224 0 .443-.031.665-.042L4.707 16zM2.145 18.852l11.803 11.803c.713.305 1.442.564 2.187.773L2.791 18.084C2.557 18.357 2.341 18.614 2.145 18.852z"/><path fill="#4788c7" d="M22.441,23.148C23.383,22.416,24,21.285,24,20c0-2.209-1.791-4-4-4c-1.285,0-2.416,0.617-3.148,1.559 L22.441,23.148z"/><path fill="#dff0fe" d="M23 14A2 2 0 1 0 23 18A2 2 0 1 0 23 14Z"/><path fill="none" stroke="#4788c7" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M3 3L37 37"/></svg>

+ 11 - 5
ux/src/App.vue

@@ -42,6 +42,10 @@ watch(() => userStore.appearance, (newValue) => {
   }
 })
 
+watch(() => userStore.cvd, () => {
+  applyTheme()
+})
+
 // THEME
 
 function applyTheme () {
@@ -50,11 +54,13 @@ function applyTheme () {
   } else {
     $q.dark.set(userStore.appearance === 'dark')
   }
-  setCssVar('primary', siteStore.theme.colorPrimary)
-  setCssVar('secondary', siteStore.theme.colorSecondary)
-  setCssVar('accent', siteStore.theme.colorAccent)
-  setCssVar('header', siteStore.theme.colorHeader)
-  setCssVar('sidebar', siteStore.theme.colorSidebar)
+  setCssVar('primary', userStore.getAccessibleColor('primary', siteStore.theme.colorPrimary))
+  setCssVar('secondary', userStore.getAccessibleColor('secondary', siteStore.theme.colorSecondary))
+  setCssVar('accent', userStore.getAccessibleColor('accent', siteStore.theme.colorAccent))
+  setCssVar('header', userStore.getAccessibleColor('header', siteStore.theme.colorHeader))
+  setCssVar('sidebar', userStore.getAccessibleColor('sidebar', siteStore.theme.colorSidebar))
+  setCssVar('positive', userStore.getAccessibleColor('positive', '#02C39A'))
+  setCssVar('negative', userStore.getAccessibleColor('negative', '#f03a47'))
 }
 
 // INIT SITE STORE

+ 26 - 10
ux/src/components/EditorMarkdown.vue

@@ -41,7 +41,7 @@
         flat
         )
         q-tooltip(anchor='center right' self='center left') {{ t('editor.markup.horizontalBar') }}
-    .editor-markdown-editor
+    .editor-markdown-mid
       //--------------------------------------------------------
       //- TOP TOOLBAR
       //--------------------------------------------------------
@@ -115,9 +115,11 @@
       //--------------------------------------------------------
       //- CODEMIRROR
       //--------------------------------------------------------
-      textarea(ref='cmRef')
+      .editor-markdown-editor
+        textarea(ref='cmRef')
     transition(name='editor-markdown-preview')
       .editor-markdown-preview(v-if='state.previewShown')
+        .editor-markdown-preview-toolbar Render Preview
         .editor-markdown-preview-content.contents(ref='editorPreviewContainer')
           div(
             ref='editorPreview'
@@ -126,7 +128,7 @@
 </template>
 
 <script setup>
-import { reactive, ref, shallowRef, onBeforeMount, onMounted, watch } from 'vue'
+import { reactive, ref, shallowRef, nextTick, onBeforeMount, onMounted, watch } from 'vue'
 import { useMeta, useQuasar, setCssVar } from 'quasar'
 import { useI18n } from 'vue-i18n'
 
@@ -169,6 +171,7 @@ const cm = shallowRef(null)
 const cmRef = ref(null)
 
 const state = reactive({
+  content: '',
   previewShown: true,
   previewHTML: ''
 })
@@ -212,7 +215,7 @@ onMounted(async () => {
     // onCmInput(editorStore.content)
   })
 
-  cm.value.setSize(null, 'calc(100vh - 150px)')
+  cm.value.setSize(null, '100%')
 
   // -> Set Keybindings
   const keyBindings = {
@@ -263,7 +266,9 @@ onMounted(async () => {
 
   // // Render initial preview
   // this.processContent(this.$store.get('editor/content'))
-  // this.refresh()
+  nextTick(() => {
+    cm.value.refresh()
+  })
 
   // this.$root.$on('editorInsert', opts => {
   //   switch (opts.kind) {
@@ -306,7 +311,7 @@ onBeforeMount(() => {
 </script>
 
 <style lang="scss">
-$editor-height: calc(100vh - 112px - 24px);
+$editor-height: calc(100vh - 64px - 94px - 2px);
 $editor-height-mobile: calc(100vh - 112px - 16px);
 
 .editor-markdown {
@@ -314,12 +319,17 @@ $editor-height-mobile: calc(100vh - 112px - 16px);
     display: flex;
     width: 100%;
   }
-  &-editor {
+  &-mid {
     background-color: $dark-6;
     flex: 1 1 50%;
     display: block;
     height: $editor-height;
     position: relative;
+  }
+  &-editor {
+    display: block;
+    height: calc(100% - 32px);
+    position: relative;
     // @include until($tablet) {
     //   height: $editor-height-mobile;
     // }
@@ -330,8 +340,6 @@ $editor-height-mobile: calc(100vh - 112px - 16px);
     position: relative;
     height: $editor-height;
     overflow: hidden;
-    padding: 1rem;
-    border-top: 32px solid $grey-3;
 
     @at-root .theme--dark & {
       background-color: $grey-9;
@@ -350,10 +358,18 @@ $editor-height-mobile: calc(100vh - 112px - 16px);
     &-enter, &-leave-to {
       max-width: 0;
     }
+    &-toolbar {
+      background-color: $grey-3;
+      color: $grey-8;
+      height: 32px;
+      display: flex;
+      align-items: center;
+      padding: 0 1rem;
+    }
     &-content {
       height: $editor-height;
       overflow-y: scroll;
-      padding: 0;
+      padding: 1rem;
       width: calc(100% + 17px);
       // -ms-overflow-style: none;
       // &::-webkit-scrollbar {

+ 234 - 0
ux/src/components/PageHeader.vue

@@ -0,0 +1,234 @@
+<template lang="pug">
+.page-header.row
+  //- PAGE ICON
+  .col-auto.q-pl-md.flex.items-center
+    q-btn.rounded-borders(
+      v-if='editorStore.isActive'
+      padding='none'
+      size='37px'
+      :icon='pageStore.icon'
+      color='primary'
+      flat
+      )
+      q-menu(content-class='shadow-7')
+        icon-picker-dialog(v-model='pageStore.icon')
+    q-icon.rounded-borders(
+      v-else
+      :name='pageStore.icon'
+      size='64px'
+      color='primary'
+    )
+  //- PAGE HEADER
+  .col.q-pa-md
+    .text-h4.page-header-title
+      span {{pageStore.title}}
+      template(v-if='editorStore.isActive')
+        span.text-grey(v-if='!pageStore.title') {{ t(`editor.props.title`)}}
+        q-btn.acrylic-btn.q-ml-md(
+          icon='las la-pen'
+          flat
+          padding='xs'
+          size='sm'
+          )
+          q-popup-edit(
+            v-model='pageStore.title'
+            auto-save
+            v-slot='scope'
+            )
+            q-input(
+              outlined
+              style='width: 450px;'
+              v-model='scope.value'
+              dense
+              autofocus
+              @keyup.enter='scope.set'
+              :label='t(`editor.props.title`)'
+              )
+    .text-subtitle2.page-header-subtitle
+      span {{ pageStore.description }}
+      template(v-if='editorStore.isActive')
+        span.text-grey(v-if='!pageStore.description') {{ t(`editor.props.shortDescription`)}}
+        q-btn.acrylic-btn.q-ml-md(
+          icon='las la-pen'
+          flat
+          padding='none xs'
+          size='xs'
+          )
+          q-popup-edit(
+            v-model='pageStore.description'
+            auto-save
+            v-slot='scope'
+            )
+            q-input(
+              outlined
+              style='width: 450px;'
+              v-model='scope.value'
+              dense
+              autofocus
+              @keyup.enter='scope.set'
+              :label='t(`editor.props.shortDescription`)'
+              )
+  //- PAGE ACTIONS
+  .col-auto.q-pa-md.flex.items-center.justify-end
+    template(v-if='!editorStore.isActive')
+      q-btn.q-mr-md(
+        flat
+        dense
+        icon='las la-bell'
+        color='grey'
+        aria-label='Watch Page'
+        )
+        q-tooltip Watch Page
+      q-btn.q-mr-md(
+        flat
+        dense
+        icon='las la-bookmark'
+        color='grey'
+        aria-label='Bookmark Page'
+        )
+        q-tooltip Bookmark Page
+      q-btn.q-mr-md(
+        flat
+        dense
+        icon='las la-share-alt'
+        color='grey'
+        aria-label='Share'
+        )
+        q-tooltip Share
+        social-sharing-menu
+      q-btn.q-mr-md(
+        flat
+        dense
+        icon='las la-print'
+        color='grey'
+        aria-label='Print'
+        )
+        q-tooltip Print
+    template(v-if='editorStore.isActive || editorStore.hasPendingChanges')
+      q-btn.acrylic-btn.q-mr-sm(
+        flat
+        icon='las la-times'
+        color='negative'
+        label='Discard'
+        aria-label='Discard'
+        no-caps
+        @click='discardChanges'
+      )
+      q-btn.acrylic-btn(
+        flat
+        icon='las la-check'
+        color='positive'
+        label='Save Changes'
+        aria-label='Save Changes'
+        no-caps
+        @click='saveChanges'
+      )
+    template(v-else)
+      q-btn.acrylic-btn(
+        flat
+        icon='las la-edit'
+        color='deep-orange-9'
+        label='Edit'
+        aria-label='Edit'
+        no-caps
+        @click='editPage'
+      )
+</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'
+import { useUserStore } from 'src/stores/user'
+
+import IconPickerDialog from 'src/components/IconPickerDialog.vue'
+import SocialSharingMenu from 'src/components/SocialSharingMenu.vue'
+
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const editorStore = useEditorStore()
+const flagsStore = useFlagsStore()
+const pageStore = usePageStore()
+const siteStore = useSiteStore()
+const userStore = useUserStore()
+
+// ROUTER
+
+const router = useRouter()
+const route = useRoute()
+
+// I18N
+
+const { t } = useI18n()
+
+// COMPUTED
+
+const editMode = computed(() => {
+  return pageStore.mode === 'edit'
+})
+const editCreateMode = computed(() => {
+  return pageStore.mode === 'edit' && pageStore.mode === 'create'
+})
+const editUrl = computed(() => {
+  let pagePath = siteStore.useLocales ? `${pageStore.locale}/` : ''
+  pagePath += !pageStore.path ? 'home' : pageStore.path
+  return `/_edit/${pagePath}`
+})
+
+// METHODS
+
+async function discardChanges () {
+  $q.loading.show()
+  try {
+    editorStore.$patch({
+      isActive: false,
+      editor: ''
+    })
+    await pageStore.pageLoad({ id: pageStore.id })
+    $q.notify({
+      type: 'positive',
+      message: 'Page has been reverted to the last saved state.'
+    })
+  } catch (err) {
+    $q.notify({
+      type: 'negative',
+      message: 'Failed to reload page state.'
+    })
+  }
+  $q.loading.hide()
+}
+
+async function saveChanges () {
+  $q.loading.show()
+  try {
+    await pageStore.pageSave()
+    $q.notify({
+      type: 'positive',
+      message: 'Page saved successfully.'
+    })
+  } catch (err) {
+    $q.notify({
+      type: 'negative',
+      message: 'Failed to save page changes.'
+    })
+  }
+  $q.loading.hide()
+}
+
+function editPage () {
+  editorStore.$patch({
+    isActive: true,
+    editor: 'markdown'
+  })
+}
+</script>

+ 33 - 0
ux/src/helpers/accessibility.js

@@ -0,0 +1,33 @@
+const protanopia = {
+  negative: '#fb8c00',
+  positive: '#2196f3',
+  primary: '#1976D2',
+  secondary: '#2196f3'
+}
+
+const deuteranopia = {
+  negative: '#ef6c00',
+  positive: '#2196f3',
+  primary: '#1976D2',
+  secondary: '#2196f3'
+}
+
+const tritanopia = {
+  primary: '#e91e63',
+  secondary: '#02C39A'
+}
+
+export function getAccessibleColor (name, base, cvd) {
+  switch (cvd) {
+    case 'protanopia': {
+      return protanopia[name] ?? base
+    }
+    case 'deuteranopia': {
+      return deuteranopia[name] ?? base
+    }
+    case 'tritanopia': {
+      return tritanopia[name] ?? base
+    }
+  }
+  return base
+}

+ 9 - 1
ux/src/i18n/locales/en.json

@@ -1708,5 +1708,13 @@
   "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.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"
 }

+ 1 - 1
ux/src/layouts/MainLayout.vue

@@ -78,7 +78,7 @@ q-layout(view='hHh Lpr lff')
         size='md'
       )
   main-overlay-dialog
-  footer-nav
+  footer-nav(v-if='!editorStore.isActive')
 </template>
 
 <script setup>

+ 2 - 130
ux/src/pages/Index.vue

@@ -27,83 +27,7 @@ q-page.column
         .text-caption.text-accent: strong Unpublished
         q-separator.q-mx-sm(vertical)
       .text-caption.text-grey-6 Last modified on #[strong {{lastModified}}]
-  .page-header.row
-    //- PAGE ICON
-    .col-auto.q-pl-md.flex.items-center
-      q-icon.rounded-borders(
-        :name='pageStore.icon'
-        size='64px'
-        color='primary'
-      )
-    //- PAGE HEADER
-    .col.q-pa-md
-      .text-h4.page-header-title {{pageStore.title}}
-      .text-subtitle2.page-header-subtitle {{pageStore.description }}
-
-    //- PAGE ACTIONS
-    .col-auto.q-pa-md.flex.items-center.justify-end
-      q-btn.q-mr-md(
-        flat
-        dense
-        icon='las la-bell'
-        color='grey'
-        aria-label='Watch Page'
-        )
-        q-tooltip Watch Page
-      q-btn.q-mr-md(
-        flat
-        dense
-        icon='las la-bookmark'
-        color='grey'
-        aria-label='Bookmark Page'
-        )
-        q-tooltip Bookmark Page
-      q-btn.q-mr-md(
-        flat
-        dense
-        icon='las la-share-alt'
-        color='grey'
-        aria-label='Share'
-        )
-        q-tooltip Share
-        social-sharing-menu
-      q-btn.q-mr-md(
-        flat
-        dense
-        icon='las la-print'
-        color='grey'
-        aria-label='Print'
-        )
-        q-tooltip Print
-      template(v-if='editorStore.hasPendingChanges')
-        q-btn.acrylic-btn.q-mr-sm(
-          flat
-          icon='las la-times'
-          color='negative'
-          label='Discard'
-          aria-label='Discard'
-          no-caps
-          @click='discardChanges'
-        )
-        q-btn.acrylic-btn(
-          flat
-          icon='las la-check'
-          color='positive'
-          label='Save Changes'
-          aria-label='Save Changes'
-          no-caps
-          @click='saveChanges'
-        )
-      template(v-else)
-        q-btn.acrylic-btn(
-          flat
-          icon='las la-edit'
-          color='deep-orange-9'
-          label='Edit'
-          aria-label='Edit'
-          no-caps
-          @click='editPage'
-        )
+  page-header
   .page-container.row.no-wrap.items-stretch(style='flex: 1 1 100%;')
     .col(style='order: 1;')
       q-no-ssr(
@@ -336,8 +260,8 @@ import { useSiteStore } from 'src/stores/site'
 
 // COMPONENTS
 
-import SocialSharingMenu from '../components/SocialSharingMenu.vue'
 import LoadingGeneric from 'src/components/LoadingGeneric.vue'
+import PageHeader from 'src/components/PageHeader.vue'
 import PageTags from '../components/PageTags.vue'
 
 const sideDialogs = {
@@ -428,17 +352,6 @@ const relationsCenter = computed(() => {
 const relationsRight = computed(() => {
   return pageStore.relations ? pageStore.relations.filter(r => r.position === 'right') : []
 })
-const editMode = computed(() => {
-  return pageStore.mode === 'edit'
-})
-const editCreateMode = computed(() => {
-  return pageStore.mode === 'edit' && pageStore.mode === 'create'
-})
-const editUrl = computed(() => {
-  let pagePath = siteStore.useLocales ? `${pageStore.locale}/` : ''
-  pagePath += !pageStore.path ? 'home' : pageStore.path
-  return `/_edit/${pagePath}`
-})
 const lastModified = computed(() => {
   return pageStore.updatedAt ? DateTime.fromISO(pageStore.updatedAt).toLocaleString(DateTime.DATETIME_MED) : 'N/A'
 })
@@ -545,47 +458,6 @@ function refreshTocExpanded (baseToc, lvl) {
     return toExpand
   }
 }
-
-async function discardChanges () {
-  $q.loading.show()
-  try {
-    await pageStore.pageLoad({ id: pageStore.id })
-    $q.notify({
-      type: 'positive',
-      message: 'Page has been reverted to the last saved state.'
-    })
-  } catch (err) {
-    $q.notify({
-      type: 'negative',
-      message: 'Failed to reload page state.'
-    })
-  }
-  $q.loading.hide()
-}
-
-async function saveChanges () {
-  $q.loading.show()
-  try {
-    await pageStore.pageSave()
-    $q.notify({
-      type: 'positive',
-      message: 'Page saved successfully.'
-    })
-  } catch (err) {
-    $q.notify({
-      type: 'negative',
-      message: 'Failed to save page changes.'
-    })
-  }
-  $q.loading.hide()
-}
-
-function editPage () {
-  editorStore.$patch({
-    isActive: true,
-    editor: 'markdown'
-  })
-}
 </script>
 
 <style lang="scss">

+ 27 - 2
ux/src/pages/ProfileInfo.vue

@@ -134,6 +134,21 @@ q-page.q-py-md(:style-fn='pageStyle')
         toggle-color='primary'
         :options='appearances'
       )
+  .text-header.q-mt-lg {{t('profile.accessibility')}}
+  q-item
+    blueprint-icon(icon='visualy-impaired')
+    q-item-section
+      q-item-label {{t(`profile.cvd`)}}
+      q-item-label(caption) {{t(`profile.cvdHint`)}}
+    q-item-section.col-auto
+      q-btn-toggle(
+        v-model='state.config.cvd'
+        push
+        glossy
+        no-caps
+        toggle-color='primary'
+        :options='cvdChoices'
+      )
   .actions-bar.q-mt-lg
     q-btn(
       icon='las la-check'
@@ -149,7 +164,7 @@ import gql from 'graphql-tag'
 
 import { useI18n } from 'vue-i18n'
 import { useMeta, useQuasar } from 'quasar'
-import { onMounted, reactive, watch } from 'vue'
+import { onMounted, reactive } from 'vue'
 
 import { useSiteStore } from 'src/stores/site'
 import { useUserStore } from 'src/stores/user'
@@ -185,7 +200,8 @@ const state = reactive({
     timezone: '',
     dateFormat: '',
     timeFormat: '12h',
-    appearance: 'site'
+    appearance: 'site',
+    cvd: 'none'
   }
 })
 
@@ -206,6 +222,12 @@ const appearances = [
   { value: 'light', label: t('profile.appearanceLight') },
   { value: 'dark', label: t('profile.appearanceDark') }
 ]
+const cvdChoices = [
+  { value: 'none', label: t('profile.cvdNone') },
+  { value: 'protanopia', label: t('profile.cvdProtanopia') },
+  { value: 'deuteranopia', label: t('profile.cvdDeuteranopia') },
+  { value: 'tritanopia', label: t('profile.cvdTritanopia') }
+]
 const timezones = Intl.supportedValuesOf('timeZone')
 
 // METHODS
@@ -232,6 +254,7 @@ async function save () {
           $dateFormat: String
           $timeFormat: String
           $appearance: UserSiteAppearance
+          $cvd: UserCvdChoices
         ) {
           updateProfile (
             name: $name
@@ -242,6 +265,7 @@ async function save () {
             dateFormat: $dateFormat
             timeFormat: $timeFormat
             appearance: $appearance
+            cvd: $cvd
           ) {
             operation {
               succeeded
@@ -283,5 +307,6 @@ onMounted(() => {
   state.config.dateFormat = userStore.dateFormat || ''
   state.config.timeFormat = userStore.timeFormat || '12h'
   state.config.appearance = userStore.appearance || 'site'
+  state.config.cvd = userStore.cvd || 'none'
 })
 </script>

+ 6 - 0
ux/src/stores/user.js

@@ -3,6 +3,7 @@ import jwtDecode from 'jwt-decode'
 import Cookies from 'js-cookie'
 import gql from 'graphql-tag'
 import { DateTime } from 'luxon'
+import { getAccessibleColor } from 'src/helpers/accessibility'
 
 export const useUserStore = defineStore('user', {
   state: () => ({
@@ -15,6 +16,7 @@ export const useUserStore = defineStore('user', {
     dateFormat: 'YYYY-MM-DD',
     timeFormat: '12h',
     appearance: 'site',
+    cvd: 'none',
     permissions: [],
     iat: 0,
     exp: null,
@@ -87,10 +89,14 @@ export const useUserStore = defineStore('user', {
         this.dateFormat = resp.prefs.dateFormat || ''
         this.timeFormat = resp.prefs.timeFormat || '12h'
         this.appearance = resp.prefs.appearance || 'site'
+        this.cvd = resp.prefs.cvd || 'none'
         this.profileLoaded = true
       } catch (err) {
         console.warn(err)
       }
+    },
+    getAccessibleColor (base, hexBase) {
+      return getAccessibleColor(base, hexBase, this.cvd)
     }
   }
 })