diff options
Diffstat (limited to 'src/components')
22 files changed, 1902 insertions, 501 deletions
diff --git a/src/components/chat_panel/chat_panel.vue b/src/components/chat_panel/chat_panel.vue index 30070d3e..f174319a 100644 --- a/src/components/chat_panel/chat_panel.vue +++ b/src/components/chat_panel/chat_panel.vue @@ -55,8 +55,8 @@ .chat-heading { cursor: pointer; .icon-comment-empty { - color: $fallback--fg; - color: var(--fg, $fallback--fg); + color: $fallback--text; + color: var(--text, $fallback--text); } } diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue new file mode 100644 index 00000000..34eec248 --- /dev/null +++ b/src/components/color_input/color_input.vue @@ -0,0 +1,53 @@ +<template> +<div class="color-control style-control" :class="{ disabled: !present || disabled }"> + <label :for="name" class="label"> + {{label}} + </label> + <input + v-if="typeof fallback !== 'undefined'" + class="opt exlcude-disabled" + :id="name + '-o'" + type="checkbox" + :checked="present" + @input="$emit('input', typeof value === 'undefined' ? fallback : undefined)"> + <label v-if="typeof fallback !== 'undefined'" class="opt-l" :for="name + '-o'"></label> + <input + :id="name" + class="color-input" + type="color" + :value="value || fallback" + :disabled="!present || disabled" + @input="$emit('input', $event.target.value)" + > + <input + :id="name + '-t'" + class="text-input" + type="text" + :value="value || fallback" + :disabled="!present || disabled" + @input="$emit('input', $event.target.value)" + > +</div> +</template> + +<script> +export default { + props: [ + 'name', 'label', 'value', 'fallback', 'disabled' + ], + computed: { + present () { + return typeof this.value !== 'undefined' + } + } +} +</script> + +<style lang="scss"> +.color-control { + input.text-input { + max-width: 7em; + flex: 1; + } +} +</style> diff --git a/src/components/contrast_ratio/contrast_ratio.vue b/src/components/contrast_ratio/contrast_ratio.vue new file mode 100644 index 00000000..bd971d00 --- /dev/null +++ b/src/components/contrast_ratio/contrast_ratio.vue @@ -0,0 +1,69 @@ +<template> +<span v-if="contrast" class="contrast-ratio"> + <span :title="hint" class="rating"> + <span v-if="contrast.aaa"> + <i class="icon-thumbs-up-alt"/> + </span> + <span v-if="!contrast.aaa && contrast.aa"> + <i class="icon-adjust"/> + </span> + <span v-if="!contrast.aaa && !contrast.aa"> + <i class="icon-attention"/> + </span> + </span> + <span class="rating" v-if="contrast && large" :title="hint_18pt"> + <span v-if="contrast.laaa"> + <i class="icon-thumbs-up-alt"/> + </span> + <span v-if="!contrast.laaa && contrast.laa"> + <i class="icon-adjust"/> + </span> + <span v-if="!contrast.laaa && !contrast.laa"> + <i class="icon-attention"/> + </span> + </span> +</span> +</template> + +<script> +export default { + props: [ + 'large', 'contrast' + ], + computed: { + hint () { + const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad') + const level = this.$t(`settings.style.common.contrast.level.${levelVal}`) + const context = this.$t('settings.style.common.contrast.context.text') + const ratio = this.contrast.text + return this.$t('settings.style.common.contrast.hint', { level, context, ratio }) + }, + hint_18pt () { + const levelVal = this.contrast.laaa ? 'aaa' : (this.contrast.laa ? 'aa' : 'bad') + const level = this.$t(`settings.style.common.contrast.level.${levelVal}`) + const context = this.$t('settings.style.common.contrast.context.18pt') + const ratio = this.contrast.text + return this.$t('settings.style.common.contrast.hint', { level, context, ratio }) + } + } +} +</script> + +<style lang="scss"> +.contrast-ratio { + display: flex; + justify-content: flex-end; + + margin-top: -4px; + margin-bottom: 5px; + + .label { + margin-right: 1em; + } + + .rating { + display: inline-block; + text-align: center; + } +} +</style> diff --git a/src/components/delete_button/delete_button.vue b/src/components/delete_button/delete_button.vue index d13547e2..b458b0dc 100644 --- a/src/components/delete_button/delete_button.vue +++ b/src/components/delete_button/delete_button.vue @@ -14,8 +14,8 @@ .icon-cancel,.delete-status { cursor: pointer; &:hover { - color: var(--cRed, $fallback--cRed); color: $fallback--cRed; + color: var(--cRed, $fallback--cRed); } } </style> diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue new file mode 100644 index 00000000..85f19eea --- /dev/null +++ b/src/components/font_control/font_control.vue @@ -0,0 +1,113 @@ +<template> +<div class="font-control style-control" :class="{ custom: isCustom }"> + <label :for="preset === 'custom' ? name : name + '-font-switcher'" class="label"> + {{label}} + </label> + <input + v-if="typeof fallback !== 'undefined'" + class="opt exlcude-disabled" + type="checkbox" + :id="name + '-o'" + :checked="present" + @input="$emit('input', typeof value === 'undefined' ? fallback : undefined)"> + <label v-if="typeof fallback !== 'undefined'" class="opt-l" :for="name + '-o'"></label> + <label :for="name + '-font-switcher'" class="select" :disabled="!present"> + <select + :disabled="!present" + v-model="preset" + class="font-switcher" + :id="name + '-font-switcher'"> + <option v-for="option in availableOptions" :value="option"> + {{ option === 'custom' ? $t('settings.style.fonts.custom') : option }} + </option> + </select> + <i class="icon-down-open"/> + </label> + <input + v-if="isCustom" + class="custom-font" + type="text" + :id="name" + v-model="family"> +</div> +</template> + +<script> +import { set } from 'vue' + +export default { + props: [ + 'name', 'label', 'value', 'fallback', 'options', 'no-inherit' + ], + data () { + return { + lValue: this.value, + availableOptions: [ + this.noInherit ? '' : 'inherit', + 'custom', + ...(this.options || []), + 'serif', + 'monospace', + 'sans-serif' + ].filter(_ => _) + } + }, + beforeUpdate () { + this.lValue = this.value + }, + computed: { + present () { + return typeof this.lValue !== 'undefined' + }, + dValue () { + return this.lValue || this.fallback || {} + }, + family: { + get () { + return this.dValue.family + }, + set (v) { + set(this.lValue, 'family', v) + this.$emit('input', this.lValue) + } + }, + isCustom () { + return this.preset === 'custom' + }, + preset: { + get () { + if (this.family === 'serif' || + this.family === 'sans-serif' || + this.family === 'monospace' || + this.family === 'inherit') { + return this.family + } else { + return 'custom' + } + }, + set (v) { + this.family = v === 'custom' ? '' : v + } + } + } +} +</script> + +<style lang="scss"> +@import '../../_variables.scss'; +.font-control { + input.custom-font { + min-width: 10em; + } + &.custom { + .select { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + .custom-font { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + } +} +</style> diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss index a137ccd5..87c89f6a 100644 --- a/src/components/notifications/notifications.scss +++ b/src/components/notifications/notifications.scss @@ -4,31 +4,28 @@ // a bit of a hack to allow scrolling below notifications padding-bottom: 15em; - .unseen-count { - display: inline-block; - background-color: $fallback--cRed; - background-color: var(--cRed, $fallback--cRed); - text-shadow: 0px 0px 3px rgba(0, 0, 0, 0.5); - border-radius: 99px; - min-width: 22px; - max-width: 22px; - min-height: 22px; - max-height: 22px; - color: white; - font-size: 15px; - line-height: 22px; - text-align: center; - vertical-align: middle - } - .loadmore-error { - color: $fallback--fg; - color: var(--fg, $fallback--fg); + color: $fallback--text; + color: var(--text, $fallback--text); } - .unseen { - box-shadow: inset 4px 0 0 var(--cRed, $fallback--cRed); - padding-left: 0; + .notification { + position: relative; + + .notification-overlay { + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; + pointer-events: none; + } + + &.unseen { + .notification-overlay { + background-image: linear-gradient(135deg, var(--badgeNotification, $fallback--cRed) 4px, transparent 10px) + } + } } } @@ -42,10 +39,10 @@ .broken-favorite { border-radius: $fallback--tooltipRadius; border-radius: var(--tooltipRadius, $fallback--tooltipRadius); - color: $fallback--faint; - color: var(--faint, $fallback--faint); - background-color: $fallback--cAlertRed; - background-color: var(--cAlertRed, $fallback--cAlertRed); + color: $fallback--text; + color: var(--alertErrorText, $fallback--text); + background-color: $fallback--alertError; + background-color: var(--alertError, $fallback--alertError); padding: 2px .5em } @@ -90,6 +87,9 @@ padding: 0.25em 0; color: $fallback--faint; color: var(--faint, $fallback--faint); + a { + color: var(--faintLink); + } } padding: 0; .media-body { diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue index 7a4322f9..64f18720 100644 --- a/src/components/notifications/notifications.vue +++ b/src/components/notifications/notifications.vue @@ -4,7 +4,7 @@ <div class="panel-heading"> <div class="title"> {{$t('notifications.notifications')}} - <span class="unseen-count" v-if="unseenCount">{{unseenCount}}</span> + <span class="badge badge-notification unseen-count" v-if="unseenCount">{{unseenCount}}</span> </div> <div @click.prevent class="loadmore-error alert error" v-if="error"> {{$t('timeline.error_fetching')}} @@ -13,6 +13,7 @@ </div> <div class="panel-body"> <div v-for="notification in visibleNotifications" :key="notification.action.id" class="notification" :class='{"unseen": !notification.seen}'> + <div class="notification-overlay"></div> <notification :notification="notification"></notification> </div> </div> diff --git a/src/components/opacity_input/opacity_input.vue b/src/components/opacity_input/opacity_input.vue new file mode 100644 index 00000000..3926915b --- /dev/null +++ b/src/components/opacity_input/opacity_input.vue @@ -0,0 +1,38 @@ +<template> +<div class="opacity-control style-control" :class="{ disabled: !present || disabled }"> + <label :for="name" class="label"> + {{$t('settings.style.common.opacity')}} + </label> + <input + v-if="typeof fallback !== 'undefined'" + class="opt exclude-disabled" + :id="name + '-o'" + type="checkbox" + :checked="present" + @input="$emit('input', !present ? fallback : undefined)"> + <label v-if="typeof fallback !== 'undefined'" class="opt-l" :for="name + '-o'"></label> + <input + :id="name" + class="input-number" + type="number" + :value="value || fallback" + :disabled="!present || disabled" + @input="$emit('input', $event.target.value)" + max="1" + min="0" + step=".05"> +</div> +</template> + +<script> +export default { + props: [ + 'name', 'value', 'fallback', 'disabled' + ], + computed: { + present () { + return typeof this.value !== 'undefined' + } + } +} +</script> diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 42e9c65c..1c79cab3 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -153,8 +153,8 @@ padding-bottom: 0; margin-left: $fallback--attachmentRadius; margin-left: var(--attachmentRadius, $fallback--attachmentRadius); - background-color: $fallback--btn; - background-color: var(--btn, $fallback--btn); + background-color: $fallback--fg; + background-color: var(--btn, $fallback--fg); border-bottom-left-radius: 0; border-bottom-right-radius: 0; } @@ -258,11 +258,13 @@ position: absolute; z-index: 1; box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5); + // this doesn't match original but i don't care, making it uniform. + box-shadow: var(--popupShadow); min-width: 75%; background: $fallback--bg; background: var(--bg, $fallback--bg); - color: $fallback--lightFg; - color: var(--lightFg, $fallback--lightFg); + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); } .autocomplete { @@ -291,8 +293,8 @@ } &.highlighted { - background-color: $fallback--btn; - background-color: var(--btn, $fallback--btn); + background-color: $fallback--fg; + background-color: var(--lightBg, $fallback--fg); } } } diff --git a/src/components/range_input/range_input.vue b/src/components/range_input/range_input.vue new file mode 100644 index 00000000..3e50664b --- /dev/null +++ b/src/components/range_input/range_input.vue @@ -0,0 +1,48 @@ +<template> +<div class="range-control style-control" :class="{ disabled: !present || disabled }"> + <label :for="name" class="label"> + {{label}} + </label> + <input + v-if="typeof fallback !== 'undefined'" + class="opt exclude-disabled" + :id="name + '-o'" + type="checkbox" + :checked="present" + @input="$emit('input', !present ? fallback : undefined)"> + <label v-if="typeof fallback !== 'undefined'" class="opt-l" :for="name + '-o'"></label> + <input + :id="name" + class="input-number" + type="range" + :value="value || fallback" + :disabled="!present || disabled" + @input="$emit('input', $event.target.value)" + :max="max || hardMax || 100" + :min="min || hardMin || 0" + :step="step || 1"> + <input + :id="name" + class="input-number" + type="number" + :value="value || fallback" + :disabled="!present || disabled" + @input="$emit('input', $event.target.value)" + :max="hardMax" + :min="hardMin" + :step="step || 1"> +</div> +</template> + +<script> +export default { + props: [ + 'name', 'value', 'fallback', 'disabled', 'label', 'max', 'min', 'step', 'hardMin', 'hardMax' + ], + computed: { + present () { + return typeof this.value !== 'undefined' + } + } +} +</script> diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index 4a236d23..7a955203 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -18,6 +18,7 @@ </transition> </div> <div class="panel-body"> +<keep-alive> <tab-switcher> <div :label="$t('settings.general')" > <div class="setting-item"> @@ -193,6 +194,7 @@ </div> </tab-switcher> +</keep-alive> </div> </div> </template> @@ -204,7 +206,7 @@ @import '../../_variables.scss'; .setting-item { - border-bottom: 2px solid var(--btn, $fallback--btn); + border-bottom: 2px solid var(--fg, $fallback--fg); margin: 1em 1em 1.4em; padding-bottom: 1.4em; @@ -253,12 +255,8 @@ .btn { min-height: 28px; - } - - .submit { - margin-top: 1em; - min-height: 30px; - width: 10em; + min-width: 10em; + padding: 0 2em; } } .select-multiple { diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js new file mode 100644 index 00000000..a1484d09 --- /dev/null +++ b/src/components/shadow_control/shadow_control.js @@ -0,0 +1,86 @@ +import ColorInput from '../color_input/color_input.vue' +import OpacityInput from '../opacity_input/opacity_input.vue' +import { getCssShadow } from '../../services/style_setter/style_setter.js' +import { hex2rgb } from '../../services/color_convert/color_convert.js' + +export default { + // 'Value' and 'Fallback' can be undefined, but if they are + // initially vue won't detect it when they become something else + // therefore i'm using "ready" which should be passed as true when + // data becomes available + props: [ + 'value', 'fallback', 'ready' + ], + data () { + return { + selectedId: 0, + cValue: this.value || this.fallback || [] + } + }, + components: { + ColorInput, + OpacityInput + }, + methods: { + add () { + this.cValue.push(Object.assign({}, this.selected)) + this.selectedId = this.cValue.length - 1 + }, + del () { + this.cValue.splice(this.selectedId, 1) + this.selectedId = this.cValue.length === 0 ? undefined : this.selectedId - 1 + }, + moveUp () { + const movable = this.cValue.splice(this.selectedId, 1)[0] + this.cValue.splice(this.selectedId - 1, 0, movable) + this.selectedId -= 1 + }, + moveDn () { + const movable = this.cValue.splice(this.selectedId, 1)[0] + this.cValue.splice(this.selectedId + 1, 0, movable) + this.selectedId += 1 + } + }, + beforeUpdate () { + this.cValue = this.value || this.fallback + }, + computed: { + selected () { + if (this.ready && this.cValue.length > 0) { + return this.cValue[this.selectedId] + } else { + return { + x: 0, + y: 0, + blur: 0, + spread: 0, + inset: false, + color: '#000000', + alpha: 1 + } + } + }, + moveUpValid () { + return this.ready && this.selectedId > 0 + }, + moveDnValid () { + return this.ready && this.selectedId < this.cValue.length - 1 + }, + present () { + return this.ready && + typeof this.cValue[this.selectedId] !== 'undefined' && + !this.usingFallback + }, + usingFallback () { + return typeof this.value === 'undefined' + }, + rgb () { + return hex2rgb(this.selected.color) + }, + style () { + return this.ready ? { + boxShadow: getCssShadow(this.cValue) + } : {} + } + } +} diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue new file mode 100644 index 00000000..744925d4 --- /dev/null +++ b/src/components/shadow_control/shadow_control.vue @@ -0,0 +1,243 @@ +<template> +<div class="shadow-control" :class="{ disabled: !present }"> + <div class="shadow-preview-container"> + <div :disabled="!present" class="y-shift-control"> + <input + v-model="selected.y" + :disabled="!present" + class="input-number" + type="number"> + <div class="wrap"> + <input + v-model="selected.y" + :disabled="!present" + class="input-range" + type="range" + max="20" + min="-20"> + </div> + </div> + <div class="preview-window"> + <div class="preview-block" :style="style"></div> + </div> + <div :disabled="!present" class="x-shift-control"> + <input + v-model="selected.x" + :disabled="!present" + class="input-number" + type="number"> + <div class="wrap"> + <input + v-model="selected.x" + :disabled="!present" + class="input-range" + type="range" + max="20" + min="-20"> + </div> + </div> + </div> + + <div class="shadow-tweak"> + <div :disabled="usingFallback" class="id-control style-control"> + <label for="shadow-switcher" class="select" :disabled="!ready || usingFallback"> + <select + v-model="selectedId" class="shadow-switcher" + :disabled="!ready || usingFallback" + id="shadow-switcher"> + <option v-for="(shadow, index) in cValue" :value="index"> + {{$t('settings.style.shadows.shadow_id', { value: index })}} + </option> + </select> + <i class="icon-down-open"/> + </label> + <button class="btn btn-default" :disabled="!ready || !present" @click="del"> + <i class="icon-cancel"/> + </button> + <button class="btn btn-default" :disabled="!moveUpValid" @click="moveUp"> + <i class="icon-up-open"/> + </button> + <button class="btn btn-default" :disabled="!moveDnValid" @click="moveDn"> + <i class="icon-down-open"/> + </button> + <button class="btn btn-default" :disabled="usingFallback" @click="add"> + <i class="icon-plus"/> + </button> + </div> + <div :disabled="!present" class="inset-control style-control"> + <label for="inset" class="label"> + {{$t('settings.style.shadows.inset')}} + </label> + <input + v-model="selected.inset" + :disabled="!present" + name="inset" + id="inset" + class="input-inset" + type="checkbox"> + <label class="checkbox-label" for="inset"></label> + </div> + <div :disabled="!present" class="blur-control style-control"> + <label for="spread" class="label"> + {{$t('settings.style.shadows.blur')}} + </label> + <input + v-model="selected.blur" + :disabled="!present" + name="blur" + id="blur" + class="input-range" + type="range" + max="20" + min="0"> + <input + v-model="selected.blur" + :disabled="!present" + class="input-number" + type="number" + min="0"> + </div> + <div :disabled="!present" class="spread-control style-control"> + <label for="spread" class="label"> + {{$t('settings.style.shadows.spread')}} + </label> + <input + v-model="selected.spread" + :disabled="!present" + name="spread" + id="spread" + class="input-range" + type="range" + max="20" + min="-20"> + <input + v-model="selected.spread" + :disabled="!present" + class="input-number" + type="number"> + </div> + <ColorInput + v-model="selected.color" + :disabled="!present" + :label="$t('settings.style.common.color')" + name="shadow"/> + <OpacityInput + v-model="selected.alpha" + :disabled="!present"/> + <p> + {{$t('settings.style.shadows.hint')}} + </p> + </div> +</div> +</template> + +<script src="./shadow_control.js" ></script> + +<style lang="scss"> +@import '../../_variables.scss'; +.shadow-control { + display: flex; + flex-wrap: wrap; + justify-content: center; + margin-bottom: 1em; + + .shadow-preview-container, + .shadow-tweak { + margin: 5px 6px 0 0; + } + .shadow-preview-container { + flex: 0; + display: flex; + flex-wrap: wrap; + + $side: 15em; + + input[type=number] { + width: 5em; + min-width: 2em; + } + .x-shift-control, + .y-shift-control { + display: flex; + flex: 0; + + &[disabled=disabled] *{ + opacity: .5 + } + + } + + .x-shift-control { + align-items: flex-start; + } + + .x-shift-control .wrap, + input[type=range] { + margin: 0; + width: $side; + height: 2em; + } + .y-shift-control { + flex-direction: column; + align-items: flex-end; + .wrap { + width: 2em; + height: $side; + } + input[type=range] { + transform-origin: 1em 1em; + transform: rotate(90deg); + } + } + .preview-window { + flex: 1; + background-color: #999999; + display: flex; + align-items: center; + justify-content: center; + background-image: + linear-gradient(45deg, #666666 25%, transparent 25%), + linear-gradient(-45deg, #666666 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #666666 75%), + linear-gradient(-45deg, transparent 75%, #666666 75%); + background-size: 20px 20px; + background-position:0 0, 0 10px, 10px -10px, -10px 0; + + border-radius: $fallback--inputRadius; + border-radius: var(--inputRadius, $fallback--inputRadius); + + .preview-block { + width: 33%; + height: 33%; + background-color: $fallback--bg; + background-color: var(--bg, $fallback--bg); + border-radius: $fallback--panelRadius; + border-radius: var(--panelRadius, $fallback--panelRadius); + } + } + } + + .shadow-tweak { + flex: 1; + min-width: 280px; + + .id-control { + align-items: stretch; + .select, .btn { + min-width: 1px; + margin-right: 5px; + } + .btn { + padding: 0 .4em; + margin: 0 .1em; + } + .select { + flex: 1; + select { + align-self: initial; + } + } + } + } +} +</style> diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 8087d392..4541c560 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -146,6 +146,7 @@ border-radius: $fallback--tooltipRadius; border-radius: var(--tooltipRadius, $fallback--tooltipRadius); box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5); + box-shadow: var(--popupShadow); margin-top: 0.25em; margin-left: 0.5em; z-index: 50; @@ -284,8 +285,8 @@ margin-left: 0.2em; } a:hover i { - color: $fallback--fg; - color: var(--fg, $fallback--fg); + color: $fallback--text; + color: var(--text, $fallback--text); } } @@ -323,6 +324,8 @@ .status-content { margin-right: 0.5em; + font-family: var(--postFont, sans-serif); + img, video { max-width: 100%; max-height: 400px; @@ -339,6 +342,10 @@ overflow: auto; } + code, samp, kbd, var, pre { + font-family: var(--postCodeFont, monospace); + } + p { margin: 0; margin-top: 0.2em; @@ -464,6 +471,7 @@ .avatar { width: 48px; height: 48px; + box-shadow: var(--avatarStatusShadow); border-radius: $fallback--avatarRadius; border-radius: var(--avatarRadius, $fallback--avatarRadius); overflow: hidden; @@ -532,6 +540,7 @@ a.unmute { .status-el:last-child { border-bottom-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;; border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); + border-bottom: none; } } diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js index 95c15b49..d833341f 100644 --- a/src/components/style_switcher/style_switcher.js +++ b/src/components/style_switcher/style_switcher.js @@ -1,4 +1,25 @@ -import { rgbstr2hex } from '../../services/color_convert/color_convert.js' +import { rgb2hex, hex2rgb, getContrastRatio, alphaBlend } from '../../services/color_convert/color_convert.js' +import { set, delete as del } from 'vue' +import { generateColors, generateShadows, generateRadii, generateFonts, composePreset } from '../../services/style_setter/style_setter.js' +import ColorInput from '../color_input/color_input.vue' +import RangeInput from '../range_input/range_input.vue' +import OpacityInput from '../opacity_input/opacity_input.vue' +import ShadowControl from '../shadow_control/shadow_control.vue' +import FontControl from '../font_control/font_control.vue' +import ContrastRatio from '../contrast_ratio/contrast_ratio.vue' +import TabSwitcher from '../tab_switcher/tab_switcher.jsx' + +// List of color values used in v1 +const v1OnlyNames = [ + 'bg', + 'fg', + 'text', + 'link', + 'cRed', + 'cGreen', + 'cBlue', + 'cOrange' +].map(_ => _ + 'ColorLocal') export default { data () { @@ -6,16 +27,72 @@ export default { availableStyles: [], selected: this.$store.state.config.theme, invalidThemeImported: false, - bgColorLocal: '', - btnColorLocal: '', + + previewShadows: {}, + previewColors: {}, + previewRadii: {}, + previewFonts: {}, + + shadowsInvalid: true, + colorsInvalid: true, + radiiInvalid: true, + + keepShadows: false, + keepOpacity: false, + keepRoundness: false, + keepFonts: false, + textColorLocal: '', linkColorLocal: '', - redColorLocal: '', - blueColorLocal: '', - greenColorLocal: '', - orangeColorLocal: '', + + bgColorLocal: '', + bgOpacityLocal: undefined, + + fgColorLocal: '', + fgTextColorLocal: undefined, + fgLinkColorLocal: undefined, + + btnColorLocal: undefined, + btnTextColorLocal: undefined, + btnOpacityLocal: undefined, + + inputColorLocal: undefined, + inputTextColorLocal: undefined, + inputOpacityLocal: undefined, + + panelColorLocal: undefined, + panelTextColorLocal: undefined, + panelFaintColorLocal: undefined, + panelOpacityLocal: undefined, + + topBarColorLocal: undefined, + topBarTextColorLocal: undefined, + topBarLinkColorLocal: undefined, + + alertErrorColorLocal: undefined, + + badgeOpacityLocal: undefined, + badgeNotificationColorLocal: undefined, + + borderColorLocal: undefined, + borderOpacityLocal: undefined, + + faintColorLocal: undefined, + faintOpacityLocal: undefined, + faintLinkColorLocal: undefined, + + cRedColorLocal: '', + cBlueColorLocal: '', + cGreenColorLocal: '', + cOrangeColorLocal: '', + + shadowSelected: undefined, + shadowsLocal: {}, + fontsLocal: {}, + btnRadiusLocal: '', inputRadiusLocal: '', + checkboxRadiusLocal: '', panelRadiusLocal: '', avatarRadiusLocal: '', avatarAltRadiusLocal: '', @@ -29,19 +106,238 @@ export default { window.fetch('/static/styles.json') .then((data) => data.json()) .then((themes) => { - self.availableStyles = themes + return Promise.all(Object.entries(themes).map(([k, v]) => { + if (typeof v === 'object') { + return Promise.resolve([k, v]) + } else if (typeof v === 'string') { + return window.fetch(v) + .then((data) => data.json()) + .then((theme) => { + return [k, theme] + }) + .catch((e) => { + console.error(e) + return [] + }) + } + })) + }) + .then((promises) => { + return promises + .filter(([k, v]) => v) + .reduce((acc, [k, v]) => { + acc[k] = v + return acc + }, {}) + }).then((themesComplete) => { + self.availableStyles = themesComplete }) }, mounted () { - this.normalizeLocalState(this.$store.state.config.colors, this.$store.state.config.radii) + this.normalizeLocalState(this.$store.state.config.customTheme) + if (typeof this.shadowSelected === 'undefined') { + this.shadowSelected = this.shadowsAvailable[0] + } + }, + computed: { + selectedVersion () { + return Array.isArray(this.selected) ? 1 : 2 + }, + currentColors () { + return { + bg: this.bgColorLocal, + text: this.textColorLocal, + link: this.linkColorLocal, + + fg: this.fgColorLocal, + fgText: this.fgTextColorLocal, + fgLink: this.fgLinkColorLocal, + + panel: this.panelColorLocal, + panelText: this.panelTextColorLocal, + panelFaint: this.panelFaintColorLocal, + + input: this.inputColorLocal, + inputText: this.inputTextColorLocal, + + topBar: this.topBarColorLocal, + topBarText: this.topBarTextColorLocal, + topBarLink: this.topBarLinkColorLocal, + + btn: this.btnColorLocal, + btnText: this.btnTextColorLocal, + + alertError: this.alertErrorColorLocal, + badgeNotification: this.badgeNotificationColorLocal, + + faint: this.faintColorLocal, + faintLink: this.faintLinkColorLocal, + border: this.borderColorLocal, + + cRed: this.cRedColorLocal, + cBlue: this.cBlueColorLocal, + cGreen: this.cGreenColorLocal, + cOrange: this.cOrangeColorLocal + } + }, + currentOpacity () { + return { + bg: this.bgOpacityLocal, + btn: this.btnOpacityLocal, + input: this.inputOpacityLocal, + panel: this.panelOpacityLocal, + topBar: this.topBarOpacityLocal, + border: this.borderOpacityLocal, + faint: this.faintOpacityLocal + } + }, + currentRadii () { + return { + btn: this.btnRadiusLocal, + input: this.inputRadiusLocal, + checkbox: this.checkboxRadiusLocal, + panel: this.panelRadiusLocal, + avatar: this.avatarRadiusLocal, + avatarAlt: this.avatarAltRadiusLocal, + tooltip: this.tooltipRadiusLocal, + attachment: this.attachmentRadiusLocal + } + }, + preview () { + return composePreset(this.previewColors, this.previewRadii, this.previewShadows, this.previewFonts) + }, + previewTheme () { + if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {}, fonts: {} } + return this.preview.theme + }, + // This needs optimization maybe + previewContrast () { + if (!this.previewTheme.colors.bg) return {} + const colors = this.previewTheme.colors + const opacity = this.previewTheme.opacity + if (!colors.bg) return {} + const hints = (ratio) => ({ + text: ratio.toPrecision(3) + ':1', + // AA level, AAA level + aa: ratio >= 4.5, + aaa: ratio >= 7, + // same but for 18pt+ texts + laa: ratio >= 3, + laaa: ratio >= 4.5 + }) + + // fgsfds :DDDD + const fgs = { + text: hex2rgb(colors.text), + panelText: hex2rgb(colors.panelText), + btnText: hex2rgb(colors.btnText), + topBarText: hex2rgb(colors.topBarText), + inputText: hex2rgb(colors.inputText), + + link: hex2rgb(colors.link), + topBarLink: hex2rgb(colors.topBarLink), + + red: hex2rgb(colors.cRed), + green: hex2rgb(colors.cGreen), + blue: hex2rgb(colors.cBlue), + orange: hex2rgb(colors.cOrange) + } + + const bgs = { + bg: hex2rgb(colors.bg), + btn: hex2rgb(colors.btn), + panel: hex2rgb(colors.panel), + topBar: hex2rgb(colors.topBar), + input: hex2rgb(colors.input), + alertError: hex2rgb(colors.alertError), + badgeNotification: hex2rgb(colors.badgeNotification) + } + + /* This is a bit confusing because "bottom layer" used is text color + * This is done to get worst case scenario when background below transparent + * layer matches text color, making it harder to read the lower alpha is. + */ + const ratios = { + bgText: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.text), fgs.text), + bgLink: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.link), fgs.link), + bgRed: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.red), fgs.red), + bgGreen: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.green), fgs.green), + bgBlue: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.blue), fgs.blue), + bgOrange: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.orange), fgs.orange), + + tintText: getContrastRatio(alphaBlend(bgs.bg, 0.5, fgs.panelText), fgs.text), + + panelText: getContrastRatio(alphaBlend(bgs.panel, opacity.panel, fgs.panelText), fgs.panelText), + + btnText: getContrastRatio(alphaBlend(bgs.btn, opacity.btn, fgs.btnText), fgs.btnText), + + inputText: getContrastRatio(alphaBlend(bgs.input, opacity.input, fgs.inputText), fgs.inputText), + + topBarText: getContrastRatio(alphaBlend(bgs.topBar, opacity.topBar, fgs.topBarText), fgs.topBarText), + topBarLink: getContrastRatio(alphaBlend(bgs.topBar, opacity.topBar, fgs.topBarLink), fgs.topBarLink) + } + + return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {}) + }, + previewRules () { + if (!this.preview.rules) return '' + return [ + ...Object.values(this.preview.rules), + 'color: var(--text)', + 'font-family: var(--interfaceFont, sans-serif)' + ].join(';') + }, + shadowsAvailable () { + return Object.keys(this.previewTheme.shadows).sort() + }, + currentShadowOverriden: { + get () { + return !!this.currentShadow + }, + set (val) { + if (val) { + set(this.shadowsLocal, this.shadowSelected, this.currentShadowFallback.map(_ => Object.assign({}, _))) + } else { + del(this.shadowsLocal, this.shadowSelected) + } + } + }, + currentShadowFallback () { + return this.previewTheme.shadows[this.shadowSelected] + }, + currentShadow: { + get () { + return this.shadowsLocal[this.shadowSelected] + }, + set (v) { + set(this.shadowsLocal, this.shadowSelected, v) + } + }, + themeValid () { + return !this.shadowsInvalid && !this.colorsInvalid && !this.radiiInvalid + } + }, + components: { + ColorInput, + OpacityInput, + RangeInput, + ContrastRatio, + ShadowControl, + FontControl, + TabSwitcher }, methods: { exportCurrentTheme () { const stringified = JSON.stringify({ // To separate from other random JSON files and possible future theme formats - _pleroma_theme_version: 1, - colors: this.$store.state.config.colors, - radii: this.$store.state.config.radii + _pleroma_theme_version: 2, + theme: { + shadows: this.shadowsLocal, + fonts: this.fontsLocal, + opacity: this.currentOpacity, + colors: this.currentColors, + radii: this.currentRadii + } }, null, 2) // Pretty-print and indent with 2 spaces // Create an invisible link with a data url and simulate a click @@ -69,7 +365,9 @@ export default { try { const parsed = JSON.parse(target.result) if (parsed._pleroma_theme_version === 1) { - this.normalizeLocalState(parsed.colors, parsed.radii) + this.normalizeLocalState(parsed, 1) + } else if (parsed._pleroma_theme_version === 2) { + this.normalizeLocalState(parsed.theme, 2) } else { // A theme from the future, spooky this.invalidThemeImported = true @@ -89,81 +387,214 @@ export default { }, setCustomTheme () { - if (!this.bgColorLocal && !this.btnColorLocal && !this.linkColorLocal) { - // reset to picked themes - } - - const rgb = (hex) => { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) - return result ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16) - } : null - } - const bgRgb = rgb(this.bgColorLocal) - const btnRgb = rgb(this.btnColorLocal) - const textRgb = rgb(this.textColorLocal) - const linkRgb = rgb(this.linkColorLocal) - - const redRgb = rgb(this.redColorLocal) - const blueRgb = rgb(this.blueColorLocal) - const greenRgb = rgb(this.greenColorLocal) - const orangeRgb = rgb(this.orangeColorLocal) - - if (bgRgb && btnRgb && linkRgb) { - this.$store.dispatch('setOption', { - name: 'customTheme', - value: { - fg: btnRgb, - bg: bgRgb, - text: textRgb, - link: linkRgb, - cRed: redRgb, - cBlue: blueRgb, - cGreen: greenRgb, - cOrange: orangeRgb, - btnRadius: this.btnRadiusLocal, - inputRadius: this.inputRadiusLocal, - panelRadius: this.panelRadiusLocal, - avatarRadius: this.avatarRadiusLocal, - avatarAltRadius: this.avatarAltRadiusLocal, - tooltipRadius: this.tooltipRadiusLocal, - attachmentRadius: this.attachmentRadiusLocal - }}) - } - }, - - normalizeLocalState (colors, radii) { - this.bgColorLocal = rgbstr2hex(colors.bg) - this.btnColorLocal = rgbstr2hex(colors.btn) - this.textColorLocal = rgbstr2hex(colors.fg) - this.linkColorLocal = rgbstr2hex(colors.link) - - this.redColorLocal = rgbstr2hex(colors.cRed) - this.blueColorLocal = rgbstr2hex(colors.cBlue) - this.greenColorLocal = rgbstr2hex(colors.cGreen) - this.orangeColorLocal = rgbstr2hex(colors.cOrange) - - this.btnRadiusLocal = radii.btnRadius || 4 - this.inputRadiusLocal = radii.inputRadius || 4 - this.panelRadiusLocal = radii.panelRadius || 10 - this.avatarRadiusLocal = radii.avatarRadius || 5 - this.avatarAltRadiusLocal = radii.avatarAltRadius || 50 - this.tooltipRadiusLocal = radii.tooltipRadius || 2 - this.attachmentRadiusLocal = radii.attachmentRadius || 5 + this.$store.dispatch('setOption', { + name: 'customTheme', + value: { + shadows: this.shadowsLocal, + fonts: this.fontsLocal, + opacity: this.currentOpacity, + colors: this.currentColors, + radii: this.currentRadii + } + }) + }, + + clearAll () { + this.normalizeLocalState(this.$store.state.config.customTheme) + }, + + // Clears all the extra stuff when loading V1 theme + clearV1 () { + Object.keys(this.$data) + .filter(_ => _.endsWith('ColorLocal') || _.endsWith('OpacityLocal')) + .filter(_ => !v1OnlyNames.includes(_)) + .forEach(key => { + set(this.$data, key, undefined) + }) + }, + + clearRoundness () { + Object.keys(this.$data) + .filter(_ => _.endsWith('RadiusLocal')) + .forEach(key => { + set(this.$data, key, undefined) + }) + }, + + clearOpacity () { + Object.keys(this.$data) + .filter(_ => _.endsWith('OpacityLocal')) + .forEach(key => { + set(this.$data, key, undefined) + }) + }, + + clearShadows () { + this.shadowsLocal = {} + }, + + clearFonts () { + this.fontsLocal = {} + }, + + /** + * This applies stored theme data onto form. + * @param {Object} input - input data + * @param {Number} version - version of data. 0 means try to guess based on data. + */ + normalizeLocalState (input, version = 0) { + const colors = input.colors || input + const radii = input.radii || input + const opacity = input.opacity + const shadows = input.shadows || {} + const fonts = input.fonts || {} + + if (version === 0) { + if (input.version) version = input.version + // Old v1 naming: fg is text, btn is foreground + if (typeof colors.text === 'undefined' && typeof colors.fg !== 'undefined') { + version = 1 + } + // New v2 naming: text is text, fg is foreground + if (typeof colors.text !== 'undefined' && typeof colors.fg !== 'undefined') { + version = 2 + } + } + + // Stuff that differs between V1 and V2 + if (version === 1) { + this.fgColorLocal = rgb2hex(colors.btn) + this.textColorLocal = rgb2hex(colors.fg) + } + + const keys = new Set(version !== 1 ? Object.keys(colors) : []) + if (version === 1) { + // V1 ignores the rest + this.clearV1() + keys + .add('bg') + .add('link') + .add('cRed') + .add('cBlue') + .add('cGreen') + .add('cOrange') + } + keys.forEach(key => { + this[key + 'ColorLocal'] = rgb2hex(colors[key]) + }) + + if (!this.keepRoundness) { + this.clearRoundness() + Object.entries(radii).forEach(([k, v]) => { + // 'Radius' is kept mostly for v1->v2 localstorage transition + const key = k.endsWith('Radius') ? k.split('Radius')[0] : k + this[key + 'RadiusLocal'] = v + }) + } + + if (!this.keepShadows) { + this.clearShadows() + this.shadowsLocal = shadows + this.shadowSelected = this.shadowsAvailable[0] + } + + if (!this.keepFonts) { + this.clearFonts() + this.fontsLocal = fonts + } + + if (opacity && !this.keepOpacity) { + this.clearOpacity() + Object.entries(opacity).forEach(([k, v]) => { + if (typeof v === 'undefined' || v === null || Number.isNaN(v)) return + this[k + 'OpacityLocal'] = v + }) + } } }, watch: { + currentRadii () { + try { + this.previewRadii = generateRadii({ radii: this.currentRadii }) + this.radiiInvalid = false + } catch (e) { + this.radiiInvalid = true + console.warn(e) + } + }, + shadowsLocal: { + handler () { + try { + this.previewShadows = generateShadows({ shadows: this.shadowsLocal }) + this.shadowsInvalid = false + } catch (e) { + this.shadowsInvalid = true + console.warn(e) + } + }, + deep: true + }, + fontsLocal: { + handler () { + try { + this.previewFonts = generateFonts({ fonts: this.fontsLocal }) + this.fontsInvalid = false + } catch (e) { + this.fontsInvalid = true + console.warn(e) + } + }, + deep: true + }, + currentColors () { + try { + this.previewColors = generateColors({ + opacity: this.currentOpacity, + colors: this.currentColors + }) + this.colorsInvalid = false + } catch (e) { + this.colorsInvalid = true + console.warn(e) + } + }, + currentOpacity () { + try { + this.previewColors = generateColors({ + opacity: this.currentOpacity, + colors: this.currentColors + }) + } catch (e) { + console.warn(e) + } + }, selected () { - this.bgColorLocal = this.selected[1] - this.btnColorLocal = this.selected[2] - this.textColorLocal = this.selected[3] - this.linkColorLocal = this.selected[4] - this.redColorLocal = this.selected[5] - this.greenColorLocal = this.selected[6] - this.blueColorLocal = this.selected[7] - this.orangeColorLocal = this.selected[8] + if (this.selectedVersion === 1) { + if (!this.keepRoundness) { + this.clearRoundness() + } + + if (!this.keepShadows) { + this.clearShadows() + } + + if (!this.keepOpacity) { + this.clearOpacity() + } + + this.clearV1() + + this.bgColorLocal = this.selected[1] + this.fgColorLocal = this.selected[2] + this.textColorLocal = this.selected[3] + this.linkColorLocal = this.selected[4] + this.cRedColorLocal = this.selected[5] + this.cGreenColorLocal = this.selected[6] + this.cBlueColorLocal = this.selected[7] + this.cOrangeColorLocal = this.selected[8] + } else if (this.selectedVersion >= 2) { + this.normalizeLocalState(this.selected.theme, 2) + } } } } diff --git a/src/components/style_switcher/style_switcher.scss b/src/components/style_switcher/style_switcher.scss new file mode 100644 index 00000000..2c33224b --- /dev/null +++ b/src/components/style_switcher/style_switcher.scss @@ -0,0 +1,275 @@ +@import '../../_variables.scss'; +.style-switcher { + .preset-switcher { + margin-right: 1em; + } + + .style-control { + display: flex; + align-items: baseline; + margin-bottom: 5px; + + .label { + flex: 1; + } + + &.disabled { + input, select { + &:not(.exclude-disabled) { + opacity: .5 + } + } + } + + input, select { + min-width: 3em; + margin: 0; + flex: 0; + + &[type=color] { + padding: 1px; + cursor: pointer; + height: 29px; + min-width: 2em; + border: none; + align-self: stretch; + } + + &[type=number] { + min-width: 5em; + } + + &[type=range] { + flex: 1; + min-width: 3em; + } + + &[type=checkbox] + label { + margin: 6px 0; + } + + &:not([type=number]):not([type=text]) { + align-self: center; + } + } + } + + .import-warning { + color: $fallback--cRed; + color: var(--cRed, $fallback--cRed); + } + + .tab-switcher { + margin: 0 -1em; + } + + .reset-container { + flex-wrap: wrap; + } + + .fonts-container, + .reset-container, + .apply-container, + .radius-container, + .color-container, + { + display: flex; + } + + .fonts-container, + .radius-container { + flex-direction: column; + } + + .color-container{ + > h4 { + width: 99%; + } + flex-wrap: wrap; + justify-content: space-between; + } + + .fonts-container, + .color-container, + .shadow-container, + .radius-container, + .presets-container { + margin: 1em 1em 0; + } + + .tab-header { + display: flex; + justify-content: space-between; + align-items: baseline; + width: 100%; + min-height: 30px; + + .btn { + min-width: 1px; + flex: 0 auto; + padding: 0 1em; + } + + p { + flex: 1; + margin: 0; + } + + margin-bottom: 1em; + } + + .shadow-selector { + .override { + flex: 1; + margin-left: .5em; + } + .select-container { + margin-top: -4px; + margin-bottom: -3px; + } + } + + .save-load, + .save-load-options { + display: flex; + justify-content: center; + align-items: baseline; + + .import-export { + display: flex; + } + + .override { + margin-left: .5em; + } + } + + .save-load-options { + flex-wrap: wrap; + margin-top: .5em; + span { + margin: 0 .5em; + } + } + + .preview-container { + border-top: 1px dashed; + border-bottom: 1px dashed; + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + margin: 1em -1em 0; + padding: 1em; + background: var(--body-background-image); + background-size: cover; + background-position: 50% 50%; + + .separator { + margin: 1em; + border-bottom: 1px solid; + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + } + + .panel-heading { + .badge, .alert, .btn, .faint { + margin-left: 1em; + } + .flex-spacer { + flex: 1; + } + } + .checkbox { + display: inline-flex; + align-items: baseline; + margin-right: 1em; + } + + .btn { + margin-left: 0; + padding: 0 1em; + min-width: 3em; + min-height: 30px; + } + } + + .apply-container { + justify-content: center; + } + + .radius-item, + .color-item { + min-width: 20em; + margin: 5px 6px 0 0; + display:flex; + flex-direction: column; + flex: 1 1 0; + + &.wide { + min-width: 60% + } + + &:not(.wide):nth-child(2n+1) { + margin-right: 7px; + + } + + .color, .opacity { + display:flex; + align-items: baseline; + } + } + + .radius-item { + flex-basis: auto; + } + + .theme-radius-rn, + .theme-color-cl { + border: 0; + box-shadow: none; + background: transparent; + color: var(--faint, $fallback--faint); + align-self: stretch; + } + + .theme-color-cl, + .theme-radius-in, + .theme-color-in { + margin-left: 4px; + } + + .theme-radius-in { + min-width: 1em; + } + + .theme-radius-in { + max-width: 7em; + flex: 1; + } + + .theme-radius-lb{ + max-width: 50em; + } + + .theme-preview-content { + padding: 20px; + } + + .btn { + margin-left: .25em; + margin-right: .25em; + } + + .dummy { + .avatar { + background: linear-gradient(135deg, #b8e1fc 0%,#a9d2f3 10%,#90bae4 25%,#90bcea 37%,#90bff0 50%,#6ba8e5 51%,#a2daf5 83%,#bdf3fd 100%); + color: black; + text-align: center; + height: 48px; + line-height: 48px; + width: 48px; + float: left; + margin-right: 1em; + } + } +} diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue index 72a338bd..2a7756ed 100644 --- a/src/components/style_switcher/style_switcher.vue +++ b/src/components/style_switcher/style_switcher.vue @@ -1,300 +1,307 @@ <template> -<div> +<div class="style-switcher"> <div class="presets-container"> - <div> - {{$t('settings.presets')}} - <label for="style-switcher" class='select'> - <select id="style-switcher" v-model="selected" class="style-switcher"> - <option v-for="style in availableStyles" - :value="style" - :style="{ - backgroundColor: style[1], - color: style[3] - }"> - {{style[0]}} - </option> - </select> - <i class="icon-down-open"/> - </label> + <div class="save-load"> + <div> + {{$t('settings.presets')}} + <label for="preset-switcher" class='select'> + <select id="preset-switcher" v-model="selected" class="preset-switcher"> + <option v-for="style in availableStyles" + :value="style" + :style="{ + backgroundColor: style[1] || style.theme.colors.bg, + color: style[3] || style.theme.colors.text + }"> + {{style[0] || style.name}} + </option> + </select> + <i class="icon-down-open"/> + </label> + </div> + <div class="import-export"> + <button class="btn" @click="exportCurrentTheme">{{ $t('settings.export_theme') }}</button> + <button class="btn" @click="importTheme">{{ $t('settings.import_theme') }}</button> + <p v-if="invalidThemeImported" class="import-warning">{{ $t('settings.invalid_theme_imported') }}</p> + </div> </div> - <div class="import-export"> - <button class="btn" @click="exportCurrentTheme">{{ $t('settings.export_theme') }}</button> - <button class="btn" @click="importTheme">{{ $t('settings.import_theme') }}</button> - <p v-if="invalidThemeImported" class="import-warning">{{ $t('settings.invalid_theme_imported') }}</p> + <div class="save-load-options"> + <span> + <input + id="keep-shadows" + type="checkbox" + v-model="keepShadows"> + <label for="keep-shadows">{{$t('settings.style.switcher.keep_shadows')}}</label> + </span> + <span> + <input + id="keep-opacity" + type="checkbox" + v-model="keepOpacity"> + <label for="keep-opacity">{{$t('settings.style.switcher.keep_opacity')}}</label> + </span> + <span> + <input + id="keep-roundness" + type="checkbox" + v-model="keepRoundness"> + <label for="keep-roundness">{{$t('settings.style.switcher.keep_roundness')}}</label> + </span> + <span> + <input + id="keep-fonts" + type="checkbox" + v-model="keepFonts"> + <label for="keep-fonts">{{$t('settings.style.switcher.keep_fonts')}}</label> + </span> + <p>{{$t('settings.style.switcher.save_load_hint')}}</p> </div> </div> <div class="preview-container"> - <div :style="{ - '--btnRadius': btnRadiusLocal + 'px', - '--inputRadius': inputRadiusLocal + 'px', - '--panelRadius': panelRadiusLocal + 'px', - '--avatarRadius': avatarRadiusLocal + 'px', - '--avatarAltRadius': avatarAltRadiusLocal + 'px', - '--tooltipRadius': tooltipRadiusLocal + 'px', - '--attachmentRadius': attachmentRadiusLocal + 'px' - }"> - <div class="panel dummy"> - <div class="panel-heading" :style="{ 'background-color': btnColorLocal, 'color': textColorLocal }">Preview</div> - <div class="panel-body theme-preview-content" :style="{ 'background-color': bgColorLocal, 'color': textColorLocal }"> - <div class="avatar" :style="{ - 'border-radius': avatarRadiusLocal + 'px' - }"> - ( ͡° ͜ʖ ͡°) - </div> - <h4>Content</h4> - <br> - A bunch of more content and - <a :style="{ color: linkColorLocal }">a nice lil' link</a> - <i :style="{ color: blueColorLocal }" class="icon-reply"/> - <i :style="{ color: greenColorLocal }" class="icon-retweet"/> - <i :style="{ color: redColorLocal }" class="icon-cancel"/> - <i :style="{ color: orangeColorLocal }" class="icon-star"/> - <br> - <button class="btn" :style="{ 'background-color': btnColorLocal, 'color': textColorLocal }">Button</button> - </div> + <div class="panel dummy" :style="previewRules"> + <div class="panel-heading"> + {{$t('settings.style.preview.header')}} + <span class="badge badge-notification"> + 99 + </span> + <span class="alert error"> + {{$t('settings.style.preview.error')}} + </span> + <button class="btn"> + {{$t('settings.style.preview.button')}} + </button> + <span class="flex-spacer"/> + <span class="faint"> + {{$t('settings.style.preview.header_faint')}} + </span> </div> - </div> - </div> - - <div class="color-container"> - <p>{{$t('settings.theme_help')}}</p> - <div class="color-item"> - <label for="bgcolor" class="theme-color-lb">{{$t('settings.background')}}</label> - <input id="bgcolor" class="theme-color-cl" type="color" v-model="bgColorLocal"> - <input id="bgcolor-t" class="theme-color-in" type="text" v-model="bgColorLocal"> - </div> - <div class="color-item"> - <label for="fgcolor" class="theme-color-lb">{{$t('settings.foreground')}}</label> - <input id="fgcolor" class="theme-color-cl" type="color" v-model="btnColorLocal"> - <input id="fgcolor-t" class="theme-color-in" type="text" v-model="btnColorLocal"> - </div> - <div class="color-item"> - <label for="textcolor" class="theme-color-lb">{{$t('settings.text')}}</label> - <input id="textcolor" class="theme-color-cl" type="color" v-model="textColorLocal"> - <input id="textcolor-t" class="theme-color-in" type="text" v-model="textColorLocal"> - </div> - <div class="color-item"> - <label for="linkcolor" class="theme-color-lb">{{$t('settings.links')}}</label> - <input id="linkcolor" class="theme-color-cl" type="color" v-model="linkColorLocal"> - <input id="linkcolor-t" class="theme-color-in" type="text" v-model="linkColorLocal"> - </div> - <div class="color-item"> - <label for="redcolor" class="theme-color-lb">{{$t('settings.cRed')}}</label> - <input id="redcolor" class="theme-color-cl" type="color" v-model="redColorLocal"> - <input id="redcolor-t" class="theme-color-in" type="text" v-model="redColorLocal"> - </div> - <div class="color-item"> - <label for="bluecolor" class="theme-color-lb">{{$t('settings.cBlue')}}</label> - <input id="bluecolor" class="theme-color-cl" type="color" v-model="blueColorLocal"> - <input id="bluecolor-t" class="theme-color-in" type="text" v-model="blueColorLocal"> - </div> - <div class="color-item"> - <label for="greencolor" class="theme-color-lb">{{$t('settings.cGreen')}}</label> - <input id="greencolor" class="theme-color-cl" type="color" v-model="greenColorLocal"> - <input id="greencolor-t" class="theme-color-in" type="green" v-model="greenColorLocal"> - </div> - <div class="color-item"> - <label for="orangecolor" class="theme-color-lb">{{$t('settings.cOrange')}}</label> - <input id="orangecolor" class="theme-color-cl" type="color" v-model="orangeColorLocal"> - <input id="orangecolor-t" class="theme-color-in" type="text" v-model="orangeColorLocal"> - </div> - </div> - - <div class="radius-container"> - <p>{{$t('settings.radii_help')}}</p> - <div class="radius-item"> - <label for="btnradius" class="theme-radius-lb">{{$t('settings.btnRadius')}}</label> - <input id="btnradius" class="theme-radius-rn" type="range" v-model="btnRadiusLocal" max="16"> - <input id="btnradius-t" class="theme-radius-in" type="text" v-model="btnRadiusLocal"> - </div> - <div class="radius-item"> - <label for="inputradius" class="theme-radius-lb">{{$t('settings.inputRadius')}}</label> - <input id="inputradius" class="theme-radius-rn" type="range" v-model="inputRadiusLocal" max="16"> - <input id="inputradius-t" class="theme-radius-in" type="text" v-model="inputRadiusLocal"> - </div> - <div class="radius-item"> - <label for="panelradius" class="theme-radius-lb">{{$t('settings.panelRadius')}}</label> - <input id="panelradius" class="theme-radius-rn" type="range" v-model="panelRadiusLocal" max="50"> - <input id="panelradius-t" class="theme-radius-in" type="text" v-model="panelRadiusLocal"> - </div> - <div class="radius-item"> - <label for="avatarradius" class="theme-radius-lb">{{$t('settings.avatarRadius')}}</label> - <input id="avatarradius" class="theme-radius-rn" type="range" v-model="avatarRadiusLocal" max="28"> - <input id="avatarradius-t" class="theme-radius-in" type="green" v-model="avatarRadiusLocal"> - </div> - <div class="radius-item"> - <label for="avataraltradius" class="theme-radius-lb">{{$t('settings.avatarAltRadius')}}</label> - <input id="avataraltradius" class="theme-radius-rn" type="range" v-model="avatarAltRadiusLocal" max="28"> - <input id="avataraltradius-t" class="theme-radius-in" type="text" v-model="avatarAltRadiusLocal"> - </div> - <div class="radius-item"> - <label for="attachmentradius" class="theme-radius-lb">{{$t('settings.attachmentRadius')}}</label> - <input id="attachmentrradius" class="theme-radius-rn" type="range" v-model="attachmentRadiusLocal" max="50"> - <input id="attachmentradius-t" class="theme-radius-in" type="text" v-model="attachmentRadiusLocal"> - </div> - <div class="radius-item"> - <label for="tooltipradius" class="theme-radius-lb">{{$t('settings.tooltipRadius')}}</label> - <input id="tooltipradius" class="theme-radius-rn" type="range" v-model="tooltipRadiusLocal" max="20"> - <input id="tooltipradius-t" class="theme-radius-in" type="text" v-model="tooltipRadiusLocal"> - </div> - </div> - - <div class="apply-container"> - <button class="btn submit" @click="setCustomTheme">{{$t('general.apply')}}</button> - </div> -</div> -</template> - -<script src="./style_switcher.js"></script> - -<style lang="scss"> -@import '../../_variables.scss'; -.style-switcher { - margin-right: 1em; -} - -.import-warning { - color: $fallback--cRed; - color: var(--cRed, $fallback--cRed); -} - -.apply-container, -.radius-container, -.color-container, -.presets-container { - display: flex; - - p { - flex: 2 0 100%; - margin-top: 2em; - margin-bottom: .5em; - } -} - -.radius-container { - flex-direction: column; -} - -.color-container { - flex-wrap: wrap; - justify-content: space-between; -} - -.presets-container { - justify-content: center; - .import-export { - display: flex; - - .btn { - margin-left: .5em; - } - } -} - -.preview-container { - border-top: 1px dashed; - border-bottom: 1px dashed; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - margin: 1em -1em 0; - padding: 1em; - - .btn { - margin-top: 1em; - min-height: 30px; - width: 10em; - } -} - -.apply-container { - justify-content: center; -} + <div class="panel-body theme-preview-content"> + <div class="avatar"> + ( ͡° ͜ʖ ͡°) + </div> + <h4>Content</h4> -.radius-item, -.color-item { - min-width: 20em; - display:flex; - flex: 1 1 0; - align-items: baseline; - margin: 5px 6px 5px 0; + <br> - label { - color: var(--faint, $fallback--faint); - } -} + <i18n path="settings.style.preview.text"> + <a style="color: var(--link)"> + {{$t('settings.style.preview.link')}} + </a> + </i18n> -.radius-item { - flex-basis: auto; -} + <i style="color: var(--cBlue)" class="icon-reply"/> + <i style="color: var(--cGreen)" class="icon-retweet"/> + <i style="color: var(--cRed)" class="icon-cancel"/> + <i style="color: var(--cOrange)" class="icon-star"/> -.theme-radius-rn, -.theme-color-cl { - border: 0; - box-shadow: none; - background: transparent; - color: var(--faint, $fallback--faint); - align-self: stretch; -} + <br> + <br> -.theme-color-cl, -.theme-radius-in, -.theme-color-in { - margin-left: 4px; -} + <input :value="$t('settings.style.preview.error')" type="text"> + <span class="alert error"> + {{$t('settings.style.preview.error')}} + </span> -.theme-color-in { - min-width: 4em; -} + <br> + <br> -.theme-radius-in { - min-width: 1em; -} + <span class="checkbox"> + <input checked="very yes" type="checkbox" id="preview_checkbox"> + <label for="preview_checkbox">{{$t('settings.style.preview.checkbox')}}</label> + </span> + <button class="btn"> + {{$t('settings.style.preview.button')}} + </button> -.theme-radius-in, -.theme-color-in { - max-width: 7em; - flex: 1; -} + <div class="separator"></div> -.theme-radius-lb, -.theme-color-lb { - flex: 2; - min-width: 7em; -} + <i18n path="settings.style.preview.fine_print" tag="span" class="faint"> + <a style="color: var(--faintLink)"> + {{$t('settings.style.preview.faint_link')}} + </a> + </i18n> + </div> + </div> + </div> -.theme-radius-lb{ - max-width: 50em; -} + <keep-alive> + <tab-switcher key="style-tweak"> + <div :label="$t('settings.style.common_colors._tab_label')" class="color-container"> + <div class="tab-header"> + <p>{{$t('settings.theme_help')}}</p> + <button class="btn" @click="clearOpacity">{{$t('settings.style.switcher.clear_opacity')}}</button> + <button class="btn" @click="clearV1">{{$t('settings.style.switcher.clear_all')}}</button> + </div> + <p>{{$t('settings.theme_help_v2_1')}}</p> + <h4>{{ $t('settings.style.common_colors.main') }}</h4> + <div class="color-item"> + <ColorInput name="bgColor" v-model="bgColorLocal" :label="$t('settings.background')"/> + <OpacityInput name="bgOpacity" v-model="bgOpacityLocal" :fallback="previewTheme.opacity.bg || 1"/> + <ColorInput name="textColor" v-model="textColorLocal" :label="$t('settings.text')"/> + <ContrastRatio :contrast="previewContrast.bgText"/> + <ColorInput name="linkColor" v-model="linkColorLocal" :label="$t('settings.links')"/> + <ContrastRatio :contrast="previewContrast.bgLink"/> + </div> + <div class="color-item"> + <ColorInput name="fgColor" v-model="fgColorLocal" :label="$t('settings.foreground')"/> + <ColorInput name="fgTextColor" v-model="fgTextColorLocal" :label="$t('settings.text')" :fallback="previewTheme.colors.fgText"/> + <ColorInput name="fgLinkColor" v-model="fgLinkColorLocal" :label="$t('settings.links')" :fallback="previewTheme.colors.fgLink"/> + <p>{{ $t('settings.style.common_colors.foreground_hint') }}</p> + </div> + <h4>{{ $t('settings.style.common_colors.rgbo') }}</h4> + <div class="color-item"> + <ColorInput name="cRedColor" v-model="cRedColorLocal" :label="$t('settings.cRed')"/> + <ContrastRatio :contrast="previewContrast.bgRed"/> + <ColorInput name="cBlueColor" v-model="cBlueColorLocal" :label="$t('settings.cBlue')"/> + <ContrastRatio :contrast="previewContrast.bgBlue"/> + </div> + <div class="color-item"> + <ColorInput name="cGreenColor" v-model="cGreenColorLocal" :label="$t('settings.cGreen')"/> + <ContrastRatio :contrast="previewContrast.bgGreen"/> + <ColorInput name="cOrangeColor" v-model="cOrangeColorLocal" :label="$t('settings.cOrange')"/> + <ContrastRatio :contrast="previewContrast.bgOrange"/> + </div> + <p>{{$t('settings.theme_help_v2_2')}}</p> + </div> -.theme-color-lb { - max-width: 10em; -} + <div :label="$t('settings.style.advanced_colors._tab_label')" class="color-container"> + <div class="tab-header"> + <p>{{$t('settings.theme_help')}}</p> + <button class="btn" @click="clearOpacity">{{$t('settings.style.switcher.clear_opacity')}}</button> + <button class="btn" @click="clearV1">{{$t('settings.style.switcher.clear_all')}}</button> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.alert') }}</h4> + <ColorInput name="alertError" v-model="alertErrorColorLocal" :label="$t('settings.style.advanced_colors.alert_error')" :fallback="previewTheme.colors.alertError"/> + <ContrastRatio :contrast="previewContrast.alertError"/> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.badge') }}</h4> + <ColorInput name="badgeNotification" v-model="badgeNotificationColorLocal" :label="$t('settings.style.advanced_colors.badge_notification')" :fallback="previewTheme.colors.badgeNotification"/> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.panel_header') }}</h4> + <ColorInput name="panelColor" v-model="panelColorLocal" :fallback="fgColorLocal" :label="$t('settings.background')"/> + <OpacityInput name="panelOpacity" v-model="panelOpacityLocal" :fallback="previewTheme.opacity.panel || 1"/> + <ColorInput name="panelTextColor" v-model="panelTextColorLocal" :fallback="previewTheme.colors.panelText" :label="$t('settings.links')"/> + <ContrastRatio :contrast="previewContrast.panelText" large="1"/> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.top_bar') }}</h4> + <ColorInput name="topBarColor" v-model="topBarColorLocal" :fallback="fgColorLocal" :label="$t('settings.background')"/> + <ColorInput name="topBarTextColor" v-model="topBarTextColorLocal" :fallback="previewTheme.colors.topBarText" :label="$t('settings.text')"/> + <ContrastRatio :contrast="previewContrast.topBarText"/> + <ColorInput name="topBarLinkColor" v-model="topBarLinkColorLocal" :fallback="previewTheme.colors.topBarLink" :label="$t('settings.links')"/> + <ContrastRatio :contrast="previewContrast.topBarLink"/> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.inputs') }}</h4> + <ColorInput name="inputColor" v-model="inputColorLocal" :fallback="fgColorLocal" :label="$t('settings.background')"/> + <OpacityInput name="inputOpacity" v-model="inputOpacityLocal" :fallback="previewTheme.opacity.input || 1"/> + <ColorInput name="inputTextColor" v-model="inputTextColorLocal" :fallback="previewTheme.colors.inputText" :label="$t('settings.text')"/> + <ContrastRatio :contrast="previewContrast.inputText"/> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.buttons') }}</h4> + <ColorInput name="btnColor" v-model="btnColorLocal" :fallback="fgColorLocal" :label="$t('settings.background')"/> + <OpacityInput name="btnOpacity" v-model="btnOpacityLocal" :fallback="previewTheme.opacity.btn || 1"/> + <ColorInput name="btnTextColor" v-model="btnTextColorLocal" :fallback="previewTheme.colors.btnText" :label="$t('settings.text')"/> + <ContrastRatio :contrast="previewContrast.btnText"/> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.borders') }}</h4> + <ColorInput name="borderColor" v-model="borderColorLocal" :fallback="previewTheme.colors.border" label="Color"/> + <OpacityInput name="borderOpacity" v-model="borderOpacityLocal" :fallback="previewTheme.opacity.border || 1"/> + </div> + <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.faint_text') }}</h4> + <ColorInput name="faintColor" v-model="faintColorLocal" :fallback="previewTheme.colors.faint || 1" :label="$t('settings.text')"/> + <ColorInput name="faintLinkColor" v-model="faintLinkColorLocal" :fallback="previewTheme.colors.faintLink" :label="$t('settings.links')"/> + <ColorInput name="panelFaintColor" v-model="panelFaintColorLocal" :fallback="previewTheme.colors.panelFaint" :label="$t('settings.style.advanced_colors.panel_header')"/> + <OpacityInput name="faintOpacity" v-model="faintOpacityLocal" :fallback="previewTheme.opacity.faint || 0.5"/> + </div> + </div> + <div :label="$t('settings.style.radii._tab_label')" class="radius-container"> + <div class="tab-header"> + <p>{{$t('settings.radii_help')}}</p> + <button class="btn" @click="clearRoundness">{{$t('settings.style.switcher.clear_all')}}</button> + </div> + <RangeInput name="btnRadius" :label="$t('settings.btnRadius')" v-model="btnRadiusLocal" :fallback="previewTheme.radii.btn" max="16" hardMin="0"/> + <RangeInput name="inputRadius" :label="$t('settings.inputRadius')" v-model="inputRadiusLocal" :fallback="previewTheme.radii.input" max="9" hardMin="0"/> + <RangeInput name="checkboxRadius" :label="$t('settings.checkboxRadius')" v-model="checkboxRadiusLocal" :fallback="previewTheme.radii.checkbox" max="16" hardMin="0"/> + <RangeInput name="panelRadius" :label="$t('settings.panelRadius')" v-model="panelRadiusLocal" :fallback="previewTheme.radii.panel" max="50" hardMin="0"/> + <RangeInput name="avatarRadius" :label="$t('settings.avatarRadius')" v-model="avatarRadiusLocal" :fallback="previewTheme.radii.avatar" max="28" hardMin="0"/> + <RangeInput name="avatarAltRadius" :label="$t('settings.avatarAltRadius')" v-model="avatarAltRadiusLocal" :fallback="previewTheme.radii.avatarAlt" max="28" hardMin="0"/> + <RangeInput name="attachmentRadius" :label="$t('settings.attachmentRadius')" v-model="attachmentRadiusLocal" :fallback="previewTheme.radii.attachment" max="50" hardMin="0"/> + <RangeInput name="tooltipRadius" :label="$t('settings.tooltipRadius')" v-model="tooltipRadiusLocal" :fallback="previewTheme.radii.tooltip" max="50" hardMin="0"/> + </div> + <div :label="$t('settings.style.shadows._tab_label')" class="shadow-container"> + <div class="tab-header shadow-selector"> + <div class="select-container"> + {{$t('settings.style.shadows.component')}} + <label for="shadow-switcher" class="select"> + <select id="shadow-switcher" v-model="shadowSelected" class="shadow-switcher"> + <option v-for="shadow in shadowsAvailable" + :value="shadow"> + {{$t('settings.style.shadows.components.' + shadow)}} + </option> + </select> + <i class="icon-down-open"/> + </label> + </div> + <div class="override"> + <label for="override" class="label"> + {{$t('settings.style.shadows.override')}} + </label> + <input + v-model="currentShadowOverriden" + name="override" + id="override" + class="input-override" + type="checkbox"> + <label class="checkbox-label" for="override"></label> + </div> + <button class="btn" @click="clearShadows">{{$t('settings.style.switcher.clear_all')}}</button> + </div> + <shadow-control :ready="!!currentShadowFallback" :fallback="currentShadowFallback" v-model="currentShadow"/> + </div> + <div :label="$t('settings.style.fonts._tab_label')" class="fonts-container"> + <div class="tab-header"> + <p>{{$t('settings.style.fonts.help')}}</p> + <button class="btn" @click="clearFonts">{{$t('settings.style.switcher.clear_all')}}</button> + </div> + <FontControl + name="ui" + v-model="fontsLocal.interface" + :label="$t('settings.style.fonts.components.interface')" + :fallback="previewTheme.fonts.interface" + no-inherit="1"/> + <FontControl + name="input" + v-model="fontsLocal.input" + :label="$t('settings.style.fonts.components.input')" + :fallback="previewTheme.fonts.input"/> + <FontControl + name="post" + v-model="fontsLocal.post" + :label="$t('settings.style.fonts.components.post')" + :fallback="previewTheme.fonts.post"/> + <FontControl + name="postCode" + v-model="fontsLocal.postCode" + :label="$t('settings.style.fonts.components.postCode')" + :fallback="previewTheme.fonts.postCode"/> + </div> + </tab-switcher> + </keep-alive> -.theme-color-cl { - padding: 1px; - max-width: 8em; - height: 100%; - flex: 0; - min-width: 2em; - cursor: pointer; - max-height: 29px; -} + <div class="apply-container"> + <button class="btn submit" :disabled="!themeValid" @click="setCustomTheme">{{$t('general.apply')}}</button> + <button class="btn" @click="clearAll">{{$t('settings.style.switcher.reset')}}</button> + </div> +</div> +</template> -.theme-preview-content { - padding: 20px; -} +<script src="./style_switcher.js"></script> -.dummy { - .avatar { - background: linear-gradient(135deg, #b8e1fc 0%,#a9d2f3 10%,#90bae4 25%,#90bcea 37%,#90bff0 50%,#6ba8e5 51%,#a2daf5 83%,#bdf3fd 100%); - color: black; - text-align: center; - height: 48px; - line-height: 48px; - width: 48px; - float: left; - margin-right: 1em; - } -} -</style> +<style src="./style_switcher.scss" lang="scss"></style> diff --git a/src/components/tab_switcher/tab_switcher.jsx b/src/components/tab_switcher/tab_switcher.jsx index 3fff38f6..9e3dee04 100644 --- a/src/components/tab_switcher/tab_switcher.jsx +++ b/src/components/tab_switcher/tab_switcher.jsx @@ -25,11 +25,14 @@ export default Vue.component('tab-switcher', { } return (<button onClick={this.activateTab(index)} class={ classes.join(' ') }>{slot.data.attrs.label}</button>) }); - const contents = ( - <div> - {this.$slots.default.filter(slot => slot.data)[this.active]} - </div> - ); + const contents = this.$slots.default.filter(_=>_.data).map(( slot, index ) => { + const active = index === this.active + return ( + <div class={active ? 'active' : 'hidden'}> + {slot} + </div> + ) + }); return ( <div class="tab-switcher"> <div class="tabs"> diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss index 374a19c5..d0e5ea87 100644 --- a/src/components/tab_switcher/tab_switcher.scss +++ b/src/components/tab_switcher/tab_switcher.scss @@ -1,6 +1,11 @@ @import '../../_variables.scss'; .tab-switcher { + .contents { + .hidden { + display: none; + } + } .tabs { display: flex; position: relative; @@ -8,6 +13,8 @@ width: 100%; overflow: hidden; padding-top: 5px; + height: 32px; + box-sizing: border-box; &::after, &::before { display: block; @@ -17,20 +24,33 @@ .tab, &::after, &::before { border-bottom: 1px solid; - border-bottom-color: $fallback--btn; - border-bottom-color: var(--btn, $fallback--btn); + border-bottom-color: $fallback--border; + border-bottom-color: var(--border, $fallback--border); } .tab { + position: relative; border-bottom-left-radius: 0; border-bottom-right-radius: 0; - padding: .3em 1em; + padding: 5px 1em 99px; &:not(.active) { - border-bottom: 1px solid; - border-bottom-color: $fallback--btn; - border-bottom-color: var(--btn, $fallback--btn); z-index: 4; + + &:hover { + z-index: 6; + } + + &::after { + content: ''; + position: absolute; + left: 0; + right: 0; + top: 26px; + border-bottom: 1px solid; + border-bottom-color: $fallback--border; + border-bottom-color: var(--border, $fallback--border); + } } &.active { diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index 2dd4376a..b69a09fd 100644 --- a/src/components/timeline/timeline.vue +++ b/src/components/timeline/timeline.vue @@ -58,15 +58,7 @@ .timeline { .loadmore-text { - opacity: 0.8; - background-color: transparent; - color: $fallback--faint; - color: var(--faint, $fallback--faint); - } - - .loadmore-error { - color: $fallback--fg; - color: var(--fg, $fallback--fg); + opacity: 1; } } @@ -79,7 +71,7 @@ border-color: var(--border, $fallback--border); padding: 10px; z-index: 1; - background-color: $fallback--btn; - background-color: var(--btn, $fallback--btn); + background-color: $fallback--fg; + background-color: var(--panel, $fallback--fg); } </style> diff --git a/src/components/user_card_content/user_card_content.js b/src/components/user_card_content/user_card_content.js index b5dd9b91..064c984d 100644 --- a/src/components/user_card_content/user_card_content.js +++ b/src/components/user_card_content/user_card_content.js @@ -12,9 +12,9 @@ export default { }, computed: { headingStyle () { - const color = this.$store.state.config.colors.bg + const color = this.$store.state.config.customTheme.colors.bg if (color) { - const rgb = hex2rgb(color) + const rgb = (typeof color === 'string') ? hex2rgb(color) : color const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .5)` return { backgroundColor: `rgb(${Math.floor(rgb.r * 0.53)}, ${Math.floor(rgb.g * 0.56)}, ${Math.floor(rgb.b * 0.59)})`, diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue index 84669d7f..bb1e314f 100644 --- a/src/components/user_card_content/user_card_content.vue +++ b/src/components/user_card_content/user_card_content.vue @@ -41,74 +41,75 @@ </div> </div> <div v-if="isOtherUser" class="user-interactions"> - <div class="follow" v-if="loggedIn"> - <span v-if="user.following"> - <!--Following them!--> - <button @click="unfollowUser" class="pressed"> - {{ $t('user_card.following') }} - </button> - </span> - <span v-if="!user.following"> - <button @click="followUser"> - {{ $t('user_card.follow') }} - </button> - </span> - </div> - <div class='mute' v-if='isOtherUser'> - <span v-if='user.muted'> - <button @click="toggleMute" class="pressed"> - {{ $t('user_card.muted') }} - </button> - </span> - <span v-if='!user.muted'> - <button @click="toggleMute"> - {{ $t('user_card.mute') }} - </button> - </span> - </div> - <div class="remote-follow" v-if='!loggedIn && user.is_local'> - <form method="POST" :action='subscribeUrl'> - <input type="hidden" name="nickname" :value="user.screen_name"> - <input type="hidden" name="profile" value=""> - <button click="submit" class="remote-button"> - {{ $t('user_card.remote_follow') }} - </button> - </form> - </div> - <div class='block' v-if='isOtherUser && loggedIn'> - <span v-if='user.statusnet_blocking'> - <button @click="unblockUser" class="pressed"> - {{ $t('user_card.blocked') }} - </button> - </span> - <span v-if='!user.statusnet_blocking'> - <button @click="blockUser"> - {{ $t('user_card.block') }} - </button> - </span> - </div> + <div class="follow" v-if="loggedIn"> + <span v-if="user.following"> + <!--Following them!--> + <button @click="unfollowUser" class="pressed"> + {{ $t('user_card.following') }} + </button> + </span> + <span v-if="!user.following"> + <button @click="followUser"> + {{ $t('user_card.follow') }} + </button> + </span> </div> - </div> - </div> - <div class="panel-body profile-panel-body"> - <div v-if="!hideUserStatsLocal || switcher" class="user-counts" :class="{clickable: switcher}"> - <div class="user-count" v-on:click.prevent="setProfileView('statuses')" :class="{selected: selected === 'statuses'}"> - <h5>{{ $t('user_card.statuses') }}</h5> - <span v-if="!hideUserStatsLocal">{{user.statuses_count}} <br></span> + <div class='mute' v-if='isOtherUser'> + <span v-if='user.muted'> + <button @click="toggleMute" class="pressed"> + {{ $t('user_card.muted') }} + </button> + </span> + <span v-if='!user.muted'> + <button @click="toggleMute"> + {{ $t('user_card.mute') }} + </button> + </span> </div> - <div class="user-count" v-on:click.prevent="setProfileView('friends')" :class="{selected: selected === 'friends'}"> - <h5>{{ $t('user_card.followees') }}</h5> - <span v-if="!hideUserStatsLocal">{{user.friends_count}}</span> + <div class="remote-follow" v-if='!loggedIn && user.is_local'> + <form method="POST" :action='subscribeUrl'> + <input type="hidden" name="nickname" :value="user.screen_name"> + <input type="hidden" name="profile" value=""> + <button click="submit" class="remote-button"> + {{ $t('user_card.remote_follow') }} + </button> + </form> </div> - <div class="user-count" v-on:click.prevent="setProfileView('followers')" :class="{selected: selected === 'followers'}"> - <h5>{{ $t('user_card.followers') }}</h5> - <span v-if="!hideUserStatsLocal">{{user.followers_count}}</span> + <div class='block' v-if='isOtherUser && loggedIn'> + <span v-if='user.statusnet_blocking'> + <button @click="unblockUser" class="pressed"> + {{ $t('user_card.blocked') }} + </button> + </span> + <span v-if='!user.statusnet_blocking'> + <button @click="blockUser"> + {{ $t('user_card.block') }} + </button> + </span> </div> </div> - <p v-if="!hideBio && user.description_html" class="profile-bio" v-html="user.description_html"></p> - <p v-else-if="!hideBio" class="profile-bio">{{ user.description }}</p> </div> </div> + <div class="panel-body profile-panel-body"> + <div v-if="!hideUserStatsLocal || switcher" class="user-counts" :class="{clickable: switcher}"> + <div class="user-count" v-on:click.prevent="setProfileView('statuses')" :class="{selected: selected === 'statuses'}"> + <h5>{{ $t('user_card.statuses') }}</h5> + <span v-if="!hideUserStatsLocal">{{user.statuses_count}} <br></span> + </div> + <div class="user-count" v-on:click.prevent="setProfileView('friends')" :class="{selected: selected === 'friends'}"> + <h5>{{ $t('user_card.followees') }}</h5> + <span v-if="!hideUserStatsLocal">{{user.friends_count}}</span> + </div> + <div class="user-count" v-on:click.prevent="setProfileView('followers')" :class="{selected: selected === 'followers'}"> + <h5>{{ $t('user_card.followers') }}</h5> + <span v-if="!hideUserStatsLocal">{{user.followers_count}}</span> + </div> + </div> + </div> + <p v-if="!hideBio && user.description_html" class="profile-bio" v-html="user.description_html"></p> + <p v-else-if="!hideBio" class="profile-bio">{{ user.description }}</p> +</div> +</div> </template> <script src="./user_card_content.js"></script> @@ -120,10 +121,12 @@ background-size: cover; border-radius: $fallback--panelRadius; border-radius: var(--panelRadius, $fallback--panelRadius); + overflow: hidden; .panel-heading { padding: 0.6em 0em; text-align: center; + box-shadow: none; } } @@ -138,15 +141,14 @@ } .user-info { - color: $fallback--lightFg; - color: var(--lightFg, $fallback--lightFg); + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); padding: 0 16px; .container { padding: 16px 10px 6px 10px; display: flex; max-height: 56px; - overflow: hidden; .avatar { border-radius: $fallback--avatarRadius; @@ -155,6 +157,7 @@ width: 56px; height: 56px; box-shadow: 0px 1px 8px rgba(0,0,0,0.75); + box-shadow: var(--avatarShadow); object-fit: cover; &.animated::before { @@ -173,8 +176,8 @@ } .usersettings { - color: $fallback--lightFg; - color: var(--lightFg, $fallback--lightFg); + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); opacity: .8; } @@ -185,6 +188,16 @@ text-overflow: ellipsis; white-space: nowrap; flex: 1 1 0; + // This is so that text doesn't get overlapped by avatar's shadow if it has + // big one + z-index: 1; + + img { + width: 26px; + height: 26px; + vertical-align: middle; + object-fit: contain + } } .user-name{ @@ -193,8 +206,8 @@ } .user-screen-name { - color: $fallback--lightFg; - color: var(--lightFg, $fallback--lightFg); + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); display: inline-block; font-weight: light; font-size: 15px; @@ -269,8 +282,8 @@ padding: .5em 1.5em 0em 1.5em; text-align: center; justify-content: space-between; - color: $fallback--lightFg; - color: var(--lightFg, $fallback--lightFg); + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); &.clickable { .user-count { |
