kmcom-nuxt-layers 1.6.39 → 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.
Files changed (50) hide show
  1. package/layers/core/app/composables/useErrorLog.ts +6 -11
  2. package/layers/core/app/composables/useLoading.ts +15 -46
  3. package/layers/core/app/composables/useScrollGuard.ts +115 -129
  4. package/layers/forms/app/components/Form/Field.vue +2 -2
  5. package/layers/forms/app/composables/useFormSchema.ts +1 -3
  6. package/layers/forms/app/config/fields.ts +12 -6
  7. package/layers/forms/app/types/fields.ts +2 -20
  8. package/layers/forms/nuxt.config.ts +0 -10
  9. package/layers/motion/app/components/Motion/CountUp.vue +39 -0
  10. package/layers/motion/app/components/Motion/Cursor.vue +101 -0
  11. package/layers/motion/app/components/Motion/Magnetic.vue +22 -0
  12. package/layers/motion/app/components/Motion/Marquee.vue +130 -130
  13. package/layers/motion/app/components/Motion/MarqueeText.vue +147 -0
  14. package/layers/motion/app/components/Motion/Tilt.vue +22 -0
  15. package/layers/motion/app/components/Motion/VelocityEffect.vue +33 -71
  16. package/layers/motion/app/composables/useCountUp.ts +61 -0
  17. package/layers/motion/app/composables/useCursorFollower.ts +25 -0
  18. package/layers/motion/app/composables/useMagneticElement.ts +56 -0
  19. package/layers/motion/app/composables/useMarqueeCopies.ts +50 -0
  20. package/layers/motion/app/composables/useMarqueeVelocity.ts +44 -0
  21. package/layers/motion/app/composables/useSmoothScroll.ts +11 -0
  22. package/layers/motion/app/composables/useTiltEffect.ts +58 -0
  23. package/layers/motion/app/plugins/locomotive-scroll.client.ts +1 -1
  24. package/layers/motion/app/types/app-config.d.ts +11 -2
  25. package/layers/motion/tsconfig.json +1 -0
  26. package/layers/routing/app/middleware/02.governance.global.ts +7 -18
  27. package/layers/routing/app/types/routing.ts +10 -0
  28. package/layers/routing/app/utils/resolveRoute.ts +31 -0
  29. package/layers/shader/package.json +2 -1
  30. package/layers/theme/app/composables/useAccentColor.ts +1 -12
  31. package/layers/theme/app/composables/useThemeContrast.ts +0 -11
  32. package/layers/theme/app/composables/useThemeMotion.ts +0 -11
  33. package/layers/theme/app/composables/useThemeTransparency.ts +0 -14
  34. package/layers/theme/app/plugins/theme.client.ts +20 -3
  35. package/layers/theme/app/types/app-config.d.ts +2 -2
  36. package/layers/ui/app/components/Base/Modal.vue +0 -1
  37. package/layers/ui/app/composables/color.ts +1 -32
  38. package/layers/ui/app/utils/createModal.ts +11 -2
  39. package/package.json +28 -26
  40. package/layers/content/app/.DS_Store +0 -0
  41. package/layers/content/app/pages/blog/[slug].vue +0 -19
  42. package/layers/content/app/pages/blog/index.vue +0 -22
  43. package/layers/core/app/.DS_Store +0 -0
  44. package/layers/core/app/assets/.DS_Store +0 -0
  45. package/layers/forms/app/.DS_Store +0 -0
  46. package/layers/layout/app/.DS_Store +0 -0
  47. package/layers/motion/app/.DS_Store +0 -0
  48. package/layers/shader/app/.DS_Store +0 -0
  49. package/layers/theme/app/.DS_Store +0 -0
  50. package/layers/ui/app/.DS_Store +0 -0
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  /**
3
2
  * Error logging composable for tracking and reporting errors
4
3
  *
@@ -49,16 +48,12 @@ export function useErrorLog() {
49
48
  const appConfig = useAppConfig()
50
49
  const route = useRoute()
51
50
 
52
- // Get error log configuration from app.config
53
- const config = computed(() => {
54
- const coreLayer = appConfig.coreLayer as { errors?: ErrorLogConfig } | undefined
55
- return {
56
- logToConsole: coreLayer?.errors?.logToConsole ?? true,
57
- logToExternal: coreLayer?.errors?.logToExternal ?? false,
58
- externalUrl: coreLayer?.errors?.externalUrl,
59
- externalToken: coreLayer?.errors?.externalToken,
60
- }
61
- })
51
+ const config = computed(() => ({
52
+ logToConsole: appConfig.coreLayer?.errors?.logToConsole ?? true,
53
+ logToExternal: appConfig.coreLayer?.errors?.logToExternal ?? false,
54
+ externalUrl: appConfig.coreLayer?.errors?.externalUrl,
55
+ externalToken: appConfig.coreLayer?.errors?.externalToken,
56
+ }))
62
57
 
63
58
  /**
64
59
  * Log an error with optional context
@@ -1,16 +1,6 @@
1
1
  // composables/useLoading.ts
2
2
 
3
- interface LoadingState {
4
- isLoading: Ref<boolean>
5
- progress: Ref<number>
6
- }
7
-
8
- // Shared state across all calls (singleton pattern)
9
- const state: LoadingState = {
10
- isLoading: ref(true), // Start as true for initial app load
11
- progress: ref(0),
12
- }
13
-
3
+ // Timer IDs are non-reactive and intentionally module-scope (singleton)
14
4
  let progressInterval: ReturnType<typeof setInterval> | null = null
15
5
  let progressTimeout: ReturnType<typeof setTimeout> | null = null
16
6
 
@@ -27,65 +17,47 @@ let progressTimeout: ReturnType<typeof setTimeout> | null = null
27
17
  * @returns Loading state and control methods
28
18
  */
