kmcom-nuxt-layers 2.2.12 → 2.2.13

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 (98) 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/theme/app/components/ThemePicker/Menu.vue +3 -25
  86. package/layers/theme/app/composables/useThemePreferenceModels.ts +39 -0
  87. package/layers/theme/server/plugins/theme-fouc.ts +1 -92
  88. package/layers/theme/server/utils/accent-css.ts +75 -0
  89. package/layers/typography/app/composables/typography.ts +3 -7
  90. package/layers/visual/app/composables/accent.ts +2 -9
  91. package/layers/visual/app/composables/gradient.ts +33 -46
  92. package/layers/visual/app/composables/picture.ts +2 -79
  93. package/layers/visual/app/utils/colorTokens.ts +23 -0
  94. package/layers/visual/app/utils/gradientStyle.ts +41 -0
  95. package/layers/visual/app/utils/responsiveSizes.ts +49 -0
  96. package/package.json +15 -4
  97. package/layers/feeds/app/utils/feed-catalog.test.ts +0 -71
  98. package/layers/feeds/server/routes/feed/discovery.get.ts +0 -31
@@ -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.2.13",
5
5
  "description": "Composable Nuxt 4 layers for building scalable Vue applications",
6
6
  "exports": {
7
7
  "./layers/core": "./layers/core/nuxt.config.ts",
@@ -139,16 +139,20 @@
139
139
  "@nuxt/eslint": "^1.15.2",
140
140
  "@nuxt/fonts": "^0.14.0",
141
141
  "@nuxt/image": "^2.0.0",
142
+ "@nuxt/test-utils": "^4.0.3",
143
+ "@playwright/test": "1.61.0",
142
144
  "@nuxt/ui": "latest",
143
145
  "@nuxtjs/device": "^4.0.0",
144
146
  "@oxc-parser/binding-darwin-arm64": "^0.134.0",
145
147
  "@perplex-digital/stylelint-config": "^17.4.0",
146
148
  "@pinia/nuxt": "^0.11.3",
149
+ "@vitejs/plugin-vue": "^6.0.7",
147
150
  "@types/node": "^25.9.1",
148
151
  "@types/three": "^0.184.1",
149
152
  "@typescript-eslint/eslint-plugin": "^8.60.1",
150
153
  "@typescript-eslint/parser": "^8.60.1",
151
154
  "@vue/eslint-config-typescript": "^14.7.0",
155
+ "@vue/test-utils": "^2.4.10",
152
156
  "@vueuse/core": "^14.3.0",
153
157
  "@vueuse/nuxt": "^14.3.0",
154
158
  "@webgpu/glslang": "^0.0.15",
@@ -167,11 +171,11 @@
167
171
  "eslint-plugin-unused-imports": "^4.4.1",
168
172
  "eslint-plugin-vue": "^10.9.2",
169
173
  "fallow": "^2.92.1",
174
+ "happy-dom": "^20.10.3",
170
175
  "netlify-cli": "^26.1.0",
171
176
  "npm-check-updates": "^22.2.2",
172
177
  "nuxt": "^4.4.7",
173
178
  "pinia": "^3.0.4",
174
- "playwright": "^1.60.0",
175
179
  "postcss-html": "^1.8.1",
176
180
  "prettier": "^3.8.3",
177
181
  "prettier-plugin-css-order": "^2.2.0",
@@ -206,7 +210,6 @@
206
210
  ],
207
211
  "dependencies": {
208
212
  "node-gyp": "^12.3.0",
209
- "pnpm": "11.5.3",
210
213
  "skills": "^1.5.10"
211
214
  },
212
215
  "engines": {
@@ -214,7 +217,7 @@
214
217
  },
215
218
  "volta": {
216
219
  "node": "24.16.0",
217
- "pnpm": "11.5.1"
220
+ "pnpm": "11.5.3"
218
221
  },
219
222
  "scripts": {
220
223
  "dev": "pnpm -F playground dev",
@@ -233,6 +236,14 @@
233
236
  "fix:layer": "f(){ pnpm turbo run lint --filter \"./layers/$1\" -- --fix && prettier --write \"layers/$1\"; }; f",
234
237
  "fix:app": "f(){ pnpm turbo run lint --filter \"./apps/$1\" -- --fix && prettier --write \"apps/$1\"; }; f",
235
238
  "typecheck": "vue-tsc --noEmit -p tsconfig.typecheck.json",
239
+ "test": "vitest run",
240
+ "test:watch": "vitest",
241
+ "test:unit": "vitest run --project unit",
242
+ "test:vitest": "vitest run --project vitest",
243
+ "test:vue": "vitest run --project vue",
244
+ "test:nuxt": "vitest run --project nuxt",
245
+ "test:playwright": "playwright test",
246
+ "test:e2e": "playwright test",
236
247
  "clean": "pnpm -r exec rm -rf node_modules .nuxt .output .data && pnpm store prune && pnpm install",
237
248
  "cleancache": "pnpm store prune --force",
238
249
  "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
- })