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
package/docs/FEEDS.md CHANGED
@@ -106,7 +106,6 @@ Plus site-wide shorthand and index routes:
106
106
  |-----|---------|
107
107
  | `/feed/rss`, `/feed/atom`, `/feed/json` | Shorthand → serves `defaultCollection` |
108
108
  | `/feed` | JSON index of all configured feeds |
109
- | `/feed/discovery` | Machine-readable feed discovery document |
110
109
  | `/feed/style.xsl` | Browser stylesheet (applied automatically) |
111
110
 
112
111
  Opening any feed URL in a browser shows a styled page thanks to the XSLT stylesheet — no app work required.
@@ -115,7 +114,7 @@ Opening any feed URL in a browser shows a styled page thanks to the XSLT stylesh
115
114
 
116
115
  ## Step 4 — Prerender collection routes (required for static / Netlify)
117
116
 
118
- Feed routes are **server routes**. The prerender crawler can't discover `/feed/:collection/*` because nothing links to them with an `<a href>`. The feeds layer already prerenders the **shorthand** routes (`/feed`, `/feed/rss`, `/feed/atom`, `/feed/json`, `/feed/style.xsl`, `/feed/discovery`). You must list the **collection-specific** routes in your app's `nuxt.config.ts` — one trio per collection in `feedsLayer.feed.collections`:
117
+ Feed routes are **server routes**. The prerender crawler can't discover `/feed/:collection/*` because nothing links to them with an `<a href>`. The feeds layer already prerenders the **shorthand** routes (`/feed`, `/feed/rss`, `/feed/atom`, `/feed/json`, `/feed/style.xsl`). You must list the **collection-specific** routes in your app's `nuxt.config.ts` — one trio per collection in `feedsLayer.feed.collections`:
119
118
 
