vue components refactoring

This commit is contained in:
Čarodej
2022-04-13 16:19:10 +02:00
parent 6a4bfa8bfe
commit 338f8664b7
251 changed files with 1068 additions and 1943 deletions

View File

@@ -0,0 +1,64 @@
<template>
<div class="relative flex items-center justify-center cursor-pointer h-14 w-14 overflow-hidden bg-light-background rounded-xl cursor-pointer z-10">
<input
ref="file"
type="file"
@change="showImagePreview($event)"
class="absolute top-0 bottom-0 left-0 right-0 z-10 w-full cursor-pointer opacity-0"
/>
<camera-icon v-if="!imagePreview" size="22" class="vue-feather text-gray-300" />
<img
v-if="imagePreview"
ref="image"
:src="imagePreview"
class="relative w-full h-full z-0 object-cover shadow-lg md:h-16 md:w-16"
alt="avatar"
/>
</div>
</template>
<script>
import { CameraIcon} from 'vue-feather-icons'
export default {
name: 'AvatarInput',
props: ['avatar'],
components: {
CameraIcon,
},
data() {
return {
imagePreview: undefined,
}
},
watch: {
imagePreview(val) {
this.$store.commit('UPDATE_AVATAR', val)
},
},
methods: {
showImagePreview(event) {
let imgPath = event.target.files[0].name,
extension = imgPath.substring(imgPath.lastIndexOf('.') + 1).toLowerCase()
if (['png', 'jpg', 'jpeg'].includes(extension)) {
let file = event.target.files[0],
reader = new FileReader()
reader.onload = () => (this.imagePreview = reader.result)
reader.readAsDataURL(file)
// Update user avatar
this.$updateImage('/user/settings', 'avatar', event.target.files[0])
} else {
alert(this.$t('wrong_image_error'))
}
},
},
created() {
// If there is default image then load
if (this.avatar) this.imagePreview = this.avatar
},
}
</script>

View File

@@ -0,0 +1,40 @@
<template>
<div>
<div
class="flex h-5 w-5 items-center justify-center rounded-md"
:class="{
'bg-theme': isClicked,
'bg-light-background dark:bg-4x-dark-foreground': !isClicked,
}"
@click="changeState"
>
<CheckIcon v-if="isClicked" class="vue-feather text-white" size="17" />
</div>
</div>
</template>
<script>
import { CheckIcon } from 'vue-feather-icons'
export default {
name: 'CheckBox',
props: ['isClicked'],
components: {
CheckIcon,
},
data() {
return {
isSwitched: undefined,
}
},
methods: {
changeState() {
this.isSwitched = !this.isSwitched
this.$emit('input', this.isSwitched)
},
},
mounted() {
this.isSwitched = this.isClicked
},
}
</script>

View File

@@ -0,0 +1,52 @@
<template>
<div @click="copyUrl" class="relative flex items-center">
<input ref="sel" :value="str" :id="id" type="text" class="focus-border-theme input-dark !pr-10" readonly />
<!--Copy icon-->
<div class="absolute right-0 px-4">
<copy-icon v-if="!isCopiedLink" size="16" class="hover-text-theme vue-feather cursor-pointer" />
<check-icon v-if="isCopiedLink" size="16" class="text-theme vue-feather cursor-pointer" />
</div>
</div>
</template>
<script>
import { CopyIcon, CheckIcon, SendIcon } from 'vue-feather-icons'
export default {
name: 'CopyInput',
props: ['size', 'str'],
components: {
CheckIcon,
CopyIcon,
SendIcon,
},
data() {
return {
isCopiedLink: false,
id: 'link-input-' + Math.floor(Math.random() * 10000000),
}
},
methods: {
copyUrl() {
// Get input value
let copyText = document.getElementById(this.id)
// select link
copyText.select()
copyText.setSelectionRange(0, 99999)
// Copy
document.execCommand('copy')
// Mark button as copied
this.isCopiedLink = true
// Reset copy button
setTimeout(() => {
this.isCopiedLink = false
}, 1000)
},
},
}
</script>

View File

