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
@@ -0,0 +1,25 @@
1
+ import type { MaybeRef } from 'vue'
2
+
3
+ export function useCursorFollower(opts: { smoothing?: MaybeRef<number> } = {}) {
4
+ const { gsap } = useGsap()
5
+ const { x: mouseX, y: mouseY } = useMouse({ type: 'client' })
6
+
7
+ const x = ref(0)
8
+ const y = ref(0)
9
+
10
+ function tick() {
11
+ const factor = unref(opts.smoothing) ?? 0.15
12
+ x.value += (mouseX.value - x.value) * factor
13
+ y.value += (mouseY.value - y.value) * factor
14
+ }
15
+
16
+ onMounted(() => {
17
+ x.value = mouseX.value
18
+ y.value = mouseY.value
19
+ gsap.ticker.add(tick)
20
+ })
21
+
22
+ onUnmounted(() => gsap.ticker.remove(tick))
23
+
24
+ return { x, y }
25
+ }
@@ -0,0 +1,56 @@
1
+ export function useMagneticElement(
2
+ elementRef: Ref<HTMLElement | null>,
3
+ opts: {
4
+ strength?: number
5
+ radius?: number
6
+ damping?: number
7
+ stiffness?: number
8
+ } = {},
9
+ ) {
10
+ const { gsap } = useGsap()
11
+ const { elementX, elementY, elementWidth, elementHeight } = useMouseInElement(elementRef)
12
+
13
+ const currentX = ref(0)
14
+ const currentY = ref(0)
15
+ const isActive = ref(false)
16
+
17
+ function tick() {
18
+ const strength = opts.strength ?? 0.3
19
+ const radius = opts.radius ?? 100
20
+ const damping = (opts.damping ?? 30) / 1000
21
+ const stiffness = (opts.stiffness ?? 300) / 1000
22
+
23
+ const dx = elementX.value - elementWidth.value / 2
24
+ const dy = elementY.value - elementHeight.value / 2
25
+ const dist = Math.sqrt(dx * dx + dy * dy)
26
+
27
+ let targetX = 0
28
+ let targetY = 0
29
+
30
+ if (dist < radius) {
31
+ isActive.value = true
32
+ targetX = dx * strength
33
+ targetY = dy * strength
34
+ } else {
35
+ isActive.value = false
36
+ }
37
+
38
+ currentX.value += (targetX - currentX.value) * stiffness
39
+ currentX.value *= 1 - damping
40
+ currentY.value += (targetY - currentY.value) * stiffness
41
+ currentY.value *= 1 - damping
42
+
43
+ if (elementRef.value) {
44
+ gsap.set(elementRef.value, { x: currentX.value, y: currentY.value })
45
+ }
46
+ }
47
+
48
+ onMounted(() => gsap.ticker.add(tick))
49
+
50
+ onUnmounted(() => {
51
+ gsap.ticker.remove(tick)
52
+ if (elementRef.value) gsap.set(elementRef.value, { x: 0, y: 0 })
53
+ })
54
+
55
+ return { isActive }
56
+ }
@@ -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
+ }
@@ -23,12 +23,19 @@ interface ScrollToOptions {
23
23
  */
24
24
  export function useSmoothScroll() {
25
25
  const nuxtApp = useNuxtApp()
26
+ const appConfig = useAppConfig()
26
27
 
27
28
  // Get the Locomotive Scroll instance from the reactive ref provided by the plugin
28
29
  const locomotiveScroll = computed<LocomotiveScroll | undefined>(
29
30
  () => (nuxtApp.$locomotiveScroll as Ref<LocomotiveScroll | null>)?.value ?? undefined
30
31
  )
31
32
 
33
+ // true if smooth scroll is configured to run (globally or per-route)
34
+ const isEnabled = computed(() => (appConfig.motion?.smoothScroll ?? true) !== false)
35
+
36
+ // true once the LocomotiveScroll instance is actually initialised on this route
37
+ const isReady = computed(() => locomotiveScroll.value != null)
38
+
32
39
  // Reactive scroll state from the plugin
33
40
  const scrollState = computed(
34
41
  () =>
@@ -110,6 +117,10 @@ export function useSmoothScroll() {
110
117
  // Locomotive Scroll instance
111
118
  locomotiveScroll,
112
119
 
120
+ // Scroll availability
121
+ isEnabled,
122
+ isReady,
123
+
113
124
  // Reactive scroll state
114
125
  scrollY,
115
126
  velocity,
@@ -0,0 +1,58 @@
1
+ export function useTiltEffect(
2
+ elementRef: Ref<HTMLElement | null>,
3
+ opts: {
4
+ maxTilt?: number
5
+ perspective?: number
6
+ damping?: number
7
+ stiffness?: number
8
+ } = {},
9
+ ) {
10
+ const { gsap } = useGsap()
11
+ const { elementX, elementY, elementWidth, elementHeight, isOutside } =
12
+ useMouseInElement(elementRef)
13
+
14
+ const currentRotateX = ref(0)
15
+ const currentRotateY = ref(0)
16
+
17
+ function tick() {
18
+ const maxTilt = opts.maxTilt ?? 15
19
+ const perspective = opts.perspective ?? 800
20
+ const damping = (opts.damping ?? 40) / 1000
21
+ const stiffness = (opts.stiffness ?? 250) / 1000
22
+
23
+ const width = elementWidth.value || 1
24
+ const height = elementHeight.value || 1
25
+
26
+ let targetRotateX = 0
27
+ let targetRotateY = 0
28
+
29
+ if (!isOutside.value) {
30
+ const nx = (elementX.value / width) * 2 - 1
31
+ const ny = (elementY.value / height) * 2 - 1
32
+ targetRotateY = nx * maxTilt
33
+ targetRotateX = -ny * maxTilt
34
+ }
35
+
36
+ currentRotateX.value += (targetRotateX - currentRotateX.value) * stiffness
37
+ currentRotateX.value *= 1 - damping
38
+ currentRotateY.value += (targetRotateY - currentRotateY.value) * stiffness
39
+ currentRotateY.value *= 1 - damping
40
+
41
+ if (elementRef.value) {
42
+ gsap.set(elementRef.value, {
43
+ rotateX: currentRotateX.value,
44
+ rotateY: currentRotateY.value,
45
+ transformPerspective: perspective,
46
+ })
47
+ }
48
+ }
49
+
50
+ onMounted(() => gsap.ticker.add(tick))
51
+
52
+ onUnmounted(() => {
53
+ gsap.ticker.remove(tick)
54
+ if (elementRef.value) gsap.set(elementRef.value, { rotateX: 0, rotateY: 0 })
55
+ })
56
+
57
+ return { rotateX: currentRotateX, rotateY: currentRotateY }
58
+ }
@@ -45,7 +45,7 @@ export default defineNuxtPlugin(() => {
45
45
  }
46
46
 
47
47
  const router = useRouter()
48
- const appConfig = useAppConfig() as any
48
+ const appConfig = useAppConfig()
49
49
  const smoothScroll: boolean | string[] = appConfig.motion?.smoothScroll ?? true
50
50
 
51
51
  if (smoothScroll === true) {
@@ -1,7 +1,16 @@
1
1
  declare module '@nuxt/schema' {
2
2
  interface AppConfigInput {
3
- motionLayer?: {
4
- name?: string
3
+ motion?: {
4
+ smoothScroll?: boolean | string[]
5
+ gsapScrollTrigger?: boolean
6
+ lenis?: {
7
+ duration?: number
8
+ orientation?: 'vertical' | 'horizontal'
9
+ gestureOrientation?: 'vertical' | 'horizontal'
10
+ smoothWheel?: boolean
11
+ smoothTouch?: boolean
12
+ touchMultiplier?: number
13
+ }
5
14
  }
6
15
  }
7
16
  }
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "extends": "../../apps/playground/tsconfig.json",
3
+
3
4
  "compilerOptions": {
4
5
  "strict": true
5
6
  },
@@ -1,27 +1,16 @@
1
1
  import { useFeatures } from '../composables/useFeatures'
2
+ import { resolveRoute } from '../utils/resolveRoute'
2
3
 
3
4
  export default defineNuxtRouteMiddleware((to) => {
4
- const { config, isStrictMode, isLayerDefaultDeny } = useRoutingConfig()
5
+ const { config } = useRoutingConfig()
5
6
  const { resolve } = useFeatures()
6
- const meta = to.meta
7
7
 
8
- if (config.debug) console.log('[routing] governance check', to.path, meta)
8
+ if (config.debug) console.log('[routing] governance check', to.path, to.meta)
9
9
 
10
- // strict default-deny: every route must declare a feature
11
- if (isStrictMode() && !meta.feature) {
12
- throw createError({ statusCode: 404 })
13
- }
10
+ const resolution = resolveRoute(to.meta, config, resolve)
14
11
 
15
- // layer default-deny: layer routes must declare a feature
16
- if (isLayerDefaultDeny() && meta.__fromLayer && !meta.feature) {
17
- throw createError({ statusCode: 404 })
18
- }
12
+ if (config.debug) console.log('[routing] resolution', resolution)
19
13
 
20
- if (!meta.feature) return
21
-
22
- const variant = resolve(meta.feature)
23
- if (config.debug) console.log(`[routing] feature "${meta.feature}" resolved to "${variant}"`)
24
-
25
- if (variant === 'disabled') throw createError({ statusCode: 404 })
26
- if (variant === 'beta' || variant === 'coming-soon') return navigateTo('/coming-soon')
14
+ if (resolution.outcome === 'deny') throw createError({ statusCode: 404 })
15
+ if (resolution.outcome === 'redirect') return navigateTo(resolution.to)
27
16
  })
@@ -1,10 +1,16 @@
1
1
  export type FeatureValue = 'enabled' | 'disabled' | 'beta' | 'coming-soon'
2
2
  export type RoutingPreset = 'simple' | 'marketing' | 'product' | 'enterprise'
3
3
 
4
+ export type RouteResolution =
5
+ | { outcome: 'allow' }
6
+ | { outcome: 'deny' }
7
+ | { outcome: 'redirect'; to: string }
8
+
4
9
  export interface RoutingLayerConfig {
5
10
  preset: RoutingPreset
6
11
  strictDefaultDeny: boolean
7
12
  layerDefaultDeny: boolean
13
+ betaRedirect: string
8
14
  runtimeFlags: boolean
9
15
  debug: boolean
10
16
  maintenance: { enabled: boolean; allowRoutes: string[] }
@@ -16,6 +22,7 @@ export const ROUTING_PRESETS: Record<RoutingPreset, Omit<RoutingLayerConfig, 'pr
16
22
  simple: {
17
23
  strictDefaultDeny: false,
18
24
  layerDefaultDeny: false,
25
+ betaRedirect: '/coming-soon',
19
26
  runtimeFlags: false,
20
27
  debug: false,
21
28
  maintenance: { enabled: false, allowRoutes: ['/maintenance'] },
@@ -24,6 +31,7 @@ export const ROUTING_PRESETS: Record<RoutingPreset, Omit<RoutingLayerConfig, 'pr
24
31
  marketing: {
25
32
  strictDefaultDeny: false,
26
33
  layerDefaultDeny: true,
34
+ betaRedirect: '/coming-soon',
27
35
  runtimeFlags: false,
28
36
  debug: false,
29
37
  maintenance: { enabled: true, allowRoutes: ['/maintenance'] },
@@ -32,6 +40,7 @@ export const ROUTING_PRESETS: Record<RoutingPreset, Omit<RoutingLayerConfig, 'pr
32
40
  product: {
33
41
  strictDefaultDeny: false,
34
42
  layerDefaultDeny: true,
43
+ betaRedirect: '/coming-soon',
35
44
  runtimeFlags: true,
36
45
  debug: false,
37
46
  maintenance: { enabled: true, allowRoutes: ['/maintenance'] },
@@ -40,6 +49,7 @@ export const ROUTING_PRESETS: Record<RoutingPreset, Omit<RoutingLayerConfig, 'pr
40
49
  enterprise: {
41
50
  strictDefaultDeny: true,
42
51
  layerDefaultDeny: true,
52
+ betaRedirect: '/coming-soon',
43
53
  runtimeFlags: true,
44
54
  debug: false,
45
55
  maintenance: { enabled: true, allowRoutes: ['/maintenance'] },
@@ -0,0 +1,31 @@
1
+ import type { FeatureValue, RouteResolution, RoutingLayerConfig } from '../types/routing'
2
+
3
+ interface RouteMeta {
4
+ feature?: string
5
+ __fromLayer?: boolean
6
+ }
7
+
8
+ export function resolveRoute(
9
+ meta: RouteMeta,
10
+ config: RoutingLayerConfig,
11
+ resolveFeature: (name: string) => FeatureValue,
12
+ ): RouteResolution {
13
+ if (config.strictDefaultDeny && !meta.feature) {
14
+ return { outcome: 'deny' }
15
+ }
16
+
17
+ if (config.layerDefaultDeny && meta.__fromLayer && !meta.feature) {
18
+ return { outcome: 'deny' }
19
+ }
20
+
21
+ if (!meta.feature) return { outcome: 'allow' }
22
+
23
+ const variant = resolveFeature(meta.feature)
24
+
25
+ if (variant === 'disabled') return { outcome: 'deny' }
26
+ if (variant === 'beta' || variant === 'coming-soon') {
27
+ return { outcome: 'redirect', to: config.betaRedirect }
28
+ }
29
+
30
+ return { outcome: 'allow' }
31
+ }
@@ -15,6 +15,7 @@
15
15
  "three": "^0.182.0"
16
16
  },
17
17
  "devDependencies": {
18
- "@types/three": "^0.182.0"
18
+ "@types/three": "^0.184.0",
19
+ "vite": "^7.0.0"
19
20
  }
20
21
  }
@@ -1,11 +1,10 @@
1
- // @ts-nocheck
2
1
  import { createSharedComposable, useLocalStorage } from '@vueuse/core'
3
2
  import type { AccentColor } from '#layers/theme/app/types/theme'
4
3
 
5
4
  export const useAccentColor = createSharedComposable(() => {
6
5
  const appConfig = useAppConfig()
7
6
 
8
- const defaultAccent = ((appConfig as any).themeLayer?.defaultAccent ?? 'blue') as AccentColor
7
+ const defaultAccent = (appConfig.themeLayer?.defaultAccent ?? 'blue') as AccentColor
9
8
  const accent = useLocalStorage<AccentColor>('theme-colour', defaultAccent)
10
9
 
11
10
  const pageAccent = ref<AccentColor | null>(null)
@@ -19,16 +18,6 @@ export const useAccentColor = createSharedComposable(() => {
19
18
  pageAccent.value = color
20
19
  }
21
20
 
22
- if (import.meta.client) {
23
- watch(
24
- activeAccent,
25
- (color) => {
26
- document.documentElement.setAttribute('data-theme-colour', color)
27
- },
28
- { immediate: true }
29
- )
30
- }
31
-
32
21
  return {
33
22
  accent: readonly(accent),
34
23
  setAccent,
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  import { createSharedComposable, useLocalStorage, usePreferredContrast } from '@vueuse/core'
3
2
  import type { PreferenceOverride } from '#layers/theme/app/types/theme'
4
3
 
@@ -16,16 +15,6 @@ export const useThemeContrast = createSharedComposable(() => {
16
15
  contrastOverride.value = value
17
16
  }
18
17
 
19
- if (import.meta.client) {
20
- watch(
21
- effectiveHighContrast,
22
- (high) => {
23
- document.documentElement.setAttribute('data-theme-contrast', high ? 'high' : 'standard')
24
- },
25
- { immediate: true }
26
- )
27
- }
28
-
29
18
  return {
30
19
  contrastOverride: readonly(contrastOverride),
31
20
  setContrastOverride,
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  import { createSharedComposable, useLocalStorage, usePreferredReducedMotion } from '@vueuse/core'
3
2
  import type { PreferenceOverride } from '#layers/theme/app/types/theme'
4
3
 
@@ -16,16 +15,6 @@ export const useThemeMotion = createSharedComposable(() => {
16
15
  motionOverride.value = value
17
16
  }
18
17
 
19
- if (import.meta.client) {
20
- watch(
21
- effectiveReducedMotion,
22
- (reduced) => {
23
- document.documentElement.setAttribute('data-theme-motion', reduced ? 'reduced' : 'full')
24
- },
25
- { immediate: true }
26
- )
27
- }
28
-
29
18
  return {
30
19
  motionOverride: readonly(motionOverride),
31
20
  setMotionOverride,
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  import {
3
2
  createSharedComposable,
4
3
  useLocalStorage,
@@ -20,19 +19,6 @@ export const useThemeTransparency = createSharedComposable(() => {
20
19
  transparencyOverride.value = value
21
20
  }
22
21
 
23
- if (import.meta.client) {
24
- watch(
25
- effectiveReducedTransparency,
26
- (reduced) => {
27
- document.documentElement.setAttribute(
28
- 'data-theme-transparency',
29
- reduced ? 'reduced' : 'full'
30
- )
31
- },
32
- { immediate: true }
33
- )
34
- }
35
-
36
22
  return {
37
23
  transparencyOverride: readonly(transparencyOverride),
38
24
  setTransparencyOverride,
@@ -1,5 +1,22 @@
1
1
  export default defineNuxtPlugin(() => {
2
- // Initialize shared composables applies data-theme-colour, data-theme-contrast,
3
- // data-theme-motion, and data-theme-transparency on <html> on first load.
4
- useTheme()
2
+ const { activeAccent } = useAccentColor()
3
+ const { effectiveHighContrast } = useThemeContrast()
4
+ const { effectiveReducedMotion } = useThemeMotion()
5
+ const { effectiveReducedTransparency } = useThemeTransparency()
6
+
7
+ watch(activeAccent, (color) => {
8
+ document.documentElement.setAttribute('data-theme-colour', color)
9
+ }, { immediate: true })
10
+
11
+ watch(effectiveHighContrast, (high) => {
12
+ document.documentElement.setAttribute('data-theme-contrast', high ? 'high' : 'standard')
13
+ }, { immediate: true })
14
+
15
+ watch(effectiveReducedMotion, (reduced) => {
16
+ document.documentElement.setAttribute('data-theme-motion', reduced ? 'reduced' : 'full')
17
+ }, { immediate: true })
18
+
19
+ watch(effectiveReducedTransparency, (reduced) => {
20
+ document.documentElement.setAttribute('data-theme-transparency', reduced ? 'reduced' : 'full')
21
+ }, { immediate: true })
5
22
  })
@@ -1,8 +1,8 @@
1
1
  declare module '@nuxt/schema' {
2
2
  interface AppConfigInput {
3
3
  themeLayer?: {
4
- accents?: string[]
5
- defaultAccent?: string
4
+ accents?: import('./theme').AccentColor[]
5
+ defaultAccent?: import('./theme').AccentColor
6
6
  }
7
7
  }
8
8
  }
@@ -1,5 +1,4 @@
1
1
  <script setup lang="ts">
2
- // @ts-nocheck
3
2
  /**
4
3
  * BaseModal — overlay-compatible modal shell.
5
4
  *
@@ -1,36 +1,5 @@
1
1
  import type { ColorUsage, UiColors } from '../types/colors'
2
2
 
3
- const colorMap: Record<UiColors, string> = {
4
- // Semantic
5
- dimmed: 'dimmed',
6
- muted: 'muted',
7
- toned: 'toned',
8
- highlighted: 'highlighted',
9
- inverted: 'inverted',
10
- default: 'default',
11
-
12
- // Status
13
- info: 'info',
14
- success: 'success',
15
- warning: 'warning',
16
- error: 'error',
17
-
18
- // Nuxt UI core
19
- primary: 'primary',
20
- neutral: 'neutral',
21
-
22
- // Custom
23
- secondary: 'secondary',
24
- accent: 'accent',
25
-
26
- // Base
27
- black: 'black',
28
- white: 'white',
29
- }
30
-
31
3
  export function useColor(color?: UiColors, type: ColorUsage = 'text') {
32
- return computed(() => {
33
- const colorName = color ? colorMap[color] : 'default'
34
- return `${type}-${colorName}`
35
- })
4
+ return computed(() => `${type}-${color ?? 'default'}`)
36
5
  }
@@ -1,5 +1,11 @@
1
1
  import type { Component } from 'vue'
2
2
 
3
+ export interface ModalController<P extends Record<string, unknown>> {
4
+ open: (props?: Partial<P>) => void
5
+ close: () => void
6
+ patch: (props: Partial<P>) => void
7
+ }
8
+
3
9
  /**
4
10
  * Factory that turns any overlay-compatible component into a shared modal composable.
5
11
  *
@@ -19,10 +25,13 @@ import type { Component } from 'vue'
19
25
  * Extend BaseModal.vue as a starting point.
20
26
  */
21
27
  export function createModal<P extends Record<string, unknown>>(component: Component) {
22
- return createSharedComposable(() => {
23
- if (import.meta.server) return { open: () => {}, close: () => {}, patch: () => {} }
28
+ return createSharedComposable((): ModalController<P> => {
29
+ if (import.meta.server) {
30
+ return { open: () => {}, close: () => {}, patch: () => {} }
31
+ }
24
32
 
25
33
  const overlay = useOverlay()
34
+ // useOverlay is a Nuxt UI auto-import; not resolvable in standalone layer typecheck
26
35
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
36
  const modal = overlay.create(component as any)
28
37
  return {