kmcom-nuxt-layers 6.2.1 → 6.3.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 (64) hide show
  1. package/docs/MIGRATION-v6.3.0.md +234 -0
  2. package/docs/MIGRATION.md +2 -0
  3. package/docs/component-levels.md +116 -0
  4. package/layers/content/app/components/Blog/ItemPage.vue +2 -7
  5. package/layers/content/app/components/Blog/List.vue +9 -2
  6. package/layers/content/app/components/Blog/Page.vue +9 -19
  7. package/layers/content/app/components/Gallery/Detail.vue +1 -0
  8. package/layers/content/app/components/Gallery/Grid.vue +9 -2
  9. package/layers/content/app/components/Gallery/ImageDetail.vue +0 -6
  10. package/layers/content/app/components/Gallery/ImagePage.vue +10 -6
  11. package/layers/content/app/components/Gallery/Page.vue +9 -19
  12. package/layers/content/app/components/Gallery/SlugPage.vue +2 -7
  13. package/layers/content/app/components/NuxtContent/Detail.vue +20 -11
  14. package/layers/content/app/components/NuxtContent/IndexTemplate.vue +21 -0
  15. package/layers/content/app/components/NuxtContent/List.vue +6 -1
  16. package/layers/content/app/components/Portfolio/ColorPalette.vue +2 -2
  17. package/layers/content/app/components/Portfolio/ItemPage.vue +2 -6
  18. package/layers/content/app/components/Portfolio/List.vue +19 -8
  19. package/layers/content/app/components/Portfolio/Page.vue +9 -19
  20. package/layers/content/app/composables/useContentItemSeo.ts +19 -0
  21. package/layers/content/app/composables/useContentSection.ts +9 -0
  22. package/layers/content/app/utils/galleryImageMeta.test.ts +30 -0
  23. package/layers/content/app/utils/galleryImageMeta.ts +11 -0
  24. package/layers/content/nuxt.config.ts +1 -1
  25. package/layers/core/app/assets/css/core.css +5 -11
  26. package/layers/core/app/assets/css/tokens/z-index.css +16 -0
  27. package/layers/core/app/components/DiagnosticsPage.vue +41 -41
  28. package/layers/core/app/components/LoadingScreen.vue +2 -0
  29. package/layers/delivery/feeds/app/components/Feeds/Index.vue +36 -75
  30. package/layers/delivery/feeds/app/components/Feeds/RouteCard.vue +6 -8
  31. package/layers/design-system/theming/nuxt.config.ts +1 -3
  32. package/layers/design-system/visual/app/components/Base/Modal.vue +34 -86
  33. package/layers/design-system/visual/app/components/HoverDimGroup.vue +46 -0
  34. package/layers/design-system/visual/app/components/Progress/Bar.vue +9 -29
  35. package/layers/motion/animations/app/components/Motion/Cursor.vue +1 -1
  36. package/layers/motion/animations/app/components/Motion/Marquee.vue +5 -0
  37. package/layers/motion/animations/app/components/Motion/MarqueeText.vue +2 -1
  38. package/layers/motion/animations/app/components/Motion/Staggered.test.ts +40 -0
  39. package/layers/motion/animations/app/components/Motion/Staggered.vue +37 -42
  40. package/layers/motion/animations/app/components/Motion/TextReveal.vue +8 -1
  41. package/layers/motion/animations/app/components/Motion/VelocityEffect.vue +6 -0
  42. package/layers/motion/animations/app/composables/useCountUp.ts +6 -0
  43. package/layers/motion/animations/app/composables/useCursorFollower.ts +4 -1
  44. package/layers/motion/animations/app/utils/pointerMotion.ts +7 -2
  45. package/layers/motion/scroll/app/components/Motion/HorizontalScroll.vue +9 -0
  46. package/layers/motion/scroll/app/components/Motion/Parallax.vue +13 -12
  47. package/layers/motion/scroll/app/components/Motion/PinnedSection.vue +16 -9
  48. package/layers/motion/scroll/app/composables/useReducedMotion.test.ts +23 -0
  49. package/layers/motion/scroll/app/composables/useReducedMotion.ts +14 -0
  50. package/layers/motion/scroll/app/utils/parallax.test.ts +24 -0
  51. package/layers/motion/scroll/app/utils/parallax.ts +16 -0
  52. package/layers/motion/scroll/nuxt.config.ts +1 -1
  53. package/layers/structure/layout/app/components/Layout/Grid/Debug.vue +1 -1
  54. package/layers/structure/layout/app/components/Layout/Page/index.vue +9 -10
  55. package/layers/structure/navigation/app/components/Links/Group.vue +12 -38
  56. package/layers/structure/navigation/app/components/Links/Named.vue +9 -0
  57. package/layers/structure/navigation/app/components/Mast/Footer.vue +1 -1
  58. package/layers/structure/navigation/app/components/Mast/Nav.vue +3 -1
  59. package/layers/structure/navigation/app/components/Mast/NavModal.vue +34 -39
  60. package/layers/structure/navigation/app/components/Site/Subtitle.vue +10 -0
  61. package/layers/structure/navigation/app/components/Site/Title.vue +8 -0
  62. package/layers/structure/navigation/nuxt.config.ts +7 -1
  63. package/package.json +1 -1
  64. /package/layers/{design-system/theming/app/assets/css/tokens.css → core/app/assets/css/tokens/elevation-motion.css} +0 -0