@@ -0,0 +1,199 @@
<template>
<div class="relative flex items-center">
<input
ref="sel"
:value="item.data.relationships.shared.data.attributes.link"
:id="id"
type="text"
class="focus-border-theme w-full appearance-none rounded-lg border border-transparent bg-light-background py-2 pr-16 pl-3 text-sm font-bold dark:bg-2x-dark-foreground"
readonly
/>
<!--Copy icon-->
<div class="flex items-center">
<div @click="copyUrl" class="absolute right-9 p-1">
<copy-icon v-if="!isCopiedLink" size="14" class="hover-text-theme vue-feather cursor-pointer" />
<check-icon v-if="isCopiedLink" size="14" class="hover-text-theme vue-feather cursor-pointer" />
</div>
<div @click.stop.prevent="moreOptions" class="absolute right-2.5 p-1">
<more-horizontal-icon size="14" class="hover-text-theme vue-feather cursor-pointer" />
</div>
</div>
<!--Hidden options-->
<ul
v-if="isOpenedMoreOptions"
class="absolute top-12 left-0 right-0 z-10 select-none overflow-y-auto overflow-x-hidden rounded-lg shadow-xl"
>
<li
v-if="item.data.type !== 'folder' && !item.data.relationships.shared.data.attributes.protected"
@click="copyDirectLink"
class="block flex cursor-pointer items-center bg-white py-2.5 px-5 hover:bg-light-background dark:bg-2x-dark-foreground dark:hover:bg-4x-dark-foreground"
>
<div class="w-8">
<download-icon size="14" />
</div>
<span class="text-sm font-bold">
{{ $t('copy_direct_download_link') }}
</span>
</li>
<li
@click="getQrCode"
class="block flex cursor-pointer items-center bg-white py-2.5 px-5 hover:bg-light-background dark:bg-2x-dark-foreground dark:hover:bg-4x-dark-foreground"
>
<div class="w-8">
<camera-icon size="14" />
</div>
<span class="text-sm font-bold">
{{ $t('get_qr_code') }}
</span>
</li>
<li
@click="sendViaEmail"
class="block flex cursor-pointer items-center bg-white py-2.5 px-5 hover:bg-light-background dark:bg-2x-dark-foreground dark:hover:bg-4x-dark-foreground"
>
<div class="w-8">
<send-icon size="14" />
</div>
<span class="text-sm font-bold">
{{ $t('sharelink.share_via_email') }}
</span>
</li>
<li
@click="copyIframe"
class="block flex cursor-pointer items-center bg-white py-2.5 px-5 hover:bg-light-background dark:bg-2x-dark-foreground dark:hover:bg-4x-dark-foreground"
>
<div class="w-8">
<code-icon size="14" />
</div>
<span class="text-sm font-bold">
{{ $t('sharelink.copy_embed') }}
</span>
</li>
</ul>
<textarea
v-model="directLink"
ref="directLinkTextarea"
class="pointer-events-none absolute right-full opacity-0"
></textarea>
<textarea
v-model="iframeCode"
ref="iframe"
class="pointer-events-none absolute right-full opacity-0"
></textarea>
</div>
</template>
<script>
import { DownloadIcon, CameraIcon, CopyIcon, CheckIcon, SendIcon, MoreHorizontalIcon, CodeIcon } from 'vue-feather-icons'
import { events } from '../../bus'
export default {
name: 'CopyShareLink',
props: ['item'],
components: {
MoreHorizontalIcon,
CameraIcon,
CheckIcon,
CopyIcon,
CodeIcon,
SendIcon,
DownloadIcon,
},
watch: {
'item': function () {
this.setClipboard()
}
},
data() {
return {
id: 'link-input-' + Math.floor(Math.random() * 10000000),
directLink: undefined,
iframeCode: undefined,
isCopiedLink: false,
isOpenedMoreOptions: false,
}
},
methods: {
moreOptions() {
this.isOpenedMoreOptions = !this.isOpenedMoreOptions
},
getQrCode() {
events.$emit('popup:open', {
name: 'share-edit',
item: this.item,
section: 'qr-code',
})
this.isOpenedMoreOptions = false
},
sendViaEmail() {
events.$emit('popup:open', {
name: 'share-edit',
item: this.item,
section: 'email-sharing',
})
this.isOpenedMoreOptions = false
},
copyDirectLink() {
let copyText = this.$refs.directLinkTextarea
copyText.select()
copyText.setSelectionRange(0, 99999)
document.execCommand('copy')
events.$emit('toaster', {
type: 'success',
message: this.$t('direct_link_copied'),
})
this.isOpenedMoreOptions = false
},
copyIframe() {
let copyText = this.$refs.iframe
copyText.select()
copyText.setSelectionRange(0, 99999)
document.execCommand('copy')
events.$emit('toaster', {
type: 'success',
message: this.$t('web_code_copied'),
})
this.isOpenedMoreOptions = false
},
copyUrl() {
// Get input value
let copyText = document.getElementById(this.id)
// select link
copyText.select()
copyText.setSelectionRange(0, 99999)
// Copy
document.execCommand('copy')
// Mark button as copied
this.isCopiedLink = true
// Reset copy button
setTimeout(() => {
this.isCopiedLink = false
}, 1000)
},
setClipboard() {
this.directLink = this.item.data.relationships.shared.data.attributes.link + '/direct'
this.iframeCode = `<iframe src="${this.item.data.relationships.shared.data.attributes.link}" width="790" height="400" allowfullscreen frameborder="0"></iframe>`
}
},
created() {
this.setClipboard()
}
}
</script>

