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.
Files changed (102) hide show
  1. package/docs/FEEDS.md +1 -2
  2. package/layers/animations/app/composables/useMagneticElement.ts +11 -9
  3. package/layers/animations/app/composables/useTiltEffect.ts +11 -9
  4. package/layers/animations/app/utils/pointerMotion.ts +31 -0
  5. package/layers/canvas/app/components/ShaderCanvas.vue +2 -2
  6. package/layers/content/app/composables/useCollectionItems.ts +28 -0
  7. package/layers/content/app/composables/useGalleryItems.ts +8 -14
  8. package/layers/content/app/composables/usePortfolioItems.ts +10 -18
  9. package/layers/core/app/composables/useBrowser.ts +9 -82
  10. package/layers/core/app/composables/useFeatures.ts +3 -27
  11. package/layers/core/app/plugins/init.ts +157 -135
  12. package/layers/core/app/utils/browserInfo.ts +115 -0
  13. package/layers/core/app/utils/featureClasses.ts +40 -0
  14. package/layers/core/app/utils/helpers.test.ts +51 -0
  15. package/layers/feeds/app/components/Feeds/Index.vue +1 -1
  16. package/layers/feeds/app/components/Feeds/RouteCard.vue +3 -9
  17. package/layers/feeds/app/utils/feed-catalog.ts +9 -4
  18. package/layers/feeds/nuxt.config.ts +0 -1
  19. package/layers/feeds/server/utils/content-adapter.test.ts +68 -0
  20. package/layers/feeds/server/utils/content-adapter.ts +2 -22
  21. package/layers/feeds/server/utils/feed-author.ts +32 -0
  22. package/layers/feeds/server/utils/feed-config.ts +88 -0
  23. package/layers/feeds/server/utils/feed-service.ts +11 -30
  24. package/layers/feeds/server/utils/feed-xml.ts +26 -0
  25. package/layers/feeds/server/utils/formats/rss.ts +10 -15
  26. package/layers/feeds/server/utils/formats.test.ts +71 -0
  27. package/layers/forms/app/components/Form/Field.vue +42 -30
  28. package/layers/forms/app/utils/fieldProps.ts +65 -0
  29. package/layers/layout/app/components/Layout/Grid/Item.vue +29 -146
  30. package/layers/layout/app/utils/gridPlacementStyle.ts +195 -0
  31. package/layers/mailer/app/types/mailer.ts +7 -25
  32. package/layers/mailer/server/utils/email.ts +28 -13
  33. package/layers/mailer/server/utils/hooks.ts +1 -20
  34. package/layers/navigation/app/composables/useSite.ts +2 -9
  35. package/layers/navigation/app/utils/site.ts +26 -0
  36. package/layers/routing/app/utils/resolveRoute.test.ts +47 -0
  37. package/layers/routing/app/utils/resolveRoute.ts +19 -10
  38. package/layers/scripts/app/composables/useAnalytics.ts +8 -41
  39. package/layers/scripts/app/composables/useGtm.ts +6 -13
  40. package/layers/scripts/app/utils/scriptClients.ts +70 -0
  41. package/layers/scroll/app/composables/useSmoothScroll.ts +9 -43
  42. package/layers/scroll/app/utils/scroll.ts +103 -0
  43. package/layers/seo/app/composables/useSeoConfig.ts +3 -9
  44. package/layers/seo/app/utils/seoConfig.ts +38 -0
  45. package/layers/shader/app/components/Material/AmbientAurora.client.vue +11 -33
  46. package/layers/shader/app/components/Material/AmbientFlow.client.vue +10 -37
  47. package/layers/shader/app/components/Material/AmbientGradientMesh.client.vue +10 -37
  48. package/layers/shader/app/components/Material/AmbientNebula.client.vue +12 -37
  49. package/layers/shader/app/components/Material/AmbientOcean.client.vue +9 -33
  50. package/layers/shader/app/components/Material/Gradient.client.vue +25 -46
  51. package/layers/shader/app/components/Material/Image.client.vue +10 -55
  52. package/layers/shader/app/components/Material/Node.client.vue +18 -5
  53. package/layers/shader/app/components/Material/Noise.client.vue +9 -43
  54. package/layers/shader/app/components/Preset/ThemeBubble.client.vue +2 -1
  55. package/layers/shader/app/components/Preset/ThemeFlow.client.vue +2 -1
  56. package/layers/shader/app/components/Preset/ThemeGradient.client.vue +2 -1
  57. package/layers/shader/app/components/Preset/ThemeLavaLamp.client.vue +2 -1
  58. package/layers/shader/app/components/Preset/ThemePlasma.client.vue +2 -1
  59. package/layers/shader/app/components/Preset/ThemeWave.client.vue +2 -1
  60. package/layers/shader/app/components/Shader/Background.client.vue +44 -24
  61. package/layers/shader/app/composables/useAmbientMaterials.ts +5 -1
  62. package/layers/shader/app/composables/useShader.ts +38 -23
  63. package/layers/shader/app/composables/useShaderGraph.ts +11 -6
  64. package/layers/shader/app/composables/useShaderMixBlend.ts +4 -4
  65. package/layers/shader/app/composables/useShaderRuntime.ts +0 -1
  66. package/layers/shader/app/composables/useShaderVec2.ts +2 -4
  67. package/layers/shader/app/composables/useThemePreset.ts +34 -8
  68. package/layers/shader/app/composables/useUniformWatchers.ts +15 -0
  69. package/layers/shader/app/composables/useUniforms.ts +0 -1
  70. package/layers/shader/app/shaders/common/blend.ts +4 -4
  71. package/layers/shader/app/shaders/common/effects.ts +38 -21
  72. package/layers/shader/app/shaders/common/grain.ts +46 -49
  73. package/layers/shader/app/shaders/common/lighting.ts +17 -15
  74. package/layers/shader/app/shaders/common/math.ts +2 -4
  75. package/layers/shader/app/shaders/common/nodes.ts +17 -0
  76. package/layers/shader/app/shaders/common/palette.ts +21 -11
  77. package/layers/shader/app/shaders/common/patterns.ts +25 -14
  78. package/layers/shader/app/shaders/common/shapes.ts +97 -88
  79. package/layers/shader/app/shaders/common/uv.ts +33 -34
  80. package/layers/shader/app/shaders/createMaterial.ts +92 -78
  81. package/layers/shader/app/shaders/layers/paperShading.ts +22 -10
  82. package/layers/shader/app/shaders/layers/shaderGradient.ts +46 -21
  83. package/layers/shader/app/utils/tsl/tween.ts +2 -4
  84. package/layers/shader/package.json +5 -1
  85. package/layers/starter/app/components/StarterDesignSystem.vue +1913 -0
  86. package/layers/starter/app/components/StarterHome.vue +407 -0
  87. package/layers/starter/nuxt.config.ts +15 -0
  88. package/layers/starter/package.json +10 -0
  89. package/layers/theme/app/components/ThemePicker/Menu.vue +3 -25
  90. package/layers/theme/app/composables/useThemePreferenceModels.ts +39 -0
  91. package/layers/theme/server/plugins/theme-fouc.ts +1 -92
  92. package/layers/theme/server/utils/accent-css.ts +75 -0
  93. package/layers/typography/app/composables/typography.ts +3 -7
  94. package/layers/visual/app/composables/accent.ts +2 -9
  95. package/layers/visual/app/composables/gradient.ts +33 -46
  96. package/layers/visual/app/composables/picture.ts +2 -79
  97. package/layers/visual/app/utils/colorTokens.ts +23 -0
  98. package/layers/visual/app/utils/gradientStyle.ts +41 -0
  99. package/layers/visual/app/utils/responsiveSizes.ts +49 -0
  100. package/package.json +17 -5
  101. package/layers/feeds/app/utils/feed-catalog.test.ts +0 -71
  102. 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
