fix radio buttons, polish stuff

This commit is contained in:
shpuld 2019-06-13 22:01:11 +03:00
parent d7b75ba037
commit 32dae8eb7a
14 changed files with 156 additions and 394 deletions

View File

@ -1,4 +1,5 @@
import Timeago from '../timeago/timeago.vue' import Timeago from '../timeago/timeago.vue'
import { forEach } from 'lodash'
export default { export default {
name: 'Poll', name: 'Poll',
@ -14,6 +15,7 @@ export default {
}, },
created () { created () {
this.refreshInterval = setTimeout(this.refreshPoll, 30 * 1000) this.refreshInterval = setTimeout(this.refreshPoll, 30 * 1000)
this.multipleChoices = this.poll.options.map(_ => false)
}, },
destroyed () { destroyed () {
clearTimeout(this.refreshInterval) clearTimeout(this.refreshInterval)
@ -38,7 +40,7 @@ export default {
}, },
choiceIndices () { choiceIndices () {
return this.multipleChoices return this.multipleChoices
.map((entry, index) => index) .map((entry, index) => entry && index)
.filter(value => typeof value === 'number') .filter(value => typeof value === 'number')
}, },
isDisabled () { isDisabled () {
@ -55,7 +57,7 @@ export default {
this.refreshInterval = setTimeout(this.refreshPoll, 30 * 1000) this.refreshInterval = setTimeout(this.refreshPoll, 30 * 1000)
}, },
percentageForOption (count) { percentageForOption (count) {
return this.totalVotesCount === 0 ? 0 : Math.round((count + 5) / (this.totalVotesCount + 10) * 100) return this.totalVotesCount === 0 ? 0 : Math.round(count / this.totalVotesCount * 100)
}, },
resultTitle (option) { resultTitle (option) {
return `${option.votes_count}/${this.totalVotesCount} ${this.$t('polls.votes')}` return `${option.votes_count}/${this.totalVotesCount} ${this.$t('polls.votes')}`
@ -63,6 +65,31 @@ export default {
fetchPoll () { fetchPoll () {
this.$store.dispatch('refreshPoll', { id: this.statusId, pollId: this.poll.id }) this.$store.dispatch('refreshPoll', { id: this.statusId, pollId: this.poll.id })
}, },
activateOption (index) {
// forgive me father: doing checking the radio/checkboxes
// in code because of customized input elements need either
// a) an extra element for the actual graphic, or b) use a
// pseudo element for the label. We use b) which mandates
// using "for" and "id" matching which isn't nice when the
// same poll appears multiple times on the site (notifs and
// timeline for example). With code we can make sure it just
// works without altering the pseudo element implementation.
const clickedElement = this.$el.querySelector(`input[value="${index}"]`)
if (this.poll.multiple) {
// Checkboxes
const wasChecked = this.multipleChoices[index]
clickedElement.checked = !wasChecked
this.$set(this.multipleChoices, index, !wasChecked)
} else {
// Radio button
const allElements = this.$el.querySelectorAll('input')
forEach(allElements, element => {
element.checked = false
})
clickedElement.checked = true
this.singleChoiceIndex = index
}
},
optionId (index) { optionId (index) {
return `poll${this.poll.id}-${index}` return `poll${this.poll.id}-${index}`
}, },

View File

@ -18,24 +18,20 @@
> >
</div> </div>
</div> </div>
<div v-else> <div v-else @click="activateOption(index)">
<input <input
v-if="poll.multiple" v-if="poll.multiple"
type="checkbox" type="checkbox"
:id="optionId(index)"
:disabled="loading" :disabled="loading"
:value="option.title" :value="index"
v-model="multipleChoices[index]"
> >
<input <input
v-else v-else
type="radio" type="radio"
:id="optionId(index)"
:disabled="loading" :disabled="loading"
:value="index" :value="index"
v-model="singleChoiceIndex"
> >
<label :for="optionId(index)"> <label>
{{option.title}} {{option.title}}
</label> </label>
</div> </div>
@ -54,7 +50,7 @@
{{totalVotesCount}} {{ $t("polls.votes") }}&nbsp;·&nbsp; {{totalVotesCount}} {{ $t("polls.votes") }}&nbsp;·&nbsp;
</div> </div>
<i18n :path="expired ? 'polls.expired' : 'polls.expires_in'"> <i18n :path="expired ? 'polls.expired' : 'polls.expires_in'">
<Timeago :time="this.poll.expires_at" :auto-update="60" /> <Timeago :time="this.poll.expires_at" :auto-update="60" :now-threshold="0" />
</i18n> </i18n>
</div> </div>
</div> </div>
@ -80,6 +76,8 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
position: relative; position: relative;
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
} }
.option-result-label { .option-result-label {
display: flex; display: flex;
@ -94,7 +92,7 @@
height: 100%; height: 100%;
position: absolute; position: absolute;
background-color: $fallback--lightBg; background-color: $fallback--lightBg;
background-color: var(--faintLink, $fallback--lightBg); background-color: var(--linkBg, $fallback--lightBg);
border-radius: $fallback--panelRadius; border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius); border-radius: var(--panelRadius, $fallback--panelRadius);
top: 0; top: 0;

View File

@ -0,0 +1,74 @@
export default {
name: 'PollForm',
props: ['visible'],
data: () => ({
pollType: 'single',
options: ['', ''],
expiryAmount: 2,
expiryUnit: 'hours',
expiryUnits: ['minutes', 'hours', 'days']
}),
computed: {
pollLimits () {
return this.$store.state.instance.pollLimits
},
maxOptions () {
return this.pollLimits.max_options
},
maxLength () {
return this.pollLimits.max_option_chars
}
},
methods: {
clear () {
this.pollType = 'single'
this.options = ['', '']
this.expiryAmount = 1
this.expiryUnit = 'minutes'
},
nextOption (index) {
const element = this.$el.querySelector(`#poll-${index+1}`)
if (element) {
element.focus()
} else {
// Try adding an option and try focusing on it
const addedOption = this.addOption()
if (addedOption) {
this.$nextTick(function () {
this.nextOption(index)
})
}
}
},
addOption () {
if (this.options.length < this.maxOptions) {
this.options.push('')
return true
}
return false
},
deleteOption (index, event) {
if (this.options.length > 2) {
this.options.splice(index, 1)
}
},
expiryAmountChange () {
this.expiryAmount = Math.max(1, this.expiryAmount)
this.expiryAmount = Math.min(120, this.expiryAmount)
this.updatePollToParent()
},
updatePollToParent () {
const unitMultiplier = this.expiryUnit === 'minutes' ? 60
: this.expiryUnit === 'hours' ? 60 * 60
: 60 * 60 * 24
const expiresIn = this.expiryAmount * unitMultiplier
this.$emit('update-poll', {
options: this.options,
multiple: this.pollType === 'multiple',
expiresIn
})
}
}
}

View File

@ -19,14 +19,14 @@
</div> </div>
<a <a
v-if="options.length < maxOptions" v-if="options.length < maxOptions"
class="add-option" class="add-option faint"
@click="addOption" @click="addOption"
> >
<i class="icon-plus" /> <i class="icon-plus" />
{{ $t("polls.add_option") }} {{ $t("polls.add_option") }}
</a> </a>
<div class="poll-type-expiry"> <div class="poll-type-expiry">
<div class="poll-type"> <div class="poll-type" :title="$t('polls.type')">
<label for="poll-type-selector" class="select"> <label for="poll-type-selector" class="select">
<select class="select" v-model="pollType" @change="updatePollToParent"> <select class="select" v-model="pollType" @change="updatePollToParent">
<option value="single">{{$t('polls.single_choice')}}</option> <option value="single">{{$t('polls.single_choice')}}</option>
@ -35,7 +35,7 @@
<i class="icon-down-open"/> <i class="icon-down-open"/>
</label> </label>
</div> </div>
<div class="poll-expiry"> <div class="poll-expiry" :title="$t('polls.expiry')">
<input <input
type="number" type="number"
class="expiry-amount hide-number-spinner" class="expiry-amount hide-number-spinner"
@ -57,87 +57,10 @@
</div> </div>
</template> </template>
<script> <script src="./poll_form.js"></script>
import * as DateUtils from 'src/services/date_utils/date_utils'
export default {
name: 'PollForm',
props: ['visible'],
data: () => ({
pollType: 'single',
options: ['', ''],
expiryAmount: 2,
expiryUnit: 'hours',
expiryUnits: ['minutes', 'hours', 'days']
}),
computed: {
pollLimits () {
return this.$store.state.instance.pollLimits
},
maxOptions () {
return this.pollLimits.max_options
},
maxLength () {
return this.pollLimits.max_option_chars
}
},
methods: {
clear () {
this.pollType = 'single'
this.options = ['', '']
this.expiryAmount = 1
this.expiryUnit = 'minutes'
},
nextOption (index) {
const element = this.$el.querySelector(`#poll-${index+1}`)
if (element) {
element.focus()
} else {
// Try adding an option and try focusing on it
const addedOption = this.addOption()
if (addedOption) {
this.$nextTick(function () {
this.nextOption(index)
})
}
}
},
addOption () {
if (this.options.length < this.maxOptions) {
this.options.push('')
return true
}
return false
},
deleteOption (index, event) {
if (this.options.length > 2) {
this.options.splice(index, 1)
}
},
expiryAmountChange () {
this.expiryAmount = Math.max(1, this.expiryAmount)
this.expiryAmount = Math.min(120, this.expiryAmount)
this.updatePollToParent()
},
updatePollToParent () {
const unitMultiplier = this.expiryUnit === 'minutes' ? 60
: this.expiryUnit === 'hours' ? 60 * 60
: 60 * 60 * 24
const expiresIn = this.expiryAmount * unitMultiplier
this.$emit('update-poll', {
options: this.options,
multiple: this.pollType === 'multiple',
expiresIn
})
}
}
}
</script>
<style lang="scss"> <style lang="scss">
@import '../../../_variables.scss'; @import '../../_variables.scss';
.poll-form { .poll-form {
display: flex; display: flex;
@ -148,8 +71,6 @@ export default {
align-self: flex-start; align-self: flex-start;
padding-top: 0.25em; padding-top: 0.25em;
cursor: pointer; cursor: pointer;
color: $fallback--faint;
color: var(--faint, $fallback--faint);
} }
.poll-option { .poll-option {
@ -194,6 +115,7 @@ export default {
.expiry-amount { .expiry-amount {
width: 3em; width: 3em;
text-align: right;
} }
.expiry-unit { .expiry-unit {

View File

@ -1,89 +0,0 @@
<template>
<div class="poll-results">
<div class="votes">
<div
class="poll-option"
v-for="(option, index) in poll.options"
:key="index"
:title="`${option.votes_count}/${totalVotesCount} ${$t('polls.votes')}`"
>
<div class="vote-label">
<span>{{percentageForOption(option.votes_count)}}%</span>
<span>{{option.title}}</span>
</div>
<div class="fill" :style="{ 'width': `${percentageForOption(option.votes_count)}%` }"></div>
</div>
</div>
<footer>
<div class="total">
{{totalVotesCount}} {{ $t("polls.votes") }}&nbsp;·&nbsp;
</div>
<div class="refresh">
<a href="#" @click.stop.prevent="fetchPoll(poll.id)">Refresh</a>
</div>
</footer>
</div>
</template>
<script>
export default {
name: 'PollResults',
props: ['poll', 'statusId'],
computed: {
totalVotesCount () {
return this.poll.votes_count
}
},
methods: {
percentageForOption (count) {
return this.totalVotesCount === 0 ? 0 : Math.round(count / this.totalVotesCount * 100)
},
fetchPoll () {
this.$store.dispatch('refreshPoll', { id: this.statusId, pollId: this.poll.id })
}
}
}
</script>
<style lang="scss">
@import '../../../_variables.scss';
.poll-results {
.votes {
display: flex;
flex-direction: column;
margin: 0 0 0.5em;
}
.poll-option {
position: relative;
display: flex;
flex-direction: row;
margin-top: 0.25em;
margin-bottom: 0.25em;
}
.fill {
height: 100%;
position: absolute;
background-color: $fallback--lightBg;
background-color: var(--faintLink, $fallback--lightBg);
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
top: 0;
left: 0;
transition: width 0.5s;
}
.vote-label {
display: flex;
align-items: center;
padding: 0.1em 0.25em;
z-index: 1;
span {
margin-right: 0.5em;
}
}
footer {
display: flex;
}
}
</style>

View File

@ -1,186 +0,0 @@
<template>
<div class="poll-vote" v-bind:class="containerClass">
<div
class="poll-choice"
v-for="(option, index) in poll.options"
:key="index"
>
<div v-if="showResults" :title="resultTitle(option)">
<div class="vote-label">
<span>{{percentageForOption(option.votes_count)}}%</span>
<span>{{option.title}}</span>
</div>
<div class="fill" :style="{ 'width': `${percentageForOption(option.votes_count)}%` }"></div>
</div>
<div v-else>
<input
v-if="poll.multiple"
type="checkbox"
:id="optionId(index)"
:disabled="loading"
:value="option.title"
v-model="multipleChoices[index]"
>
<input
v-else
type="radio"
:id="optionId(index)"
:disabled="loading"
:value="index"
v-model="singleChoiceIndex"
>
<label :for="optionId(index)">
{{option.title}}
</label>
</div>
</div>
<div class="footer">
<button
class="btn btn-default poll-vote-button"
type="button"
@click="vote"
:disabled="isDisabled"
>
{{$t('polls.vote')}}
</button>
<Timeago :time="this.poll.expires_at" :auto-update="1"></Timeago>
</div>
</div>
</template>
<script>
import Timeago from '../../timeago/timeago.vue'
export default {
name: 'PollVote',
props: ['poll', 'statusId'],
components: { Timeago },
data () {
return {
loading: false,
multipleChoices: [],
singleChoiceIndex: undefined
}
},
computed: {
expired () {
return new Date() > this.poll.expires_at
},
showResults () {
return this.poll.voted || this.expired
},
totalVotesCount () {
return this.poll.votes_count
},
timeleft () {
const expiresAt = new Date(this.poll.expires_at)
return expiresAt
},
expiresAt () {
return Date.parse(this.poll.expires_at).toLocaleString()
},
containerClass () {
return {
loading: this.loading
}
},
choiceIndices () {
return this.multipleChoices.map((entry, index) => index).filter(value => typeof value === 'number')
},
isDisabled () {
const noChoice = this.poll.multiple ? this.choiceIndices.length === 0 : this.singleChoiceIndex === undefined
return this.loading || noChoice
}
},
methods: {
percentageForOption (count) {
return this.totalVotesCount === 0 ? 0 : Math.round(count / this.totalVotesCount * 100)
},
resultTitle (option) {
return `${option.votes_count}/${this.totalVotesCount} ${this.$t('polls.votes')}`
},
fetchPoll () {
this.$store.dispatch('refreshPoll', { id: this.statusId, pollId: this.poll.id })
},
optionId (index) {
return `poll${this.poll.id}-${index}`
},
vote () {
this.loading = true
if (this.poll.multiple) {
if (this.choiceIndices.length === 0) return
this.$store.dispatch(
'votePoll',
{ id: this.statusId, pollId: this.poll.id, choices: this.choiceIndices }
).then(poll => {
this.loading = false
})
} else {
if (this.singleChoiceIndex === undefined) return
this.$store.dispatch(
'votePoll',
{ id: this.statusId, pollId: this.poll.id, choices: [this.singleChoiceIndex] }
).then(poll => {
this.loading = false
})
}
}
}
}
</script>
<style lang="scss">
@import '../../../_variables.scss';
.poll-results {
.votes {
display: flex;
flex-direction: column;
margin: 0 0 0.5em;
}
.poll-option {
position: relative;
display: flex;
flex-direction: row;
margin-top: 0.25em;
margin-bottom: 0.25em;
}
.fill {
height: 100%;
position: absolute;
background-color: $fallback--lightBg;
background-color: var(--faintLink, $fallback--lightBg);
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
top: 0;
left: 0;
transition: width 0.5s;
}
.vote-label {
display: flex;
align-items: center;
padding: 0.1em 0.25em;
z-index: 1;
span {
margin-right: 0.5em;
}
}
footer {
display: flex;
}
}
.poll-vote {
margin: 0.7em 0 0;
&.loading * {
cursor: progress;
}
.poll-choice {
padding: 0.4em 0;
}
.poll-vote-button {
margin: 1em 0 0;
}
}
</style>

View File

@ -2,7 +2,7 @@ import statusPoster from '../../services/status_poster/status_poster.service.js'
import MediaUpload from '../media_upload/media_upload.vue' import MediaUpload from '../media_upload/media_upload.vue'
import ScopeSelector from '../scope_selector/scope_selector.vue' import ScopeSelector from '../scope_selector/scope_selector.vue'
import EmojiInput from '../emoji-input/emoji-input.vue' import EmojiInput from '../emoji-input/emoji-input.vue'
import PollForm from '../poll/poll_form/poll_form.vue' import PollForm from '../poll/poll_form.vue'
import fileTypeService from '../../services/file_type/file_type.service.js' import fileTypeService from '../../services/file_type/file_type.service.js'
import Completion from '../../services/completion/completion.js' import Completion from '../../services/completion/completion.js'
import { take, filter, reject, map, uniqBy } from 'lodash' import { take, filter, reject, map, uniqBy } from 'lodash'
@ -82,7 +82,7 @@ const PostStatusForm = {
contentType contentType
}, },
caret: 0, caret: 0,
pollFormVisible: true pollFormVisible: false
} }
}, },
computed: { computed: {
@ -188,7 +188,7 @@ const PostStatusForm = {
return this.$store.state.instance.safeDM return this.$store.state.instance.safeDM
}, },
pollsAvailable () { pollsAvailable () {
return true // this.$store.state.instance.pollsAvailable return this.$store.state.instance.pollsAvailable
}, },
hideScopeNotice () { hideScopeNotice () {
return this.$store.state.config.hideScopeNotice return this.$store.state.config.hideScopeNotice

View File

@ -103,7 +103,7 @@
<div v-if="pollsAvailable" class="poll-icon"> <div v-if="pollsAvailable" class="poll-icon">
<label <label
class="btn btn-default" class="btn btn-default"
:title="$t('tool_tip.poll')" :title="$t('polls.add_poll')"
@click="togglePollForm"> @click="togglePollForm">
<i class="icon-chart-bar" :class="pollFormVisible && 'selected'" /> <i class="icon-chart-bar" :class="pollFormVisible && 'selected'" />
</label> </label>

View File

@ -9,7 +9,7 @@ import * as DateUtils from 'src/services/date_utils/date_utils.js'
export default { export default {
name: 'Timeago', name: 'Timeago',
props: ['time', 'autoUpdate', 'longFormat'], props: ['time', 'autoUpdate', 'longFormat', 'nowThreshold'],
data () { data () {
return { return {
relativeTime: { key: 'time.now', num: 0 }, relativeTime: { key: 'time.now', num: 0 },
@ -27,18 +27,14 @@ export default {
return typeof this.time === 'string' return typeof this.time === 'string'
? new Date(Date.parse(this.time)).toLocaleString() ? new Date(Date.parse(this.time)).toLocaleString()
: this.time.toLocaleString() : this.time.toLocaleString()
},
relativeTimeObject () {
return this.longFormat
? DateUtils.relativeTime(this.time)
: DateUtils.relativeTimeShort(this.time)
} }
}, },
methods: { methods: {
refreshRelativeTimeObject () { refreshRelativeTimeObject () {
const nowThreshold = typeof this.nowThreshold === 'number' ? this.nowThreshold : 1
this.relativeTime = this.longFormat this.relativeTime = this.longFormat
? DateUtils.relativeTime(this.time) ? DateUtils.relativeTime(this.time, nowThreshold)
: DateUtils.relativeTimeShort(this.time) : DateUtils.relativeTimeShort(this.time, nowThreshold)
if (this.autoUpdate) { if (this.autoUpdate) {
this.interval = setTimeout( this.interval = setTimeout(

View File

@ -80,10 +80,12 @@
"no_more_notifications": "No more notifications" "no_more_notifications": "No more notifications"
}, },
"polls": { "polls": {
"add_poll": "Add Poll",
"add_option": "Add Option", "add_option": "Add Option",
"option": "Option", "option": "Option",
"votes": "votes", "votes": "votes",
"vote": "Vote", "vote": "Vote",
"type": "Poll type",
"single_choice": "Single choice", "single_choice": "Single choice",
"multiple_choices": "Multiple choices", "multiple_choices": "Multiple choices",
"expiry": "Poll age", "expiry": "Poll age",
@ -540,8 +542,7 @@
"repeat": "Repeat", "repeat": "Repeat",
"reply": "Reply", "reply": "Reply",
"favorite": "Favorite", "favorite": "Favorite",
"user_settings": "User Settings", "user_settings": "User Settings"
"poll": "Add Poll"
}, },
"upload":{ "upload":{
"error": { "error": {

View File

@ -36,6 +36,7 @@
"chat": "Paikallinen Chat", "chat": "Paikallinen Chat",
"friend_requests": "Seurauspyynnöt", "friend_requests": "Seurauspyynnöt",
"mentions": "Maininnat", "mentions": "Maininnat",
"interactions": "Interaktiot",
"dms": "Yksityisviestit", "dms": "Yksityisviestit",
"public_tl": "Julkinen Aikajana", "public_tl": "Julkinen Aikajana",
"timeline": "Aikajana", "timeline": "Aikajana",
@ -54,6 +55,24 @@
"repeated_you": "toisti viestisi", "repeated_you": "toisti viestisi",
"no_more_notifications": "Ei enempää ilmoituksia" "no_more_notifications": "Ei enempää ilmoituksia"
}, },
"polls": {
"add_poll": "Lisää äänestys",
"add_option": "Lisää vaihtoehto",
"option": "Vaihtoehto",
"votes": "ääntä",
"vote": "Äänestä",
"type": "Äänestyksen tyyppi",
"single_choice": "Yksi valinta",
"multiple_choices": "Monivalinta",
"expiry": "Äänestyksen kesto",
"expires_in": "Päättyy {0} päästä",
"expired": "Päättyi {0} sitten"
},
"interactions": {
"favs_repeats": "Toistot ja tykkäykset",
"follows": "Uudet seuraukset",
"load_older": "Lataa vanhempia interaktioita"
},
"post_status": { "post_status": {
"new_status": "Uusi viesti", "new_status": "Uusi viesti",
"account_not_locked_warning": "Tilisi ei ole {0}. Kuka vain voi seurata sinua nähdäksesi 'vain-seuraajille' -viestisi", "account_not_locked_warning": "Tilisi ei ole {0}. Kuka vain voi seurata sinua nähdäksesi 'vain-seuraajille' -viestisi",

View File

@ -56,11 +56,10 @@ const defaultState = {
backendVersion: '', backendVersion: '',
frontendVersion: '', frontendVersion: '',
pollsAvailable: true,
pollLimits: { pollLimits: {
max_options: 4, max_options: 4,
max_option_chars: 255, max_option_chars: 255
min_expiration: 60,
max_expiration: 600
} }
} }

View File

@ -6,12 +6,12 @@ export const WEEK = 7 * DAY
export const MONTH = 30 * DAY export const MONTH = 30 * DAY
export const YEAR = 365.25 * DAY export const YEAR = 365.25 * DAY
export const relativeTime = date => { export const relativeTime = (date, nowThreshold = 1) => {
if (typeof date === 'string') date = Date.parse(date) if (typeof date === 'string') date = Date.parse(date)
const round = Date.now() > date ? Math.floor : Math.ceil const round = Date.now() > date ? Math.floor : Math.ceil
const d = Math.abs(Date.now() - date) const d = Math.abs(Date.now() - date)
let r = { num: round(d / YEAR), key: 'time.years' } let r = { num: round(d / YEAR), key: 'time.years' }
if (d < 30 * SECOND) { if (d < nowThreshold * SECOND) {
r.num = 0 r.num = 0
r.key = 'time.now' r.key = 'time.now'
} else if (d < MINUTE) { } else if (d < MINUTE) {
@ -38,8 +38,8 @@ export const relativeTime = date => {
return r return r
} }
export const relativeTimeShort = date => { export const relativeTimeShort = (date, nowThreshold = 1) => {
const r = relativeTime(date) const r = relativeTime(date, nowThreshold)
r.key += '_short' r.key += '_short'
return r return r
} }

View File

@ -202,6 +202,7 @@ const generateColors = (input) => {
colors.topBarLink = col.topBarLink || getTextColor(colors.topBar, colors.fgLink) colors.topBarLink = col.topBarLink || getTextColor(colors.topBar, colors.fgLink)
colors.faintLink = col.faintLink || Object.assign({}, col.link) colors.faintLink = col.faintLink || Object.assign({}, col.link)
colors.linkBg = alphaBlend(colors.link, 0.4, colors.bg)
colors.icon = mixrgb(colors.bg, colors.text) colors.icon = mixrgb(colors.bg, colors.text)