kmcom-nuxt-layers 1.6.38 → 1.6.40

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.
@@ -0,0 +1,186 @@
1
+ # UPage + Swiss Grid Integration
2
+
3
+ ## The Problem
4
+
5
+ `<LayoutMain>` renders `<main class="mastmain">` which sets:
6
+
7
+ ```css
8
+ .mastmain {
9
+ grid-auto-rows: calc((100vh - 11 * gap) / 12); /* ≈ 8.5vh per row */
10
+ }
11
+ ```
12
+
13
+ Every direct child that doesn't explicitly span rows becomes ~8.5vh tall. `<UPage>` and Nuxt UI's page components (`UPageSection`, `UPageHero`, etc.) don't span rows — so they collapse.
14
+
15
+ `.basesection` fixes this for `<LayoutSection>` by spanning 12 rows:
16
+
17
+ ```css
18
+ .basesection {
19
+ grid-row: span 12; /* 12 × 8.5vh ≈ 100vh */
20
+ }
21
+ ```
22
+
23
+ `<UPage>` has no equivalent. It's designed for document flow, not a fixed-row grid.
24
+
25
+ ---
26
+
27
+ ## Approaches
28
+
29
+ ### Option A — `minmax` on `grid-auto-rows` (Recommended)
30
+
31
+ **Change in `grids.css`:**
32
+
33
+ ```css
34
+ .mastmain {
35
+ /* Before */
36
+ grid-auto-rows: var(--grid-row-height);
37
+
38
+ /* After — allow rows to grow beyond their minimum */
39
+ grid-auto-rows: minmax(var(--grid-row-height), auto);
40
+ }
41
+ ```
42
+
43
+ The mobile breakpoint already uses this. Apply it at all breakpoints.
44
+
45
+ **What changes:**
46
+ - `<LayoutSection>` spanning 12 rows = at least `12 × 8.5vh ≈ 100vh` (min preserved)
47
+ - `<UPage>` in a single row = auto-sized to its content height
48
+ - Column alignment is fully preserved
49
+
50
+ **Trade-off:** `rowStart` placement in `<LayoutGridItem>` becomes less predictable when rows above it grow beyond their minimum. `rowSpan` presets are unaffected.
51
+
52
+ **Implementation:** One-line CSS change in the layout layer source.
53
+
54
+ ---
55
+
56
+ ### Option B — `display: contents` on UPage
57
+
58
+ Configure `<UPage>` to be transparent to the grid so its children participate directly:
59
+
60
+ ```ts
61
+ // In frontend app.config.ts, override the layout layer's UPage config:
62
+ ui: {
63
+ page: {
64
+ root: 'col-span-full [display:contents]',
65
+ },
66
+ }
67
+ ```
68
+
69
+ With `display: contents`, `<UPage>` generates no box — its children (`UPageHero`, `UPageSection`, etc.) become direct grid items in `mastmain`.
70
+
71
+ **What changes:**
72
+ - `<UPage>` styling is effectively gone (use with care)
73
+ - Each child component becomes a grid item and needs its own column/row spanning
74
+ - Requires configuring `UPageSection`, `UPageHero`, etc. similarly
75
+
76
+ **Trade-off:** You lose `<UPage>`'s layout structure. Works best when you're using it purely as a semantic grouping with no visual styling.
77
+
78
+ ---
79
+
80
+ ### Option C — `<LayoutPage>` component (Hybrid mode)
81
+
82
+ A new component that gives pages an explicit choice between Swiss Grid and normal document flow:
83
+
84
+ ```vue
85
+ <!-- layers/layout/app/components/Layout/Page/index.vue -->
86
+ <script setup lang="ts">
87
+ interface Props {
88
+ mode?: 'swiss' | 'flow'
89
+ }
90
+ const { mode = 'swiss' } = defineProps<Props>()
91
+ </script>
92
+
93
+ <template>
94
+ <!-- Swiss: children participate directly in mastmain grid (use LayoutSection) -->
95
+ <template v-if="mode === 'swiss'">
96
+ <slot />
97
+ </template>
98
+
99
+ <!-- Flow: UPage mode — auto-height row that fills content -->
100
+ <div
101
+ v-else
102
+ class="col-span-full"
103
+ style="grid-row: auto / span 999; height: max-content; min-height: 0"
104
+ >
105
+ <UPage>
106
+ <slot />
107
+ </UPage>
108
+ </div>
109
+ </template>
110
+ ```
111
+
112
+ Usage:
113
+
114
+ ```vue
115
+ <!-- Swiss Grid page -->
116
+ <template>
117
+ <LayoutPage>
118
+ <LayoutSection full-height>
119
+ <LayoutGridItem preset="centered">
120
+ <h1>Title</h1>
121
+ </LayoutGridItem>
122
+ </LayoutSection>
123
+ </LayoutPage>
124
+ </template>
125
+
126
+ <!-- UPage page inside LayoutMain -->
127
+ <template>
128
+ <LayoutPage mode="flow">
129
+ <UPageHero title="About" />
130
+ <UPageSection>...</UPageSection>
131
+ </LayoutPage>
132
+ </template>
133
+ ```
134
+
135
+ **Trade-off:** `span 999` is an escape hatch — it works because CSS Grid clamps spans to the available track count. It's pragmatic but not semantically clean. Requires the component to live in the layout layer source.
136
+
137
+ ---
138
+
139
+ ### Option D — Per-page `<LayoutMain tag="div">` opt-in
140
+
141
+ Don't apply `<LayoutMain>` in the layouts at all. Let pages that want the Swiss Grid add it themselves inside their template:
142
+
143
+ ```vue
144
+ <!-- A Swiss Grid page — no UPage -->
145
+ <template>
146
+ <LayoutMain tag="div">
147
+ <LayoutSection full-height>
148
+ <LayoutGridItem preset="hero">
149
+ <h1>Home</h1>
150
+ </LayoutGridItem>
151
+ </LayoutSection>
152
+ </LayoutMain>
153
+ </template>
154
+
155
+ <!-- A normal page — uses UMain from the layout, no grid involvement -->
156
+ <template>
157
+ <UPage>
158
+ <UPageHero title="About" />
159
+ </UPage>
160
+ </template>
161
+ ```
162
+
163
+ **This is the current approach after the revert.** The layout layer is available everywhere but not globally applied. The `mastmain` CSS class exists — any element can opt in via `class="mastmain"` or `<LayoutMain tag="div">`.
164
+
165
+ **Trade-off:** No automatic column alignment across pages. Pages must explicitly adopt the grid.
166
+
167
+ ---
168
+
169
+ ## Recommendation
170
+
171
+ **Short-term (no code changes required):** Option D — current approach. Build new sections using `<LayoutSection>` / `<LayoutGridItem>` inside pages that want the Swiss Grid. Existing `<UPage>` pages are untouched.
172
+
173
+ **Medium-term (one CSS change):** Option A — change `grid-auto-rows` to `minmax(var(--grid-row-height), auto)` in `grids.css`. This makes `<LayoutMain>` compatible with both Swiss Grid sections and UPage content at the same time, unlocking global application in the layouts with zero page changes.
174
+
175
+ **Long-term:** Migrate pages to the Swiss Grid pattern (no `<UPage>`) to take full advantage of precise row placement and the full 18-column subgrid system.
176
+
177
+ ---
178
+
179
+ ## CSS Grid Behaviour Reference
180
+
181
+ | Scenario | `grid-auto-rows: 8.5vh` | `grid-auto-rows: minmax(8.5vh, auto)` |
182
+ |---|---|---|
183
+ | `<LayoutSection>` (spans 12 rows) | ✅ Exactly 100vh | ✅ At least 100vh, grows with content |
184
+ | `<UPage>` (1 auto row) | ❌ Collapsed to 8.5vh | ✅ Grows to content height |
185
+ | `<LayoutGridItem rowStart="3">` | ✅ Predictable position | ⚠️ Position shifts if rows above grew |
186
+ | `<LayoutGridItem rowSpan="6">` | ✅ 6 × 8.5vh = 51vh | ✅ At least 6 × 8.5vh |
@@ -1,142 +1,142 @@
1
1
  <script setup lang="ts">
