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
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Migration Guide — v6.4.x → v6.5.0
|
|
2
|
+
|
|
3
|
+
**Audience:** an AI coding agent (or developer) updating a downstream Nuxt app that consumes these
|
|
4
|
+
layers.
|
|
5
|
+
**What changed:** layers stop making app-wide choices for you, the published package declares the
|
|
6
|
+
dependencies its layers import, and two diagnostic API routes are now dev-only.
|
|
7
|
+
|
|
8
|
+
Work top to bottom. Each change has a **Detect** step and an **Apply** step; skip anything Detect
|
|
9
|
+
doesn't find.
|
|
10
|
+
|
|
11
|
+
## 0. Preconditions
|
|
12
|
+
|
|
13
|
+
- Update the package to `^6.5.0`, reinstall, run `npx nuxt prepare`.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Required changes
|
|
18
|
+
|
|
19
|
+
### 1. `core` no longer sets `future.compatibilityVersion: 5` or `devtools`
|
|
20
|
+
|
|
21
|
+
Layer config merges into the app, so `core` was opting every consumer into Nuxt 5 behaviour and
|
|
22
|
+
forcing devtools on. Both are now the app's call.
|
|
23
|
+
|
|
24
|
+
**Detect:** your app's `nuxt.config.ts` has no `future.compatibilityVersion`.
|
|
25
|
+
|
|
26
|
+
**Apply:** to keep the behaviour you had on v6.4, add:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
export default defineNuxtConfig({
|
|
30
|
+
future: { compatibilityVersion: 5 },
|
|
31
|
+
devtools: { enabled: true },
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Install the peers for the layers you use
|
|
36
|
+
|
|
37
|
+
The root package now lists these as optional peer dependencies. They were always imported at
|
|
38
|
+
runtime; previously the app only worked if something else happened to install them.
|
|
39
|
+
|
|
40
|
+
**Detect:** you extend `mailer`/`forms`, `database`, `auth` or `feeds`.
|
|
41
|
+
|
|
42
|
+
**Apply:**
|
|
43
|
+
|
|
44
|
+
| Layer | Install |
|
|
45
|
+
| ------------------ | ------------------------------------------ |
|
|
46
|
+
| `mailer` / `forms` | `resend hookable` (plus `zod` for `forms`) |
|
|
47
|
+
| `database` | `drizzle-orm @neondatabase/serverless` |
|
|
48
|
+
| `auth` | `nuxt-auth-utils` |
|
|
49
|
+
| `feeds` | `feed` |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Behaviour changes (no action unless you rely on them)
|
|
54
|
+
|
|
55
|
+
- **`GET /api/forms/status`** returns only `{ configured }` outside dev. `emailFrom`/`emailTo` are
|
|
56
|
+
dev-only because `emailTo` is the private inbox every contact submission goes to.
|
|
57
|
+
- **`GET /api/metadata/status`** returns 404 outside dev. Each call ran a live search on every
|
|
58
|
+
provider, spending real API quota.
|
|
59
|
+
- **`GET /api/metadata/search`** rejects an unknown `mediaType` or a non-positive `limit` with a
|
|
60
|
+
400, and caps `limit` at 50.
|
|
61
|
+
- **`feeds`** now always extends `core` and `content`. The `FEEDS_STANDALONE` env var is gone.
|
|
62
|
+
- **`routing`**: `layerDefaultDeny` now works in npm consumers. Layer pages were never tagged
|
|
63
|
+
before, so a layer page without a `feature` meta will now be denied if you enabled
|
|
64
|
+
`layerDefaultDeny`.
|
|
65
|
+
- **`auth`** middleware redirects to `/auth/github` with `external: true`, so client-side
|
|
66
|
+
navigation reaches the OAuth route instead of the 404 page.
|
|
67
|
+
- **`forms`**: `contactSchema` moved from `app/utils/` to `shared/utils/`. Import it from
|
|
68
|
+
`#layers/forms/shared/utils/contact-schema` if you imported it by path.
|
|
69
|
+
- **`@nuxt/eslint`** loads only when installed, so apps without it no longer fail outside
|
|
70
|
+
production.
|
|
71
|
+
- **`feeds`** and **`routing`** server routes now read your `app.config.ts` when Nitro
|
|
72
|
+
auto-imports are on. They imported `useAppConfig` from `nitropack/runtime`, which never sees
|
|
73
|
+
it, so feeds were titled "My Site", linked to the request origin (localhost when prerendered)
|
|
74
|
+
and ignored `feedsLayer`, and `/api/feature-flags` returned `{}`. They now use `site`,
|
|
75
|
+
`feedsLayer` and `routingLayer.features`. With `experimental.nitroAutoImports: false` (the
|
|
76
|
+
Nuxt 5 default) Nuxt can't evaluate app.config.ts in Nitro, so they keep the old defaults;
|
|
77
|
+
set `nitroAutoImports: true` to get your values. Layer server code reads app config through
|
|
78
|
+
core's `readServerAppConfig()`.
|
|
79
|
+
- **`core` loading screen** stops being a timer. `minDuration` defaults to `0` (was `1500`) and
|
|
80
|
+
`maxDuration` to `1500` (was `3000`). A new `showAfter` (default `400` ms) keeps the screen
|
|
81
|
+
hidden on fast loads. The percentage moves at real milestones (hydrated, fonts ready, page
|
|
82
|
+
loaded) instead of a simulated trickle; `trickleProgress` is gone. Under
|
|
83
|
+
`prefers-reduced-motion` the screen is hidden and loading ends on mount. `useLoading()` adds
|
|
84
|
+
`finishNow()` and `skipped`. Set `minDuration: 1500` to keep the old pacing.
|
|
85
|
+
- **`theming`**: the Reduce Motion switch also ends the boot loading screen.
|
|
86
|
+
- **`forms`**: `contactSchema` accepts any non-blank name (was 3+ characters), so short names
|
|
87
|
+
like Li or Ng get through.
|
package/docs/MIGRATION.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
> **Upgrading?** v6.
|
|
1
|
+
> **Upgrading?** v6.4 → v6.5: [`MIGRATION-v6.5.0.md`](./MIGRATION-v6.5.0.md) · v6.3 → v6.4:
|
|
2
|
+
> [`MIGRATION-v6.4.0.md`](./MIGRATION-v6.4.0.md) · v6.2 → v6.3:
|
|
2
3
|
> [`MIGRATION-v6.3.0.md`](./MIGRATION-v6.3.0.md). The guides below cover older versions.
|
|
3
4
|
|
|
4
5
|
# Agent Migration Guide — Update an app to nuxt-layers v2.2.x
|
package/docs/USAGE.md
CHANGED
|
@@ -93,12 +93,21 @@ npm install gsap locomotive-scroll
|
|
|
93
93
|
# canvas / shader
|
|
94
94
|
npm install three @tresjs/nuxt @tresjs/core @tresjs/cientos @tresjs/post-processing
|
|
95
95
|
|
|
96
|
-
# forms
|
|
97
|
-
npm install zod
|
|
96
|
+
# forms / mailer
|
|
97
|
+
npm install zod resend hookable
|
|
98
|
+
|
|
99
|
+
# database
|
|
100
|
+
npm install drizzle-orm @neondatabase/serverless
|
|
101
|
+
|
|
102
|
+
# auth
|
|
103
|
+
npm install nuxt-auth-utils
|
|
98
104
|
|
|
99
105
|
# content
|
|
100
106
|
npm install @nuxt/content better-sqlite3
|
|
101
107
|
|
|
108
|
+
# feeds
|
|
109
|
+
npm install feed
|
|
110
|
+
|
|
102
111
|
# scripts (baseline-status badge)
|
|
103
112
|
npm install @nuxt/scripts baseline-status
|
|
104
113
|
```
|
|
@@ -188,14 +197,30 @@ export default defineAppConfig({
|
|
|
188
197
|
|
|
189
198
|
### Available Config Namespaces
|
|
190
199
|
|
|
191
|
-
| Namespace | Layer | Key Options
|
|
192
|
-
| ----------------------- | ------- |
|
|
193
|
-
| `coreLayer.loading` | core | `enabled`, `minDuration`, `maxDuration`, `label`, `background`, `textColor` |
|
|
194
|
-
| `coreLayer.scrollGuard` | core | `enabled`, `strict`, `excludeSelectors`, `debug`
|
|
195
|
-
| `coreLayer.errors` | core | `logToConsole`, `logToExternal`, `externalUrl`
|
|
196
|
-
| `coreLayer.notFound` | core | `icon`, `title`, `description`, `showHomeButton`, `actions`, `suggestions`
|
|
197
|
-
| `motion` | motion | `gsapScrollTrigger`, `lenis.*` (duration, smoothWheel, smoothTouch, etc.)
|
|
198
|
-
| `themeLayer` | theming | `accents`, `defaultAccent`
|
|
200
|
+
| Namespace | Layer | Key Options |
|
|
201
|
+
| ----------------------- | ------- | ---------------------------------------------------------------------------------------- |
|
|
202
|
+
| `coreLayer.loading` | core | `enabled`, `minDuration`, `maxDuration`, `showAfter`, `label`, `background`, `textColor` |
|
|
203
|
+
| `coreLayer.scrollGuard` | core | `enabled`, `strict`, `excludeSelectors`, `debug` |
|
|
204
|
+
| `coreLayer.errors` | core | `logToConsole`, `logToExternal`, `externalUrl` |
|
|
205
|
+
| `coreLayer.notFound` | core | `icon`, `title`, `description`, `showHomeButton`, `actions`, `suggestions` |
|
|
206
|
+
| `motion` | motion | `gsapScrollTrigger`, `lenis.*` (duration, smoothWheel, smoothTouch, etc.) |
|
|
207
|
+
| `themeLayer` | theming | `accents`, `defaultAccent` |
|
|
208
|
+
|
|
209
|
+
### Overriding Layer Components
|
|
210
|
+
|
|
211
|
+
Layer components are auto-imported by file name, and the app always wins: a component in your
|
|
212
|
+
app with the same name replaces the layer's copy everywhere, including where other layers render
|
|
213
|
+
it. These unprefixed names are the ones most likely to collide by accident:
|
|
214
|
+
|
|
215
|
+
| Component | Layer | Notes |
|
|
216
|
+
| -------------------------------------- | ------ | -------------------------------------------------------------- |
|
|
217
|
+
| `LoadingScreen` | core | Meant to be overridable (custom boot screen) |
|
|
218
|
+
| `ElementTw` | core | Rendered by typography and visual — shadowing changes them too |
|
|
219
|
+
| `Element`, `AppContainer` | core | Primitives; pick another name for app components |
|
|
220
|
+
| `ErrorBoundary` | core | Pick another name for app boundaries |
|
|
221
|
+
| `HStack`, `VStack`, `ZStack`, `Spacer` | layout | Primitives; pick another name for app components |
|
|
222
|
+
|
|
223
|
+
Override one on purpose by creating the same-named file in your `app/components/`.
|
|
199
224
|
|
|
200
225
|
## Optional Modules
|
|
201
226
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
<template>
|
|
18
18
|
<figure class="my-6">
|
|
19
|
-
<img :src :alt :width :height class="w-full rounded-lg" />
|
|
19
|
+
<img :src :alt :width :height loading="lazy" decoding="async" class="w-full rounded-lg" />
|
|
20
20
|
<figcaption v-if="caption" class="text-muted mt-2 text-center text-sm italic">
|
|
21
21
|
{{ caption }}
|
|
22
22
|
</figcaption>
|
|
@@ -4,6 +4,6 @@ import type { ContentLayerConfig } from '../types/content'
|
|
|
4
4
|
export function useContentSection(section: keyof NonNullable<ContentLayerConfig['sections']>) {
|
|
5
5
|
const contentLayer = useAppConfig().contentLayer as ContentLayerConfig | undefined
|
|
6
6
|
if (contentLayer?.sections?.[section] === false) {
|
|
7
|
-
throw createError({
|
|
7
|
+
throw createError({ status: 404, statusText: 'Not Found' })
|
|
8
8
|
}
|
|
9
9
|
}
|
|
@@ -33,10 +33,13 @@ export default defineAppConfig({
|
|
|
33
33
|
loading: {
|
|
34
34
|
// Opt in: app.vue renders LoadingScreen for every app, so it stays off unless enabled
|
|
35
35
|
enabled: false,
|
|
36
|
-
// Minimum time (ms, from navigation start) the count
|
|
37
|
-
|
|
36
|
+
// Minimum time (ms, from navigation start) before the count may reach 100 - 0 finishes
|
|
37
|
+
// as soon as the work does; raise it only for a deliberate brand moment
|
|
38
|
+
minDuration: 0,
|
|
38
39
|
// Longest wait (ms, after mount) for fonts and images before finishing anyway
|
|
39
|
-
maxDuration:
|
|
40
|
+
maxDuration: 1500,
|
|
41
|
+
// Delay (ms, from first paint) before the screen fades in - faster loads never see it
|
|
42
|
+
showAfter: 400,
|
|
40
43
|
// Small caption above the counter - empty string hides it
|
|
41
44
|
label: 'Loading',
|
|
42
45
|
// Background color
|
|
@@ -76,6 +79,7 @@ declare module '@nuxt/schema' {
|
|
|
76
79
|
enabled?: boolean
|
|
77
80
|
minDuration?: number
|
|
78
81
|
maxDuration?: number
|
|
82
|
+
showAfter?: number
|
|
79
83
|
label?: string
|
|
80
84
|
background?: string
|
|
81
85
|
textColor?: string
|
|
@@ -5,16 +5,18 @@
|
|
|
5
5
|
// Beat on "100" so the finished count registers before the exit starts
|
|
6
6
|
const HOLD_MS = 350
|
|
7
7
|
|
|
8
|
-
const { enabled, label, background, textColor, zIndex } = resolveLoadingConfig(
|
|
8
|
+
const { enabled, label, background, textColor, zIndex, showAfter } = resolveLoadingConfig(
|
|
9
9
|
useAppConfig().coreLayer
|
|
10
10
|
)
|
|
11
11
|
|
|
12
|
-
const { isLoading, progress } = useLoading()
|
|
12
|
+
const { isLoading, progress, skipped } = useLoading()
|
|
13
13
|
// Written only here; everything else reads it through useLoading().screen
|
|
14
14
|
const screen = useState<LoadingScreenState>('core:loading:screen', () => 'hidden')
|
|
15
15
|
|
|
16
16
|
const visible = ref(enabled)
|
|
17
17
|
if (enabled) screen.value = 'visible'
|
|
18
|
+
// Leave without the beat or the exit animation: skipped, or done before it ever faded in
|
|
19
|
+
const instant = ref(false)
|
|
18
20
|
|
|
19
21
|
const displayProgress = computed(() => Math.floor(progress.value))
|
|
20
22
|
|
|
@@ -35,22 +37,31 @@
|
|
|
35
37
|
isLoading,
|
|
36
38
|
(loading) => {
|
|
37
39
|
if (loading || !visible.value) return
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
instant.value = skipped.value || performance.now() < showAfter
|
|
41
|
+
setTimeout(
|
|
42
|
+
() => {
|
|
43
|
+
visible.value = false
|
|
44
|
+
screen.value = 'leaving'
|
|
45
|
+
},
|
|
46
|
+
instant.value ? 0 : HOLD_MS
|
|
47
|
+
)
|
|
42
48
|
},
|
|
43
49
|
{ immediate: true }
|
|
44
50
|
)
|
|
45
51
|
</script>
|
|
46
52
|
|
|
47
53
|
<template>
|
|
48
|
-
<Transition name="loading-screen" @after-leave="screen = 'hidden'">
|
|
54
|
+
<Transition name="loading-screen" :css="!instant" @after-leave="screen = 'hidden'">
|
|
49
55
|
<div
|
|
50
56
|
v-if="visible"
|
|
51
57
|
id="loading-screen"
|
|
52
58
|
class="loading-screen"
|
|
53
|
-
:style="{
|
|
59
|
+
:style="{
|
|
60
|
+
backgroundColor: background,
|
|
61
|
+
color: textColor,
|
|
62
|
+
zIndex,
|
|
63
|
+
'--loading-show-after': `${showAfter}ms`,
|
|
64
|
+
}"
|
|
54
65
|
>
|
|
55
66
|
<div
|
|
56
67
|
class="loading-content"
|
|
@@ -76,6 +87,10 @@
|
|
|
76
87
|
position: fixed;
|
|
77
88
|
inset: 0;
|
|
78
89
|
container-type: size;
|
|
90
|
+
|
|
91
|
+
/* Fast loads never see it: hidden and click-through until showAfter has passed.
|
|
92
|
+
`backwards`, not `both` - a held end state would block the exit transition below */
|
|
93
|
+
animation: loading-show 0.2s ease var(--loading-show-after, 0ms) backwards;
|
|
79
94
|
}
|
|
80
95
|
|
|
81
96
|
.loading-content {
|
|
@@ -94,7 +109,8 @@
|
|
|
94
109
|
sans-serif;
|
|
95
110
|
|
|
96
111
|
/* `backwards`, not `both` - a held end state would block the exit transition below */
|
|
97
|
-
animation: loading-enter 0.9s cubic-bezier(0.16, 1, 0.3, 1)
|
|
112
|
+
animation: loading-enter 0.9s cubic-bezier(0.16, 1, 0.3, 1)
|
|
113
|
+
calc(var(--loading-show-after, 0ms) + 0.1s) backwards;
|
|
98
114
|
}
|
|
99
115
|
|
|
100
116
|
.loading-label {
|
|
@@ -141,6 +157,13 @@
|
|
|
141
157
|
}
|
|
142
158
|
}
|
|
143
159
|
|
|
160
|
+
@keyframes loading-show {
|
|
161
|
+
from {
|
|
162
|
+
visibility: hidden;
|
|
163
|
+
opacity: 0;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
144
167
|
@keyframes loading-enter {
|
|
145
168
|
from {
|
|
146
169
|
transform: translateY(0.75rem);
|
|
@@ -173,19 +196,10 @@
|
|
|
173
196
|
opacity: 0;
|
|
174
197
|
}
|
|
175
198
|
|
|
199
|
+
/* No screen under reduced motion - the plugin ends loading on mount (finishNow) */
|
|
176
200
|
@media (prefers-reduced-motion: reduce) {
|
|
177
|
-
.loading-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
.loading-screen-leave-to .loading-content {
|
|
182
|
-
transform: none;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
@keyframes loading-fade-in {
|
|
187
|
-
from {
|
|
188
|
-
opacity: 0;
|
|
201
|
+
.loading-screen {
|
|
202
|
+
display: none;
|
|
189
203
|
}
|
|
190
204
|
}
|
|
191
205
|
</style>
|
|
@@ -78,11 +78,11 @@ export function useCache() {
|
|
|
78
78
|
try {
|
|
79
79
|
const cacheNames = await caches.keys()
|
|
80
80
|
await Promise.all(cacheNames.map((name) => caches.delete(name)))
|
|
81
|
-
if (
|
|
81
|
+
if (import.meta.dev) {
|
|
82
82
|
console.log('[useCache] All caches cleared')
|
|
83
83
|
}
|
|
84
84
|
} catch (error) {
|
|
85
|
-
if (
|
|
85
|
+
if (import.meta.dev) {
|
|
86
86
|
console.error('[useCache] Failed to clear cache:', error)
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -73,7 +73,7 @@ export function useErrorLog() {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
// Console logging (development only by default)
|
|
76
|
-
if (config.value.logToConsole && import.meta.dev
|
|
76
|
+
if (config.value.logToConsole && import.meta.dev) {
|
|
77
77
|
console.group('🔴 Error Logged')
|
|
78
78
|
|
|
79
79
|
console.error('Error:', errorObj)
|
|
@@ -96,7 +96,7 @@ export function useErrorLog() {
|
|
|
96
96
|
const { externalUrl, externalToken } = config.value
|
|
97
97
|
|
|
98
98
|
if (!externalUrl) {
|
|
99
|
-
if (
|
|
99
|
+
if (import.meta.dev) {
|
|
100
100
|
console.warn('Error logging enabled but no externalUrl configured')
|
|
101
101
|
}
|
|
102
102
|
return
|
|
@@ -120,7 +120,7 @@ export function useErrorLog() {
|
|
|
120
120
|
})
|
|
121
121
|
} catch (loggingError) {
|
|
122
122
|
// Don't let logging errors crash the app
|
|
123
|
-
if (import.meta.dev
|
|
123
|
+
if (import.meta.dev) {
|
|
124
124
|
console.error('Failed to send error to external service:', loggingError)
|
|
125
125
|
}
|
|
126
126
|
}
|
|
@@ -130,7 +130,7 @@ export function useErrorLog() {
|
|
|
130
130
|
* Log a warning (non-critical error)
|
|
131
131
|
*/
|
|
132
132
|
const logWarning = (message: string, context?: ErrorContext) => {
|
|
133
|
-
if (config.value.logToConsole && import.meta.dev
|
|
133
|
+
if (config.value.logToConsole && import.meta.dev) {
|
|
134
134
|
console.warn('⚠️ Warning:', message, context)
|
|
135
135
|
}
|
|
136
136
|
}
|
|
@@ -139,7 +139,7 @@ export function useErrorLog() {
|
|
|
139
139
|
* Log info (for debugging)
|
|
140
140
|
*/
|
|
141
141
|
const logInfo = (message: string, data?: unknown) => {
|
|
142
|
-
if (config.value.logToConsole && import.meta.dev
|
|
142
|
+
if (config.value.logToConsole && import.meta.dev) {
|
|
143
143
|
console.log('ℹ️ Info:', message, data)
|
|
144
144
|
}
|
|
145
145
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// composables/useLoading.ts
|
|
2
2
|
|
|
3
3
|
import type { LoadingScreenState } from '../types/loading'
|
|
4
|
-
import { approachProgress, resolveLoadingConfig
|
|
4
|
+
import { approachProgress, resolveLoadingConfig } from '../utils/loading'
|
|
5
5
|
|
|
6
6
|
// Frame-loop state is non-reactive and intentionally module-scope (singleton)
|
|
7
7
|
const _loop = {
|
|
@@ -16,9 +16,10 @@ const _loop = {
|
|
|
16
16
|
* Loading state composable for app initialization and async operations
|
|
17
17
|
*
|
|
18
18
|
* Features:
|
|
19
|
-
* -
|
|
20
|
-
*
|
|
21
|
-
* -
|
|
19
|
+
* - Real progress: callers raise the target at real milestones (`updateProgress`) and a per-frame
|
|
20
|
+
* loop eases the displayed value toward it, then counts up to 100 once work is done
|
|
21
|
+
* - Optional `minDuration` hold before 100 (default 0: finish as soon as the work does)
|
|
22
|
+
* - `finishNow()` ends the boot screen at once, e.g. for reduced motion
|
|
22
23
|
* - Shared state across all component instances
|
|
23
24
|
* - Can be disabled via app.config
|
|
24
25
|
* - Works without UI component (headless)
|
|
@@ -30,9 +31,8 @@ export function useLoading() {
|
|
|
30
31
|
const isLoading = useState('core:loading', () => enabled)
|
|
31
32
|
const progress = useState('core:loading:progress', () => 0)
|
|
32
33
|
const screen = useState<LoadingScreenState>('core:loading:screen', () => 'hidden')
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const trickleTau = Math.max(minDuration, 500) * 0.8
|
|
34
|
+
// Set by finishNow: the boot screen leaves at once, without its beat or exit animation
|
|
35
|
+
const skipped = useState('core:loading:skipped', () => false)
|
|
36
36
|
|
|
37
37
|
function tick(now: number): void {
|
|
38
38
|
// Cap at one 30fps frame so a main-thread hitch (hydration, a backgrounded tab) is followed
|
|
@@ -40,9 +40,7 @@ export function useLoading() {
|
|
|
40
40
|
const dt = Math.min((now - _loop.lastTime) / 1000, 1 / 30)
|
|
41
41
|
_loop.lastTime = now
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
_loop.target = Math.max(_loop.target, trickleProgress(elapsed, trickleTau))
|
|
45
|
-
if (_loop.done && elapsed >= minDuration) _loop.target = 100
|
|
43
|
+
if (_loop.done && now - _loop.startedAt >= minDuration) _loop.target = 100
|
|
46
44
|
|
|
47
45
|
progress.value = approachProgress(progress.value, _loop.target, dt)
|
|
48
46
|
|
|
@@ -82,6 +80,15 @@ export function useLoading() {
|
|
|
82
80
|
_loop.target = Math.max(_loop.target, Math.min(Math.max(value, 0), 99))
|
|
83
81
|
}
|
|
84
82
|
|
|
83
|
+
function finishNow(): void {
|
|
84
|
+
skipped.value = true
|
|
85
|
+
if (import.meta.client) cancelAnimationFrame(_loop.frame)
|
|
86
|
+
_loop.frame = 0
|
|
87
|
+
_loop.done = true
|
|
88
|
+
progress.value = 100
|
|
89
|
+
isLoading.value = false
|
|
90
|
+
}
|
|
91
|
+
|
|
85
92
|
async function withLoading(fn: () => Promise<void>): Promise<void> {
|
|
86
93
|
startLoading()
|
|
87
94
|
try {
|
|
@@ -95,9 +102,11 @@ export function useLoading() {
|
|
|
95
102
|
isLoading: readonly(isLoading),
|
|
96
103
|
progress: readonly(progress),
|
|
97
104
|
screen: readonly(screen),
|
|
105
|
+
skipped: readonly(skipped),
|
|
98
106
|
startLoading,
|
|
99
107
|
stopLoading,
|
|
100
108
|
updateProgress,
|
|
109
|
+
finishNow,
|
|
101
110
|
withLoading,
|
|
102
111
|
}
|
|
103
112
|
}
|
|
@@ -6,12 +6,13 @@ import { resolveLoadingConfig } from '../utils/loading'
|
|
|
6
6
|
* Loading screen initialization plugin
|
|
7
7
|
*
|
|
8
8
|
* Client-side only. Once the app has mounted (hydration done) it:
|
|
9
|
-
* 1. Starts the
|
|
10
|
-
*
|
|
9
|
+
* 1. Starts the count, timed from navigation start, and moves it at real milestones:
|
|
10
|
+
* hydrated (50), fonts ready (75)
|
|
11
11
|
* 2. Waits for fonts and the initial page's images, capped at `maxDuration`
|
|
12
12
|
* 3. Marks loading done - the count then finishes to 100 before the screen exits
|
|
13
13
|
*
|
|
14
14
|
* Starting after mount keeps the counter at the SSR'd 0 through hydration (no mismatch).
|
|
15
|
+
* With `prefers-reduced-motion: reduce` it ends at once instead: CSS hides the screen.
|
|
15
16
|
*
|
|
16
17
|
* Configuration via app.config.ts:
|
|
17
18
|
* - Set `coreLayer.loading.enabled: false` to disable
|
|
@@ -26,11 +27,18 @@ export default defineNuxtPlugin({
|
|
|
26
27
|
return // Disabled, skip initialization
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
const { startLoading, stopLoading } = useLoading()
|
|
30
|
+
const { isLoading, startLoading, stopLoading, updateProgress, finishNow } = useLoading()
|
|
30
31
|
|
|
31
32
|
// Not awaited: app:mounted handlers run serially, so waiting here would hold up other layers
|
|
32
33
|
nuxtApp.hook('app:mounted', () => {
|
|
34
|
+
// CSS hides the screen under reduced motion, so don't hold the page (GSAP, scrolling) for it.
|
|
35
|
+
// After hydration, not in setup: state changes before then would fail to hydrate
|
|
36
|
+
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return finishNow()
|
|
37
|
+
// Already ended by another layer, e.g. the theme's Reduce Motion switch
|
|
38
|
+
if (!isLoading.value) return
|
|
33
39
|
startLoading(0)
|
|
40
|
+
updateProgress(50)
|
|
41
|
+
void document.fonts?.ready.then(() => updateProgress(75))
|
|
34
42
|
void Promise.race([
|
|
35
43
|
Promise.all([pageLoaded(), document.fonts?.ready]),
|
|
36
44
|
new Promise((resolve) => setTimeout(resolve, maxDuration)),
|
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
export type LoadingConfig = {
|
|
7
7
|
/** Master toggle - set to false to disable completely */
|
|
8
8
|
enabled: boolean
|
|
9
|
-
/** Minimum time (ms, from navigation start) the count
|
|
9
|
+
/** Minimum time (ms, from navigation start) before the count may reach 100 - 0 finishes
|
|
10
|
+
* as soon as the work does */
|
|
10
11
|
minDuration: number
|
|
11
12
|
/** Longest wait (ms, after the app mounts) for fonts and images before finishing anyway */
|
|
12
13
|
maxDuration: number
|
|
14
|
+
/** Delay (ms, from first paint) before the screen fades in - faster loads never see it */
|
|
15
|
+
showAfter: number
|
|
13
16
|
/** Small caption above the counter - empty string hides it */
|
|
14
17
|
label: string
|
|
15
18
|
/** Background color */
|
|
@@ -46,12 +49,16 @@ export type UseLoadingReturn = {
|
|
|
46
49
|
progress: Readonly<Ref<number>>
|
|
47
50
|
/** Boot loading screen lifecycle (readonly) */
|
|
48
51
|
screen: Readonly<Ref<LoadingScreenState>>
|
|
49
|
-
/**
|
|
52
|
+
/** True once `finishNow` has skipped the boot screen (readonly) */
|
|
53
|
+
skipped: Readonly<Ref<boolean>>
|
|
54
|
+
/** Start counting from 0, timed from `since` (a `performance.now()` value; default now) */
|
|
50
55
|
startLoading: (since?: number) => void
|
|
51
56
|
/** Mark work done - the count finishes to 100 once `minDuration` has passed */
|
|
52
57
|
stopLoading: () => void
|
|
53
|
-
/** Raise the
|
|
58
|
+
/** Raise the target to a real milestone (0-99) - only `stopLoading` completes */
|
|
54
59
|
updateProgress: (value: number) => void
|
|
60
|
+
/** End the boot screen now, without its beat or exit animation. Call after hydration */
|
|
61
|
+
finishNow: () => void
|
|
55
62
|
/** Execute async function with loading state */
|
|
56
63
|
withLoading: (fn: () => Promise<void>) => Promise<void>
|
|
57
64
|
}
|
|
@@ -3,8 +3,9 @@ import type { LoadingConfig } from '../types/loading'
|
|
|
3
3
|
/** Fallbacks for keys app.config leaves unset - core's app.config ships the same values. */
|
|
4
4
|
const LOADING_DEFAULTS: LoadingConfig = {
|
|
5
5
|
enabled: false,
|
|
6
|
-
minDuration:
|
|
7
|
-
maxDuration:
|
|
6
|
+
minDuration: 0,
|
|
7
|
+
maxDuration: 1500,
|
|
8
|
+
showAfter: 400,
|
|
8
9
|
label: 'Loading',
|
|
9
10
|
background: '#0a0a0a',
|
|
10
11
|
textColor: '#ffffff',
|
|
@@ -18,27 +19,17 @@ export function resolveLoadingConfig(coreLayer: unknown): LoadingConfig {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/*
|
|
21
|
-
* Pure easing math behind `useLoading`'s progress counter.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* Pure easing math behind `useLoading`'s progress counter. The target only moves at real
|
|
23
|
+
* milestones; `approachProgress` runs once per animation frame to move the displayed value
|
|
24
|
+
* toward it, so the counter climbs smoothly instead of jumping.
|
|
24
25
|
*/
|
|
25
26
|
|
|
26
|
-
/** Simulated progress stays below this until loading actually finishes. */
|
|
27
|
-
const TRICKLE_CAP = 90
|
|
28
27
|
/** Time constant (s) of the displayed value's exponential ease toward its target. */
|
|
29
28
|
const EASE = 0.15
|
|
30
29
|
/** Speed band (%/s) for the displayed value: no crawling at the tail, no leaps on big jumps. */
|
|
31
30
|
const MIN_SPEED = 24
|
|
32
31
|
const MAX_SPEED = 140
|
|
33
32
|
|
|
34
|
-
/**
|
|
35
|
-
* Simulated progress `elapsed` ms after loading started: quick at first, then slowing as it
|
|
36
|
-
* approaches the cap. `tau` (ms) sets the pace — ~63% of the cap is reached after `tau`.
|
|
37
|
-
*/
|
|
38
|
-
export function trickleProgress(elapsed: number, tau: number): number {
|
|
39
|
-
return TRICKLE_CAP * (1 - Math.exp(-Math.max(elapsed, 0) / tau))
|
|
40
|
-
}
|
|
41
|
-
|
|
42
33
|
/**
|
|
43
34
|
* Advances `current` toward `target` over `dt` seconds. Eases out as the gap closes, within the
|
|
44
35
|
* speed band, and never moves backwards or past `target`. A rAF timestamp can precede the
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
|
|
3
|
+
// @nuxt/eslint is an optional peer: load it outside production only when the app has it
|
|
4
|
+
// installed, otherwise a consumer without it fails module resolution in dev/test builds.
|
|
5
|
+
const lintModules: string[] = []
|
|
6
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
7
|
+
try {
|
|
8
|
+
createRequire(import.meta.url).resolve('@nuxt/eslint')
|
|
9
|
+
lintModules.push('@nuxt/eslint')
|
|
10
|
+
} catch {}
|
|
11
|
+
}
|
|
12
|
+
|
|
1
13
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
2
14
|
export default defineNuxtConfig({
|
|
3
15
|
$meta: {
|
|
@@ -15,7 +27,7 @@ export default defineNuxtConfig({
|
|
|
15
27
|
|
|
16
28
|
// Base modules (always loaded)
|
|
17
29
|
modules: [
|
|
18
|
-
...
|
|
30
|
+
...lintModules,
|
|
19
31
|
'@nuxt/ui',
|
|
20
32
|
// '@nuxt/image'
|
|
21
33
|
'@vueuse/nuxt',
|
|
@@ -27,24 +39,12 @@ export default defineNuxtConfig({
|
|
|
27
39
|
|
|
28
40
|
css: ['#layers/core/app/assets/css/core.css'],
|
|
29
41
|
|
|
30
|
-
|
|
31
|
-
enabled: true,
|
|
32
|
-
|
|
33
|
-
vscode: {
|
|
34
|
-
enabled: true,
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
|
|
38
|
-
// Force light mode for consistent appearance
|
|
42
|
+
// Follow the OS colour scheme; light when it can't be detected
|
|
39
43
|
colorMode: {
|
|
40
44
|
preference: 'system',
|
|
41
45
|
fallback: 'light',
|
|
42
46
|
},
|
|
43
47
|
|
|
44
|
-
future: {
|
|
45
|
-
compatibilityVersion: 5,
|
|
46
|
-
},
|
|
47
|
-
|
|
48
48
|
compatibilityDate: '2026-01-24',
|
|
49
49
|
|
|
50
50
|
typescript: {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// eslint-disable-next-line no-restricted-imports -- the fallback below is the one place it belongs
|
|
2
|
+
import { useAppConfig as useNitroAppConfig } from 'nitropack/runtime'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The app's merged app.config, read on the server.
|
|
6
|
+
*
|
|
7
|
+
* Nuxt hands Nitro its merged `useAppConfig` as an auto-import, so this leaves `useAppConfig`
|
|
8
|
+
* unimported on purpose. With `experimental.nitroAutoImports: false` (Nuxt 5's default) nothing
|
|
9
|
+
* is injected and Nuxt can't evaluate app.config.ts in Nitro at all - importing `nuxt/server`
|
|
10
|
+
* there crashes the server - so fall back to Nitro's own app config, which is empty.
|
|
11
|
+
*/
|
|
12
|
+
export function readServerAppConfig(): Record<string, unknown> {
|
|
13
|
+
// `typeof` doesn't throw for an identifier that was never injected
|
|
14
|
+
return typeof useAppConfig === 'function'
|
|
15
|
+
? (useAppConfig() as Record<string, unknown>)
|
|
16
|
+
: (useNitroAppConfig() as Record<string, unknown>)
|
|
17
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { FormSubmitEvent } from '@nuxt/ui'
|
|
3
3
|
|
|
4
|
-
import { contactSchema, type ContactData } from '
|
|
4
|
+
import { contactSchema, type ContactData } from '../../../shared/utils/contact-schema'
|
|
5
5
|
|
|
6
6
|
const emit = defineEmits<{
|
|
7
7
|
submit: [data: ContactData]
|