@anja.codes/create-vitakit 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anja.codes/create-vitakit",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Scaffold a vitakit project: an academic website and a LaTeX CV from one set of files.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -1,39 +1,183 @@
1
1
  <script setup lang="ts">
2
2
  const profile = useProfile()
3
+ const route = useRoute()
4
+
5
+ // Mobile menu (below the `lg` breakpoint, i.e. md and smaller screens).
6
+ const menuOpen = ref(false)
7
+ const menuButton = ref<HTMLButtonElement | null>(null)
8
+ const closeButton = ref<HTMLButtonElement | null>(null)
9
+
10
+ function openMenu() {
11
+ menuOpen.value = true
12
+ }
13
+
14
+ function closeMenu() {
15
+ menuOpen.value = false
16
+ }
17
+
18
+ // Close after navigating, so the new page is visible.
19
+ watch(() => route.fullPath, closeMenu)
20
+
21
+ function onKeydown(event: KeyboardEvent) {
22
+ if (event.key === 'Escape') closeMenu()
23
+ }
24
+
25
+ // Lock page scrolling while the menu is open, and move focus into the
26
+ // panel (and back to the toggle when it closes).
27
+ watch(menuOpen, async (open) => {
28
+ if (!import.meta.client) return
29
+ const root = document.documentElement
30
+ if (open) {
31
+ // Keep the layout from shifting when the scrollbar disappears.
32
+ const scrollbar = window.innerWidth - root.clientWidth
33
+ root.style.overflow = 'hidden'
34
+ if (scrollbar > 0) root.style.paddingRight = `${scrollbar}px`
35
+ document.addEventListener('keydown', onKeydown)
36
+ await nextTick()
37
+ closeButton.value?.focus()
38
+ } else {
39
+ unlockPage()
40
+ menuButton.value?.focus()
41
+ }
42
+ })
43
+
44
+ function unlockPage() {
45
+ const root = document.documentElement
46
+ root.style.overflow = ''
47
+ root.style.paddingRight = ''
48
+ document.removeEventListener('keydown', onKeydown)
49
+ }
50
+
51
+ // Close the menu if the window grows past the mobile breakpoint.
52
+ let desktopQuery: MediaQueryList | undefined
53
+ function onBreakpointChange(event: MediaQueryListEvent) {
54
+ if (event.matches) closeMenu()
55
+ }
56
+
57
+ onMounted(() => {
58
+ desktopQuery = window.matchMedia('(min-width: 64rem)')
59
+ desktopQuery.addEventListener('change', onBreakpointChange)
60
+ })
61
+
62
+ onBeforeUnmount(() => {
63
+ desktopQuery?.removeEventListener('change', onBreakpointChange)
64
+ unlockPage()
65
+ })
3
66
  </script>
4
67
 
5
68
  <template>
6
- <div class="min-h-screen flex flex-col">
7
- <!-- Shows content problems while developing; a build fails on them instead. -->
8
- <VitaIssues />
9
-
10
- <header class="border-b border-neutral-200">
11
- <div class="mx-auto flex max-w-3xl flex-wrap items-baseline gap-x-6 gap-y-2 px-4 py-6">
12
- <NuxtLink to="/" class="text-lg font-semibold no-underline">
13
- {{ profile.name }}
14
- </NuxtLink>
15
- <nav class="ml-auto flex gap-4 text-sm">
16
- <NuxtLink
17
- v-for="item in profile.nav"
18
- :key="item.url"
19
- :to="item.url"
20
- class="no-underline hover:underline"
21
- >
22
- {{ item.label }}
69
+ <div>
70
+ <!-- `inert` keeps the page behind the open menu from being focused or clicked. -->
71
+ <div class="min-h-screen flex flex-col" :inert="menuOpen">
72
+ <!-- Shows content problems while developing; a build fails on them instead. -->
73
+ <VitaIssues />
74
+
75
+ <header class="border-b border-neutral-200">
76
+ <div class="mx-auto flex max-w-3xl items-center gap-x-6 px-4 py-6">
77
+ <NuxtLink to="/" class="text-lg font-semibold no-underline">
78
+ {{ profile.name }}
23
79
  </NuxtLink>
