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
@@ -9,155 +9,177 @@
9
9
  *
10
10
  * This runs before other plugins and ensures the app is ready.
11
11
  */
12
+
13
+ function logCoreDeviceState(isDev: boolean) {
14
+ const device = useDevice()
15
+ if (isDev && device) {
16
+ console.log('[Core Layer] Device detection:', {
17
+ mobile: device.isMobile,
18
+ desktop: device.isDesktop,
19
+ tablet: device.isTablet,
20
+ })
21
+ }
22
+ }
23
+
24
+ function logCoreOnlineState(isDev: boolean) {
25
+ const isOnline = useOnline()
26
+ if (isDev) {
27
+ console.log('[Core Layer] VueUse loaded, online status:', isOnline.value)
28
+ }
29
+ }
30
+
31
+ function logCoreBrowserState(isDev: boolean) {
32
+ const { name, version, engine, os } = useBrowser()
33
+
34
+ if (isDev) {
35
+ console.log('[Core Layer] Browser detection:', {
36
+ name: name.value,
37
+ version: version.value,
38
+ engine: engine.value,
39
+ os: os.value,
40
+ })
41
+ }
42
+ }
43
+
44
+ function logCoreScreenState(isDev: boolean) {
45
+ const { breakpoint, isRetina, orientation } = useScreen()
46
+
47
+ if (isDev) {
48
+ console.log('[Core Layer] Screen detection:', {
49
+ breakpoint: breakpoint.value,
50
+ retina: isRetina.value,
51
+ orientation: orientation.value,
52
+ })
53
+ }
54
+ }
55
+
56
+ function logCoreNetworkState(isDev: boolean) {
57
+ const { connectionQuality, effectiveType, saveData } = useNetworkInfo()
58
+
59
+ if (isDev) {
60
+ console.log('[Core Layer] Network detection:', {
61
+ quality: connectionQuality.value,
62
+ type: effectiveType.value,
63
+ dataSaver: saveData.value,
64
+ })
65
+ }
66
+ }
67
+
68
+ function logCoreFeatureState(isDev: boolean) {
69
+ const { grid, subgrid, containerQueries, webGL, darkMode } = useFeatures()
70
+
71
+ if (isDev) {
72
+ console.log('[Core Layer] Feature detection:', {
73
+ grid: grid.value,
74
+ subgrid: subgrid.value,
75
+ containerQueries: containerQueries.value,
76
+ webGL: webGL.value,
77
+ darkMode: darkMode.value,
78
+ })
79
+ }
80
+ }
81
+
82
+ function logCoreCacheState(isDev: boolean) {
83
+ const { offlineReady, isOnline: cacheOnline } = useCache()
84
+
85
+ if (isDev) {
86
+ console.log('[Core Layer] Cache status:', {
87
+ online: cacheOnline.value,
88
+ offlineReady: offlineReady.value,
89
+ })
90
+ }
91
+ }
92
+
93
+ function logCoreClientDiagnostics(isDev: boolean) {
94
+ if (!import.meta.client) return
95
+
96
+ logCoreBrowserState(isDev)
97
+ logCoreScreenState(isDev)
98
+ logCoreNetworkState(isDev)
99
+ logCoreFeatureState(isDev)
100
+ logCoreCacheState(isDev)
101
+ }
102
+
103
+ function logCoreRenderingState(isDev: boolean) {
104
+ const { mode, isServer, isClient, isHydrated } = useRendering()
105
+
106
+ if (isDev) {
107
+ console.log('[Core Layer] Rendering mode:', {
108
+ mode: mode.value,
109
+ server: isServer.value,
110
+ client: isClient.value,
111
+ hydrated: isHydrated.value,
112
+ })
113
+ }
114
+ }
115
+
116
+ function logCoreEnvironmentState(isDev: boolean) {
117
+ const env = useEnv() as unknown as Record<string, unknown>
118
+
119
+ if (isDev) {
120
+ const publicConfig = env.public as Record<string, unknown> | undefined
121
+
122
+ console.log('[Core Layer] Environment config loaded:', {
123
+ hasPublicConfig: Boolean(publicConfig),
124
+ publicKeys: Object.keys(publicConfig ?? {}),
125
+ })
126
+ }
127
+ }
128
+
129
+ function verifyCoreLayerModules(isDev: boolean) {
130
+ try {
131
+ logCoreDeviceState(isDev)
132
+ logCoreOnlineState(isDev)
133
+ logCoreClientDiagnostics(isDev)
134
+ logCoreRenderingState(isDev)
135
+ logCoreEnvironmentState(isDev)
136
+ } catch (error) {
137
+ console.error('[Core Layer] Module verification failed:', error)
138
+ }
139
+ }
140
+
141
+ type HookableNuxtApp = Pick<ReturnType<typeof useNuxtApp>, 'hook'>
142
+
143
+ function registerCoreLayerHooks(nuxtApp: HookableNuxtApp, isDev: boolean) {
144
+ if (!isDev) return
145
+
146
+ nuxtApp.hook('app:created', () => {
147
+ console.log('✅ [Core Layer] App created')
148
+ })
149
+
150
+ nuxtApp.hook('app:beforeMount', () => {
151
+ console.log('⏳ [Core Layer] App mounting...')
152
+ })
153
+
154
+ nuxtApp.hook('app:mounted', () => {
155
+ console.log('✅ [Core Layer] App mounted')
156
+ })
157
+
158
+ nuxtApp.hook('page:start', () => {
159
+ console.log('📄 [Core Layer] Page navigation started')
160
+ })
161
+
162
+ nuxtApp.hook('page:finish', () => {
163
+ console.log('✅ [Core Layer] Page navigation finished')
164
+ })
165
+ }
166
+
12
167
  export default defineNuxtPlugin({
13
168
  name: 'core:init',
14
169
  setup(nuxtApp) {
15
170
  const config = useAppConfig()
16
- // const isDev = import.meta.dev
17
171
  const isDev = process.env.NODE_ENV === 'development'
18
172
 
19
- // 1. Log initialization (dev only)
20
173
  if (isDev) {
21
174
  console.log('🚀 [Core Layer] Initializing...')
22
175
 
23
176
  console.log('[Core Layer] Config:', config.coreLayer)
24
177
  }
25
178
 
26
- // 2. Verify modules and composables are loaded
27
- try {
28
- // Test @nuxtjs/device module (SSR-safe: returns undefined before device plugin runs)
29
- const device = useDevice()
30
-
31
- if (isDev && device) {
32
- console.log('[Core Layer] Device detection:', {
33
- mobile: device.isMobile,
34
- desktop: device.isDesktop,
35
- tablet: device.isTablet,
36
- })
37
- }
38
-
39
- // Test @vueuse/nuxt module
40
- const isOnline = useOnline()
41
-
42
- if (isDev) {
43
- console.log('[Core Layer] VueUse loaded, online status:', isOnline.value)
44
- }
45
-
46
- // Client-only detection (requires browser APIs)
47
- if (import.meta.client) {
48
- // Test browser detection composable
49
- const { name, version, engine, os } = useBrowser()
50
-
51
- if (isDev) {
52
- console.log('[Core Layer] Browser detection:', {
53
- name: name.value,
54
- version: version.value,
55
- engine: engine.value,
56
- os: os.value,
57
- })
58
- }
59
-
60
- // Test screen/breakpoint composable
61
- const { breakpoint, isRetina, orientation } = useScreen()
62
-
63
- if (isDev) {
64
- console.log('[Core Layer] Screen detection:', {
65
- breakpoint: breakpoint.value,
66
- retina: isRetina.value,
67
- orientation: orientation.value,
68
- })
69
- }
70
-
71
- // Test network info composable
72
- const { connectionQuality, effectiveType, saveData } = useNetworkInfo()
73
-
74
- if (isDev) {
75
- console.log('[Core Layer] Network detection:', {
76
- quality: connectionQuality.value,
77
- type: effectiveType.value,
78
- dataSaver: saveData.value,
79
- })
80
- }
81
-
82
- // Test feature detection composable
83
- const { grid, subgrid, containerQueries, webGL, darkMode } = useFeatures()
84
-
85
- if (isDev) {
86
- console.log('[Core Layer] Feature detection:', {
87
- grid: grid.value,
88
- subgrid: subgrid.value,
89
- containerQueries: containerQueries.value,
90
- webGL: webGL.value,
91
- darkMode: darkMode.value,
92
- })
93
- }
94
-
95
- // Test cache management composable
96
- const { offlineReady, isOnline: cacheOnline } = useCache()
97
-
98
- if (isDev) {
99
- console.log('[Core Layer] Cache status:', {
100
- online: cacheOnline.value,
101
- offlineReady: offlineReady.value,
102
- })
103
- }
104
-
105
- // PWA composable is only available in production
106
- // Use usePWAInfo() from core layer for PWA status
107
- }
108
-
109
- // Test rendering mode detection (works on both server and client)
110
- const { mode, isServer, isClient, isHydrated } = useRendering()
111
-
112
- if (isDev) {
113
- console.log('[Core Layer] Rendering mode:', {
114
- mode: mode.value,
115
- server: isServer.value,
116
- client: isClient.value,
117
- hydrated: isHydrated.value,
118
- })
119
- }
120
-
121
- // Test environment access (works on both server and client)
122
- const env = useEnv() as unknown as Record<string, unknown>
123
-
124
- if (isDev) {
125
- const publicConfig = env.public as Record<string, unknown> | undefined
126
-
127
- console.log('[Core Layer] Environment config loaded:', {
128
- hasPublicConfig: Boolean(publicConfig),
129
- publicKeys: Object.keys(publicConfig ?? {}),
130
- })
131
- }
132
- } catch (error) {
133
- console.error('[Core Layer] Module verification failed:', error)
134
- }
135
-
136
- // 3. App lifecycle hooks (dev logging)
137
- if (isDev) {
138
- nuxtApp.hook('app:created', () => {
139
- console.log('✅ [Core Layer] App created')
140
- })
179
+ verifyCoreLayerModules(isDev)
141
180
 
142
- nuxtApp.hook('app:beforeMount', () => {
143
- console.log('⏳ [Core Layer] App mounting...')
144
- })
145
-
146
- nuxtApp.hook('app:mounted', () => {
147
- console.log('✅ [Core Layer] App mounted')
148
- })
149
-
150
- nuxtApp.hook('page:start', () => {
151
- console.log('📄 [Core Layer] Page navigation started')
152
- })
153
-
154
- nuxtApp.hook('page:finish', () => {
155
- console.log('✅ [Core Layer] Page navigation finished')
156
- })
157
- }
181
+ registerCoreLayerHooks(nuxtApp, isDev)
158
182
 
159
- // 4. Provide global helpers (optional)
160
- // Make core utilities available throughout the app
161
183
  return {
162
184
  provide: {
163
185
  coreLayer: {
@@ -0,0 +1,115 @@
1
+ export type BrowserInfo = {
2
+ name: string
3
+ version: string
4
+ engine: string
5
+ os: string
6
+ }
7
+
8
+ type BrowserRule = {
9
+ match: (ua: string) => boolean
10
+ name: BrowserInfo['name']
11
+ engine: BrowserInfo['engine']
12
+ version: (ua: string) => string
13
+ }
14
+
15
+ type OsRule = {
16
+ match: (ua: string) => boolean
17
+ os: BrowserInfo['os']
18
+ }
19
+
20
+ const OS_RULES: OsRule[] = [
21
+ { match: (ua) => ua.includes('Android'), os: 'android' },
22
+ { match: (ua) => ua.includes('iOS') || ua.includes('iPhone') || ua.includes('iPad'), os: 'ios' },
23
+ { match: (ua) => ua.includes('Win'), os: 'windows' },
24
+ { match: (ua) => ua.includes('Mac'), os: 'macos' },
25
+ { match: (ua) => ua.includes('Linux'), os: 'linux' },
26
+ ]
27
+
28
+ const BROWSER_RULES: BrowserRule[] = [
29
+ {
30
+ match: (ua) => ua.includes('Edg/'),
31
+ name: 'edge',
32
+ engine: 'blink',
33
+ version: (ua) => ua.match(/Edg\/([\d.]+)/)?.[1] ?? '0',
34
+ },
35
+ {
36
+ match: (ua) => ua.includes('Chrome/') && !ua.includes('Edg'),
37
+ name: 'chrome',
38
+ engine: 'blink',
39
+ version: (ua) => ua.match(/Chrome\/([\d.]+)/)?.[1] ?? '0',
40
+ },
41
+ {
42
+ match: (ua) => ua.includes('Safari/') && !ua.includes('Chrome'),
43
+ name: 'safari',
44
+ engine: 'webkit',
45
+ version: (ua) => ua.match(/Version\/([\d.]+)/)?.[1] ?? '0',
46
+ },
47
+ {
48
+ match: (ua) => ua.includes('Firefox/'),
49
+ name: 'firefox',
50
+ engine: 'gecko',
51
+ version: (ua) => ua.match(/Firefox\/([\d.]+)/)?.[1] ?? '0',
52
+ },
53
+ {
54
+ match: (ua) => ua.includes('Opera/') || ua.includes('OPR/'),
55
+ name: 'opera',
56
+ engine: 'blink',
57
+ version: (ua) => ua.match(/(?:Opera|OPR)\/([\d.]+)/)?.[1] ?? '0',
58
+ },
59
+ ]
60
+
61
+ function detectOs(ua: string): BrowserInfo['os'] {
62
+ return OS_RULES.find((rule) => rule.match(ua))?.os ?? 'unknown'
63
+ }
64
+
65
+ function detectBrowser(ua: string): Pick<BrowserInfo, 'name' | 'version' | 'engine'> {
66
+ const rule = BROWSER_RULES.find((candidate) => candidate.match(ua))
67
+ if (!rule) return { name: 'unknown', version: '0', engine: 'unknown' }
68
+ return {
69
+ name: rule.name,
70
+ version: rule.version(ua),
71
+ engine: rule.engine,
72
+ }
73
+ }
74
+
75
+ export function parseBrowserInfo(userAgent?: string | null): BrowserInfo {
76
+ if (!import.meta.client) {
77
+ return {
78
+ name: 'unknown',
79
+ version: '0',
80
+ engine: 'unknown',
81
+ os: 'unknown',
82
+ }
83
+ }
84
+
85
+ const ua = userAgent ?? navigator.userAgent
86
+ return {
87
+ ...detectBrowser(ua),
88
+ os: detectOs(ua),
89
+ }
90
+ }
91
+
92
+ export function getBrowserVersionParts(version: string) {
93
+ const [major = '0', minor = '0'] = version.split('.')
94
+ return {
95
+ major: Number.parseInt(major, 10) || 0,
96
+ minor: Number.parseInt(minor, 10) || 0,
97
+ }
98
+ }
99
+
100
+ // fallow-ignore-next-line complexity
101
+ export function isBrowserAtLeast(version: string, minVersion: string): boolean {
102
+ const current = getBrowserVersionParts(version)
103
+ const [minMajor = '', minMinor = '0'] = minVersion.split('.')
104
+
105
+ if (!minMajor) return false
106
+
107
+ const required = {
108
+ major: Number.parseInt(minMajor, 10) || 0,
109
+ minor: Number.parseInt(minMinor, 10) || 0,
110
+ }
111
+
112
+ if (current.major > required.major) return true
113
+ if (current.major < required.major) return false
114
+ return current.minor >= required.minor
115
+ }
@@ -0,0 +1,40 @@
1
+ import type { FeatureDetection } from '#layers/core/app/types/detection'
2
+
3
+ const PREFIXES = ['supports-', 'no-', 'has-'] as const
4
+
5
+ const SUPPORT_CLASS_MAP: Array<[keyof FeatureDetection, string, string]> = [
6
+ ['grid', 'supports-grid', 'no-grid'],
7
+ ['subgrid', 'supports-subgrid', 'no-subgrid'],
8
+ ['containerQueries', 'supports-container-queries', 'no-container-queries'],
9
+ ['has', 'supports-has', 'no-has'],
10
+ ['aspectRatio', 'supports-aspect-ratio', 'no-aspect-ratio'],
11
+ ['backdropFilter', 'supports-backdrop-filter', 'no-backdrop-filter'],
12
+ ]
13
+
14
+ const EXTRA_CLASS_MAP: Array<[keyof FeatureDetection, string]> = [
15
+ ['intersectionObserver', 'has-intersection-observer'],
16
+ ['resizeObserver', 'has-resize-observer'],
17
+ ['serviceWorker', 'has-service-worker'],
18
+ ['webGL', 'has-webgl'],
19
+ ['webp', 'supports-webp'],
20
+ ['avif', 'supports-avif'],
21
+ ]
22
+
23
+ export function getFeatureClassNames(features: FeatureDetection): string[] {
24
+ const classes = SUPPORT_CLASS_MAP.map(([key, supportedClass, unsupportedClass]) =>
25
+ features[key] ? supportedClass : unsupportedClass
26
+ )
27
+
28
+ classes.push(
29
+ ...EXTRA_CLASS_MAP.flatMap(([key, className]) => (features[key] ? [className] : []))
30
+ )
31
+
32
+ return classes
33
+ }
34
+
35
+ export function removeFeatureClasses(classList: DOMTokenList) {
36
+ const classNames = Array.from(classList).filter((cls) =>
37
+ PREFIXES.some((prefix) => cls.startsWith(prefix))
38
+ )
39
+ classList.remove(...classNames)
40
+ }
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { clamp, groupBy, omit, pick, removeEmpty, safeJsonParse } from './helpers'
4
+
5
+ describe('core helpers', () => {
6
+ it('parses JSON with a fallback', () => {
7
+ expect(safeJsonParse('{"enabled":true}', { enabled: false })).toEqual({ enabled: true })
8
+ expect(safeJsonParse('not-json', { enabled: false })).toEqual({ enabled: false })
9
+ })
10
+
11
+ it('removes nullish values without stripping other falsy values', () => {
12
+ expect(
13
+ removeEmpty({
14
+ title: 'Nuxt Layers',
15
+ count: 0,
16
+ empty: '',
17
+ missing: undefined,
18
+ none: null,
19
+ })
20
+ ).toEqual({
21
+ title: 'Nuxt Layers',
22
+ count: 0,
23
+ empty: '',
24
+ })
25
+ })
26
+
27
+ it('groups, picks, omits, and clamps values', () => {
28
+ const rows = [
29
+ { kind: 'blog', id: 1 },
30
+ { kind: 'docs', id: 2 },
31
+ { kind: 'blog', id: 3 },
32
+ ]
33
+
34
+ expect(groupBy(rows, 'kind')).toEqual({
35
+ blog: [
36
+ { kind: 'blog', id: 1 },
37
+ { kind: 'blog', id: 3 },
38
+ ],
39
+ docs: [{ kind: 'docs', id: 2 }],
40
+ })
41
+ expect(pick({ title: 'Nuxt', description: 'Layers', slug: 'nuxt-layers' }, ['title', 'slug'])).toEqual({
42
+ title: 'Nuxt',
43
+ slug: 'nuxt-layers',
44
+ })
45
+ expect(omit({ title: 'Nuxt', description: 'Layers', slug: 'nuxt-layers' }, ['description'])).toEqual({
46
+ title: 'Nuxt',
47
+ slug: 'nuxt-layers',
48
+ })
49
+ expect(clamp(12, 0, 10)).toBe(10)
50
+ })
51
+ })
@@ -26,7 +26,7 @@
26
26
  class="absolute -left-20 bottom-0 h-80 w-80 rounded-full bg-sky-400/12 blur-3xl dark:bg-sky-500/10"
27
27
  />
28
28
  <div
29
- class="absolute inset-x-8 top-0 h-px bg-gradient-to-r from-transparent via-orange-400/35 to-transparent"
29
+ class="absolute inset-x-8 top-0 h-px bg-linear-to-r from-transparent via-orange-400/35 to-transparent"
30
30
  />
31
31
  </div>
32
32
 
@@ -1,4 +1,6 @@
1
1
  <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
2
4
  import { type FeedRoute } from '../../utils/feed-catalog'
3
5
 
4
6
  const { route, compact = false } = defineProps<{
@@ -7,7 +9,7 @@
7
9
  }>()
8
10
 
9
11
  const routeThemes: Record<
10
- 'index' | 'discovery' | 'rss' | 'atom' | 'json',
12
+ 'index' | 'rss' | 'atom' | 'json',
11
13
  {
12
14
  strip: string
13
15
  dot: string
@@ -17,10 +19,6 @@
17
19
  strip: 'bg-slate-400',
18
20
  dot: 'bg-slate-500',
19
21
  },
20
- discovery: {
21
- strip: 'bg-sky-400',
22
- dot: 'bg-sky-500',
23
- },
24
22
  rss: {
25
23
  strip: 'bg-orange-400',
26
24
  dot: 'bg-orange-500',
@@ -44,10 +42,6 @@
44
42
  return 'Human landing page for the feed catalog.'
45
43
  }
46
44
 
47
- if (route.kind === 'discovery') {
48
- return 'JSON manifest of every exposed collection feed.'
49
- }
50
-
51
45
  return route.contentType ?? 'Reader-friendly syndicated feed.'
52
46
  }
53
47
  </script>
@@ -27,7 +27,7 @@ export type FeedCatalogInput = {
27
27
  }
28
28
 
29
29
  export type FeedRoute = {
30
- kind: 'index' | 'discovery' | 'format'
30
+ kind: 'index' | 'format'
31
31
  label: string
32
32
  path: string
33
33
  format?: FeedFormatKey
@@ -117,7 +117,6 @@ function resolveFeedState(
117
117
  function resolveSiteRoutes(): FeedRoute[] {
118
118
  return [
119
119
  { kind: 'index', label: 'Feed index', path: '/feed' },
120
- { kind: 'discovery', label: 'Discovery index', path: '/feed/discovery' },
121
120
  ...FEED_FORMATS.map(
122
121
  (format) =>
123
122
  ({
@@ -133,9 +132,11 @@ function resolveSiteRoutes(): FeedRoute[] {
133
132
 
134
133
  function resolveCollectionGroups(
135
134
  collections: string[],
136
- availableCollections: string[]
135
+ availableCollections: string[],
136
+ defaultCollection: string
137
137
  ): FeedCollectionGroup[] {
138
138
  return collections
139
+ .filter((collection) => collection !== defaultCollection)
139
140
  .filter((collection) => availableCollections.includes(collection))
140
141
  .map((collection) => ({
141
142
  collection,
@@ -174,6 +175,10 @@ export function createFeedCatalog(input: FeedCatalogInput = {}): FeedCatalog {
174
175
  },
175
176
  feed,
176
177
  siteRoutes: resolveSiteRoutes(),
177
- collectionGroups: resolveCollectionGroups(feed.collections, availableCollections),
178
+ collectionGroups: resolveCollectionGroups(
179
+ feed.collections,
180
+ availableCollections,
181
+ feed.defaultCollection
182
+ ),
178
183
  }
179
184
  }
@@ -19,7 +19,6 @@ export default defineNuxtConfig({
19
19
  // Collection-specific routes (/feed/blog/rss etc.) must be added to
20
20
  // nitro.prerender.routes in the consuming app's nuxt.config.ts.
21
21
  routes: [
22
- '/feed/discovery',
23
22
  '/feed/demo',
24
23
  '/feed/rss',
25
24
  '/feed/atom',
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it, beforeEach, vi } from 'vitest'
2
+
3
+ import type { H3Event } from 'h3'
4
+
5
+ const queryCollectionMock = vi.hoisted(() => vi.fn())
6
+
7
+ vi.mock('@nuxt/content/nitro', () => ({
8
+ queryCollection: queryCollectionMock,
9
+ }))
10
+
11
+ import { getContentFeedItems } from './content-adapter'
12
+
13
+ describe('getContentFeedItems', () => {
14
+ beforeEach(() => {
15
+ queryCollectionMock.mockReset()
16
+ })
17
+
18
+ it('filters drafts, sorts by date, and maps feed items', async () => {
19
+ queryCollectionMock.mockReturnValue({
20
+ all: vi.fn().mockResolvedValue([
21
+ {
22
+ draft: true,
23
+ title: 'Draft post',
24
+ path: '/draft',
25
+ date: '2026-01-04T00:00:00.000Z',
26
+ },
27
+ {
28
+ title: 'Latest post',
29
+ description: 'Newest content',
30
+ path: '/latest',
31
+ date: '2026-01-03T00:00:00.000Z',
32
+ authors: [{ name: 'Ada Lovelace' }],
33
+ tags: ['nuxt'],
34
+ },
35
+ {
36
+ stem: 'Older post',
37
+ _path: '/older',
38
+ createdAt: '2026-01-01T00:00:00.000Z',
39
+ author: { name: 'Grace Hopper' },
40
+ },
41
+ ]),
42
+ })
43
+
44
+ const items = await getContentFeedItems({} as H3Event, 'articles', 2)
45
+
46
+ expect(queryCollectionMock).toHaveBeenCalledWith({}, 'articles')
47
+ expect(items).toEqual([
48
+ {
49
+ title: 'Latest post',
50
+ description: 'Newest content',
51
+ link: '/latest',
52
+ id: '/latest',
53
+ date: new Date('2026-01-03T00:00:00.000Z'),
54
+ author: 'Ada Lovelace',
55
+ tags: ['nuxt'],
56
+ },
57
+ {
58
+ title: 'Older post',
59
+ description: undefined,
60
+ link: '/older',
61
+ id: '/older',
62
+ date: new Date('2026-01-01T00:00:00.000Z'),
63
+ author: 'Grace Hopper',
64
+ tags: undefined,
65
+ },
66
+ ])
67
+ })
68
+ })