1<script setup>
2import { useDark, useToggle } from "@vueuse/core";
3
4const config = useRuntimeConfig().public;
5
6const pageBackground = ref("bg-stone-100 dark:bg-neutral-900");
7
8const isDark = useDark();
9const toggleDark = useToggle(isDark);
10
11useHead({
12 title: config.title,
13 meta: [
14 {
15 name: "viewport",
16 content: "width=device-width, initial-scale=1"
17 },
18 ...config.meta
19 ],
20 bodyAttrs: {
21 class: pageBackground
22 }
23});
24
25const { data } = await useAsyncData("navigation", () => {
26 return queryCollectionNavigation("pages", ["path"]);
27});
28
29const pages = data.value ? data.value[0]?.children : [];
30
31const links = ref(
32 [
33 {
34 icon: "ant-design:github-filled",
35 title: "GitHub",
36 href: config.links.github
37 },
38 {
39 icon: "ri:mastodon-fill",
40 title: "Mastodon",
41 href: config.links.mastodon
42 },
43 {
44 icon: "ri:bluesky-fill",
45 title: "BlueSky",
46 href: config.links.bluesky
47 }
48 ].reverse()
49);
50
51const date = new Date();
52
53function scrollToTop() {
54 window.scrollTo({
55 top: 0,
56 behavior: "smooth"
57 });
58}
59</script>
60
61<template>
62 <div :class="[
63 'page-container',
64 pageBackground,
65 'text-gray-800 dark:text-gray-300',
66 'min-h-screen max-w-4xl',
67 'grid grid-cols-1 grid-rows-[auto_1fr_auto]',
68 'mx-auto px-6',
69 ]">
70 <header :class="[
71 'border-b-2 border-stone-200 dark:border-stone-800',
72 'py-6 md:py-8',
73 'flex justify-between align-center',
74 ]">
75 <div class="flex items-center gap-4 sm:gap-6">
76 <img src="/logo.png" alt="Logo" class="hidden sm:block h-8" />
77 <NuxtLink to="/" class="text-xl leading-5 sm:text-2xl font-medium font-serif-bold">
78 {{ config.title }}
79 </NuxtLink>
80 </div>
81
82 <div class="flex items-center gap-4 sm:gap-8">
83 <Country />
84 <div
85 class="cursor-pointer"
86 @click="toggleDark()"
87 >
88 <Icon v-if="isDark" name="ri:sun-fill" size="1.5rem" mode="svg" />
89 <Icon v-else name="ri:moon-fill" size="1.5rem" mode="svg" />
90 </div>
91 <nav :class="['flex items-start gap-4', 'text-gray-800 dark:text-gray-200', 'font-semibold']">
92 <template v-for="item in pages" :key="item.path">
93 <NuxtLink v-if="item.title" :to="item.path.replace('/pages', '')">
94 {{ item.title }}
95 </NuxtLink>
96 </template>
97 </nav>
98
99 <div class="flex items-center gap-2">
100 <template v-for="link in links" :key="link.title">
101 <NuxtLink v-if="link.href" :to="link.href" target="_blank" :aria-label="link.title">
102 <Icon :name="link.icon" size="2rem" :title="link.title" />
103 </NuxtLink>
104 </template>
105 </div>
106 </div>
107 </header>
108 <main class=".content-grid">
109 <NuxtPage />
110 </main>
111 <footer :class="[
112 'border-t-2 border-stone-200 dark:border-stone-800',
113 'p-4',
114 'flex justify-between',
115 ]">
116 <p>
117 ©
118 {{ date.getFullYear() }}
119 {{ config.author }}
120
121 <span v-if="config.author !== 'finxol'" class="text-sm text-gray-500">
122 <span class="mx-3">
123 —
124 </span>
125 Theme by <a class="underline" href="https://github.com/finxol/nuxt-blog-template" target="_blank" rel="noopener noreferrer">finxol</a>
126 </span>
127 </p>
128 <button :class="['text-gray-600 dark:text-gray-300', 'font-light']" @click="scrollToTop">
129 Back to top
130 </button>
131 </footer>
132 </div>
133</template>
134
135<style>
136@import "assets/fonts/orkney/orkney.css";
137@import "assets/fonts/recoleta/recoleta.css";
138@import "assets/fonts/ibm-plex-mono/css/ibm-plex-mono-all.min.css";
139
140body {
141 font-family: 'OrkneyRegular', sans-serif;
142}
143
144h1, h2, h3, h4, h5, h6 {
145 font-family: 'recoleta-regular', serif;
146}
147
148code, pre {
149 font-family: 'IBM Plex Mono', 'Courier New', monospace;
150}
151
152html:not(.dark) .prose pre {
153 background-color: oklch(0.95 0.0013 106.42);
154 color: oklch(0.15 0.0013 106.42);
155}
156
157html.dark .schema {
158 filter: invert(1);
159}
160
161details.minor-callout summary {
162 @apply text-stone-500 px-4 text-base;
163}
164
165@media print {
166 @page {
167 size: A4 portrait;
168 }
169
170 .page-container > *:not(main) {
171 display: none;
172 }
173
174 :not(h1, h2, h3, h4, h5, h6, li) > a[href]:after {
175 content: " (" attr(href) ")";
176 font-family: 'IBM Plex Mono', 'Courier New', monospace;
177 }
178
179 p {
180 break-inside: avoid-page;
181 orphans: 3;
182 widows: 3;
183 }
184
185 /* Force all backgrounds to be white and text to be black for print */
186 html.dark * {
187 background-color: white !important;
188 color: black !important;
189 }
190
191 /* Preserve some specific text colors for readability */
192 html.dark a {
193 color: #1f2937 !important;
194 }
195
196 html.dark .text-sm {
197 color: #6b7280 !important;
198 }
199}
200
201.content-grid {
202 --padding-inline: min(2%, 1.5rem);
203
204 display: grid;
205 grid-template-columns:
206 [full-width-start] var(--padding-inline)
207 [breakout-start] var(--padding-inline)
208 [content-start] 1fr
209 [content-end]
210 var(--padding-inline) [breakout-end]
211 var(--padding-inline) [full-width-end];
212 justify-content: start;
213 align-content: start;
214 row-gap: calc(var(--spacing) * 4);
215}
216
217.content-grid > :not(.breakout, .full-width),
218.full-width > :not(.breakout, .full-width) {
219 grid-column: content;
220}
221
222.content-grid > .breakout {
223 grid-column: breakout;
224}
225
226.content-grid > .full-width {
227 grid-column: full-width;
228}
229</style>