kmcom-nuxt-layers 6.4.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 (129) hide show
  1. package/docs/MIGRATION-v6.5.0.md +87 -0
  2. package/docs/MIGRATION.md +2 -1
  3. package/docs/USAGE.md +35 -10
  4. package/layers/content/app/components/content/Figure.vue +1 -1
  5. package/layers/content/app/composables/useContentSection.ts +1 -1
  6. package/layers/core/app/app.config.ts +7 -3
  7. package/layers/core/app/components/LoadingScreen.vue +35 -21
  8. package/layers/core/app/composables/useCache.ts +2 -2
  9. package/layers/core/app/composables/useErrorLog.ts +5 -5
  10. package/layers/core/app/composables/useLoading.ts +19 -10
  11. package/layers/core/app/plugins/loading.client.ts +11 -3
  12. package/layers/core/app/types/loading.ts +10 -3
  13. package/layers/core/app/utils/loading.ts +6 -15
  14. package/layers/core/nuxt.config.ts +14 -14
  15. package/layers/core/server/utils/app-config.ts +17 -0
  16. package/layers/data/auth/app/middleware/auth.ts +1 -1
  17. package/layers/data/forms/app/components/Form/Contact.vue +1 -1
  18. package/layers/data/forms/server/api/contact.post.ts +16 -8
  19. package/layers/data/forms/server/api/forms/status.get.ts +3 -0
  20. package/layers/data/forms/{app → shared}/utils/contact-schema.ts +2 -1
  21. package/layers/data/metadata/server/api/metadata/search.get.ts +27 -7
  22. package/layers/data/metadata/server/api/metadata/status.get.ts +4 -1
  23. package/layers/data/metadata/server/api/metadata/sync.post.ts +13 -3
  24. package/layers/data/metadata/server/utils/metadata/resolve-record.ts +3 -3
  25. package/layers/delivery/feeds/nuxt.config.ts +6 -1
  26. package/layers/delivery/feeds/package.json +2 -2
  27. package/layers/delivery/feeds/server/routes/feed/demo.get.ts +2 -2
  28. package/layers/delivery/feeds/server/utils/content-adapter.ts +1 -1
  29. package/layers/delivery/feeds/server/utils/feed-service.ts +2 -2
  30. package/layers/design-system/theming/app/plugins/theme.client.ts +7 -0
  31. package/layers/design-system/theming/nuxt.config.ts +0 -2
  32. package/layers/motion/scroll/app/plugins/locomotive-scroll.client.ts +3 -2
  33. package/layers/render/canvas/nuxt.config.ts +0 -4
  34. package/layers/render/shader/nuxt.config.ts +0 -4
  35. package/layers/structure/routing/app/middleware/01.maintenance.global.ts +4 -1
  36. package/layers/structure/routing/app/middleware/02.governance.global.ts +1 -1
  37. package/layers/structure/routing/nuxt.config.ts +19 -12
  38. package/layers/structure/routing/server/api/feature-flags.get.ts +4 -2
  39. package/package.json +35 -5
  40. package/docs/FALLOW-COMPLEXITY-DUPLICATION-AUDIT.md +0 -64
  41. package/docs/IMPROVE-AUDIT-README.md +0 -29
  42. package/docs/IMPROVE-AUDIT-RESULTS.md +0 -51
  43. package/docs/IMPROVE-DEEP-AUDIT-RESULTS.md +0 -81
  44. package/docs/fallow-refactor/apps-debug.md +0 -27
  45. package/docs/fallow-refactor/apps-playground.md +0 -46
  46. package/docs/fallow-refactor/apps-visual-identity.md +0 -41
  47. package/docs/fallow-refactor/layers-animations.md +0 -34
  48. package/docs/fallow-refactor/layers-canvas.md +0 -32
  49. package/docs/fallow-refactor/layers-content.md +0 -33
  50. package/docs/fallow-refactor/layers-core.md +0 -39
  51. package/docs/fallow-refactor/layers-feeds.md +0 -39
  52. package/docs/fallow-refactor/layers-forms.md +0 -30
  53. package/docs/fallow-refactor/layers-layout.md +0 -42
  54. package/docs/fallow-refactor/layers-mailer.md +0 -32
  55. package/docs/fallow-refactor/layers-motion.md +0 -27
  56. package/docs/fallow-refactor/layers-navigation.md +0 -31
  57. package/docs/fallow-refactor/layers-page-transitions.md +0 -30
  58. package/docs/fallow-refactor/layers-routing.md +0 -33
  59. package/docs/fallow-refactor/layers-scripts.md +0 -35
  60. package/docs/fallow-refactor/layers-scroll.md +0 -38
  61. package/docs/fallow-refactor/layers-seo.md +0 -32
  62. package/docs/fallow-refactor/layers-shader.md +0 -53
  63. package/docs/fallow-refactor/layers-theme.md +0 -33
  64. package/docs/fallow-refactor/layers-transitions.md +0 -27
  65. package/docs/fallow-refactor/layers-typography.md +0 -29
  66. package/docs/fallow-refactor/layers-visual.md +0 -34
  67. package/docs/superpowers/plans/2026-09-03-layout-layer-fluid-rebuild.md +0 -1850
  68. package/docs/superpowers/plans/2026-09-03-vue-element-system.md +0 -1019
  69. package/docs/superpowers/plans/2026-09-04-grid-layer.md +0 -977
  70. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-0-1-design.md +0 -533
  71. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-2-design.md +0 -154
  72. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-3-design.md +0 -109
  73. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-4-design.md +0 -124
  74. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-5-design.md +0 -121
  75. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-6-design.md +0 -77
  76. package/docs/superpowers/specs/2026-08-30-layer-taxonomy-tier-7-design.md +0 -73
  77. package/docs/superpowers/specs/2026-08-31-swiss-grid-layer-design.md +0 -190
  78. package/docs/superpowers/specs/2026-09-03-layout-layer-fluid-rebuild-design.md +0 -312
  79. package/docs/superpowers/specs/2026-09-03-vue-element-system-design.md +0 -342
  80. package/docs/superpowers/specs/2026-09-04-grid-layer-design.md +0 -236
  81. package/layers/content/app/utils/galleryImageMeta.test.ts +0 -30
  82. package/layers/core/app/components/Element.test.ts +0 -90
  83. package/layers/core/app/components/ElementTw.test.ts +0 -64
  84. package/layers/core/app/composables/useLayoutAttrs.test.ts +0 -77
  85. package/layers/core/app/utils/browserInfo.test.ts +0 -81
  86. package/layers/core/app/utils/featureClasses.test.ts +0 -101
  87. package/layers/core/app/utils/loading.test.ts +0 -48
  88. package/layers/core/app/utils/resolveElementStyle.test.ts +0 -99
  89. package/layers/core/app/utils/scrollGuardClamp.test.ts +0 -36
  90. package/layers/core/app/utils/styleToTailwindClasses.test.ts +0 -31
  91. package/layers/data/forms/app/utils/contact-schema.test.ts +0 -75
  92. package/layers/data/forms/app/utils/fieldProps.test.ts +0 -141
  93. package/layers/data/metadata/providers/comicvine/server/utils/metadata-comicvine/normalise.test.ts +0 -201
  94. package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/client.test.ts +0 -76
  95. package/layers/data/metadata/providers/google-books/server/utils/metadata-google-books/normalise.test.ts +0 -144
  96. package/layers/data/metadata/providers/openlibrary/server/utils/metadata-openlibrary/client.test.ts +0 -25
  97. package/layers/data/metadata/providers/openlibrary/server/utils/metadata-openlibrary/normalise.test.ts +0 -144
  98. package/layers/data/metadata/providers/themoviedb/server/utils/metadata-tmdb/client.test.ts +0 -29
  99. package/layers/data/metadata/providers/themoviedb/server/utils/metadata-tmdb/normalise.test.ts +0 -162
  100. package/layers/data/metadata/server/utils/metadata/cache.test.ts +0 -39
  101. package/layers/data/metadata/server/utils/metadata/provider-fetch.test.ts +0 -27
  102. package/layers/data/metadata/server/utils/metadata/provider-registry.test.ts +0 -54
  103. package/layers/data/metadata/server/utils/metadata/resolve-record.test.ts +0 -87
  104. package/layers/data/metadata/server/utils/metadata/search.test.ts +0 -204
  105. package/layers/delivery/feeds/server/utils/content-adapter.test.ts +0 -67
  106. package/layers/delivery/feeds/server/utils/feed-author.test.ts +0 -71
  107. package/layers/delivery/feeds/server/utils/feed-config.test.ts +0 -184
  108. package/layers/delivery/feeds/server/utils/formats.test.ts +0 -71
  109. package/layers/delivery/seo/app/utils/seoConfig.test.ts +0 -58
  110. package/layers/design-system/theming/server/utils/accent-css.test.ts +0 -71
  111. package/layers/design-system/theming/server/utils/fouc-config.test.ts +0 -78
  112. package/layers/design-system/typography/app/components/Typography/Headline.test.ts +0 -36
  113. package/layers/design-system/typography/app/components/Typography/index.test.ts +0 -31
  114. package/layers/design-system/typography/app/composables/color.test.ts +0 -30
  115. package/layers/design-system/typography/app/composables/typography.test.ts +0 -53
  116. package/layers/design-system/visual/app/components/HoverDimGroup.test.ts +0 -38
  117. package/layers/design-system/visual/app/composables/tint.test.ts +0 -22
  118. package/layers/design-system/visual/app/utils/colorTokens.test.ts +0 -51
  119. package/layers/design-system/visual/app/utils/gradientStyle.test.ts +0 -75
  120. package/layers/design-system/visual/app/utils/responsiveSizes.test.ts +0 -71
  121. package/layers/motion/animations/app/components/Motion/Staggered.test.ts +0 -40
  122. package/layers/motion/scroll/app/composables/useReducedMotion.test.ts +0 -23
  123. package/layers/motion/scroll/app/utils/parallax.test.ts +0 -24
  124. package/layers/structure/grid/app/composables/useGridTracks.test.ts +0 -94
  125. package/layers/structure/grid/app/types/tracks.test.ts +0 -32
  126. package/layers/structure/grid/app/utils/placement.test.ts +0 -19
  127. package/layers/structure/layout/app/utils/gridPlacementStyle.test.ts +0 -269
  128. package/layers/structure/navigation/app/utils/site.test.ts +0 -39
  129. package/layers/structure/routing/app/utils/resolveRoute.test.ts +0 -47
