kmcom-nuxt-layers 1.3.0 → 1.4.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/layers/content/app/components/Blog/List.vue +5 -1
- package/layers/content/app/components/Gallery/AmbientImage.vue +5 -12
- package/layers/content/app/components/Gallery/Detail.vue +8 -6
- package/layers/content/app/components/Gallery/Grid.vue +11 -3
- package/layers/content/app/components/Portfolio/ColorPalette.vue +1 -4
- package/layers/content/app/components/Portfolio/Detail.vue +6 -1
- package/layers/content/app/components/Portfolio/List.vue +5 -1
- package/layers/content/app/components/content/Figure.vue +1 -7
- package/layers/content/package.json +5 -5
- package/layers/core/app/assets/css/main.css +5 -0
- package/layers/core/app/composables/useCache.ts +8 -4
- package/layers/core/app/composables/useErrorLog.ts +9 -5
- package/layers/core/app/composables/useScrollGuard.ts +4 -2
- package/layers/core/app/plugins/feature-detection.client.ts +1 -1
- package/layers/core/app/plugins/init.ts +2 -1
- package/layers/core/app/plugins/scroll-guard.client.ts +4 -1
- package/layers/core/app.config.ts +0 -9
- package/layers/forms/app/components/Form/Contact.vue +16 -7
- package/layers/forms/nuxt.config.ts +18 -0
- package/layers/forms/package.json +2 -0
- package/layers/layout/app/components/Layout/Container.vue +1 -4
- package/layers/layout/app/components/Layout/Grid/Debug.vue +0 -1
- package/layers/layout/app/components/Layout/Grid/Item.vue +12 -6
- package/layers/layout/app/components/Layout/Main.vue +1 -4
- package/layers/layout/app/components/Layout/Page/Container.vue +3 -1
- package/layers/layout/app/components/Layout/Page/Header.vue +16 -5
- package/layers/layout/app/components/Layout/Section/Grid.vue +1 -4
- package/layers/layout/app/components/Layout/Section/Sidebar.vue +6 -1
- package/layers/layout/app/components/Layout/Section/Stack.vue +1 -1
- package/layers/layout/app/components/Layout/Section/Title.vue +33 -0
- package/layers/layout/app/composables/useGridConfig.ts +6 -1
- package/layers/motion/app/components/Motion/HorizontalScroll.vue +61 -0
- package/layers/motion/app/components/Motion/PinnedSection.vue +77 -0
- package/layers/motion/app/components/Motion/ScrollProgress.vue +8 -56
- package/layers/motion/app/components/Motion/ScrollScene.vue +121 -0
- package/layers/motion/app/components/Motion/ScrollStep.vue +45 -0
- package/layers/motion/app/components/Motion/TextReveal.vue +28 -63
- package/layers/motion/app/composables/useScrollSteps.ts +41 -0
- package/layers/motion/app/composables/useSectionProgress.ts +58 -0
- package/layers/motion/app/composables/useSmoothScroll.ts +3 -2
- package/layers/motion/app/plugins/locomotive-scroll.client.ts +6 -6
- package/layers/motion/nuxt.config.ts +6 -0
- package/layers/motion/package.json +2 -1
- package/layers/shader/app/components/Preset/ThemeAurora.client.vue +86 -0
- package/layers/shader/app/components/Preset/ThemeFlow.client.vue +86 -0
- package/layers/shader/app/components/Preset/ThemeGradient.client.vue +87 -0
- package/layers/shader/app/components/Shader/Background.client.vue +6 -0
- package/layers/shader/app/composables/useAmbientMaterials.ts +150 -0
- package/layers/shader/app/composables/useThemeColors.ts +43 -0
- package/layers/shader/app/utils/tsl/oklch.ts +12 -6
- package/layers/theme/app/assets/css/theme.css +19 -14
- package/layers/theme/app/components/ThemePicker/AccentButton.vue +2 -2
- package/layers/theme/app/components/ThemePicker/Colors.vue +2 -4
- package/layers/theme/app/components/ThemePicker/Menu.vue +4 -13
- package/layers/theme/app/components/ThemePicker/MenuButton.vue +1 -7
- package/layers/theme/app/composables/useAccentColor.ts +38 -0
- package/layers/theme/app/composables/useTheme.ts +14 -0
- package/layers/theme/app/composables/useThemeContrast.ts +34 -0
- package/layers/theme/app/composables/useThemeMotion.ts +34 -0
- package/layers/theme/app/composables/useThemePreferences.ts +3 -156
- package/layers/theme/app/composables/useThemeTransparency.ts +41 -0
- package/layers/theme/app/plugins/theme.client.ts +3 -3
- package/layers/theme/app/types/theme.ts +4 -0
- package/layers/theme/nuxt.config.ts +7 -0
- package/layers/ui/app/app.config.ts +44 -0
- package/layers/ui/app/assets/css/main.css +14 -0
- package/layers/ui/app/components/Accent/Blob.vue +29 -0
- package/layers/ui/app/components/Accent/Scene.vue +38 -0
- package/layers/ui/app/components/Gradient/Background.vue +22 -0
- package/layers/ui/app/components/Gradient/Text.vue +22 -0
- package/layers/ui/app/components/Progress/Bar.vue +25 -0
- package/layers/ui/app/components/Progress/Circular.vue +69 -0
- package/layers/ui/app/components/Tint/Overlay.vue +25 -0
- package/layers/ui/app/components/Typography/CodeBlock.vue +2 -1
- package/layers/ui/app/components/Typography/Headline.vue +2 -1
- package/layers/ui/app/components/Typography/QuoteBlock.vue +2 -1
- package/layers/ui/app/components/Typography/TextStroke.vue +18 -16
- package/layers/ui/app/composables/accent.ts +51 -0
- package/layers/ui/app/composables/gradient.ts +79 -0
- package/layers/ui/app/composables/tint.ts +20 -0
- package/layers/ui/app/types/accent.ts +17 -0
- package/layers/ui/app/types/gradient.ts +27 -0
- package/layers/ui/app/types/tint.ts +25 -0
- package/package.json +32 -30
- package/layers/motion/app/utils/gsapAnimations.ts +0 -122
- 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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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">
|
|
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
|
|
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
|
|
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>
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
"preview": "nuxt preview .playground"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
-
"@nuxt/content": "^3.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = (
|
|
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 ?? [
|
|
44
|
+
excludeSelectors: coreLayer?.scrollGuard?.excludeSelectors ?? [
|
|
45
|
+
'.carousel',
|
|
46
|
+
'.overflow-intent',
|
|
47
|
+
],
|
|
45
48
|
})
|
|
46
49
|
|
|
47
50
|
// Log clamped count after initial scan settles
|
|
@@ -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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
color: 'success'
|
|
33
|
-
|
|
34
|
-
|
|
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
|
+
}
|
|
@@ -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(
|
|
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-
|
|
226
|
-
|
|
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,16 +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="
|
|
28
|
-
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
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>
|
|
37
|
+
|
|
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>
|
|
32
43
|
</div>
|
|
33
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 {
|
|
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>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* PageHeader - Standard page header component
|
|
4
|
+
*
|
|
5
|
+
* Displays a centered page title and optional description with consistent spacing.
|
|
6
|
+
* Used by PageContainer for visible page headers.
|
|
7
|
+
*
|
|
8
|
+
* @prop {string} title - Page title (displays as h1)
|
|
9
|
+
* @prop {string} description - Optional page description
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <LayoutSectionTitle
|
|
13
|
+
* title="About Us"
|
|
14
|
+
* description="Learn about our company and mission"
|
|
15
|
+
* />
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
title: string
|
|
20
|
+
description?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
defineProps<Props>()
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<LayoutGridItem class="prose-rhythm text-center" align="start" justify="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
|
+
</LayoutGridItem>
|
|
33
|
+
</template>
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
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?: {
|