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.
- package/docs/MIGRATION-v6.5.0.md +87 -0
- package/docs/MIGRATION.md +2 -1
- package/docs/USAGE.md +35 -10
- 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 +7 -3
- 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 +6 -15
- 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/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/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/components/HoverDimGroup.test.ts +0 -38
- 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
|
@@ -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 '../../
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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({
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
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({
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const limit =
|
|
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
|
-
|
|
3
|
+
|
|
4
|
+
const str = (v: unknown) => (typeof v === 'string' && v.trim() ? v.trim() : undefined)
|
|
4
5
|
|
|
5
6
|
export default defineEventHandler(async (event) => {
|
|
6
|
-
|
|
7
|
-
|
|
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({
|
|
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({
|
|
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({
|
|
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:
|
|
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": "
|
|
12
|
-
"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 {
|
|
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 =
|
|
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({
|
|
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 {
|
|
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 =
|
|
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
|
|
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(() => {
|
|
@@ -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"
|
|
@@ -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 |
|