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.
- 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/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 +15 -4
- package/layers/feeds/app/utils/feed-catalog.test.ts +0 -71
- 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
|
-
|
|
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
|
-
|
|
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-
|
|
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' | '
|
|
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' | '
|
|
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(
|
|
178
|
+
collectionGroups: resolveCollectionGroups(
|
|
179
|
+
feed.collections,
|
|
180
|
+
availableCollections,
|
|
181
|
+
feed.defaultCollection
|
|
182
|
+
),
|
|
178
183
|
}
|
|
179
184
|
}
|
|
@@ -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
|
+
})
|