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
|
@@ -24,13 +24,9 @@ function normalizeAxis(
|
|
|
24
24
|
) {
|
|
25
25
|
const { prefix = '', fallback = '', numericFormatter } = options
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return `[${value}]`
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return value ?? fallback
|
|
27
|
+
return typeof value === 'number'
|
|
28
|
+
? numericFormatter?.(value) ?? (prefix ? `${prefix}-[${value}]` : `[${value}]`)
|
|
29
|
+
: value ?? fallback
|
|
34
30
|
}
|
|
35
31
|
|
|
36
32
|
function getSizeClass(size: FontSize | undefined): string {
|
|
@@ -2,6 +2,7 @@ import type { ComputedRef, CSSProperties, MaybeRefOrGetter } from 'vue'
|
|
|
2
2
|
import { toValue } from 'vue'
|
|
3
3
|
|
|
4
4
|
import type { BlobBlur, BlobConfig } from '../types/accent'
|
|
5
|
+
import { resolveUiColorToken } from '../utils/colorTokens'
|
|
5
6
|
|
|
6
7
|
const BLUR_PX_MAP: Record<string, number> = {
|
|
7
8
|
none: 0,
|
|
@@ -18,14 +19,6 @@ function resolveBlurPx(blur: BlobBlur = '3xl'): number {
|
|
|
18
19
|
return BLUR_PX_MAP[blur] ?? 64
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
function resolveColor(config: BlobConfig): string {
|
|
22
|
-
const { color = 'primary', shade = 500, customColor } = config
|
|
23
|
-
if (color === 'custom') return customColor ?? 'transparent'
|
|
24
|
-
if (color === 'white') return '#ffffff'
|
|
25
|
-
if (color === 'black') return '#000000'
|
|
26
|
-
return `var(--ui-color-${color}-${shade})`
|
|
27
|
-
}
|
|
28
|
-
|
|
29
22
|
export function useAccentBlob(config: MaybeRefOrGetter<BlobConfig>): {
|
|
30
23
|
style: ComputedRef<CSSProperties>
|
|
31
24
|
} {
|
|
@@ -42,7 +35,7 @@ export function useAccentBlob(config: MaybeRefOrGetter<BlobConfig>): {
|
|
|
42
35
|
transform: 'translate(-50%, -50%)',
|
|
43
36
|
width: size,
|
|
44
37
|
height: size,
|
|
45
|
-
backgroundColor:
|
|
38
|
+
backgroundColor: resolveUiColorToken(resolved),
|
|
46
39
|
opacity,
|
|
47
40
|
borderRadius: '9999px',
|
|
48
41
|
filter: blurPx > 0 ? `blur(${blurPx}px)` : undefined,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ComputedRef, CSSProperties, MaybeRefOrGetter } from 'vue'
|
|
2
2
|
|
|
3
|
-
import type { GradientConfig
|
|
3
|
+
import type { GradientConfig } from '../types/gradient'
|
|
4
|
+
import { buildGradientStyle } from '../utils/gradientStyle'
|
|
4
5
|
|
|
5
6
|
const DEFAULT_CONFIG: GradientConfig = {
|
|
6
7
|
shape: 'linear',
|
|
@@ -9,44 +10,41 @@ const DEFAULT_CONFIG: GradientConfig = {
|
|
|
9
10
|
to: { color: 'secondary', shade: 500 },
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
'to-bl': 'to bottom left',
|
|
19
|
-
'to-l': 'to left',
|
|
20
|
-
'to-tl': 'to top left',
|
|
13
|
+
function resolveGradientConfig(
|
|
14
|
+
raw: GradientConfig | string,
|
|
15
|
+
override: Partial<GradientConfig> | undefined,
|
|
16
|
+
appConfig: ReturnType<typeof useAppConfig>
|
|
17
|
+
): GradientConfig {
|
|
18
|
+
return mergeGradientOverride(resolveGradientPreset(raw, appConfig), override)
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return
|
|
21
|
+
function resolveGradientPreset(
|
|
22
|
+
raw: GradientConfig | string,
|
|
23
|
+
appConfig: ReturnType<typeof useAppConfig>
|
|
24
|
+
): GradientConfig {
|
|
25
|
+
if (typeof raw !== 'string') return raw
|
|
26
|
+
|
|
27
|
+
return resolveGradientPresetByName(raw, getGradientPresetMap(appConfig))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getGradientPresetMap(appConfig: ReturnType<typeof useAppConfig>) {
|
|
31
|
+
return (appConfig.uiLayer as Record<string, unknown> | undefined)?.['gradients'] as
|
|
32
|
+
| Record<string, GradientConfig>
|
|
33
|
+
| undefined
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
function resolveGradientPresetByName(
|
|
37
|
+
name: string,
|
|
38
|
+
presets: Record<string, GradientConfig> | undefined
|
|
39
|
+
) {
|
|
40
|
+
return presets?.[name] ?? DEFAULT_CONFIG
|
|
41
|
+
}
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
const dir = DIRECTION_MAP[cfg.direction ?? 'to-br'] ?? 'to bottom right'
|
|
49
|
-
return { backgroundImage: `linear-gradient(${dir}, ${stops})` }
|
|
43
|
+
function mergeGradientOverride(
|
|
44
|
+
resolved: GradientConfig,
|
|
45
|
+
override: Partial<GradientConfig> | undefined
|
|
46
|
+
): GradientConfig {
|
|
47
|
+
return override ? { ...resolved, ...override } : resolved
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
export function useGradient(
|
|
@@ -58,18 +56,7 @@ export function useGradient(
|
|
|
58
56
|
const style = computed((): CSSProperties => {
|
|
59
57
|
const raw = toValue(config)
|
|
60
58
|
const override = overrides ? toValue(overrides) : undefined
|
|
61
|
-
|
|
62
|
-
const presets = (appConfig.uiLayer as Record<string, unknown> | undefined)?.['gradients'] as
|
|
63
|
-
| Record<string, GradientConfig>
|
|
64
|
-
| undefined
|
|
65
|
-
let resolved: GradientConfig =
|
|
66
|
-
typeof raw === 'string' ? (presets?.[raw] ?? DEFAULT_CONFIG) : raw
|
|
67
|
-
|
|
68
|
-
if (override) {
|
|
69
|
-
resolved = { ...resolved, ...override }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return buildStyle(resolved)
|
|
59
|
+
return buildGradientStyle(resolveGradientConfig(raw, override, appConfig))
|
|
73
60
|
})
|
|
74
61
|
|
|
75
62
|
return { style }
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import type { MaybeRefOrGetter } from 'vue'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
BREAKPOINT_VALUES,
|
|
5
|
-
DEVICE_BREAKPOINT_VALUES,
|
|
6
|
-
PHONE_BREAKPOINT_VALUES,
|
|
7
|
-
TABLET_BREAKPOINT_VALUES,
|
|
8
|
-
} from '../types/breakpoints'
|
|
9
3
|
import type { PictureProps, ResponsiveSizes, UsePictureReturn } from '../types/media'
|
|
4
|
+
import { buildResponsiveSizesQueries } from '../utils/responsiveSizes'
|
|
10
5
|
|
|
11
6
|
/**
|
|
12
7
|
* Convert ResponsiveSizes object to CSS sizes attribute string
|
|
@@ -32,79 +27,7 @@ import type { PictureProps, ResponsiveSizes, UsePictureReturn } from '../types/m
|
|
|
32
27
|
* ```
|
|
33
28
|
*/
|
|
34
29
|
function responsiveSizesToString(sizes: ResponsiveSizes): string {
|
|
35
|
-
|
|
36
|
-
const breakpointEntries: Array<{ key: keyof ResponsiveSizes; value: number }> = []
|
|
37
|
-
|
|
38
|
-
// Add Tailwind breakpoints
|
|
39
|
-
const tailwindBreakpoints: Array<keyof typeof BREAKPOINT_VALUES> = ['2xl', 'xl', 'lg', 'md', 'sm']
|
|
40
|
-
for (const bp of tailwindBreakpoints) {
|
|
41
|
-
if (sizes[bp]) {
|
|
42
|
-
breakpointEntries.push({ key: bp, value: BREAKPOINT_VALUES[bp] })
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Add device breakpoints (skip mobile as it's 0px - handled by default)
|
|
47
|
-
const deviceBreakpoints: Array<keyof typeof DEVICE_BREAKPOINT_VALUES> = [
|
|
48
|
-
'wide',
|
|
49
|
-
'desktop',
|
|
50
|
-
'tablet',
|
|
51
|
-
]
|
|
52
|
-
for (const bp of deviceBreakpoints) {
|
|
53
|
-
if (sizes[bp]) {
|
|
54
|
-
breakpointEntries.push({ key: bp, value: DEVICE_BREAKPOINT_VALUES[bp] })
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Add phone breakpoints
|
|
59
|
-
const phoneBreakpoints: Array<keyof typeof PHONE_BREAKPOINT_VALUES> = [
|
|
60
|
-
'phone-lg',
|
|
61
|
-
'phone-md',
|
|
62
|
-
'phone-sm',
|
|
63
|
-
]
|
|
64
|
-
for (const bp of phoneBreakpoints) {
|
|
65
|
-
if (sizes[bp]) {
|
|
66
|
-
breakpointEntries.push({ key: bp, value: PHONE_BREAKPOINT_VALUES[bp] })
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Add tablet breakpoints
|
|
71
|
-
const tabletBreakpoints: Array<keyof typeof TABLET_BREAKPOINT_VALUES> = [
|
|
72
|
-
'tablet-lg',
|
|
73
|
-
'tablet-md',
|
|
74
|
-
'tablet-sm',
|
|
75
|
-
]
|
|
76
|
-
for (const bp of tabletBreakpoints) {
|
|
77
|
-
if (sizes[bp]) {
|
|
78
|
-
breakpointEntries.push({ key: bp, value: TABLET_BREAKPOINT_VALUES[bp] })
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Sort by value descending (largest to smallest)
|
|
83
|
-
breakpointEntries.sort((a, b) => b.value - a.value)
|
|
84
|
-
|
|
85
|
-
// Build media queries
|
|
86
|
-
const mediaQueries: string[] = []
|
|
87
|
-
|
|
88
|
-
// Add orientation breakpoints first (these are feature queries, not min-width)
|
|
89
|
-
if (sizes.landscape) {
|
|
90
|
-
mediaQueries.push(`(orientation: landscape) ${sizes.landscape}`)
|
|
91
|
-
}
|
|
92
|
-
if (sizes.portrait) {
|
|
93
|
-
mediaQueries.push(`(orientation: portrait) ${sizes.portrait}`)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Add min-width breakpoints
|
|
97
|
-
for (const entry of breakpointEntries) {
|
|
98
|
-
const size = sizes[entry.key]
|
|
99
|
-
if (size) {
|
|
100
|
-
mediaQueries.push(`(min-width: ${entry.value}px) ${size}`)
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Add default size at the end (no media query)
|
|
105
|
-
mediaQueries.push(sizes.default)
|
|
106
|
-
|
|
107
|
-
return mediaQueries.join(', ')
|
|
30
|
+
return buildResponsiveSizesQueries(sizes).join(', ')
|
|
108
31
|
}
|
|
109
32
|
|
|
110
33
|
/**
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
type ResolvableColorToken = {
|
|
2
|
+
color?: string | undefined
|
|
3
|
+
shade?: number | undefined
|
|
4
|
+
opacity?: number | undefined
|
|
5
|
+
customColor?: string | undefined
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// fallow-ignore-next-line complexity
|
|
9
|
+
export function resolveUiColorToken(token: ResolvableColorToken): string {
|
|
10
|
+
const { color = 'primary', shade = 500, opacity, customColor } = token
|
|
11
|
+
|
|
12
|
+
if (color === 'custom') return customColor ?? 'transparent'
|
|
13
|
+
if (color === 'transparent') return 'transparent'
|
|
14
|
+
if (color === 'white') {
|
|
15
|
+
return opacity !== undefined ? `rgb(255 255 255 / ${opacity / 100})` : '#ffffff'
|
|
16
|
+
}
|
|
17
|
+
if (color === 'black') {
|
|
18
|
+
return opacity !== undefined ? `rgb(0 0 0 / ${opacity / 100})` : '#000000'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const value = `var(--ui-color-${color}-${shade})`
|
|
22
|
+
return opacity !== undefined ? `color-mix(in srgb, ${value} ${opacity}%, transparent)` : value
|
|
23
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CSSProperties } from 'vue'
|
|
2
|
+
|
|
3
|
+
import type { GradientConfig, GradientStop } from '../types/gradient'
|
|
4
|
+
import { resolveUiColorToken } from './colorTokens'
|
|
5
|
+
|
|
6
|
+
const DIRECTION_MAP: Record<string, string> = {
|
|
7
|
+
'to-t': 'to top',
|
|
8
|
+
'to-tr': 'to top right',
|
|
9
|
+
'to-r': 'to right',
|
|
10
|
+
'to-br': 'to bottom right',
|
|
11
|
+
'to-b': 'to bottom',
|
|
12
|
+
'to-bl': 'to bottom left',
|
|
13
|
+
'to-l': 'to left',
|
|
14
|
+
'to-tl': 'to top left',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function resolveGradientColor(stop: GradientStop): string {
|
|
18
|
+
return resolveUiColorToken(stop)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveGradientStops(cfg: GradientConfig) {
|
|
22
|
+
const from = resolveGradientColor(cfg.from)
|
|
23
|
+
const to = resolveGradientColor(cfg.to)
|
|
24
|
+
const via = cfg.via ? resolveGradientColor(cfg.via) : undefined
|
|
25
|
+
return via ? `${from}, ${via}, ${to}` : `${from}, ${to}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// fallow-ignore-next-line complexity
|
|
29
|
+
export function buildGradientStyle(cfg: GradientConfig): CSSProperties {
|
|
30
|
+
const stops = resolveGradientStops(cfg)
|
|
31
|
+
|
|
32
|
+
if (cfg.shape === 'radial') {
|
|
33
|
+
return { backgroundImage: `radial-gradient(circle, ${stops})` }
|
|
34
|
+
}
|
|
35
|
+
if (cfg.shape === 'conic') {
|
|
36
|
+
return { backgroundImage: `conic-gradient(${stops})` }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const direction = DIRECTION_MAP[cfg.direction ?? 'to-br'] ?? 'to bottom right'
|
|
40
|
+
return { backgroundImage: `linear-gradient(${direction}, ${stops})` }
|
|
41
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BREAKPOINT_VALUES,
|
|
3
|
+
DEVICE_BREAKPOINT_VALUES,
|
|
4
|
+
PHONE_BREAKPOINT_VALUES,
|
|
5
|
+
TABLET_BREAKPOINT_VALUES,
|
|
6
|
+
} from '../types/breakpoints'
|
|
7
|
+
|
|
8
|
+
import type { ResponsiveSizes } from '../types/media'
|
|
9
|
+
|
|
10
|
+
type BreakpointGroup = readonly [keyof ResponsiveSizes, number]
|
|
11
|
+
|
|
12
|
+
const BREAKPOINT_GROUPS = [
|
|
13
|
+
['2xl', BREAKPOINT_VALUES['2xl']],
|
|
14
|
+
['xl', BREAKPOINT_VALUES.xl],
|
|
15
|
+
['lg', BREAKPOINT_VALUES.lg],
|
|
16
|
+
['md', BREAKPOINT_VALUES.md],
|
|
17
|
+
['sm', BREAKPOINT_VALUES.sm],
|
|
18
|
+
['wide', DEVICE_BREAKPOINT_VALUES.wide],
|
|
19
|
+
['desktop', DEVICE_BREAKPOINT_VALUES.desktop],
|
|
20
|
+
['tablet', DEVICE_BREAKPOINT_VALUES.tablet],
|
|
21
|
+
['phone-lg', PHONE_BREAKPOINT_VALUES['phone-lg']],
|
|
22
|
+
['phone-md', PHONE_BREAKPOINT_VALUES['phone-md']],
|
|
23
|
+
['phone-sm', PHONE_BREAKPOINT_VALUES['phone-sm']],
|
|
24
|
+
['tablet-lg', TABLET_BREAKPOINT_VALUES['tablet-lg']],
|
|
25
|
+
['tablet-md', TABLET_BREAKPOINT_VALUES['tablet-md']],
|
|
26
|
+
['tablet-sm', TABLET_BREAKPOINT_VALUES['tablet-sm']],
|
|
27
|
+
] as const satisfies readonly BreakpointGroup[]
|
|
28
|
+
|
|
29
|
+
// fallow-ignore-next-line complexity
|
|
30
|
+
export function buildResponsiveSizesQueries(sizes: ResponsiveSizes): string[] {
|
|
31
|
+
const breakpoints = BREAKPOINT_GROUPS.filter(([key]) => sizes[key])
|
|
32
|
+
.map(([key, value]) => ({ key, value }))
|
|
33
|
+
.sort((a, b) => b.value - a.value)
|
|
34
|
+
|
|
35
|
+
const queries: string[] = []
|
|
36
|
+
|
|
37
|
+
if (sizes.landscape) queries.push(`(orientation: landscape) ${sizes.landscape}`)
|
|
38
|
+
if (sizes.portrait) queries.push(`(orientation: portrait) ${sizes.portrait}`)
|
|
39
|
+
|
|
40
|
+
for (const breakpoint of breakpoints) {
|
|
41
|
+
const size = sizes[breakpoint.key]
|
|
42
|
+
if (size) {
|
|
43
|
+
queries.push(`(min-width: ${breakpoint.value}px) ${size}`)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
queries.push(sizes.default)
|
|
48
|
+
return queries
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kmcom-nuxt-layers",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.3.0",
|
|
5
5
|
"description": "Composable Nuxt 4 layers for building scalable Vue applications",
|
|
6
6
|
"exports": {
|
|
7
7
|
"./layers/core": "./layers/core/nuxt.config.ts",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"./layers/theme": "./layers/theme/nuxt.config.ts",
|
|
25
25
|
"./layers/content": "./layers/content/nuxt.config.ts",
|
|
26
26
|
"./layers/feeds": "./layers/feeds/nuxt.config.ts",
|
|
27
|
-
"./layers/routing": "./layers/routing/nuxt.config.ts"
|
|
27
|
+
"./layers/routing": "./layers/routing/nuxt.config.ts",
|
|
28
|
+
"./layers/starter": "./layers/starter/nuxt.config.ts"
|
|
28
29
|
},
|
|
29
30
|
"files": [
|
|
30
31
|
"layers/*/nuxt.config.ts",
|
|
@@ -139,16 +140,20 @@
|
|
|
139
140
|
"@nuxt/eslint": "^1.15.2",
|
|
140
141
|
"@nuxt/fonts": "^0.14.0",
|
|
141
142
|
"@nuxt/image": "^2.0.0",
|
|
143
|
+
"@nuxt/test-utils": "^4.0.3",
|
|
144
|
+
"@playwright/test": "1.61.0",
|
|
142
145
|
"@nuxt/ui": "latest",
|
|
143
146
|
"@nuxtjs/device": "^4.0.0",
|
|
144
147
|
"@oxc-parser/binding-darwin-arm64": "^0.134.0",
|
|
145
148
|
"@perplex-digital/stylelint-config": "^17.4.0",
|
|
146
149
|
"@pinia/nuxt": "^0.11.3",
|
|
150
|
+
"@vitejs/plugin-vue": "^6.0.7",
|
|
147
151
|
"@types/node": "^25.9.1",
|
|
148
152
|
"@types/three": "^0.184.1",
|
|
149
153
|
"@typescript-eslint/eslint-plugin": "^8.60.1",
|
|
150
154
|
"@typescript-eslint/parser": "^8.60.1",
|
|
151
155
|
"@vue/eslint-config-typescript": "^14.7.0",
|
|
156
|
+
"@vue/test-utils": "^2.4.10",
|
|
152
157
|
"@vueuse/core": "^14.3.0",
|
|
153
158
|
"@vueuse/nuxt": "^14.3.0",
|
|
154
159
|
"@webgpu/glslang": "^0.0.15",
|
|
@@ -167,11 +172,11 @@
|
|
|
167
172
|
"eslint-plugin-unused-imports": "^4.4.1",
|
|
168
173
|
"eslint-plugin-vue": "^10.9.2",
|
|
169
174
|
"fallow": "^2.92.1",
|
|
175
|
+
"happy-dom": "^20.10.3",
|
|
170
176
|
"netlify-cli": "^26.1.0",
|
|
171
177
|
"npm-check-updates": "^22.2.2",
|
|
172
178
|
"nuxt": "^4.4.7",
|
|
173
179
|
"pinia": "^3.0.4",
|
|
174
|
-
"playwright": "^1.60.0",
|
|
175
180
|
"postcss-html": "^1.8.1",
|
|
176
181
|
"prettier": "^3.8.3",
|
|
177
182
|
"prettier-plugin-css-order": "^2.2.0",
|
|
@@ -206,7 +211,6 @@
|
|
|
206
211
|
],
|
|
207
212
|
"dependencies": {
|
|
208
213
|
"node-gyp": "^12.3.0",
|
|
209
|
-
"pnpm": "11.5.3",
|
|
210
214
|
"skills": "^1.5.10"
|
|
211
215
|
},
|
|
212
216
|
"engines": {
|
|
@@ -214,7 +218,7 @@
|
|
|
214
218
|
},
|
|
215
219
|
"volta": {
|
|
216
220
|
"node": "24.16.0",
|
|
217
|
-
"pnpm": "11.5.
|
|
221
|
+
"pnpm": "11.5.3"
|
|
218
222
|
},
|
|
219
223
|
"scripts": {
|
|
220
224
|
"dev": "pnpm -F playground dev",
|
|
@@ -233,6 +237,14 @@
|
|
|
233
237
|
"fix:layer": "f(){ pnpm turbo run lint --filter \"./layers/$1\" -- --fix && prettier --write \"layers/$1\"; }; f",
|
|
234
238
|
"fix:app": "f(){ pnpm turbo run lint --filter \"./apps/$1\" -- --fix && prettier --write \"apps/$1\"; }; f",
|
|
235
239
|
"typecheck": "vue-tsc --noEmit -p tsconfig.typecheck.json",
|
|
240
|
+
"test": "vitest run",
|
|
241
|
+
"test:watch": "vitest",
|
|
242
|
+
"test:unit": "vitest run --project unit",
|
|
243
|
+
"test:vitest": "vitest run --project vitest",
|
|
244
|
+
"test:vue": "vitest run --project vue",
|
|
245
|
+
"test:nuxt": "vitest run --project nuxt",
|
|
246
|
+
"test:playwright": "playwright test",
|
|
247
|
+
"test:e2e": "playwright test",
|
|
236
248
|
"clean": "pnpm -r exec rm -rf node_modules .nuxt .output .data && pnpm store prune && pnpm install",
|
|
237
249
|
"cleancache": "pnpm store prune --force",
|
|
238
250
|
"update": "ncu -i",
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { createFeedCatalog } from './feed-catalog'
|
|
4
|
-
|
|
5
|
-
describe('createFeedCatalog', () => {
|
|
6
|
-
it('builds the feed manifest from app config and content collections', () => {
|
|
7
|
-
const catalog = createFeedCatalog({
|
|
8
|
-
site: {
|
|
9
|
-
title: 'Nuxt Layers Playground',
|
|
10
|
-
description: 'Demo and development playground for nuxt-layers.',
|
|
11
|
-
url: 'https://nuxtlayers.netlify.app/',
|
|
12
|
-
},
|
|
13
|
-
feed: {
|
|
14
|
-
collections: ['blog', 'portfolio', 'gallery'],
|
|
15
|
-
defaultCollection: 'blog',
|
|
16
|
-
limit: 30,
|
|
17
|
-
},
|
|
18
|
-
manifest: {
|
|
19
|
-
content: { type: 'page' },
|
|
20
|
-
blog: { type: 'page' },
|
|
21
|
-
portfolio: { type: 'page' },
|
|
22
|
-
gallery: { type: 'page' },
|
|
23
|
-
info: { type: 'data' },
|
|
24
|
-
},
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
expect(catalog.site.title).toBe('Nuxt Layers Playground')
|
|
28
|
-
expect(catalog.feed.defaultCollection).toBe('blog')
|
|
29
|
-
expect(catalog.feed.limit).toBe(30)
|
|
30
|
-
expect(catalog.feed.collections).toEqual(['blog', 'portfolio', 'gallery'])
|
|
31
|
-
expect(catalog.feed.availableCollections).toEqual(['content', 'blog', 'portfolio', 'gallery'])
|
|
32
|
-
expect(catalog.feed.missingCollections).toEqual([])
|
|
33
|
-
expect(catalog.siteRoutes.map((route) => route.path)).toEqual([
|
|
34
|
-
'/feed',
|
|
35
|
-
'/feed/discovery',
|
|
36
|
-
'/feed/rss',
|
|
37
|
-
'/feed/atom',
|
|
38
|
-
'/feed/json',
|
|
39
|
-
])
|
|
40
|
-
expect(catalog.collectionGroups).toHaveLength(3)
|
|
41
|
-
expect(catalog.collectionGroups[1]).toMatchObject({
|
|
42
|
-
collection: 'portfolio',
|
|
43
|
-
label: 'Portfolio',
|
|
44
|
-
})
|
|
45
|
-
expect(catalog.collectionGroups[1]?.routes.map((route) => route.path)).toEqual([
|
|
46
|
-
'/feed/portfolio/rss',
|
|
47
|
-
'/feed/portfolio/atom',
|
|
48
|
-
'/feed/portfolio/json',
|
|
49
|
-
])
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('surfaces configured collections that do not exist in content', () => {
|
|
53
|
-
const catalog = createFeedCatalog({
|
|
54
|
-
feed: {
|
|
55
|
-
collections: ['blog', 'missing'],
|
|
56
|
-
defaultCollection: 'missing',
|
|
57
|
-
},
|
|
58
|
-
manifest: {
|
|
59
|
-
blog: { type: 'page' },
|
|
60
|
-
gallery: { type: 'page' },
|
|
61
|
-
},
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
expect(catalog.feed.missingCollections).toEqual(['missing'])
|
|
65
|
-
expect(catalog.collectionGroups).toHaveLength(1)
|
|
66
|
-
expect(catalog.collectionGroups[0]).toMatchObject({
|
|
67
|
-
collection: 'blog',
|
|
68
|
-
label: 'Blog',
|
|
69
|
-
})
|
|
70
|
-
})
|
|
71
|
-
})
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import contentManifest from '#content/manifest'
|
|
2
|
-
|
|
3
|
-
import { createFeedCatalog } from '../../../app/utils/feed-catalog'
|
|
4
|
-
|
|
5
|
-
export default defineEventHandler((event) => {
|
|
6
|
-
const appConfig = useAppConfig()
|
|
7
|
-
|
|
8
|
-
const requestUrl = getRequestURL(event)
|
|
9
|
-
// Always use the request origin so discovery URLs are reachable from wherever
|
|
10
|
-
// the request came from, not the configured canonical site URL.
|
|
11
|
-
const baseUrl = `${requestUrl.protocol}//${requestUrl.host}`
|
|
12
|
-
|
|
13
|
-
const catalog = createFeedCatalog({
|
|
14
|
-
site: appConfig.site,
|
|
15
|
-
feed: appConfig.feedsLayer?.feed,
|
|
16
|
-
manifest: contentManifest,
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
setHeader(event, 'Cache-Control', 'public, max-age=300, s-maxage=3600')
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
feeds: catalog.collectionGroups.flatMap((group) =>
|
|
23
|
-
group.routes.map((route) => ({
|
|
24
|
-
collection: group.collection,
|
|
25
|
-
format: route.label,
|
|
26
|
-
url: `${baseUrl}${route.path}`,
|
|
27
|
-
contentType: route.contentType ?? 'application/octet-stream',
|
|
28
|
-
}))
|
|
29
|
-
),
|
|
30
|
-
}
|
|
31
|
-
})
|