View File

@@ -0,0 +1,97 @@
<template>
<div
class="relative flex h-[175px] items-center justify-center rounded-lg bg-light-background dark:bg-2x-dark-foreground"
:class="{ 'is-error': error }"
>
<!--Reset Image-->
<div
v-if="imagePreview"
@click="resetImage"
class="absolute right-0 top-0 z-[9] flex h-7 w-7 -translate-y-3 translate-x-3 cursor-pointer items-center justify-center rounded-md rounded-full dark:bg-4x-dark-foreground bg-white shadow-lg"
>
<x-icon size="14" class="vue-feather dark:text-gray-500" />
</div>
<input
@change="showImagePreview($event)"
ref="file"
type="file"
class="absolute top-0 left-0 right-0 bottom-0 z-10 w-full cursor-pointer opacity-0"
/>
<!--Default image preview-->
<img
v-if="imagePreview"
:src="imagePreview"
ref="image"
class="absolute h-full w-full object-contain py-4 px-12"
/>
<!--Drop image zone-->
<div v-if="!isData" class="text-center">
<image-icon size="34" class="vue-feather text-theme mb-4 inline-block" />
<b class="block text-base font-bold leading-3">
{{ $te('input_image.title') ? $t('input_image.title') : 'Upload Image' }}
</b>
<small class="text-xs text-gray-500">
{{
$te('input_image.supported')
? $t('input_image.supported')
: 'Supported formats are .png, .jpg, .jpeg.'
}}
</small>
</div>
</div>
</template>
<script>
import { XIcon, ImageIcon } from 'vue-feather-icons'
export default {
name: 'ImageInput',
props: ['image', 'error'],
components: {
ImageIcon,
XIcon,
},
data() {
return {
imagePreview: undefined,
}
},
computed: {
isData() {
return !(typeof this.imagePreview === 'undefined' || this.imagePreview === '')
},
},
methods: {
resetImage() {
this.imagePreview = undefined
this.$emit('input', undefined)
},
showImagePreview(event) {
const imgPath = event.target.files[0].name,
extn = imgPath.substring(imgPath.lastIndexOf('.') + 1).toLowerCase()
if (['png', 'jpg', 'jpeg', 'svg'].includes(extn)) {
const file = event.target.files[0],
reader = new FileReader()
reader.onload = () => (this.imagePreview = reader.result)
reader.readAsDataURL(file)
// Update user avatar
this.$emit('input', event.target.files[0])
} else {
alert(this.$t('wrong_image_error'))
}
},
},
created() {
// If has default image then load
if (this.image) this.imagePreview = this.image
},
}
</script>

View File