@@ -1,16 +1,20 @@
1
- import { createError, defineEventHandler, readBody } from 'h3'
1
+ import { createError, defineEventHandler, getRequestHeader, getRequestIP, readBody } from 'h3'
2
2
  import { sendContactEmail } from '#layers/mailer/server/utils/email'
3
3
  import { mailerLayerHooks } from '#layers/mailer/server/utils/hooks'
4
4
 
5
- import { contactSchema } from '../../app/utils/contact-schema'
5
+ import { contactSchema } from '../../shared/utils/contact-schema'
6
6
 
7
7
  type RateLimitRecord = { count: number; resetAt: number }
8
+ // Per-instance memory: serverless instances each keep their own count. Move to useStorage()
9
+ // or the host's rate limiting if the limit must hold across instances.
8
10
  const rateLimitStore = new Map<string, RateLimitRecord>()
9
11
  const RATE_LIMIT_MAX = 5
10
12
  const RATE_LIMIT_WINDOW_MS = 60_000
11
13
 
12
14
  function checkRateLimit(ip: string): boolean {
13
15
  const now = Date.now()
16
+ // Drop expired windows so the map doesn't grow with every IP ever seen
17
+ for (const [key, entry] of rateLimitStore) if (now > entry.resetAt) rateLimitStore.delete(key)
14
18
  const record = rateLimitStore.get(ip)
15
19
  if (!record || now > record.resetAt) {
16
20
  rateLimitStore.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS })
@@ -22,11 +26,15 @@ function checkRateLimit(ip: string): boolean {
22
26
  }
23
27
 
24
28
  export default defineEventHandler(async (event) => {
25
- const ip = getRequestIP(event, { xForwardedFor: true }) ?? 'unknown'
29
+ // Netlify's client IP header can't be set by the client; X-Forwarded-For's first entry can
30
+ const ip =
31
+ getRequestHeader(event, 'x-nf-client-connection-ip') ??
32
+ getRequestIP(event, { xForwardedFor: true }) ??
33
+ 'unknown'
26
34
  if (!checkRateLimit(ip)) {
27
35
  throw createError({
28
- statusCode: 429,
29
- statusMessage: 'Too many requests. Please try again later.',
36
+ status: 429,
37
+ statusText: 'Too many requests. Please try again later.',
30
38
  })
31
39
  }
32
40
 
@@ -34,7 +42,7 @@ export default defineEventHandler(async (event) => {
34
42
  const result = contactSchema.safeParse(body)
35
43
 
36
44
  if (!result.success) {
37
- throw createError({ statusCode: 400, data: result.error.flatten().fieldErrors })
45
+ throw createError({ status: 400, data: result.error.flatten().fieldErrors })
38
46
  }
39
47
 
40
48
  const data = result.data
@@ -49,7 +57,7 @@ export default defineEventHandler(async (event) => {
49
57
 
50
58
  await mailerLayerHooks.callHook('contact:failed', { ...data, error: emailResult.error })
51
59
  throw createError({
52
- statusCode: 500,
53
- statusMessage: 'Failed to send message. Please try again later.',
60
+ status: 500,
61
+ statusText: 'Failed to send message. Please try again later.',
54
62
  })
55
63
  })
@@ -4,6 +4,9 @@ import { useMailerConfig } from '#layers/mailer/server/utils/config'
4
4
  export default defineEventHandler(() => {
5
5
  const { resendApiKey, emailFrom, emailTo } = useMailerConfig()
6
6
 
7
+ // Addresses are dev-only: emailTo is the private inbox every submission lands in
8
+ if (!import.meta.dev) return { configured: Boolean(resendApiKey) }
9
+
7
10
  return {
8
11
  configured: Boolean(resendApiKey),
9
12
  emailFrom,
@@ -1,7 +1,8 @@
1
1
  import { z } from 'zod'
2
2
 
3
3
  export const contactSchema = z.object({
4
- name: z.string().min(3, 'Name must be at least 3 characters'),
4
+ // Real names can be one or two letters (Li, Ng), so only require one
5
+ name: z.string().trim().min(1, 'Please enter your name'),
5
6
  email: z.string().email('Please enter a valid email'),
6
7
  message: z.string().min(8, 'Message must be at least 8 characters'),
7
8
  })
@@ -1,19 +1,39 @@
1
1
  import { searchMetadata } from '../../utils/metadata/search'
2
2
  import { createError, defineEventHandler, getQuery } from 'h3'
3
- import type { MetadataMediaType, MetadataProviderId } from '#layers/metadata/shared/types'
3
+ import type { MetadataMediaType } from '#layers/metadata/shared/types'
4
+
5
+ const MEDIA_TYPES = new Set<MetadataMediaType>([
6
+ 'book',
7
+ 'comic',
8
+ 'comic-series',
9
+ 'manga',
10
+ 'graphic-novel',
11
+ 'collected-edition',
12
+ 'movie',
13
+ 'tv-show',
14
+ ])
15
+ const MAX_LIMIT = 50
4
16
 
5
17
  // fallow-ignore-next-line complexity
6
18
  export default defineEventHandler(async (event) => {
7
19
  const query = getQuery(event)
8
20
 
9
21
  const q = String(query.q ?? '').trim()
10
- if (!q) throw createError({ statusCode: 400, message: 'q is required' })
22
+ if (!q) throw createError({ status: 400, message: 'q is required' })
23
+
24
+ const mediaType = query.mediaType ? (String(query.mediaType) as MetadataMediaType) : undefined
25
+ if (mediaType && !MEDIA_TYPES.has(mediaType)) {
26
+ throw createError({ status: 400, message: `Unknown mediaType '${mediaType}'` })
27
+ }
28
+
29
+ // Unknown provider ids are dropped by searchMetadata's registry filter
30
+ const providers = query.providers ? String(query.providers).split(',').filter(Boolean) : undefined
11
31
 
12
- const mediaType = query.mediaType as MetadataMediaType | undefined
13
- const providers = query.providers
14
- ? (String(query.providers).split(',').filter(Boolean) as MetadataProviderId[])
15
- : undefined
16
- const limit = query.limit ? Number(query.limit) : 10
32
+ const rawLimit = query.limit ? Number(query.limit) : 10
33
+ if (!Number.isInteger(rawLimit) || rawLimit < 1) {
34
+ throw createError({ status: 400, message: 'limit must be a positive integer' })
35
+ }
36
+ const limit = Math.min(rawLimit, MAX_LIMIT)
17
37
 
18
38
  return searchMetadata({
19
39
  query: q,
@@ -1,7 +1,10 @@
1
1
  import { getProviders } from '../../utils/metadata/provider-registry'
2
- import { defineEventHandler } from 'h3'
2
+ import { createError, defineEventHandler } from 'h3'
3
3
 
4
4
  export default defineEventHandler(async () => {
5
+ // Dev-only: each call runs a live search on every provider, spending real API quota
6
+ if (!import.meta.dev) throw createError({ status: 404 })
7
+
5
8
  const providers = getProviders()
6
9
 
7
10
  return Promise.all(
@@ -1,8 +1,18 @@
1
1
  import { resolveRecord } from '../../utils/metadata/resolve-record'
2
2
  import { defineEventHandler, readBody } from 'h3'
3
- import type { MetadataSyncInput } from '#layers/metadata/shared/types'
3
+
4
+ const str = (v: unknown) => (typeof v === 'string' && v.trim() ? v.trim() : undefined)
4
5
 
5
6
  export default defineEventHandler(async (event) => {
6
- const body = await readBody<MetadataSyncInput>(event)
7
- return resolveRecord({ ...body })
7
+ // Pick known fields only; resolveRecord rejects missing provider/providerId with a 400
8
+ const body = (await readBody<Record<string, unknown>>(event)) ?? {}
9
+ const provider = str(body.provider)
10
+ const providerId = str(body.providerId)
11
+ const resourceType = str(body.resourceType)
12
+ return resolveRecord({
13
+ ...(provider && { provider }),
14
+ ...(providerId && { providerId }),
15
+ ...(resourceType && { resourceType }),
16
+ force: body.force === true,
17
+ })
8
18
  })
@@ -20,12 +20,12 @@ export async function resolveRecord(
20
20
  ): Promise<{ record: MetadataRecord; synced: boolean }> {
21
21
  const { provider: providerName, providerId, resourceType, force } = input
22
22
  if (!providerName || !providerId) {
23
- throw createError({ statusCode: 400, message: 'provider and providerId are required' })
23
+ throw createError({ status: 400, message: 'provider and providerId are required' })
24
24
  }
25
25
 
26
26
  const provider = getProvider(providerName)
27
27
  if (!provider) {
28
- throw createError({ statusCode: 404, message: `Provider '${providerName}' not registered` })
28
+ throw createError({ status: 404, message: `Provider '${providerName}' not registered` })
29
29
  }
30
30
 
31
31
  const cacheKey = makeCacheKey('metadata', providerName, resourceType ?? 'item', providerId)
@@ -40,7 +40,7 @@ export async function resolveRecord(
40
40
  providerId,
41
41
  ...(resourceType && { resourceType }),
42
42
  })
43
- if (!record) throw createError({ statusCode: 404, message: 'Not found' })
43
+ if (!record) throw createError({ status: 404, message: 'Not found' })
44
44
 
45
45
  const now = new Date().toISOString()
46
46
  await setCacheRecord(cacheKey, {
@@ -4,7 +4,7 @@ export default defineNuxtConfig({
4
4
  name: 'feeds',
5
5
  },
6
6
 
7
- extends: process.env.FEEDS_STANDALONE ? ['../../core', '../../content'] : [],
7
+ extends: ['../../core', '../../content'],
8
8
 
9
9
  alias: {
10
10
  '#layers/feeds': import.meta.dirname,
@@ -29,4 +29,9 @@ export default defineNuxtConfig({
29
29
  },
30
30
 
31
31
  compatibilityDate: '2025-07-15',
32
+
33
+ typescript: {
34
+ typeCheck: false,
35
+ strict: true,
36
+ },
32
37
  })
@@ -8,8 +8,8 @@
8
8
  "typecheck": "vue-tsc --noEmit -p ../../../tsconfig.typecheck.json",
9
9
  "dev": "nuxi dev .playground",
10
10
  "dev:prepare": "nuxt prepare .playground",
11
- "build": "FEEDS_STANDALONE=true nuxi build",
12
- "generate": "FEEDS_STANDALONE=true nuxi generate",
11
+ "build": "nuxi build",
12
+ "generate": "nuxi generate",
13
13
  "preview": "nuxt preview .playground",
14
14
  "lint": "eslint ."
15
15
  },
@@ -1,6 +1,6 @@
1
1
  import { buildFeed } from '../../utils/feed-service'
2
2
  import { defineEventHandler, getRequestURL, setHeader } from 'h3'
3
- import { useAppConfig } from 'nitropack/runtime'
3
+ import { readServerAppConfig } from '#layers/core/server/utils/app-config'
4
4
 
5
5
  const RSS_ICON = `<svg viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
6
6
  <path d="M2.5 3a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5H3a.5.5 0 0 1-.5-.5V3Z"/>
@@ -26,7 +26,7 @@ function formatDate(date: Date): string {
26
26
  }
27
27
 
28
28
  export default defineEventHandler(async (event) => {
29
- const appConfig = useAppConfig()
29
+ const appConfig = readServerAppConfig()
30
30
  const feedConfig =
31
31
  (appConfig as { feedsLayer?: { feed?: { defaultCollection?: string } } }).feedsLayer?.feed ?? {}
32
32
  const defaultCollection = feedConfig.defaultCollection ?? 'blog'
@@ -39,7 +39,7 @@ export async function getContentFeedItems(
39
39
  limit: number = 30
40
40
  ): Promise<FeedItem[]> {
41
41
  if (!COLLECTION_NAME_RE.test(collection)) {
42
- throw createError({ statusCode: 400, statusMessage: 'Invalid collection name' })
42
+ throw createError({ status: 400, statusText: 'Invalid collection name' })
43
43
  }
44
44
 
45
45
  // queryCollection is keyed by literal collection names, but feed routes accept
@@ -1,6 +1,6 @@
1
1
  import { getContentFeedItems } from './content-adapter'
2
2
  import { getRequestURL, type H3Event } from 'h3'
3
- import { useAppConfig } from 'nitropack/runtime'
3
+ import { readServerAppConfig } from '#layers/core/server/utils/app-config'
4
4
 
5
5
  import {
6
6
  createFeedConfig,
@@ -16,7 +16,7 @@ export async function buildFeed(
16
16
  collection?: string,
17
17
  options?: { unlimited?: boolean }
18
18
  ): Promise<{ items: FeedItem[]; config: FeedConfig }> {
19
- const appConfig = useAppConfig() as unknown as AppFeedConfig
19
+ const appConfig = readServerAppConfig() as unknown as AppFeedConfig
20
20
  const feedConfig = appConfig.feedsLayer?.feed
21
21
  const requestUrl = getRequestURL(event)
22
22
  const { site, siteUrl } = resolveFeedSiteConfig(appConfig, requestUrl)
@@ -50,6 +50,13 @@ export default defineNuxtPlugin({
50
50
  { immediate: true }
51
51
  )
52
52
 
53
+ // The Reduce Motion switch (restored on mount) also ends core's boot loading screen. Core
54
+ // handles the OS setting itself, so this only reacts to changes, never before hydration
55
+ const loading = useLoading()
56
+ watch(effectiveReducedMotion, (reduced) => {
57
+ if (reduced && loading.isLoading.value) loading.finishNow()
58
+ })
59
+
53
60
  watch(
54
61
  effectiveReducedTransparency,
55
62
  (reduced) => {
@@ -28,8 +28,6 @@ export default defineNuxtConfig({
28
28
  // Shadow/duration tokens live in core (core/app/assets/css/tokens/elevation-motion.css).
29
29
  css: ['#layers/theming/app/assets/css/theme.css'],
30
30
 
31
- plugins: ['#layers/theming/app/plugins/theme.client.ts'],
32
-
33
31
  colorMode: {
34
32
  dataValue: 'theme-mode',
35
33
  storageKey: 'theme-mode',
@@ -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
  })
@@ -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.4.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"
@@ -1,64 +0,0 @@
1
- # Fallow Complexity and Duplication Audit
2
-
3
- This file is the entry point for the layer-by-layer Fallow refactor handoff. Each app or layer has its own document under `docs/fallow-refactor/`.
4
-
5
- Use the split documents instead of one combined warning list. They separate complexity work from duplication work and identify whether the likely extraction target is a component, composable, utility, or local refactor.
6
-
7
- ## Source Data
8
-
9
- The audit uses two sources:
10
-
11
- - Complexity data from `fallow health --complexity --hotspots --targets --format json --quiet --explain`.
12
- - Duplication data from the user-provided Fallow Problems export.
13
-
14
- Run these commands before and after each implementation phase:
15
-
16
- ```bash
17
- FALLOW_AGENT_SOURCE=codex fallow health --complexity --hotspots --targets --format json --quiet --explain 2>/dev/null || true
18
- FALLOW_AGENT_SOURCE=codex fallow dupes --top 80 --format json --quiet --explain 2>/dev/null || true
19
- pnpm typecheck
20
- ```
21
-
22
- ## Recommended Order
23
-
24
- 1. [apps/playground](./fallow-refactor/apps-playground.md)
25
- 2. [layers/shader](./fallow-refactor/layers-shader.md)
26
- 3. [layers/layout](./fallow-refactor/layers-layout.md)
27
- 4. [layers/core](./fallow-refactor/layers-core.md)
28
- 5. [layers/feeds](./fallow-refactor/layers-feeds.md)
29
- 6. Smaller cleanup documents, ordered by risk and locality.
30
-
31
- ## App Documents
32
-
33
- - [apps/debug](./fallow-refactor/apps-debug.md)
34
- - [apps/playground](./fallow-refactor/apps-playground.md)
35
- - [apps/visual-identity](./fallow-refactor/apps-visual-identity.md)
36
-
37
- ## Layer Documents
38
-
39
- - [layers/animations](./fallow-refactor/layers-animations.md)
40
- - [layers/canvas](./fallow-refactor/layers-canvas.md)
41
- - [layers/content](./fallow-refactor/layers-content.md)
42
- - [layers/core](./fallow-refactor/layers-core.md)
43
- - [layers/feeds](./fallow-refactor/layers-feeds.md)
44
- - [layers/forms](./fallow-refactor/layers-forms.md)
45
- - [layers/layout](./fallow-refactor/layers-layout.md)
46
- - [layers/mailer](./fallow-refactor/layers-mailer.md)
47
- - [layers/motion](./fallow-refactor/layers-motion.md)
48
- - [layers/navigation](./fallow-refactor/layers-navigation.md)
49
- - [layers/page-transitions](./fallow-refactor/layers-page-transitions.md)
50
- - [layers/routing](./fallow-refactor/layers-routing.md)
51
- - [layers/scroll](./fallow-refactor/layers-scroll.md)
52
- - [layers/scripts](./fallow-refactor/layers-scripts.md)
53
- - [layers/seo](./fallow-refactor/layers-seo.md)
54
- - [layers/shader](./fallow-refactor/layers-shader.md)
55
- - [layers/theme](./fallow-refactor/layers-theme.md)
56
- - [layers/transitions](./fallow-refactor/layers-transitions.md)
57
- - [layers/typography](./fallow-refactor/layers-typography.md)
58
- - [layers/visual](./fallow-refactor/layers-visual.md)
59
-
60
- ## Non-Goals
61
-
62
- Do not address unused exports, unused files, unused dependencies, circular dependencies, or catalog warnings as part of this specific task. Those warnings need separate verification and tracing.
63
-
64
- Do not run `fallow fix` for this work. These refactors are structural and require manual review.
@@ -1,29 +0,0 @@
1
- # Improve Audit
2
-
3
- Audit written against commit `a2805da`.
4
-
5
- ## Files
6
-
7
- | File | Purpose |
8
- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
9
- | [IMPROVE-AUDIT-RESULTS.md](./IMPROVE-AUDIT-RESULTS.md) | Vetted improve audit findings, direction options, verification results, and recommended planning order. |
10
-
11
- ## Recommended Next Work
12
-
13
- Recommended first implementation set:
14
-
15
- 1. Fix the current feeds lint failure.
16
- 2. Bring published server routes into root typecheck and type-aware lint coverage.
17
- 3. Stop exposing configured mailer addresses from the public forms status endpoint.
18
- 4. Correct the forms demo mailer environment variable names.
19
- 5. Add focused behavior tests around feed, forms/mailer, and routing paths.
20
-
21
- ## Dependency Order
22
-
23
- | Order | Finding | Depends On | Status |
24
- | ----- | -------------------------------------------------- | ------------------------------------------------- | ------ |
25
- | 1 | Current feeds lint failure | None | TODO |
26
- | 2 | Server route typecheck coverage | Feeds lint fix recommended first | TODO |
27
- | 3 | Public forms status endpoint leaks email addresses | Server route typecheck coverage recommended first | TODO |
28
- | 4 | Wrong mailer env var docs in forms demo | None | TODO |
29
- | 5 | Missing behavior tests | Typecheck/lint baseline | TODO |