feat: accessibilty cvd option + editor page header

pull/6775/head
Nicolas Giard 2 years ago
parent 0adb4dc3e3
commit 72eccb7a2a
No known key found for this signature in database
GPG Key ID: 85061B8F9D55B7C8

@ -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'
}

@ -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
}
})

@ -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

@ -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>

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -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

@ -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 {

@ -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>

@ -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
}

@ -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"
}

@ -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>

@ -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">

@ -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>

@ -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)
}
}
})

Loading…
Cancel
Save