add TOC add better blog navigation and a new blog post
This commit is contained in:
21
components/MiniBlogCard.vue
Normal file
21
components/MiniBlogCard.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps({
|
||||
to: String,
|
||||
title: String,
|
||||
description: String,
|
||||
rightAlign: Boolean,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink :to="props.to"
|
||||
class="py-8 px-6 border-[#ECE6E7] dark:border-[#232326] border rounded-lg hover:dark:bg-obsidian-night hover:bg-[hsl(270,68%,95.71%)] hover:transition-colors select-none"
|
||||
:class="props.rightAlign ? 'text-right' : ''">
|
||||
<div
|
||||
class="p-1.5 inline-flex bg-[#ECE6E7] dark:bg-[#1A1A1D] rounded-full mb-4 border border-gray-300/80 dark:border-neutral-800/70">
|
||||
<Icon :name="props.rightAlign ? 'tabler:arrow-right' : 'tabler:arrow-left'" size="24" />
|
||||
</div>
|
||||
<p class="font-semibold">{{ props.title }}</p>
|
||||
<p class="text-sm">{{ props.description }}</p>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
@@ -2,96 +2,76 @@
|
||||
let colorMode = useColorMode();
|
||||
|
||||
const changeTheme = () => {
|
||||
if (colorMode.preference === "dark") {
|
||||
// from dark => light
|
||||
colorMode.preference = "light"
|
||||
document.documentElement.classList.remove("dark");
|
||||
} else if (colorMode.preference === "light") {
|
||||
// from light => system
|
||||
colorMode.preference = "system";
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
} else {
|
||||
// from system => dark
|
||||
colorMode.preference = "dark";
|
||||
document.documentElement.classList.add("dark");
|
||||
}
|
||||
if (colorMode.preference === "dark") {
|
||||
// from dark => light
|
||||
colorMode.preference = "light"
|
||||
} else if (colorMode.preference === "light") {
|
||||
// from light => system
|
||||
colorMode.preference = "system";
|
||||
} else {
|
||||
// from system => dark
|
||||
colorMode.preference = "dark";
|
||||
}
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="h-16 z-10 w-full">
|
||||
<div class="px-6 max-w-7xl grid gap-2 grid-cols-12 ms-auto me-auto h-full justify-evenly">
|
||||
<div class="ml-0 col-span-6 sm:col-span-4 flex items-center">
|
||||
<ul class="flex gap-x-8">
|
||||
<li
|
||||
class="absolute w-fit -translate-x-full top-0 px-2 py-4 bg-soft-lavender dark:bg-midnight text-deep-indigo dark:text-white opacity-0 focus-within:translate-x-0 focus-within:opacity-100">
|
||||
<a href="#main">Skip to content</a>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/">
|
||||
Home
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/blog">
|
||||
Blog
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mr-0 col-span-4 items-center hidden sm:flex"> </div>
|
||||
<div class="mr-0 col-span-6 sm:col-span-4 flex items-center justify-end">
|
||||
<ul class="flex gap-x-4">
|
||||
<li>
|
||||
<a href="https://www.github.com/juls0730">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 19c-4.3 1.4-4.3-2.5-6-3m12 5v-3.5c0-1 .1-1.4-.5-2c2.8-.3 5.5-1.4 5.5-6a4.6 4.6 0 0 0-1.3-3.2a4.2 4.2 0 0 0-.1-3.2s-1.1-.3-3.5 1.3a12.3 12.3 0 0 0-6.2 0C6.5 2.8 5.4 3.1 5.4 3.1a4.2 4.2 0 0 0-.1 3.2A4.6 4.6 0 0 0 4 9.5c0 4.6 2.7 5.7 5.5 6c-.6.6-.6 1.2-.5 2V21" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://x.com/julie4055_">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="m4 4l11.733 16H20L8.267 4zm0 16l6.768-6.768m2.46-2.46L20 4" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button @click="changeTheme">
|
||||
<div v-if="$colorMode.preference === 'dark'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 3h.393a7.5 7.5 0 0 0 7.92 12.446A9 9 0 1 1 12 2.992z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div v-else-if="$colorMode.preference === 'light'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2"
|
||||
d="M14.828 14.828a4 4 0 1 0-5.656-5.656a4 4 0 0 0 5.656 5.656m-8.485 2.829l-1.414 1.414M6.343 6.343L4.929 4.929m12.728 1.414l1.414-1.414m-1.414 12.728l1.414 1.414M4 12H2m10-8V2m8 10h2m-10 8v2" />
|
||||
</svg>
|
||||
</div>
|
||||
<div v-else>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 256 256">
|
||||
<path fill="currentColor"
|
||||
d="M208 36H48a28 28 0 0 0-28 28v112a28 28 0 0 0 28 28h160a28 28 0 0 0 28-28V64a28 28 0 0 0-28-28Zm4 140a4 4 0 0 1-4 4H48a4 4 0 0 1-4-4V64a4 4 0 0 1 4-4h160a4 4 0 0 1 4 4Zm-40 52a12 12 0 0 1-12 12H96a12 12 0 0 1 0-24h64a12 12 0 0 1 12 12Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<nav class="h-16 z-10 w-full">
|
||||
<div class="px-6 max-w-7xl grid gap-2 grid-cols-12 ms-auto me-auto h-full justify-evenly">
|
||||
<div class="ml-0 col-span-6 sm:col-span-4 flex items-center">
|
||||
<ul class="flex gap-x-8">
|
||||
<li
|
||||
class="absolute w-fit -translate-x-full top-0 px-2 py-4 bg-soft-lavender dark:bg-midnight text-deep-indigo dark:text-white opacity-0 focus-within:translate-x-0 focus-within:opacity-100">
|
||||
<a href="#main">Skip to content</a>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/">
|
||||
Home
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/blog">
|
||||
Blog
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mr-0 col-span-4 items-center hidden sm:flex"> </div>
|
||||
<div class="mr-0 col-span-6 sm:col-span-4 flex items-center justify-end">
|
||||
<ul class="flex gap-x-4">
|
||||
<li>
|
||||
<a href="https://www.github.com/juls0730">
|
||||
<button>
|
||||
<Icon name="tabler:brand-github" size="22" />
|
||||
</button>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://x.com/julie4055_">
|
||||
<button>
|
||||
<Icon name="tabler:brand-x" size="22" />
|
||||
</button>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button @click="changeTheme">
|
||||
<Icon v-if="$colorMode.preference === 'dark'" name="tabler:moon" size="22" />
|
||||
<Icon v-else-if="$colorMode.preference === 'light'" name="tabler:sun-high" size="22" />
|
||||
<Icon v-else name="ph:monitor-bold" size="22" />
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped>
|
||||
button {
|
||||
padding: 0.25rem
|
||||
}
|
||||
</style>
|
||||
@@ -6,7 +6,7 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="dark:bg-dark-slate bg-touched-lavender relative border p-6 col-span-12 sm:col-span-10 sm:col-start-2 md:col-start-auto md:col-span-6 xl:!col-span-4 h-[60vw] max-h-[425px] min-h-[375px] border-soft-lilac dark:border-midnight-slate/30 shadow-md rounded-lg">
|
||||
class="dark:bg-dark-slate bg-touched-lavender relative border p-6 h-[60vw] max-h-[425px] min-h-[375px] border-soft-lilac dark:border-midnight-slate/30 shadow-md rounded-lg">
|
||||
<div class="flex mb-4 items-center" v-if="headerIcon">
|
||||
<Icon size="64" class="text-sea-green" :name="headerIcon" />
|
||||
<div class="ml-auto flex">
|
||||
|
||||
75
components/TableOfContents.vue
Normal file
75
components/TableOfContents.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
const props = withDefaults(defineProps<{ doc: any, activeTocId: string | null }>(), {})
|
||||
const router = useRouter()
|
||||
|
||||
const sliderHeight = useState('sliderHeight', () => 0)
|
||||
const sliderTop = useState('sliderTop', () => 0)
|
||||
const tocLinksH2: Ref<Array<HTMLElement>> = ref([])
|
||||
const tocLinksH3: Ref<Array<HTMLElement>> = ref([])
|
||||
|
||||
const tocLinks = computed(() => props.doc?.body?.toc?.links ?? [])
|
||||
|
||||
const onClick = (id: string) => {
|
||||
const el = document.getElementById(id)
|
||||
if (el) {
|
||||
router.push({ hash: `#${id}` })
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}
|
||||
}
|
||||
|
||||
const tocHeader = ref();
|
||||
const tocIsClosed = ref(false);
|
||||
|
||||
watchDebounced(
|
||||
() => props.activeTocId,
|
||||
(newActiveTocId) => {
|
||||
const h2Link = tocLinksH2.value.find((el: HTMLElement) => el.id === `toc-${newActiveTocId}`)
|
||||
const h3Link = tocLinksH3.value.find((el: HTMLElement) => el.id === `toc-${newActiveTocId}`)
|
||||
|
||||
// TODO: dont hard code these offsets
|
||||
if (h2Link) {
|
||||
sliderHeight.value = h2Link.offsetHeight
|
||||
sliderTop.value = h2Link.offsetTop - 24
|
||||
} else if (h3Link) {
|
||||
sliderHeight.value = h3Link.offsetHeight
|
||||
sliderTop.value = h3Link.offsetTop - 24
|
||||
}
|
||||
},
|
||||
{ debounce: 0, immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border-[#ECE6E7] dark:border-[#232326] pb-3 border-b border-dashed lg:border-b-0 overflow-hidden"
|
||||
v-if="tocLinks.length > 0">
|
||||
<span ref="tocHeader" @click="tocIsClosed = !tocIsClosed"
|
||||
class="cursor-pointer lg:cursor-auto flex justify-between">
|
||||
<h4 class="font-bold lg:mb-0">Table of Contents</h4>
|
||||
<Icon name="tabler:chevron-down" class="lg:!hidden transition-transform duration-200"
|
||||
:class="tocIsClosed ? 'rotate-180' : ''" />
|
||||
</span>
|
||||
<nav class="flex space-y-3 overflow-ellipsis">
|
||||
<div class="relative w-0.5 rounded hidden lg:block">
|
||||
<div class="absolute left-0 w-full transition-all duration-200 rounded bg-fuschia"
|
||||
:style="{ height: `${sliderHeight}px`, top: `${sliderTop}px` }"></div>
|
||||
</div>
|
||||
<ul class="lg:pl-4 lg:block" :class="tocIsClosed ? 'hidden' : ''">
|
||||
<li role="link" v-for="{ id, text, children } in tocLinks" :id="`toc-${id}`" :key="id" ref="tocLinksH2"
|
||||
class="cursor-pointer lg:text-sm ml-0 mb-2 last:mb-0"
|
||||
:class="{ 'font-semibold': id === activeTocId }" @click="onClick(id)">
|
||||
{{ text }}
|
||||
<ul v-if="children" class="ml-3 my-2">
|
||||
<li role="link" v-for=" { id: childId, text: childText } in children" :id="`toc-${childId}`"
|
||||
:key="childId" ref="tocLinksH3" class="cursor-pointer lg:text-xs ml-0 mb-2 last:mb-0"
|
||||
:class="{ 'font-semibold': childId === activeTocId }" @click.stop="onClick(childId)">
|
||||
{{ childText }}
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,121 +1,136 @@
|
||||
<template>
|
||||
<div class="container my-2 dark:bg-[#1d1b1d] bg-[hsl(270,26.89%,94.47%)]">
|
||||
<span v-if="filename" class="filename-text text-xs leading-none tracking-tight text-gray-400 font-jetbrains">
|
||||
{{ filename }}
|
||||
</span>
|
||||
<slot />
|
||||
<div class="bottom-container">
|
||||
<div class="copy-container">
|
||||
<button
|
||||
class="rounded hover:bg-zinc-300/70 dark:hover:bg-zinc-800/60 transition-colors duration-200 flex p-1"
|
||||
@click="copy(code)" @keypress.space="copy(code)">
|
||||
<div class="h-6" v-if="copied">
|
||||
<Icon size="24" name="tabler:check" />
|
||||
</div>
|
||||
<div class="h-6" v-else>
|
||||
<Icon size="24" name="tabler:copy" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="code-container" class="pt-3 overflow-hidden rounded-md my-2 dark:bg-[#1d1b1d] bg-[#f1edf5] shadow-sm">
|
||||
<div class="flex justify-between mx-2 mb-1">
|
||||
<span v-if="language"
|
||||
class="px-2 py-1 text-xs leading-none tracking-tight text-gray-400 font-jetbrains capitalize">
|
||||
{{ language }}
|
||||
</span>
|
||||
<span v-if="filename" class="px-2 py-1 text-xs leading-none tracking-tight text-gray-400 font-jetbrains">
|
||||
{{ filename }}
|
||||
</span>
|
||||
</div>
|
||||
<div ref="codeElm">
|
||||
<slot />
|
||||
</div>
|
||||
<div class="bottom-container">
|
||||
<div class="copy-container">
|
||||
<button class="p-1 hover:bg-zinc-300/70 dark:hover:bg-zinc-700/40" @click="copyCode()"
|
||||
@keypress.space="copyCode()">
|
||||
<div class="h-6" v-if="copied">
|
||||
<Icon size="24" name="tabler:check" />
|
||||
</div>
|
||||
<div class="h-6" v-else>
|
||||
<Icon size="24" name="tabler:copy" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
const { copy, copied } = useClipboard();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
code?: string;
|
||||
language?: string | null;
|
||||
filename?: string | null;
|
||||
highlights?: Array<any>;
|
||||
}>(),
|
||||
{ code: '', language: null, filename: null, highlights: undefined }
|
||||
defineProps<{
|
||||
code?: string;
|
||||
language?: string | null;
|
||||
filename?: string | null;
|
||||
highlights?: Array<number>;
|
||||
}>(),
|
||||
{ code: '', language: null, filename: null, highlights: [] }
|
||||
);
|
||||
|
||||
const codeElm = ref();
|
||||
|
||||
const copyCode = () => {
|
||||
if (!codeElm.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
let str = "";
|
||||
let lines = codeElm.value.getElementsByClassName("line");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let line = lines[i]
|
||||
str += line.textContent
|
||||
if (!str.endsWith("\n") && i != lines.length - 1) {
|
||||
str += "\n"
|
||||
}
|
||||
}
|
||||
|
||||
copy(str);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
padding-top: 0.5em;
|
||||
overflow: hidden;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.container:is(:hover, :focus-within) .bottom-container {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
#code-container:is(:hover, :focus-within) .bottom-container {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.bottom-container {
|
||||
display: flex;
|
||||
transition-property: opacity;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 250ms;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
transition-property: opacity;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 250ms;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.copy-container {
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
.filename-text {
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
right: 0.25rem;
|
||||
padding: 0.25em 0.5em;
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
:slotted(pre) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
display: flex;
|
||||
flex: 1 1 0%;
|
||||
overflow-x: auto;
|
||||
padding: 1rem;
|
||||
line-height: 1.625;
|
||||
counter-reset: lines;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
display: flex;
|
||||
flex: 1 1 0%;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 1rem;
|
||||
line-height: 1.625;
|
||||
counter-reset: lines;
|
||||
}
|
||||
|
||||
:slotted(pre code) {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: max-content;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:slotted(pre code .line) {
|
||||
min-height: 1em;
|
||||
min-width: 100%;
|
||||
min-height: 1em;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
:slotted(pre code .line::before) {
|
||||
counter-increment: lines;
|
||||
content: counter(lines);
|
||||
width: 1em;
|
||||
margin-right: 1.5rem;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
color: rgba(115, 138, 148, 0.4);
|
||||
counter-increment: lines;
|
||||
content: counter(lines);
|
||||
width: 1em;
|
||||
margin-right: 1.5rem;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
color: rgba(115, 138, 148, 0.4);
|
||||
}
|
||||
|
||||
:slotted(pre code .highlight) {
|
||||
background-color: #363b46;
|
||||
display: block;
|
||||
margin-right: -1em;
|
||||
margin-left: -1em;
|
||||
padding-right: 1em;
|
||||
padding-left: 0.75em;
|
||||
border-left: 0.25em solid red;
|
||||
:slotted(pre code .highlighted) {
|
||||
background-color: #e4e1ee;
|
||||
}
|
||||
|
||||
.dark :slotted(pre code .highlighted) {
|
||||
background-color: #2e2b2e;
|
||||
}
|
||||
</style>
|
||||
@@ -1,41 +1,35 @@
|
||||
<template>
|
||||
<div :id="id"
|
||||
class="group flex mt-2">
|
||||
<div class="text-2xl">
|
||||
<slot />
|
||||
</div>
|
||||
<button @click="copy(location.origin + location.pathname + '#' + id)"
|
||||
class="group-hover:opacity-100 ml-2 hover:bg-zinc-800 flex items-center h-fit p-[2px] transition-opacity duration-200 rounded opacity-0">
|
||||
<div class="h-4"
|
||||
v-if="copied">
|
||||
<Icon size="16"
|
||||
name="tabler:check" />
|
||||
</div>
|
||||
<div class="h-4"
|
||||
v-else>
|
||||
<Icon size="16"
|
||||
name="tabler:link" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="group flex mt-2">
|
||||
<h2 :id="id" class="text-2xl">
|
||||
<slot />
|
||||
</h2>
|
||||
<button @click="copy(location.origin + location.pathname + '#' + id)"
|
||||
class="dark:text-white ml-2 group-hover:opacity-100 opacity-0 transition-all">
|
||||
<div class="h-5" v-if="copied">
|
||||
<Icon size="20" name="tabler:check" />
|
||||
</div>
|
||||
<div class="h-5" v-else>
|
||||
<Icon size="20" name="tabler:link" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
useClipboard, useBrowserLocation
|
||||
} from '@vueuse/core';
|
||||
const { copy, copied, text } = useClipboard();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
id?: string;
|
||||
}>(),
|
||||
{ id: '' }
|
||||
import { useBrowserLocation, useClipboard } from '@vueuse/core';
|
||||
const { copy, copied } = useClipboard();
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
id?: string;
|
||||
}>(),
|
||||
{ id: '' }
|
||||
);
|
||||
|
||||
const location = useBrowserLocation()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
35
components/content/ProseH3.vue
Executable file
35
components/content/ProseH3.vue
Executable file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="group flex mt-2">
|
||||
<h3 :id="id" class="text-xl">
|
||||
<slot />
|
||||
</h3>
|
||||
<button @click="copy(location.origin + location.pathname + '#' + id)"
|
||||
class="dark:text-white ml-2 group-hover:opacity-100 opacity-0 transition-all">
|
||||
<div class="h-5" v-if="copied">
|
||||
<Icon size="20" name="tabler:check" />
|
||||
</div>
|
||||
<div class="h-5" v-else>
|
||||
<Icon size="20" name="tabler:link" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useBrowserLocation, useClipboard } from '@vueuse/core';
|
||||
const { copy, copied } = useClipboard();
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
id?: string;
|
||||
}>(),
|
||||
{ id: '' }
|
||||
);
|
||||
|
||||
const location = useBrowserLocation()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user