24
- </nav>
25
- </div>
26
- </header>
27
-
28
- <main class="mx-auto w-full max-w-3xl flex-1 px-4 py-10">
29
- <slot />
30
- </main>
31
-
32
- <footer class="border-t border-neutral-200">
33
- <div class="mx-auto flex max-w-3xl flex-wrap items-baseline gap-2 px-4 py-6 text-sm text-neutral-600">
34
- <p>&copy; {{ new Date().getFullYear() }} {{ profile.name }}</p>
35
- <VitaMarkdown class="ml-auto text-xs text-neutral-400" :html="profile.footerHtml" />
36
- </div>
37
- </footer>
80
+
81
+ <!-- Desktop navigation -->
82
+ <nav class="ml-auto hidden gap-4 text-sm lg:flex" aria-label="Main">
83
+ <NuxtLink
84
+ v-for="item in profile.nav"
85
+ :key="item.url"
86
+ :to="item.url"
87
+ class="no-underline hover:underline"
88
+ >
89
+ {{ item.label }}
90
+ </NuxtLink>
91
+ </nav>
92
+
93
+ <!-- Mobile menu toggle -->
94
+ <button
95
+ ref="menuButton"
96
+ type="button"
97
+ class="-mr-2 ml-auto rounded p-2 text-neutral-700 hover:bg-neutral-100 lg:hidden"
98
+ aria-controls="mobile-menu"
99
+ :aria-expanded="menuOpen"
100
+ @click="openMenu"
101
+ >
102
+ <span class="sr-only">Open menu</span>
103
+ <svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
104
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5M3.75 17.25h16.5" />
105
+ </svg>
106
+ </button>
107
+ </div>
108
+ </header>
109
+
110
+ <main class="mx-auto w-full max-w-3xl flex-1 px-4 py-10">
111
+ <slot />
112
+ </main>
113
+
114
+ <footer class="border-t border-neutral-200">
115
+ <div class="mx-auto flex max-w-3xl flex-wrap items-baseline gap-2 px-4 py-6 text-sm text-neutral-600">
116
+ <p>&copy; {{ new Date().getFullYear() }} {{ profile.name }}</p>
117
+ <VitaMarkdown class="ml-auto text-xs text-neutral-400" :html="profile.footerHtml" />
118
+ </div>
119
+ </footer>
120
+ </div>
121
+
122
+ <!-- Mobile slide-over menu -->
123
+ <Teleport to="body">
124
+ <Transition
125
+ enter-active-class="transition-opacity duration-300 ease-out"
126
+ enter-from-class="opacity-0"
127
+ leave-active-class="transition-opacity duration-200 ease-in"
128
+ leave-to-class="opacity-0"
129
+ >
130
+ <div
131
+ v-if="menuOpen"
132
+ class="fixed inset-0 z-40 bg-neutral-900/40 lg:hidden"
133
+ aria-hidden="true"
134
+ @click="closeMenu"
135
+ />
136
+ </Transition>
137
+
138
+ <Transition
139
+ enter-active-class="transition-transform duration-300 ease-out"
140
+ enter-from-class="translate-x-full"
141
+ leave-active-class="transition-transform duration-200 ease-in"
142
+ leave-to-class="translate-x-full"
143
+ >
144
+ <div
145
+ v-if="menuOpen"
146
+ id="mobile-menu"
147
+ class="fixed inset-y-0 right-0 z-50 flex w-full max-w-xs flex-col overflow-y-auto bg-white shadow-xl lg:hidden"
148
+ role="dialog"
149
+ aria-modal="true"
150
+ aria-label="Menu"
151
+ >
152
+ <div class="flex items-center justify-between border-b border-neutral-200 px-4 py-6">
153
+ <span class="text-lg font-semibold">{{ profile.name }}</span>
154
+ <button
155
+ ref="closeButton"
156
+ type="button"
157
+ class="-mr-2 rounded p-2 text-neutral-700 hover:bg-neutral-100"
158
+ @click="closeMenu"
159
+ >
160
+ <span class="sr-only">Close menu</span>
161
+ <svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
162
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
163
+ </svg>
164
+ </button>
165
+ </div>
166
+
167
+ <nav class="flex flex-col px-2 py-4" aria-label="Main">
168
+ <NuxtLink
169
+ v-for="item in profile.nav"
170
+ :key="item.url"
171
+ :to="item.url"
172
+ class="rounded px-2 py-3 text-base no-underline hover:bg-neutral-100"
173
+ active-class="font-semibold"
174
+ @click="closeMenu"
175
+ >
176
+ {{ item.label }}
177
+ </NuxtLink>
178
+ </nav>
179
+ </div>
180
+ </Transition>
181
+ </Teleport>
38
182
  </div>
39
183
  </template>
@@ -30,4 +30,4 @@ nav:
30
30
  - { label: CV, to: /cv/ }
31
31
 
32
32
  # Inline Markdown, shown quietly at the bottom of every page.
33
- footer: Built with [vitakit](https://github.com/anja-codes/vitakit).
33
+ footer: Built with [vitakit](https://vitakit.anja.codes/).
@@ -1,5 +1,7 @@
1
1
  # Selection for this document. Order lives in cv.tex.
2
2
  visibility: cv # include entries marked public and cv, not private
3
+ # portrait: /img/portrait.jpg # a photo beside the header: JPG, PNG or PDF below public/;
4
+ # # true uses avatar from content/profile.yml
3
5
 
4
6
  sections:
5
7
  talks:
@@ -16,7 +16,7 @@
16
16
  "pdf": "vitakit pdf"
17
17
  },
18
18
  "dependencies": {
19
- "@anja.codes/vitakit": "^0.3.0",
19
+ "@anja.codes/vitakit": "^0.5.0",
20
20
  "@nuxt/fonts": "^0.14.0",
21
21
  "@tailwindcss/vite": "^4.3.3",
22
22
  "nuxt": "^4.5.2",