kmcom-nuxt-layers 1.3.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/layers/content/app/components/Blog/List.vue +5 -1
  2. package/layers/content/app/components/Gallery/AmbientImage.vue +5 -12
  3. package/layers/content/app/components/Gallery/Detail.vue +8 -6
  4. package/layers/content/app/components/Gallery/Grid.vue +11 -3
  5. package/layers/content/app/components/Portfolio/ColorPalette.vue +1 -4
  6. package/layers/content/app/components/Portfolio/Detail.vue +6 -1
  7. package/layers/content/app/components/Portfolio/List.vue +5 -1
  8. package/layers/content/app/components/content/Figure.vue +1 -7
  9. package/layers/content/nuxt.config.ts +16 -5
  10. package/layers/content/package.json +5 -5
  11. package/layers/core/app/assets/css/main.css +5 -0
  12. package/layers/core/app/composables/useCache.ts +8 -4
  13. package/layers/core/app/composables/useErrorLog.ts +9 -5
  14. package/layers/core/app/composables/useScrollGuard.ts +4 -2
  15. package/layers/core/app/plugins/feature-detection.client.ts +1 -1
  16. package/layers/core/app/plugins/init.ts +2 -1
  17. package/layers/core/app/plugins/scroll-guard.client.ts +4 -1
  18. package/layers/core/app.config.ts +0 -9
  19. package/layers/forms/app/components/Form/Contact.vue +16 -7
  20. package/layers/forms/nuxt.config.ts +18 -0
  21. package/layers/forms/package.json +2 -0
  22. package/layers/layout/app/components/Layout/Container.vue +1 -4
  23. package/layers/layout/app/components/Layout/Grid/Debug.vue +0 -1
  24. package/layers/layout/app/components/Layout/Grid/Item.vue +12 -6
  25. package/layers/layout/app/components/Layout/Main.vue +1 -4
  26. package/layers/layout/app/components/Layout/Page/Container.vue +3 -1
  27. package/layers/layout/app/components/Layout/Page/Header.vue +16 -7
  28. package/layers/layout/app/components/Layout/Section/Grid.vue +1 -4
  29. package/layers/layout/app/components/Layout/Section/Sidebar.vue +6 -1
  30. package/layers/layout/app/components/Layout/Section/Stack.vue +1 -1
  31. package/layers/layout/app/composables/useGridConfig.ts +6 -1
  32. package/layers/motion/app/components/Motion/HorizontalScroll.vue +61 -0
  33. package/layers/motion/app/components/Motion/PinnedSection.vue +77 -0
  34. package/layers/motion/app/components/Motion/ScrollProgress.vue +8 -56
  35. package/layers/motion/app/components/Motion/ScrollScene.vue +121 -0
  36. package/layers/motion/app/components/Motion/ScrollStep.vue +45 -0
  37. package/layers/motion/app/components/Motion/TextReveal.vue +28 -63
  38. package/layers/motion/app/composables/useScrollSteps.ts +41 -0
  39. package/layers/motion/app/composables/useSectionProgress.ts +58 -0
  40. package/layers/motion/app/composables/useSmoothScroll.ts +3 -2
  41. package/layers/motion/app/plugins/locomotive-scroll.client.ts +6 -6
  42. package/layers/motion/nuxt.config.ts +6 -0
  43. package/layers/motion/package.json +2 -1
  44. package/layers/routing/app/app.config.ts +20 -0
  45. package/layers/routing/app/composables/useFeatures.ts +12 -0
  46. package/layers/routing/app/composables/useMaintenance.ts +7 -0
  47. package/layers/routing/app/composables/useRoutingConfig.ts +20 -0
  48. package/layers/routing/app/middleware/01.maintenance.global.ts +6 -0
  49. package/layers/routing/app/middleware/02.governance.global.ts +25 -0
  50. package/layers/routing/app/plugins/feature-flags.client.ts +15 -0
  51. package/layers/routing/app/plugins/scroll-routing.client.ts +21 -0
  52. package/layers/routing/app/types/route-meta.d.ts +6 -0
  53. package/layers/routing/app/types/routing.ts +48 -0
  54. package/layers/routing/nuxt.config.ts +27 -0
  55. package/layers/routing/package.json +6 -0
  56. package/layers/shader/app/components/Preset/ThemeAurora.client.vue +86 -0
  57. package/layers/shader/app/components/Preset/ThemeBubble.client.vue +87 -0
  58. package/layers/shader/app/components/Preset/ThemeFlow.client.vue +86 -0
  59. package/layers/shader/app/components/Preset/ThemeGradient.client.vue +87 -0
  60. package/layers/shader/app/components/Preset/ThemeLavaLamp.client.vue +86 -0
  61. package/layers/shader/app/components/Preset/ThemePlasma.client.vue +86 -0
  62. package/layers/shader/app/components/Preset/ThemeWave.client.vue +86 -0
  63. package/layers/shader/app/components/Shader/Background.client.vue +15 -0
  64. package/layers/shader/app/composables/useAmbientMaterials.ts +306 -0
  65. package/layers/shader/app/composables/useThemeColors.ts +52 -0
  66. package/layers/shader/app/utils/tsl/oklch.ts +12 -6
  67. package/layers/theme/app/assets/css/theme.css +19 -14
  68. package/layers/theme/app/components/ThemePicker/AccentButton.vue +2 -2
  69. package/layers/theme/app/components/ThemePicker/Colors.vue +2 -4
  70. package/layers/theme/app/components/ThemePicker/Menu.vue +4 -13
  71. package/layers/theme/app/components/ThemePicker/MenuButton.vue +1 -7
  72. package/layers/theme/app/composables/useAccentColor.ts +38 -0
  73. package/layers/theme/app/composables/useTheme.ts +14 -0
  74. package/layers/theme/app/composables/useThemeContrast.ts +34 -0
  75. package/layers/theme/app/composables/useThemeMotion.ts +34 -0
  76. package/layers/theme/app/composables/useThemePreferences.ts +3 -156
  77. package/layers/theme/app/composables/useThemeTransparency.ts +41 -0
  78. package/layers/theme/app/plugins/theme.client.ts +3 -3
  79. package/layers/theme/app/types/theme.ts +4 -0
  80. package/layers/theme/nuxt.config.ts +7 -0
  81. package/layers/ui/app/app.config.ts +44 -0
  82. package/layers/ui/app/assets/css/main.css +14 -0
  83. package/layers/ui/app/components/Accent/Blob.vue +29 -0
  84. package/layers/ui/app/components/Accent/Scene.vue +38 -0
  85. package/layers/ui/app/components/Gradient/Background.vue +22 -0
  86. package/layers/ui/app/components/Gradient/Text.vue +22 -0
  87. package/layers/ui/app/components/Progress/Bar.vue +25 -0
  88. package/layers/ui/app/components/Progress/Circular.vue +69 -0
  89. package/layers/ui/app/components/Tint/Overlay.vue +25 -0
  90. package/layers/ui/app/components/Typography/CodeBlock.vue +2 -1
  91. package/layers/ui/app/components/Typography/Headline.vue +2 -1
  92. package/layers/ui/app/components/Typography/QuoteBlock.vue +2 -1
  93. package/layers/ui/app/components/Typography/TextStroke.vue +18 -16
  94. package/layers/ui/app/composables/accent.ts +51 -0
  95. package/layers/ui/app/composables/gradient.ts +79 -0
  96. package/layers/ui/app/composables/tint.ts +20 -0
  97. package/layers/ui/app/types/accent.ts +17 -0
  98. package/layers/ui/app/types/gradient.ts +27 -0
  99. package/layers/ui/app/types/tint.ts +25 -0
  100. package/package.json +37 -31
  101. package/layers/motion/app/utils/gsapAnimations.ts +0 -122
  102. package/layers/ui/app.config.ts +0 -12
