kmcom-nuxt-layers 6.3.0 → 6.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/docs/MIGRATION-v6.4.0.md +215 -0
- package/docs/MIGRATION-v6.5.0.md +87 -0
- package/docs/MIGRATION.md +3 -1
- package/docs/USAGE.md +35 -10
- package/docs/adr/0003-modular-step-system-replaces-fluid-clamps.md +16 -0
- package/docs/component-levels.md +1 -0
- package/docs/layout-shift-causes.md +18 -0
- package/layers/content/app/components/content/Figure.vue +1 -1
- package/layers/content/app/composables/useContentSection.ts +1 -1
- package/layers/core/app/app.config.ts +9 -5
- package/layers/core/app/app.vue +2 -1
- package/layers/core/app/components/LoadingScreen.vue +35 -21
- package/layers/core/app/composables/useCache.ts +2 -2
- package/layers/core/app/composables/useErrorLog.ts +5 -5
- package/layers/core/app/composables/useLoading.ts +19 -10
- package/layers/core/app/plugins/loading.client.ts +11 -3
- package/layers/core/app/types/loading.ts +10 -3
- package/layers/core/app/utils/loading.ts +7 -16
- package/layers/core/nuxt.config.ts +14 -14
- package/layers/core/server/utils/app-config.ts +17 -0
- package/layers/data/auth/app/middleware/auth.ts +1 -1
- package/layers/data/forms/app/components/Form/Contact.vue +1 -1
- package/layers/data/forms/server/api/contact.post.ts +16 -8
- package/layers/data/forms/server/api/forms/status.get.ts +3 -0
- package/layers/data/forms/{app → shared}/utils/contact-schema.ts +2 -1
- package/layers/data/metadata/server/api/metadata/search.get.ts +27 -7
- package/layers/data/metadata/server/api/metadata/status.get.ts +4 -1
- package/layers/data/metadata/server/api/metadata/sync.post.ts +13 -3
- package/layers/data/metadata/server/utils/metadata/resolve-record.ts +3 -3
- package/layers/delivery/feeds/nuxt.config.ts +6 -1
- package/layers/delivery/feeds/package.json +2 -2
- package/layers/delivery/feeds/server/routes/feed/demo.get.ts +2 -2
- package/layers/delivery/feeds/server/utils/content-adapter.ts +1 -1
- package/layers/delivery/feeds/server/utils/feed-service.ts +2 -2
- package/layers/design-system/theming/app/plugins/theme.client.ts +7 -0
- package/layers/design-system/theming/nuxt.config.ts +0 -2
- package/layers/design-system/typography/app/assets/css/typography.css +41 -5
- package/layers/design-system/visual/app/components/HoverDimGroup.vue +11 -3
- package/layers/motion/scroll/app/plugins/locomotive-scroll.client.ts +3 -2
- package/layers/render/canvas/nuxt.config.ts +0 -4
- package/layers/render/shader/nuxt.config.ts +0 -4
- package/layers/structure/navigation/app/app.config.ts +37 -14
- package/layers/structure/navigation/app/components/Mast/Nav.vue +14 -13
- package/layers/structure/navigation/app/components/Mast/NavModal.vue +83 -28
- package/layers/structure/navigation/app/components/Site/Logo.vue +35 -0
- package/layers/structure/navigation/app/components/Site/Title.vue +3 -3
- package/layers/structure/navigation/app/composables/mastNav.ts +38 -13
- package/layers/structure/navigation/app/types/nav.ts +34 -0
- package/layers/structure/routing/app/middleware/01.maintenance.global.ts +4 -1
- package/layers/structure/routing/app/middleware/02.governance.global.ts +1 -1
- package/layers/structure/routing/nuxt.config.ts +19 -12
- package/layers/structure/routing/server/api/feature-flags.get.ts +4 -2
- package/package.json +35 -5
- package/docs/FALLOW-COMPLEXITY-DUPLICATION-AUDIT.md +0 -64
- package/docs/IMPROVE-AUDIT-README.md +0 -29
- package/docs/IMPROVE-AUDIT-RESULTS.md +0 -51
- package/docs/IMPROVE-DEEP-AUDIT-RESULTS.md +0 -81
- package/docs/fallow-refactor/apps-debug.md +0 -27
- package/docs/fallow-refactor/apps-playground.md +0 -46
- package/docs/fallow-refactor/apps-visual-identity.md +0 -41
- package/docs/fallow-refactor/layers-animations.md +0 -34
- package/docs/fallow-refactor/layers-canvas.md +0 -32
- package/docs/fallow-refactor/layers-content.md +0 -33
- package/docs/fallow-refactor/layers-core.md +0 -39
- package/docs/fallow-refactor/layers-feeds.md +0 -39
- package/docs/fallow-refactor/layers-forms.md +0 -30
- package/docs/fallow-refactor/layers-layout.md +0 -42
- package/docs/fallow-refactor/layers-mailer.md +0 -32
- package/docs/fallow-refactor/layers-motion.md +0 -27
- package/docs/fallow-refactor/layers-navigation.md +0 -31
- package/docs/fallow-refactor/layers-page-transitions.md +0 -30
- package/docs/fallow-refactor/layers-routing.md +0 -33
- package/docs/fallow-refactor/layers-scripts.md +0 -35
- package/docs/fallow-refactor/layers-scroll.md +0 -38
- package/docs/fallow-refactor/layers-seo.md +0 -32
- package/docs/fallow-refactor/layers-shader.md +0 -53
- package/docs/fallow-refactor/layers-theme.md +0 -33
- package/docs/fallow-refactor/layers-transitions.md +0 -27
- package/docs/fallow-refactor/layers-typography.md +0 -29
- package/docs/fallow-refactor/layers-visual.md +0 -34
- package/docs/superpowers/plans/2026-09-03-layout-layer-fluid-rebuild.md +0 -1850
- package/docs/superpowers/plans/2026-09-03-vue-element-system.md +0 -1019
- package/docs/superpowers/plans/2026-09-04-grid-layer.md +0 -977
- package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-0-1-design.md +0 -533
- package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-2-design.md +0 -154
- package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-3-design.md +0 -109
- package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-4-design.md +0 -124
- package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-5-design.md +0 -121
- package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-6-design.md +0 -77
- package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-7-design.md +0 -73
- package/docs/superpowers/specs/2026-08-31-swiss-grid-layer-design.md +0 -190
- package/docs/superpowers/specs/2026-09-03-layout-layer-fluid-rebuild-design.md +0 -312
- package/docs/superpowers/specs/2026-09-03-vue-element-system-design.md +0 -342
- package/docs/superpowers/specs/2026-09-04-grid-layer-design.md +0 -236
- package/layers/content/app/utils/galleryImageMeta.test.ts +0 -30
- package/layers/core/app/components/Element.test.ts +0 -90
- package/layers/core/app/components/ElementTw.test.ts +0 -64
- package/layers/core/app/composables/useLayoutAttrs.test.ts +0 -77
- package/layers/core/app/utils/browserInfo.test.ts +0 -81
- package/layers/core/app/utils/featureClasses.test.ts +0 -101
- package/layers/core/app/utils/loading.test.ts +0 -48
- package/layers/core/app/utils/resolveElementStyle.test.ts +0 -99
- package/layers/core/app/utils/scrollGuardClamp.test.ts +0 -36
- package/layers/core/app/utils/styleToTailwindClasses.test.ts +0 -31
- package/layers/data/forms/app/utils/contact-schema.test.ts +0 -75
- package/layers/data/forms/app/utils/fieldProps.test.ts +0 -141
- package/layers/data/metadata/providers/comicvine/server/utils/metadata-comicvine/normalise.test.ts +0 -201
- package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/client.test.ts +0 -76
- package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/normalise.test.ts +0 -144
- package/layers/data/metadata/providers/openlibrary/server/utils/metadata-openlibrary/client.test.ts +0 -25
- package/layers/data/metadata/providers/openlibrary/server/utils/metadata-openlibrary/normalise.test.ts +0 -144
- package/layers/data/metadata/providers/themoviedb/server/utils/metadata-tmdb/client.test.ts +0 -29
- package/layers/data/metadata/providers/themoviedb/server/utils/metadata-tmdb/normalise.test.ts +0 -162
- package/layers/data/metadata/server/utils/metadata/cache.test.ts +0 -39
- package/layers/data/metadata/server/utils/metadata/provider-fetch.test.ts +0 -27
- package/layers/data/metadata/server/utils/metadata/provider-registry.test.ts +0 -54
- package/layers/data/metadata/server/utils/metadata/resolve-record.test.ts +0 -87
- package/layers/data/metadata/server/utils/metadata/search.test.ts +0 -204
- package/layers/delivery/feeds/server/utils/content-adapter.test.ts +0 -67
- package/layers/delivery/feeds/server/utils/feed-author.test.ts +0 -71
- package/layers/delivery/feeds/server/utils/feed-config.test.ts +0 -184
- package/layers/delivery/feeds/server/utils/formats.test.ts +0 -71
- package/layers/delivery/seo/app/utils/seoConfig.test.ts +0 -58
- package/layers/design-system/theming/server/utils/accent-css.test.ts +0 -71
- package/layers/design-system/theming/server/utils/fouc-config.test.ts +0 -78
- package/layers/design-system/typography/app/components/Typography/Headline.test.ts +0 -36
- package/layers/design-system/typography/app/components/Typography/index.test.ts +0 -31
- package/layers/design-system/typography/app/composables/color.test.ts +0 -30
- package/layers/design-system/typography/app/composables/typography.test.ts +0 -53
- package/layers/design-system/visual/app/composables/tint.test.ts +0 -22
- package/layers/design-system/visual/app/utils/colorTokens.test.ts +0 -51
- package/layers/design-system/visual/app/utils/gradientStyle.test.ts +0 -75
- package/layers/design-system/visual/app/utils/responsiveSizes.test.ts +0 -71
- package/layers/motion/animations/app/components/Motion/Staggered.test.ts +0 -40
- package/layers/motion/scroll/app/composables/useReducedMotion.test.ts +0 -23
- package/layers/motion/scroll/app/utils/parallax.test.ts +0 -24
- package/layers/structure/grid/app/composables/useGridTracks.test.ts +0 -94
- package/layers/structure/grid/app/types/tracks.test.ts +0 -32
- package/layers/structure/grid/app/utils/placement.test.ts +0 -19
- package/layers/structure/layout/app/utils/gridPlacementStyle.test.ts +0 -269
- package/layers/structure/navigation/app/utils/site.test.ts +0 -39
- package/layers/structure/routing/app/utils/resolveRoute.test.ts +0 -47
|
@@ -12,10 +12,18 @@
|
|
|
12
12
|
setup(props, { slots, attrs }) {
|
|
13
13
|
const hoveredIndex = ref<number | null>(null)
|
|
14
14
|
|
|
15
|
+
// v-for over slots nests fragments (v-for → slot → element), so flatten all the way down
|
|
16
|
+
const flatten = (nodes: VNode[]): VNode[] =>
|
|
17
|
+
nodes.flatMap((node) =>
|
|
18
|
+
node.type === Fragment && Array.isArray(node.children)
|
|
19
|
+
? flatten(node.children as VNode[])
|
|
20
|
+
: [node]
|
|
21
|
+
)
|
|
22
|
+
|
|
15
23
|
return () => {
|
|
16
|
-
const children = (slots.default?.() ?? [])
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
const children = flatten(slots.default?.() ?? []).filter(
|
|
25
|
+
(node) => typeof node.type === 'string' || typeof node.type === 'object'
|
|
26
|
+
)
|
|
19
27
|
|
|
20
28
|
const styledChildren = children.map((vnode, index) =>
|
|
21
29
|
cloneVNode(vnode, {
|
|
@@ -61,10 +61,11 @@ export default defineNuxtPlugin({
|
|
|
61
61
|
// Without a mounted screen the state stays 'hidden' and GSAP is never touched.
|
|
62
62
|
watch(
|
|
63
63
|
screen,
|
|
64
|
-
(state) => {
|
|
64
|
+
(state, previous) => {
|
|
65
65
|
gsap.globalTimeline.paused(state !== 'hidden')
|
|
66
66
|
|
|
67
|
-
if
|
|
67
|
+
// Start as the screen leaves, or once it's gone if it skipped 'leaving' (finishNow)
|
|
68
|
+
if (state === 'visible' || previous === 'leaving') return
|
|
68
69
|
instance.value?.start()
|
|
69
70
|
// Remeasure once the screen's scroll lock is off the page
|
|
70
71
|
requestAnimationFrame(() => {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { SiteConfig } from '#layers/core/app/types/site'
|
|
2
2
|
|
|
3
|
+
import type { MastNavLink, MastNavTrigger, MastNavUi, SiteLogoConfig } from './types/nav'
|
|
4
|
+
|
|
3
5
|
export default defineAppConfig({
|
|
4
6
|
site: {
|
|
5
7
|
title: '',
|
|
@@ -7,29 +9,50 @@ export default defineAppConfig({
|
|
|
7
9
|
description: '',
|
|
8
10
|
} satisfies SiteConfig,
|
|
9
11
|
mastNav: {
|
|
10
|
-
links: [] as
|
|
11
|
-
id: string
|
|
12
|
-
label: string
|
|
13
|
-
to:
|
|
14
|
-
| string
|
|
15
|
-
| { name: string; params?: Record<string, unknown>; query?: Record<string, unknown> }
|
|
16
|
-
}>,
|
|
12
|
+
links: [] as MastNavLink[],
|
|
17
13
|
scrollBehaviour: 'router' as 'smooth-scroll' | 'router',
|
|
14
|
+
// UButton props for <MastNav>; attrs on the component win
|
|
15
|
+
trigger: {
|
|
16
|
+
icon: 'lucide:menu',
|
|
17
|
+
color: 'neutral',
|
|
18
|
+
variant: 'ghost',
|
|
19
|
+
size: 'xl',
|
|
20
|
+
} as MastNavTrigger,
|
|
21
|
+
ui: {
|
|
22
|
+
content: 'bg-default text-default',
|
|
23
|
+
close: 'absolute top-4 right-4',
|
|
24
|
+
nav: 'flex flex-col items-center gap-2',
|
|
25
|
+
// Weight keys off aria-current so active and inactive never carry two font-* classes
|
|
26
|
+
link: 'cursor-pointer border-0 bg-transparent px-4 py-3 text-3xl tracking-widest uppercase transition-all font-light aria-[current=page]:font-normal',
|
|
27
|
+
linkActive: '',
|
|
28
|
+
} satisfies MastNavUi,
|
|
18
29
|
},
|
|
30
|
+
siteLogo: {
|
|
31
|
+
stacked: true,
|
|
32
|
+
ui: { root: 'font-display leading-none tracking-tight', word: '' },
|
|
33
|
+
sizes: {
|
|
34
|
+
sm: 'text-step-1',
|
|
35
|
+
md: 'text-step-4',
|
|
36
|
+
lg: 'text-step-6',
|
|
37
|
+
display: 'text-step-display',
|
|
38
|
+
},
|
|
39
|
+
} satisfies SiteLogoConfig,
|
|
19
40
|
})
|
|
20
41
|
|
|
21
42
|
declare module '@nuxt/schema' {
|
|
22
43
|
interface AppConfigInput {
|
|
23
44
|
// `site` is already typed by core's own AppConfigInput augmentation — no need to redeclare it.
|
|
24
45
|
mastNav?: {
|
|
25
|
-
links?:
|
|
26
|
-
id: string
|
|
27
|
-
label: string
|
|
28
|
-
to:
|
|
29
|
-
| string
|
|
30
|
-
| { name: string; params?: Record<string, unknown>; query?: Record<string, unknown> }
|
|
31
|
-
}>
|
|
46
|
+
links?: MastNavLink[]
|
|
32
47
|
scrollBehaviour?: 'smooth-scroll' | 'router'
|
|
48
|
+
trigger?: MastNavTrigger
|
|
49
|
+
ui?: Partial<MastNavUi>
|
|
50
|
+
}
|
|
51
|
+
siteLogo?: {
|
|
52
|
+
stacked?: boolean
|
|
53
|
+
ui?: Partial<SiteLogoConfig['ui']>
|
|
54
|
+
/** Merged per key, so a site can retune one size without restating the others. */
|
|
55
|
+
sizes?: Partial<SiteLogoConfig['sizes']>
|
|
33
56
|
}
|
|
34
57
|
}
|
|
35
58
|
}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import type { ButtonProps } from '@nuxt/ui'
|
|
3
|
+
|
|
4
|
+
// A UButton styled by `mastNav.trigger` in app.config; attrs (class, label, icon…) win.
|
|
5
|
+
// Positioning is the parent's call, e.g. `<MastNav class="fixed top-0 right-0" />`.
|
|
6
|
+
defineOptions({ inheritAttrs: false })
|
|
7
|
+
|
|
4
8
|
const { open } = useMastNav()
|
|
5
|
-
|
|
9
|
+
// app.config types the trigger loosely (see MastNavTrigger); UButton validates the values
|
|
10
|
+
const trigger = ((useAppConfig().mastNav as { trigger?: unknown } | undefined)?.trigger ??
|
|
11
|
+
{}) as ButtonProps
|
|
6
12
|
</script>
|
|
7
13
|
|
|
8
14
|
<template>
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
>
|
|
15
|
-
<slot name="icon">
|
|
16
|
-
<UIcon name="lucide:menu" class="size-7" />
|
|
17
|
-
</slot>
|
|
18
|
-
</button>
|
|
15
|
+
<UButton aria-label="Open navigation menu" v-bind="{ ...trigger, ...$attrs }" @click="open">
|
|
16
|
+
<template v-if="$slots.default" #default>
|
|
17
|
+
<slot />
|
|
18
|
+
</template>
|
|
19
|
+
</UButton>
|
|
19
20
|
</template>
|
|
@@ -1,19 +1,61 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { MastNavLink } from '../../types/nav'
|
|
2
|
+
import type { MastNavLink, MastNavUi } from '../../types/nav'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// Render once, in the layout. Behaviour lives here (dialog, scroll lock via useMastNav,
|
|
5
|
+
// smooth-scroll vs router, active link); the look comes from `mastNav.ui`, `ui` and slots.
|
|
6
|
+
const {
|
|
7
|
+
open = undefined,
|
|
8
|
+
links = undefined,
|
|
9
|
+
ui = undefined,
|
|
10
|
+
} = defineProps<{
|
|
11
|
+
/** Manual control; omit to follow useMastNav().isOpen. */
|
|
12
|
+
open?: boolean
|
|
13
|
+
/** Replaces `mastNav.links`, e.g. filtered by the site's own page flags. */
|
|
14
|
+
links?: MastNavLink[]
|
|
15
|
+
/** Per-key override of `mastNav.ui` (strings replace, no class merging). */
|
|
16
|
+
ui?: Partial<MastNavUi>
|
|
17
|
+
}>()
|
|
5
18
|
const emit = defineEmits<{
|
|
6
19
|
'update:open': [value: boolean]
|
|
7
20
|
close: []
|
|
8
21
|
}>()
|
|
9
22
|
|
|
10
|
-
const
|
|
23
|
+
const config = useAppConfig().mastNav as {
|
|
11
24
|
links: MastNavLink[]
|
|
12
|
-
scrollBehaviour:
|
|
25
|
+
scrollBehaviour: 'smooth-scroll' | 'router'
|
|
26
|
+
ui: MastNavUi
|
|
13
27
|
}
|
|
28
|
+
const classes = computed(() => ({ ...config.ui, ...ui }))
|
|
29
|
+
const visibleLinks = computed(() => (links ?? config.links).filter((link) => !link.hidden))
|
|
30
|
+
|
|
14
31
|
const activeSection = useState<string>('activeSection', () => '')
|
|
15
32
|
const route = useRoute()
|
|
16
|
-
const
|
|
33
|
+
const router = useRouter()
|
|
34
|
+
const { isOpen, close: closeNav } = useMastNav()
|
|
35
|
+
// On the smooth-scroll index every link routes to index, so only the section counts there
|
|
36
|
+
const smoothOnIndex = computed(
|
|
37
|
+
() => config.scrollBehaviour === 'smooth-scroll' && route.name === 'index'
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
function matchesRoute(link: MastNavLink) {
|
|
41
|
+
try {
|
|
42
|
+
return router.resolve(link.to).path === route.path
|
|
43
|
+
} catch {
|
|
44
|
+
return false // a named route that doesn't exist
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const activeIds = computed(
|
|
49
|
+
() =>
|
|
50
|
+
new Set(
|
|
51
|
+
visibleLinks.value
|
|
52
|
+
.filter(
|
|
53
|
+
(link) =>
|
|
54
|
+
activeSection.value === link.id || (!smoothOnIndex.value && matchesRoute(link))
|
|
55
|
+
)
|
|
56
|
+
.map((link) => link.id)
|
|
57
|
+
)
|
|
58
|
+
)
|
|
17
59
|
|
|
18
60
|
function dismiss() {
|
|
19
61
|
emit('update:open', false)
|
|
@@ -21,8 +63,8 @@
|
|
|
21
63
|
closeNav()
|
|
22
64
|
}
|
|
23
65
|
|
|
24
|
-
function
|
|
25
|
-
if (
|
|
66
|
+
function navigate(link: MastNavLink) {
|
|
67
|
+
if (smoothOnIndex.value) {
|
|
26
68
|
try {
|
|
27
69
|
useSmoothScroll().scrollTo(`#${link.id}`)
|
|
28
70
|
} catch {}
|
|
@@ -37,35 +79,48 @@
|
|
|
37
79
|
<!-- UModal supplies the dialog role, focus trap, Escape and portal; the title stays
|
|
38
80
|
visually hidden because the content slot replaces the header. -->
|
|
39
81
|
<UModal
|
|
40
|
-
:open
|
|
82
|
+
:open="open ?? isOpen"
|
|
41
83
|
fullscreen
|
|
42
84
|
title="Site navigation"
|
|
43
|
-
:ui="{ overlay: 'z-(--z-modal)', content:
|
|
85
|
+
:ui="{ overlay: 'z-(--z-modal)', content: `${classes.content} z-(--z-modal)` }"
|
|
44
86
|
@update:open="(value) => !value && dismiss()"
|
|
45
87
|
>
|
|
46
88
|
<template #content>
|
|
47
89
|
<div class="relative flex h-full flex-col items-center justify-center">
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
90
|
+
<slot name="close" :close="dismiss">
|
|
91
|
+
<UButton
|
|
92
|
+
icon="lucide:x"
|
|
93
|
+
color="neutral"
|
|
94
|
+
variant="ghost"
|
|
95
|
+
size="xl"
|
|
96
|
+
aria-label="Close navigation"
|
|
97
|
+
:class="classes.close"
|
|
98
|
+
@click="dismiss"
|
|
99
|
+
/>
|
|
100
|
+
</slot>
|
|
57
101
|
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
102
|
+
<slot name="header" />
|
|
103
|
+
|
|
104
|
+
<HoverDimGroup tag="nav" :class="classes.nav">
|
|
105
|
+
<template v-for="link in visibleLinks" :key="link.id">
|
|
106
|
+
<slot
|
|
107
|
+
name="link"
|
|
108
|
+
:link
|
|
109
|
+
:active="activeIds.has(link.id)"
|
|
110
|
+
:navigate="() => navigate(link)"
|
|
111
|
+
>
|
|
112
|
+
<button
|
|
113
|
+
:class="[classes.link, activeIds.has(link.id) && classes.linkActive]"
|
|
114
|
+
:aria-current="activeIds.has(link.id) ? 'page' : undefined"
|
|
115
|
+
@click="() => navigate(link)"
|
|
116
|
+
>
|
|
117
|
+
{{ link.label }}
|
|
118
|
+
</button>
|
|
119
|
+
</slot>
|
|
120
|
+
</template>
|
|
68
121
|
</HoverDimGroup>
|
|
122
|
+
|
|
123
|
+
<slot name="footer" />
|
|
69
124
|
</div>
|
|
70
125
|
</template>
|
|
71
126
|
</UModal>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { SiteLogoConfig, SiteLogoSize } from '../../types/nav'
|
|
3
|
+
|
|
4
|
+
// The site wordmark from `site.title`, sized on the step scale. Look comes from `siteLogo` in
|
|
5
|
+
// app.config; `ui` overrides per instance. Replaces the deprecated SiteTitle/SiteSubtitle.
|
|
6
|
+
const {
|
|
7
|
+
as = 'span',
|
|
8
|
+
size = 'sm',
|
|
9
|
+
stacked = undefined,
|
|
10
|
+
ui = undefined,
|
|
11
|
+
} = defineProps<{
|
|
12
|
+
as?: string
|
|
13
|
+
size?: SiteLogoSize
|
|
14
|
+
/** One word per line. Defaults to `siteLogo.stacked`. */
|
|
15
|
+
stacked?: boolean
|
|
16
|
+
ui?: Partial<SiteLogoConfig['ui']>
|
|
17
|
+
}>()
|
|
18
|
+
|
|
19
|
+
const config = useAppConfig().siteLogo as SiteLogoConfig
|
|
20
|
+
const site = useSite()
|
|
21
|
+
const isStacked = computed(() => stacked ?? config.stacked)
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<component :is="as" :class="[ui?.root ?? config.ui.root, config.sizes[size]]">
|
|
26
|
+
<slot :words="site.titleWords" :title="site.title">
|
|
27
|
+
<span
|
|
28
|
+
v-for="(word, i) in site.titleWords"
|
|
29
|
+
:key="i"
|
|
30
|
+
:class="[isStacked && 'block', ui?.word ?? config.ui.word]"
|
|
31
|
+
>{{ word }}{{ !isStacked && i < site.titleWords.length - 1 ? ' ' : '' }}</span
|
|
32
|
+
>
|
|
33
|
+
</slot>
|
|
34
|
+
</component>
|
|
35
|
+
</template>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* @deprecated
|
|
4
|
-
*
|
|
3
|
+
* @deprecated Use `<SiteLogo :stacked="false">` (or `useSite().titleWords`). Remove in the
|
|
4
|
+
* next major.
|
|
5
5
|
*/
|
|
6
6
|
if (import.meta.dev)
|
|
7
7
|
// eslint-disable-next-line no-console -- one-time deprecation notice, dev only
|
|
8
|
-
console.warn('[navigation] <SiteTitle> is deprecated —
|
|
8
|
+
console.warn('[navigation] <SiteTitle> is deprecated — use <SiteLogo>.')
|
|
9
9
|
|
|
10
10
|
const site = useSite()
|
|
11
11
|
</script>
|
|
@@ -1,15 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Shared open state for the site nav. Plain `useState`, not an overlay: layouts and triggers read
|
|
3
|
+
* `isOpen`, and `<MastNavModal />` (rendered once by the layout) binds to it. Any number of
|
|
4
|
+
* `<MastNav>` triggers share the one modal.
|
|
5
|
+
*
|
|
6
|
+
* Scroll lock: UModal (Reka) locks body scroll and pads for the scrollbar, so this only stops
|
|
7
|
+
* Lenis — and only after Reka has measured. Hiding overflow first (lockScrolling) left Reka
|
|
8
|
+
* measuring a 0px scrollbar, and the page widened by the scrollbar on open.
|
|
9
|
+
*/
|
|
10
|
+
export function useMastNav() {
|
|
11
|
+
const isOpen = useState('mast-nav:open', () => false)
|
|
3
12
|
|
|
4
|
-
|
|
5
|
-
onOpen: () => {
|
|
13
|
+
function smoothScroll() {
|
|
6
14
|
try {
|
|
7
|
-
useSmoothScroll().
|
|
8
|
-
} catch {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
return useSmoothScroll().locomotiveScroll
|
|
16
|
+
} catch {
|
|
17
|
+
return undefined
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function open() {
|
|
22
|
+
const scroll = smoothScroll()
|
|
23
|
+
isOpen.value = true
|
|
24
|
+
if (import.meta.client) nextTick(() => scroll?.value?.stop())
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function close() {
|
|
28
|
+
const scroll = smoothScroll()
|
|
29
|
+
isOpen.value = false
|
|
30
|
+
if (!import.meta.client) return
|
|
31
|
+
scroll?.value?.start()
|
|
32
|
+
// Lenis/Locomotive measure the page while stopped; resize after restart so they resync
|
|
33
|
+
scroll?.value?.lenisInstance?.resize()
|
|
34
|
+
requestAnimationFrame(() => scroll?.value?.resize())
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const toggle = () => (isOpen.value ? close() : open())
|
|
38
|
+
|
|
39
|
+
return { isOpen: readonly(isOpen), open, close, toggle }
|
|
40
|
+
}
|
|
@@ -8,4 +8,38 @@ export type MastNavLink = {
|
|
|
8
8
|
params?: Record<string, string | number>
|
|
9
9
|
query?: Record<string, string | number>
|
|
10
10
|
}
|
|
11
|
+
/** Skip the link without passing a custom `links` prop. */
|
|
12
|
+
hidden?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* UButton props for the MastNav trigger. Plain strings: Nuxt UI's ButtonProps is derived from the
|
|
17
|
+
* resolved app config, so using it inside app.config.ts would be a type cycle.
|
|
18
|
+
*/
|
|
19
|
+
export type MastNavTrigger = {
|
|
20
|
+
label?: string
|
|
21
|
+
icon?: string
|
|
22
|
+
trailingIcon?: string
|
|
23
|
+
color?: string
|
|
24
|
+
variant?: string
|
|
25
|
+
size?: string
|
|
26
|
+
square?: boolean
|
|
27
|
+
class?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Class strings for MastNavModal's parts. Merged per key over app.config; strings replace. */
|
|
31
|
+
export type MastNavUi = {
|
|
32
|
+
content: string
|
|
33
|
+
close: string
|
|
34
|
+
nav: string
|
|
35
|
+
link: string
|
|
36
|
+
linkActive: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type SiteLogoSize = 'sm' | 'md' | 'lg' | 'display'
|
|
40
|
+
|
|
41
|
+
export type SiteLogoConfig = {
|
|
42
|
+
stacked: boolean
|
|
43
|
+
ui: { root: string; word: string }
|
|
44
|
+
sizes: Record<SiteLogoSize, string>
|
|
11
45
|
}
|
|
@@ -2,5 +2,8 @@ export default defineNuxtRouteMiddleware((to) => {
|
|
|
2
2
|
const { isEnabled, allowRoutes } = useMaintenance()
|
|
3
3
|
if (!isEnabled.value) return
|
|
4
4
|
if (allowRoutes.value.some((r) => to.path.startsWith(r))) return
|
|
5
|
-
|
|
5
|
+
const target = allowRoutes.value[0]
|
|
6
|
+
// No allowed route to send visitors to: show the maintenance state as an error instead
|
|
7
|
+
if (!target) return abortNavigation(createError({ status: 503 }))
|
|
8
|
+
return navigateTo(target)
|
|
6
9
|
})
|
|
@@ -11,6 +11,6 @@ export default defineNuxtRouteMiddleware((to) => {
|
|
|
11
11
|
|
|
12
12
|
if (config.debug) console.log('[routing] resolution', resolution)
|
|
13
13
|
|
|
14
|
-
if (resolution.outcome === 'deny')
|
|
14
|
+
if (resolution.outcome === 'deny') return abortNavigation(createError({ status: 404 }))
|
|
15
15
|
if (resolution.outcome === 'redirect') return navigateTo(resolution.to)
|
|
16
16
|
})
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
|
|
1
3
|
type RoutePage = {
|
|
2
4
|
file?: string
|
|
3
5
|
meta?: Record<string, unknown>
|
|
@@ -21,19 +23,24 @@ export default defineNuxtConfig({
|
|
|
21
23
|
strict: true,
|
|
22
24
|
},
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
modules: [
|
|
27
|
+
// Tag pages that don't live in the app's own pages dir. Compared against the app's pages
|
|
28
|
+
// dir, not process.cwd(): an npm consumer's layer pages sit under <app>/node_modules, which
|
|
29
|
+
// is inside cwd, so a cwd prefix check never tagged anything.
|
|
30
|
+
(_options, nuxt) => {
|
|
31
|
+
const appPagesDir = `${resolve(nuxt.options.srcDir, nuxt.options.dir.pages)}/`
|
|
32
|
+
nuxt.hook('pages:extend', (pages) => {
|
|
33
|
+
const tag = (list: RoutePage[]) => {
|
|
34
|
+
for (const page of list) {
|
|
35
|
+
if (page.file && !page.file.startsWith(appPagesDir)) {
|
|
36
|
+
page.meta ??= {}
|
|
37
|
+
page.meta.__fromLayer = true
|
|
38
|
+
}
|
|
39
|
+
if (page.children) tag(page.children)
|
|
32
40
|
}
|
|
33
|
-
if (page.children) tag(page.children)
|
|
34
41
|
}
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
tag(pages as RoutePage[])
|
|
43
|
+
})
|
|
37
44
|
},
|
|
38
|
-
|
|
45
|
+
],
|
|
39
46
|
})
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { readServerAppConfig } from '#layers/core/server/utils/app-config'
|
|
1
2
|
import { defineEventHandler } from 'h3'
|
|
2
|
-
import { useAppConfig } from 'nitropack/runtime'
|
|
3
3
|
import type { FeatureValue } from '../../app/types/routing'
|
|
4
4
|
|
|
5
5
|
export default defineEventHandler(() => {
|
|
6
|
-
const config =
|
|
6
|
+
const config = readServerAppConfig() as {
|
|
7
|
+
routingLayer?: { features?: Record<string, FeatureValue> }
|
|
8
|
+
}
|
|
7
9
|
return config.routingLayer?.features ?? {}
|
|
8
10
|
})
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kmcom-nuxt-layers",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "6.
|
|
4
|
+
"version": "6.5.0",
|
|
5
5
|
"description": "Composable Nuxt 4 layers for building scalable Vue applications",
|
|
6
6
|
"exports": {
|
|
7
7
|
"./layers/core": "./layers/core/nuxt.config.ts",
|
|
@@ -57,7 +57,12 @@
|
|
|
57
57
|
"layers/*/*/providers/*/tsconfig.json",
|
|
58
58
|
"layers/*/*/providers/*/server/**",
|
|
59
59
|
"types/**",
|
|
60
|
-
"docs/"
|
|
60
|
+
"docs/",
|
|
61
|
+
"!**/*.test.ts",
|
|
62
|
+
"!**/*.spec.ts",
|
|
63
|
+
"!docs/superpowers/**",
|
|
64
|
+
"!docs/fallow-refactor/**",
|
|
65
|
+
"!docs/*AUDIT*"
|
|
61
66
|
],
|
|
62
67
|
"repository": {
|
|
63
68
|
"type": "git",
|
|
@@ -72,6 +77,7 @@
|
|
|
72
77
|
"packages/*"
|
|
73
78
|
],
|
|
74
79
|
"peerDependencies": {
|
|
80
|
+
"@neondatabase/serverless": "^0.10.0",
|
|
75
81
|
"@nuxt/content": "^3.14.0",
|
|
76
82
|
"@nuxt/eslint": "^1.15.2",
|
|
77
83
|
"@nuxt/image": "^2.0.0",
|
|
@@ -87,10 +93,15 @@
|
|
|
87
93
|
"@vueuse/nuxt": "^14.3.0 || ^15.0.0",
|
|
88
94
|
"baseline-status": "^1.1.1",
|
|
89
95
|
"better-sqlite3": "^12.10.0 || ^13.0.0",
|
|
96
|
+
"drizzle-orm": "^0.45.4",
|
|
97
|
+
"feed": "^5.2.1",
|
|
90
98
|
"gsap": "^3.15.0",
|
|
99
|
+
"hookable": "^5.5.3 || ^6.0.0",
|
|
91
100
|
"locomotive-scroll": "^5.0.1",
|
|
92
101
|
"nuxt": "^4.6.1",
|
|
102
|
+
"nuxt-auth-utils": "^0.5.0",
|
|
93
103
|
"pinia": "^3.0.4 || ^4.1.0",
|
|
104
|
+
"resend": "^4.0.0",
|
|
94
105
|
"tailwindcss": "^4.3.0",
|
|
95
106
|
"three": ">=0.184.0",
|
|
96
107
|
"v-gsap-nuxt": ">=1.5.12",
|
|
@@ -150,6 +161,24 @@
|
|
|
150
161
|
},
|
|
151
162
|
"baseline-status": {
|
|
152
163
|
"optional": true
|
|
164
|
+
},
|
|
165
|
+
"@neondatabase/serverless": {
|
|
166
|
+
"optional": true
|
|
167
|
+
},
|
|
168
|
+
"drizzle-orm": {
|
|
169
|
+
"optional": true
|
|
170
|
+
},
|
|
171
|
+
"feed": {
|
|
172
|
+
"optional": true
|
|
173
|
+
},
|
|
174
|
+
"hookable": {
|
|
175
|
+
"optional": true
|
|
176
|
+
},
|
|
177
|
+
"nuxt-auth-utils": {
|
|
178
|
+
"optional": true
|
|
179
|
+
},
|
|
180
|
+
"resend": {
|
|
181
|
+
"optional": true
|
|
153
182
|
}
|
|
154
183
|
},
|
|
155
184
|
"devDependencies": {
|
|
@@ -211,7 +240,9 @@
|
|
|
211
240
|
"vue-tsc": "3.3.13",
|
|
212
241
|
"zod": "^4.3.6",
|
|
213
242
|
"zod-to-json-schema": "^3.25.2",
|
|
214
|
-
"vite-plus": "1.3.0"
|
|
243
|
+
"vite-plus": "1.3.0",
|
|
244
|
+
"node-gyp": "^12.3.0",
|
|
245
|
+
"skills": "^1.5.23"
|
|
215
246
|
},
|
|
216
247
|
"browserslist": [
|
|
217
248
|
"last 2 Chrome major versions",
|
|
@@ -221,8 +252,7 @@
|
|
|
221
252
|
"extends browserslist-config-baseline"
|
|
222
253
|
],
|
|
223
254
|
"dependencies": {
|
|
224
|
-
"
|
|
225
|
-
"skills": "^1.5.23"
|
|
255
|
+
"@iconify/vue": "^5.0.1"
|
|
226
256
|
},
|
|
227
257
|
"engines": {
|
|
228
258
|
"node": ">=24 <25"
|