@@ -0,0 +1,99 @@
<template>
<div
class="focus-border-theme input-dark focus-within-border-theme"
:class="{'!border-rose-600':isError, '!py-3.5': !emails.length, '!px-2 !pt-[10px] !pb-0.5': emails.length}"
>
<div @click="$refs.input.focus()" class="flex flex-wrap cursor-text items-center" style="grid-template-columns: auto minmax(0,1fr);">
<div
class="whitespace-nowrap flex items-center rounded-lg bg-theme-100 mr-2 mb-2 py-1 px-2 cursor-pointer w-fit"
@click="removeEmail(email)"
v-for="(email, i) in emails"
:key="i"
>
<small class="text-sm text-theme mr-1">
{{ email }}
</small>
<x-icon class="vue-feather text-theme" size="14" />
</div>
<input
@keydown.delete="removeLastEmail($event)"
@keyup="handleEmail()"
v-model="email"
:size="inputSize"
class="w-auto font-bold text-sm bg-transparent"
:class="{'mb-2': emails.length}"
:placeholder="placeHolder"
autocomplete="new-password"
ref="input"
/>
</div>
</div>
</template>
<script>
import { XIcon } from 'vue-feather-icons'
import { events } from '../../bus'
export default {
name: 'MultiEmailInput',
components: { XIcon },
props: ['isError', 'label'],
computed: {
placeHolder() {
return !this.emails.length ? this.$t('shared_form.email_placeholder') : ''
},
inputSize() {
return this.email && this.email.length > 14 ? this.email.length : 14
},
},
data() {
return {
emails: [],
email: undefined,
}
},
methods: {
removeEmail(email) {
this.emails = this.emails.filter((item) => item !== email)
// After remove email send new emails list to parent
events.$emit('emailsInputValues', this.emails)
},
removeLastEmail(event) {
// If is input empty and presse backspace remove last email from array
if (event.code === 'Backspace' && this.email === '') this.emails.pop()
},
handleEmail() {
if (! this.email.length)
return;
// Get index of @ and last dot
let lastDot = this.email.lastIndexOf('.')
let at = this.email.indexOf('@')
// Check if is after @ some dot, if email have @ anf if dont have more like one
if (lastDot < at || at === -1 || this.email.match(/@/g).length > 1) return
// First email dont need to be separated by comma or space to be sended
if (this.emails.length === 0) events.$emit('emailsInputValues', [this.email])
// After come or backspace push the single email to array or emails
if (this.email.includes(',') || this.email.includes(' ')) {
let email = this.email.replace(/[","," "]/, '')
this.email = ''
// Push single email to aray of emails
this.emails.push(email)
events.$emit('emailsInputValues', this.emails)
}
},
},
created() {
this.$nextTick(() => {
this.$refs.input.focus()
})
},
}
</script>

View File

@@ -0,0 +1,113 @@
<template>
<div class="search-bar">
<div v-if="!query" class="icon">
<search-icon size="19" />
</div>
<div @click="clearInput" v-if="query" class="icon">
<x-icon class="pointer" size="19"></x-icon>
</div>
<input
v-model="query"
@input="$emit('input', query)"
class="query focus-border-theme"
type="text"
name="searchInput"
:placeholder="$t('search_translations')"
/>
</div>
</template>
<script>
import { SearchIcon, XIcon } from 'vue-feather-icons'
export default {
name: 'SearchInput',
components: {
SearchIcon,
XIcon,
},
data() {
return {
query: undefined,
}
},
methods: {
clearInput() {
this.query = undefined
this.$emit('reset-query')
},
},
}
</script>
<style lang="scss" scoped>
@import '../../../sass/vuefilemanager/variables';
@import '../../../sass/vuefilemanager/mixins';
@import '../../../sass/vuefilemanager/forms';
.search-bar {
position: relative;
width: 100%;
border-radius: 8px;
input {
background: $light_background;
border-radius: 8px;
outline: 0;
padding: 9px 20px 9px 43px;
font-weight: 700;
@include font-size(16);
width: 100%;
height: 50px;
min-width: 175px;
transition: 0.15s all ease;
border: 1px solid transparent;
-webkit-appearance: none;
box-shadow: none;
&::placeholder {
color: $light_text;
@include font-size(14);
font-weight: 700;
}
&:focus {
border-width: 1px;
border-style: solid;
}
&:focus + .icon {
path {
color: inherit;
}
}
}
.icon {
height: 100%;
position: absolute;
top: 0;
left: 0;
padding: 11px 15px;
display: flex;
align-items: center;
circle,
line {
color: $light_text;
}
.pointer {
cursor: pointer;
}
}
}
.dark {
.search-bar {
input {
background: $dark_mode_foreground;
}
}
}
</style>

View File

