kmcom-nuxt-layers 1.6.40 → 1.6.43
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/layers/core/app/composables/useErrorLog.ts +6 -11
- package/layers/core/app/composables/useLoading.ts +15 -46
- package/layers/core/app/composables/useScrollGuard.ts +115 -129
- package/layers/forms/app/components/Form/Field.vue +2 -2
- package/layers/forms/app/composables/useFormSchema.ts +1 -3
- package/layers/forms/app/config/fields.ts +12 -6
- package/layers/forms/app/types/fields.ts +2 -20
- package/layers/forms/nuxt.config.ts +0 -10
- package/layers/motion/app/components/Motion/CountUp.vue +39 -0
- package/layers/motion/app/components/Motion/Cursor.vue +101 -0
- package/layers/motion/app/components/Motion/Magnetic.vue +22 -0
- package/layers/motion/app/components/Motion/Tilt.vue +22 -0
- package/layers/motion/app/components/Motion/VelocityEffect.vue +33 -71
- package/layers/motion/app/composables/useCountUp.ts +61 -0
- package/layers/motion/app/composables/useCursorFollower.ts +25 -0
- package/layers/motion/app/composables/useMagneticElement.ts +56 -0
- package/layers/motion/app/composables/useSmoothScroll.ts +11 -0
- package/layers/motion/app/composables/useTiltEffect.ts +58 -0
- package/layers/motion/app/plugins/locomotive-scroll.client.ts +1 -1
- package/layers/motion/app/types/app-config.d.ts +11 -2
- package/layers/routing/app/middleware/02.governance.global.ts +7 -18
- package/layers/routing/app/types/routing.ts +10 -0
- package/layers/routing/app/utils/resolveRoute.ts +31 -0
- package/layers/shader/package.json +2 -1
- package/layers/theme/app/composables/useAccentColor.ts +1 -12
- package/layers/theme/app/composables/useThemeContrast.ts +0 -11
- package/layers/theme/app/composables/useThemeMotion.ts +0 -11
- package/layers/theme/app/composables/useThemeTransparency.ts +0 -14
- package/layers/theme/app/plugins/theme.client.ts +20 -3
- package/layers/theme/app/types/app-config.d.ts +2 -2
- package/layers/ui/app/components/Base/Modal.vue +0 -1
- package/layers/ui/app/composables/color.ts +1 -32
- package/layers/ui/app/utils/createModal.ts +11 -2
- package/package.json +28 -26
- package/layers/content/app/.DS_Store +0 -0
- package/layers/content/app/pages/blog/[slug].vue +0 -19
- package/layers/content/app/pages/blog/index.vue +0 -22
- package/layers/core/app/.DS_Store +0 -0
- package/layers/core/app/assets/.DS_Store +0 -0
- package/layers/forms/app/.DS_Store +0 -0
- package/layers/layout/app/.DS_Store +0 -0
- package/layers/motion/app/.DS_Store +0 -0
- package/layers/shader/app/.DS_Store +0 -0
- package/layers/theme/app/.DS_Store +0 -0
- package/layers/ui/app/.DS_Store +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const {
|
|
3
|
+
strength = 0.3,
|
|
4
|
+
radius = 100,
|
|
5
|
+
damping = 30,
|
|
6
|
+
stiffness = 300,
|
|
7
|
+
} = defineProps<{
|
|
8
|
+
strength?: number
|
|
9
|
+
radius?: number
|
|
10
|
+
damping?: number
|
|
11
|
+
stiffness?: number
|
|
12
|
+
}>()
|
|
13
|
+
|
|
14
|
+
const el = ref<HTMLElement | null>(null)
|
|
15
|
+
const { isActive } = useMagneticElement(el, { strength, radius, damping, stiffness })
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<div ref="el" class="will-change-transform">
|
|
20
|
+
<slot :is-active="isActive" />
|
|
21
|
+
</div>
|
|
22
|
+
</template>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const {
|
|
3
|
+
maxTilt = 15,
|
|
4
|
+
perspective = 800,
|
|
5
|
+
damping = 40,
|
|
6
|
+
stiffness = 250,
|
|
7
|
+
} = defineProps<{
|
|
8
|
+
maxTilt?: number
|
|
9
|
+
perspective?: number
|
|
10
|
+
damping?: number
|
|
11
|
+
stiffness?: number
|
|
12
|
+
}>()
|
|
13
|
+
|
|
14
|
+
const el = ref<HTMLElement | null>(null)
|
|
15
|
+
useTiltEffect(el, { maxTilt, perspective, damping, stiffness })
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<div ref="el" class="will-change-transform" style="transform-style: preserve-3d">
|
|
20
|
+
<slot />
|
|
21
|
+
</div>
|
|
22
|
+
</template>
|
|
@@ -9,78 +9,43 @@ type EffectType =
|
|
|
9
9
|
| 'translateY'
|
|
10
10
|
| 'hueRotate'
|
|
11
11
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
* Lerp factor for smoothing (0.1 = very smooth, 0.3 = responsive)
|
|
28
|
-
*/
|
|
29
|
-
smoothFactor?: number
|
|
30
|
-
/**
|
|
31
|
-
* Tag to render as
|
|
32
|
-
*/
|
|
33
|
-
as?: string
|
|
34
|
-
}>(),
|
|
35
|
-
{
|
|
36
|
-
smooth: true,
|
|
37
|
-
smoothFactor: 0.15,
|
|
38
|
-
as: 'div',
|
|
39
|
-
}
|
|
40
|
-
)
|
|
41
|
-
|
|
12
|
+
const {
|
|
13
|
+
effect,
|
|
14
|
+
intensity = undefined,
|
|
15
|
+
damping = 15,
|
|
16
|
+
stiffness = 150,
|
|
17
|
+
as = 'div',
|
|
18
|
+
} = defineProps<{
|
|
19
|
+
effect: EffectType
|
|
20
|
+
intensity?: number
|
|
21
|
+
damping?: number
|
|
22
|
+
stiffness?: number
|
|
23
|
+
as?: string
|
|
24
|
+
}>()
|
|
25
|
+
|
|
26
|
+
const { gsap } = useGsap()
|
|
42
27
|
const { velocity } = useSmoothScroll()
|
|
43
28
|
|
|
44
|
-
// Smoothed velocity for stable animations
|
|
45
29
|
const smoothVelocity = ref(0)
|
|
46
30
|
const smoothAbsVelocity = ref(0)
|
|
47
|
-
let animationFrame: number | null = null
|
|
48
31
|
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
|
|
32
|
+
function tick() {
|
|
33
|
+
const d = damping / 1000
|
|
34
|
+
const s = stiffness / 1000
|
|
52
35
|
|
|
53
|
-
|
|
54
|
-
smoothVelocity.value
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
props.smoothFactor
|
|
59
|
-
)
|
|
36
|
+
smoothVelocity.value += (velocity.value - smoothVelocity.value) * s
|
|
37
|
+
smoothVelocity.value *= 1 - d
|
|
38
|
+
|
|
39
|
+
smoothAbsVelocity.value += (Math.abs(velocity.value) - smoothAbsVelocity.value) * s
|
|
40
|
+
smoothAbsVelocity.value *= 1 - d
|
|
60
41
|
|
|
61
42
|
if (Math.abs(smoothVelocity.value) < 0.01) smoothVelocity.value = 0
|
|
62
43
|
if (smoothAbsVelocity.value < 0.01) smoothAbsVelocity.value = 0
|
|
63
|
-
|
|
64
|
-
animationFrame = requestAnimationFrame(updateSmoothedValues)
|
|
65
44
|
}
|
|
66
45
|
|
|
67
|
-
onMounted(() =>
|
|
68
|
-
|
|
69
|
-
animationFrame = requestAnimationFrame(updateSmoothedValues)
|
|
70
|
-
}
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
onUnmounted(() => {
|
|
74
|
-
if (animationFrame) cancelAnimationFrame(animationFrame)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
// Get the active velocity value
|
|
78
|
-
const activeVelocity = computed(() => (props.smooth ? smoothVelocity.value : velocity.value))
|
|
79
|
-
const activeAbsVelocity = computed(() =>
|
|
80
|
-
props.smooth ? smoothAbsVelocity.value : Math.abs(velocity.value)
|
|
81
|
-
)
|
|
46
|
+
onMounted(() => gsap.ticker.add(tick))
|
|
47
|
+
onUnmounted(() => gsap.ticker.remove(tick))
|
|
82
48
|
|
|
83
|
-
// Default intensities per effect type
|
|
84
49
|
const defaultIntensities: Record<EffectType, number> = {
|
|
85
50
|
skew: 3,
|
|
86
51
|
scale: 0.12,
|
|
@@ -92,15 +57,14 @@ const defaultIntensities: Record<EffectType, number> = {
|
|
|
92
57
|
hueRotate: 45,
|
|
93
58
|
}
|
|
94
59
|
|
|
95
|
-
const
|
|
60
|
+
const activeIntensity = computed(() => intensity ?? defaultIntensities[effect])
|
|
96
61
|
|
|
97
|
-
// Compute the style based on effect type
|
|
98
62
|
const effectStyle = computed(() => {
|
|
99
|
-
const vel =
|
|
100
|
-
const absVel =
|
|
101
|
-
const int =
|
|
63
|
+
const vel = smoothVelocity.value
|
|
64
|
+
const absVel = smoothAbsVelocity.value
|
|
65
|
+
const int = activeIntensity.value
|
|
102
66
|
|
|
103
|
-
switch (
|
|
67
|
+
switch (effect) {
|
|
104
68
|
case 'skew':
|
|
105
69
|
return { transform: `skewX(${Math.max(-15, Math.min(15, vel * int))}deg)` }
|
|
106
70
|
case 'scale':
|
|
@@ -122,13 +86,11 @@ const effectStyle = computed(() => {
|
|
|
122
86
|
}
|
|
123
87
|
})
|
|
124
88
|
|
|
125
|
-
// Classes for performance optimization
|
|
126
89
|
const effectClasses = computed(() => {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
classes.push('will-change-transform')
|
|
90
|
+
if (['skew', 'scale', 'rotate', 'translateY'].includes(effect)) {
|
|
91
|
+
return ['will-change-transform']
|
|
130
92
|
}
|
|
131
|
-
return
|
|
93
|
+
return []
|
|
132
94
|
})
|
|
133
95
|
</script>
|
|
134
96
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { MaybeRefOrGetter } from 'vue'
|
|
2
|
+
import type { ScrollTrigger as GSAPScrollTrigger } from 'gsap/ScrollTrigger'
|
|
3
|
+
|
|
4
|
+
interface UseCountUpOptions {
|
|
5
|
+
to: number
|
|
6
|
+
from?: number
|
|
7
|
+
duration?: number
|
|
8
|
+
ease?: string
|
|
9
|
+
format?: (n: number) => string
|
|
10
|
+
once?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useCountUp(elementRef: MaybeRefOrGetter<HTMLElement | null>, opts: UseCountUpOptions) {
|
|
14
|
+
const { gsap, ScrollTrigger } = useGsap()
|
|
15
|
+
|
|
16
|
+
const displayValue = ref(String(opts.from ?? 0))
|
|
17
|
+
const isComplete = ref(false)
|
|
18
|
+
|
|
19
|
+
const format = opts.format ?? ((n: number) => n.toLocaleString())
|
|
20
|
+
const counter = { value: opts.from ?? 0 }
|
|
21
|
+
|
|
22
|
+
let scrollTriggerInstance: GSAPScrollTrigger | null = null
|
|
23
|
+
let tween: gsap.core.Tween | null = null
|
|
24
|
+
|
|
25
|
+
function startCount() {
|
|
26
|
+
tween = gsap.to(counter, {
|
|
27
|
+
value: opts.to,
|
|
28
|
+
duration: opts.duration ?? 2,
|
|
29
|
+
ease: opts.ease ?? 'power2.out',
|
|
30
|
+
onUpdate: () => {
|
|
31
|
+
displayValue.value = format(Math.round(counter.value))
|
|
32
|
+
},
|
|
33
|
+
onComplete: () => {
|
|
34
|
+
isComplete.value = true
|
|
35
|
+
displayValue.value = format(opts.to)
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onMounted(async () => {
|
|
41
|
+
await nextTick()
|
|
42
|
+
displayValue.value = format(opts.from ?? 0)
|
|
43
|
+
|
|
44
|
+
const el = toValue(elementRef)
|
|
45
|
+
if (!el) return
|
|
46
|
+
|
|
47
|
+
scrollTriggerInstance = ScrollTrigger.create({
|
|
48
|
+
trigger: el,
|
|
49
|
+
start: 'top 80%',
|
|
50
|
+
once: opts.once ?? true,
|
|
51
|
+
onEnter: startCount,
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
onUnmounted(() => {
|
|
56
|
+
tween?.kill()
|
|
57
|
+
scrollTriggerInstance?.kill()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
return { displayValue, isComplete }
|
|
61
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MaybeRef } from 'vue'
|
|
2
|
+
|
|
3
|
+
export function useCursorFollower(opts: { smoothing?: MaybeRef<number> } = {}) {
|
|
4
|
+
const { gsap } = useGsap()
|
|
5
|
+
const { x: mouseX, y: mouseY } = useMouse({ type: 'client' })
|
|
6
|
+
|
|
7
|
+
const x = ref(0)
|
|
8
|
+
const y = ref(0)
|
|
9
|
+
|
|
10
|
+
function tick() {
|
|
11
|
+
const factor = unref(opts.smoothing) ?? 0.15
|
|
12
|
+
x.value += (mouseX.value - x.value) * factor
|
|
13
|
+
y.value += (mouseY.value - y.value) * factor
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
onMounted(() => {
|
|
17
|
+
x.value = mouseX.value
|
|
18
|
+
y.value = mouseY.value
|
|
19
|
+
gsap.ticker.add(tick)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
onUnmounted(() => gsap.ticker.remove(tick))
|
|
23
|
+
|
|
24
|
+
return { x, y }
|
|
25
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export function useMagneticElement(
|
|
2
|
+
elementRef: Ref<HTMLElement | null>,
|
|
3
|
+
opts: {
|
|
4
|
+
strength?: number
|
|
5
|
+
radius?: number
|
|
6
|
+
damping?: number
|
|
7
|
+
stiffness?: number
|
|
8
|
+
} = {},
|
|
9
|
+
) {
|
|
10
|
+
const { gsap } = useGsap()
|
|
11
|
+
const { elementX, elementY, elementWidth, elementHeight } = useMouseInElement(elementRef)
|
|
12
|
+
|
|
13
|
+
const currentX = ref(0)
|
|
14
|
+
const currentY = ref(0)
|
|
15
|
+
const isActive = ref(false)
|
|
16
|
+
|
|
17
|
+
function tick() {
|
|
18
|
+
const strength = opts.strength ?? 0.3
|
|
19
|
+
const radius = opts.radius ?? 100
|
|
20
|
+
const damping = (opts.damping ?? 30) / 1000
|
|
21
|
+
const stiffness = (opts.stiffness ?? 300) / 1000
|
|
22
|
+
|
|
23
|
+
const dx = elementX.value - elementWidth.value / 2
|
|
24
|
+
const dy = elementY.value - elementHeight.value / 2
|
|
25
|
+
const dist = Math.sqrt(dx * dx + dy * dy)
|
|
26
|
+
|
|
27
|
+
let targetX = 0
|
|
28
|
+
let targetY = 0
|
|
29
|
+
|
|
30
|
+
if (dist < radius) {
|
|
31
|
+
isActive.value = true
|
|
32
|
+
targetX = dx * strength
|
|
33
|
+
targetY = dy * strength
|
|
34
|
+
} else {
|
|
35
|
+
isActive.value = false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
currentX.value += (targetX - currentX.value) * stiffness
|
|
39
|
+
currentX.value *= 1 - damping
|
|
40
|
+
currentY.value += (targetY - currentY.value) * stiffness
|
|
41
|
+
currentY.value *= 1 - damping
|
|
42
|
+
|
|
43
|
+
if (elementRef.value) {
|
|
44
|
+
gsap.set(elementRef.value, { x: currentX.value, y: currentY.value })
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
onMounted(() => gsap.ticker.add(tick))
|
|
49
|
+
|
|
50
|
+
onUnmounted(() => {
|
|
51
|
+
gsap.ticker.remove(tick)
|
|
52
|
+
if (elementRef.value) gsap.set(elementRef.value, { x: 0, y: 0 })
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
return { isActive }
|
|
56
|
+
}
|
|
@@ -23,12 +23,19 @@ interface ScrollToOptions {
|
|
|
23
23
|
*/
|
|
24
24
|
export function useSmoothScroll() {
|
|
25
25
|
const nuxtApp = useNuxtApp()
|
|
26
|
+
const appConfig = useAppConfig()
|
|
26
27
|
|
|
27
28
|
// Get the Locomotive Scroll instance from the reactive ref provided by the plugin
|
|
28
29
|
const locomotiveScroll = computed<LocomotiveScroll | undefined>(
|
|
29
30
|
() => (nuxtApp.$locomotiveScroll as Ref<LocomotiveScroll | null>)?.value ?? undefined
|
|
30
31
|
)
|
|
31
32
|
|
|
33
|
+
// true if smooth scroll is configured to run (globally or per-route)
|
|
34
|
+
const isEnabled = computed(() => (appConfig.motion?.smoothScroll ?? true) !== false)
|
|
35
|
+
|
|
36
|
+
// true once the LocomotiveScroll instance is actually initialised on this route
|
|
37
|
+
const isReady = computed(() => locomotiveScroll.value != null)
|
|
38
|
+
|
|
32
39
|
// Reactive scroll state from the plugin
|
|
33
40
|
const scrollState = computed(
|
|
34
41
|
() =>
|
|
@@ -110,6 +117,10 @@ export function useSmoothScroll() {
|
|
|
110
117
|
// Locomotive Scroll instance
|
|
111
118
|
locomotiveScroll,
|
|
112
119
|
|
|
120
|
+
// Scroll availability
|
|
121
|
+
isEnabled,
|
|
122
|
+
isReady,
|
|
123
|
+
|
|
113
124
|
// Reactive scroll state
|
|
114
125
|
scrollY,
|
|
115
126
|
velocity,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export function useTiltEffect(
|
|
2
|
+
elementRef: Ref<HTMLElement | null>,
|
|
3
|
+
opts: {
|
|
4
|
+
maxTilt?: number
|
|
5
|
+
perspective?: number
|
|
6
|
+
damping?: number
|
|
7
|
+
stiffness?: number
|
|
8
|
+
} = {},
|
|
9
|
+
) {
|
|
10
|
+
const { gsap } = useGsap()
|
|
11
|
+
const { elementX, elementY, elementWidth, elementHeight, isOutside } =
|
|
12
|
+
useMouseInElement(elementRef)
|
|
13
|
+
|
|
14
|
+
const currentRotateX = ref(0)
|
|
15
|
+
const currentRotateY = ref(0)
|
|
16
|
+
|
|
17
|
+
function tick() {
|
|
18
|
+
const maxTilt = opts.maxTilt ?? 15
|
|
19
|
+
const perspective = opts.perspective ?? 800
|
|
20
|
+
const damping = (opts.damping ?? 40) / 1000
|
|
21
|
+
const stiffness = (opts.stiffness ?? 250) / 1000
|
|
22
|
+
|
|
23
|
+
const width = elementWidth.value || 1
|
|
24
|
+
const height = elementHeight.value || 1
|
|
25
|
+
|
|
26
|
+
let targetRotateX = 0
|
|
27
|
+
let targetRotateY = 0
|
|
28
|
+
|
|
29
|
+
if (!isOutside.value) {
|
|
30
|
+
const nx = (elementX.value / width) * 2 - 1
|
|
31
|
+
const ny = (elementY.value / height) * 2 - 1
|
|
32
|
+
targetRotateY = nx * maxTilt
|
|
33
|
+
targetRotateX = -ny * maxTilt
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
currentRotateX.value += (targetRotateX - currentRotateX.value) * stiffness
|
|
37
|
+
currentRotateX.value *= 1 - damping
|
|
38
|
+
currentRotateY.value += (targetRotateY - currentRotateY.value) * stiffness
|
|
39
|
+
currentRotateY.value *= 1 - damping
|
|
40
|
+
|
|
41
|
+
if (elementRef.value) {
|
|
42
|
+
gsap.set(elementRef.value, {
|
|
43
|
+
rotateX: currentRotateX.value,
|
|
44
|
+
rotateY: currentRotateY.value,
|
|
45
|
+
transformPerspective: perspective,
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
onMounted(() => gsap.ticker.add(tick))
|
|
51
|
+
|
|
52
|
+
onUnmounted(() => {
|
|
53
|
+
gsap.ticker.remove(tick)
|
|
54
|
+
if (elementRef.value) gsap.set(elementRef.value, { rotateX: 0, rotateY: 0 })
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return { rotateX: currentRotateX, rotateY: currentRotateY }
|
|
58
|
+
}
|
|
@@ -45,7 +45,7 @@ export default defineNuxtPlugin(() => {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const router = useRouter()
|
|
48
|
-
const appConfig = useAppConfig()
|
|
48
|
+
const appConfig = useAppConfig()
|
|
49
49
|
const smoothScroll: boolean | string[] = appConfig.motion?.smoothScroll ?? true
|
|
50
50
|
|
|
51
51
|
if (smoothScroll === true) {
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
declare module '@nuxt/schema' {
|
|
2
2
|
interface AppConfigInput {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
motion?: {
|
|
4
|
+
smoothScroll?: boolean | string[]
|
|
5
|
+
gsapScrollTrigger?: boolean
|
|
6
|
+
lenis?: {
|
|
7
|
+
duration?: number
|
|
8
|
+
orientation?: 'vertical' | 'horizontal'
|
|
9
|
+
gestureOrientation?: 'vertical' | 'horizontal'
|
|
10
|
+
smoothWheel?: boolean
|
|
11
|
+
smoothTouch?: boolean
|
|
12
|
+
touchMultiplier?: number
|
|
13
|
+
}
|
|
5
14
|
}
|
|
6
15
|
}
|
|
7
16
|
}
|
|
@@ -1,27 +1,16 @@
|
|
|
1
1
|
import { useFeatures } from '../composables/useFeatures'
|
|
2
|
+
import { resolveRoute } from '../utils/resolveRoute'
|
|
2
3
|
|
|
3
4
|
export default defineNuxtRouteMiddleware((to) => {
|
|
4
|
-
const { config
|
|
5
|
+
const { config } = useRoutingConfig()
|
|
5
6
|
const { resolve } = useFeatures()
|
|
6
|
-
const meta = to.meta
|
|
7
7
|
|
|
8
|
-
if (config.debug) console.log('[routing] governance check', to.path, meta)
|
|
8
|
+
if (config.debug) console.log('[routing] governance check', to.path, to.meta)
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
if (isStrictMode() && !meta.feature) {
|
|
12
|
-
throw createError({ statusCode: 404 })
|
|
13
|
-
}
|
|
10
|
+
const resolution = resolveRoute(to.meta, config, resolve)
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
if (isLayerDefaultDeny() && meta.__fromLayer && !meta.feature) {
|
|
17
|
-
throw createError({ statusCode: 404 })
|
|
18
|
-
}
|
|
12
|
+
if (config.debug) console.log('[routing] resolution', resolution)
|
|
19
13
|
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
const variant = resolve(meta.feature)
|
|
23
|
-
if (config.debug) console.log(`[routing] feature "${meta.feature}" resolved to "${variant}"`)
|
|
24
|
-
|
|
25
|
-
if (variant === 'disabled') throw createError({ statusCode: 404 })
|
|
26
|
-
if (variant === 'beta' || variant === 'coming-soon') return navigateTo('/coming-soon')
|
|
14
|
+
if (resolution.outcome === 'deny') throw createError({ statusCode: 404 })
|
|
15
|
+
if (resolution.outcome === 'redirect') return navigateTo(resolution.to)
|
|
27
16
|
})
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
export type FeatureValue = 'enabled' | 'disabled' | 'beta' | 'coming-soon'
|
|
2
2
|
export type RoutingPreset = 'simple' | 'marketing' | 'product' | 'enterprise'
|
|
3
3
|
|
|
4
|
+
export type RouteResolution =
|
|
5
|
+
| { outcome: 'allow' }
|
|
6
|
+
| { outcome: 'deny' }
|
|
7
|
+
| { outcome: 'redirect'; to: string }
|
|
8
|
+
|
|
4
9
|
export interface RoutingLayerConfig {
|
|
5
10
|
preset: RoutingPreset
|
|
6
11
|
strictDefaultDeny: boolean
|
|
7
12
|
layerDefaultDeny: boolean
|
|
13
|
+
betaRedirect: string
|
|
8
14
|
runtimeFlags: boolean
|
|
9
15
|
debug: boolean
|
|
10
16
|
maintenance: { enabled: boolean; allowRoutes: string[] }
|
|
@@ -16,6 +22,7 @@ export const ROUTING_PRESETS: Record<RoutingPreset, Omit<RoutingLayerConfig, 'pr
|
|
|
16
22
|
simple: {
|
|
17
23
|
strictDefaultDeny: false,
|
|
18
24
|
layerDefaultDeny: false,
|
|
25
|
+
betaRedirect: '/coming-soon',
|
|
19
26
|
runtimeFlags: false,
|
|
20
27
|
debug: false,
|
|
21
28
|
maintenance: { enabled: false, allowRoutes: ['/maintenance'] },
|
|
@@ -24,6 +31,7 @@ export const ROUTING_PRESETS: Record<RoutingPreset, Omit<RoutingLayerConfig, 'pr
|
|
|
24
31
|
marketing: {
|
|
25
32
|
strictDefaultDeny: false,
|
|
26
33
|
layerDefaultDeny: true,
|
|
34
|
+
betaRedirect: '/coming-soon',
|
|
27
35
|
runtimeFlags: false,
|
|
28
36
|
debug: false,
|
|
29
37
|
maintenance: { enabled: true, allowRoutes: ['/maintenance'] },
|
|
@@ -32,6 +40,7 @@ export const ROUTING_PRESETS: Record<RoutingPreset, Omit<RoutingLayerConfig, 'pr
|
|
|
32
40
|
product: {
|
|
33
41
|
strictDefaultDeny: false,
|
|
34
42
|
layerDefaultDeny: true,
|
|
43
|
+
betaRedirect: '/coming-soon',
|
|
35
44
|
runtimeFlags: true,
|
|
36
45
|
debug: false,
|
|
37
46
|
maintenance: { enabled: true, allowRoutes: ['/maintenance'] },
|
|
@@ -40,6 +49,7 @@ export const ROUTING_PRESETS: Record<RoutingPreset, Omit<RoutingLayerConfig, 'pr
|
|
|
40
49
|
enterprise: {
|
|
41
50
|
strictDefaultDeny: true,
|
|
42
51
|
layerDefaultDeny: true,
|
|
52
|
+
betaRedirect: '/coming-soon',
|
|
43
53
|
runtimeFlags: true,
|
|
44
54
|
debug: false,
|
|
45
55
|
maintenance: { enabled: true, allowRoutes: ['/maintenance'] },
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { FeatureValue, RouteResolution, RoutingLayerConfig } from '../types/routing'
|
|
2
|
+
|
|
3
|
+
interface RouteMeta {
|
|
4
|
+
feature?: string
|
|
5
|
+
__fromLayer?: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function resolveRoute(
|
|
9
|
+
meta: RouteMeta,
|
|
10
|
+
config: RoutingLayerConfig,
|
|
11
|
+
resolveFeature: (name: string) => FeatureValue,
|
|
12
|
+
): RouteResolution {
|
|
13
|
+
if (config.strictDefaultDeny && !meta.feature) {
|
|
14
|
+
return { outcome: 'deny' }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (config.layerDefaultDeny && meta.__fromLayer && !meta.feature) {
|
|
18
|
+
return { outcome: 'deny' }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!meta.feature) return { outcome: 'allow' }
|
|
22
|
+
|
|
23
|
+
const variant = resolveFeature(meta.feature)
|
|
24
|
+
|
|
25
|
+
if (variant === 'disabled') return { outcome: 'deny' }
|
|
26
|
+
if (variant === 'beta' || variant === 'coming-soon') {
|
|
27
|
+
return { outcome: 'redirect', to: config.betaRedirect }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { outcome: 'allow' }
|
|
31
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
import { createSharedComposable, useLocalStorage } from '@vueuse/core'
|
|
3
2
|
import type { AccentColor } from '#layers/theme/app/types/theme'
|
|
4
3
|
|
|
5
4
|
export const useAccentColor = createSharedComposable(() => {
|
|
6
5
|
const appConfig = useAppConfig()
|
|
7
6
|
|
|
8
|
-
const defaultAccent = (
|
|
7
|
+
const defaultAccent = (appConfig.themeLayer?.defaultAccent ?? 'blue') as AccentColor
|
|
9
8
|
const accent = useLocalStorage<AccentColor>('theme-colour', defaultAccent)
|
|
10
9
|
|
|
11
10
|
const pageAccent = ref<AccentColor | null>(null)
|
|
@@ -19,16 +18,6 @@ export const useAccentColor = createSharedComposable(() => {
|
|
|
19
18
|
pageAccent.value = color
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
if (import.meta.client) {
|
|
23
|
-
watch(
|
|
24
|
-
activeAccent,
|
|
25
|
-
(color) => {
|
|
26
|
-
document.documentElement.setAttribute('data-theme-colour', color)
|
|
27
|
-
},
|
|
28
|
-
{ immediate: true }
|
|
29
|
-
)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
21
|
return {
|
|
33
22
|
accent: readonly(accent),
|
|
34
23
|
setAccent,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
import { createSharedComposable, useLocalStorage, usePreferredContrast } from '@vueuse/core'
|
|
3
2
|
import type { PreferenceOverride } from '#layers/theme/app/types/theme'
|
|
4
3
|
|
|
@@ -16,16 +15,6 @@ export const useThemeContrast = createSharedComposable(() => {
|
|
|
16
15
|
contrastOverride.value = value
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
if (import.meta.client) {
|
|
20
|
-
watch(
|
|
21
|
-
effectiveHighContrast,
|
|
22
|
-
(high) => {
|
|
23
|
-
document.documentElement.setAttribute('data-theme-contrast', high ? 'high' : 'standard')
|
|
24
|
-
},
|
|
25
|
-
{ immediate: true }
|
|
26
|
-
)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
18
|
return {
|
|
30
19
|
contrastOverride: readonly(contrastOverride),
|
|
31
20
|
setContrastOverride,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
import { createSharedComposable, useLocalStorage, usePreferredReducedMotion } from '@vueuse/core'
|
|
3
2
|
import type { PreferenceOverride } from '#layers/theme/app/types/theme'
|
|
4
3
|
|
|
@@ -16,16 +15,6 @@ export const useThemeMotion = createSharedComposable(() => {
|
|
|
16
15
|
motionOverride.value = value
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
if (import.meta.client) {
|
|
20
|
-
watch(
|
|
21
|
-
effectiveReducedMotion,
|
|
22
|
-
(reduced) => {
|
|
23
|
-
document.documentElement.setAttribute('data-theme-motion', reduced ? 'reduced' : 'full')
|
|
24
|
-
},
|
|
25
|
-
{ immediate: true }
|
|
26
|
-
)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
18
|
return {
|
|
30
19
|
motionOverride: readonly(motionOverride),
|
|
31
20
|
setMotionOverride,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
import {
|
|
3
2
|
createSharedComposable,
|
|
4
3
|
useLocalStorage,
|
|
@@ -20,19 +19,6 @@ export const useThemeTransparency = createSharedComposable(() => {
|
|
|
20
19
|
transparencyOverride.value = value
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
if (import.meta.client) {
|
|
24
|
-
watch(
|
|
25
|
-
effectiveReducedTransparency,
|
|
26
|
-
(reduced) => {
|
|
27
|
-
document.documentElement.setAttribute(
|
|
28
|
-
'data-theme-transparency',
|
|
29
|
-
reduced ? 'reduced' : 'full'
|
|
30
|
-
)
|
|
31
|
-
},
|
|
32
|
-
{ immediate: true }
|
|
33
|
-
)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
22
|
return {
|
|
37
23
|
transparencyOverride: readonly(transparencyOverride),
|
|
38
24
|
setTransparencyOverride,
|