kmcom-nuxt-layers 2.2.12 → 2.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.
- package/docs/FEEDS.md +1 -2
- package/layers/animations/app/composables/useMagneticElement.ts +11 -9
- package/layers/animations/app/composables/useTiltEffect.ts +11 -9
- package/layers/animations/app/utils/pointerMotion.ts +31 -0
- package/layers/canvas/app/components/ShaderCanvas.vue +2 -2
- package/layers/content/app/composables/useCollectionItems.ts +28 -0
- package/layers/content/app/composables/useGalleryItems.ts +8 -14
- package/layers/content/app/composables/usePortfolioItems.ts +10 -18
- package/layers/core/app/composables/useBrowser.ts +9 -82
- package/layers/core/app/composables/useFeatures.ts +3 -27
- package/layers/core/app/plugins/init.ts +157 -135
- package/layers/core/app/utils/browserInfo.ts +115 -0
- package/layers/core/app/utils/featureClasses.ts +40 -0
- package/layers/core/app/utils/helpers.test.ts +51 -0
- package/layers/feeds/app/components/Feeds/Index.vue +1 -1
- package/layers/feeds/app/components/Feeds/RouteCard.vue +3 -9
- package/layers/feeds/app/utils/feed-catalog.ts +9 -4
- package/layers/feeds/nuxt.config.ts +0 -1
- package/layers/feeds/server/utils/content-adapter.test.ts +68 -0
- package/layers/feeds/server/utils/content-adapter.ts +2 -22
- package/layers/feeds/server/utils/feed-author.ts +32 -0
- package/layers/feeds/server/utils/feed-config.ts +88 -0
- package/layers/feeds/server/utils/feed-service.ts +11 -30
- package/layers/feeds/server/utils/feed-xml.ts +26 -0
- package/layers/feeds/server/utils/formats/rss.ts +10 -15
- package/layers/feeds/server/utils/formats.test.ts +71 -0
- package/layers/forms/app/components/Form/Field.vue +42 -30
- package/layers/forms/app/utils/fieldProps.ts +65 -0
- package/layers/layout/app/components/Layout/Grid/Item.vue +29 -146
- package/layers/layout/app/utils/gridPlacementStyle.ts +195 -0
- package/layers/mailer/app/types/mailer.ts +7 -25
- package/layers/mailer/server/utils/email.ts +28 -13
- package/layers/mailer/server/utils/hooks.ts +1 -20
- package/layers/navigation/app/composables/useSite.ts +2 -9
- package/layers/navigation/app/utils/site.ts +26 -0
- package/layers/routing/app/utils/resolveRoute.test.ts +47 -0
- package/layers/routing/app/utils/resolveRoute.ts +19 -10
- package/layers/scripts/app/composables/useAnalytics.ts +8 -41
- package/layers/scripts/app/composables/useGtm.ts +6 -13
- package/layers/scripts/app/utils/scriptClients.ts +70 -0
- package/layers/scroll/app/composables/useSmoothScroll.ts +9 -43
- package/layers/scroll/app/utils/scroll.ts +103 -0
- package/layers/seo/app/composables/useSeoConfig.ts +3 -9
- package/layers/seo/app/utils/seoConfig.ts +38 -0
- package/layers/shader/app/components/Material/AmbientAurora.client.vue +11 -33
- package/layers/shader/app/components/Material/AmbientFlow.client.vue +10 -37
- package/layers/shader/app/components/Material/AmbientGradientMesh.client.vue +10 -37
- package/layers/shader/app/components/Material/AmbientNebula.client.vue +12 -37
- package/layers/shader/app/components/Material/AmbientOcean.client.vue +9 -33
- package/layers/shader/app/components/Material/Gradient.client.vue +25 -46
- package/layers/shader/app/components/Material/Image.client.vue +10 -55
- package/layers/shader/app/components/Material/Node.client.vue +18 -5
- package/layers/shader/app/components/Material/Noise.client.vue +9 -43
- package/layers/shader/app/components/Preset/ThemeBubble.client.vue +2 -1
- package/layers/shader/app/components/Preset/ThemeFlow.client.vue +2 -1
- package/layers/shader/app/components/Preset/ThemeGradient.client.vue +2 -1
- package/layers/shader/app/components/Preset/ThemeLavaLamp.client.vue +2 -1
- package/layers/shader/app/components/Preset/ThemePlasma.client.vue +2 -1
- package/layers/shader/app/components/Preset/ThemeWave.client.vue +2 -1
- package/layers/shader/app/components/Shader/Background.client.vue +44 -24
- package/layers/shader/app/composables/useAmbientMaterials.ts +5 -1
- package/layers/shader/app/composables/useShader.ts +38 -23
- package/layers/shader/app/composables/useShaderGraph.ts +11 -6
- package/layers/shader/app/composables/useShaderMixBlend.ts +4 -4
- package/layers/shader/app/composables/useShaderRuntime.ts +0 -1
- package/layers/shader/app/composables/useShaderVec2.ts +2 -4
- package/layers/shader/app/composables/useThemePreset.ts +34 -8
- package/layers/shader/app/composables/useUniformWatchers.ts +15 -0
- package/layers/shader/app/composables/useUniforms.ts +0 -1
- package/layers/shader/app/shaders/common/blend.ts +4 -4
- package/layers/shader/app/shaders/common/effects.ts +38 -21
- package/layers/shader/app/shaders/common/grain.ts +46 -49
- package/layers/shader/app/shaders/common/lighting.ts +17 -15
- package/layers/shader/app/shaders/common/math.ts +2 -4
- package/layers/shader/app/shaders/common/nodes.ts +17 -0
- package/layers/shader/app/shaders/common/palette.ts +21 -11
- package/layers/shader/app/shaders/common/patterns.ts +25 -14
- package/layers/shader/app/shaders/common/shapes.ts +97 -88
- package/layers/shader/app/shaders/common/uv.ts +33 -34
- package/layers/shader/app/shaders/createMaterial.ts +92 -78
- package/layers/shader/app/shaders/layers/paperShading.ts +22 -10
- package/layers/shader/app/shaders/layers/shaderGradient.ts +46 -21
- package/layers/shader/app/utils/tsl/tween.ts +2 -4
- package/layers/shader/package.json +5 -1
- package/layers/starter/app/components/StarterDesignSystem.vue +1913 -0
- package/layers/starter/app/components/StarterHome.vue +407 -0
- package/layers/starter/nuxt.config.ts +15 -0
- package/layers/starter/package.json +10 -0
- package/layers/theme/app/components/ThemePicker/Menu.vue +3 -25
- package/layers/theme/app/composables/useThemePreferenceModels.ts +39 -0
- package/layers/theme/server/plugins/theme-fouc.ts +1 -92
- package/layers/theme/server/utils/accent-css.ts +75 -0
- package/layers/typography/app/composables/typography.ts +3 -7
- package/layers/visual/app/composables/accent.ts +2 -9
- package/layers/visual/app/composables/gradient.ts +33 -46
- package/layers/visual/app/composables/picture.ts +2 -79
- package/layers/visual/app/utils/colorTokens.ts +23 -0
- package/layers/visual/app/utils/gradientStyle.ts +41 -0
- package/layers/visual/app/utils/responsiveSizes.ts +49 -0
- package/package.json +17 -5
- package/layers/feeds/app/utils/feed-catalog.test.ts +0 -71
- package/layers/feeds/server/routes/feed/discovery.get.ts +0 -31
|
@@ -32,6 +32,11 @@
|
|
|
32
32
|
* </BaseGridItem>
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
+
import {
|
|
36
|
+
buildGridPlacementClasses,
|
|
37
|
+
buildGridPlacementStyle,
|
|
38
|
+
} from '#layers/layout/app/utils/gridPlacementStyle'
|
|
39
|
+
|
|
35
40
|
type ColSpanValue = number | 'full'
|
|
36
41
|
type Alignment = 'start' | 'center' | 'end' | 'stretch'
|
|
37
42
|
type LayerName = 'back' | 'mid' | 'front' | 'top'
|
|
@@ -100,152 +105,30 @@
|
|
|
100
105
|
const gap = computed(() => gapProp ?? presetConfig.value?.gap)
|
|
101
106
|
const density = computed(() => densityProp ?? presetConfig.value?.density)
|
|
102
107
|
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const getResponsiveValue = <T,>(
|
|
129
|
-
value: T | ResponsiveValue<T> | undefined,
|
|
130
|
-
breakpoint: 'md' | 'lg'
|
|
131
|
-
): T | undefined => {
|
|
132
|
-
if (value === undefined) return undefined
|
|
133
|
-
if (typeof value === 'object' && value !== null && 'default' in value) {
|
|
134
|
-
return value[breakpoint]
|
|
135
|
-
}
|
|
136
|
-
return undefined
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const style = computed(() => {
|
|
140
|
-
const styles: Record<string, string> = {}
|
|
141
|
-
|
|
142
|
-
const colStartVal = getDefaultValue(colStart.value, undefined)
|
|
143
|
-
const colSpanVal = getDefaultValue(
|
|
144
|
-
colSpan.value as ColSpanValue | ResponsiveValue<number> | undefined,
|
|
145
|
-
'full' as ColSpanValue
|
|
146
|
-
)
|
|
147
|
-
const rowStartVal = getDefaultValue(rowStart.value, undefined)
|
|
148
|
-
const rowSpanVal = getDefaultValue(rowSpan.value, 1)
|
|
149
|
-
|
|
150
|
-
if (bleed) {
|
|
151
|
-
if (bleed === 'both') {
|
|
152
|
-
styles.gridColumn = '1 / -1'
|
|
153
|
-
styles.marginInline = 'calc(-1 * var(--grid-padding))'
|
|
154
|
-
} else if (bleed === 'left') {
|
|
155
|
-
const spanNum = typeof colSpanVal === 'number' ? colSpanVal : undefined
|
|
156
|
-
styles.gridColumn = spanNum ? `1 / span ${spanNum}` : '1 / -1'
|
|
157
|
-
styles.marginInlineStart = 'calc(-1 * var(--grid-padding))'
|
|
158
|
-
} else if (bleed === 'right') {
|
|
159
|
-
styles.gridColumn = `${colStartVal ?? 'auto'} / -1`
|
|
160
|
-
styles.marginInlineEnd = 'calc(-1 * var(--grid-padding))'
|
|
161
|
-
}
|
|
162
|
-
styles.gridRow = `${rowStartVal ?? 'auto'} / span ${rowSpanVal}`
|
|
163
|
-
} else if (colSpanVal === 'full') {
|
|
164
|
-
// 'full' span: use inline gridColumn directly (no CSS var approach needed)
|
|
165
|
-
styles.gridColumn = `${colStartVal ?? 1} / -1`
|
|
166
|
-
// Still set row vars for responsive row support
|
|
167
|
-
styles['--_rs'] = String(rowStartVal ?? 'auto')
|
|
168
|
-
styles['--_re'] = String(rowSpanVal)
|
|
169
|
-
|
|
170
|
-
const mdRowStart = getResponsiveValue(rowStart.value, 'md')
|
|
171
|
-
const lgRowStart = getResponsiveValue(rowStart.value, 'lg')
|
|
172
|
-
const mdRowSpan = getResponsiveValue(rowSpan.value, 'md')
|
|
173
|
-
const lgRowSpan = getResponsiveValue(rowSpan.value, 'lg')
|
|
174
|
-
|
|
175
|
-
if (mdRowStart !== undefined) styles['--_md-rs'] = String(mdRowStart)
|
|
176
|
-
if (lgRowStart !== undefined) styles['--_lg-rs'] = String(lgRowStart)
|
|
177
|
-
if (mdRowSpan !== undefined) styles['--_md-re'] = String(mdRowSpan)
|
|
178
|
-
if (lgRowSpan !== undefined) styles['--_lg-re'] = String(lgRowSpan)
|
|
179
|
-
} else {
|
|
180
|
-
// Set CSS custom properties instead of grid-column/grid-row directly.
|
|
181
|
-
// The <style> block below reads these vars and applies them at each breakpoint,
|
|
182
|
-
// which correctly cascades without inline-style specificity conflicts.
|
|
183
|
-
styles['--_cs'] = String(colStartVal ?? 'auto')
|
|
184
|
-
styles['--_ce'] = String(colSpanVal)
|
|
185
|
-
styles['--_rs'] = String(rowStartVal ?? 'auto')
|
|
186
|
-
styles['--_re'] = String(rowSpanVal)
|
|
187
|
-
|
|
188
|
-
const mdColStart = getResponsiveValue(colStart.value, 'md')
|
|
189
|
-
const lgColStart = getResponsiveValue(colStart.value, 'lg')
|
|
190
|
-
const mdColSpan = getResponsiveValue(
|
|
191
|
-
colSpan.value as ResponsiveValue<number> | undefined,
|
|
192
|
-
'md'
|
|
193
|
-
)
|
|
194
|
-
const lgColSpan = getResponsiveValue(
|
|
195
|
-
colSpan.value as ResponsiveValue<number> | undefined,
|
|
196
|
-
'lg'
|
|
197
|
-
)
|
|
198
|
-
const mdRowStart = getResponsiveValue(rowStart.value, 'md')
|
|
199
|
-
const lgRowStart = getResponsiveValue(rowStart.value, 'lg')
|
|
200
|
-
const mdRowSpan = getResponsiveValue(rowSpan.value, 'md')
|
|
201
|
-
const lgRowSpan = getResponsiveValue(rowSpan.value, 'lg')
|
|
202
|
-
|
|
203
|
-
if (mdColStart !== undefined) styles['--_md-cs'] = String(mdColStart)
|
|
204
|
-
if (lgColStart !== undefined) styles['--_lg-cs'] = String(lgColStart)
|
|
205
|
-
if (mdColSpan !== undefined) styles['--_md-ce'] = String(mdColSpan)
|
|
206
|
-
if (lgColSpan !== undefined) styles['--_lg-ce'] = String(lgColSpan)
|
|
207
|
-
if (mdRowStart !== undefined) styles['--_md-rs'] = String(mdRowStart)
|
|
208
|
-
if (lgRowStart !== undefined) styles['--_lg-rs'] = String(lgRowStart)
|
|
209
|
-
if (mdRowSpan !== undefined) styles['--_md-re'] = String(mdRowSpan)
|
|
210
|
-
if (lgRowSpan !== undefined) styles['--_lg-re'] = String(lgRowSpan)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Content alignment
|
|
214
|
-
if (align.value || justify.value) {
|
|
215
|
-
styles.display = 'grid'
|
|
216
|
-
styles.width = '100%'
|
|
217
|
-
styles.height = '100%'
|
|
218
|
-
styles.placeItems = `${align.value ?? 'stretch'} ${justify.value ?? 'stretch'}`
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Gap override — cascades to nested content via --grid-gap
|
|
222
|
-
if (gap.value) styles['--grid-gap'] = gap.value
|
|
223
|
-
|
|
224
|
-
// Density — sets --rhythm base unit for child spacing utilities
|
|
225
|
-
if (density.value) {
|
|
226
|
-
const rhythmMap: Record<Density, string> = {
|
|
227
|
-
compact: '0.125rem',
|
|
228
|
-
normal: '0.25rem',
|
|
229
|
-
relaxed: '0.5rem',
|
|
230
|
-
}
|
|
231
|
-
styles['--rhythm'] = rhythmMap[density.value]
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Z-index
|
|
235
|
-
const zIndex = z ?? (layer ? layerZIndex[layer] : undefined)
|
|
236
|
-
if (zIndex !== undefined) styles.zIndex = String(zIndex)
|
|
237
|
-
|
|
238
|
-
return styles
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
const classes = computed(() => {
|
|
242
|
-
const classList: string[] = ['gi-placed', '@container', '@container/item']
|
|
243
|
-
|
|
244
|
-
if (aspect) classList.push(aspectClasses[aspect])
|
|
245
|
-
if (container.value) classList.push(`layout-container-${container.value}`)
|
|
246
|
-
|
|
247
|
-
return classList.join(' ')
|
|
248
|
-
})
|
|
108
|
+
const style = computed(() =>
|
|
109
|
+
buildGridPlacementStyle({
|
|
110
|
+
colStart: colStart.value,
|
|
111
|
+
colSpan: colSpan.value as ColSpanValue | ResponsiveValue<number> | undefined,
|
|
112
|
+
rowStart: rowStart.value,
|
|
113
|
+
rowSpan: rowSpan.value,
|
|
114
|
+
align: align.value,
|
|
115
|
+
justify: justify.value,
|
|
116
|
+
container: container.value,
|
|
117
|
+
gap: gap.value,
|
|
118
|
+
density: density.value,
|
|
119
|
+
z,
|
|
120
|
+
layer,
|
|
121
|
+
bleed,
|
|
122
|
+
aspect,
|
|
123
|
+
})
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const classes = computed(() =>
|
|
127
|
+
buildGridPlacementClasses({
|
|
128
|
+
aspect,
|
|
129
|
+
container: container.value,
|
|
130
|
+
})
|
|
131
|
+
)
|
|
249
132
|
</script>
|
|
250
133
|
|
|
251
134
|
<template>
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
type ResponsiveValue<T> = {
|
|
2
|
+
default: T
|
|
3
|
+
md?: T
|
|
4
|
+
lg?: T
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
type Alignment = 'start' | 'center' | 'end' | 'stretch'
|
|
8
|
+
type LayerName = 'back' | 'mid' | 'front' | 'top'
|
|
9
|
+
type BleedDirection = 'left' | 'right' | 'both'
|
|
10
|
+
type AspectRatio = '1/1' | '4/3' | '3/4' | '16/9' | '9/16' | '2/1' | '1/2'
|
|
11
|
+
type ContainerSize = 'content' | 'wide' | 'fluid' | 'full'
|
|
12
|
+
type Density = 'compact' | 'normal' | 'relaxed'
|
|
13
|
+
|
|
14
|
+
export type GridPlacementInput = {
|
|
15
|
+
colStart?: number | ResponsiveValue<number> | undefined
|
|
16
|
+
colSpan?: number | 'full' | ResponsiveValue<number> | undefined
|
|
17
|
+
rowStart?: number | ResponsiveValue<number> | undefined
|
|
18
|
+
rowSpan?: number | ResponsiveValue<number> | undefined
|
|
19
|
+
align?: Alignment | undefined
|
|
20
|
+
justify?: Alignment | undefined
|
|
21
|
+
container?: ContainerSize | undefined
|
|
22
|
+
gap?: string | undefined
|
|
23
|
+
density?: Density | undefined
|
|
24
|
+
z?: number | undefined
|
|
25
|
+
layer?: LayerName | undefined
|
|
26
|
+
bleed?: BleedDirection | undefined
|
|
27
|
+
aspect?: AspectRatio | undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type PlacementStyle = Record<string, string>
|
|
31
|
+
|
|
32
|
+
export const layerZIndex: Record<LayerName, number> = {
|
|
33
|
+
back: 0,
|
|
34
|
+
mid: 10,
|
|
35
|
+
front: 20,
|
|
36
|
+
top: 30,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const aspectClasses: Record<AspectRatio, string> = {
|
|
40
|
+
'1/1': 'aspect-square',
|
|
41
|
+
'4/3': 'aspect-[4/3]',
|
|
42
|
+
'3/4': 'aspect-[3/4]',
|
|
43
|
+
'16/9': 'aspect-video',
|
|
44
|
+
'9/16': 'aspect-[9/16]',
|
|
45
|
+
'2/1': 'aspect-[2/1]',
|
|
46
|
+
'1/2': 'aspect-[1/2]',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// fallow-ignore-next-line complexity
|
|
50
|
+
export function resolveDefaultPlacement<T>(
|
|
51
|
+
value: T | ResponsiveValue<T> | undefined,
|
|
52
|
+
fallback: T | undefined
|
|
53
|
+
): T | undefined {
|
|
54
|
+
if (value === undefined) return fallback
|
|
55
|
+
if (typeof value === 'object' && value !== null && 'default' in value) {
|
|
56
|
+
return value.default
|
|
57
|
+
}
|
|
58
|
+
return value
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// fallow-ignore-next-line complexity
|
|
62
|
+
function resolveResponsivePlacementValue<T>(
|
|
63
|
+
value: T | ResponsiveValue<T> | undefined,
|
|
64
|
+
breakpoint: 'md' | 'lg'
|
|
65
|
+
): T | undefined {
|
|
66
|
+
if (value === undefined) return undefined
|
|
67
|
+
if (typeof value === 'object' && value !== null && 'default' in value) {
|
|
68
|
+
return value[breakpoint]
|
|
69
|
+
}
|
|
70
|
+
return undefined
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function resolveResponsivePlacementVars<T>(
|
|
74
|
+
value: T | ResponsiveValue<T> | undefined,
|
|
75
|
+
prefix: string
|
|
76
|
+
): PlacementStyle {
|
|
77
|
+
const styles: PlacementStyle = {}
|
|
78
|
+
const mdValue = resolveResponsivePlacementValue(value, 'md')
|
|
79
|
+
const lgValue = resolveResponsivePlacementValue(value, 'lg')
|
|
80
|
+
|
|
81
|
+
if (mdValue !== undefined) styles[`--_md-${prefix}`] = String(mdValue)
|
|
82
|
+
if (lgValue !== undefined) styles[`--_lg-${prefix}`] = String(lgValue)
|
|
83
|
+
|
|
84
|
+
return styles
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// fallow-ignore-next-line complexity
|
|
88
|
+
export function resolveBleedStyles(args: {
|
|
89
|
+
bleed?: BleedDirection | undefined
|
|
90
|
+
colStart?: number | undefined
|
|
91
|
+
colSpan?: number | 'full' | undefined
|
|
92
|
+
rowStart?: number | undefined
|
|
93
|
+
rowSpan?: number | undefined
|
|
94
|
+
}): PlacementStyle {
|
|
95
|
+
const styles: PlacementStyle = {}
|
|
96
|
+
const { bleed, colStart, colSpan, rowStart, rowSpan } = args
|
|
97
|
+
|
|
98
|
+
if (!bleed) return styles
|
|
99
|
+
|
|
100
|
+
if (bleed === 'both') {
|
|
101
|
+
styles.gridColumn = '1 / -1'
|
|
102
|
+
styles.marginInline = 'calc(-1 * var(--grid-padding))'
|
|
103
|
+
} else if (bleed === 'left') {
|
|
104
|
+
const spanNum = typeof colSpan === 'number' ? colSpan : undefined
|
|
105
|
+
styles.gridColumn = spanNum ? `1 / span ${spanNum}` : '1 / -1'
|
|
106
|
+
styles.marginInlineStart = 'calc(-1 * var(--grid-padding))'
|
|
107
|
+
} else if (bleed === 'right') {
|
|
108
|
+
styles.gridColumn = `${colStart ?? 'auto'} / -1`
|
|
109
|
+
styles.marginInlineEnd = 'calc(-1 * var(--grid-padding))'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
styles.gridRow = `${rowStart ?? 'auto'} / span ${rowSpan}`
|
|
113
|
+
return styles
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// fallow-ignore-next-line complexity
|
|
117
|
+
export function resolveAlignmentStyles(align?: Alignment, justify?: Alignment): PlacementStyle {
|
|
118
|
+
if (!align && !justify) return {}
|
|
119
|
+
return {
|
|
120
|
+
display: 'grid',
|
|
121
|
+
width: '100%',
|
|
122
|
+
height: '100%',
|
|
123
|
+
placeItems: `${align ?? 'stretch'} ${justify ?? 'stretch'}`,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function resolveRhythmStyles(density?: Density): PlacementStyle {
|
|
128
|
+
if (!density) return {}
|
|
129
|
+
|
|
130
|
+
const rhythmMap: Record<Density, string> = {
|
|
131
|
+
compact: '0.125rem',
|
|
132
|
+
normal: '0.25rem',
|
|
133
|
+
relaxed: '0.5rem',
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { '--rhythm': rhythmMap[density] }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function resolveLayerStyles(z?: number, layer?: LayerName): PlacementStyle {
|
|
140
|
+
const zIndex = z ?? (layer ? layerZIndex[layer] : undefined)
|
|
141
|
+
return zIndex !== undefined ? { zIndex: String(zIndex) } : {}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// fallow-ignore-next-line complexity
|
|
145
|
+
export function buildGridPlacementStyle(input: GridPlacementInput): PlacementStyle {
|
|
146
|
+
const style: PlacementStyle = {}
|
|
147
|
+
const colStart = resolveDefaultPlacement(input.colStart, undefined)
|
|
148
|
+
const colSpan = resolveDefaultPlacement(input.colSpan, 'full')
|
|
149
|
+
const rowStart = resolveDefaultPlacement(input.rowStart, undefined)
|
|
150
|
+
const rowSpan = resolveDefaultPlacement(input.rowSpan, 1)
|
|
151
|
+
|
|
152
|
+
Object.assign(style, resolveBleedStyles({
|
|
153
|
+
bleed: input.bleed,
|
|
154
|
+
colStart,
|
|
155
|
+
colSpan,
|
|
156
|
+
rowStart,
|
|
157
|
+
rowSpan,
|
|
158
|
+
}))
|
|
159
|
+
|
|
160
|
+
if (!input.bleed) {
|
|
161
|
+
if (colSpan === 'full') {
|
|
162
|
+
style.gridColumn = `${colStart ?? 1} / -1`
|
|
163
|
+
style['--_rs'] = String(rowStart ?? 'auto')
|
|
164
|
+
style['--_re'] = String(rowSpan)
|
|
165
|
+
Object.assign(style, resolveResponsivePlacementVars(input.rowStart, 'rs'))
|
|
166
|
+
Object.assign(style, resolveResponsivePlacementVars(input.rowSpan, 're'))
|
|
167
|
+
} else {
|
|
168
|
+
style['--_cs'] = String(colStart ?? 'auto')
|
|
169
|
+
style['--_ce'] = String(colSpan)
|
|
170
|
+
style['--_rs'] = String(rowStart ?? 'auto')
|
|
171
|
+
style['--_re'] = String(rowSpan)
|
|
172
|
+
Object.assign(style, resolveResponsivePlacementVars(input.colStart, 'cs'))
|
|
173
|
+
Object.assign(style, resolveResponsivePlacementVars(input.colSpan, 'ce'))
|
|
174
|
+
Object.assign(style, resolveResponsivePlacementVars(input.rowStart, 'rs'))
|
|
175
|
+
Object.assign(style, resolveResponsivePlacementVars(input.rowSpan, 're'))
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
Object.assign(style, resolveAlignmentStyles(input.align, input.justify))
|
|
180
|
+
Object.assign(style, resolveRhythmStyles(input.density))
|
|
181
|
+
Object.assign(style, resolveLayerStyles(input.z, input.layer))
|
|
182
|
+
|
|
183
|
+
if (input.gap) style['--grid-gap'] = input.gap
|
|
184
|
+
|
|
185
|
+
return style
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function buildGridPlacementClasses(input: Pick<GridPlacementInput, 'aspect' | 'container'>) {
|
|
189
|
+
const classList: string[] = ['gi-placed', '@container', '@container/item']
|
|
190
|
+
|
|
191
|
+
if (input.aspect) classList.push(aspectClasses[input.aspect])
|
|
192
|
+
if (input.container) classList.push(`layout-container-${input.container}`)
|
|
193
|
+
|
|
194
|
+
return classList.join(' ')
|
|
195
|
+
}
|
|
@@ -1,25 +1,7 @@
|
|
|
1
|
-
export type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
name: string
|
|
9
|
-
email: string
|
|
10
|
-
message: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export type ContactSentPayload = {
|
|
14
|
-
messageId: string
|
|
15
|
-
} & ContactSubmittedPayload
|
|
16
|
-
|
|
17
|
-
export type ContactFailedPayload = {
|
|
18
|
-
error: unknown
|
|
19
|
-
} & ContactSubmittedPayload
|
|
20
|
-
|
|
21
|
-
export type MailerLayerHooks = {
|
|
22
|
-
'contact:submitted': (payload: ContactSubmittedPayload) => void
|
|
23
|
-
'contact:sent': (payload: ContactSentPayload) => void
|
|
24
|
-
'contact:failed': (payload: ContactFailedPayload) => void
|
|
25
|
-
}
|
|
1
|
+
export type {
|
|
2
|
+
ContactEmailData,
|
|
3
|
+
ContactFailedPayload,
|
|
4
|
+
ContactSentPayload,
|
|
5
|
+
ContactSubmittedPayload,
|
|
6
|
+
MailerLayerHooks,
|
|
7
|
+
} from '#layers/mailer/shared/contact'
|
|
@@ -1,27 +1,42 @@
|
|
|
1
1
|
import { Resend } from 'resend'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import type { ContactEmailData } from '#layers/mailer/shared/contact'
|
|
4
|
+
|
|
5
|
+
type MailerTransport = {
|
|
6
|
+
from: string
|
|
7
|
+
to: string
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
function resolveMailerTransport() {
|
|
10
11
|
const { resendApiKey, emailFrom, emailTo } = useMailerConfig()
|
|
12
|
+
if (!resendApiKey) return null
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return { success: false as const, error: 'No API key configured' }
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const resend = new Resend(resendApiKey)
|
|
18
|
-
const { data: result, error } = await resend.emails.send({
|
|
14
|
+
return {
|
|
15
|
+
apiKey: resendApiKey,
|
|
19
16
|
from: emailFrom ?? '',
|
|
20
17
|
to: emailTo ?? '',
|
|
18
|
+
} satisfies MailerTransport & { apiKey: string }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildContactEmailPayload(data: ContactEmailData, transport: MailerTransport) {
|
|
22
|
+
return {
|
|
23
|
+
from: transport.from,
|
|
24
|
+
to: transport.to,
|
|
21
25
|
replyTo: data.email,
|
|
22
26
|
subject: `Contact form submission from ${data.name}`,
|
|
23
27
|
text: `Name: ${data.name}\nEmail: ${data.email}\n\nMessage:\n${data.message}`,
|
|
24
|
-
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function sendContactEmail(data: ContactEmailData) {
|
|
32
|
+
const transport = resolveMailerTransport()
|
|
33
|
+
if (!transport) {
|
|
34
|
+
console.warn('[mailer] NUXT_MAILER_LAYER_RESEND_API_KEY not set — email skipped')
|
|
35
|
+
return { success: false as const, error: 'No API key configured' }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const resend = new Resend(transport.apiKey)
|
|
39
|
+
const { data: result, error } = await resend.emails.send(buildContactEmailPayload(data, transport))
|
|
25
40
|
|
|
26
41
|
if (error) return { success: false as const, error }
|
|
27
42
|
return { success: true as const, messageId: result?.id ?? '' }
|
|
@@ -1,23 +1,4 @@
|
|
|
1
1
|
import { createHooks } from 'hookable'
|
|
2
|
-
|
|
3
|
-
export type ContactSubmittedPayload = {
|
|
4
|
-
name: string
|
|
5
|
-
email: string
|
|
6
|
-
message: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export type ContactSentPayload = {
|
|
10
|
-
messageId: string
|
|
11
|
-
} & ContactSubmittedPayload
|
|
12
|
-
|
|
13
|
-
export type ContactFailedPayload = {
|
|
14
|
-
error: unknown
|
|
15
|
-
} & ContactSubmittedPayload
|
|
16
|
-
|
|
17
|
-
export type MailerLayerHooks = {
|
|
18
|
-
'contact:submitted': (payload: ContactSubmittedPayload) => void
|
|
19
|
-
'contact:sent': (payload: ContactSentPayload) => void
|
|
20
|
-
'contact:failed': (payload: ContactFailedPayload) => void
|
|
21
|
-
}
|
|
2
|
+
import type { MailerLayerHooks } from '#layers/mailer/shared/contact'
|
|
22
3
|
|
|
23
4
|
export const mailerLayerHooks = createHooks<MailerLayerHooks>()
|
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveSiteConfig } from '../utils/site'
|
|
2
2
|
|
|
3
3
|
export function useSite() {
|
|
4
4
|
const config = useAppConfig().site ?? {}
|
|
5
|
-
|
|
6
|
-
return {
|
|
7
|
-
title: (config.title ?? '') as string,
|
|
8
|
-
titleWords: splitSpaces((config.title ?? '') as string) as string[],
|
|
9
|
-
subtitle: (config.subtitle ?? '') as string,
|
|
10
|
-
subtitleWords: splitSpaces((config.subtitle ?? '') as string) as string[],
|
|
11
|
-
description: (config.description ?? '') as string,
|
|
12
|
-
}
|
|
5
|
+
return resolveSiteConfig(config)
|
|
13
6
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { splitSpaces } from '../utils/regex'
|
|
2
|
+
|
|
3
|
+
function resolveSiteText(value?: string) {
|
|
4
|
+
const text = value ?? ''
|
|
5
|
+
return {
|
|
6
|
+
text,
|
|
7
|
+
words: splitSpaces(text) as string[],
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolveSiteConfig(site: {
|
|
12
|
+
title?: string
|
|
13
|
+
subtitle?: string
|
|
14
|
+
description?: string
|
|
15
|
+
}) {
|
|
16
|
+
const title = resolveSiteText(site.title)
|
|
17
|
+
const subtitle = resolveSiteText(site.subtitle)
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
title: title.text,
|
|
21
|
+
titleWords: title.words,
|
|
22
|
+
subtitle: subtitle.text,
|
|
23
|
+
subtitleWords: subtitle.words,
|
|
24
|
+
description: site.description ?? '',
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import type { FeatureValue, RouteResolution, RoutingLayerConfig } from '../types/routing'
|
|
4
|
+
import { resolveRoute } from './resolveRoute'
|
|
5
|
+
|
|
6
|
+
function createConfig(overrides: Partial<RoutingLayerConfig> = {}): RoutingLayerConfig {
|
|
7
|
+
return {
|
|
8
|
+
preset: 'marketing',
|
|
9
|
+
strictDefaultDeny: false,
|
|
10
|
+
layerDefaultDeny: false,
|
|
11
|
+
betaRedirect: '/coming-soon',
|
|
12
|
+
runtimeFlags: false,
|
|
13
|
+
debug: false,
|
|
14
|
+
maintenance: { enabled: false, allowRoutes: ['/maintenance'] },
|
|
15
|
+
scrollRouting: { enabled: false, mode: 'replace' },
|
|
16
|
+
features: {},
|
|
17
|
+
...overrides,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('resolveRoute', () => {
|
|
22
|
+
it('allows routes without a feature by default', () => {
|
|
23
|
+
expect(resolveRoute({}, createConfig(), () => 'enabled')).toEqual({ outcome: 'allow' })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('denies routes when strict default deny is enabled', () => {
|
|
27
|
+
expect(resolveRoute({}, createConfig({ strictDefaultDeny: true }), () => 'enabled')).toEqual({
|
|
28
|
+
outcome: 'deny',
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('denies layer-owned routes without a feature when layer default deny is enabled', () => {
|
|
33
|
+
expect(
|
|
34
|
+
resolveRoute({ __fromLayer: true }, createConfig({ layerDefaultDeny: true }), () => 'enabled')
|
|
35
|
+
).toEqual({ outcome: 'deny' })
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const featureCases: Array<[FeatureValue, RouteResolution]> = [
|
|
39
|
+
['disabled', { outcome: 'deny' }],
|
|
40
|
+
['beta', { outcome: 'redirect', to: '/coming-soon' }],
|
|
41
|
+
['coming-soon', { outcome: 'redirect', to: '/coming-soon' }],
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
it.each(featureCases)('handles %s feature variants', (variant, expected) => {
|
|
45
|
+
expect(resolveRoute({ feature: 'feature-x' }, createConfig(), () => variant)).toEqual(expected)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -5,27 +5,36 @@ type RouteMeta = {
|
|
|
5
5
|
__fromLayer?: boolean
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
function shouldDenyStrict(meta: RouteMeta, config: RoutingLayerConfig): boolean {
|
|
9
|
+
return config.strictDefaultDeny && !meta.feature
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function shouldDenyLayer(meta: RouteMeta, config: RoutingLayerConfig): boolean {
|
|
13
|
+
return config.layerDefaultDeny && meta.__fromLayer === true && !meta.feature
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolveFeatureOutcome(variant: FeatureValue, config: RoutingLayerConfig): RouteResolution {
|
|
17
|
+
if (variant === 'disabled') return { outcome: 'deny' }
|
|
18
|
+
if (variant === 'beta' || variant === 'coming-soon') {
|
|
19
|
+
return { outcome: 'redirect', to: config.betaRedirect }
|
|
20
|
+
}
|
|
21
|
+
return { outcome: 'allow' }
|
|
22
|
+
}
|
|
23
|
+
|
|
8
24
|
export function resolveRoute(
|
|
9
25
|
meta: RouteMeta,
|
|
10
26
|
config: RoutingLayerConfig,
|
|
11
27
|
resolveFeature: (name: string) => FeatureValue
|
|
12
28
|
): RouteResolution {
|
|
13
|
-
if (config
|
|
29
|
+
if (shouldDenyStrict(meta, config)) {
|
|
14
30
|
return { outcome: 'deny' }
|
|
15
31
|
}
|
|
16
32
|
|
|
17
|
-
if (
|
|
33
|
+
if (shouldDenyLayer(meta, config)) {
|
|
18
34
|
return { outcome: 'deny' }
|
|
19
35
|
}
|
|
20
36
|
|
|
21
37
|
if (!meta.feature) return { outcome: 'allow' }
|
|
22
38
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (variant === 'disabled') return { outcome: 'deny' }
|
|
26
|
-
if (variant === 'beta' || variant === 'coming-soon') {
|
|
27
|
-
return { outcome: 'redirect', to: config.betaRedirect }
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return { outcome: 'allow' }
|
|
39
|
+
return resolveFeatureOutcome(resolveFeature(meta.feature), config)
|
|
31
40
|
}
|