2
- const props = withDefaults(
3
- defineProps<{
4
- /**
5
- * Base animation speed in pixels per second
6
- */
7
- speed?: number
8
- /**
9
- * Direction of scroll
10
- */
11
- direction?: 'left' | 'right'
12
- /**
13
- * Pause animation on hover
14
- */
15
- pauseOnHover?: boolean
16
- /**
17
- * Gap between repeated items
18
- */
19
- gap?: string
20
- /**
21
- * Enable velocity-based speed (scroll velocity affects marquee speed)
22
- */
23
- velocityBased?: boolean
24
- /**
25
- * How much scroll velocity affects speed (0-1)
26
- * 0 = no effect, 1 = velocity fully controls speed
27
- */
28
- velocitySensitivity?: number
29
- /**
30
- * Reverse direction based on scroll direction
31
- */
32
- velocityDirection?: boolean
33
- /**
34
- * Minimum speed multiplier when using velocity
35
- */
36
- minSpeed?: number
37
- /**
38
- * Maximum speed multiplier when using velocity
39
- */
40
- maxSpeed?: number
41
- }>(),
42
- {
43
- speed: 50,
44
- direction: 'left',
45
- pauseOnHover: true,
46
- gap: '2rem',
47
- velocityBased: false,
48
- velocitySensitivity: 0.5,
49
- velocityDirection: false,
50
- minSpeed: 0.2,
51
- maxSpeed: 5,
52
- }
53
- )
54
-
55
- const { gsap } = useGsap()
56
- const { velocity: scrollVelocity, direction: scrollDirection } = useSmoothScroll()
57
-
58
- const containerRef = ref<HTMLElement | null>(null)
59
- const contentRef = ref<HTMLElement | null>(null)
60
- const tweenRef = ref<gsap.core.Tween | null>(null)
61
-
62
- const isPaused = ref(false)
63
- const currentTimeScale = ref(1)
64
-
65
- onMounted(() => {
66
- if (!containerRef.value || !contentRef.value) return
67
-
68
- const content = contentRef.value
69
- const contentWidth = content.offsetWidth
70
-
71
- // Set up the infinite scroll animation
72
- tweenRef.value = gsap.to(content, {
73
- x: props.direction === 'left' ? -contentWidth / 2 : contentWidth / 2,
74
- duration: contentWidth / props.speed,
75
- ease: 'none',
76
- repeat: -1,
77
- modifiers: {
78
- x: gsap.utils.unitize((x) => {
79
- const mod = contentWidth / 2
80
- return props.direction === 'left' ? parseFloat(x) % mod : -Math.abs(parseFloat(x) % mod)
81
- }),
82
- },
2
+ const props = withDefaults(
3
+ defineProps<{
4
+ /**
5
+ * Base animation speed in pixels per second
6
+ */
7
+ speed?: number
8
+ /**
9
+ * Direction of scroll
10
+ */
11
+ direction?: 'left' | 'right'
12
+ /**
13
+ * Pause animation on hover
14
+ */
15
+ pauseOnHover?: boolean
16
+ /**
17
+ * Gap between repeated items
18
+ */
19
+ gap?: string
20
+ /**
21
+ * Enable velocity-based speed (scroll velocity affects marquee speed)
22
+ */
23
+ velocityBased?: boolean
24
+ /**
25
+ * How much scroll velocity affects speed (0-1)
26
+ * 0 = no effect, 1 = velocity fully controls speed
27
+ */
28
+ velocitySensitivity?: number
29
+ /**
30
+ * Reverse direction based on scroll direction
31
+ */
32
+ velocityDirection?: boolean
33
+ /**
34
+ * Minimum speed multiplier when using velocity
35
+ */
36
+ minSpeed?: number
37
+ /**
38
+ * Maximum speed multiplier when using velocity
39
+ */
40
+ maxSpeed?: number
41
+ }>(),
42
+ {
43
+ speed: 50,
44
+ direction: 'left',
45
+ pauseOnHover: true,
46
+ gap: '2rem',
47
+ velocityBased: false,
48
+ velocitySensitivity: 0.5,
49
+ velocityDirection: false,
50
+ minSpeed: 0.2,
51
+ maxSpeed: 5,
52
+ }
53
+ )
54
+
55
+ const { gsap } = useGsap()
56
+ const { velocity: scrollVelocity, direction: scrollDirection } = useSmoothScroll()
57
+
58
+ const containerRef = ref<HTMLElement | null>(null)
59
+ const contentRef = ref<HTMLElement | null>(null)
60
+ const tweenRef = ref<gsap.core.Tween | null>(null)
61
+
62
+ const isPaused = ref(false)
63
+ const currentTimeScale = ref(1)
64
+
65
+ onMounted(() => {
66
+ if (!containerRef.value || !contentRef.value) return
67
+
68
+ const content = contentRef.value
69
+ const contentWidth = content.offsetWidth
70
+
71
+ // Set up the infinite scroll animation
72
+ tweenRef.value = gsap.to(content, {
73
+ x: props.direction === 'left' ? -contentWidth / 2 : contentWidth / 2,
74
+ duration: contentWidth / props.speed,
75
+ ease: 'none',
76
+ repeat: -1,
77
+ modifiers: {
78
+ x: gsap.utils.unitize((x) => {
79
+ const mod = contentWidth / 2
80
+ return props.direction === 'left' ? parseFloat(x) % mod : Math.abs(parseFloat(x) % mod)
81
+ }),
82
+ },
83
+ })
83
84
  })
84
- })
85
-
86
- // Watch velocity changes for velocity-based animation
87
- watch(
88
- [scrollVelocity, scrollDirection],
89
- ([vel, dir]) => {
90
- if (!props.velocityBased || !tweenRef.value || isPaused.value) return
91
-
92
- // Calculate velocity multiplier
93
- const absVelocity = Math.abs(vel)
94
- const velocityEffect = absVelocity * props.velocitySensitivity * 0.02
95
85
 
96
- // Base multiplier from velocity magnitude
97
- let multiplier = 1 + velocityEffect
86
+ // Watch velocity changes for velocity-based animation
87
+ watch(
88
+ [scrollVelocity, scrollDirection],
89
+ ([vel, dir]) => {
90
+ if (!props.velocityBased || !tweenRef.value || isPaused.value) return
91
+
92
+ // Calculate velocity multiplier
93
+ const absVelocity = Math.abs(vel)
94
+ const velocityEffect = absVelocity * props.velocitySensitivity * 0.02
95
+
96
+ // Base multiplier from velocity magnitude
97
+ let multiplier = 1 + velocityEffect
98
+
99
+ // Clamp to min/max
100
+ multiplier = Math.max(props.minSpeed, Math.min(props.maxSpeed, multiplier))
101
+
102
+ // Optionally reverse direction based on scroll direction
103
+ if (props.velocityDirection && dir !== 0) {
104
+ const baseDirection = props.direction === 'left' ? 1 : -1
105
+ multiplier *= dir * baseDirection
106
+ }
107
+
108
+ // Smoothly interpolate to target
109
+ gsap.to(currentTimeScale, {
110
+ value: multiplier,
111
+ duration: 0.3,
112
+ ease: 'power2.out',
113
+ onUpdate: () => {
114
+ if (tweenRef.value) {
115
+ tweenRef.value.timeScale(currentTimeScale.value)
116
+ }
117
+ },
118
+ })
119
+ },
120
+ { immediate: false }
121
+ )
98
122
 
99
- // Clamp to min/max
100
- multiplier = Math.max(props.minSpeed, Math.min(props.maxSpeed, multiplier))
123
+ onUnmounted(() => {
124
+ tweenRef.value?.kill()
125
+ })
101
126
 
102
- // Optionally reverse direction based on scroll direction
103
- if (props.velocityDirection && dir !== 0) {
104
- const baseDirection = props.direction === 'left' ? 1 : -1
105
- multiplier *= dir * baseDirection
127
+ function handleMouseEnter() {
128
+ if (props.pauseOnHover && tweenRef.value) {
129
+ gsap.to(tweenRef.value, { timeScale: 0, duration: 0.5 })
130
+ isPaused.value = true
106
131
  }
107
-
108
- // Smoothly interpolate to target
109
- gsap.to(currentTimeScale, {
110
- value: multiplier,
111
- duration: 0.3,
112
- ease: 'power2.out',
113
- onUpdate: () => {
114
- if (tweenRef.value) {
115
- tweenRef.value.timeScale(currentTimeScale.value)
116
- }
117
- },
118
- })
119
- },
120
- { immediate: false }
121
- )
122
-
123
- onUnmounted(() => {
124
- tweenRef.value?.kill()
125
- })
126
-
127
- function handleMouseEnter() {
128
- if (props.pauseOnHover && tweenRef.value) {
129
- gsap.to(tweenRef.value, { timeScale: 0, duration: 0.5 })
130
- isPaused.value = true
131
132
  }
132
- }
133
133
 
134
- function handleMouseLeave() {
135
- if (props.pauseOnHover && tweenRef.value) {
136
- gsap.to(tweenRef.value, { timeScale: currentTimeScale.value, duration: 0.5 })
137
- isPaused.value = false
134
+ function handleMouseLeave() {
135
+ if (props.pauseOnHover && tweenRef.value) {
136
+ gsap.to(tweenRef.value, { timeScale: currentTimeScale.value, duration: 0.5 })
137
+ isPaused.value = false
138
+ }
138
139
  }
139
- }
140
140
  </script>
141
141
 
142
142
  <template>
@@ -0,0 +1,147 @@
1
+ <script setup lang="ts">
2
+ import type { ScrollTrigger as GSAPScrollTrigger } from 'gsap/ScrollTrigger'
3
+
4
+ interface VelocityMapping {
5
+ input: [number, number]
6
+ output: [number, number]
7
+ }
8
+
9
+ const {
10
+ texts = [],
11
+ velocity = 100,
12
+ className = '',
13
+ damping = 50,
14
+ stiffness = 400,
15
+ velocityMapping = { input: [0, 1000] as [number, number], output: [0, 5] as [number, number] },
16
+ pauseOnHover = false,
17
+ parallaxClassName = '',
18
+ scrollerClassName = '',
19
+ parallaxStyle = {},
20
+ scrollerStyle = {},
21
+ } = defineProps<{
22
+ texts?: string[]
23
+ velocity?: number
24
+ className?: string
25
+ damping?: number
26
+ stiffness?: number
27
+ velocityMapping?: VelocityMapping
28
+ pauseOnHover?: boolean
29
+ parallaxClassName?: string
30
+ scrollerClassName?: string
31
+ parallaxStyle?: Record<string, string | number>
32
+ scrollerStyle?: Record<string, string | number>
33
+ }>()
34
+
35
+ const { gsap, ScrollTrigger } = useGsap()
36
+ const { velocityFactor } = useMarqueeVelocity({ damping, stiffness, velocityMapping })
37
+
38
+ const containerRef = ref<HTMLElement[]>([])
39
+ const copyRefs = ref<HTMLSpanElement[]>([])
40
+
41
+ const { copyWidths, calculatedCopies } = useMarqueeCopies(
42
+ containerRef,
43
+ copyRefs,
44
+ computed(() => texts.length)
45
+ )
46
+
47
+ const baseX = ref<number[]>([])
48
+ const directionFactors = ref<number[]>([])
49
+ const isPaused = ref(false)
50
+
51
+ let scrollTriggerInstance: GSAPScrollTrigger | null = null
52
+ let animLastTime = 0
53
+
54
+ const wrap = (min: number, max: number, v: number): number => {
55
+ const range = max - min
56
+ if (range === 0) return min
57
+ return ((((v - min) % range) + range) % range) + min
58
+ }
59
+
60
+ const scrollTransforms = computed(() =>
61
+ texts.map((_, index) => {
62
+ const singleWidth = copyWidths.value[index]
63
+ if (!singleWidth) return '0px'
64
+ return `${wrap(-singleWidth, 0, baseX.value[index] ?? 0)}px`
65
+ })
66
+ )
67
+
68
+ const setCopyRef = (el: Element | ComponentPublicInstance | null, index: number) => {
69
+ if (el instanceof HTMLSpanElement) copyRefs.value[index] = el
70
+ }
71
+
72
+ function animate() {
73
+ if (isPaused.value) return
74
+
75
+ const now = performance.now()
76
+ const delta = animLastTime ? now - animLastTime : 16
77
+ animLastTime = now
78
+
79
+ texts.forEach((_, index) => {
80
+ const baseVelocity = index % 2 !== 0 ? -velocity : velocity
81
+ let moveBy = (directionFactors.value[index] ?? 1) * baseVelocity * (delta / 1000)
82
+
83
+ if (velocityFactor.value < 0) {
84
+ directionFactors.value[index] = -1
85
+ } else if (velocityFactor.value > 0) {
86
+ directionFactors.value[index] = 1
87
+ }
88
+
89
+ moveBy += (directionFactors.value[index] ?? 1) * moveBy * velocityFactor.value
90
+ baseX.value[index] = (baseX.value[index] ?? 0) + moveBy
91
+ })
92
+ }
93
+
94
+ onMounted(async () => {
95
+ await nextTick()
96
+
97
+ baseX.value = new Array(texts.length).fill(0)
98
+ directionFactors.value = new Array(texts.length).fill(1)
99
+ animLastTime = performance.now()
100
+
101
+ const firstContainer = containerRef.value[0]
102
+ if (firstContainer) {
103
+ scrollTriggerInstance = ScrollTrigger.create({
104
+ trigger: firstContainer,
105
+ start: 'top bottom',
106
+ end: 'bottom top',
107
+ })
108
+ }
109
+
110
+ gsap.ticker.add(animate)
111
+ })
112
+
113
+ onUnmounted(() => {
114
+ gsap.ticker.remove(animate)
115
+ scrollTriggerInstance?.kill()
116
+ })
117
+ </script>
118
+
119
+ <template>
120
+ <section
121
+ class="w-full overflow-x-hidden"
122
+ @mouseenter="isPaused = pauseOnHover"
123
+ @mouseleave="isPaused = false"
124
+ >
125
+ <div
126
+ v-for="(text, index) in texts"
127
+ :key="index"
128
+ ref="containerRef"
129
+ :class="`${parallaxClassName} relative w-full overflow-x-hidden overflow-y-visible`"
130
+ :style="parallaxStyle"
131
+ >
132
+ <div
133
+ :class="`${scrollerClassName} font-styles-logo flex py-2 text-center font-sans text-4xl font-medium tracking-[-0.02em] whitespace-nowrap text-neutral drop-shadow md:text-[5rem] md:leading-[5rem]`"
134
+ :style="{ transform: `translateX(${scrollTransforms[index] ?? '0px'})`, ...scrollerStyle }"
135
+ >
136
+ <span
137
+ v-for="spanIndex in calculatedCopies[index] ?? 15"
138
+ :key="spanIndex"
139
+ :ref="spanIndex === 1 ? (el) => setCopyRef(el, index) : undefined"
140
+ :class="`shrink-0 ${className} text-primary-500`"
141
+ >
142
+ {{ text }}&nbsp;
143
+ </span>
144
+ </div>
145
+ </div>
146
+ </section>
147
+ </template>
@@ -0,0 +1,50 @@
1
+ import type { MaybeRef } from 'vue'
2
+
3
+ export function useMarqueeCopies(
4
+ containerRefs: Ref<HTMLElement[]>,
5
+ copyRefs: Ref<HTMLSpanElement[]>,
6
+ rowCount: MaybeRef<number>,
7
+ ) {
8
+ const copyWidths = ref<number[]>([])
9
+ const calculatedCopies = ref<number[]>([])
10
+
11
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null
12
+
13
+ function calculate() {
14
+ const n = unref(rowCount)
15
+ for (let i = 0; i < n; i++) {
16
+ const copy = copyRefs.value[i]
17
+ const container = containerRefs.value[i]
18
+ if (!copy || !container) continue
19
+ const singleWidth = copy.offsetWidth
20
+ if (singleWidth === 0) continue
21
+ const effectiveWidth = Math.max(container.offsetWidth, window.innerWidth)
22
+ const minCopies = Math.ceil((effectiveWidth * 2.5) / singleWidth)
23
+ copyWidths.value[i] = singleWidth
24
+ calculatedCopies.value[i] = Math.max(minCopies, 8)
25
+ }
26
+ }
27
+
28
+ function debouncedCalculate() {
29
+ if (debounceTimer) clearTimeout(debounceTimer)
30
+ debounceTimer = setTimeout(() => {
31
+ calculate()
32
+ debounceTimer = null
33
+ }, 150)
34
+ }
35
+
36
+ onMounted(() => {
37
+ nextTick(() => {
38
+ calculate()
39
+ setTimeout(calculate, 100)
40
+ })
41
+ window.addEventListener('resize', debouncedCalculate, { passive: true })
42
+ })
43
+
44
+ onUnmounted(() => {
45
+ if (debounceTimer) clearTimeout(debounceTimer)
46
+ window.removeEventListener('resize', debouncedCalculate)
47
+ })
48
+
49
+ return { copyWidths, calculatedCopies }
50
+ }
@@ -0,0 +1,44 @@
1
+ import type { MaybeRef } from 'vue'
2
+
3
+ interface VelocityMapping {
4
+ input: [number, number]
5
+ output: [number, number]
6
+ }
7
+
8
+ interface UseMarqueeVelocityOptions {
9
+ damping?: MaybeRef<number>
10
+ stiffness?: MaybeRef<number>
11
+ velocityMapping?: MaybeRef<VelocityMapping>
12
+ }
13
+
14
+ export function useMarqueeVelocity(opts: UseMarqueeVelocityOptions = {}) {
15
+ const { velocity: rawVelocity } = useSmoothScroll()
16
+ const { gsap } = useGsap()
17
+
18
+ const smoothVelocity = ref(0)
19
+ const velocityFactor = ref(0)
20
+
21
+ function tick() {
22
+ const damping = (unref(opts.damping) ?? 50) / 1000
23
+ const stiffness = (unref(opts.stiffness) ?? 400) / 1000
24
+ const mapping: VelocityMapping = unref(opts.velocityMapping) ?? {
25
+ input: [0, 1000],
26
+ output: [0, 5],
27
+ }
28
+
29
+ smoothVelocity.value += (rawVelocity.value - smoothVelocity.value) * stiffness
30
+ smoothVelocity.value *= 1 - damping
31
+
32
+ const inputRange = mapping.input[1] - mapping.input[0]
33
+ const outputRange = mapping.output[1] - mapping.output[0]
34
+ let t = (Math.abs(smoothVelocity.value) - mapping.input[0]) / inputRange
35
+ t = Math.max(0, Math.min(1, t))
36
+ velocityFactor.value = mapping.output[0] + t * outputRange
37
+ if (smoothVelocity.value < 0) velocityFactor.value *= -1
38
+ }
39
+
40
+ onMounted(() => gsap.ticker.add(tick))
41
+ onUnmounted(() => gsap.ticker.remove(tick))
42
+
43
+ return { velocityFactor }
44
+ }
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "extends": "../../apps/playground/tsconfig.json",
3
+
3
4
  "compilerOptions": {
4
5
  "strict": true
5
6
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kmcom-nuxt-layers",
3
3
  "private": false,
4
- "version": "1.6.38",
4
+ "version": "1.6.40",
5
5
  "description": "Composable Nuxt 4 layers for building scalable Vue applications",
6
6
  "exports": {
7
7
  "./layers/core": "./layers/core/nuxt.config.ts",