@@ -0,0 +1,234 @@
1
+ # Migration Guide — v6.2.x → v6.3.0
2
+
3
+ **Audience:** an AI coding agent (or developer) updating a downstream Nuxt app that consumes these
4
+ layers.
5
+ **What changed:** the 2026-10 atomic design audit — components moved to the right level, tokens
6
+ moved into `core`, motion respects reduced motion, modals use `UModal`, and organisms no longer set
7
+ page meta.
8
+
9
+ Work top to bottom. Each change has a **Detect** step (how to tell whether it applies) and an
10
+ **Apply** step. Skip anything Detect doesn't find. Run the **Verify** checklist before declaring
11
+ done. An app component with the same name as a layer component overrides it, so a change to a layer
12
+ component you've already overridden doesn't apply to you.
13
+
14
+ ## 0. Preconditions
15
+
16
+ - Update the package to `^6.3.0` and reinstall.
17
+ - Run `npx nuxt prepare` so the new auto-imports are typed.
18
+
19
+ ---
20
+
21
+ ## Required changes
22
+
23
+ ### 1. `MastNav` no longer positions itself
24
+
25
+ The hamburger button dropped `fixed top-0 right-0`. Where it sits is now the parent's decision.
26
+
27
+ **Detect:** the app renders `<MastNav` and has no `app/components/Mast/Nav.vue` of its own.
28
+
29
+ **Apply:** put the old positioning back where you render it, or position it differently:
30
+
31
+ ```vue
32
+ <!-- BEFORE -->
33
+ <MastNav />
34
+
35
+ <!-- AFTER -->
36
+ <MastNav class="fixed top-0 right-0" />
37
+ ```
38
+
39
+ ### 2. Detail organisms no longer set page meta
40
+
41
+ `NuxtContentDetail` and `GalleryImageDetail` used to call `useSeoMeta`. That moved up into the page
42
+ components (`BlogItemPage`, `PortfolioItemPage`, `GallerySlugPage`, `GalleryImagePage`), so an
43
+ organism never decides page meta.
44
+
45
+ **Detect:** grep your own pages for `<NuxtContentDetail` or `<GalleryImageDetail`. If you only use
46
+ the layer's `*Page` components, nothing changes for you.
47
+
48
+ **Apply:** set the meta in your page. For a content item, `useContentItemSeo` does what the
49
+ organism used to do:
50
+
51
+ ```vue
52
+ <script setup lang="ts">
53
+ const slug = useRoute().params.slug as string
54
+ // Registers title/description/og:image, then fetches. Call it before any other await.
55
+ await useContentItemSeo('blog', slug)
56
+ </script>
57
+
58
+ <template>
59
+ <NuxtContentDetail collection="blog" :slug />
60
+ </template>
61
+ ```
62
+
63
+ For a gallery image, `galleryImageMeta(item, index)` (auto-imported util) returns the
64
+ `{ title, description, ogImage }` the organism used to set. Pass it to `useSeoMeta` with getters,
65
+ as `GalleryImagePage` does.
66
+
67
+ ### 3. `BaseModal` and `MastNavModal` are built on `UModal`
68
+
69
+ Same props, emits and slots (`open`, `title`, `description`, `size`, `update:open`, `close`,
70
+ `#header`, default, `#footer="{ dismiss }"`). `UModal` now handles the dialog role, focus trap,
71
+ Escape key and teleport. What can break:
72
+
73
+ | Before | After |
74
+ | ------------------------------------------------------------------- | --------------------------------------------------------------------------- |
75
+ | Rendered in place | Teleported to `<body>`: scoped or parent-selector CSS no longer reaches it |
76
+ | `#header` replaced only the title block, so the close button stayed | `#header` replaces the whole header, close button included, so add your own |
77
+ | `.base-modal-*` / `.nav-modal-*` transition classes | Gone: `UModal` runs its own transitions |
78
+ | `z-50` (`BaseModal`), `useZIndex('modal')` (`MastNavModal`) | `z-(--z-modal)` = 400 |
79
+ | `size="full"` = `max-w-full` | `size="full"` = `UModal` `fullscreen` |
80
+
81
+ **Detect:** grep for `<BaseModal` with a `#header` slot, CSS that targets `base-modal-` or
82
+ `nav-modal-`, and styles that reach into the modal from a parent. If you ship your own
83
+ `app/components/Mast/NavModal.vue`, your override still runs and its `nav-modal` CSS still applies.
84
+
85
+ **Apply:** put a close button inside a custom `#header`. Move modal styling to the `ui`/`class`
86
+ props or global CSS. Delete dead transition CSS.
87
+
88
+ ### 4. Cursor and grid-debug overlays left `z-9999`
89
+
90
+ `MotionCursor` now uses `z-(--z-cursor)` = 600, and `LayoutGridDebug` uses `z-(--z-debug)` = 700.
91
+ Both still sit above the layout scale (header 100 … toast 500).
92
+
93
+ **Detect:** the app has its own elements above `z-index: 600` that should stay _under_ the custom
94
+ cursor, or it relied on the cursor beating something at `z-9999`.
95
+
96
+ **Apply:** move those elements onto the scale (`z-(--z-modal)`, `z-(--z-toast)`, …), or raise
97
+ `--z-cursor` in your own `:root`.
98
+
99
+ ### 5. `NuxtContentList` `status` is a typed union
100
+
101
+ `status` changed from `string` to `'idle' | 'pending' | 'success' | 'error'`, and `'error'` now
102
+ renders an error alert instead of the empty message.
103
+
104
+ **Detect:** typecheck errors on `<NuxtContentList :status=…>`.
105
+
106
+ **Apply:** pass the `status` from `useAsyncData`/`useFetch` (already that union). To change the error
107
+ UI, use the new `error-message` prop or the `#error` slot.
108
+
109
+ ---
110
+
111
+ ## Recommended — deprecated components
112
+
113
+ Each one still works in v6.3 and prints a dev-only console warning. They'll be removed in the next
114
+ major.
115
+
116
+ ### 6. `LinksGroup` → `HoverDimGroup`
117
+
118
+ Renamed and moved to `visual`: it dims any children, not just links. The `tag` prop is unchanged.
119
+
120
+ **Detect:** grep for `<LinksGroup`.
121
+
122
+ **Apply:** rename the tag.
123
+
124
+ ```vue
125
+ <!-- BEFORE -->
126
+ <LinksGroup tag="nav">…</LinksGroup>
127
+ <!-- AFTER -->
128
+ <HoverDimGroup tag="nav">…</HoverDimGroup>
129
+ ```
130
+
131
+ ### 7. `LinksNamed` → `ULink`
132
+
133
+ **Detect:** grep for `<LinksNamed`.
134
+
135
+ **Apply:**
136
+
137
+ ```vue
138
+ <!-- BEFORE -->
139
+ <LinksNamed name="blog-slug" :params="{ slug }" color="primary">Read</LinksNamed>
140
+ <!-- AFTER -->
141
+ <ULink :to="{ name: 'blog-slug', params: { slug } }" class="text-primary">Read</ULink>
142
+ ```
143
+
144
+ ### 8. `SiteTitle` / `SiteSubtitle` → `useSite()`
145
+
146
+ **Detect:** grep for `<SiteTitle` or `<SiteSubtitle`.
147
+
148
+ **Apply:**
149
+
150
+ ```vue
151
+ <script setup lang="ts">
152
+ const site = useSite()
153
+ </script>
154
+
155
+ <template>
156
+ <span v-for="(word, i) in site.titleWords" :key="i" class="site-word">{{ word }}</span>
157
+ </template>
158
+ ```
159
+
160
+ ---
161
+
162
+ ## Behaviour changes — check, usually no code
163
+
164
+ ### 9. Motion respects reduced motion
165
+
166
+ When the OS asks for reduced motion, or the ThemePicker motion setting is `on`, these now render
167
+ static or paused: `MotionStaggered`, `MotionTextReveal`, `MotionCountUp` (jumps to the end value),
168
+ `MotionMarquee`, `MotionMarqueeText`, `MotionVelocityEffect`, `MotionParallax`,
169
+ `MotionPinnedSection` (items shown, no pin), `MotionHorizontalScroll` (native horizontal scroll),
170
+ the pointer-follow effects (`MotionTilt`, `MotionMagnetic`) and the cursor follower.
171
+
172
+ **Check:** e2e tests that emulate `prefers-reduced-motion: reduce` and still expect motion.
173
+
174
+ ### 10. `MotionStaggered` now actually reveals its children
175
+
176
+ Before, `ref`/`class` on the `<slot>` never reached the DOM, so items stayed at `opacity: 0` for
177
+ good. It now staggers its rendered **element** children (text nodes aren't animated) and adds the
178
+ `transitions` layer's `animate-<animation>` class.
179
+
180
+ **Check:** remove any workaround CSS that forced `.motion-staggered > *` visible.
181
+
182
+ ### 11. `LayoutPage` sets meta with `useSeoMeta`
183
+
184
+ It used `useHead`. Now `og:title` is always set, and a page's own `useSeoMeta` overrides
185
+ `LayoutPage`'s values instead of adding duplicate tags.
186
+
187
+ **Check:** nothing to do, unless you relied on the duplicate tags.
188
+
189
+ ### 12. Wider `extends`
190
+
191
+ `content` now extends `layout` and `visual`, and `scroll` and `navigation` now extend `visual`. An
192
+ app that loads `content` or `navigation` therefore also gets the `layout`/`visual` components and
193
+ composables auto-imported. App components with the same name still win.
194
+
195
+ **Check:** after `nuxt prepare`, look for auto-import name clashes. The likeliest new one is
196
+ `useReducedMotion` (scroll layer), which `motion-v` also exports.
197
+
198
+ ### 13. Colour and markup tweaks
199
+
200
+ - `Feeds` (index page) and `FeedsRouteCard` use semantic colours (`bg-elevated`, `text-muted`,
201
+ `border-default`). Only the RSS/Atom/JSON format hues stay fixed.
202
+ - `MastFooter` copyright: `text-neutral-300` → `text-muted`.
203
+ - `BlogPage`, `GalleryPage` and `PortfolioPage` render through the new `NuxtContentIndexTemplate`
204
+ (`UPageHeader` + `UPageBody` inside `LayoutSection` > `LayoutGridItem`).
205
+
206
+ **Check:** visual regression on the feeds page, the footer and the content index pages, in light and
207
+ dark mode.
208
+
209
+ ---
210
+
211
+ ## New — optional
212
+
213
+ | Addition | Where | Use |
214
+ | ------------------------------------- | ------------------------------------------ | --------------------------------------------------------- |
215
+ | `title`, `description` props | `BlogPage`, `GalleryPage`, `PortfolioPage` | Override the index heading and meta (defaults unchanged) |
216
+ | `items` prop | `BlogList`, `GalleryGrid`, `PortfolioList` | Pass data in. The component only fetches when it's absent |
217
+ | `error-message` prop, `#error` slot | `NuxtContentList` | Custom error state |
218
+ | `NuxtContentIndexTemplate` | content | Heading + body skeleton for your own index pages |
219
+ | `useContentSection(section)` | content | 404 when `contentLayer.sections.<section>` is `false` |
220
+ | `useContentItemSeo(collection, slug)` | content | Fetch an item and set its page meta |
221
+ | `useReducedMotion()` | scroll | `ComputedRef<boolean>`: OS setting + ThemePicker override |
222
+ | `--z-header` … `--z-debug` | core CSS | Stacking scale: `z-(--z-modal)` |
223
+ | Shadow / duration tokens | core CSS | Moved from `theming`, so they work without it |
224
+ | `docs/component-levels.md` | repo | Atomic level of every component |
225
+
226
+ ## Verify
227
+
228
+ - [ ] `npx nuxt prepare && npx nuxi typecheck` is clean.
229
+ - [ ] Nav button is where it was (§1).
230
+ - [ ] Each content item page has the right `<title>` and `og:image` (§2).
231
+ - [ ] Modals open, close on Escape and backdrop, and custom headers have a close button (§3).
232
+ - [ ] The cursor sits above modals and toasts (§4).
233
+ - [ ] No `[navigation] … is deprecated` warnings in the dev console (§6–8).
234
+ - [ ] With reduced motion on, nothing moves and all content is visible (§9–10).
package/docs/MIGRATION.md CHANGED
@@ -1,3 +1,5 @@
1
+ > **Upgrading to v6.3.0?** See [`MIGRATION-v6.3.0.md`](./MIGRATION-v6.3.0.md). The guides below cover older versions.
2
+
1
3
  # Agent Migration Guide — Update an app to nuxt-layers v2.2.x
2
4
 
3
5
  **Audience:** an AI coding agent (or developer) updating a downstream Nuxt app that consumes these layers.
@@ -0,0 +1,116 @@
1
+ # Component levels
2
+
3
+ Atomic design level for every UI component under `layers/**/app/components`. Folders stay by
4
+ domain (`Motion/`, `Layout/`); this table is the level record. `tests/vitest/component-levels.test.ts`
5
+ fails when a component is added without a row here, or a row points at a deleted file.
6
+
7
+ Dependencies point down: atom < molecule < organism < template < page. **behaviour** sits beside
8
+ atom: a slot wrapper that adds behaviour (motion, hover, error capture) and renders whatever it is
9
+ given, so it may wrap any level. `render/shader` pipeline nodes are a render graph, not UI, and are
10
+ left out.
11
+
12
+ | Level | Meaning |
13
+ | --------- | ------------------------------------------------ |
14
+ | atom | One element or vendor primitive |
15
+ | behaviour | Slot wrapper adding behaviour, no markup opinion |
16
+ | molecule | A few atoms doing one job |
17
+ | organism | A standalone section |
18
+ | template | Page-type skeleton: props and slots only |
19
+ | page | Binds route and data, fills one template |
20
+
21
+ | Component | Level |
22
+ | -------------------------------------------------------------------------- | --------- |
23
+ | `layers/content/app/components/Blog/Article.vue` | organism |
24
+ | `layers/content/app/components/Blog/ItemPage.vue` | page |
25
+ | `layers/content/app/components/Blog/List.vue` | organism |
26
+ | `layers/content/app/components/Blog/Page.vue` | page |
27
+ | `layers/content/app/components/Gallery/Detail.vue` | organism |
28
+ | `layers/content/app/components/Gallery/Grid.vue` | organism |
29
+ | `layers/content/app/components/Gallery/ImageDetail.vue` | organism |
30
+ | `layers/content/app/components/Gallery/ImagePage.vue` | page |
31
+ | `layers/content/app/components/Gallery/Lightbox.vue` | organism |
32
+ | `layers/content/app/components/Gallery/Page.vue` | page |
33
+ | `layers/content/app/components/Gallery/SlugPage.vue` | page |
34
+ | `layers/content/app/components/NuxtContent/Detail.vue` | template |
35
+ | `layers/content/app/components/NuxtContent/IndexTemplate.vue` | template |
36
+ | `layers/content/app/components/NuxtContent/List.vue` | organism |
37
+ | `layers/content/app/components/NuxtContent/Surround.vue` | molecule |
38
+ | `layers/content/app/components/NuxtContent/Toc.vue` | molecule |
39
+ | `layers/content/app/components/Portfolio/ColorPalette.vue` | organism |
40
+ | `layers/content/app/components/Portfolio/Detail.vue` | organism |
41
+ | `layers/content/app/components/Portfolio/ItemPage.vue` | page |
42
+ | `layers/content/app/components/Portfolio/List.vue` | organism |
43
+ | `layers/content/app/components/Portfolio/Page.vue` | page |
44
+ | `layers/content/app/components/Portfolio/Typography.vue` | organism |
45
+ | `layers/content/app/components/content/Figure.vue` | molecule |
46
+ | `layers/core/app/components/AppContainer.vue` | atom |
47
+ | `layers/core/app/components/DiagnosticsPage.vue` | organism |
48
+ | `layers/core/app/components/Element.vue` | atom |
49
+ | `layers/core/app/components/ElementTw.vue` | atom |
50
+ | `layers/core/app/components/ErrorBoundary.vue` | behaviour |
51
+ | `layers/core/app/components/LoadingScreen.vue` | organism |
52
+ | `layers/data/forms/app/components/Form/Contact.vue` | organism |
53
+ | `layers/data/forms/app/components/Form/Field.vue` | molecule |
54
+ | `layers/delivery/feeds/app/components/Feeds/Index.vue` | organism |
55
+ | `layers/delivery/feeds/app/components/Feeds/RouteCard.vue` | molecule |
56
+ | `layers/delivery/scripts/app/components/BaselineStatus.vue` | atom |
57
+ | `layers/design-system/theming/app/components/ThemePicker/AccentButton.vue` | atom |
58
+ | `layers/design-system/theming/app/components/ThemePicker/Colors.vue` | molecule |
59
+ | `layers/design-system/theming/app/components/ThemePicker/Menu.vue` | organism |
60
+ | `layers/design-system/theming/app/components/ThemePicker/MenuButton.vue` | atom |
61
+ | `layers/design-system/typography/app/components/Typography/CodeBlock.vue` | atom |
62
+ | `layers/design-system/typography/app/components/Typography/Headline.vue` | atom |
63
+ | `layers/design-system/typography/app/components/Typography/QuoteBlock.vue` | molecule |
64
+ | `layers/design-system/typography/app/components/Typography/RepeatText.vue` | atom |
65
+ | `layers/design-system/typography/app/components/Typography/TextStroke.vue` | atom |
66
+ | `layers/design-system/typography/app/components/Typography/index.vue` | atom |
67
+ | `layers/design-system/visual/app/components/Accent/Blob.vue` | atom |
68
+ | `layers/design-system/visual/app/components/Accent/Scene.vue` | molecule |
69
+ | `layers/design-system/visual/app/components/Base/Modal.vue` | molecule |
70
+ | `layers/design-system/visual/app/components/Gradient/Background.vue` | atom |
71
+ | `layers/design-system/visual/app/components/Gradient/Text.vue` | atom |
72
+ | `layers/design-system/visual/app/components/HoverDimGroup.vue` | behaviour |
73
+ | `layers/design-system/visual/app/components/Media/Picture.vue` | atom |
74
+ | `layers/design-system/visual/app/components/Progress/Bar.vue` | atom |
75
+ | `layers/design-system/visual/app/components/Progress/Circular.vue` | atom |
76
+ | `layers/design-system/visual/app/components/Tint/Overlay.vue` | atom |
77
+ | `layers/motion/animations/app/components/Motion/CountUp.vue` | atom |
78
+ | `layers/motion/animations/app/components/Motion/Cursor.vue` | atom |
79
+ | `layers/motion/animations/app/components/Motion/Magnetic.vue` | behaviour |
80
+ | `layers/motion/animations/app/components/Motion/Marquee.vue` | behaviour |
81
+ | `layers/motion/animations/app/components/Motion/MarqueeText.vue` | molecule |
82
+ | `layers/motion/animations/app/components/Motion/Staggered.vue` | behaviour |
83
+ | `layers/motion/animations/app/components/Motion/TextReveal.vue` | atom |
84
+ | `layers/motion/animations/app/components/Motion/Tilt.vue` | behaviour |
85
+ | `layers/motion/animations/app/components/Motion/VelocityEffect.vue` | behaviour |
86
+ | `layers/motion/scroll/app/components/Motion/HorizontalScroll.vue` | behaviour |
87
+ | `layers/motion/scroll/app/components/Motion/Parallax.vue` | behaviour |
88
+ | `layers/motion/scroll/app/components/Motion/PinnedSection.vue` | behaviour |
89
+ | `layers/motion/scroll/app/components/Motion/ScrollLink.vue` | atom |
90
+ | `layers/motion/scroll/app/components/Motion/ScrollProgress.vue` | atom |
91
+ | `layers/motion/scroll/app/components/Motion/ScrollScene.vue` | behaviour |
92
+ | `layers/motion/scroll/app/components/Motion/ScrollStats.vue` | molecule |
93
+ | `layers/motion/scroll/app/components/Motion/ScrollStep.vue` | behaviour |
94
+ | `layers/motion/transitions/app/components/Motion/Transition.vue` | behaviour |
95
+ | `layers/render/canvas/app/components/ShaderCanvas.vue` | atom |
96
+ | `layers/structure/layout/app/components/HStack.vue` | atom |
97
+ | `layers/structure/layout/app/components/Layout/Grid/Debug.vue` | atom |
98
+ | `layers/structure/layout/app/components/Layout/Grid/Item.vue` | atom |
99
+ | `layers/structure/layout/app/components/Layout/Main.vue` | template |
100
+ | `layers/structure/layout/app/components/Layout/Page/Header.vue` | organism |
101
+ | `layers/structure/layout/app/components/Layout/Page/index.vue` | template |
102
+ | `layers/structure/layout/app/components/Layout/Section/Gallery.vue` | organism |
103
+ | `layers/structure/layout/app/components/Layout/Section/Hero.vue` | organism |
104
+ | `layers/structure/layout/app/components/Layout/Section/Split.vue` | organism |
105
+ | `layers/structure/layout/app/components/Layout/Section/index.vue` | organism |
106
+ | `layers/structure/layout/app/components/Spacer.vue` | atom |
107
+ | `layers/structure/layout/app/components/VStack.vue` | atom |
108
+ | `layers/structure/layout/app/components/ZStack.vue` | atom |
109
+ | `layers/structure/navigation/app/components/Links/Group.vue` | behaviour |
110
+ | `layers/structure/navigation/app/components/Links/Named.vue` | atom |
111
+ | `layers/structure/navigation/app/components/Mast/Footer.vue` | organism |
112
+ | `layers/structure/navigation/app/components/Mast/Header.vue` | organism |
113
+ | `layers/structure/navigation/app/components/Mast/Nav.vue` | organism |
114
+ | `layers/structure/navigation/app/components/Mast/NavModal.vue` | organism |
115
+ | `layers/structure/navigation/app/components/Site/Subtitle.vue` | atom |
116
+ | `layers/structure/navigation/app/components/Site/Title.vue` | atom |
@@ -1,15 +1,10 @@
1
1
  <script setup lang="ts">
2
- import type { ContentLayerConfig } from '#layers/content/app/types/content'
3
-
4
2
  const { slug } = defineProps<{
5
3
  slug: string
6
4
  }>()
7
5
 
8
- const appConfig = useAppConfig()
9
- const contentLayer = appConfig.contentLayer as ContentLayerConfig | undefined
10
- if (contentLayer?.sections?.blog === false) {
11
- throw createError({ statusCode: 404, statusMessage: 'Not Found' })
12
- }
6
+ useContentSection('blog')
7
+ await useContentItemSeo('blog', slug)
13
8
  </script>
14
9
 
15
10
  <template>
@@ -1,11 +1,18 @@
1
1
  <script setup lang="ts">
2
+ import type { BlogCollectionItem } from '@nuxt/content'
3
+
2
4
  import type { BlogQueryOptions } from '../../types/content'
3
5
 
4
- const { options = {} } = defineProps<{
6
+ const { items = undefined, options = {} } = defineProps<{
7
+ items?: BlogCollectionItem[]
5
8
  options?: BlogQueryOptions
6
9
  }>()
7
10
 
8
- const { data: posts, status } = await useBlogPosts(options)
11
+ // Fetch only when the parent didn't pass items (e.g. a related-items list it already has).
12
+ // ponytail: decided once at setup — toggling `items` after mount won't start or stop the fetch
13
+ const fetched = items ? undefined : await useBlogPosts(options)
14
+ const posts = computed(() => items ?? fetched?.data.value)
15
+ const status = computed(() => (items ? 'success' : (fetched?.status.value ?? 'idle')))
9
16
  </script>
10
17
 
11
18
  <template>
@@ -1,25 +1,15 @@
1
1
  <script setup lang="ts">
2
- import type { ContentLayerConfig } from '#layers/content/app/types/content'
2
+ const { title = 'Blog', description = 'Articles and posts' } = defineProps<{
3
+ title?: string
4
+ description?: string
5
+ }>()
3
6
 
4
- const appConfig = useAppConfig()
5
- const contentLayer = appConfig.contentLayer as ContentLayerConfig | undefined
6
- if (contentLayer?.sections?.blog === false) {
7
- throw createError({ statusCode: 404, statusMessage: 'Not Found' })
8
- }
9
-
10
- useSeoMeta({
11
- title: 'Blog',
12
- description: 'Articles and posts',
13
- })
7
+ useContentSection('blog')
8
+ useSeoMeta({ title: () => title, description: () => description })
14
9
  </script>
15
10
 
16
11
  <template>
17
- <LayoutSection>
18
- <LayoutGridItem>
19
- <UPageHeader title="Blog" description="Articles and posts" />
20
- <UPageBody>
21
- <BlogList />
22
- </UPageBody>
23
- </LayoutGridItem>
24
- </LayoutSection>
12
+ <NuxtContentIndexTemplate :title :description>
13
+ <BlogList />
14
+ </NuxtContentIndexTemplate>
25
15
  </template>
@@ -66,6 +66,7 @@
66
66
  </div>
67
67
  </NuxtLink>
68
68
  <button
69
+ :aria-label="`View ${img.alt || 'image'} larger`"
69
70
  class="absolute top-2 right-2 z-10 cursor-pointer rounded-full bg-black/50 p-1.5 text-white opacity-0 transition-opacity duration-300 group-hover:opacity-100 hover:bg-black/70"
70
71
  @click.stop="() => openLightbox(asGallery(item).images!, idx)"
71
72
  >
@@ -1,11 +1,18 @@
1
1
  <script setup lang="ts">
2
+ import type { GalleryCollectionItem } from '@nuxt/content'
3
+
2
4
  import type { GalleryQueryOptions } from '../../types/content'
3
5
 
4
- const { options = {} } = defineProps<{
6
+ const { items: passed = undefined, options = {} } = defineProps<{
7
+ items?: GalleryCollectionItem[]
5
8
  options?: GalleryQueryOptions
6
9
  }>()
7
10
 
8
- const { data: items, status } = await useGalleryItems(options)
11
+ // Fetch only when the parent didn't pass items (e.g. a related-items list it already has).
12
+ // ponytail: decided once at setup — toggling `items` after mount won't start or stop the fetch
13
+ const fetched = passed ? undefined : await useGalleryItems(options)
14
+ const items = computed(() => passed ?? fetched?.data.value)
15
+ const status = computed(() => (passed ? 'success' : (fetched?.status.value ?? 'idle')))
9
16
  </script>
10
17
 
11
18
  <template>
@@ -13,12 +13,6 @@
13
13
  const totalImages = computed(() => item.value?.images?.length ?? 0)
14
14
  const hasPrevious = computed(() => index > 0)
15
15
  const hasNext = computed(() => index < totalImages.value - 1)
16
-
17
- useSeoMeta({
18
- title: () => image.value?.title || image.value?.alt || `Image ${index + 1}`,
19
- description: () => image.value?.caption || item.value?.title,
20
- ogImage: () => image.value?.src,
21
- })
22
16
  </script>
23
17
 
24
18
  <template>
@@ -1,16 +1,20 @@
1
1
  <script setup lang="ts">
2
- import type { ContentLayerConfig } from '#layers/content/app/types/content'
2
+ import type { GalleryCollectionItem } from '@nuxt/content'
3
3
 
4
4
  const { slug, index } = defineProps<{
5
5
  slug: string
6
6
  index: number
7
7
  }>()
8
8
 
9
- const appConfig = useAppConfig()
10
- const contentLayer = appConfig.contentLayer as ContentLayerConfig | undefined
11
- if (contentLayer?.sections?.gallery === false) {
12
- throw createError({ statusCode: 404, statusMessage: 'Not Found' })
13
- }
9
+ useContentSection('gallery')
10
+
11
+ const { data } = await useCollectionItem('gallery', slug)
12
+ const meta = computed(() => galleryImageMeta(data.value as GalleryCollectionItem | null, index))
13
+ useSeoMeta({
14
+ title: () => meta.value.title,
15
+ description: () => meta.value.description,
16
+ ogImage: () => meta.value.ogImage,
17
+ })
14
18
  </script>
15
19
 
16
20
  <template>
@@ -1,25 +1,15 @@
1
1
  <script setup lang="ts">
2
- import type { ContentLayerConfig } from '#layers/content/app/types/content'
2
+ const { title = 'Gallery', description = 'Photo collections and visual work' } = defineProps<{
3
+ title?: string
4
+ description?: string
5
+ }>()
3
6
 
4
- const appConfig = useAppConfig()
5
- const contentLayer = appConfig.contentLayer as ContentLayerConfig | undefined
6
- if (contentLayer?.sections?.gallery === false) {
7
- throw createError({ statusCode: 404, statusMessage: 'Not Found' })
8
- }
9
-
10
- useSeoMeta({
11
- title: 'Gallery',
12
- description: 'Photo collections and visual work',
13
- })
7
+ useContentSection('gallery')
8
+ useSeoMeta({ title: () => title, description: () => description })
14
9
  </script>
15
10
 
16
11
  <template>
17
- <LayoutSection>
18
- <LayoutGridItem>
19
- <UPageHeader title="Gallery" description="Photo collections and visual work" />
20
- <UPageBody>
21
- <GalleryGrid />
22
- </UPageBody>
23
- </LayoutGridItem>
24
- </LayoutSection>
12
+ <NuxtContentIndexTemplate :title :description>
13
+ <GalleryGrid />
14
+ </NuxtContentIndexTemplate>
25
15
  </template>
@@ -1,15 +1,10 @@
1
1
  <script setup lang="ts">
2
- import type { ContentLayerConfig } from '#layers/content/app/types/content'
3
-
4
2
  const { slug } = defineProps<{
5
3
  slug: string
6
4
  }>()
7
5
 
8
- const appConfig = useAppConfig()
9
- const contentLayer = appConfig.contentLayer as ContentLayerConfig | undefined
10
- if (contentLayer?.sections?.gallery === false) {
11
- throw createError({ statusCode: 404, statusMessage: 'Not Found' })
12
- }
6
+ useContentSection('gallery')
7
+ await useContentItemSeo('gallery', slug)
13
8
  </script>
14
9
 
15
10
  <template>
@@ -20,23 +20,32 @@
20
20
  const surroundLinks = computed<ContentSurroundLink[] | undefined>(
21
21
  () => surround.value ?? undefined
22
22
  )
23
- const itemImage = computed(() => (item.value as { image?: string } | null)?.image)
24
23
 
25
- useSeoMeta({
26
- title: item.value?.title,
27
- description: item.value?.description,
28
- ogImage: itemImage,
24
+ // Optional props only bound when present (exactOptionalPropertyTypes rejects `undefined`)
25
+ const headerProps = computed(() =>
26
+ item.value?.description ? { description: item.value.description } : {}
27
+ )
28
+ const surroundProps = computed(() =>
29
+ surroundLinks.value?.length ? { surround: surroundLinks.value } : {}
30
+ )
31
+ const tocProps = computed(() => {
32
+ const links = item.value?.body?.toc?.links
33
+ return links ? { links } : {}
29
34
  })
30
35
  </script>
31
36
 
32
37
  <template>
33
38
  <div>
34
39
  <USkeleton v-if="status === 'pending'" class="h-96 w-full" />
40
+ <UAlert
41
+ v-else-if="status === 'error'"
42
+ color="error"
43
+ variant="subtle"
44
+ icon="lucide:circle-alert"
45
+ title="This page couldn't be loaded. Try again shortly."
46
+ />
35
47
  <UPage v-else-if="item">
36
- <UPageHeader
37
- :title="item.title"
38
- v-bind="item.description ? { description: item.description } : {}"
39
- >
48
+ <UPageHeader :title="item.title" v-bind="headerProps">
40
49
  <template v-if="$slots.headline" #headline>
41
50
  <slot name="headline" :item />
42
51
  </template>
@@ -51,11 +60,11 @@
51
60
  <slot name="after-content" :item />
52
61
 
53
62
  <USeparator class="my-8" />
54
- <NuxtContentSurround v-bind="surroundLinks?.length ? { surround: surroundLinks } : {}" />
63
+ <NuxtContentSurround v-bind="surroundProps" />
55
64
  </UPageBody>
56
65
 
57
66
  <template v-if="!hideToc" #right>
58
- <NuxtContentToc v-bind="item.body?.toc?.links ? { links: item.body.toc.links } : {}" />
67
+ <NuxtContentToc v-bind="tocProps" />
59
68
  </template>
60
69
  </UPage>
61
70
  <div v-else class="text-muted py-12 text-center">{{ notFoundMessage }}</div>
@@ -0,0 +1,21 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Content index skeleton — a page header over a list. Props and slots only: fetching, meta
4
+ * and the section guard belong to the *Page component that fills it.
5
+ */
6
+ const { title, description = undefined } = defineProps<{
7
+ title: string
8
+ description?: string
9
+ }>()
10
+ </script>
11
+
12
+ <template>
13
+ <LayoutSection>
14
+ <LayoutGridItem>
15
+ <UPageHeader :title v-bind="description ? { description } : {}" />
16
+ <UPageBody>
17
+ <slot />
18
+ </UPageBody>
19
+ </LayoutGridItem>
20
+ </LayoutSection>
21
+ </template>