kmcom-nuxt-layers 1.4.0 → 1.5.1

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.
@@ -1,9 +1,11 @@
1
- // Conditionally load nuxt-studio if installed (optional peer dependency)
1
+ // Conditionally load nuxt-studio if installed (optional peer dependency) and in development
2
2
  const studioModule: string[] = []
3
- try {
4
- await import('nuxt-studio')
5
- studioModule.push('nuxt-studio')
6
- } catch {}
3
+ if (process.env.NODE_ENV === 'development') {
4
+ try {
5
+ await import('nuxt-studio')
6
+ studioModule.push('nuxt-studio')
7
+ } catch {}
8
+ }
7
9
 
8
10
  // https://nuxt.com/docs/api/configuration/nuxt-config
9
11
  export default defineNuxtConfig({
@@ -20,6 +22,15 @@ export default defineNuxtConfig({
20
22
 
21
23
  modules: ['@nuxt/ui', '@nuxt/content', ...studioModule],
22
24
 
25
+ // Configure @nuxt/content for production/Netlify builds
26
+ content: {
27
+ // Disable local database for production builds to avoid better-sqlite3 native module issues
28
+ // database: process.env.NETLIFY || process.env.NODE_ENV === 'production' ? false : {},
29
+ experimental: {
30
+ nativeSqlite: true,
31
+ },
32
+ },
33
+
23
34
  css: ['#layers/content/app/assets/css/main.css'],
24
35
 
25
36
  compatibilityDate: '2026-01-24',
@@ -23,7 +23,7 @@
23
23
  */
24
24
 
25
25
  interface Props<T> {
26
- items: T[]
26
+ items?: T[]
27
27
  columns?: 2 | 3 | 4 | 6
28
28
  itemRowSpan?: number
29
29
  fullHeight?: boolean
@@ -0,0 +1,20 @@
1
+ import type { RoutingLayerConfig } from './types/routing'
2
+
3
+ export default defineAppConfig({
4
+ routingLayer: {
5
+ preset: 'simple',
6
+ strictDefaultDeny: false,
7
+ layerDefaultDeny: false,
8
+ runtimeFlags: false,
9
+ debug: false,
10
+ maintenance: { enabled: false, allowRoutes: ['/maintenance'] },
11
+ scrollRouting: { enabled: false, mode: 'replace' },
12
+ features: {},
13
+ } satisfies RoutingLayerConfig,
14
+ })
15
+
16
+ declare module '@nuxt/schema' {
17
+ interface AppConfigInput {
18
+ routingLayer?: Partial<import('./types/routing').RoutingLayerConfig>
19
+ }
20
+ }
@@ -0,0 +1,12 @@
1
+ import type { FeatureValue } from '../types/routing'
2
+
3
+ export function useFeatures() {
4
+ const { config } = useRoutingConfig()
5
+ const runtimeFlags = useState<Record<string, FeatureValue>>('routing:flags', () => ({}))
6
+
7
+ function resolve(name: string): FeatureValue {
8
+ return runtimeFlags.value[name] ?? config.features[name] ?? 'disabled'
9
+ }
10
+
11
+ return { resolve, runtimeFlags }
12
+ }
@@ -0,0 +1,7 @@
1
+ export function useMaintenance() {
2
+ const { config } = useRoutingConfig()
3
+ return {
4
+ isEnabled: computed(() => config.maintenance.enabled),
5
+ allowRoutes: computed(() => config.maintenance.allowRoutes),
6
+ }
7
+ }
@@ -0,0 +1,20 @@
1
+ import { defu } from 'defu'
2
+ import { ROUTING_PRESETS } from '../types/routing'
3
+ import type { RoutingLayerConfig, FeatureValue } from '../types/routing'
4
+
5
+ export function useRoutingConfig() {
6
+ const appConfig = useAppConfig()
7
+ const user = appConfig.routingLayer as Partial<RoutingLayerConfig>
8
+ const preset = ROUTING_PRESETS[user.preset ?? 'simple']
9
+ const config = defu(user, preset) as RoutingLayerConfig
10
+
11
+ return {
12
+ config,
13
+ isStrictMode: () => config.strictDefaultDeny,
14
+ isLayerDefaultDeny: () => config.layerDefaultDeny,
15
+ getFeatureVariant: (name: string): FeatureValue =>
16
+ config.features[name] ?? 'disabled',
17
+ isFeatureEnabled: (name: string) =>
18
+ config.features[name] === 'enabled',
19
+ }
20
+ }
@@ -0,0 +1,6 @@
1
+ export default defineNuxtRouteMiddleware((to) => {
2
+ const { isEnabled, allowRoutes } = useMaintenance()
3
+ if (!isEnabled.value) return
4
+ if (allowRoutes.value.some(r => to.path.startsWith(r))) return
5
+ return navigateTo(allowRoutes.value[0])
6
+ })
@@ -0,0 +1,25 @@
1
+ export default defineNuxtRouteMiddleware((to) => {
2
+ const { config, isStrictMode, isLayerDefaultDeny } = useRoutingConfig()
3
+ const { resolve } = useFeatures()
4
+ const meta = to.meta
5
+
6
+ if (config.debug) console.log('[routing] governance check', to.path, meta)
7
+
8
+ // strict default-deny: every route must declare a feature
9
+ if (isStrictMode() && !meta.feature) {
10
+ throw createError({ statusCode: 404 })
11
+ }
12
+
13
+ // layer default-deny: layer routes must declare a feature
14
+ if (isLayerDefaultDeny() && meta.__fromLayer && !meta.feature) {
15
+ throw createError({ statusCode: 404 })
16
+ }
17
+
18
+ if (!meta.feature) return
19
+
20
+ const variant = resolve(meta.feature)
21
+ if (config.debug) console.log(`[routing] feature "${meta.feature}" resolved to "${variant}"`)
22
+
23
+ if (variant === 'disabled') throw createError({ statusCode: 404 })
24
+ if (variant === 'beta' || variant === 'coming-soon') return navigateTo('/coming-soon')
25
+ })
@@ -0,0 +1,15 @@
1
+ import type { FeatureValue } from '../types/routing'
2
+
3
+ export default defineNuxtPlugin(async () => {
4
+ const { config } = useRoutingConfig()
5
+ if (!config.runtimeFlags) return
6
+
7
+ const { runtimeFlags } = useFeatures()
8
+ try {
9
+ const data = await $fetch<Record<string, FeatureValue>>('/api/feature-flags')
10
+ runtimeFlags.value = data
11
+ }
12
+ catch (e) {
13
+ if (config.debug) console.warn('[routing] Failed to fetch feature flags', e)
14
+ }
15
+ })
@@ -0,0 +1,21 @@
1
+ export default defineNuxtPlugin(() => {
2
+ const { config } = useRoutingConfig()
3
+ if (!config.scrollRouting.enabled) return
4
+
5
+ const router = useRouter()
6
+ const observer = new IntersectionObserver(
7
+ (entries) => {
8
+ const visible = entries.find(e => e.isIntersecting)
9
+ if (!visible) return
10
+ const id = visible.target.getAttribute('data-section')
11
+ if (!id) return
12
+ const method = config.scrollRouting.mode === 'push' ? 'push' : 'replace'
13
+ router[method]({ hash: `#${id}` })
14
+ },
15
+ { threshold: 0.5 },
16
+ )
17
+
18
+ onNuxtReady(() => {
19
+ document.querySelectorAll('[data-section]').forEach(el => observer.observe(el))
20
+ })
21
+ })
@@ -0,0 +1,6 @@
1
+ declare module 'vue-router' {
2
+ interface RouteMeta {
3
+ feature?: string
4
+ __fromLayer?: boolean
5
+ }
6
+ }
@@ -0,0 +1,48 @@
1
+ export type FeatureValue = 'enabled' | 'disabled' | 'beta' | 'coming-soon'
2
+ export type RoutingPreset = 'simple' | 'marketing' | 'product' | 'enterprise'
3
+
4
+ export interface RoutingLayerConfig {
5
+ preset: RoutingPreset
6
+ strictDefaultDeny: boolean
7
+ layerDefaultDeny: boolean
8
+ runtimeFlags: boolean
9
+ debug: boolean
10
+ maintenance: { enabled: boolean; allowRoutes: string[] }
11
+ scrollRouting: { enabled: boolean; mode: 'replace' | 'push' }
12
+ features: Record<string, FeatureValue>
13
+ }
14
+
15
+ export const ROUTING_PRESETS: Record<RoutingPreset, Omit<RoutingLayerConfig, 'preset' | 'features'>> = {
16
+ simple: {
17
+ strictDefaultDeny: false,
18
+ layerDefaultDeny: false,
19
+ runtimeFlags: false,
20
+ debug: false,
21
+ maintenance: { enabled: false, allowRoutes: ['/maintenance'] },
22
+ scrollRouting: { enabled: false, mode: 'replace' },
23
+ },
24
+ marketing: {
25
+ strictDefaultDeny: false,
26
+ layerDefaultDeny: true,
27
+ runtimeFlags: false,
28
+ debug: false,
29
+ maintenance: { enabled: true, allowRoutes: ['/maintenance'] },
30
+ scrollRouting: { enabled: true, mode: 'replace' },
31
+ },
32
+ product: {
33
+ strictDefaultDeny: false,
34
+ layerDefaultDeny: true,
35
+ runtimeFlags: true,
36
+ debug: false,
37
+ maintenance: { enabled: true, allowRoutes: ['/maintenance'] },
38
+ scrollRouting: { enabled: false, mode: 'replace' },
39
+ },
40
+ enterprise: {
41
+ strictDefaultDeny: true,
42
+ layerDefaultDeny: true,
43
+ runtimeFlags: true,
44
+ debug: false,
45
+ maintenance: { enabled: true, allowRoutes: ['/maintenance'] },
46
+ scrollRouting: { enabled: false, mode: 'replace' },
47
+ },
48
+ }
@@ -0,0 +1,27 @@
1
+ import type { NuxtPage } from '@nuxt/schema'
2
+
3
+ export default defineNuxtConfig({
4
+ $meta: { name: 'routing' },
5
+
6
+ alias: {
7
+ '#layers/routing': import.meta.dirname,
8
+ },
9
+
10
+ compatibilityDate: '2026-01-30',
11
+
12
+ hooks: {
13
+ 'pages:extend'(pages) {
14
+ const cwd = process.cwd()
15
+ const tag = (list: NuxtPage[]) => {
16
+ for (const page of list) {
17
+ if (page.file && !page.file.startsWith(cwd)) {
18
+ page.meta ??= {}
19
+ page.meta.__fromLayer = true
20
+ }
21
+ if (page.children) tag(page.children)
22
+ }
23
+ }
24
+ tag(pages)
25
+ },
26
+ },
27
+ })
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "@layers/routing",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./nuxt.config.ts"
6
+ }
@@ -0,0 +1,87 @@
1
+ <script setup lang="ts">
2
+ // @ts-nocheck - TSL types
3
+ import {
4
+ createAmbientUniforms,
5
+ createThemeBubbleColorNode,
6
+ } from '#layers/shader/app/composables/useAmbientMaterials'
7
+
8
+ const props = withDefaults(
9
+ defineProps<{
10
+ speed?: number
11
+ intensity?: number
12
+ mouseInteraction?: boolean
13
+ mouseStrength?: number
14
+ color1?: string
15
+ color2?: string
16
+ color3?: string
17
+ color4?: string
18
+ }>(),
19
+ {
20
+ speed: 1.0,
21
+ intensity: 1.0,
22
+ mouseInteraction: true,
23
+ mouseStrength: 0.3,
24
+ color1: '#8b5cf6',
25
+ color2: '#6366f1',
26
+ color3: '#a78bfa',
27
+ color4: '#38bdf8',
28
+ }
29
+ )
30
+
31
+ const emit = defineEmits<{
32
+ node: [colorNode: any]
33
+ }>()
34
+
35
+ const uniforms = createAmbientUniforms({
36
+ speed: props.speed,
37
+ intensity: props.intensity,
38
+ mouseInteraction: props.mouseInteraction,
39
+ })
40
+ if (props.mouseInteraction) {
41
+ uniforms.mouseStrength.value = props.mouseStrength
42
+ }
43
+
44
+ const c1 = useShaderColor(props.color1)
45
+ const c2 = useShaderColor(props.color2)
46
+ const c3 = useShaderColor(props.color3)
47
+ const c4 = useShaderColor(props.color4)
48
+
49
+ // color3/color4 accepted for API consistency but Bubble only uses color1+color2
50
+ const colorNode = createThemeBubbleColorNode(uniforms, {
51
+ color1: c1.node,
52
+ color2: c2.node,
53
+ color3: c3.node,
54
+ color4: c4.node,
55
+ })
56
+
57
+ watch(() => props.color1, (hex) => c1.tweenTo(hex, 0.8))
58
+ watch(() => props.color2, (hex) => c2.tweenTo(hex, 0.8))
59
+ watch(() => props.color3, (hex) => c3.tweenTo(hex, 0.8))
60
+ watch(() => props.color4, (hex) => c4.tweenTo(hex, 0.8))
61
+
62
+ try {
63
+ const runtime = useShaderRuntimeContext()
64
+ watch(
65
+ () => [runtime.mouse.mouseX.value, runtime.mouse.mouseY.value],
66
+ ([mx, my]) => {
67
+ uniforms.mouseX.value = mx
68
+ uniforms.mouseY.value = my
69
+ },
70
+ { immediate: true }
71
+ )
72
+ } catch {
73
+ // No runtime context
74
+ }
75
+
76
+ watch(() => props.speed, (v) => { uniforms.speed.value = v })
77
+ watch(() => props.intensity, (v) => { uniforms.intensity.value = v })
78
+ watch(() => props.mouseInteraction, (v) => { uniforms.mouseStrength.value = v ? props.mouseStrength : 0 })
79
+ watch(() => props.mouseStrength, (v) => { if (props.mouseInteraction) uniforms.mouseStrength.value = v })
80
+
81
+ emit('node', colorNode)
82
+ defineExpose({ uniforms, colorNode })
83
+ </script>
84
+
85
+ <template>
86
+ <slot />
87
+ </template>
@@ -0,0 +1,86 @@
1
+ <script setup lang="ts">
2
+ // @ts-nocheck - TSL types
3
+ import {
4
+ createAmbientUniforms,
5
+ createThemeLavaLampColorNode,
6
+ } from '#layers/shader/app/composables/useAmbientMaterials'
7
+
8
+ const props = withDefaults(
9
+ defineProps<{
10
+ speed?: number
11
+ intensity?: number
12
+ mouseInteraction?: boolean
13
+ mouseStrength?: number
14
+ color1?: string
15
+ color2?: string
16
+ color3?: string
17
+ color4?: string
18
+ }>(),
19
+ {
20
+ speed: 1.0,
21
+ intensity: 1.0,
22
+ mouseInteraction: true,
23
+ mouseStrength: 0.3,
24
+ color1: '#8b5cf6',
25
+ color2: '#6366f1',
26
+ color3: '#a78bfa',
27
+ color4: '#38bdf8',
28
+ }
29
+ )
30
+
31
+ const emit = defineEmits<{
32
+ node: [colorNode: any]
33
+ }>()
34
+
35
+ const uniforms = createAmbientUniforms({
36
+ speed: props.speed,
37
+ intensity: props.intensity,
38
+ mouseInteraction: props.mouseInteraction,
39
+ })
40
+ if (props.mouseInteraction) {
41
+ uniforms.mouseStrength.value = props.mouseStrength
42
+ }
43
+
44
+ const c1 = useShaderColor(props.color1)
45
+ const c2 = useShaderColor(props.color2)
46
+ const c3 = useShaderColor(props.color3)
47
+ const c4 = useShaderColor(props.color4)
48
+
49
+ const colorNode = createThemeLavaLampColorNode(uniforms, {
50
+ color1: c1.node,
51
+ color2: c2.node,
52
+ color3: c3.node,
53
+ color4: c4.node,
54
+ })
55
+
56
+ watch(() => props.color1, (hex) => c1.tweenTo(hex, 0.8))
57
+ watch(() => props.color2, (hex) => c2.tweenTo(hex, 0.8))
58
+ watch(() => props.color3, (hex) => c3.tweenTo(hex, 0.8))
59
+ watch(() => props.color4, (hex) => c4.tweenTo(hex, 0.8))
60
+
61
+ try {
62
+ const runtime = useShaderRuntimeContext()
63
+ watch(
64
+ () => [runtime.mouse.mouseX.value, runtime.mouse.mouseY.value],
65
+ ([mx, my]) => {
66
+ uniforms.mouseX.value = mx
67
+ uniforms.mouseY.value = my
68
+ },
69
+ { immediate: true }
70
+ )
71
+ } catch {
72
+ // No runtime context
73
+ }
74
+
75
+ watch(() => props.speed, (v) => { uniforms.speed.value = v })
76
+ watch(() => props.intensity, (v) => { uniforms.intensity.value = v })
77
+ watch(() => props.mouseInteraction, (v) => { uniforms.mouseStrength.value = v ? props.mouseStrength : 0 })
78
+ watch(() => props.mouseStrength, (v) => { if (props.mouseInteraction) uniforms.mouseStrength.value = v })
79
+
80
+ emit('node', colorNode)
81
+ defineExpose({ uniforms, colorNode })
82
+ </script>
83
+
84
+ <template>
85
+ <slot />
86
+ </template>
@@ -0,0 +1,86 @@
1
+ <script setup lang="ts">
2
+ // @ts-nocheck - TSL types
3
+ import {
4
+ createAmbientUniforms,
5
+ createThemePlasmaColorNode,
6
+ } from '#layers/shader/app/composables/useAmbientMaterials'
7
+
8
+ const props = withDefaults(
9
+ defineProps<{
10
+ speed?: number
11
+ intensity?: number
12
+ mouseInteraction?: boolean
13
+ mouseStrength?: number
14
+ color1?: string
15
+ color2?: string
16
+ color3?: string
17
+ color4?: string
18
+ }>(),
19
+ {
20
+ speed: 1.0,
21
+ intensity: 1.0,
22
+ mouseInteraction: true,
23
+ mouseStrength: 0.3,
24
+ color1: '#8b5cf6',
25
+ color2: '#6366f1',
26
+ color3: '#a78bfa',
27
+ color4: '#38bdf8',
28
+ }
29
+ )
30
+
31
+ const emit = defineEmits<{
32
+ node: [colorNode: any]
33
+ }>()
34
+
35
+ const uniforms = createAmbientUniforms({
36
+ speed: props.speed,
37
+ intensity: props.intensity,
38
+ mouseInteraction: props.mouseInteraction,
39
+ })
40
+ if (props.mouseInteraction) {
41
+ uniforms.mouseStrength.value = props.mouseStrength
42
+ }
43
+
44
+ const c1 = useShaderColor(props.color1)
45
+ const c2 = useShaderColor(props.color2)
46
+ const c3 = useShaderColor(props.color3)
47
+ const c4 = useShaderColor(props.color4)
48
+
49
+ const colorNode = createThemePlasmaColorNode(uniforms, {
50
+ color1: c1.node,
51
+ color2: c2.node,
52
+ color3: c3.node,
53
+ color4: c4.node,
54
+ })
55
+
56
+ watch(() => props.color1, (hex) => c1.tweenTo(hex, 0.8))
57
+ watch(() => props.color2, (hex) => c2.tweenTo(hex, 0.8))
58
+ watch(() => props.color3, (hex) => c3.tweenTo(hex, 0.8))
59
+ watch(() => props.color4, (hex) => c4.tweenTo(hex, 0.8))
60
+
61
+ try {
62
+ const runtime = useShaderRuntimeContext()
63
+ watch(
64
+ () => [runtime.mouse.mouseX.value, runtime.mouse.mouseY.value],
65
+ ([mx, my]) => {
66
+ uniforms.mouseX.value = mx
67
+ uniforms.mouseY.value = my
68
+ },
69
+ { immediate: true }
70
+ )
71
+ } catch {
72
+ // No runtime context
73
+ }
74
+
75
+ watch(() => props.speed, (v) => { uniforms.speed.value = v })
76
+ watch(() => props.intensity, (v) => { uniforms.intensity.value = v })
77
+ watch(() => props.mouseInteraction, (v) => { uniforms.mouseStrength.value = v ? props.mouseStrength : 0 })
78
+ watch(() => props.mouseStrength, (v) => { if (props.mouseInteraction) uniforms.mouseStrength.value = v })
79
+
80
+ emit('node', colorNode)
81
+ defineExpose({ uniforms, colorNode })
82
+ </script>
83
+
84
+ <template>
85
+ <slot />
86
+ </template>
@@ -0,0 +1,86 @@
1
+ <script setup lang="ts">
2
+ // @ts-nocheck - TSL types
3
+ import {
4
+ createAmbientUniforms,
5
+ createThemeWaveColorNode,
6
+ } from '#layers/shader/app/composables/useAmbientMaterials'
7
+
8
+ const props = withDefaults(
9
+ defineProps<{
10
+ speed?: number
11
+ intensity?: number
12
+ mouseInteraction?: boolean
13
+ mouseStrength?: number
14
+ color1?: string
15
+ color2?: string
16
+ color3?: string
17
+ color4?: string
18
+ }>(),
19
+ {
20
+ speed: 1.0,
21
+ intensity: 1.0,
22
+ mouseInteraction: true,
23
+ mouseStrength: 0.3,
24
+ color1: '#8b5cf6',
25
+ color2: '#6366f1',
26
+ color3: '#a78bfa',
27
+ color4: '#38bdf8',
28
+ }
29
+ )
30
+
31
+ const emit = defineEmits<{
32
+ node: [colorNode: any]
33
+ }>()
34
+
35
+ const uniforms = createAmbientUniforms({
36
+ speed: props.speed,
37
+ intensity: props.intensity,
38
+ mouseInteraction: props.mouseInteraction,
39
+ })
40
+ if (props.mouseInteraction) {
41
+ uniforms.mouseStrength.value = props.mouseStrength
42
+ }
43
+
44
+ const c1 = useShaderColor(props.color1)
45
+ const c2 = useShaderColor(props.color2)
46
+ const c3 = useShaderColor(props.color3)
47
+ const c4 = useShaderColor(props.color4)
48
+
49
+ const colorNode = createThemeWaveColorNode(uniforms, {
50
+ color1: c1.node,
51
+ color2: c2.node,
52
+ color3: c3.node,
53
+ color4: c4.node,
54
+ })
55
+
56
+ watch(() => props.color1, (hex) => c1.tweenTo(hex, 0.8))
57
+ watch(() => props.color2, (hex) => c2.tweenTo(hex, 0.8))
58
+ watch(() => props.color3, (hex) => c3.tweenTo(hex, 0.8))
59
+ watch(() => props.color4, (hex) => c4.tweenTo(hex, 0.8))
60
+
61
+ try {
62
+ const runtime = useShaderRuntimeContext()
63
+ watch(
64
+ () => [runtime.mouse.mouseX.value, runtime.mouse.mouseY.value],
65
+ ([mx, my]) => {
66
+ uniforms.mouseX.value = mx
67
+ uniforms.mouseY.value = my
68
+ },
69
+ { immediate: true }
70
+ )
71
+ } catch {
72
+ // No runtime context
73
+ }
74
+
75
+ watch(() => props.speed, (v) => { uniforms.speed.value = v })
76
+ watch(() => props.intensity, (v) => { uniforms.intensity.value = v })
77
+ watch(() => props.mouseInteraction, (v) => { uniforms.mouseStrength.value = v ? props.mouseStrength : 0 })
78
+ watch(() => props.mouseStrength, (v) => { if (props.mouseInteraction) uniforms.mouseStrength.value = v })
79
+
80
+ emit('node', colorNode)
81
+ defineExpose({ uniforms, colorNode })
82
+ </script>
83
+
84
+ <template>
85
+ <slot />
86
+ </template>
@@ -138,6 +138,15 @@ watch(
138
138
  }
139
139
  )
140
140
 
141
+ watch(
142
+ () => props.clearColor,
143
+ (color) => {
144
+ if (renderer && initialized) {
145
+ renderer.setClearColor(new Color(color))
146
+ }
147
+ }
148
+ )
149
+
141
150
  watch([width, height], ([w, h]) => {
142
151
  if (!initialized) return
143
152
  camera.aspect = w / (h || 1)
@@ -10,6 +10,7 @@ import {
10
10
  mix,
11
11
  mul,
12
12
  pow,
13
+ sign,
13
14
  sin,
14
15
  smoothstep,
15
16
  sub,
@@ -22,6 +23,7 @@ import {
22
23
  import { MeshBasicNodeMaterial } from 'three/webgpu'
23
24
  import {
24
25
  simplexNoise2D,
26
+ simplexNoise3d,
25
27
  fbm2D,
26
28
  fbm3dSimplex,
27
29
  ridgedFbm2d,
@@ -427,6 +429,160 @@ export function createThemeAuroraColorNode(
427
429
  })()
428
430
  }
429
431
 
432
+ export function createThemeWaveColorNode(
433
+ uniforms: AmbientUniforms,
434
+ colors: ThemeColorUniforms,
435
+ ): any {
436
+ const { speed: uSpeed, intensity: uIntensity, mouseX: uMouseX, mouseY: uMouseY, mouseStrength: uMouseStrength } = uniforms
437
+
438
+ return Fn(() => {
439
+ const t = mul(time, uSpeed, 0.1)
440
+ const mouseOff = vec2(
441
+ mul(sub(uMouseX, 0.5), uMouseStrength),
442
+ mul(sub(uMouseY, 0.5), uMouseStrength),
443
+ )
444
+ const uvCoord = add(sub(uv(), 0.5), mouseOff)
445
+
446
+ // Noise-driven rotation
447
+ const degree = simplexNoise2D(vec2(t, uvCoord.x.mul(uvCoord.y))).mul(0.5).add(0.5)
448
+ const angle = degree.sub(0.5).mul(720.0 * Math.PI / 180.0).add(Math.PI)
449
+ const cosA = angle.cos()
450
+ const sinA = angle.sin()
451
+ const rx = uvCoord.x.mul(cosA).sub(uvCoord.y.mul(sinA))
452
+ const ry = uvCoord.x.mul(sinA).add(uvCoord.y.mul(cosA))
453
+
454
+ // Wave warp (order matters: warped x feeds into y)
455
+ const waveSpeed = mul(time, uSpeed, 2.0)
456
+ const wx = rx.add(sin(ry.mul(5.0).add(waveSpeed)).div(30.0))
457
+ const wy = ry.add(sin(wx.mul(7.5).add(waveSpeed)).div(15.0))
458
+
459
+ // -5° rotation for layer blend
460
+ const COS5 = Math.cos(-5 * Math.PI / 180)
461
+ const SIN5 = Math.sin(-5 * Math.PI / 180)
462
+ const rotated5x = wx.mul(COS5).sub(wy.mul(SIN5))
463
+
464
+ const layer1 = mix(colors.color1, colors.color2, smoothstep(-0.3, 0.2, rotated5x))
465
+ const layer2 = mix(colors.color3, colors.color4, smoothstep(-0.3, 0.2, rotated5x))
466
+
467
+ return mix(layer1, layer2, smoothstep(0.5, -0.3, wy)).mul(uIntensity)
468
+ })()
469
+ }
470
+
471
+ export function createThemeLavaLampColorNode(
472
+ uniforms: AmbientUniforms,
473
+ colors: ThemeColorUniforms,
474
+ ): any {
475
+ const { speed: uSpeed, intensity: uIntensity, mouseX: uMouseX, mouseY: uMouseY, mouseStrength: uMouseStrength } = uniforms
476
+
477
+ return Fn(() => {
478
+ const t = mul(time, uSpeed, 0.2)
479
+ const mouseOff = vec2(
480
+ mul(sub(uMouseX, 0.5), uMouseStrength),
481
+ mul(sub(uMouseY, 0.5), uMouseStrength),
482
+ )
483
+ const uvCoord = add(sub(uv(), 0.5), mouseOff)
484
+
485
+ // 4 blobs anchored to quadrants, oscillation wide enough to cross center and intersect
486
+ const b1 = vec2(sin(mul(t, 0.9)).mul(0.22).sub(0.20), sin(mul(t, 0.7)).mul(0.22).sub(0.18))
487
+ const b2 = vec2(sin(mul(t, 0.8).add(2.1)).mul(0.22).add(0.20), sin(mul(t, 1.0).add(1.5)).mul(0.22).sub(0.18))
488
+ const b3 = vec2(sin(mul(t, 0.6).add(4.2)).mul(0.22).sub(0.20), sin(mul(t, 0.9).add(3.1)).mul(0.22).add(0.18))
489
+ const b4 = vec2(sin(mul(t, 1.1).add(1.0)).mul(0.22).add(0.20), sin(mul(t, 0.5).add(2.8)).mul(0.22).add(0.18))
490
+
491
+ // Per-blob breathing k: oscillates between -3.5 (large) and -6.5 (tight)
492
+ const k1 = sin(mul(t, 0.4)).mul(1.5).sub(5.0)
493
+ const k2 = sin(add(mul(t, 0.35), 2.1)).mul(1.5).sub(5.0)
494
+ const k3 = sin(add(mul(t, 0.45), 4.2)).mul(1.5).sub(5.0)
495
+ const k4 = sin(add(mul(t, 0.3), 1.0)).mul(1.5).sub(5.0)
496
+
497
+ const w1 = exp(length(sub(uvCoord, b1)).mul(k1))
498
+ const w2 = exp(length(sub(uvCoord, b2)).mul(k2))
499
+ const w3 = exp(length(sub(uvCoord, b3)).mul(k3))
500
+ const w4 = exp(length(sub(uvCoord, b4)).mul(k4))
501
+ const wTotal = add(w1, w2, w3, w4)
502
+
503
+ // Weighted colour blend
504
+ const colorNode = add(
505
+ mul(colors.color1, w1),
506
+ mul(colors.color2, w2),
507
+ mul(colors.color3, w3),
508
+ mul(colors.color4, w4),
509
+ ).div(wTotal.add(0.001))
510
+
511
+ // Darken space between blobs
512
+ const blobStrength = smoothstep(0.05, 0.9, wTotal)
513
+ const bg = mul(mix(colors.color1, colors.color2, 0.3), 0.15)
514
+ return mix(bg, colorNode, blobStrength).mul(uIntensity)
515
+ })()
516
+ }
517
+
518
+ export function createThemeBubbleColorNode(
519
+ uniforms: AmbientUniforms,
520
+ colors: ThemeColorUniforms,
521
+ ): any {
522
+ const { speed: uSpeed, intensity: uIntensity, mouseX: uMouseX, mouseY: uMouseY, mouseStrength: uMouseStrength } = uniforms
523
+
524
+ return Fn(() => {
525
+ const t = mul(time, uSpeed, 0.06)
526
+ const mouseOff = vec2(
527
+ mul(sub(uMouseX, 0.5), uMouseStrength),
528
+ mul(sub(uMouseY, 0.5), uMouseStrength),
529
+ )
530
+ const uvCoord = add(uv(), mouseOff)
531
+
532
+ const n1 = simplexNoise3d(vec3(uvCoord.x.mul(1.5), uvCoord.y.mul(1.5), t)).mul(0.5).add(0.5)
533
+ const n2 = simplexNoise3d(vec3(
534
+ uvCoord.x.mul(2.5).add(1.3), uvCoord.y.mul(2.5).add(0.7), t.mul(0.8),
535
+ )).mul(0.5).add(0.5)
536
+
537
+ const blend1 = mix(colors.color1, colors.color2, smoothstep(0.3, 0.7, n1))
538
+ const blend2 = mix(colors.color3, colors.color4, smoothstep(0.3, 0.7, n2))
539
+ return mix(blend1, blend2, smoothstep(0.35, 0.65, n2)).mul(uIntensity)
540
+ })()
541
+ }
542
+
543
+ export function createThemePlasmaColorNode(
544
+ uniforms: AmbientUniforms,
545
+ colors: ThemeColorUniforms,
546
+ ): any {
547
+ const { speed: uSpeed, intensity: uIntensity, mouseX: uMouseX, mouseY: uMouseY, mouseStrength: uMouseStrength } = uniforms
548
+
549
+ return Fn(() => {
550
+ const t = mul(time, uSpeed, 0.03)
551
+ const mouseOff = vec2(
552
+ mul(sub(uMouseX, 0.5), uMouseStrength, 0.2),
553
+ mul(sub(uMouseY, 0.5), uMouseStrength, 0.2),
554
+ )
555
+ const uvMut = add(sub(uv(), 0.5).mul(1.2), mouseOff).toVar()
556
+
557
+ const h = simplexNoise3d(vec3(uvMut.x.mul(2.0), uvMut.y.mul(2.0), t)).toVar()
558
+
559
+ // Unrolled distortion loop (n = 1..4)
560
+ for (let n = 1; n < 5; n++) {
561
+ const i = float(n)
562
+ uvMut.subAssign(vec2(
563
+ float(0.7).div(i).mul(sin(i.mul(uvMut.y).add(i).add(t.mul(1.5)).add(h.mul(i)))),
564
+ float(0.4).div(i).mul(sin(uvMut.x.add(4.0).sub(i).add(h).add(t.mul(1.5)).add(i.mul(0.3)))),
565
+ ))
566
+ }
567
+
568
+ // Final UV shift
569
+ uvMut.subAssign(vec2(
570
+ float(1.2).mul(sin(uvMut.x.add(t).add(h))),
571
+ float(0.4).mul(sin(uvMut.y.add(t).add(h.mul(0.3)))),
572
+ ))
573
+
574
+ const cx = sin(uvMut.x.mul(2.0)).mul(0.5).add(0.5)
575
+ const cxy = sin(uvMut.x.add(uvMut.y).mul(1.5)).mul(0.5).add(0.5)
576
+ const cy = sin(uvMut.y.mul(2.0)).mul(0.5).add(0.5)
577
+
578
+ const t12 = mix(colors.color1, colors.color2, cx)
579
+ const t34 = mix(colors.color3, colors.color4, cy)
580
+ const vivid = mix(t12, t34, cxy)
581
+ // Mix against near-black base so black/gray reads through between colour bands
582
+ return mix(vec3(0.03, 0.03, 0.03), vivid, float(0.65)).mul(uIntensity)
583
+ })()
584
+ }
585
+
430
586
  export function createOceanColorNode(uniforms: AmbientUniforms): any {
431
587
  const { speed: uSpeed, intensity: uIntensity, mouseX: uMouseX, mouseY: uMouseY, mouseStrength: uMouseStrength } = uniforms
432
588
 
@@ -24,19 +24,28 @@ export function useThemeColors() {
24
24
  }
25
25
  }
26
26
 
27
+ function applyColors() {
28
+ const isDark = colorMode.value === 'dark'
29
+ const primary = cssVarToHex('--ui-color-primary-500')
30
+ // CSS vars not ready yet (returns fallback) — retry next frame
31
+ if (primary === '#888888') {
32
+ requestAnimationFrame(applyColors)
33
+ return
34
+ }
35
+ primaryHex.value = primary
36
+ secondaryHex.value = isDark ? cssVarToHex('--ui-color-neutral-700') : cssVarToHex('--ui-color-secondary-500')
37
+ primaryLightHex.value = isDark ? cssVarToHex('--ui-color-neutral-900') : cssVarToHex('--ui-color-primary-300')
38
+ infoHex.value = isDark ? cssVarToHex('--ui-color-secondary-500') : cssVarToHex('--ui-color-neutral-300')
39
+ }
40
+
27
41
  function refresh() {
28
- nextTick(() => {
29
- const isDark = colorMode.value === 'dark'
30
- primaryHex.value = cssVarToHex(isDark ? '--ui-color-primary-500' : '--ui-color-primary-600')
31
- secondaryHex.value = cssVarToHex(isDark ? '--ui-color-secondary-500' : '--ui-color-secondary-700')
32
- primaryLightHex.value = cssVarToHex(isDark ? '--ui-color-primary-300' : '--ui-color-primary-400')
33
- infoHex.value = cssVarToHex(isDark ? '--ui-color-info-500' : '--ui-color-info-600')
34
- })
42
+ nextTick(applyColors)
35
43
  }
36
44
 
37
45
  if (import.meta.client) {
38
46
  watch(activeAccent, refresh, { immediate: true })
39
47
  watch(() => colorMode.value, refresh)
48
+ onMounted(applyColors)
40
49
  }
41
50
 
42
51
  return { primaryHex, secondaryHex, infoHex, primaryLightHex, clearColor, refresh }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kmcom-nuxt-layers",
3
3
  "private": false,
4
- "version": "1.4.0",
4
+ "version": "1.5.1",
5
5
  "description": "Composable Nuxt 4 layers for building scalable Vue applications",
6
6
  "files": [
7
7
  "layers/*/nuxt.config.ts",
@@ -159,6 +159,9 @@
159
159
  "dependencies": {
160
160
  "skills": "^1.4.3"
161
161
  },
162
+ "engines": {
163
+ "node": ">=18 <21"
164
+ },
162
165
  "scripts": {
163
166
  "dev": "pnpm -F playground dev",
164
167
  "dev:core": "PLAYGROUND_LAYERS=core pnpm -F playground dev",
@@ -197,6 +200,7 @@
197
200
  "layer:rebuild:ui": "pnpm build:ui && pnpm layer:generate:ui && pnpm layer:ui",
198
201
  "layer:rebuild:layout": "pnpm build:layout && pnpm layer:generate:layout && pnpm layer:layout",
199
202
  "layer:rebuild:motion": "pnpm build:motion && pnpm layer:generate:motion && pnpm layer:motion",
200
- "browserlist": "npx update-browserslist-db@latest"
203
+ "browserlist": "npx update-browserslist-db@latest",
204
+ "deploy": "turbo run build --filter playground"
201
205
  }
202
206
  }