120
119
  ```ts
121
120
  // nuxt.config.ts
@@ -1,3 +1,5 @@
1
+ import { resolvePointerSpring, usePointerMotionFrame } from '../utils/pointerMotion'
2
+
1
3
  export function useMagneticElement(
2
4
  elementRef: Ref<HTMLElement | null>,
3
5
  opts: {
@@ -7,7 +9,6 @@ export function useMagneticElement(
7
9
  stiffness?: number
8
10
  } = {}
9
11
  ) {
10
- const { gsap } = useGsap()
11
12
  const { elementX, elementY, elementWidth, elementHeight } = useMouseInElement(elementRef)
12
13
 
13
14
  const currentX = ref(0)
@@ -33,20 +34,21 @@ export function useMagneticElement(
33
34
  targetY = dy * strength
34
35
  }
35
36
 
36
- currentX.value += (targetX - currentX.value) * stiffness
37
- currentX.value *= 1 - damping
38
- currentY.value += (targetY - currentY.value) * stiffness
39
- currentY.value *= 1 - damping
37
+ currentX.value = resolvePointerSpring(currentX.value, targetX, {
38
+ damping,
39
+ stiffness,
40
+ })
41
+ currentY.value = resolvePointerSpring(currentY.value, targetY, {
42
+ damping,
43
+ stiffness,
44
+ })
40
45
 
41
46
  if (elementRef.value) {
42
47
  gsap.set(elementRef.value, { x: currentX.value, y: currentY.value })
43
48
  }
44
49
  }
45
50
 
46
- onMounted(() => gsap.ticker.add(tick))
47
-
48
- onUnmounted(() => {
49
- gsap.ticker.remove(tick)
51
+ const { gsap } = usePointerMotionFrame(tick, () => {
50
52
  if (elementRef.value) gsap.set(elementRef.value, { x: 0, y: 0 })
51
53
  })
52
54
 
@@ -1,3 +1,5 @@
1
+ import { resolvePointerSpring, usePointerMotionFrame } from '../utils/pointerMotion'
2
+
1
3
  export function useTiltEffect(
2
4
  elementRef: Ref<HTMLElement | null>,
3
5
  opts: {
@@ -7,7 +9,6 @@ export function useTiltEffect(
7
9
  stiffness?: number
8
10
  } = {}
9
11
  ) {
10
- const { gsap } = useGsap()
11
12
  const { elementX, elementY, elementWidth, elementHeight, isOutside } =
12
13
  useMouseInElement(elementRef)
13
14
 
@@ -33,10 +34,14 @@ export function useTiltEffect(
33
34
  targetRotateX = -ny * maxTilt
34
35
  }
35
36
 
36
- currentRotateX.value += (targetRotateX - currentRotateX.value) * stiffness
37
- currentRotateX.value *= 1 - damping
38
- currentRotateY.value += (targetRotateY - currentRotateY.value) * stiffness
39
- currentRotateY.value *= 1 - damping
37
+ currentRotateX.value = resolvePointerSpring(currentRotateX.value, targetRotateX, {
38
+ damping,
39
+ stiffness,
40
+ })
41
+ currentRotateY.value = resolvePointerSpring(currentRotateY.value, targetRotateY, {
42
+ damping,
43
+ stiffness,
44
+ })
40
45
 
41
46
  if (elementRef.value) {
42
47
  gsap.set(elementRef.value, {
@@ -47,10 +52,7 @@ export function useTiltEffect(
47
52
  }
48
53
  }
49
54
 
50
- onMounted(() => gsap.ticker.add(tick))
51
-
52
- onUnmounted(() => {
53
- gsap.ticker.remove(tick)
55
+ const { gsap } = usePointerMotionFrame(tick, () => {
54
56
  if (elementRef.value) gsap.set(elementRef.value, { rotateX: 0, rotateY: 0 })
55
57
  })
56
58
 
@@ -0,0 +1,31 @@
1
+ export function resolvePointerSpring(
2
+ current: number,
3
+ target: number,
4
+ options: {
5
+ damping?: number
6
+ stiffness?: number
7
+ } = {}
8
+ ) {
9
+ const damping = options.damping ?? 0
10
+ const stiffness = options.stiffness ?? 0
11
+ const next = current + (target - current) * stiffness
12
+ return next * (1 - damping)
13
+ }
14
+
15
+ export function usePointerMotionFrame(
16
+ update: () => void,
17
+ cleanup?: () => void
18
+ ) {
19
+ const { gsap } = useGsap()
20
+
21
+ onMounted(() => {
22
+ gsap.ticker.add(update)
23
+ })
24
+
25
+ onUnmounted(() => {
26
+ gsap.ticker.remove(update)
27
+ cleanup?.()
28
+ })
29
+
30
+ return { gsap }
31
+ }
@@ -9,7 +9,7 @@
9
9
  SRGBColorSpace,
10
10
  type ToneMapping,
11
11
  } from 'three'
12
- import { WebGPURenderer } from 'three/webgpu'
12
+ import * as ThreeWebGPU from 'three/webgpu'
13
13
 
14
14
  const {
15
15
  clearColor = '#000000',
@@ -70,7 +70,7 @@
70
70
  // WebGPU renderer factory — TresJS v5 calls renderer.init() automatically
71
71
  // when the returned object has isRenderer === true (three/webgpu Renderer base class)
72
72
  function webgpuRendererFactory({ canvas }: TresRendererSetupContext) {
73
- const r = new WebGPURenderer({
73
+ const r = new ThreeWebGPU.WebGPURenderer({
74
74
  canvas: unref(canvas),
75
75
  antialias,
76
76
  // WebGPU has no 'default' power preference — omit to let the browser decide
@@ -0,0 +1,28 @@
1
+ import type { Collections } from '@nuxt/content'
2
+
3
+ type CollectionItem<TCollection extends keyof Collections> = Collections[TCollection]
4
+
5
+ export function useCollectionItems<TCollection extends keyof Collections>(args: {
6
+ key: string
7
+ collection: TCollection
8
+ sort: (a: CollectionItem<TCollection>, b: CollectionItem<TCollection>) => number
9
+ options?: { limit?: number | undefined }
10
+ filter?: (item: CollectionItem<TCollection>) => boolean
11
+ }) {
12
+ const { key, collection, sort, options = {}, filter } = args
13
+
14
+ return useContentData(key, async () => {
15
+ let items = (await queryCollection(collection).all()) as CollectionItem<TCollection>[]
16
+ items = items.sort(sort)
17
+
18
+ if (filter) {
19
+ items = items.filter(filter)
20
+ }
21
+
22
+ if (options.limit !== undefined) {
23
+ items = items.slice(0, options.limit)
24
+ }
25
+
26
+ return items
27
+ })
28
+ }
@@ -1,21 +1,15 @@
1
1
  import type { GalleryQueryOptions } from '../types/content'
2
+ import { useCollectionItems } from './useCollectionItems'
2
3
 
3
4
  export function useGalleryItems(options: GalleryQueryOptions = {}) {
4
5
  const { tags, limit } = options
5
6
 
6
- return useContentData('gallery-items', async () => {
7
- let items = (await queryCollection('gallery').all()).sort((a, b) =>
8
- (b.date ?? '').localeCompare(a.date ?? '')
9
- )
10
-
11
- if (tags?.length) {
12
- items = items.filter((item) => item.tags?.some((tag: string) => tags.includes(tag)))
13
- }
14
-
15
- if (limit) {
16
- items = items.slice(0, limit)
17
- }
18
-
19
- return items
7
+ return useCollectionItems({
8
+ key: 'gallery-items',
9
+ collection: 'gallery',
10
+ sort: (a, b) => (b.date ?? '').localeCompare(a.date ?? ''),
11
+ options: { limit },
12
+ filter: (item) =>
13
+ !tags?.length || Boolean(item.tags?.some((tag: string) => tags.includes(tag))),
20
14
  })
21
15
  }
@@ -1,25 +1,17 @@
1
1
  import type { PortfolioQueryOptions } from '../types/content'
2
+ import { useCollectionItems } from './useCollectionItems'
2
3
 
3
4
  export function usePortfolioItems(options: PortfolioQueryOptions = {}) {
4
5
  const { featured, tags, limit } = options
5
6
 
6
- return useContentData('portfolio-items', async () => {
7
- let items = (await queryCollection('portfolio').all()).sort(
8
- (a, b) => (b.year ?? 0) - (a.year ?? 0)
9
- )
10
-
11
- if (featured !== undefined) {
12
- items = items.filter((item) => item.featured === featured)
13
- }
14
-
15
- if (tags?.length) {
16
- items = items.filter((item) => item.tags?.some((tag: string) => tags.includes(tag)))
17
- }
18
-
19
- if (limit) {
20
- items = items.slice(0, limit)
21
- }
22
-
23
- return items
7
+ return useCollectionItems({
8
+ key: 'portfolio-items',
9
+ collection: 'portfolio',
10
+ sort: (a, b) => (b.year ?? 0) - (a.year ?? 0),
11
+ options: { limit },
12
+ // fallow-ignore-next-line complexity
13
+ filter: (item) =>
14
+ (featured === undefined || item.featured === featured) &&
15
+ (!tags?.length || Boolean(item.tags?.some((tag: string) => tags.includes(tag)))),
24
16
  })
25
17
  }
@@ -1,63 +1,9 @@
1
- // composables/useBrowser.ts
2
-
3
- type BrowserInfo = {
4
- name: string
5
- version: string
6
- engine: string
7
- os: string
8
- }
9
-
10
- /**
11
- * Parse user agent to detect browser information
12
- */
13
- function parseBrowserInfo(): BrowserInfo {
14
- if (!import.meta.client) {
15
- return {
16
- name: 'unknown',
17
- version: '0',
18
- engine: 'unknown',
19
- os: 'unknown',
20
- }
21
- }
22
-
23
- const ua = navigator.userAgent
24
- let name = 'unknown'
25
- let version = '0'
26
- let engine = 'unknown'
27
- let os = 'unknown'
28
-
29
- // Detect OS
30
- if (ua.includes('Win')) os = 'windows'
31
- else if (ua.includes('Mac')) os = 'macos'
32
- else if (ua.includes('Linux')) os = 'linux'
33
- else if (ua.includes('Android')) os = 'android'
34
- else if (ua.includes('iOS') || ua.includes('iPhone') || ua.includes('iPad')) os = 'ios'
35
-
36
- // Detect browser (order matters - check most specific first)
37
- if (ua.includes('Edg/')) {
38
- name = 'edge'
39
- engine = 'blink'
40
- version = ua.match(/Edg\/([\d.]+)/)?.[1] || '0'
41
- } else if (ua.includes('Chrome/') && !ua.includes('Edg')) {
42
- name = 'chrome'
43
- engine = 'blink'
44
- version = ua.match(/Chrome\/([\d.]+)/)?.[1] || '0'
45
- } else if (ua.includes('Safari/') && !ua.includes('Chrome')) {
46
- name = 'safari'
47
- engine = 'webkit'
48
- version = ua.match(/Version\/([\d.]+)/)?.[1] || '0'
49
- } else if (ua.includes('Firefox/')) {
50
- name = 'firefox'
51
- engine = 'gecko'
52
- version = ua.match(/Firefox\/([\d.]+)/)?.[1] || '0'
53
- } else if (ua.includes('Opera/') || ua.includes('OPR/')) {
54
- name = 'opera'
55
- engine = 'blink'
56
- version = ua.match(/(?:Opera|OPR)\/([\d.]+)/)?.[1] || '0'
57
- }
58
-
59
- return { name, version, engine, os }
60
- }
1
+ import {
2
+ getBrowserVersionParts,
3
+ isBrowserAtLeast,
4
+ parseBrowserInfo,
5
+ type BrowserInfo,
6
+ } from '#layers/core/app/utils/browserInfo'
61
7
 
62
8
  /**
63
9
  * Browser detection composable
@@ -93,15 +39,11 @@ export function useBrowser() {
93
39
 
94
40
  // Version helpers
95
41
  const majorVersion = computed(() => {
96
- const parts = info.value.version.split('.')
97
- const major = parts[0]
98
- return major ? parseInt(major, 10) : 0
42
+ return getBrowserVersionParts(info.value.version).major
99
43
  })
100
44
 
101
45
  const minorVersion = computed(() => {
102
- const parts = info.value.version.split('.')
103
- const minor = parts[1] || '0'
104
- return parseInt(minor, 10)
46
+ return getBrowserVersionParts(info.value.version).minor
105
47
  })
106
48
 
107
49
  /**
@@ -109,22 +51,7 @@ export function useBrowser() {
109
51
  * @param minVersion - Minimum version string (e.g., "100" or "100.0")
110
52
  */
111
53
  const isAtLeast = (minVersion: string): boolean => {
112
- const parts = minVersion.split('.')
113
- const minMajor = parts[0]
114
- const minMinor = parts[1] || '0'
115
-
116
- if (!minMajor) return false
117
-
118
- const major = majorVersion.value
119
- const minor = minorVersion.value
120
-
121
- const minMajorNum = parseInt(minMajor, 10)
122
- const minMinorNum = parseInt(minMinor, 10)
123
-
124
- if (major > minMajorNum) return true
125
- if (major === minMajorNum && minor >= minMinorNum) return true
126
-
127
- return false
54
+ return isBrowserAtLeast(info.value.version, minVersion)
128
55
  }
129
56
 
130
57
  return {
@@ -1,5 +1,6 @@
1
1
  // composables/useFeatures.ts
2
2
  import type { FeatureDetection } from '#layers/core/app/types/detection'
3
+ import { getFeatureClassNames, removeFeatureClasses } from '#layers/core/app/utils/featureClasses'
3
4
  import {
4
5
  usePreferredContrast,
5
6
  usePreferredDark,
@@ -139,34 +140,9 @@ async function checkImageFormat(format: 'webp' | 'avif'): Promise<boolean> {
139
140
  function applyFeatureClasses(features: FeatureDetection) {
140
141
  if (!import.meta.client || !document.documentElement) return
141
142
 
142
- // Remove old classes first
143
143
  const htmlClasses = document.documentElement.classList
144
- const classesToRemove = Array.from(htmlClasses).filter(
145
- (cls) => cls.startsWith('supports-') || cls.startsWith('no-') || cls.startsWith('has-')
146
- )
147
- htmlClasses.remove(...classesToRemove)
148
-
149
- const classes: string[] = []
150
-
151
- // CSS features
152
- classes.push(features.grid ? 'supports-grid' : 'no-grid')
153
- classes.push(features.subgrid ? 'supports-subgrid' : 'no-subgrid')
154
- classes.push(features.containerQueries ? 'supports-container-queries' : 'no-container-queries')
155
- classes.push(features.has ? 'supports-has' : 'no-has')
156
- classes.push(features.aspectRatio ? 'supports-aspect-ratio' : 'no-aspect-ratio')
157
- classes.push(features.backdropFilter ? 'supports-backdrop-filter' : 'no-backdrop-filter')
158
-
159
- // JS APIs
160
- if (features.intersectionObserver) classes.push('has-intersection-observer')
161
- if (features.resizeObserver) classes.push('has-resize-observer')
162
- if (features.serviceWorker) classes.push('has-service-worker')
163
- if (features.webGL) classes.push('has-webgl')
164
-
165
- // Image formats
166
- if (features.webp) classes.push('supports-webp')
167
- if (features.avif) classes.push('supports-avif')
168
-
169
- htmlClasses.add(...classes)
144
+ removeFeatureClasses(htmlClasses)
145
+ htmlClasses.add(...getFeatureClassNames(features))
170
146
  }
171
147
 
172
148
  /**