kmcom-nuxt-layers 2.2.12 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/starter/app/components/StarterDesignSystem.vue +1913 -0
- package/layers/starter/app/components/StarterHome.vue +407 -0
- package/layers/starter/nuxt.config.ts +15 -0
- package/layers/starter/package.json +10 -0
- 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 +17 -5
- package/layers/feeds/app/utils/feed-catalog.test.ts +0 -71
- 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
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
/**
|