@@ -0,0 +1,85 @@
<template>
<div class="select-box">
<div
class="box-item active-bg-theme-100 active-border-theme"
:class="{ active: item.value === input }"
@click="getSelectedValue(item)"
v-for="(item, i) in data"
:key="i"
>
<span class="box-value active-text-theme">{{ item.label }}</span>
</div>
</div>
</template>
<script>
export default {
name: 'SelectBoxInput',
props: ['data', 'value'],
data() {
return {
input: undefined,
}
},
methods: {
getSelectedValue(item) {
if (!this.input || this.input !== item.value) this.input = item.value
else this.input = undefined
this.$emit('input', this.input)
},
},
created() {
if (this.value) this.input = this.value
},
}
</script>
<style lang="scss" scoped>
@import '../../../sass/vuefilemanager/variables';
@import '../../../sass/vuefilemanager/mixins';
@import '../../../sass/vuefilemanager/inapp-forms';
@import '../../../sass/vuefilemanager/forms';
.select-box {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
flex-direction: row;
margin-bottom: 10px;
.box-item {
margin-bottom: 10px;
padding: 12px 4px;
text-align: center;
background: $light_background;
border-radius: 8px;
font-weight: 700;
border: 2px solid $light_background;
cursor: pointer;
flex-direction: column;
flex-basis: 55px;
.box-value {
@include font-size(15);
}
}
}
@media only screen and (max-width: 960px) {
.select-box {
.box-item {
flex-basis: calc(34% - 10px);
}
}
}
.dark {
.select-box {
.box-item {
border-color: $dark_mode_border_color;
background: lighten($dark_mode_foreground, 3%);
}
}
}
</style>

View File

@@ -0,0 +1,276 @@
<template>
<div class="select">
<!--Area-->
<div
class="input-area rounded-lg bg-light-background dark:bg-2x-dark-foreground"
:class="{ 'is-active': isOpen, '!border-rose-600': isError }"
@click="openMenu"
>
<!--If is selected-->
<div class="selected flex w-full items-center" v-if="selected">
<div class="option-icon" v-if="selected.icon">
<user-icon v-if="selected.icon === 'user'" size="14" class="vue-feather text-theme" />
<edit2-icon v-if="selected.icon === 'user-edit'" size="14" class="vue-feather text-theme" />
</div>
<span class="option-value inline-block w-full overflow-hidden text-ellipsis whitespace-nowrap pl-2">
{{ selected.label }}
</span>
</div>
<!--If is empty-->
<div class="not-selected" v-if="!selected">
<span class="option-value placehoder">{{ placeholder }}</span>
</div>
<chevron-down-icon size="19" class="chevron" />
</div>
<!--Options-->
<transition name="slide-in">
<div class="input-options rounded-lg" v-if="isOpen">
<div v-if="options.length > 5" class="select-search">
<input
v-model="query"
ref="search"
type="text"
:placeholder="$te('search_in_list') ? $t('search_in_list') : 'Search in list...'"
class="search-input focus-border-theme rounded-lg"
/>
</div>
<ul class="option-list">
<li class="option-item" @click="selectOption(option)" v-for="(option, i) in optionList" :key="i">
<div class="option-icon" v-if="option.icon">
<user-icon v-if="option.icon === 'user'" size="14" />
<edit2-icon v-if="option.icon === 'user-edit'" size="14" />
</div>
<span class="option-value">
{{ $t(option.label) }}
</span>
</li>
</ul>
</div>
</transition>
</div>
</template>
<script>
import { ChevronDownIcon, Edit2Icon, UserIcon } from 'vue-feather-icons'
import { debounce, omitBy } from 'lodash'
export default {
name: 'SelectInput',
props: ['placeholder', 'options', 'isError', 'default'],
components: {
Edit2Icon,
UserIcon,
ChevronDownIcon,
},
watch: {
query: debounce(function (val) {
this.searchedResults = omitBy(this.options, (string) => {
return !string.label.toLowerCase().includes(val.toLowerCase())
})
}, 200),
},
computed: {
isSearching() {
return this.searchedResults && this.query !== ''
},
optionList() {
return this.isSearching ? this.searchedResults : this.options
},
},
data() {
return {
searchedResults: undefined,
selected: undefined,
isOpen: false,
query: '',
}
},
methods: {
selectOption(option) {
// Emit selected
this.$emit('input', option.value)
this.$emit('change', option.value)
// Get selected
this.selected = option
// Close menu
this.isOpen = false
},
openMenu() {
this.isOpen = !this.isOpen
if (this.$refs.search && this.isOpen) {
this.$nextTick(() => this.$refs.search.focus())
}
},
},
created() {
if (this.default) this.selected = this.options.find((option) => option.value === this.default)
},
}
</script>
<style lang="scss" scoped>
@import '../../../sass/vuefilemanager/variables';
@import '../../../sass/vuefilemanager/mixins';
/* TODO: refactor to the tailwind */
.select {
position: relative;
user-select: none;
width: 100%;
}
.select-search {
background: white;
position: sticky;
top: 0;
padding: 13px;
.search-input {
border: 1px solid transparent;
background: $light_background;
@include transition(150ms);
@include font-size(14);
padding: 13px 20px;
appearance: none;
font-weight: 700;
outline: 0;
width: 100%;
}
}
.input-options {
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.12);
background: white;
position: absolute;
overflow: hidden;
top: 65px;
left: 0;
right: 0;
z-index: 9;
max-height: 295px;
overflow-y: auto;
.option-item {
padding: 13px 20px;
display: block;
cursor: pointer;
&:hover {
color: $theme;
background: $light_background;
}
&:last-child {
border-bottom: none;
}
}
}
.input-area {
border-width: 1px;
border-style: solid;
border-color: transparent;
justify-content: space-between;
@include transition(150ms);
align-items: center;
padding: 13px 20px;
display: flex;
outline: 0;
width: 100%;
cursor: pointer;
.chevron {
@include transition(150ms);
}
&.is-active {
//box-shadow: 0 0 7px rgba($theme, 0.3);
.chevron {
@include transform(rotate(180deg));
}
}
&.is-error {
border-color: $danger;
box-shadow: 0 0 7px rgba($danger, 0.3);
}
}
.option-icon {
width: 20px;
display: inline-block;
@include font-size(10);
}
.option-value {
@include font-size(14);
font-weight: 700;
vertical-align: middle;
&.placehoder {
color: rgba($text, 0.5);
}
}
.slide-in-enter-active {
transition: all 150ms ease;
}
.slide-in-enter /* .list-leave-active below version 2.1.8 */ {
opacity: 0;
transform: translateY(-50px);
}
.dark {
.select-search {
background: $dark_mode_foreground;
.search-input {
background: $dark_mode_background;
}
}
.popup-wrapper {
.input-area {
background: lighten($dark_mode_foreground, 3%);
}
}
.input-options {
background: $dark_mode_foreground;
.option-item {
border-bottom: none;
&:hover {
background: lighten($dark_mode_foreground, 5%);
.option-icon {
path,
circle {
color: inherit;
}
}
}
&:last-child {
border-bottom: none;
}
}
}
.option-value {
&.placehoder {
color: $dark_mode_text_secondary;
}
}
}
</style>