29
19
  export function useLoading() {
30
- /**
31
- * Start loading with simulated progress
32
- * Progress will automatically increment from 0 to ~90
33
- */
20
+ const appConfig = useAppConfig()
21
+ const isLoading = useState('core:loading', () => appConfig.coreLayer?.loading?.enabled !== false)
22
+ const progress = useState('core:loading:progress', () => 0)
23
+
34
24
  function startLoading(): void {
35
- state.isLoading.value = true
36
- state.progress.value = 0
25
+ isLoading.value = true
26
+ progress.value = 0
37
27
 
38
- // Clear any existing intervals
39
28
  if (progressInterval) clearInterval(progressInterval)
40
29
  if (progressTimeout) clearTimeout(progressTimeout)
41
30
 
42
- // Simulate progress: increment randomly to 90%, then wait
43
31
  progressInterval = setInterval(() => {
44
- if (state.progress.value < 90) {
45
- // Random increment between 3-8%
32
+ if (progress.value < 90) {
46
33
  const increment = Math.random() * 5 + 3
47
- state.progress.value = Math.min(state.progress.value + increment, 90)
34
+ progress.value = Math.min(progress.value + increment, 90)
48
35
  } else {
49
- // Stop at 90, wait for manual completion
50
36
  if (progressInterval) {
51
37
  clearInterval(progressInterval)
52
38
  progressInterval = null
53
39
  }
54
40
  }
55
- }, 150) // Update every 150ms
41
+ }, 150)
56
42
  }
57
43
 
58
- /**
59
- * Stop loading and jump to 100%
60
- */
61
44
  function stopLoading(): void {
62
- // Clear any running intervals
63
45
  if (progressInterval) {
64
46
  clearInterval(progressInterval)
65
47
  progressInterval = null
66
48
  }
67
49
 
68
- // Jump to 100%
69
- state.progress.value = 100
50
+ progress.value = 100
70
51
 
71
- // Mark as not loading after a brief delay (allows 100% to be visible)
72
52
  progressTimeout = setTimeout(() => {
73
- state.isLoading.value = false
53
+ isLoading.value = false
74
54
  }, 100)
75
55
  }
76
56
 
77
- /**
78
- * Manually update progress (0-100)
79
- * @param value - Progress percentage (0-100)
80
- */
81
57
  function updateProgress(value: number): void {
82
- state.progress.value = Math.min(Math.max(value, 0), 100)
58
+ progress.value = Math.min(Math.max(value, 0), 100)
83
59
  }
84
60
 
