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.
Files changed (142) hide show
  1. package/docs/MIGRATION-v6.4.0.md +215 -0
  2. package/docs/MIGRATION-v6.5.0.md +87 -0
  3. package/docs/MIGRATION.md +3 -1
  4. package/docs/USAGE.md +35 -10
  5. package/docs/adr/0003-modular-step-system-replaces-fluid-clamps.md +16 -0
  6. package/docs/component-levels.md +1 -0
  7. package/docs/layout-shift-causes.md +18 -0
  8. package/layers/content/app/components/content/Figure.vue +1 -1
  9. package/layers/content/app/composables/useContentSection.ts +1 -1
  10. package/layers/core/app/app.config.ts +9 -5
  11. package/layers/core/app/app.vue +2 -1
  12. package/layers/core/app/components/LoadingScreen.vue +35 -21
  13. package/layers/core/app/composables/useCache.ts +2 -2
  14. package/layers/core/app/composables/useErrorLog.ts +5 -5
  15. package/layers/core/app/composables/useLoading.ts +19 -10
  16. package/layers/core/app/plugins/loading.client.ts +11 -3
  17. package/layers/core/app/types/loading.ts +10 -3
  18. package/layers/core/app/utils/loading.ts +7 -16
  19. package/layers/core/nuxt.config.ts +14 -14
  20. package/layers/core/server/utils/app-config.ts +17 -0
  21. package/layers/data/auth/app/middleware/auth.ts +1 -1
  22. package/layers/data/forms/app/components/Form/Contact.vue +1 -1
  23. package/layers/data/forms/server/api/contact.post.ts +16 -8
  24. package/layers/data/forms/server/api/forms/status.get.ts +3 -0
  25. package/layers/data/forms/{app → shared}/utils/contact-schema.ts +2 -1
  26. package/layers/data/metadata/server/api/metadata/search.get.ts +27 -7
  27. package/layers/data/metadata/server/api/metadata/status.get.ts +4 -1
  28. package/layers/data/metadata/server/api/metadata/sync.post.ts +13 -3
  29. package/layers/data/metadata/server/utils/metadata/resolve-record.ts +3 -3
  30. package/layers/delivery/feeds/nuxt.config.ts +6 -1
  31. package/layers/delivery/feeds/package.json +2 -2
  32. package/layers/delivery/feeds/server/routes/feed/demo.get.ts +2 -2
  33. package/layers/delivery/feeds/server/utils/content-adapter.ts +1 -1
  34. package/layers/delivery/feeds/server/utils/feed-service.ts +2 -2
  35. package/layers/design-system/theming/app/plugins/theme.client.ts +7 -0
  36. package/layers/design-system/theming/nuxt.config.ts +0 -2
  37. package/layers/design-system/typography/app/assets/css/typography.css +41 -5
  38. package/layers/design-system/visual/app/components/HoverDimGroup.vue +11 -3
  39. package/layers/motion/scroll/app/plugins/locomotive-scroll.client.ts +3 -2
  40. package/layers/render/canvas/nuxt.config.ts +0 -4
  41. package/layers/render/shader/nuxt.config.ts +0 -4
  42. package/layers/structure/navigation/app/app.config.ts +37 -14
  43. package/layers/structure/navigation/app/components/Mast/Nav.vue +14 -13
  44. package/layers/structure/navigation/app/components/Mast/NavModal.vue +83 -28
  45. package/layers/structure/navigation/app/components/Site/Logo.vue +35 -0
  46. package/layers/structure/navigation/app/components/Site/Title.vue +3 -3
  47. package/layers/structure/navigation/app/composables/mastNav.ts +38 -13
  48. package/layers/structure/navigation/app/types/nav.ts +34 -0
  49. package/layers/structure/routing/app/middleware/01.maintenance.global.ts +4 -1
  50. package/layers/structure/routing/app/middleware/02.governance.global.ts +1 -1
  51. package/layers/structure/routing/nuxt.config.ts +19 -12
  52. package/layers/structure/routing/server/api/feature-flags.get.ts +4 -2
  53. package/package.json +35 -5
  54. package/docs/FALLOW-COMPLEXITY-DUPLICATION-AUDIT.md +0 -64
  55. package/docs/IMPROVE-AUDIT-README.md +0 -29
  56. package/docs/IMPROVE-AUDIT-RESULTS.md +0 -51
  57. package/docs/IMPROVE-DEEP-AUDIT-RESULTS.md +0 -81
  58. package/docs/fallow-refactor/apps-debug.md +0 -27
  59. package/docs/fallow-refactor/apps-playground.md +0 -46
  60. package/docs/fallow-refactor/apps-visual-identity.md +0 -41
  61. package/docs/fallow-refactor/layers-animations.md +0 -34
  62. package/docs/fallow-refactor/layers-canvas.md +0 -32
  63. package/docs/fallow-refactor/layers-content.md +0 -33
  64. package/docs/fallow-refactor/layers-core.md +0 -39
  65. package/docs/fallow-refactor/layers-feeds.md +0 -39
  66. package/docs/fallow-refactor/layers-forms.md +0 -30
  67. package/docs/fallow-refactor/layers-layout.md +0 -42
  68. package/docs/fallow-refactor/layers-mailer.md +0 -32
  69. package/docs/fallow-refactor/layers-motion.md +0 -27
  70. package/docs/fallow-refactor/layers-navigation.md +0 -31
  71. package/docs/fallow-refactor/layers-page-transitions.md +0 -30
  72. package/docs/fallow-refactor/layers-routing.md +0 -33
  73. package/docs/fallow-refactor/layers-scripts.md +0 -35
  74. package/docs/fallow-refactor/layers-scroll.md +0 -38
  75. package/docs/fallow-refactor/layers-seo.md +0 -32
  76. package/docs/fallow-refactor/layers-shader.md +0 -53
  77. package/docs/fallow-refactor/layers-theme.md +0 -33
  78. package/docs/fallow-refactor/layers-transitions.md +0 -27
  79. package/docs/fallow-refactor/layers-typography.md +0 -29
  80. package/docs/fallow-refactor/layers-visual.md +0 -34
  81. package/docs/superpowers/plans/2026-09-03-layout-layer-fluid-rebuild.md +0 -1850
  82. package/docs/superpowers/plans/2026-09-03-vue-element-system.md +0 -1019
  83. package/docs/superpowers/plans/2026-09-04-grid-layer.md +0 -977
  84. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-0-1-design.md +0 -533
  85. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-2-design.md +0 -154
  86. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-3-design.md +0 -109
  87. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-4-design.md +0 -124
  88. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-5-design.md +0 -121
  89. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-6-design.md +0 -77
  90. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-7-design.md +0 -73
  91. package/docs/superpowers/specs/2026-08-31-swiss-grid-layer-design.md +0 -190
  92. package/docs/superpowers/specs/2026-09-03-layout-layer-fluid-rebuild-design.md +0 -312
  93. package/docs/superpowers/specs/2026-09-03-vue-element-system-design.md +0 -342
  94. package/docs/superpowers/specs/2026-09-04-grid-layer-design.md +0 -236
  95. package/layers/content/app/utils/galleryImageMeta.test.ts +0 -30
  96. package/layers/core/app/components/Element.test.ts +0 -90
  97. package/layers/core/app/components/ElementTw.test.ts +0 -64
  98. package/layers/core/app/composables/useLayoutAttrs.test.ts +0 -77
  99. package/layers/core/app/utils/browserInfo.test.ts +0 -81
  100. package/layers/core/app/utils/featureClasses.test.ts +0 -101
  101. package/layers/core/app/utils/loading.test.ts +0 -48
  102. package/layers/core/app/utils/resolveElementStyle.test.ts +0 -99
  103. package/layers/core/app/utils/scrollGuardClamp.test.ts +0 -36
  104. package/layers/core/app/utils/styleToTailwindClasses.test.ts +0 -31
  105. package/layers/data/forms/app/utils/contact-schema.test.ts +0 -75
  106. package/layers/data/forms/app/utils/fieldProps.test.ts +0 -141
  107. package/layers/data/metadata/providers/comicvine/server/utils/metadata-comicvine/normalise.test.ts +0 -201
  108. package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/client.test.ts +0 -76
  109. package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/normalise.test.ts +0 -144
  110. package/layers/data/metadata/providers/openlibrary/server/utils/metadata-openlibrary/client.test.ts +0 -25
  111. package/layers/data/metadata/providers/openlibrary/server/utils/metadata-openlibrary/normalise.test.ts +0 -144
  112. package/layers/data/metadata/providers/themoviedb/server/utils/metadata-tmdb/client.test.ts +0 -29
  113. package/layers/data/metadata/providers/themoviedb/server/utils/metadata-tmdb/normalise.test.ts +0 -162
  114. package/layers/data/metadata/server/utils/metadata/cache.test.ts +0 -39
  115. package/layers/data/metadata/server/utils/metadata/provider-fetch.test.ts +0 -27
  116. package/layers/data/metadata/server/utils/metadata/provider-registry.test.ts +0 -54
  117. package/layers/data/metadata/server/utils/metadata/resolve-record.test.ts +0 -87
  118. package/layers/data/metadata/server/utils/metadata/search.test.ts +0 -204
  119. package/layers/delivery/feeds/server/utils/content-adapter.test.ts +0 -67
  120. package/layers/delivery/feeds/server/utils/feed-author.test.ts +0 -71
  121. package/layers/delivery/feeds/server/utils/feed-config.test.ts +0 -184
  122. package/layers/delivery/feeds/server/utils/formats.test.ts +0 -71
  123. package/layers/delivery/seo/app/utils/seoConfig.test.ts +0 -58
  124. package/layers/design-system/theming/server/utils/accent-css.test.ts +0 -71
  125. package/layers/design-system/theming/server/utils/fouc-config.test.ts +0 -78
  126. package/layers/design-system/typography/app/components/Typography/Headline.test.ts +0 -36
  127. package/layers/design-system/typography/app/components/Typography/index.test.ts +0 -31
  128. package/layers/design-system/typography/app/composables/color.test.ts +0 -30
  129. package/layers/design-system/typography/app/composables/typography.test.ts +0 -53
  130. package/layers/design-system/visual/app/composables/tint.test.ts +0 -22
  131. package/layers/design-system/visual/app/utils/colorTokens.test.ts +0 -51
  132. package/layers/design-system/visual/app/utils/gradientStyle.test.ts +0 -75
  133. package/layers/design-system/visual/app/utils/responsiveSizes.test.ts +0 -71
  134. package/layers/motion/animations/app/components/Motion/Staggered.test.ts +0 -40
  135. package/layers/motion/scroll/app/composables/useReducedMotion.test.ts +0 -23
  136. package/layers/motion/scroll/app/utils/parallax.test.ts +0 -24
  137. package/layers/structure/grid/app/composables/useGridTracks.test.ts +0 -94
  138. package/layers/structure/grid/app/types/tracks.test.ts +0 -32
  139. package/layers/structure/grid/app/utils/placement.test.ts +0 -19
  140. package/layers/structure/layout/app/utils/gridPlacementStyle.test.ts +0 -269
  141. package/layers/structure/navigation/app/utils/site.test.ts +0 -39
  142. 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
