kmcom-nuxt-layers 6.2.1 → 6.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/MIGRATION-v6.3.0.md +234 -0
- package/docs/MIGRATION.md +2 -0
- package/docs/component-levels.md +116 -0
- package/layers/content/app/components/Blog/ItemPage.vue +2 -7
- package/layers/content/app/components/Blog/List.vue +9 -2
- package/layers/content/app/components/Blog/Page.vue +9 -19
- package/layers/content/app/components/Gallery/Detail.vue +1 -0
- package/layers/content/app/components/Gallery/Grid.vue +9 -2
- package/layers/content/app/components/Gallery/ImageDetail.vue +0 -6
- package/layers/content/app/components/Gallery/ImagePage.vue +10 -6
- package/layers/content/app/components/Gallery/Page.vue +9 -19
- package/layers/content/app/components/Gallery/SlugPage.vue +2 -7
- package/layers/content/app/components/NuxtContent/Detail.vue +20 -11
- package/layers/content/app/components/NuxtContent/IndexTemplate.vue +21 -0
- package/layers/content/app/components/NuxtContent/List.vue +6 -1
- package/layers/content/app/components/Portfolio/ColorPalette.vue +2 -2
- package/layers/content/app/components/Portfolio/ItemPage.vue +2 -6
- package/layers/content/app/components/Portfolio/List.vue +19 -8
- package/layers/content/app/components/Portfolio/Page.vue +9 -19
- package/layers/content/app/composables/useContentItemSeo.ts +19 -0
- package/layers/content/app/composables/useContentSection.ts +9 -0
- package/layers/content/app/utils/galleryImageMeta.test.ts +30 -0
- package/layers/content/app/utils/galleryImageMeta.ts +11 -0
- package/layers/content/nuxt.config.ts +1 -1
- package/layers/core/app/assets/css/core.css +5 -11
- package/layers/core/app/assets/css/tokens/z-index.css +16 -0
- package/layers/core/app/components/DiagnosticsPage.vue +41 -41
- package/layers/core/app/components/LoadingScreen.vue +2 -0
- package/layers/delivery/feeds/app/components/Feeds/Index.vue +36 -75
- package/layers/delivery/feeds/app/components/Feeds/RouteCard.vue +6 -8
- package/layers/design-system/theming/nuxt.config.ts +1 -3
- package/layers/design-system/visual/app/components/Base/Modal.vue +34 -86
- package/layers/design-system/visual/app/components/HoverDimGroup.vue +46 -0
- package/layers/design-system/visual/app/components/Progress/Bar.vue +9 -29
- package/layers/motion/animations/app/components/Motion/Cursor.vue +1 -1
- package/layers/motion/animations/app/components/Motion/Marquee.vue +5 -0
- package/layers/motion/animations/app/components/Motion/MarqueeText.vue +2 -1
- package/layers/motion/animations/app/components/Motion/Staggered.test.ts +40 -0
- package/layers/motion/animations/app/components/Motion/Staggered.vue +37 -42
- package/layers/motion/animations/app/components/Motion/TextReveal.vue +8 -1
- package/layers/motion/animations/app/components/Motion/VelocityEffect.vue +6 -0
- package/layers/motion/animations/app/composables/useCountUp.ts +6 -0
- package/layers/motion/animations/app/composables/useCursorFollower.ts +4 -1
- package/layers/motion/animations/app/utils/pointerMotion.ts +7 -2
- package/layers/motion/scroll/app/components/Motion/HorizontalScroll.vue +9 -0
- package/layers/motion/scroll/app/components/Motion/Parallax.vue +13 -12
- package/layers/motion/scroll/app/components/Motion/PinnedSection.vue +16 -9
- package/layers/motion/scroll/app/composables/useReducedMotion.test.ts +23 -0
- package/layers/motion/scroll/app/composables/useReducedMotion.ts +14 -0
- package/layers/motion/scroll/app/utils/parallax.test.ts +24 -0
- package/layers/motion/scroll/app/utils/parallax.ts +16 -0
- package/layers/motion/scroll/nuxt.config.ts +1 -1
- package/layers/structure/layout/app/components/Layout/Grid/Debug.vue +1 -1
- package/layers/structure/layout/app/components/Layout/Page/index.vue +9 -10
- package/layers/structure/navigation/app/components/Links/Group.vue +12 -38
- package/layers/structure/navigation/app/components/Links/Named.vue +9 -0
- package/layers/structure/navigation/app/components/Mast/Footer.vue +1 -1
- package/layers/structure/navigation/app/components/Mast/Nav.vue +3 -1
- package/layers/structure/navigation/app/components/Mast/NavModal.vue +34 -39
- package/layers/structure/navigation/app/components/Site/Subtitle.vue +10 -0
- package/layers/structure/navigation/app/components/Site/Title.vue +8 -0
- package/layers/structure/navigation/nuxt.config.ts +7 -1
- package/package.json +1 -1
- /package/layers/{design-system/theming/app/assets/css/tokens.css → core/app/assets/css/tokens/elevation-motion.css} +0 -0
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
//
|
|
2
|
+
// Explicit import so Staggered mounts under vitest's 'vue' project (no Nuxt auto-imports)
|
|
3
|
+
// — same exemption as ElementTw.vue (eslint.config.mjs).
|
|
4
|
+
import { onBeforeUnmount, onMounted, useTemplateRef } from 'vue'
|
|
5
|
+
|
|
3
6
|
const {
|
|
4
7
|
staggerDelay = 100,
|
|
5
8
|
initialDelay = 0,
|
|
@@ -13,61 +16,53 @@
|
|
|
13
16
|
animation?: string
|
|
14
17
|
}>()
|
|
15
18
|
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const slots = useSlots()
|
|
19
|
+
// Stagger the rendered DOM children, not slot vnodes: a `ref`/`class` on `<slot>` becomes a
|
|
20
|
+
// slot prop and never reaches an element, which left every item hidden for good.
|
|
21
|
+
const reduced = useReducedMotion()
|
|
22
|
+
const root = useTemplateRef<HTMLElement>('root')
|
|
23
|
+
const timers: ReturnType<typeof setTimeout>[] = []
|
|
22
24
|
|
|
23
|
-
// Methods
|
|
24
|
-
const setItemRef = (el: HTMLElement | null, index: number) => {
|
|
25
|
-
if (el) {
|
|
26
|
-
items.value[index] = el
|
|
27
|
-
animatedItems.value[index] = false
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Lifecycle
|
|
32
25
|
onMounted(() => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}, 100)
|
|
26
|
+
const items = [...(root.value?.children ?? [])]
|
|
27
|
+
if (reduced.value) {
|
|
28
|
+
root.value?.setAttribute('data-reduced', '')
|
|
29
|
+
return items.forEach((item) => item.classList.add('animated'))
|
|
30
|
+
}
|
|
31
|
+
items.forEach((item, index) => {
|
|
32
|
+
timers.push(
|
|
33
|
+
setTimeout(
|
|
34
|
+
() => item.classList.add(`animate-${animation}`, 'animated'),
|
|
35
|
+
initialDelay + index * staggerDelay
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
})
|
|
47
39
|
})
|
|
40
|
+
|
|
41
|
+
onBeforeUnmount(() => timers.forEach(clearTimeout))
|
|
48
42
|
</script>
|
|
49
43
|
|
|
50
44
|
<template>
|
|
51
|
-
<div class="motion-staggered">
|
|
52
|
-
<slot
|
|
53
|
-
v-for="(_, index) in slots.default?.()"
|
|
54
|
-
:key="index"
|
|
55
|
-
:ref="(el: HTMLElement | null) => setItemRef(el, index)"
|
|
56
|
-
:class="{ animated: animatedItems[index] }"
|
|
57
|
-
/>
|
|
45
|
+
<div ref="root" class="motion-staggered">
|
|
46
|
+
<slot />
|
|
58
47
|
</div>
|
|
59
48
|
</template>
|
|
60
49
|
|
|
61
50
|
<style scoped>
|
|
62
51
|
/* stylelint-disable selector-pseudo-class-no-unknown */
|
|
63
|
-
.motion-staggered :deep(
|
|
52
|
+
.motion-staggered > :deep(*) {
|
|
53
|
+
transform: translateY(20px);
|
|
54
|
+
opacity: 0;
|
|
55
|
+
transition:
|
|
56
|
+
opacity 0.3s ease-out,
|
|
57
|
+
transform 0.3s ease-out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.motion-staggered > :deep(.animated) {
|
|
64
61
|
transform: translateY(0);
|
|
65
62
|
opacity: 1;
|
|
66
63
|
}
|
|
67
64
|
|
|
68
|
-
.motion-staggered :deep(*) {
|
|
69
|
-
|
|
70
|
-
opacity: 0;
|
|
71
|
-
transition: all 0.3s ease-out;
|
|
65
|
+
.motion-staggered[data-reduced] > :deep(*) {
|
|
66
|
+
transition: none;
|
|
72
67
|
}
|
|
73
68
|
</style>
|
|
@@ -27,6 +27,12 @@
|
|
|
27
27
|
start?: string
|
|
28
28
|
}>()
|
|
29
29
|
|
|
30
|
+
// Swapped to a plain span after mount: the server can't know the preference, so deciding in
|
|
31
|
+
// render would mismatch on hydration.
|
|
32
|
+
const reduced = useReducedMotion()
|
|
33
|
+
const isStatic = ref(false)
|
|
34
|
+
onMounted(() => (isStatic.value = reduced.value))
|
|
35
|
+
|
|
30
36
|
const animBinding = computed(() => ({
|
|
31
37
|
y: 40,
|
|
32
38
|
opacity: 0,
|
|
@@ -39,8 +45,9 @@
|
|
|
39
45
|
</script>
|
|
40
46
|
|
|
41
47
|
<template>
|
|
48
|
+
<span v-if="isStatic" class="motion-text-reveal">{{ text }}</span>
|
|
42
49
|
<span
|
|
43
|
-
v-if="type === 'chars'"
|
|
50
|
+
v-else-if="type === 'chars'"
|
|
44
51
|
v-gsap.whenVisible.once.splitText.chars.from="animBinding"
|
|
45
52
|
class="motion-text-reveal"
|
|
46
53
|
>{{ text }}</span
|
|
@@ -25,11 +25,17 @@
|
|
|
25
25
|
|
|
26
26
|
const { gsap } = useGsap()
|
|
27
27
|
const { velocity } = useSmoothScroll()
|
|
28
|
+
const reduced = useReducedMotion()
|
|
28
29
|
|
|
29
30
|
const smoothVelocity = ref(0)
|
|
30
31
|
const smoothAbsVelocity = ref(0)
|
|
31
32
|
|
|
32
33
|
function tick() {
|
|
34
|
+
if (reduced.value) {
|
|
35
|
+
smoothVelocity.value = 0
|
|
36
|
+
smoothAbsVelocity.value = 0
|
|
37
|
+
return
|
|
38
|
+
}
|
|
33
39
|
const d = damping / 1000
|
|
34
40
|
const s = stiffness / 1000
|
|
35
41
|
|
|
@@ -15,6 +15,7 @@ export function useCountUp(
|
|
|
15
15
|
opts: UseCountUpOptions
|
|
16
16
|
) {
|
|
17
17
|
const { gsap, ScrollTrigger } = useGsap()
|
|
18
|
+
const reduced = useReducedMotion()
|
|
18
19
|
|
|
19
20
|
const displayValue = ref(String(opts.from ?? 0))
|
|
20
21
|
const isComplete = ref(false)
|
|
@@ -26,6 +27,11 @@ export function useCountUp(
|
|
|
26
27
|
let tween: gsap.core.Tween | null = null
|
|
27
28
|
|
|
28
29
|
function startCount() {
|
|
30
|
+
if (reduced.value) {
|
|
31
|
+
displayValue.value = format(opts.to)
|
|
32
|
+
isComplete.value = true
|
|
33
|
+
return
|
|
34
|
+
}
|
|
29
35
|
tween = gsap.to(counter, {
|
|
30
36
|
value: opts.to,
|
|
31
37
|
duration: opts.duration ?? 2,
|
|
@@ -4,11 +4,14 @@ export function useCursorFollower(opts: { smoothing?: MaybeRef<number> } = {}) {
|
|
|
4
4
|
const { gsap } = useGsap()
|
|
5
5
|
const { x: mouseX, y: mouseY } = useMouse({ type: 'client' })
|
|
6
6
|
|
|
7
|
+
const reduced = useReducedMotion()
|
|
8
|
+
|
|
7
9
|
const x = ref(0)
|
|
8
10
|
const y = ref(0)
|
|
9
11
|
|
|
10
12
|
function tick() {
|
|
11
|
-
|
|
13
|
+
// Reduced motion: track the pointer exactly instead of trailing it
|
|
14
|
+
const factor = reduced.value ? 1 : (unref(opts.smoothing) ?? 0.15)
|
|
12
15
|
x.value += (mouseX.value - x.value) * factor
|
|
13
16
|
y.value += (mouseY.value - y.value) * factor
|
|
14
17
|
}
|
|
@@ -14,13 +14,18 @@ export function resolvePointerSpring(
|
|
|
14
14
|
|
|
15
15
|
export function usePointerMotionFrame(update: () => void, cleanup?: () => void) {
|
|
16
16
|
const { gsap } = useGsap()
|
|
17
|
+
const reduced = useReducedMotion()
|
|
18
|
+
|
|
19
|
+
// Checked per frame so a reduced-motion toggle applies live; reduced holds the element at rest.
|
|
20
|
+
// ponytail: cleanup re-runs every frame while reduced — cheap gsap.set, gate it if it ever shows up in a profile
|
|
21
|
+
const frame = () => (reduced.value ? cleanup?.() : update())
|
|
17
22
|
|
|
18
23
|
onMounted(() => {
|
|
19
|
-
gsap.ticker.add(
|
|
24
|
+
gsap.ticker.add(frame)
|
|
20
25
|
})
|
|
21
26
|
|
|
22
27
|
onUnmounted(() => {
|
|
23
|
-
gsap.ticker.remove(
|
|
28
|
+
gsap.ticker.remove(frame)
|
|
24
29
|
cleanup?.()
|
|
25
30
|
})
|
|
26
31
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
}>()
|
|
12
12
|
|
|
13
13
|
const { gsap } = useGsap()
|
|
14
|
+
const reduced = useReducedMotion()
|
|
14
15
|
|
|
15
16
|
const sectionRef = useTemplateRef<HTMLElement>('sectionRef')
|
|
16
17
|
const trackRef = useTemplateRef<HTMLElement>('trackRef')
|
|
@@ -21,6 +22,14 @@
|
|
|
21
22
|
if (!sectionRef.value || !trackRef.value) return
|
|
22
23
|
|
|
23
24
|
const track = trackRef.value
|
|
25
|
+
|
|
26
|
+
// Reduced motion: no pinned scrub — panels scroll natively sideways instead.
|
|
27
|
+
// ponytail: checked once at mount — a later reduced-motion toggle applies on next mount/reload; re-run set-up in a watch if live toggling matters
|
|
28
|
+
if (reduced.value) {
|
|
29
|
+
sectionRef.value.style.overflowX = 'auto'
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
const trackWidth = track.scrollWidth - window.innerWidth
|
|
25
34
|
|
|
26
35
|
const tween = gsap.to(track, {
|
|
@@ -34,32 +34,33 @@
|
|
|
34
34
|
}>()
|
|
35
35
|
|
|
36
36
|
const { gsap } = useGsap()
|
|
37
|
+
const reduced = useReducedMotion()
|
|
37
38
|
|
|
38
39
|
const containerRef = useTemplateRef<HTMLElement | null>('containerRef')
|
|
39
40
|
const contentRef = useTemplateRef<HTMLElement | null>('contentRef')
|
|
40
41
|
|
|
42
|
+
let tween: gsap.core.Tween | null = null
|
|
43
|
+
|
|
41
44
|
onMounted(() => {
|
|
42
45
|
if (!containerRef.value || !contentRef.value) return
|
|
46
|
+
// ponytail: checked once at mount — a later reduced-motion toggle applies on next mount/reload; re-run set-up in a watch if live toggling matters
|
|
47
|
+
if (reduced.value) return
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
const xAmount = direction === 'horizontal' ? speed * 200 : 0
|
|
46
|
-
|
|
47
|
-
const animationProps: gsap.TweenVars = {
|
|
49
|
+
tween = gsap.to(contentRef.value, {
|
|
48
50
|
scrollTrigger: {
|
|
49
51
|
trigger: containerRef.value,
|
|
50
52
|
start: 'top bottom',
|
|
51
53
|
end: 'bottom top',
|
|
52
|
-
scrub
|
|
54
|
+
scrub,
|
|
53
55
|
},
|
|
54
56
|
ease: 'none',
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (xAmount !== 0) animationProps.x = xAmount
|
|
59
|
-
if (rotate !== 0) animationProps.rotation = rotate
|
|
60
|
-
if (scale !== 0) animationProps.scale = 1 + scale
|
|
57
|
+
...parallaxMotionVars({ speed, direction, rotate, scale }),
|
|
58
|
+
})
|
|
59
|
+
})
|
|
61
60
|
|
|
62
|
-
|
|
61
|
+
onUnmounted(() => {
|
|
62
|
+
tween?.scrollTrigger?.kill()
|
|
63
|
+
tween?.kill()
|
|
63
64
|
})
|
|
64
65
|
</script>
|
|
65
66
|
|
|
@@ -11,21 +11,31 @@
|
|
|
11
11
|
}>()
|
|
12
12
|
|
|
13
13
|
const { gsap, ScrollTrigger } = useGsap()
|
|
14
|
+
const reduced = useReducedMotion()
|
|
14
15
|
|
|
15
16
|
const sectionRef = useTemplateRef<HTMLElement>('sectionRef')
|
|
16
17
|
const contentRef = useTemplateRef<HTMLElement>('contentRef')
|
|
17
18
|
|
|
18
19
|
const cleanups: Array<() => void> = []
|
|
19
20
|
|
|
21
|
+
// Reduced motion: no pin, no reveal — show every child at rest (inline style beats the
|
|
22
|
+
// first-paint hide rule below).
|
|
23
|
+
// ponytail: checked once at mount — a later reduced-motion toggle applies on next mount/reload
|
|
20
24
|
onMounted(() => {
|
|
21
|
-
|
|
25
|
+
const section = sectionRef.value
|
|
26
|
+
const content = contentRef.value
|
|
27
|
+
if (!section || !content) return
|
|
28
|
+
const items = Array.from(content.querySelectorAll(':scope > *'))
|
|
29
|
+
if (reduced.value) return void gsap.set(items, { opacity: 1, y: 0, scale: 1 })
|
|
30
|
+
pinAndReveal(section, items)
|
|
31
|
+
})
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
const endValue = `+=${
|
|
33
|
+
function pinAndReveal(section: HTMLElement, items: Element[]) {
|
|
34
|
+
const endValue = `+=${(duration * window.innerHeight) / 100}`
|
|
25
35
|
|
|
26
36
|
// Pin the section
|
|
27
37
|
const pinSt = ScrollTrigger.create({
|
|
28
|
-
trigger:
|
|
38
|
+
trigger: section,
|
|
29
39
|
start: 'top top',
|
|
30
40
|
end: endValue,
|
|
31
41
|
pin: true,
|
|
@@ -34,10 +44,7 @@
|
|
|
34
44
|
cleanups.push(() => pinSt.kill())
|
|
35
45
|
|
|
36
46
|
// Animate each direct child
|
|
37
|
-
const items = Array.from(contentRef.value.querySelectorAll(':scope > *'))
|
|
38
47
|
const total = items.length
|
|
39
|
-
if (total === 0) return
|
|
40
|
-
|
|
41
48
|
items.forEach((item, i) => {
|
|
42
49
|
gsap.set(item, { opacity: 0, y: 60, scale: 0.9 })
|
|
43
50
|
|
|
@@ -46,7 +53,7 @@
|
|
|
46
53
|
|
|
47
54
|
const tween = gsap.to(item, {
|
|
48
55
|
scrollTrigger: {
|
|
49
|
-
trigger:
|
|
56
|
+
trigger: section,
|
|
50
57
|
start: 'top top',
|
|
51
58
|
end: endValue,
|
|
52
59
|
scrub,
|
|
@@ -63,7 +70,7 @@
|
|
|
63
70
|
tween.kill()
|
|
64
71
|
})
|
|
65
72
|
})
|
|
66
|
-
}
|
|
73
|
+
}
|
|
67
74
|
|
|
68
75
|
onUnmounted(() => {
|
|
69
76
|
cleanups.forEach((fn) => fn())
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { computed, ref } from 'vue'
|
|
2
|
+
import { describe, expect, it, vi } from 'vite-plus/test'
|
|
3
|
+
|
|
4
|
+
const system = ref<'reduce' | 'no-preference'>('no-preference')
|
|
5
|
+
vi.mock('@vueuse/core', () => ({ usePreferredReducedMotion: () => system }))
|
|
6
|
+
const state = ref<'on' | 'off' | 'system'>('system')
|
|
7
|
+
vi.stubGlobal('useState', () => state)
|
|
8
|
+
vi.stubGlobal('computed', computed)
|
|
9
|
+
|
|
10
|
+
const { useReducedMotion } = await import('./useReducedMotion')
|
|
11
|
+
|
|
12
|
+
describe('useReducedMotion', () => {
|
|
13
|
+
it.each([
|
|
14
|
+
['system', 'no-preference', false],
|
|
15
|
+
['system', 'reduce', true],
|
|
16
|
+
['on', 'no-preference', true],
|
|
17
|
+
['off', 'reduce', false],
|
|
18
|
+
] as const)('override %s + OS %s → %s', (override, os, expected) => {
|
|
19
|
+
state.value = override
|
|
20
|
+
system.value = os
|
|
21
|
+
expect(useReducedMotion().value).toBe(expected)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { usePreferredReducedMotion } from '@vueuse/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* True when motion should be reduced. Honours the ThemePicker override when the theming layer
|
|
5
|
+
* is loaded — it keeps it in `useState('theme-motion')`, read here by key so scroll doesn't
|
|
6
|
+
* depend on theming — and falls back to the OS `prefers-reduced-motion` setting.
|
|
7
|
+
*/
|
|
8
|
+
export function useReducedMotion() {
|
|
9
|
+
const override = useState<'on' | 'off' | 'system'>('theme-motion', () => 'system')
|
|
10
|
+
const system = usePreferredReducedMotion()
|
|
11
|
+
return computed(
|
|
12
|
+
() => override.value === 'on' || (override.value === 'system' && system.value === 'reduce')
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vite-plus/test'
|
|
2
|
+
|
|
3
|
+
import { parallaxMotionVars } from './parallax'
|
|
4
|
+
|
|
5
|
+
const base = { speed: 0.5, direction: 'vertical', rotate: 0, scale: 0 } as const
|
|
6
|
+
|
|
7
|
+
describe('parallaxMotionVars', () => {
|
|
8
|
+
it('moves up on y for vertical, right on x for horizontal', () => {
|
|
9
|
+
expect(parallaxMotionVars(base)).toEqual({ y: -100 })
|
|
10
|
+
expect(parallaxMotionVars({ ...base, direction: 'horizontal' })).toEqual({ x: 100 })
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('adds rotation and scale only when set', () => {
|
|
14
|
+
expect(parallaxMotionVars({ ...base, rotate: 15, scale: 0.2 })).toEqual({
|
|
15
|
+
y: -100,
|
|
16
|
+
rotation: 15,
|
|
17
|
+
scale: 1.2,
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('returns nothing to animate at zero speed', () => {
|
|
22
|
+
expect(parallaxMotionVars({ ...base, speed: 0 })).toEqual({})
|
|
23
|
+
})
|
|
24
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type ParallaxMotion = {
|
|
2
|
+
speed: number
|
|
3
|
+
direction: 'vertical' | 'horizontal'
|
|
4
|
+
rotate: number
|
|
5
|
+
scale: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** MotionParallax tween vars — only the properties that actually move, so GSAP leaves the rest. */
|
|
9
|
+
export function parallaxMotionVars({ speed, direction, rotate, scale }: ParallaxMotion) {
|
|
10
|
+
const vars: { x?: number; y?: number; rotation?: number; scale?: number } = {}
|
|
11
|
+
const travel = direction === 'vertical' ? speed * -200 : speed * 200
|
|
12
|
+
if (travel !== 0) vars[direction === 'vertical' ? 'y' : 'x'] = travel
|
|
13
|
+
if (rotate !== 0) vars.rotation = rotate
|
|
14
|
+
if (scale !== 0) vars.scale = 1 + scale
|
|
15
|
+
return vars
|
|
16
|
+
}
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
|
|
73
73
|
<template>
|
|
74
74
|
<Teleport to="body">
|
|
75
|
-
<div v-if="visible" :style class="grid-debug fixed inset-0 z-
|
|
75
|
+
<div v-if="visible" :style class="grid-debug fixed inset-0 z-(--z-debug)" aria-hidden="true">
|
|
76
76
|
<div v-for="i in cols" :key="i" :style="{ backgroundColor: color }" class="h-full" />
|
|
77
77
|
</div>
|
|
78
78
|
</Teleport>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* <Transition> and Nuxt's page-root check (NUXT_E4004) require.
|
|
11
11
|
*
|
|
12
12
|
* Responsibilities:
|
|
13
|
-
* -
|
|
13
|
+
* - title/description meta via useSeoMeta() (pages may override)
|
|
14
14
|
* - provides 'pageTitle' to child components
|
|
15
15
|
* - optional visible header (LayoutSection + LayoutPageHeader)
|
|
16
16
|
*
|
|
@@ -45,15 +45,14 @@
|
|
|
45
45
|
|
|
46
46
|
const { title, description = undefined, showHeader = false } = defineProps<Props>()
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
: undefined,
|
|
48
|
+
// Deliberately a "connected" template: pages (and consumers such as KMCOMv5) set their title
|
|
49
|
+
// and description through these props. useSeoMeta rather than hand-written useHead tags, so a
|
|
50
|
+
// page that also calls useSeoMeta overrides these keys instead of adding duplicates.
|
|
51
|
+
useSeoMeta({
|
|
52
|
+
title: () => title,
|
|
53
|
+
ogTitle: () => title,
|
|
54
|
+
description: () => description,
|
|
55
|
+
ogDescription: () => description,
|
|
57
56
|
})
|
|
58
57
|
|
|
59
58
|
provide('pageTitle', title)
|
|
@@ -1,46 +1,20 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { defineComponent, h } from 'vue'
|
|
3
3
|
|
|
4
|
+
import HoverDimGroup from '#layers/visual/app/components/HoverDimGroup.vue'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @deprecated Renamed to `HoverDimGroup` (visual layer) — it dims any children, not just
|
|
8
|
+
* links. Kept for one release so consumers can migrate; remove after that.
|
|
9
|
+
*/
|
|
4
10
|
export default defineComponent({
|
|
5
11
|
name: 'LinksGroup',
|
|
6
12
|
inheritAttrs: false,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
setup(props, { slots, attrs }) {
|
|
13
|
-
const hoveredIndex = ref<number | null>(null)
|
|
14
|
-
|
|
15
|
-
return () => {
|
|
16
|
-
const children = (slots.default?.() ?? [])
|
|
17
|
-
.flatMap((node) => (node.type === Fragment ? (node.children as VNode[]) : [node]))
|
|
18
|
-
.filter((node) => typeof node.type === 'string' || typeof node.type === 'object')
|
|
19
|
-
|
|
20
|
-
const styledChildren = children.map((vnode, index) =>
|
|
21
|
-
cloneVNode(vnode, {
|
|
22
|
-
onMouseenter: () => {
|
|
23
|
-
hoveredIndex.value = index
|
|
24
|
-
},
|
|
25
|
-
style: {
|
|
26
|
-
opacity:
|
|
27
|
-
hoveredIndex.value !== null && hoveredIndex.value !== index ? props.dimOpacity : 1,
|
|
28
|
-
transition: `opacity ${props.duration}ms ease-out`,
|
|
29
|
-
},
|
|
30
|
-
})
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
return h(
|
|
34
|
-
props.tag,
|
|
35
|
-
{
|
|
36
|
-
...attrs,
|
|
37
|
-
onMouseleave: () => {
|
|
38
|
-
hoveredIndex.value = null
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
styledChildren
|
|
42
|
-
)
|
|
43
|
-
}
|
|
13
|
+
setup(_, { attrs, slots }) {
|
|
14
|
+
if (import.meta.dev)
|
|
15
|
+
// eslint-disable-next-line no-console -- one-time deprecation notice, dev only
|
|
16
|
+
console.warn('[navigation] <LinksGroup> is deprecated — use <HoverDimGroup>.')
|
|
17
|
+
return () => h(HoverDimGroup, attrs, slots)
|
|
44
18
|
},
|
|
45
19
|
})
|
|
46
20
|
</script>
|
|
@@ -17,6 +17,15 @@
|
|
|
17
17
|
variant?: string
|
|
18
18
|
color?: UiColors
|
|
19
19
|
}>()
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @deprecated Thin wrapper over `ULink` — use `<ULink :to="{ name, params, query }">`.
|
|
23
|
+
* Unused in this repo and KMCOMv5; remove in the next major.
|
|
24
|
+
*/
|
|
25
|
+
if (import.meta.dev)
|
|
26
|
+
// eslint-disable-next-line no-console -- one-time deprecation notice, dev only
|
|
27
|
+
console.warn('[navigation] <LinksNamed> is deprecated — use <ULink :to="{ name }">.')
|
|
28
|
+
|
|
20
29
|
const colorClass = useColor(() => color, 'text')
|
|
21
30
|
// `name` is a plain string here, not one of the app's typed route names
|
|
22
31
|
const to = computed(
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
// Positioning is the parent's call (e.g. `<MastNav class="fixed top-0 right-0" />`); the
|
|
3
|
+
// z-index stays here so the trigger always sits on the dropdown layer.
|
|
2
4
|
const { open } = useMastNav()
|
|
3
5
|
const z = useGridConfig().useZIndex('dropdown')
|
|
4
6
|
</script>
|
|
@@ -6,7 +8,7 @@
|
|
|
6
8
|
<template>
|
|
7
9
|
<button
|
|
8
10
|
aria-label="Open navigation menu"
|
|
9
|
-
class="
|
|
11
|
+
class="flex cursor-pointer items-center justify-center border-0 bg-transparent p-4"
|
|
10
12
|
:style="{ zIndex: z }"
|
|
11
13
|
@click="() => open()"
|
|
12
14
|
>
|