85
- /**
86
- * Execute async function with loading state
87
- * @param fn - Async function to execute
88
- */
89
61
  async function withLoading(fn: () => Promise<void>): Promise<void> {
90
62
  startLoading()
91
63
  try {
@@ -96,11 +68,8 @@ export function useLoading() {
96
68
  }
97
69
 
98
70
  return {
99
- // State (reactive refs)
100
- isLoading: readonly(state.isLoading),
101
- progress: readonly(state.progress),
102
-
103
- // Actions
71
+ isLoading: readonly(isLoading),
72
+ progress: readonly(progress),
104
73
  startLoading,
105
74
  stopLoading,
106
75
  updateProgress,
@@ -14,12 +14,9 @@ interface SavedStyles {
14
14
  }
15
15
 
16
16
  // ---------------------------------------------------------------------------
17
- // Singleton state (shared across all calls)
17
+ // Singleton DOM tracking state (non-reactive, intentionally module-scope)
18
18
  // ---------------------------------------------------------------------------
19
19
 
20
- const isEnabled = ref(false)
21
- const clampedCount = ref(0)
22
-
23
20
  const clampedElements = new WeakMap<HTMLElement, SavedStyles>()
24
21
  /** Live set so we can iterate and restore on disable */
25
22
  const clampedSet = new Set<HTMLElement>()
@@ -36,155 +33,147 @@ let savedBodyTransition = ''
36
33
  let opts: ScrollGuardConfig | null = null
37
34
 
38
35
  // ---------------------------------------------------------------------------
39
- // Internal helpers
36
+ // Composable
40
37
  // ---------------------------------------------------------------------------
41
38
 
42
- function shouldExclude(el: HTMLElement): boolean {
43
- if (!opts) return true
44
- return opts.excludeSelectors.some((sel) => {
45
- try {
46
- return el.matches(sel)
47
- } catch {
48
- return false
49
- }
50
- })
51
- }
52
-
53
- function clampElement(el: HTMLElement) {
54
- if (!opts || clampedElements.has(el) || shouldExclude(el)) return
55
- if (el.scrollWidth <= window.innerWidth) return
56
-
57
- // Save original styles
58
- clampedElements.set(el, {
59
- transition: el.style.transition,
60
- maxWidth: el.style.maxWidth,
61
- boxSizing: el.style.boxSizing,
62
- overflowX: el.style.overflowX,
63
- })
64
- clampedSet.add(el)
65
-
66
- el.style.transition = `max-width ${opts.transitionDuration}ms ease`
67
- el.style.maxWidth = '100%'
68
- el.style.boxSizing = 'border-box'
69
-
70
- if (opts.debug) {
71
- el.style.outline = '2px dashed red'
72
- setTimeout(() => {
73
- el.style.outline = ''
74
- }, 1000)
39
+ /**
40
+ * Horizontal scroll guard composable
41
+ *
42
+ * Prevents unintended horizontal overflow by clamping overflowing elements
43
+ * and hiding horizontal scroll on html/body. Fully togglable at runtime.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * const { isEnabled, clampedCount, enable, disable, toggle, scan } = useScrollGuard()
48
+ *
49
+ * // Disable temporarily for a modal with intentional overflow
50
+ * disable()
51
+ * // Re-enable when done
52
+ * enable()
53
+ * ```
54
+ */
55
+ export function useScrollGuard() {
56
+ const isEnabled = useState('core:scroll-guard:enabled', () => false)
57
+ const clampedCount = useState('core:scroll-guard:clamped', () => 0)
58
+
59
+ function shouldExclude(el: HTMLElement): boolean {
60
+ if (!opts) return true
61
+ return opts.excludeSelectors.some((sel) => {
62
+ try {
63
+ return el.matches(sel)
64
+ } catch {
65
+ return false
66
+ }
67
+ })
75
68
  }
76
69
 
77
- clampedCount.value++
78
- }
70
+ function clampElement(el: HTMLElement) {
71
+ if (!opts || clampedElements.has(el) || shouldExclude(el)) return
72
+ if (el.scrollWidth <= window.innerWidth) return
73
+
74
+ clampedElements.set(el, {
75
+ transition: el.style.transition,
76
+ maxWidth: el.style.maxWidth,
77
+ boxSizing: el.style.boxSizing,
78
+ overflowX: el.style.overflowX,
79
+ })
80
+ clampedSet.add(el)
81
+
82
+ el.style.transition = `max-width ${opts.transitionDuration}ms ease`
83
+ el.style.maxWidth = '100%'
84
+ el.style.boxSizing = 'border-box'
85
+
86
+ if (opts.debug) {
87
+ el.style.outline = '2px dashed red'
88
+ setTimeout(() => {
89
+ el.style.outline = ''
90
+ }, 1000)
91
+ }
79
92
 
93
+ clampedCount.value++
94
+ }
80
95
 
81
- function guard() {
82
- if (!isEnabled.value || !opts) return
96
+ function guard() {
97
+ if (!isEnabled.value || !opts) return
83
98
 
84
- const html = document.documentElement
85
- const body = document.body
99
+ const html = document.documentElement
100
+ const body = document.body
86
101
 
87
- html.style.overflowX = 'clip'
88
- body.style.overflowX = 'clip'
89
- body.style.maxWidth = '100vw'
102
+ html.style.overflowX = 'clip'
103
+ body.style.overflowX = 'clip'
104
+ body.style.maxWidth = '100vw'
90
105
 
91
- if (!opts.strict) return
106
+ if (!opts.strict) return
92
107
 
93
- const elements = document.body.querySelectorAll<HTMLElement>('*')
94
- for (const el of elements) {
95
- if (el.offsetParent !== null) {
96
- clampElement(el)
108
+ const elements = document.body.querySelectorAll<HTMLElement>('*')
109
+ for (const el of elements) {
110
+ if (el.offsetParent !== null) {
111
+ clampElement(el)
112
+ }
97
113
  }
98
114
  }
99
- }
100
115
 
101
- function startObservers() {
102
- if (!opts) return
103
-
104
- // MutationObserver for dynamically added content
105
- observer = new MutationObserver((mutations) => {
106
- if (!isEnabled.value || !opts?.strict) return
107
- for (const m of mutations) {
108
- for (const node of m.addedNodes) {
109
- if (node instanceof HTMLElement) {
110
- clampElement(node)
111
- for (const child of node.querySelectorAll<HTMLElement>('*')) {
112
- clampElement(child)
116
+ function startObservers() {
117
+ if (!opts) return
118
+
119
+ observer = new MutationObserver((mutations) => {
120
+ if (!isEnabled.value || !opts?.strict) return
121
+ for (const m of mutations) {
122
+ for (const node of m.addedNodes) {
123
+ if (node instanceof HTMLElement) {
124
+ clampElement(node)
125
+ for (const child of node.querySelectorAll<HTMLElement>('*')) {
126
+ clampElement(child)
127
+ }
113
128
  }
114
129
  }
115
130
  }
116
- }
117
- })
118
- observer.observe(document.body, { childList: true, subtree: true })
119
-
120
- // Debounced resize handler
121
- debouncedGuard = debounce(guard, opts.resizeDebounce)
122
- resizeHandler = debouncedGuard
123
- window.addEventListener('resize', resizeHandler)
124
- }
131
+ })
132
+ observer.observe(document.body, { childList: true, subtree: true })
125
133
 
126
- function stopObservers() {
127
- if (observer) {
128
- observer.disconnect()
129
- observer = null
130
- }
131
- if (resizeHandler) {
132
- window.removeEventListener('resize', resizeHandler)
133
- resizeHandler = null
134
- debouncedGuard = null
134
+ debouncedGuard = debounce(guard, opts.resizeDebounce)
135
+ resizeHandler = debouncedGuard
136
+ window.addEventListener('resize', resizeHandler)
135
137
  }
136
- }
137
138
 
138
- function restoreAll() {
139
- // Restore all clamped elements
140
- for (const el of clampedSet) {
141
- const saved = clampedElements.get(el)
142
- if (saved) {
143
- el.style.transition = saved.transition
144
- el.style.maxWidth = saved.maxWidth
145
- el.style.boxSizing = saved.boxSizing
146
- el.style.overflowX = saved.overflowX
147
- clampedElements.delete(el)
139
+ function stopObservers() {
140
+ if (observer) {
141
+ observer.disconnect()
142
+ observer = null
143
+ }
144
+ if (resizeHandler) {
145
+ window.removeEventListener('resize', resizeHandler)
146
+ resizeHandler = null
147
+ debouncedGuard = null
148
148
  }
149
149
  }
150
- clampedSet.clear()
151
- clampedCount.value = 0
152
-
153
- // Restore html/body styles
154
- const html = document.documentElement
155
- const body = document.body
156
- html.style.overflowX = savedHtmlOverflowX
157
- body.style.overflowX = savedBodyOverflowX
158
- body.style.maxWidth = savedBodyMaxWidth
159
- body.style.transition = savedBodyTransition
160
- }
161
150
 
162
- // ---------------------------------------------------------------------------
163
- // Composable
164
- // ---------------------------------------------------------------------------
151
+ function restoreAll() {
152
+ for (const el of clampedSet) {
153
+ const saved = clampedElements.get(el)
154
+ if (saved) {
155
+ el.style.transition = saved.transition
156
+ el.style.maxWidth = saved.maxWidth
157
+ el.style.boxSizing = saved.boxSizing
158
+ el.style.overflowX = saved.overflowX
159
+ clampedElements.delete(el)
160
+ }
161
+ }
162
+ clampedSet.clear()
163
+ clampedCount.value = 0
164
+
165
+ const html = document.documentElement
166
+ const body = document.body
167
+ html.style.overflowX = savedHtmlOverflowX
168
+ body.style.overflowX = savedBodyOverflowX
169
+ body.style.maxWidth = savedBodyMaxWidth
170
+ body.style.transition = savedBodyTransition
171
+ }
165
172
 
166
- /**
167
- * Horizontal scroll guard composable
168
- *
169
- * Prevents unintended horizontal overflow by clamping overflowing elements
170
- * and hiding horizontal scroll on html/body. Fully togglable at runtime.
171
- *
172
- * @example
173
- * ```ts
174
- * const { isEnabled, clampedCount, enable, disable, toggle, scan } = useScrollGuard()
175
- *
176
- * // Disable temporarily for a modal with intentional overflow
177
- * disable()
178
- * // Re-enable when done
179
- * enable()
180
- * ```
181
- */
182
- export function useScrollGuard() {
183
173
  function enable(config?: Partial<ScrollGuardConfig>) {
184
174
  if (!import.meta.client) return
185
175
  if (isEnabled.value) return
186
176
 
187
- // Resolve config: explicit arg → app.config → defaults
188
177
  const appConfig = useAppConfig()
189
178
  const configFromApp = (
190
179
  appConfig.coreLayer as { scrollGuard?: Partial<ScrollGuardConfig> } | undefined
@@ -201,7 +190,6 @@ export function useScrollGuard() {
201
190
  ...config,
202
191
  }
203
192
 
204
- // Save original html/body styles before we touch them
205
193
  const html = document.documentElement
206
194
  const body = document.body
207
195
  savedHtmlOverflowX = html.style.overflowX
@@ -210,8 +198,6 @@ export function useScrollGuard() {
210
198
  savedBodyTransition = body.style.transition
211
199
 
212
200
  isEnabled.value = true
213
-
214
- // Initial scan
215
201
  guard()
216
202
  startObservers()
217
203
  }
@@ -1,7 +1,7 @@
1
1
  <!-- eslint-disable vue/require-default-prop -->
2
2
  <script setup lang="ts">
3
- import { fieldConfigs } from '../../config/fields'
4
- import type { FieldSize, FieldType } from '../../types/fields'
3
+ import { fieldConfigs, type FieldType } from '../../config/fields'
4
+ import type { FieldSize } from '../../types/fields'
5
5
 
6
6
  const {
7
7
  type = 'text',
@@ -1,7 +1,5 @@
1
- // @ts-nocheck
2
1
  import { z, type ZodObject, type ZodRawShape, type ZodTypeAny } from 'zod'
3
- import { fieldConfigs } from '../config/fields'
4
- import type { FieldType } from '../types/fields'
2
+ import { fieldConfigs, type FieldType } from '../config/fields'
5
3
 
6
4
  /**
7
5
  * Creates a Zod schema from a field type map
@@ -1,12 +1,12 @@
1
- // @ts-nocheck
2
1
  import { z } from 'zod'
3
- import type { FieldConfig, FieldType } from '../types/fields'
2
+ import type { FieldConfig } from '../types/fields'
4
3
 
5
4
  /**
6
- * Field configuration registry
7
- * Defines default behavior, validation, and UI props for each field type
5
+ * Field configuration registry — single source of truth for all field types.
6
+ * FieldType is derived from the keys of this object, so adding a new field
7
+ * here is sufficient; the type stays in sync automatically.
8
8
  */
9
- export const fieldConfigs: Record<FieldType, FieldConfig> = {
9
+ const _fieldConfigs = {
10
10
  text: {
11
11
  inputType: 'text',
12
12
  inputMode: 'text',
@@ -102,4 +102,10 @@ export const fieldConfigs: Record<FieldType, FieldConfig> = {
102
102
  component: 'UInputNumber',
103
103
  format: 'currency',
104
104
  },
105
- } as const
105
+ } satisfies Record<string, FieldConfig>
106
+
107
+ // FieldType is derived from the keys — adding an entry above is all that's needed
108
+ export type FieldType = keyof typeof _fieldConfigs
109
+
110
+ // Cast to widened FieldConfig so callers get safe property access (icon?, placeholder?, etc.)
111
+ export const fieldConfigs = _fieldConfigs as Record<FieldType, FieldConfig>
@@ -1,22 +1,4 @@
1
- // @ts-nocheck
2
- import type { ZodSchema } from 'zod'
3
-
4
- /**
5
- * Supported field types for the FormField component
6
- */
7
- export type FieldType =
8
- | 'text'
9
- | 'email'
10
- | 'phone'
11
- | 'password'
12
- | 'url'
13
- | 'textarea'
14
- | 'name'
15
- | 'number'
16
- | 'date'
17
- | 'time'
18
- | 'search'
19
- | 'currency'
1
+ import type { ZodTypeAny } from 'zod'
20
2
 
21
3
  /**
22
4
  * HTML input modes for mobile keyboard optimization
@@ -48,7 +30,7 @@ export interface FieldConfig {
48
30
  /** Placeholder text */
49
31
  placeholder?: string
50
32
  /** Zod validation schema */
51
- validation: ZodSchema
33
+ validation: ZodTypeAny
52
34
  /** Nuxt UI component to use for rendering */
53
35
  component?: FieldComponent
54
36
  /** Special formatting to apply */
@@ -23,13 +23,3 @@ export default defineNuxtConfig({
23
23
  strict: true,
24
24
  },
25
25
  })
26
-
27
- declare module '@nuxt/schema' {
28
- interface RuntimeConfig {
29
- formsLayer?: {
30
- resendApiKey: string
31
- emailFrom: string
32
- emailTo: string
33
- }
34
- }
35
- }
@@ -0,0 +1,39 @@
1
+ <script setup lang="ts">
2
+ const {
3
+ to,
4
+ from = 0,
5
+ duration = 2,
6
+ ease = 'power2.out',
7
+ format = undefined,
8
+ once = true,
9
+ prefix = '',
10
+ suffix = '',
11
+ as = 'span',
12
+ } = defineProps<{
13
+ to: number
14
+ from?: number
15
+ duration?: number
16
+ ease?: string
17
+ format?: (n: number) => string
18
+ once?: boolean
19
+ prefix?: string
20
+ suffix?: string
21
+ as?: string
22
+ }>()
23
+
24
+ const el = ref<HTMLElement | null>(null)
25
+ const { displayValue, isComplete } = useCountUp(el, {
26
+ to,
27
+ from,
28
+ duration,
29
+ ease,
30
+ once,
31
+ ...(format !== undefined ? { format } : {}),
32
+ })
33
+ </script>
34
+
35
+ <template>
36
+ <component :is="as" ref="el" :class="{ 'is-complete': isComplete }">
37
+ {{ prefix }}{{ displayValue }}{{ suffix }}
38
+ </component>
39
+ </template>
@@ -0,0 +1,101 @@
1
+ <script setup lang="ts">
2
+ type CursorType = 'dot' | 'ring' | 'dot-ring' | 'glow'
3
+
4
+ const {
5
+ type = 'dot-ring',
6
+ visible = true,
7
+ dotSize = 8,
8
+ ringSize = 36,
9
+ glowSize = 80,
10
+ smoothing = 0.15,
11
+ dotSmoothing = 0.35,
12
+ ringSmoothing = 0.1,
13
+ glowSmoothing = 0.08,
14
+ color = 'var(--ui-color-primary-500)',
15
+ hideCursor = true,
16
+ } = defineProps<{
17
+ type?: CursorType
18
+ visible?: boolean
19
+ dotSize?: number
20
+ ringSize?: number
21
+ glowSize?: number
22
+ smoothing?: number
23
+ dotSmoothing?: number
24
+ ringSmoothing?: number
25
+ glowSmoothing?: number
26
+ color?: string
27
+ hideCursor?: boolean
28
+ }>()
29
+
30
+ // Primary follower: dot in 'dot'/'dot-ring', ring in 'ring', blob in 'glow'
31
+ const primarySmoothing = computed(() => {
32
+ if (type === 'glow') return glowSmoothing ?? smoothing
33
+ if (type === 'ring') return ringSmoothing ?? smoothing
34
+ return dotSmoothing ?? smoothing
35
+ })
36
+
37
+ // Secondary follower: lagging ring in 'dot-ring' only
38
+ const secondarySmoothing = computed(() => ringSmoothing ?? smoothing * 0.4)
39
+
40
+ const primary = useCursorFollower({ smoothing: primarySmoothing })
41
+ const secondary = useCursorFollower({ smoothing: secondarySmoothing })
42
+
43
+ onMounted(() => {
44
+ if (hideCursor) document.documentElement.style.cursor = 'none'
45
+ })
46
+
47
+ onUnmounted(() => {
48
+ document.documentElement.style.cursor = ''
49
+ })
50
+ </script>
51
+
52
+ <template>
53
+ <Teleport to="body">
54
+ <div
55
+ class="motion-cursor pointer-events-none fixed inset-0 z-9999 transition-opacity duration-300"
56
+ :style="{ opacity: visible ? 1 : 0 }"
57
+ aria-hidden="true"
58
+ >
59
+ <!-- dot — 'dot' and 'dot-ring' modes -->
60
+ <div
61
+ v-if="type === 'dot' || type === 'dot-ring'"
62
+ class="absolute top-0 left-0 rounded-full will-change-transform"
63
+ :style="{
64
+ width: `${dotSize}px`,
65
+ height: `${dotSize}px`,
66
+ backgroundColor: color,
67
+ transform: `translate(${primary.x.value - dotSize / 2}px, ${primary.y.value - dotSize / 2}px)`,
68
+ }"
69
+ />
70
+
71
+ <!-- ring — primary in 'ring' mode, secondary (lagging) in 'dot-ring' -->
72
+ <div
73
+ v-if="type === 'ring' || type === 'dot-ring'"
74
+ class="absolute top-0 left-0 rounded-full border-2 will-change-transform"
75
+ :style="{
76
+ width: `${ringSize}px`,
77
+ height: `${ringSize}px`,
78
+ borderColor: color,
79
+ transform:
80
+ type === 'dot-ring'
81
+ ? `translate(${secondary.x.value - ringSize / 2}px, ${secondary.y.value - ringSize / 2}px)`
82
+ : `translate(${primary.x.value - ringSize / 2}px, ${primary.y.value - ringSize / 2}px)`,
83
+ }"
84
+ />
85
+
86
+ <!-- glow — large blurred blob -->
87
+ <div
88
+ v-if="type === 'glow'"
89
+ class="absolute top-0 left-0 rounded-full will-change-transform"
90
+ :style="{
91
+ width: `${glowSize}px`,
92
+ height: `${glowSize}px`,
93
+ backgroundColor: color,
94
+ opacity: 0.3,
95
+ filter: 'blur(20px)',
96
+ transform: `translate(${primary.x.value - glowSize / 2}px, ${primary.y.value - glowSize / 2}px)`,
97
+ }"
98
+ />
99
+ </div>
100
+ </Teleport>
101
+ </template>