- .flatMap((node) => (node.type === Fragment ? (node.children as VNode[]) : [node]))
18
- .filter((node) => typeof node.type === 'string' || typeof node.type === 'object')
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 (state !== 'leaving') return
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(() => {
@@ -167,10 +167,6 @@ export default defineNuxtConfig({
167
167
  '#layers/canvas': import.meta.dirname,
168
168
  },
169
169
 
170
- imports: {
171
- dirs: [`${import.meta.dirname}/app/composables`],
172
- },
173
-
174
170
  vite: {
175
171
  plugins: [threeWebGPUSSRStub()],
176
172
  optimizeDeps: {
@@ -18,8 +18,4 @@ export default defineNuxtConfig({
18
18
  '#layers/shader': import.meta.dirname,
19
19
  '#layers/shader/types': `${import.meta.dirname}/app/types`,
20
20
  },
21
-
22
- imports: {
23
- dirs: [`${import.meta.dirname}/app/composables`],
24
- },
25
21
  })
@@ -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 Array<{
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?: Array<{
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
- // Positioning is the parent's call (e.g. `<MastNav class="fixed top-0 right-0" />`); the
3
- // z-index stays here so the trigger always sits on the dropdown layer.
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
- const z = useGridConfig().useZIndex('dropdown')
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
- <button
10
- aria-label="Open navigation menu"
11
- class="flex cursor-pointer items-center justify-center border-0 bg-transparent p-4"
12
- :style="{ zIndex: z }"
13
- @click="() => open()"
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
- const { open } = defineProps<{ open?: boolean }>()
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 { links, scrollBehaviour } = useAppConfig().mastNav as {
23
+ const config = useAppConfig().mastNav as {
11
24
  links: MastNavLink[]
12
- scrollBehaviour: string
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 { close: closeNav } = useMastNav()
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 handleNav(link: MastNavLink) {
25
- if (scrollBehaviour === 'smooth-scroll' && route.name === 'index') {
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: 'bg-default text-default z-(--z-modal)' }"
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
- <UButton
49
- icon="lucide:x"
50
- color="neutral"
51
- variant="ghost"
52
- size="xl"
53
- aria-label="Close navigation"
54
- class="absolute top-4 right-4"
55
- @click="dismiss"
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
- <HoverDimGroup tag="nav" class="flex flex-col items-center gap-2">
59
- <button
60
- v-for="link in links"
61
- :key="link.id"
62
- class="cursor-pointer border-0 bg-transparent px-4 py-3 text-3xl tracking-widest uppercase transition-all"
63
- :class="activeSection === link.id ? 'font-normal' : 'font-light'"
64
- @click="() => handleNav(link)"
65
- >
66
- {{ link.label }}
67
- </button>
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 Render `useSite().titleWords` directly. Unused in this repo and KMCOMv5;
4
- * remove in the next major.
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 — render useSite().titleWords directly.')
8
+ console.warn('[navigation] <SiteTitle> is deprecated — use <SiteLogo>.')
9
9
 
10
10
  const site = useSite()
11
11
  </script>
@@ -1,15 +1,40 @@
1
- import MastNavModal from '../components/Mast/NavModal.vue'
2
- import { createModal } from '../utils/createModal'
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
- export const useMastNav = createModal(MastNavModal, {
5
- onOpen: () => {
13
+ function smoothScroll() {
6
14
  try {
7
- useSmoothScroll().lockScrolling()
8
- } catch {}
9
- },
10
- onClose: () => {
11
- try {
12
- useSmoothScroll().unlockScrolling()
13
- } catch {}
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
- return navigateTo(allowRoutes.value[0])
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') throw createError({ statusCode: 404 })
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
- hooks: {
25
- 'pages:extend'(pages: unknown[]) {
26
- const cwd = process.cwd()
27
- const tag = (list: RoutePage[]) => {
28
- for (const page of list) {
29
- if (page.file && !page.file.startsWith(cwd)) {
30
- page.meta ??= {}
31
- page.meta.__fromLayer = true
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
- tag(pages as unknown as RoutePage[])
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 = useAppConfig() as { routingLayer?: { features?: Record<string, FeatureValue> } }
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.3.0",
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
- "node-gyp": "^12.3.0",
225
- "skills": "^1.5.23"
255
+ "@iconify/vue": "^5.0.1"
226
256
  },
227
257
  "engines": {
228
258
  "node": ">=24 <25"