kmcom-nuxt-layers 6.2.1 → 6.4.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/docs/MIGRATION-v6.3.0.md +234 -0
- package/docs/MIGRATION-v6.4.0.md +215 -0
- package/docs/MIGRATION.md +3 -0
- package/docs/adr/0003-modular-step-system-replaces-fluid-clamps.md +16 -0
- package/docs/component-levels.md +117 -0
- package/docs/layout-shift-causes.md +18 -0
- package/layers/content/app/components/Blog/ItemPage.vue +2 -7
- package/layers/content/app/components/Blog/List.vue +9 -2
- package/layers/content/app/components/Blog/Page.vue +9 -19
- package/layers/content/app/components/Gallery/Detail.vue +1 -0
- package/layers/content/app/components/Gallery/Grid.vue +9 -2
- package/layers/content/app/components/Gallery/ImageDetail.vue +0 -6
- package/layers/content/app/components/Gallery/ImagePage.vue +10 -6
- package/layers/content/app/components/Gallery/Page.vue +9 -19
- package/layers/content/app/components/Gallery/SlugPage.vue +2 -7
- package/layers/content/app/components/NuxtContent/Detail.vue +20 -11
- package/layers/content/app/components/NuxtContent/IndexTemplate.vue +21 -0
- package/layers/content/app/components/NuxtContent/List.vue +6 -1
- package/layers/content/app/components/Portfolio/ColorPalette.vue +2 -2
- package/layers/content/app/components/Portfolio/ItemPage.vue +2 -6
- package/layers/content/app/components/Portfolio/List.vue +19 -8
- package/layers/content/app/components/Portfolio/Page.vue +9 -19
- package/layers/content/app/composables/useContentItemSeo.ts +19 -0
- package/layers/content/app/composables/useContentSection.ts +9 -0
- package/layers/content/app/utils/galleryImageMeta.test.ts +30 -0
- package/layers/content/app/utils/galleryImageMeta.ts +11 -0
- package/layers/content/nuxt.config.ts +1 -1
- package/layers/core/app/app.config.ts +2 -2
- package/layers/core/app/app.vue +2 -1
- package/layers/core/app/assets/css/core.css +5 -11
- package/layers/core/app/assets/css/tokens/z-index.css +16 -0
- package/layers/core/app/components/DiagnosticsPage.vue +41 -41
- package/layers/core/app/components/LoadingScreen.vue +2 -0
- package/layers/core/app/utils/loading.test.ts +2 -2
- package/layers/core/app/utils/loading.ts +1 -1
- package/layers/delivery/feeds/app/components/Feeds/Index.vue +36 -75
- package/layers/delivery/feeds/app/components/Feeds/RouteCard.vue +6 -8
- package/layers/design-system/theming/nuxt.config.ts +1 -3
- package/layers/design-system/typography/app/assets/css/typography.css +41 -5
- package/layers/design-system/visual/app/components/Base/Modal.vue +34 -86
- package/layers/design-system/visual/app/components/HoverDimGroup.test.ts +38 -0
- package/layers/design-system/visual/app/components/HoverDimGroup.vue +54 -0
- package/layers/design-system/visual/app/components/Progress/Bar.vue +9 -29
- package/layers/motion/animations/app/components/Motion/Cursor.vue +1 -1
- package/layers/motion/animations/app/components/Motion/Marquee.vue +5 -0
- package/layers/motion/animations/app/components/Motion/MarqueeText.vue +2 -1
- package/layers/motion/animations/app/components/Motion/Staggered.test.ts +40 -0
- package/layers/motion/animations/app/components/Motion/Staggered.vue +37 -42
- package/layers/motion/animations/app/components/Motion/TextReveal.vue +8 -1
- package/layers/motion/animations/app/components/Motion/VelocityEffect.vue +6 -0
- package/layers/motion/animations/app/composables/useCountUp.ts +6 -0
- package/layers/motion/animations/app/composables/useCursorFollower.ts +4 -1
- package/layers/motion/animations/app/utils/pointerMotion.ts +7 -2
- package/layers/motion/scroll/app/components/Motion/HorizontalScroll.vue +9 -0
- package/layers/motion/scroll/app/components/Motion/Parallax.vue +13 -12
- package/layers/motion/scroll/app/components/Motion/PinnedSection.vue +16 -9
- package/layers/motion/scroll/app/composables/useReducedMotion.test.ts +23 -0
- package/layers/motion/scroll/app/composables/useReducedMotion.ts +14 -0
- package/layers/motion/scroll/app/utils/parallax.test.ts +24 -0
- package/layers/motion/scroll/app/utils/parallax.ts +16 -0
- package/layers/motion/scroll/nuxt.config.ts +1 -1
- package/layers/structure/layout/app/components/Layout/Grid/Debug.vue +1 -1
- package/layers/structure/layout/app/components/Layout/Page/index.vue +9 -10
- package/layers/structure/navigation/app/app.config.ts +37 -14
- package/layers/structure/navigation/app/components/Links/Group.vue +12 -38
- package/layers/structure/navigation/app/components/Links/Named.vue +9 -0
- package/layers/structure/navigation/app/components/Mast/Footer.vue +1 -1
- package/layers/structure/navigation/app/components/Mast/Nav.vue +14 -11
- package/layers/structure/navigation/app/components/Mast/NavModal.vue +95 -45
- package/layers/structure/navigation/app/components/Site/Logo.vue +35 -0
- package/layers/structure/navigation/app/components/Site/Subtitle.vue +10 -0
- package/layers/structure/navigation/app/components/Site/Title.vue +8 -0
- package/layers/structure/navigation/app/composables/mastNav.ts +38 -13
- package/layers/structure/navigation/app/types/nav.ts +34 -0
- package/layers/structure/navigation/nuxt.config.ts +7 -1
- package/package.json +1 -1
- /package/layers/{design-system/theming/app/assets/css/tokens.css → core/app/assets/css/tokens/elevation-motion.css} +0 -0
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import type { BlogCollectionItem } from '@nuxt/content'
|
|
3
|
+
|
|
2
4
|
import type { BlogQueryOptions } from '../../types/content'
|
|
3
5
|
|
|
4
|
-
const { options = {} } = defineProps<{
|
|
6
|
+
const { items = undefined, options = {} } = defineProps<{
|
|
7
|
+
items?: BlogCollectionItem[]
|
|
5
8
|
options?: BlogQueryOptions
|
|
6
9
|
}>()
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
// Fetch only when the parent didn't pass items (e.g. a related-items list it already has).
|
|
12
|
+
// ponytail: decided once at setup — toggling `items` after mount won't start or stop the fetch
|
|
13
|
+
const fetched = items ? undefined : await useBlogPosts(options)
|
|
14
|
+
const posts = computed(() => items ?? fetched?.data.value)
|
|
15
|
+
const status = computed(() => (items ? 'success' : (fetched?.status.value ?? 'idle')))
|
|
9
16
|
</script>
|
|
10
17
|
|
|
11
18
|
<template>
|
|
@@ -1,25 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
2
|
+
const { title = 'Blog', description = 'Articles and posts' } = defineProps<{
|
|
3
|
+
title?: string
|
|
4
|
+
description?: string
|
|
5
|
+
}>()
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
if (contentLayer?.sections?.blog === false) {
|
|
7
|
-
throw createError({ statusCode: 404, statusMessage: 'Not Found' })
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
useSeoMeta({
|
|
11
|
-
title: 'Blog',
|
|
12
|
-
description: 'Articles and posts',
|
|
13
|
-
})
|
|
7
|
+
useContentSection('blog')
|
|
8
|
+
useSeoMeta({ title: () => title, description: () => description })
|
|
14
9
|
</script>
|
|
15
10
|
|
|
16
11
|
<template>
|
|
17
|
-
<
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
<UPageBody>
|
|
21
|
-
<BlogList />
|
|
22
|
-
</UPageBody>
|
|
23
|
-
</LayoutGridItem>
|
|
24
|
-
</LayoutSection>
|
|
12
|
+
<NuxtContentIndexTemplate :title :description>
|
|
13
|
+
<BlogList />
|
|
14
|
+
</NuxtContentIndexTemplate>
|
|
25
15
|
</template>
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
</div>
|
|
67
67
|
</NuxtLink>
|
|
68
68
|
<button
|
|
69
|
+
:aria-label="`View ${img.alt || 'image'} larger`"
|
|
69
70
|
class="absolute top-2 right-2 z-10 cursor-pointer rounded-full bg-black/50 p-1.5 text-white opacity-0 transition-opacity duration-300 group-hover:opacity-100 hover:bg-black/70"
|
|
70
71
|
@click.stop="() => openLightbox(asGallery(item).images!, idx)"
|
|
71
72
|
>
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import type { GalleryCollectionItem } from '@nuxt/content'
|
|
3
|
+
|
|
2
4
|
import type { GalleryQueryOptions } from '../../types/content'
|
|
3
5
|
|
|
4
|
-
const { options = {} } = defineProps<{
|
|
6
|
+
const { items: passed = undefined, options = {} } = defineProps<{
|
|
7
|
+
items?: GalleryCollectionItem[]
|
|
5
8
|
options?: GalleryQueryOptions
|
|
6
9
|
}>()
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
// Fetch only when the parent didn't pass items (e.g. a related-items list it already has).
|
|
12
|
+
// ponytail: decided once at setup — toggling `items` after mount won't start or stop the fetch
|
|
13
|
+
const fetched = passed ? undefined : await useGalleryItems(options)
|
|
14
|
+
const items = computed(() => passed ?? fetched?.data.value)
|
|
15
|
+
const status = computed(() => (passed ? 'success' : (fetched?.status.value ?? 'idle')))
|
|
9
16
|
</script>
|
|
10
17
|
|
|
11
18
|
<template>
|
|
@@ -13,12 +13,6 @@
|
|
|
13
13
|
const totalImages = computed(() => item.value?.images?.length ?? 0)
|
|
14
14
|
const hasPrevious = computed(() => index > 0)
|
|
15
15
|
const hasNext = computed(() => index < totalImages.value - 1)
|
|
16
|
-
|
|
17
|
-
useSeoMeta({
|
|
18
|
-
title: () => image.value?.title || image.value?.alt || `Image ${index + 1}`,
|
|
19
|
-
description: () => image.value?.caption || item.value?.title,
|
|
20
|
-
ogImage: () => image.value?.src,
|
|
21
|
-
})
|
|
22
16
|
</script>
|
|
23
17
|
|
|
24
18
|
<template>
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type {
|
|
2
|
+
import type { GalleryCollectionItem } from '@nuxt/content'
|
|
3
3
|
|
|
4
4
|
const { slug, index } = defineProps<{
|
|
5
5
|
slug: string
|
|
6
6
|
index: number
|
|
7
7
|
}>()
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
useContentSection('gallery')
|
|
10
|
+
|
|
11
|
+
const { data } = await useCollectionItem('gallery', slug)
|
|
12
|
+
const meta = computed(() => galleryImageMeta(data.value as GalleryCollectionItem | null, index))
|
|
13
|
+
useSeoMeta({
|
|
14
|
+
title: () => meta.value.title,
|
|
15
|
+
description: () => meta.value.description,
|
|
16
|
+
ogImage: () => meta.value.ogImage,
|
|
17
|
+
})
|
|
14
18
|
</script>
|
|
15
19
|
|
|
16
20
|
<template>
|
|
@@ -1,25 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
2
|
+
const { title = 'Gallery', description = 'Photo collections and visual work' } = defineProps<{
|
|
3
|
+
title?: string
|
|
4
|
+
description?: string
|
|
5
|
+
}>()
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
if (contentLayer?.sections?.gallery === false) {
|
|
7
|
-
throw createError({ statusCode: 404, statusMessage: 'Not Found' })
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
useSeoMeta({
|
|
11
|
-
title: 'Gallery',
|
|
12
|
-
description: 'Photo collections and visual work',
|
|
13
|
-
})
|
|
7
|
+
useContentSection('gallery')
|
|
8
|
+
useSeoMeta({ title: () => title, description: () => description })
|
|
14
9
|
</script>
|
|
15
10
|
|
|
16
11
|
<template>
|
|
17
|
-
<
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
<UPageBody>
|
|
21
|
-
<GalleryGrid />
|
|
22
|
-
</UPageBody>
|
|
23
|
-
</LayoutGridItem>
|
|
24
|
-
</LayoutSection>
|
|
12
|
+
<NuxtContentIndexTemplate :title :description>
|
|
13
|
+
<GalleryGrid />
|
|
14
|
+
</NuxtContentIndexTemplate>
|
|
25
15
|
</template>
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { ContentLayerConfig } from '#layers/content/app/types/content'
|
|
3
|
-
|
|
4
2
|
const { slug } = defineProps<{
|
|
5
3
|
slug: string
|
|
6
4
|
}>()
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (contentLayer?.sections?.gallery === false) {
|
|
11
|
-
throw createError({ statusCode: 404, statusMessage: 'Not Found' })
|
|
12
|
-
}
|
|
6
|
+
useContentSection('gallery')
|
|
7
|
+
await useContentItemSeo('gallery', slug)
|
|
13
8
|
</script>
|
|
14
9
|
|
|
15
10
|
<template>
|
|
@@ -20,23 +20,32 @@
|
|
|
20
20
|
const surroundLinks = computed<ContentSurroundLink[] | undefined>(
|
|
21
21
|
() => surround.value ?? undefined
|
|
22
22
|
)
|
|
23
|
-
const itemImage = computed(() => (item.value as { image?: string } | null)?.image)
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
description: item.value
|
|
28
|
-
|
|
24
|
+
// Optional props only bound when present (exactOptionalPropertyTypes rejects `undefined`)
|
|
25
|
+
const headerProps = computed(() =>
|
|
26
|
+
item.value?.description ? { description: item.value.description } : {}
|
|
27
|
+
)
|
|
28
|
+
const surroundProps = computed(() =>
|
|
29
|
+
surroundLinks.value?.length ? { surround: surroundLinks.value } : {}
|
|
30
|
+
)
|
|
31
|
+
const tocProps = computed(() => {
|
|
32
|
+
const links = item.value?.body?.toc?.links
|
|
33
|
+
return links ? { links } : {}
|
|
29
34
|
})
|
|
30
35
|
</script>
|
|
31
36
|
|
|
32
37
|
<template>
|
|
33
38
|
<div>
|
|
34
39
|
<USkeleton v-if="status === 'pending'" class="h-96 w-full" />
|
|
40
|
+
<UAlert
|
|
41
|
+
v-else-if="status === 'error'"
|
|
42
|
+
color="error"
|
|
43
|
+
variant="subtle"
|
|
44
|
+
icon="lucide:circle-alert"
|
|
45
|
+
title="This page couldn't be loaded. Try again shortly."
|
|
46
|
+
/>
|
|
35
47
|
<UPage v-else-if="item">
|
|
36
|
-
<UPageHeader
|
|
37
|
-
:title="item.title"
|
|
38
|
-
v-bind="item.description ? { description: item.description } : {}"
|
|
39
|
-
>
|
|
48
|
+
<UPageHeader :title="item.title" v-bind="headerProps">
|
|
40
49
|
<template v-if="$slots.headline" #headline>
|
|
41
50
|
<slot name="headline" :item />
|
|
42
51
|
</template>
|
|
@@ -51,11 +60,11 @@
|
|
|
51
60
|
<slot name="after-content" :item />
|
|
52
61
|
|
|
53
62
|
<USeparator class="my-8" />
|
|
54
|
-
<NuxtContentSurround v-bind="
|
|
63
|
+
<NuxtContentSurround v-bind="surroundProps" />
|
|
55
64
|
</UPageBody>
|
|
56
65
|
|
|
57
66
|
<template v-if="!hideToc" #right>
|
|
58
|
-
<NuxtContentToc v-bind="
|
|
67
|
+
<NuxtContentToc v-bind="tocProps" />
|
|
59
68
|
</template>
|
|
60
69
|
</UPage>
|
|
61
70
|
<div v-else class="text-muted py-12 text-center">{{ notFoundMessage }}</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Content index skeleton — a page header over a list. Props and slots only: fetching, meta
|
|
4
|
+
* and the section guard belong to the *Page component that fills it.
|
|
5
|
+
*/
|
|
6
|
+
const { title, description = undefined } = defineProps<{
|
|
7
|
+
title: string
|
|
8
|
+
description?: string
|
|
9
|
+
}>()
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<LayoutSection>
|
|
14
|
+
<LayoutGridItem>
|
|
15
|
+
<UPageHeader :title v-bind="description ? { description } : {}" />
|
|
16
|
+
<UPageBody>
|
|
17
|
+
<slot />
|
|
18
|
+
</UPageBody>
|
|
19
|
+
</LayoutGridItem>
|
|
20
|
+
</LayoutSection>
|
|
21
|
+
</template>
|
|
@@ -3,16 +3,21 @@
|
|
|
3
3
|
status,
|
|
4
4
|
hasItems = true,
|
|
5
5
|
emptyMessage = 'No items found',
|
|
6
|
+
errorMessage = "These items couldn't be loaded. Try again shortly.",
|
|
6
7
|
} = defineProps<{
|
|
7
|
-
status:
|
|
8
|
+
status: 'idle' | 'pending' | 'success' | 'error'
|
|
8
9
|
hasItems?: boolean
|
|
9
10
|
emptyMessage?: string
|
|
11
|
+
errorMessage?: string
|
|
10
12
|
}>()
|
|
11
13
|
</script>
|
|
12
14
|
|
|
13
15
|
<template>
|
|
14
16
|
<div>
|
|
15
17
|
<USkeleton v-if="status === 'pending'" class="h-64 w-full" />
|
|
18
|
+
<slot v-else-if="status === 'error'" name="error">
|
|
19
|
+
<UAlert color="error" variant="subtle" icon="lucide:circle-alert" :title="errorMessage" />
|
|
20
|
+
</slot>
|
|
16
21
|
<template v-else-if="hasItems">
|
|
17
22
|
<slot />
|
|
18
23
|
</template>
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<template>
|
|
18
18
|
<div v-if="colors.length" class="space-y-6">
|
|
19
19
|
<h3 class="text-highlighted text-2xl font-semibold tracking-tight">Palette</h3>
|
|
20
|
-
<
|
|
20
|
+
<HoverDimGroup class="palette-grid gap-3">
|
|
21
21
|
<div v-for="color in colors" :key="color.value">
|
|
22
22
|
<div
|
|
23
23
|
class="border-default/20 flex aspect-2/3 cursor-default flex-col justify-between rounded-lg border p-4 shadow-lg"
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<p class="font-mono text-xs uppercase opacity-50">{{ color.value }}</p>
|
|
31
31
|
</div>
|
|
32
32
|
</div>
|
|
33
|
-
</
|
|
33
|
+
</HoverDimGroup>
|
|
34
34
|
</div>
|
|
35
35
|
</template>
|
|
36
36
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { ContentLayerConfig } from '#layers/content/app/types/content'
|
|
3
2
|
import type { PageCollections } from '@nuxt/content'
|
|
4
3
|
|
|
5
4
|
const { slug, collection = 'portfolio' } = defineProps<{
|
|
@@ -7,11 +6,8 @@
|
|
|
7
6
|
collection?: keyof PageCollections
|
|
8
7
|
}>()
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (contentLayer?.sections?.portfolio === false) {
|
|
13
|
-
throw createError({ statusCode: 404, statusMessage: 'Not Found' })
|
|
14
|
-
}
|
|
9
|
+
useContentSection('portfolio')
|
|
10
|
+
await useContentItemSeo(collection, slug)
|
|
15
11
|
</script>
|
|
16
12
|
|
|
17
13
|
<template>
|
|
@@ -3,19 +3,30 @@
|
|
|
3
3
|
|
|
4
4
|
import type { PortfolioQueryOptions } from '../../types/content'
|
|
5
5
|
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
items: passed = undefined,
|
|
8
|
+
options = {},
|
|
9
|
+
collection = 'portfolio',
|
|
10
|
+
} = defineProps<{
|
|
11
|
+
items?: Collections[keyof Collections][]
|
|
7
12
|
options?: PortfolioQueryOptions
|
|
8
13
|
collection?: keyof Collections
|
|
9
14
|
}>()
|
|
10
15
|
|
|
11
16
|
const { featured, tags, limit } = options
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// Fetch only when the parent didn't pass items (e.g. a related-items list it already has).
|
|
18
|
+
// ponytail: decided once at setup — toggling `items` after mount won't start or stop the fetch
|
|
19
|
+
const fetched = passed
|
|
20
|
+
? undefined
|
|
21
|
+
: await useCollectionItems(collection, {
|
|
22
|
+
tags,
|
|
23
|
+
limit,
|
|
24
|
+
sortKey: 'year',
|
|
25
|
+
sortDirection: 'desc',
|
|
26
|
+
filter: featured !== undefined ? (item) => fieldFeatured(item) === featured : undefined,
|
|
27
|
+
})
|
|
28
|
+
const items = computed(() => passed ?? fetched?.data.value)
|
|
29
|
+
const status = computed(() => (passed ? 'success' : (fetched?.status.value ?? 'idle')))
|
|
19
30
|
|
|
20
31
|
function fieldFeatured(item: unknown): boolean {
|
|
21
32
|
return typeof item === 'object' && item !== null && 'featured' in item
|
|
@@ -1,25 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
2
|
+
const { title = 'Portfolio', description = 'Featured projects and work' } = defineProps<{
|
|
3
|
+
title?: string
|
|
4
|
+
description?: string
|
|
5
|
+
}>()
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
if (contentLayer?.sections?.portfolio === false) {
|
|
7
|
-
throw createError({ statusCode: 404, statusMessage: 'Not Found' })
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
useSeoMeta({
|
|
11
|
-
title: 'Portfolio',
|
|
12
|
-
description: 'Featured projects and work',
|
|
13
|
-
})
|
|
7
|
+
useContentSection('portfolio')
|
|
8
|
+
useSeoMeta({ title: () => title, description: () => description })
|
|
14
9
|
</script>
|
|
15
10
|
|
|
16
11
|
<template>
|
|
17
|
-
<
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
<UPageBody>
|
|
21
|
-
<PortfolioList />
|
|
22
|
-
</UPageBody>
|
|
23
|
-
</LayoutGridItem>
|
|
24
|
-
</LayoutSection>
|
|
12
|
+
<NuxtContentIndexTemplate :title :description>
|
|
13
|
+
<PortfolioList />
|
|
14
|
+
</NuxtContentIndexTemplate>
|
|
25
15
|
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Collections } from '@nuxt/content'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Page meta for a content item. Called by the *Page components rather than the detail
|
|
5
|
+
* organisms, so an organism never sets page meta. Shares useCollectionItem's cache key with
|
|
6
|
+
* the organism that renders the item.
|
|
7
|
+
*
|
|
8
|
+
* Meta is registered before anything is awaited — Nuxt's instance is gone after an `await`
|
|
9
|
+
* inside a plain composable. Returns the async data so the caller can `await` it.
|
|
10
|
+
*/
|
|
11
|
+
export function useContentItemSeo(collection: keyof Collections, slug: string) {
|
|
12
|
+
const result = useCollectionItem(collection, slug)
|
|
13
|
+
useSeoMeta({
|
|
14
|
+
title: () => result.data.value?.title,
|
|
15
|
+
description: () => result.data.value?.description,
|
|
16
|
+
ogImage: () => (result.data.value as { image?: string } | null)?.image,
|
|
17
|
+
})
|
|
18
|
+
return result
|
|
19
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ContentLayerConfig } from '../types/content'
|
|
2
|
+
|
|
3
|
+
/** 404s the current route when `contentLayer.sections.<section>` is switched off in app.config. */
|
|
4
|
+
export function useContentSection(section: keyof NonNullable<ContentLayerConfig['sections']>) {
|
|
5
|
+
const contentLayer = useAppConfig().contentLayer as ContentLayerConfig | undefined
|
|
6
|
+
if (contentLayer?.sections?.[section] === false) {
|
|
7
|
+
throw createError({ statusCode: 404, statusMessage: 'Not Found' })
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { GalleryCollectionItem } from '@nuxt/content'
|
|
2
|
+
import { describe, expect, it } from 'vite-plus/test'
|
|
3
|
+
|
|
4
|
+
import { galleryImageMeta } from './galleryImageMeta'
|
|
5
|
+
|
|
6
|
+
const gallery = (images: Array<Record<string, string>>) =>
|
|
7
|
+
({ title: 'Autumn', images }) as unknown as GalleryCollectionItem
|
|
8
|
+
|
|
9
|
+
describe('galleryImageMeta', () => {
|
|
10
|
+
it("prefers the image's own title and caption", () => {
|
|
11
|
+
const item = gallery([{ src: '/a.jpg', alt: 'Leaf', title: 'Red leaf', caption: 'Oct' }])
|
|
12
|
+
expect(galleryImageMeta(item, 0)).toEqual({
|
|
13
|
+
title: 'Red leaf',
|
|
14
|
+
description: 'Oct',
|
|
15
|
+
ogImage: '/a.jpg',
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('falls back to alt text, then the gallery title', () => {
|
|
20
|
+
expect(galleryImageMeta(gallery([{ src: '/a.jpg', alt: 'Leaf' }]), 0)).toMatchObject({
|
|
21
|
+
title: 'Leaf',
|
|
22
|
+
description: 'Autumn',
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('names a missing image by its position', () => {
|
|
27
|
+
expect(galleryImageMeta(gallery([]), 2).title).toBe('Image 3')
|
|
28
|
+
expect(galleryImageMeta(null, 0).title).toBe('Image 1')
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { GalleryCollectionItem } from '@nuxt/content'
|
|
2
|
+
|
|
3
|
+
/** Page meta for one image of a gallery: its own title/caption, falling back to the gallery's. */
|
|
4
|
+
export function galleryImageMeta(item: GalleryCollectionItem | null | undefined, index: number) {
|
|
5
|
+
const image = item?.images?.[index]
|
|
6
|
+
return {
|
|
7
|
+
title: image?.title || image?.alt || `Image ${index + 1}`,
|
|
8
|
+
description: image?.caption || item?.title,
|
|
9
|
+
ogImage: image?.src,
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -31,8 +31,8 @@ export default defineAppConfig({
|
|
|
31
31
|
|
|
32
32
|
// Loading screen configuration (initial app load only)
|
|
33
33
|
loading: {
|
|
34
|
-
//
|
|
35
|
-
enabled:
|
|
34
|
+
// Opt in: app.vue renders LoadingScreen for every app, so it stays off unless enabled
|
|
35
|
+
enabled: false,
|
|
36
36
|
// Minimum time (ms, from navigation start) the count runs before reaching 100 (prevents flash)
|
|
37
37
|
minDuration: 1500,
|
|
38
38
|
// Longest wait (ms, after mount) for fonts and images before finishing anyway
|
package/layers/core/app/app.vue
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
@import '#layers/core/app/assets/css/tokens/breakpoints.css';
|
|
34
34
|
@import '#layers/core/app/assets/css/tokens/spacing-fluid.css';
|
|
35
35
|
@import '#layers/core/app/assets/css/tokens/container.css';
|
|
36
|
+
@import '#layers/core/app/assets/css/tokens/z-index.css';
|
|
36
37
|
|
|
37
38
|
/* Relative path, not the '#layers/typography' alias — that alias only exists
|
|
38
39
|
when the typography layer is in a consumer's `extends`. This file is a CSS
|
|
@@ -41,14 +42,7 @@
|
|
|
41
42
|
only extend core, since it lives in the same single published package. */
|
|
42
43
|
@import '../../../../design-system/typography/app/assets/css/typography.css';
|
|
43
44
|
|
|
44
|
-
/*
|
|
45
|
-
an
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
root. It previously was NOT imported here — only registered as a standalone
|
|
49
|
-
stylesheet via theming/nuxt.config.ts's `css:` array — so its @theme block
|
|
50
|
-
was silently inert: the browser just ignores the unrecognized at-rule, and
|
|
51
|
-
every rounded-, shadow-, duration- utility fell back to Tailwind's stock
|
|
52
|
-
defaults instead of this file's values. Confirmed via compiled dev output:
|
|
53
|
-
the custom --shadow-sm value never appeared; Tailwind's built-in default did. */
|
|
54
|
-
@import '../../../../design-system/theming/app/assets/css/tokens.css';
|
|
45
|
+
/* Shadow/duration tokens. Lived in theming until the 2026-10 atomic audit — moved here because
|
|
46
|
+
sub-atomic tokens belong to the foundation, and an @theme block only merges into Tailwind's
|
|
47
|
+
theme when it's in this one build root's @import chain anyway. */
|
|
48
|
+
@import '#layers/core/app/assets/css/tokens/elevation-motion.css';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI stacking scale, for layers that can't reach layout's useZIndex() (it reads
|
|
3
|
+
* layoutLayer.ui.grid.layers from app.config). The shared steps mirror that config —
|
|
4
|
+
* tests/vitest/z-index-scale.test.ts fails if they drift. Use as `z-(--z-modal)`, never a raw z-9999.
|
|
5
|
+
*/
|
|
6
|
+
:root {
|
|
7
|
+
--z-header: 100;
|
|
8
|
+
--z-dropdown: 200;
|
|
9
|
+
--z-overlay: 300;
|
|
10
|
+
--z-modal: 400;
|
|
11
|
+
--z-toast: 500;
|
|
12
|
+
|
|
13
|
+
/* Above everything interactive — not in layout's scale, only these consumers need them */
|
|
14
|
+
--z-cursor: 600;
|
|
15
|
+
--z-debug: 700;
|
|
16
|
+
}
|