- if (typeof value === 'number') {
28
- if (numericFormatter) return numericFormatter(value)
29
- if (prefix) return `${prefix}-[${value}]`
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: resolveColor(resolved),
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, GradientStop } from '../types/gradient'
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
- const DIRECTION_MAP: Record<string, string> = {
13
- 'to-t': 'to top',
14
- 'to-tr': 'to top right',
15
- 'to-r': 'to right',
16
- 'to-br': 'to bottom right',
17
- 'to-b': 'to bottom',
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 resolveColor(stop: GradientStop): string {
24
- const { color, shade = 500, opacity } = stop
25
- if (color === 'transparent') return 'transparent'
26
- if (color === 'white') {
27
- return opacity !== undefined ? `rgb(255 255 255 / ${opacity / 100})` : '#ffffff'
28
- }
29
- if (color === 'black') {
30
- return opacity !== undefined ? `rgb(0 0 0 / ${opacity / 100})` : '#000000'
31
- }
32
- const v = `var(--ui-color-${color}-${shade})`
33
- return opacity !== undefined ? `color-mix(in srgb, ${v} ${opacity}%, transparent)` : v
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 buildStyle(cfg: GradientConfig): CSSProperties {
37
- const from = resolveColor(cfg.from)
38
- const to = resolveColor(cfg.to)
39
- const via = cfg.via ? resolveColor(cfg.via) : undefined
40
- const stops = via ? `${from}, ${via}, ${to}` : `${from}, ${to}`
36
+ function resolveGradientPresetByName(
37
+ name: string,
38
+ presets: Record<string, GradientConfig> | undefined
39
+ ) {
40
+ return presets?.[name] ?? DEFAULT_CONFIG
41
+ }
41
42
 
42
- if (cfg.shape === 'radial') {
43
- return { backgroundImage: `radial-gradient(circle, ${stops})` }
44
- }
45
- if (cfg.shape === 'conic') {
46
- return { backgroundImage: `conic-gradient(${stops})` }
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
- // Combine all min-width breakpoints with their values
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.2.12",
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.1"
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
- })