@@ -9,7 +9,11 @@ const { data: posts, status } = await useBlogPosts(options)
9
9
  </script>
10
10
 
11
11
  <template>
12
- <NuxtContentList :status="status" :has-items="!!posts?.length" empty-message="No blog posts found">
12
+ <NuxtContentList
13
+ :status="status"
14
+ :has-items="!!posts?.length"
15
+ empty-message="No blog posts found"
16
+ >
13
17
  <UBlogPosts orientation="vertical">
14
18
  <UBlogPost
15
19
  v-for="post in posts"
@@ -20,30 +20,23 @@ const {
20
20
  <!-- Full-viewport ambient glow teleported to body to escape stacking contexts -->
21
21
  <ClientOnly>
22
22
  <Teleport to="body">
23
- <div
24
- class="fixed inset-0 z-0 pointer-events-none overflow-hidden"
25
- aria-hidden="true"
26
- >
23
+ <div class="fixed inset-0 z-0 pointer-events-none overflow-hidden" aria-hidden="true">
27
24
  <img
28
25
  :src="src"
29
26
  alt=""
30
27
  class="absolute inset-0 w-full h-full object-cover blur-[100px] opacity-30 saturate-150 scale-125"
31
28
  />
32
29
  <!-- Gradient fade so it blends into the page bg -->
33
- <div class="absolute inset-0 bg-linear-to-b from-transparent via-transparent to-default" />
30
+ <div
31
+ class="absolute inset-0 bg-linear-to-b from-transparent via-transparent to-default"
32
+ />
34
33
  </div>
35
34
  </Teleport>
36
35
  </ClientOnly>
37
36
 
38
37
  <!-- Main image in normal flow -->
39
38
  <figure class="my-6">
40
- <img
41
- :src="src"
42
- :alt="alt"
43
- :width="width"
44
- :height="height"
45
- class="w-full rounded-lg"
46
- />
39
+ <img :src="src" :alt="alt" :width="width" :height="height" class="w-full rounded-lg" />
47
40
  <figcaption v-if="caption" class="mt-3 text-center text-sm text-muted italic">
48
41
  {{ caption }}
49
42
  </figcaption>
@@ -7,7 +7,10 @@ const lightboxOpen = ref(false)
7
7
  const lightboxIndex = ref(0)
8
8
  const lightboxImages = ref<{ src: string; alt: string; width?: number; height?: number }[]>([])
9
9
 
10
- function openLightbox(images: { src: string; alt: string; width?: number; height?: number }[], index: number) {
10
+ function openLightbox(
11
+ images: { src: string; alt: string; width?: number; height?: number }[],
12
+ index: number
13
+ ) {
11
14
  lightboxImages.value = images
12
15
  lightboxIndex.value = index
13
16
  lightboxOpen.value = true
@@ -36,16 +39,15 @@ function openLightbox(images: { src: string; alt: string; width?: number; height
36
39
  :key="img.src"
37
40
  class="overflow-hidden rounded-lg group relative"
38
41
  >
39
- <NuxtLink
40
- :to="`/gallery/${slug}/${idx}`"
41
- class="block relative overflow-hidden"
42
- >
42
+ <NuxtLink :to="`/gallery/${slug}/${idx}`" class="block relative overflow-hidden">
43
43
  <img
44
44
  :src="img.src"
45
45
  :alt="img.alt"
46
46
  class="w-full h-48 object-cover transition-transform duration-300 group-hover:scale-105"
47
47
  />
48
- <div class="absolute inset-0 bg-primary/60 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
48
+ <div
49
+ class="absolute inset-0 bg-primary/60 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center"
50
+ >
49
51
  <span class="text-white font-semibold text-sm text-center px-3">
50
52
  {{ img.title || img.alt }}
51
53
  </span>
@@ -9,7 +9,11 @@ const { data: items, status } = await useGalleryItems(options)
9
9
  </script>
10
10
 
11
11
  <template>
12
- <NuxtContentList :status="status" :has-items="!!items?.length" empty-message="No gallery items found">
12
+ <NuxtContentList
13
+ :status="status"
14
+ :has-items="!!items?.length"
15
+ empty-message="No gallery items found"
16
+ >
13
17
  <UPageGrid>
14
18
  <NuxtLink
15
19
  v-for="item in items"
@@ -24,7 +28,9 @@ const { data: items, status } = await useGalleryItems(options)
24
28
  :alt="item.images[0].alt"
25
29
  class="w-full h-48 object-cover transition-transform duration-300 group-hover:scale-105"
26
30
  />
27
- <div class="absolute inset-0 bg-primary/60 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
31
+ <div
32
+ class="absolute inset-0 bg-primary/60 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center"
33
+ >
28
34
  <span class="text-white font-semibold text-center px-3">
29
35
  {{ item.title }}
30
36
  </span>
@@ -32,7 +38,9 @@ const { data: items, status } = await useGalleryItems(options)
32
38
  </div>
33
39
  <div class="p-3 space-y-1.5">
34
40
  <p class="font-medium text-highlighted">{{ item.title }}</p>
35
- <p v-if="item.description" class="text-sm text-muted line-clamp-2">{{ item.description }}</p>
41
+ <p v-if="item.description" class="text-sm text-muted line-clamp-2">
42
+ {{ item.description }}
43
+ </p>
36
44
  <div class="flex flex-wrap items-center gap-2">
37
45
  <span v-if="item.images?.length" class="text-xs text-muted">
38
46
  {{ item.images.length }} image{{ item.images.length === 1 ? '' : 's' }}
@@ -19,10 +19,7 @@ function contrastColor(hex: string): string {
19
19
  <div v-if="colors.length" class="space-y-6">
20
20
  <h3 class="text-2xl font-semibold tracking-tight text-highlighted">Palette</h3>
21
21
  <LinksGroup class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3">
22
- <div
23
- v-for="color in colors"
24
- :key="color.value"
25
- >
22
+ <div v-for="color in colors" :key="color.value">
26
23
  <div
27
24
  class="aspect-2/3 flex flex-col justify-between p-4 rounded-lg border border-default/20 shadow-lg cursor-default"
28
25
  :style="{ backgroundColor: color.value, color: contrastColor(color.value) }"
@@ -5,7 +5,12 @@ const { slug } = defineProps<{
5
5
  </script>
6
6
 
7
7
  <template>
8
- <NuxtContentDetail collection="portfolio" :slug not-found-message="Portfolio item not found" hide-toc>
8
+ <NuxtContentDetail
9
+ collection="portfolio"
10
+ :slug
11
+ not-found-message="Portfolio item not found"
12
+ hide-toc
13
+ >
9
14
  <template #headline="{ item }">
10
15
  <div class="flex flex-wrap items-center gap-2">
11
16
  <UBadge v-if="item.client" color="primary" variant="subtle">
@@ -9,7 +9,11 @@ const { data: items, status } = await usePortfolioItems(options)
9
9
  </script>
10
10
 
11
11
  <template>
12
- <NuxtContentList :status="status" :has-items="!!items?.length" empty-message="No portfolio items found">
12
+ <NuxtContentList
13
+ :status="status"
14
+ :has-items="!!items?.length"
15
+ empty-message="No portfolio items found"
16
+ >
13
17
  <UPageGrid>
14
18
  <UPageCard
15
19
  v-for="item in items"
@@ -17,13 +17,7 @@ const {
17
17
 
18
18
  <template>
19
19
  <figure class="my-6">
20
- <img
21
- :src="src"
22
- :alt="alt"
23
- :width="width"
24
- :height="height"
25
- class="w-full rounded-lg"
26
- />
20
+ <img :src="src" :alt="alt" :width="width" :height="height" class="w-full rounded-lg" />
27
21
  <figcaption v-if="caption" class="mt-2 text-center text-sm text-muted italic">
28
22
  {{ caption }}
29
23
  </figcaption>
@@ -1,9 +1,11 @@
1
- // Conditionally load nuxt-studio if installed (optional peer dependency)
1
+ // Conditionally load nuxt-studio if installed (optional peer dependency) and in development
2
2
  const studioModule: string[] = []
3
- try {
4
- await import('nuxt-studio')
5
- studioModule.push('nuxt-studio')
6
- } catch {}
3
+ if (process.env.NODE_ENV === 'development') {
4
+ try {
5
+ await import('nuxt-studio')
6
+ studioModule.push('nuxt-studio')
7
+ } catch {}
8
+ }
7
9
 
8
10
  // https://nuxt.com/docs/api/configuration/nuxt-config
9
11
  export default defineNuxtConfig({
@@ -20,6 +22,15 @@ export default defineNuxtConfig({
20
22
 
21
23
  modules: ['@nuxt/ui', '@nuxt/content', ...studioModule],
22
24
 
25
+ // Configure @nuxt/content for production/Netlify builds
26
+ content: {
27
+ // Disable local database for production builds to avoid better-sqlite3 native module issues
28
+ // database: process.env.NETLIFY || process.env.NODE_ENV === 'production' ? false : {},
29
+ experimental: {
30
+ nativeSqlite: true,
31
+ },
32
+ },
33
+
23
34
  css: ['#layers/content/app/assets/css/main.css'],
24
35
 
25
36
  compatibilityDate: '2026-01-24',
@@ -12,13 +12,13 @@
12
12
  "preview": "nuxt preview .playground"
13
13
  },
14
14
  "devDependencies": {
15
- "@nuxt/content": "^3.11.0",
16
- "nuxt-studio": "^1.2.0",
17
- "zod": "^3.25.76",
18
- "zod-to-json-schema": "^3.25.1"
15
+ "@nuxt/content": "^3.12.0",
16
+ "nuxt-studio": "^1.2.0"
19
17
  },
20
18
  "dependencies": {
21
- "better-sqlite3": "^12.6.2"
19
+ "better-sqlite3": "^12.6.2",
20
+ "zod": "^4.3.6",
21
+ "zod-to-json-schema": "^3.25.1"
22
22
  },
23
23
  "peerDependencies": {
24
24
  "nuxt-studio": "^1.2.0"
@@ -1,3 +1,8 @@
1
+ /* Define dark variant before Tailwind generates utilities so the selector
2
+ is established in a single pass, preventing a Tailwind v4 CSS regeneration
3
+ and the resulting HMR layout shift on first load. */
4
+ @custom-variant dark (&:where([data-theme-mode="dark"], [data-theme-mode="dark"] *));
5
+
1
6
  @import 'tailwindcss';
2
7
  @import '@nuxt/ui';
3
8
 
@@ -78,11 +78,15 @@ export function useCache() {
78
78
 
79
79
  try {
80
80
  const cacheNames = await caches.keys()
81
- await Promise.all(cacheNames.map((name) => caches.delete(name)))
82
-
83
- console.log('[useCache] All caches cleared')
81
+ ;(await Promise.all(cacheNames.map((name) => caches.delete(name)))(
82
+ process.env.NODE_ENV === 'development'
83
+ ))
84
+ ? console.log('[useCache] All caches cleared')
85
+ : ''
84
86
  } catch (error) {
85
- console.error('[useCache] Failed to clear cache:', error)
87
+ process.env.NODE_ENV === 'development'
88
+ ? console.error('[useCache] Failed to clear cache:', error)
89
+ : ''
86
90
  }
87
91
  }
88
92
 
@@ -75,7 +75,7 @@ export function useErrorLog() {
75
75
  }
76
76
 
77
77
  // Console logging (development only by default)
78
- if (config.value.logToConsole && import.meta.dev) {
78
+ if (config.value.logToConsole && import.meta.dev && process.env.NODE_ENV === 'development') {
79
79
  // eslint-disable-next-line no-console
80
80
  console.group('🔴 Error Logged')
81
81
  // eslint-disable-next-line no-console
@@ -100,7 +100,9 @@ export function useErrorLog() {
100
100
 
101
101
  if (!externalUrl) {
102
102
  // eslint-disable-next-line no-console
103
- console.warn('Error logging enabled but no externalUrl configured')
103
+ process.env.NODE_ENV === 'development'
104
+ ? console.warn('Error logging enabled but no externalUrl configured')
105
+ : ''
104
106
  return
105
107
  }
106
108
 
@@ -124,7 +126,9 @@ export function useErrorLog() {
124
126
  // Don't let logging errors crash the app
125
127
  if (import.meta.dev) {
126
128
  // eslint-disable-next-line no-console
127
- console.error('Failed to send error to external service:', loggingError)
129
+ process.env.NODE_ENV === 'development'
130
+ ? console.error('Failed to send error to external service:', loggingError)
131
+ : ''
128
132
  }
129
133
  }
130
134
  }
@@ -135,7 +139,7 @@ export function useErrorLog() {
135
139
  const logWarning = (message: string, context?: ErrorContext) => {
136
140
  if (config.value.logToConsole && import.meta.dev) {
137
141
  // eslint-disable-next-line no-console
138
- console.warn('⚠️ Warning:', message, context)
142
+ process.env.NODE_ENV === 'development' ? console.warn('⚠️ Warning:', message, context) : ''
139
143
  }
140
144
  }
141
145
 
@@ -145,7 +149,7 @@ export function useErrorLog() {
145
149
  const logInfo = (message: string, data?: unknown) => {
146
150
  if (config.value.logToConsole && import.meta.dev) {
147
151
  // eslint-disable-next-line no-console
148
- console.log('ℹ️ Info:', message, data)
152
+ process.env.NODE_ENV === 'development' ? console.log('ℹ️ Info:', message, data) : ''
149
153
  }
150
154
  }
151
155
 
@@ -1,7 +1,7 @@
1
1
  // composables/useScrollGuard.ts
2
2
 
3
- import { debounce } from '#layers/core/app/utils/helpers'
4
3
  import type { ScrollGuardConfig } from '#layers/core/app/types/scroll-guard'
4
+ import { debounce } from '#layers/core/app/utils/helpers'
5
5
 
6
6
  /**
7
7
  * Original inline styles stored per element so we can restore them on disable
@@ -198,7 +198,9 @@ export function useScrollGuard() {
198
198
 
199
199
  // Resolve config: explicit arg → app.config → defaults
200
200
  const appConfig = useAppConfig()
201
- const configFromApp = (appConfig.coreLayer as { scrollGuard?: Partial<ScrollGuardConfig> } | undefined)?.scrollGuard
201
+ const configFromApp = (
202
+ appConfig.coreLayer as { scrollGuard?: Partial<ScrollGuardConfig> } | undefined
203
+ )?.scrollGuard
202
204
 
203
205
  opts = {
204
206
  enabled: true,
@@ -15,7 +15,7 @@ export default defineNuxtPlugin(() => {
15
15
  // Initialize feature detection
16
16
  const features = useFeatures()
17
17
 
18
- if (import.meta.dev) {
18
+ if (import.meta.dev && process.env.NODE_ENV === 'development') {
19
19
  // eslint-disable-next-line no-console
20
20
  console.log('[Feature Detection] Initialized:', {
21
21
  grid: features.grid.value,
@@ -11,7 +11,8 @@
11
11
  */
12
12
  export default defineNuxtPlugin((nuxtApp) => {
13
13
  const config = useAppConfig()
14
- const isDev = import.meta.dev
14
+ // const isDev = import.meta.dev
15
+ const isDev = process.env.NODE_ENV === 'development' ? true : false
15
16
 
16
17
  // ============================================================
17
18
  // 1. Log initialization (dev only)
@@ -41,7 +41,10 @@ export default defineNuxtPlugin(() => {
41
41
  console.log('[Scroll Guard] Initialized', {
42
42
  strict: coreLayer?.scrollGuard?.strict ?? true,
43
43
  debug: coreLayer?.scrollGuard?.debug ?? false,
44
- excludeSelectors: coreLayer?.scrollGuard?.excludeSelectors ?? ['.carousel', '.overflow-intent'],
44
+ excludeSelectors: coreLayer?.scrollGuard?.excludeSelectors ?? [
45
+ '.carousel',
46
+ '.overflow-intent',
47
+ ],
45
48
  })
46
49
 
47
50
  // Log clamped count after initial scan settles
@@ -182,12 +182,3 @@ declare module '@nuxt/schema' {
182
182
  }
183
183
  }
184
184
  }
185
-
186
- declare module '@nuxt/schema' {
187
- interface AppConfigInput {
188
- coreLayer?: {
189
- /** Project name */
190
- name?: string
191
- }
192
- }
193
- }
@@ -24,14 +24,23 @@ const state = reactive<Partial<FormState>>({
24
24
  })
25
25
 
26
26
  const toast = useToast()
27
+ const isLoading = ref(false)
27
28
 
28
29
  async function onSubmit(event: FormSubmitEvent<FormState>) {
29
- toast.add({
30
- title: 'Success',
31
- description: 'The form has been submitted.',
32
- color: 'success',
33
- })
34
- emit('submit', event.data)
30
+ isLoading.value = true
31
+ try {
32
+ await $fetch('/api/contact', { method: 'POST', body: event.data })
33
+ toast.add({ title: 'Message sent!', description: 'Thanks for reaching out.', color: 'success' })
34
+ emit('submit', event.data)
35
+ } catch {
36
+ toast.add({
37
+ title: 'Something went wrong',
38
+ description: 'Please try again later.',
39
+ color: 'error',
40
+ })
41
+ } finally {
42
+ isLoading.value = false
43
+ }
35
44
  }
36
45
 
37
46
  async function onError() {
@@ -73,6 +82,6 @@ async function onError() {
73
82
  class="w-full"
74
83
  />
75
84
 
76
- <UButton type="submit" size="xl"> Submit </UButton>
85
+ <UButton type="submit" size="xl" :loading="isLoading" :disabled="isLoading"> Submit </UButton>
77
86
  </UForm>
78
87
  </template>
@@ -10,8 +10,26 @@ export default defineNuxtConfig({
10
10
 
11
11
  compatibilityDate: '2026-01-24',
12
12
 
13
+ runtimeConfig: {
14
+ formsLayer: {
15
+ resendApiKey: '', // env: NUXT_FORMS_LAYER_RESEND_API_KEY
16
+ emailFrom: 'contact@example.com', // env: NUXT_FORMS_LAYER_EMAIL_FROM
17
+ emailTo: '', // env: NUXT_FORMS_LAYER_EMAIL_TO
18
+ },
19
+ },
20
+
13
21
  typescript: {
14
22
  typeCheck: true,
15
23
  strict: true,
16
24
  },
17
25
  })
26
+
27
+ declare module '@nuxt/schema' {
28
+ interface RuntimeConfig {
29
+ formsLayer?: {
30
+ resendApiKey: string
31
+ emailFrom: string
32
+ emailTo: string
33
+ }
34
+ }
35
+ }
@@ -11,6 +11,8 @@
11
11
  "preview": "nuxt preview .playground"
12
12
  },
13
13
  "dependencies": {
14
+ "hookable": "^5.5.3",
15
+ "resend": "^4.0.0",
14
16
  "zod": "^3.24.2"
15
17
  }
16
18
  }
@@ -40,10 +40,7 @@ const sizeClass: Record<GridContainerSize, string> = {
40
40
  </script>
41
41
 
42
42
  <template>
43
- <component
44
- :is="tag"
45
- :class="sizeClass[size]"
46
- >
43
+ <component :is="tag" :class="sizeClass[size]">
47
44
  <slot />
48
45
  </component>
49
46
  </template>
@@ -75,4 +75,3 @@ defineExpose({ toggle })
75
75
  </div>
76
76
  </Teleport>
77
77
  </template>
78
-
@@ -101,7 +101,7 @@ const getDefaultValue = <T,>(value: T | ResponsiveValue<T> | undefined, defaultV
101
101
 
102
102
  const getResponsiveValue = <T,>(
103
103
  value: T | ResponsiveValue<T> | undefined,
104
- breakpoint: 'md' | 'lg',
104
+ breakpoint: 'md' | 'lg'
105
105
  ): T | undefined => {
106
106
  if (value === undefined) return undefined
107
107
  if (typeof value === 'object' && value !== null && 'default' in value) {
@@ -114,7 +114,10 @@ const style = computed(() => {
114
114
  const styles: Record<string, string> = {}
115
115
 
116
116
  const colStartVal = getDefaultValue(colStart.value, undefined)
117
- const colSpanVal = getDefaultValue(colSpan.value as ColSpanValue | ResponsiveValue<number> | undefined, 'full' as ColSpanValue)
117
+ const colSpanVal = getDefaultValue(
118
+ colSpan.value as ColSpanValue | ResponsiveValue<number> | undefined,
119
+ 'full' as ColSpanValue
120
+ )
118
121
  const rowStartVal = getDefaultValue(rowStart.value, undefined)
119
122
  const rowSpanVal = getDefaultValue(rowSpan.value, 1)
120
123
 
@@ -208,22 +211,25 @@ const classes = computed(() => {
208
211
  </template>
209
212
 
210
213
  <style>
214
+ /* stylelint-disable custom-property-pattern */
211
215
  .gi-placed {
212
- grid-column: var(--_cs, auto) / span var(--_ce, 1);
213
216
  grid-row: var(--_rs, auto) / span var(--_re, 1);
217
+ grid-column: var(--_cs, auto) / span var(--_ce, 1);
214
218
  }
215
219
 
216
220
  @media (width >= 48rem) {
217
221
  .gi-placed {
218
- grid-column: var(--_md-cs, var(--_cs, auto)) / span var(--_md-ce, var(--_ce, 1));
219
222
  grid-row: var(--_md-rs, var(--_rs, auto)) / span var(--_md-re, var(--_re, 1));
223
+ grid-column: var(--_md-cs, var(--_cs, auto)) / span var(--_md-ce, var(--_ce, 1));
220
224
  }
221
225
  }
222
226
 
223
227
  @media (width >= 80rem) {
224
228
  .gi-placed {
225
- grid-column: var(--_lg-cs, var(--_md-cs, var(--_cs, auto))) / span var(--_lg-ce, var(--_md-ce, var(--_ce, 1)));
226
- grid-row: var(--_lg-rs, var(--_md-rs, var(--_rs, auto))) / span var(--_lg-re, var(--_md-re, var(--_re, 1)));
229
+ grid-row: var(--_lg-rs, var(--_md-rs, var(--_rs, auto))) / span
230
+ var(--_lg-re, var(--_md-re, var(--_re, 1)));
231
+ grid-column: var(--_lg-cs, var(--_md-cs, var(--_cs, auto))) / span
232
+ var(--_lg-ce, var(--_md-ce, var(--_ce, 1)));
227
233
  }
228
234
  }
229
235
  </style>
@@ -27,10 +27,7 @@ const { mode } = useGridConfig()
27
27
  </script>
28
28
 
29
29
  <template>
30
- <component
31
- :is="tag"
32
- :class="mode !== 'disabled' ? 'mastmain' : undefined"
33
- >
30
+ <component :is="tag" :class="mode !== 'disabled' ? 'mastmain' : undefined">
34
31
  <slot />
35
32
  </component>
36
33
  </template>
@@ -44,6 +44,7 @@ interface Props {
44
44
  showHeader?: boolean
45
45
  headerPreset?: string
46
46
  layout?: 'grid' | 'upage'
47
+ back?: string
47
48
  }
48
49
 
49
50
  const {
@@ -52,6 +53,7 @@ const {
52
53
  showHeader = true,
53
54
  headerPreset = 'centered',
54
55
  layout = 'grid',
56
+ back,
55
57
  } = defineProps<Props>()
56
58
 
57
59
  // Set page metadata for SEO and browser tab
@@ -76,7 +78,7 @@ provide('pageTitle', title)
76
78
  <!-- Optional visible header -->
77
79
  <LayoutSection v-if="showHeader">
78
80
  <LayoutGridItem :preset="headerPreset">
79
- <LayoutPageHeader :title :description />
81
+ <LayoutPageHeader :title :description :back />
80
82
  </LayoutGridItem>
81
83
  </LayoutSection>
82
84
 
@@ -18,18 +18,27 @@
18
18
  interface Props {
19
19
  title: string
20
20
  description?: string
21
+ back?: string
21
22
  }
22
23
 
23
24
  defineProps<Props>()
24
25
  </script>
25
26
 
26
27
  <template>
27
- <!-- <div class="prose-rhythm text-center">
28
- <h1 class="text-5xl font-bold">{{ title }}</h1>
29
- <p v-if="description" class="text-xl text-gray-600 dark:text-gray-400">
30
- {{ description }}
31
- </p>
32
- </div> -->
28
+ <div :class="back ? 'flex items-center gap-4 py-6' : 'py-10 text-center'">
29
+ <!-- Sub-page: back button + title -->
30
+ <template v-if="back">
31
+ <UButton :to="back" variant="ghost" icon="i-lucide-arrow-left" />
32
+ <div>
33
+ <h1 class="text-3xl font-bold text-highlighted">{{ title }}</h1>
34
+ <p v-if="description" class="text-muted">{{ description }}</p>
35
+ </div>
36
+ </template>
33
37
 
34
- <UPageHeader :title :description />
38
+ <!-- Centered page header (homepage, section intros) -->
39
+ <template v-else>
40
+ <h1 class="text-4xl font-bold text-highlighted">{{ title }}</h1>
41
+ <p v-if="description" class="mt-3 text-lg text-muted">{{ description }}</p>
42
+ </template>
43
+ </div>
35
44
  </template>
@@ -29,10 +29,7 @@ const { minItemWidth = '200px', fullHeight = false } = defineProps<Props>()
29
29
  <template>
30
30
  <LayoutSection :full-height>
31
31
  <LayoutGridItem>
32
- <div
33
- class="ram-grid"
34
- :style="{ '--min-item-width': minItemWidth }"
35
- >
32
+ <div class="ram-grid" :style="{ '--min-item-width': minItemWidth }">
36
33
  <slot />
37
34
  </div>
38
35
  </LayoutGridItem>
@@ -27,7 +27,12 @@ interface Props {
27
27
  fullHeight?: boolean
28
28
  }
29
29
 
30
- const { sidebarMin = '150px', sidebarMax = '25%', reverse = false, fullHeight = false } = defineProps<Props>()
30
+ const {
31
+ sidebarMin = '150px',
32
+ sidebarMax = '25%',
33
+ reverse = false,
34
+ fullHeight = false,
35
+ } = defineProps<Props>()
31
36
  </script>
32
37
 
33
38
  <template>
@@ -48,8 +48,8 @@ const { fullHeight = true } = defineProps<Props>()
48
48
  .stack-inner {
49
49
  display: grid;
50
50
  grid-template-rows: auto 1fr auto;
51
- height: 100%;
52
51
  width: 100%;
52
+ height: 100%;
53
53
  }
54
54
 
55
55
  .stack-header,
@@ -1,4 +1,9 @@
1
- import type { GridConfig, GridLayers, GridMode, GridPresetsItem } from '#layers/layout/app/types/layouts'
1
+ import type {
2
+ GridConfig,
3
+ GridLayers,
4
+ GridMode,
5
+ GridPresetsItem,
6
+ } from '#layers/layout/app/types/layouts'
2
7
 
3
8
  interface LayoutLayerConfig {
4
9
  layoutLayer?: {