You've already forked obsidian-visualiser
Components rework and lots of fixes
This commit is contained in:
19
components/standard/Accordion.vue
Normal file
19
components/standard/Accordion.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
const collapsed = defineModel<boolean>({
|
||||
default: true,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div @click="collapsed = !collapsed" class="flex gap-2 items-center cursor-pointer hover:text-opacity-75 text-light-100 dark:text-dark-100" :class="$attrs.class">
|
||||
<div class="w-4 h-4 transition-transform" :class="{'-rotate-90': collapsed}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 8L12 17L21 8"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="font-semibold xl:text-base text-sm flex-1"><slot name="header"></slot></div>
|
||||
</div>
|
||||
<div v-if="!collapsed">
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</template>
|
||||
27
components/standard/Highlight.vue
Normal file
27
components/standard/Highlight.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div>
|
||||
{{beforeText}}<span class="font-bold">{{matchedText}}</span>{{afterText}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Prop
|
||||
{
|
||||
text: string;
|
||||
matched: string;
|
||||
}
|
||||
const props = defineProps<Prop>();
|
||||
|
||||
const beforeText = computed(() => {
|
||||
const pos = props.text.toLowerCase().indexOf(props.matched.toLowerCase());
|
||||
return props.text.substring(0, pos);
|
||||
})
|
||||
const matchedText = computed(() => {
|
||||
const pos = props.text.toLowerCase().indexOf(props.matched.toLowerCase());
|
||||
return props.text.substring(pos, pos + props.matched.length);
|
||||
})
|
||||
const afterText = computed(() => {
|
||||
const pos = props.text.toLowerCase().indexOf(props.matched.toLowerCase()) + props.matched.length;
|
||||
return props.text.substring(pos);
|
||||
})
|
||||
</script>
|
||||
52
components/standard/HoverPopup.vue
Normal file
52
components/standard/HoverPopup.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<Teleport to="#teleports" v-if="display">
|
||||
<div :class="$attrs.class" class="absolute border-2 border-light-35 dark:border-dark-35 max-w-[550px] max-h-[450px] bg-light-0 dark:bg-dark-0 text-light-100 dark:text-dark-100" :style="pos"
|
||||
@mouseenter="debounce(show, 250)" @mouseleave="debounce(() => { emit('beforeHide'); display = false }, 250)">
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</Teleport>
|
||||
<span ref="el" @mouseenter="debounce(show, 250)" @mouseleave="debounce(() => { emit('beforeHide'); display = false }, 250)">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTemplateRef } from 'vue';
|
||||
|
||||
const display = ref(false), fetched = ref(false);
|
||||
const el = useTemplateRef('el'), pos = ref<Record<string, string>>();
|
||||
const emit = defineEmits(['beforeShow', 'beforeHide']);
|
||||
|
||||
async function show()
|
||||
{
|
||||
if(display.value)
|
||||
return;
|
||||
|
||||
emit('beforeShow');
|
||||
|
||||
const rect = (el.value as HTMLDivElement)?.getBoundingClientRect();
|
||||
|
||||
if(!rect)
|
||||
return;
|
||||
|
||||
const r: Record<string, string> = {};
|
||||
if (rect.bottom + 450 < window.innerHeight)
|
||||
r.top = (rect.bottom + 4) + "px";
|
||||
else
|
||||
r.bottom = (window.innerHeight - rect.top + 4) + "px";
|
||||
if (rect.right + 550 < window.innerWidth)
|
||||
r.left = rect.left + "px";
|
||||
else
|
||||
r.right = (window.innerWidth - rect.right + 4) + "px";
|
||||
|
||||
pos.value = r;
|
||||
display.value = true;
|
||||
}
|
||||
let debounceFn: () => void, timeout: NodeJS.Timeout;
|
||||
function debounce(fn: () => void, ms: number)
|
||||
{
|
||||
debounceFn = fn;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(debounceFn, ms);
|
||||
}
|
||||
</script>
|
||||
@@ -1,3 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits(['input', 'change', 'focus', 'blur']);
|
||||
const model = defineModel();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input v-bind="$attrs" class="caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 bg-light-20 dark:bg-dark-20 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 border border-light-35 dark:border-dark-35"/>
|
||||
<input v-model="model" v-bind="$attrs" @input="e => emit('input', e)" @change="e => emit('change', e)" @focus="e => emit('focus', e)" @blur="e => emit('blur', e)"
|
||||
class="caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 bg-light-20 dark:bg-dark-20 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 border border-light-35 dark:border-dark-35"/>
|
||||
</template>
|
||||
23
components/standard/InputField.vue
Normal file
23
components/standard/InputField.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
interface Prop
|
||||
{
|
||||
error?: string | boolean;
|
||||
title: string;
|
||||
}
|
||||
const props = defineProps<Prop>();
|
||||
const model = defineModel<string>();
|
||||
|
||||
const err = ref<string | boolean | undefined>(props.error);
|
||||
|
||||
watchEffect(() => err.value = props.error);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template></template>
|
||||
<div class="m-1">
|
||||
<label v-if="title" class="pe-4">{{ title }}</label>
|
||||
<Input @input="err = false" :class="{ 'input-has-error': !!err }" v-model="model"
|
||||
v-bind="$attrs" />
|
||||
<span v-if="err && typeof err === 'string'" class="text-light-red dark:text-dark-red block pb-2">{{ err }}</span>
|
||||
</div>
|
||||
</template>
|
||||
29
components/standard/Markdown.vue
Normal file
29
components/standard/Markdown.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<template
|
||||
v-if="model && model.length > 0">
|
||||
<Suspense>
|
||||
<MarkdownRenderer #default :key="key" v-if="node" :node="node"></MarkdownRenderer>
|
||||
<template #fallback><div class="loading"></div></template>
|
||||
</Suspense>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { hash } from 'ohash'
|
||||
import { watch, computed } from 'vue'
|
||||
import type { Root } from 'hast';
|
||||
|
||||
const model = defineModel<string>();
|
||||
|
||||
const parser = useMarkdown();
|
||||
const key = computed(() => hash(model.value));
|
||||
|
||||
const node = ref<Root>();
|
||||
|
||||
watch(model, async () => {
|
||||
if(model.value && model.value)
|
||||
{
|
||||
node.value = parser(model.value);
|
||||
}
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
120
components/standard/MarkdownRenderer.vue
Normal file
120
components/standard/MarkdownRenderer.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<script lang="ts">
|
||||
import type { RootContent, Root } from 'hast';
|
||||
import { Text, Comment } from 'vue';
|
||||
|
||||
import ProseP from '~/components/prose/ProseP.vue';
|
||||
import ProseA from '~/components/prose/ProseA.vue';
|
||||
import ProseBlockquote from '~/components/prose/ProseBlockquote.vue';
|
||||
import ProseCode from '~/components/prose/ProseCode.vue';
|
||||
import ProsePre from '~/components/prose/ProsePre.vue';
|
||||
import ProseEm from '~/components/prose/ProseEm.vue';
|
||||
import ProseH1 from '~/components/prose/ProseH1.vue';
|
||||
import ProseH2 from '~/components/prose/ProseH2.vue';
|
||||
import ProseH3 from '~/components/prose/ProseH3.vue';
|
||||
import ProseH4 from '~/components/prose/ProseH4.vue';
|
||||
import ProseH5 from '~/components/prose/ProseH5.vue';
|
||||
import ProseH6 from '~/components/prose/ProseH6.vue';
|
||||
import ProseHr from '~/components/prose/ProseHr.vue';
|
||||
import ProseImg from '~/components/prose/ProseImg.vue';
|
||||
import ProseUl from '~/components/prose/ProseUl.vue';
|
||||
import ProseOl from '~/components/prose/ProseOl.vue';
|
||||
import ProseLi from '~/components/prose/ProseLi.vue';
|
||||
import ProseStrong from '~/components/prose/ProseStrong.vue';
|
||||
import ProseTable from '~/components/prose/ProseTable.vue';
|
||||
import ProseTag from '~/components/prose/ProseTag.vue';
|
||||
import ProseThead from '~/components/prose/ProseThead.vue';
|
||||
import ProseTbody from '~/components/prose/ProseTbody.vue';
|
||||
import ProseTd from '~/components/prose/ProseTd.vue';
|
||||
import ProseTh from '~/components/prose/ProseTh.vue';
|
||||
import ProseTr from '~/components/prose/ProseTr.vue';
|
||||
import ProseScript from '~/components/prose/ProseScript.vue';
|
||||
|
||||
const proseList = {
|
||||
"p": ProseP,
|
||||
"a": ProseA,
|
||||
"blockquote": ProseBlockquote,
|
||||
"code": ProseCode,
|
||||
"pre": ProsePre,
|
||||
"em": ProseEm,
|
||||
"h1": ProseH1,
|
||||
"h2": ProseH2,
|
||||
"h3": ProseH3,
|
||||
"h4": ProseH4,
|
||||
"h5": ProseH5,
|
||||
"h6": ProseH6,
|
||||
"hr": ProseHr,
|
||||
"img": ProseImg,
|
||||
"ul": ProseUl,
|
||||
"ol": ProseOl,
|
||||
"li": ProseLi,
|
||||
"strong": ProseStrong,
|
||||
"table": ProseTable,
|
||||
"tag": ProseTag,
|
||||
"thead": ProseThead,
|
||||
"tbody": ProseTbody,
|
||||
"td": ProseTd,
|
||||
"th": ProseTh,
|
||||
"tr": ProseTr,
|
||||
"script": ProseScript
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MarkdownRenderer',
|
||||
props: {
|
||||
node: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
proses: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
node: {
|
||||
handler: function(val, old) {
|
||||
|
||||
},
|
||||
deep: true,
|
||||
}
|
||||
},
|
||||
async setup(props) {
|
||||
if(props.proses)
|
||||
{
|
||||
for(const prose of Object.keys(props.proses))
|
||||
{
|
||||
if(typeof props.proses[prose] === 'string')
|
||||
props.proses[prose] = await resolveComponent(props.proses[prose]);
|
||||
}
|
||||
}
|
||||
return { tags: Object.assign({}, proseList, props.proses) };
|
||||
},
|
||||
render(ctx: any) {
|
||||
const { node, tags } = ctx;
|
||||
|
||||
if(!node)
|
||||
return null;
|
||||
|
||||
return h('div', null, {default: () => (node as Root).children.map(e => renderNode(e, tags)).filter(e => !!e)});
|
||||
}
|
||||
});
|
||||
|
||||
function renderNode(node: RootContent, tags: Record<string, any>): VNode | undefined
|
||||
{
|
||||
if(node.type === 'text' && node.value.length > 0 && node.value !== '\n')
|
||||
{
|
||||
return h(Text, node.value);
|
||||
}
|
||||
else if(node.type === 'comment' && node.value.length > 0 && node.value !== '\n')
|
||||
{
|
||||
return h(Comment, node.value);
|
||||
}
|
||||
else if(node.type === 'element')
|
||||
{
|
||||
node.tagName === 'tag' && console.log(node);
|
||||
return h(tags[node.tagName] ?? node.tagName, { ...node.properties, class: node.properties.className }, {default: () => node.children.map(e => renderNode(e, tags)).filter(e => !!e)});
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
</script>
|
||||
70
components/standard/NavBar.vue
Normal file
70
components/standard/NavBar.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateRef } from 'vue';
|
||||
|
||||
const containerRef = useTemplateRef("container");
|
||||
const { direction } = useSwipe(window, { threshold: 150 });
|
||||
|
||||
watch(direction, () => {
|
||||
if(direction.value === 'right')
|
||||
toggleNavigation(true);
|
||||
if(direction.value === 'left')
|
||||
toggleNavigation(false);
|
||||
});
|
||||
|
||||
function toggleNavigation(bool?: boolean)
|
||||
{
|
||||
containerRef.value?.setAttribute('aria-expanded', bool === undefined ? (containerRef.value?.getAttribute('aria-expanded') !== 'true').toString() : bool.toString());
|
||||
}
|
||||
const hideNavigation = () => toggleNavigation(false);
|
||||
onMounted(() => {
|
||||
const links = containerRef.value?.getElementsByTagName('a');
|
||||
if(links)
|
||||
{
|
||||
for(const link of links)
|
||||
{
|
||||
link.addEventListener("click", hideNavigation);
|
||||
}
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
const links = containerRef.value?.getElementsByTagName('a');
|
||||
if(links)
|
||||
{
|
||||
for(const link of links)
|
||||
{
|
||||
link.addEventListener("click", hideNavigation);
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="container" aria-expanded="false" class="group flex h-screen overflow-hidden">
|
||||
<div class="z-50 sm:hidden block absolute top-0 left-0 p-2 border-e border-b border-light-35 dark:border-dark-35 bg-light-0 dark:bg-dark-0 cursor-pointer hover:bg-light-25 dark:hover:bg-dark-25" @click="e => toggleNavigation()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="w-4 h-4 fill-light-100 dark:fill-dark-100">
|
||||
<line x1="21" y1="6" x2="3" y2="6"></line>
|
||||
<line x1="15" y1="12" x2="3" y2="12"></line>
|
||||
<line x1="17" y1="18" x2="3" y2="18"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="bg-light-0 sm:my-8 sm:py-3 dark:bg-dark-0 top-0 z-40 xl:w-96 sm:w-[15em] w-full border-r border-light-30 dark:border-dark-30 flex flex-col justify-between max-sm:absolute max-sm:-top-0 max-sm:-bottom-0 sm:left-0 max-sm:-left-full max-sm:group-aria-expanded:left-0 max-sm:transition-[left] py-8 max-sm:z-40">
|
||||
<div class="relative bottom-6 flex flex-1 flex-col gap-4 xl:px-6 px-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<NuxtLink @click="hideNavigation" class=" text-light-100 dark:text-dark-100 hover:text-opacity-70 max-sm:ps-6" aria-label="Accueil" :to="{ path: '/', force: true }"><ThemeIcon class="inline" icon="logo" :width=56 :height=56 /></NuxtLink>
|
||||
<div class="flex gap-4 items-center">
|
||||
<ThemeSwitch />
|
||||
<NuxtLink @click="hideNavigation" class="" :to="{ path: '/user/profile', force: true }"><ThemeIcon icon="user" :width=32 :height=32 /></NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex"><SearchView @navigate="hideNavigation" /></div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="text-center xl:text-sm text-xs text-light-70 dark:text-dark-70">
|
||||
<NuxtLink class="hover:underline italic" :to="{ path: '/third-party', force: true }">Mentions légales</NuxtLink>
|
||||
<p>Copyright Peaceultime - 2024</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
91
components/standard/SearchView.vue
Normal file
91
components/standard/SearchView.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import type { Search, File, Project, User } from '~/types/api';
|
||||
|
||||
const input = ref(''), results = ref<Search>({ projects: [], files: [], users: [] });
|
||||
const pos = ref<DOMRect>(), loading = ref(false);
|
||||
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
const emit = defineEmits(['navigate']);
|
||||
|
||||
function search(e: Event)
|
||||
{
|
||||
pos.value = (e.currentTarget as HTMLElement)?.getBoundingClientRect();
|
||||
|
||||
loading.value = true;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(debounced, 250);
|
||||
}
|
||||
async function debounced()
|
||||
{
|
||||
if(!input.value)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
const fetch = await useFetch(`/api/search`, {
|
||||
query: {
|
||||
"search": `%${input.value}%`
|
||||
}
|
||||
});
|
||||
|
||||
results.value = fetch.data.value;
|
||||
}
|
||||
catch (e) {
|
||||
results.value = { projects: [], files: [], users: [] };
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full border border-light-30 dark:border-dark-30">
|
||||
<Input class="w-full" type="text" placeholder="Recherche" v-model="input" @input="search" />
|
||||
</div>
|
||||
<Teleport to="#teleports" v-if="input !== '' && !!pos">
|
||||
<div class="absolute z-50 border border-light-35 dark:border-dark-35 bg-light-0 dark:bg-dark-0 text-light-100 dark:text-dark-100 divide-y divide-light-35 dark:divide-dark-35 max-h-96 overflow-auto
|
||||
scroll"
|
||||
:style="{top: (pos.bottom + 4) + 'px', left: pos.left + 'px', width: pos.width + 'px'}">
|
||||
<div class="loading" v-if="loading"></div>
|
||||
<template v-else>
|
||||
<div class="cursor-pointer hover:bg-light-25 dark:hover:bg-dark-25 px-4 py-1 " v-for="result of results.projects" :key="result.id"
|
||||
@mouseenter="(e) => (e.target as HTMLElement).classList.add('is-selected')"
|
||||
@mouseleave="(e) => (e.target as HTMLElement).classList.remove('is-selected')"
|
||||
@mousedown.prevent="navigateTo(`/explorer/${result.id}/${result.home}`); input = ''; emit('navigate');">
|
||||
<div class="">
|
||||
<Highlight class="text-lg" :text="result.name" :matched="input" />
|
||||
<div class="flex justify-between text-sm">
|
||||
<div class="">{{ result.username }}</div>
|
||||
<div class="">{{ result.pages }} pages</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cursor-pointer hover:bg-light-25 dark:hover:bg-dark-25 px-4 py-1 " v-for="result of results.files" :key="result.id"
|
||||
@mouseenter="(e) => (e.target as HTMLElement).classList.add('is-selected')"
|
||||
@mouseleave="(e) => (e.target as HTMLElement).classList.remove('is-selected')"
|
||||
@mousedown.prevent="navigateTo(`/explorer/${result.project}/${result.path}`); input = ''; emit('navigate');">
|
||||
<div class="">
|
||||
<Highlight class="text-lg" :text="result.title" :matched="input" />
|
||||
<div class="flex justify-between text-sm">
|
||||
<div class="">{{ result.username }}</div>
|
||||
<div class="">{{ result.comments }} commentaires</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cursor-pointer hover:bg-light-25 dark:hover:bg-dark-25 px-4 py-1 " v-for="result of results.users" :key="result.id"
|
||||
@mouseenter="(e) => (e.target as HTMLElement).classList.add('is-selected')"
|
||||
@mouseleave="(e) => (e.target as HTMLElement).classList.remove('is-selected')"
|
||||
@mousedown.prevent="navigateTo(`/user/${result.id}`); input = ''; emit('navigate');">
|
||||
<div class="">
|
||||
<Highlight class="text-lg" :text="result.username" :matched="input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class=""
|
||||
v-if="results.projects.length === 0 && results.files.length === 0 && results.users.length === 0">
|
||||
Aucun résultat
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
16
components/standard/ThemeIcon.vue
Normal file
16
components/standard/ThemeIcon.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
interface Prop
|
||||
{
|
||||
icon: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
defineProps<Prop>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<img :src="`/icons/${icon}.light.svg`" class="block dark:hidden" :width="width" :height="height" />
|
||||
<img :src="`/icons/${icon}.dark.svg`" class="dark:block hidden" :width="width" :height="height" />
|
||||
</span>
|
||||
</template>
|
||||
39
components/standard/ThemeSwitch.client.vue
Normal file
39
components/standard/ThemeSwitch.client.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script setup>
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const isDark = computed({
|
||||
get() {
|
||||
return colorMode.value === 'dark'
|
||||
},
|
||||
set() {
|
||||
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex relative">
|
||||
<span class="hidden dark:block absolute top-[3px] left-[2px] z-[1] px-[5px]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4 stroke-light-100 dark:stroke-dark-100">
|
||||
<path d="M12 3a6.364 6.364 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<div class="
|
||||
before:absolute before:top-0 before:left-0 before:right-0 before:bottom-0 before:opacity-0 before:block before:z-10
|
||||
after:transition-all after:w-4 after:h-4 after:border after:border-light-30 dark:after:border-dark-30 after:block after:m-[3px] after:top-[-1px] after:pointer-events-none after:absolute after:left-0 after:bg-light-0 after:translate-x-[1px] dark:after:translate-x-[26px]
|
||||
inline-block relative cursor-pointer w-[50px] h-[22px] select-none border border-light-35 dark:border-dark-35 bg-light-0 dark:bg-dark-30 dark:hover:border-dark-35" @click="isDark = !isDark"></div>
|
||||
<span class="block dark:hidden absolute top-[3px] left-[22px] z-[1] px-[5px]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4 stroke-light-100 dark:stroke-dark-100 ">
|
||||
<circle cx="12" cy="12" r="4"></circle>
|
||||
<path d="M12 2v2"></path>
|
||||
<path d="M12 20v2"></path>
|
||||
<path d="m4.93 4.93 1.41 1.41"></path>
|
||||
<path d="m17.66 17.66 1.41 1.41"></path>
|
||||
<path d="M2 12h2"></path>
|
||||
<path d="M20 12h2"></path>
|
||||
<path d="m6.34 17.66-1.41 1.41"></path>
|
||||
<path d="m19.07 4.93-1.41 1.41"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user