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
|
@@ -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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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 (
|
|
45
|
-
// Random increment between 3-8%
|
|
32
|
+
if (progress.value < 90) {
|
|
46
33
|
const increment = Math.random() * 5 + 3
|
|
47
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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 (
|
|
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
|
-
//
|
|
36
|
+
// Composable
|
|
40
37
|
// ---------------------------------------------------------------------------
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
+
function guard() {
|
|
97
|
+
if (!isEnabled.value || !opts) return
|
|
83
98
|
|
|
84
|
-
|
|
85
|
-
|
|
99
|
+
const html = document.documentElement
|
|
100
|
+
const body = document.body
|
|
86
101
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
102
|
+
html.style.overflowX = 'clip'
|
|
103
|
+
body.style.overflowX = 'clip'
|
|
104
|
+
body.style.maxWidth = '100vw'
|
|
90
105
|
|
|
91
|
-
|
|
106
|
+
if (!opts.strict) return
|
|
92
107
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2
|
+
import type { FieldConfig } from '../types/fields'
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
|
-
* Field configuration registry
|
|
7
|
-
*
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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:
|
|
33
|
+
validation: ZodTypeAny
|
|
52
34
|
/** Nuxt UI component to use for rendering */
|
|
53
35
|
component?: FieldComponent
|
|
54
36
|
/** Special formatting to apply */
|
|
@@ -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>
|