View File

@@ -0,0 +1,103 @@
<template>
<div>
<div class="switch-content">
<label class="input-label" v-if="label"> {{ label }}: </label>
<small class="input-info" v-if="info">
{{ info }}
</small>
</div>
<div class="switch-content text-right">
<div class="switch" :class="{ active: state }" @click="changeState">
<div class="switch-button"></div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'SwitchInput',
props: ['label', 'name', 'state', 'info', 'input', 'isDisabled'],
data() {
return {
isSwitched: undefined,
}
},
methods: {
changeState() {
if (this.isDisabled) return
this.isSwitched = !this.isSwitched
this.$emit('input', this.isSwitched)
},
},
mounted() {
this.isSwitched = this.state
},
}
</script>
<style lang="scss" scoped>
@import '../../../sass/vuefilemanager/variables';
@import '../../../sass/vuefilemanager/mixins';
.input-wrapper {
display: flex;
width: 100%;
.input-label {
color: $text;
}
.switch-content {
width: 100%;
&:last-child {
width: 80px;
}
}
}
.switch {
width: 50px;
height: 28px;
border-radius: 50px;
display: block;
background: #f1f1f5;
position: relative;
@include transition;
.switch-button {
@include transition;
width: 22px;
height: 22px;
border-radius: 50px;
display: block;
background: white;
position: absolute;
top: 3px;
left: 3px;
box-shadow: 0 2px 4px rgba(37, 38, 94, 0.1);
cursor: pointer;
}
&.active {
.switch-button {
left: 25px;
}
}
}
.dark {
.switch {
background: $dark_mode_foreground;
}
.popup-wrapper {
.switch {
background: lighten($dark_mode_foreground, 3%);
}
}
}
</style>