kmcom-nuxt-layers 1.3.1 → 1.5.0

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 (102) hide show
  1. package/layers/content/app/components/Blog/List.vue +5 -1
  2. package/layers/content/app/components/Gallery/AmbientImage.vue +5 -12
  3. package/layers/content/app/components/Gallery/Detail.vue +8 -6
  4. package/layers/content/app/components/Gallery/Grid.vue +11 -3
  5. package/layers/content/app/components/Portfolio/ColorPalette.vue +1 -4
  6. package/layers/content/app/components/Portfolio/Detail.vue +6 -1
  7. package/layers/content/app/components/Portfolio/List.vue +5 -1
  8. package/layers/content/app/components/content/Figure.vue +1 -7
  9. package/layers/content/nuxt.config.ts +16 -5
  10. package/layers/content/package.json +5 -5
  11. package/layers/core/app/assets/css/main.css +5 -0
  12. package/layers/core/app/composables/useCache.ts +8 -4
  13. package/layers/core/app/composables/useErrorLog.ts +9 -5
  14. package/layers/core/app/composables/useScrollGuard.ts +4 -2
  15. package/layers/core/app/plugins/feature-detection.client.ts +1 -1
  16. package/layers/core/app/plugins/init.ts +2 -1
  17. package/layers/core/app/plugins/scroll-guard.client.ts +4 -1
  18. package/layers/core/app.config.ts +0 -9
  19. package/layers/forms/app/components/Form/Contact.vue +16 -7
  20. package/layers/forms/nuxt.config.ts +18 -0
  21. package/layers/forms/package.json +2 -0
  22. package/layers/layout/app/components/Layout/Container.vue +1 -4
  23. package/layers/layout/app/components/Layout/Grid/Debug.vue +0 -1
  24. package/layers/layout/app/components/Layout/Grid/Item.vue +12 -6
  25. package/layers/layout/app/components/Layout/Main.vue +1 -4
  26. package/layers/layout/app/components/Layout/Page/Container.vue +3 -1
  27. package/layers/layout/app/components/Layout/Page/Header.vue +16 -7
  28. package/layers/layout/app/components/Layout/Section/Grid.vue +1 -4
  29. package/layers/layout/app/components/Layout/Section/Sidebar.vue +6 -1
  30. package/layers/layout/app/components/Layout/Section/Stack.vue +1 -1
  31. package/layers/layout/app/composables/useGridConfig.ts +6 -1
  32. package/layers/motion/app/components/Motion/HorizontalScroll.vue +61 -0
  33. package/layers/motion/app/components/Motion/PinnedSection.vue +77 -0
  34. package/layers/motion/app/components/Motion/ScrollProgress.vue +8 -56
  35. package/layers/motion/app/components/Motion/ScrollScene.vue +121 -0
  36. package/layers/motion/app/components/Motion/ScrollStep.vue +45 -0
  37. package/layers/motion/app/components/Motion/TextReveal.vue +28 -63
  38. package/layers/motion/app/composables/useScrollSteps.ts +41 -0
  39. package/layers/motion/app/composables/useSectionProgress.ts +58 -0
  40. package/layers/motion/app/composables/useSmoothScroll.ts +3 -2
  41. package/layers/motion/app/plugins/locomotive-scroll.client.ts +6 -6
  42. package/layers/motion/nuxt.config.ts +6 -0
  43. package/layers/motion/package.json +2 -1
  44. package/layers/routing/app/app.config.ts +20 -0
  45. package/layers/routing/app/composables/useFeatures.ts +12 -0
  46. package/layers/routing/app/composables/useMaintenance.ts +7 -0
  47. package/layers/routing/app/composables/useRoutingConfig.ts +20 -0
  48. package/layers/routing/app/middleware/01.maintenance.global.ts +6 -0
  49. package/layers/routing/app/middleware/02.governance.global.ts +25 -0
  50. package/layers/routing/app/plugins/feature-flags.client.ts +15 -0
  51. package/layers/routing/app/plugins/scroll-routing.client.ts +21 -0
  52. package/layers/routing/app/types/route-meta.d.ts +6 -0
  53. package/layers/routing/app/types/routing.ts +48 -0
  54. package/layers/routing/nuxt.config.ts +27 -0
  55. package/layers/routing/package.json +6 -0
  56. package/layers/shader/app/components/Preset/ThemeAurora.client.vue +86 -0
  57. package/layers/shader/app/components/Preset/ThemeBubble.client.vue +87 -0
  58. package/layers/shader/app/components/Preset/ThemeFlow.client.vue +86 -0
  59. package/layers/shader/app/components/Preset/ThemeGradient.client.vue +87 -0
  60. package/layers/shader/app/components/Preset/ThemeLavaLamp.client.vue +86 -0
  61. package/layers/shader/app/components/Preset/ThemePlasma.client.vue +86 -0
  62. package/layers/shader/app/components/Preset/ThemeWave.client.vue +86 -0
  63. package/layers/shader/app/components/Shader/Background.client.vue +15 -0
  64. package/layers/shader/app/composables/useAmbientMaterials.ts +306 -0
  65. package/layers/shader/app/composables/useThemeColors.ts +52 -0
  66. package/layers/shader/app/utils/tsl/oklch.ts +12 -6
  67. package/layers/theme/app/assets/css/theme.css +19 -14
  68. package/layers/theme/app/components/ThemePicker/AccentButton.vue +2 -2
  69. package/layers/theme/app/components/ThemePicker/Colors.vue +2 -4
  70. package/layers/theme/app/components/ThemePicker/Menu.vue +4 -13
  71. package/layers/theme/app/components/ThemePicker/MenuButton.vue +1 -7
  72. package/layers/theme/app/composables/useAccentColor.ts +38 -0
  73. package/layers/theme/app/composables/useTheme.ts +14 -0
  74. package/layers/theme/app/composables/useThemeContrast.ts +34 -0
  75. package/layers/theme/app/composables/useThemeMotion.ts +34 -0
  76. package/layers/theme/app/composables/useThemePreferences.ts +3 -156
  77. package/layers/theme/app/composables/useThemeTransparency.ts +41 -0
  78. package/layers/theme/app/plugins/theme.client.ts +3 -3
  79. package/layers/theme/app/types/theme.ts +4 -0
  80. package/layers/theme/nuxt.config.ts +7 -0
  81. package/layers/ui/app/app.config.ts +44 -0
  82. package/layers/ui/app/assets/css/main.css +14 -0
  83. package/layers/ui/app/components/Accent/Blob.vue +29 -0
  84. package/layers/ui/app/components/Accent/Scene.vue +38 -0
  85. package/layers/ui/app/components/Gradient/Background.vue +22 -0
  86. package/layers/ui/app/components/Gradient/Text.vue +22 -0
  87. package/layers/ui/app/components/Progress/Bar.vue +25 -0
  88. package/layers/ui/app/components/Progress/Circular.vue +69 -0
  89. package/layers/ui/app/components/Tint/Overlay.vue +25 -0
  90. package/layers/ui/app/components/Typography/CodeBlock.vue +2 -1
  91. package/layers/ui/app/components/Typography/Headline.vue +2 -1
  92. package/layers/ui/app/components/Typography/QuoteBlock.vue +2 -1
  93. package/layers/ui/app/components/Typography/TextStroke.vue +18 -16
  94. package/layers/ui/app/composables/accent.ts +51 -0
  95. package/layers/ui/app/composables/gradient.ts +79 -0
  96. package/layers/ui/app/composables/tint.ts +20 -0
  97. package/layers/ui/app/types/accent.ts +17 -0
  98. package/layers/ui/app/types/gradient.ts +27 -0
  99. package/layers/ui/app/types/tint.ts +25 -0
  100. package/package.json +37 -31
  101. package/layers/motion/app/utils/gsapAnimations.ts +0 -122
  102. package/layers/ui/app.config.ts +0 -12
@@ -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>
@@ -118,6 +118,12 @@ async function init() {
118
118
  planeMesh = new Mesh(geometry, props.material ?? new MeshBasicMaterial({ color: 0x000000 }))
119
119
  scene.add(planeMesh)
120
120
 
121
+ // Pre-compile the shader pipeline asynchronously before starting the render loop.
122
+ // Without this, the first render() call uses device.createRenderPipeline() which
123
+ // blocks the main thread for 1-5s on first visit (cold WebGPU pipeline cache).
124
+ // compileAsync() uses device.createRenderPipelineAsync() instead, which is non-blocking.
125
+ await renderer.compileAsync(scene, camera)
126
+
121
127
  initialized = true
122
128
  emit('ready', renderer)
123
129
  animate()
@@ -132,6 +138,15 @@ watch(
132
138
  }
133
139
  )
134
140
 
141
+ watch(
142
+ () => props.clearColor,
143
+ (color) => {
144
+ if (renderer && initialized) {
145
+ renderer.setClearColor(new Color(color))
146
+ }
147
+ }
148
+ )
149
+
135
150
  watch([width, height], ([w, h]) => {
136
151
  if (!initialized) return
137
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,
@@ -277,6 +279,310 @@ export function createGradientMeshColorNode(uniforms: AmbientUniforms): any {
277
279
  })()
278
280
  }
279
281
 
282
+ export interface ThemeColorUniforms {
283
+ color1: any // TSL uniform node wrapping a THREE.Color
284
+ color2: any
285
+ color3: any
286
+ color4: any
287
+ }
288
+
289
+ export function createThemeGradientColorNode(
290
+ uniforms: AmbientUniforms,
291
+ colors: ThemeColorUniforms,
292
+ ): any {
293
+ const { speed: uSpeed, intensity: uIntensity,
294
+ mouseX: uMouseX, mouseY: uMouseY, mouseStrength: uMouseStrength } = uniforms
295
+
296
+ return Fn(() => {
297
+ const t = mul(time, uSpeed, 0.2)
298
+ const uvCoord = uv()
299
+
300
+ // Gentle UV shift for p2-p4 area
301
+ const mouseOffset = vec2(
302
+ mul(sub(uMouseX, 0.5), uMouseStrength, 0.05),
303
+ mul(sub(uMouseY, 0.5), uMouseStrength, 0.05),
304
+ )
305
+ const adjustedUV = add(uvCoord, mouseOffset)
306
+
307
+ // p1 attracted toward mouse cursor
308
+ const p1Base = vec2(add(0.2, mul(sin(mul(t, 0.5)), 0.15)), add(0.3, mul(sin(add(mul(t, 0.4), 1.57)), 0.15)))
309
+ const mousePos = vec2(uMouseX, uMouseY)
310
+ const p1 = mix(p1Base, mousePos, mul(uMouseStrength, 0.5))
311
+
312
+ const p2 = vec2(add(0.8, mul(sin(add(mul(t, 0.6), 1.57)), 0.1)), add(0.2, mul(sin(mul(t, 0.5)), 0.1)))
313
+ const p3 = vec2(add(0.3, mul(sin(mul(t, 0.7)), 0.15)), add(0.8, mul(sin(add(mul(t, 0.3), 1.57)), 0.1)))
314
+ const p4 = vec2(add(0.7, mul(sin(add(mul(t, 0.4), 1.57)), 0.15)), add(0.7, mul(sin(mul(t, 0.6)), 0.15)))
315
+
316
+ const nm1 = simplexNoise2D(add(mul(adjustedUV, 3.0), mul(t, 0.3))).mul(0.5).add(0.5)
317
+ const nm2 = simplexNoise2D(add(mul(adjustedUV, 4.5), mul(t, -0.2), 8.0)).mul(0.5).add(0.5)
318
+ const nm3 = fbm2D(add(adjustedUV, mul(t, 0.1)), { octaves: 3, frequency: 2.0 }).mul(0.5).add(0.5)
319
+ const nm4 = simplexNoise2D(add(mul(adjustedUV, 7.0), mul(t, 0.5))).mul(0.5).add(0.5)
320
+
321
+ const d1 = mul(sub(1.0, smoothstep(0.0, 0.7, length(sub(adjustedUV, p1)))), add(0.7, mul(nm1, 0.3)))
322
+ const d2 = mul(sub(1.0, smoothstep(0.0, 0.7, length(sub(adjustedUV, p2)))), add(0.7, mul(nm2, 0.3)))
323
+ const d3 = mul(sub(1.0, smoothstep(0.0, 0.7, length(sub(adjustedUV, p3)))), add(0.7, mul(nm3, 0.3)))
324
+ const d4 = mul(sub(1.0, smoothstep(0.0, 0.7, length(sub(adjustedUV, p4)))), add(0.7, mul(nm4, 0.3)))
325
+
326
+ let colorNode = mul(colors.color1, d1)
327
+ colorNode = add(colorNode, mul(colors.color2, d2))
328
+ colorNode = add(colorNode, mul(colors.color3, d3))
329
+ colorNode = add(colorNode, mul(colors.color4, d4))
330
+
331
+ // Normalise first, then vignette, then intensity
332
+ const totalWeight = add(d1, d2, d3, d4, 0.01)
333
+ colorNode = colorNode.div(totalWeight)
334
+
335
+ const dist = length(sub(uvCoord, 0.5))
336
+ colorNode = mul(colorNode, sub(1.0, mul(dist, 0.25)))
337
+ colorNode = mul(colorNode, uIntensity)
338
+
339
+ return colorNode
340
+ })()
341
+ }
342
+
343
+ export function createThemeFlowColorNode(
344
+ uniforms: AmbientUniforms,
345
+ colors: ThemeColorUniforms,
346
+ ): any {
347
+ const { speed: uSpeed, intensity: uIntensity, mouseX: uMouseX, mouseY: uMouseY, mouseStrength: uMouseStrength } = uniforms
348
+
349
+ return Fn(() => {
350
+ const t = mul(time, uSpeed, 0.15)
351
+ const uvCoord = uv()
352
+
353
+ const mouseOffset = vec2(
354
+ mul(sub(uMouseX, 0.5), uMouseStrength, 0.3),
355
+ mul(sub(uMouseY, 0.5), uMouseStrength, 0.3),
356
+ )
357
+
358
+ const warpCoarse1 = simplexNoise2D(add(mul(uvCoord, 1.5), t))
359
+ const warpCoarse2 = simplexNoise2D(add(mul(uvCoord, 1.5), mul(t, -0.5), 7.0))
360
+ const warpedUV1 = add(uvCoord, mul(vec2(warpCoarse1, warpCoarse2), 0.25), mouseOffset)
361
+
362
+ const warpMed1 = simplexNoise2D(add(mul(warpedUV1, 3.0), mul(t, 0.7)))
363
+ const warpMed2 = simplexNoise2D(add(mul(warpedUV1, 3.0), mul(t, -0.3), 15.0))
364
+ const warpedUV2 = add(warpedUV1, mul(vec2(warpMed1, warpMed2), 0.12))
365
+
366
+ const warpFine1 = simplexNoise2D(add(mul(warpedUV2, 5.0), mul(t, 1.2)))
367
+ const warpFine2 = simplexNoise2D(add(mul(warpedUV2, 5.0), mul(t, -0.8), 25.0))
368
+ const warpedUV = add(warpedUV2, mul(vec2(warpFine1, warpFine2), 0.05))
369
+
370
+ const n1 = fbm2D(warpedUV, { octaves: 5, frequency: 2.0 }).mul(0.5).add(0.5)
371
+ const n2 = ridgedFbm2d(warpedUV, { octaves: 4, frequency: 1.5 })
372
+
373
+ let colorNode = mix(colors.color1, colors.color2, n1)
374
+ colorNode = mix(colorNode, colors.color3, mul(n2, 0.5))
375
+
376
+ const iridescence = mix(colors.color3, colors.color4, add(mul(n1, 0.6), mul(n2, 0.4)))
377
+ colorNode = mix(colorNode, iridescence, 0.3)
378
+ colorNode = mix(colorNode, colors.color4, mul(smoothstep(0.6, 0.9, n2), 0.25))
379
+
380
+ const brightness = add(0.85, mul(0.15, sin(add(mul(n1, 6.28), t))))
381
+ colorNode = mul(colorNode, brightness, uIntensity)
382
+
383
+ const dist = length(sub(uvCoord, 0.5))
384
+ colorNode = mul(colorNode, sub(1.0, mul(dist, 0.3)))
385
+
386
+ return colorNode
387
+ })()
388
+ }
389
+
390
+ export function createThemeAuroraColorNode(
391
+ uniforms: AmbientUniforms,
392
+ colors: ThemeColorUniforms,
393
+ ): any {
394
+ const { speed: uSpeed, intensity: uIntensity, mouseX: uMouseX, mouseY: uMouseY, mouseStrength: uMouseStrength } = uniforms
395
+
396
+ return Fn(() => {
397
+ const t = mul(time, uSpeed, 0.2)
398
+ const uvCoord = uv()
399
+
400
+ const mouseOffset = vec2(
401
+ mul(sub(uMouseX, 0.5), uMouseStrength),
402
+ mul(sub(uMouseY, 0.5), uMouseStrength),
403
+ )
404
+
405
+ const curtainCoord = add(
406
+ vec2(mul(uvCoord.x, 3.0), mul(uvCoord.y, 0.5)),
407
+ vec2(t, mul(t, 0.3)),
408
+ mouseOffset,
409
+ )
410
+
411
+ const curtain1 = simplexNoise2D(curtainCoord).mul(0.5).add(0.5)
412
+ const curtain2 = simplexNoise2D(add(mul(curtainCoord, 1.5), vec2(mul(t, -0.2), 5.0))).mul(0.5).add(0.5)
413
+ const curtain3 = simplexNoise2D(add(mul(curtainCoord, 0.7), vec2(mul(t, 0.4), 12.0))).mul(0.5).add(0.5)
414
+ const detail = fbm2D(curtainCoord, { octaves: 4, frequency: 2.0 }).mul(0.5).add(0.5)
415
+ const curtain = mul(add(mul(curtain1, 0.4), mul(curtain2, 0.25), mul(curtain3, 0.2), mul(detail, 0.15)), 1.0)
416
+
417
+ const fade = mul(pow(sub(1.0, uvCoord.y), 1.2), smoothstep(0.0, 0.3, uvCoord.y))
418
+ const aurora = mul(smoothstep(0.2, 0.8, mul(curtain, fade)), uIntensity)
419
+
420
+ const shimmer = simplexNoise2D(add(mul(curtainCoord, 8.0), mul(t, 3.0))).mul(0.5).add(0.5)
421
+
422
+ const colorDriver = add(mul(curtain2, 0.6), mul(curtain3, 0.4))
423
+ const auroraColor = mix(colors.color1, colors.color2, colorDriver)
424
+
425
+ const skyColor = mul(colors.color1, 0.04)
426
+ const shimmerColor = mul(colors.color3, mul(shimmer, mul(aurora, 0.15)))
427
+
428
+ return add(mix(skyColor, auroraColor, aurora), shimmerColor)
429
+ })()
430
+ }
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
+
280
586
  export function createOceanColorNode(uniforms: AmbientUniforms): any {
281
587
  const { speed: uSpeed, intensity: uIntensity, mouseX: uMouseX, mouseY: uMouseY, mouseStrength: uMouseStrength } = uniforms
282
588
 
@@ -0,0 +1,52 @@
1
+ import { parseOKLCH, oklchToColor } from '../utils/tsl/oklch'
2
+
3
+ export function useThemeColors() {
4
+ const { activeAccent } = useAccentColor()
5
+ const colorMode = useColorMode()
6
+
7
+ const primaryHex = ref('#8b5cf6')
8
+ const secondaryHex = ref('#6366f1')
9
+ const infoHex = ref('#38bdf8')
10
+ const primaryLightHex = ref('#a78bfa')
11
+
12
+ const clearColor = computed(() =>
13
+ colorMode.value === 'dark' ? '#0a0a0a' : '#f8f8f8'
14
+ )
15
+
16
+ function cssVarToHex(varName: string): string {
17
+ const raw = getComputedStyle(document.documentElement)
18
+ .getPropertyValue(varName).trim()
19
+ try {
20
+ const [l, c, h] = parseOKLCH(raw)
21
+ return `#${oklchToColor(l, c, h).getHexString()}`
22
+ } catch {
23
+ return '#888888'
24
+ }
25
+ }
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
+
41
+ function refresh() {
42
+ nextTick(applyColors)
43
+ }
44
+
45
+ if (import.meta.client) {
46
+ watch(activeAccent, refresh, { immediate: true })
47
+ watch(() => colorMode.value, refresh)
48
+ onMounted(applyColors)
49
+ }
50
+
51
+ return { primaryHex, secondaryHex, infoHex, primaryLightHex, clearColor, refresh }
52
+ }
@@ -23,15 +23,21 @@ export function oklchToLinearSRGB(l: TSLNode, c: TSLNode, h: TSLNode): TSLNode {
23
23
  // OKLab -> linear sRGB (approximate matrix transform)
24
24
  const l_ = l.add(mul(a, float(0.3963377774))).add(mul(b, float(0.2158037573)))
25
25
  const m_ = l.sub(mul(a, float(0.1055613458))).sub(mul(b, float(0.0638541728)))
26
- const s_ = l.sub(mul(a, float(0.0894841775))).sub(mul(b, float(1.2914855480)))
26
+ const s_ = l.sub(mul(a, float(0.0894841775))).sub(mul(b, float(1.291485548)))
27
27
 
28
28
  const lCubed = pow(l_, 3)
29
29
  const mCubed = pow(m_, 3)
30
30
  const sCubed = pow(s_, 3)
31
31
 
32
- const r = mul(lCubed, float(4.0767416621)).sub(mul(mCubed, float(3.3077115913))).add(mul(sCubed, float(0.2309699292)))
33
- const g = mul(lCubed, float(-1.2684380046)).add(mul(mCubed, float(2.6097574011))).sub(mul(sCubed, float(0.3413193965)))
34
- const bOut = mul(lCubed, float(-0.0041960863)).sub(mul(mCubed, float(0.7034186147))).add(mul(sCubed, float(1.7076147010)))
32
+ const r = mul(lCubed, float(4.0767416621))
33
+ .sub(mul(mCubed, float(3.3077115913)))
34
+ .add(mul(sCubed, float(0.2309699292)))
35
+ const g = mul(lCubed, float(-1.2684380046))
36
+ .add(mul(mCubed, float(2.6097574011)))
37
+ .sub(mul(sCubed, float(0.3413193965)))
38
+ const bOut = mul(lCubed, float(-0.0041960863))
39
+ .sub(mul(mCubed, float(0.7034186147)))
40
+ .add(mul(sCubed, float(1.707614701)))
35
41
 
36
42
  return vec3(r, g, bOut)
37
43
  }
@@ -70,7 +76,7 @@ export function oklchToColor(l: number, c: number, h: number): Color {
70
76
  // OKLab to linear sRGB (approximate)
71
77
  const l_ = l + a * 0.3963377774 + b * 0.2158037573
72
78
  const m_ = l - a * 0.1055613458 - b * 0.0638541728
73
- const s_ = l - a * 0.0894841775 - b * 1.2914855480
79
+ const s_ = l - a * 0.0894841775 - b * 1.291485548
74
80
 
75
81
  const lCubed = l_ * l_ * l_
76
82
  const mCubed = m_ * m_ * m_
@@ -78,7 +84,7 @@ export function oklchToColor(l: number, c: number, h: number): Color {
78
84
 
79
85
  const r = lCubed * 4.0767416621 - mCubed * 3.3077115913 + sCubed * 0.2309699292
80
86
  const g = lCubed * -1.2684380046 + mCubed * 2.6097574011 - sCubed * 0.3413193965
81
- const bOut = lCubed * -0.0041960863 - mCubed * 0.7034186147 + sCubed * 1.7076147010
87
+ const bOut = lCubed * -0.0041960863 - mCubed * 0.7034186147 + sCubed * 1.707614701
82
88
 
83
89
  // Clamp to [0,1]
84
90
  const color = new Color()
@@ -1,47 +1,52 @@
1
1
  /*
2
2
  * Theme Layer CSS
3
3
  *
4
- * Accent colors are handled via appConfig.ui.colors.primary at runtime.
5
- * The [data-theme] attribute provides a CSS hook for custom styling.
6
- * Preference classes are applied to <html> by useThemePreferences.
4
+ * Accessibility attributes are applied to <html> by the individual composables.
5
+ * The dark mode @custom-variant lives in core/main.css so it is defined in the
6
+ * same Tailwind pass as all utility generation (avoids HMR regeneration shift).
7
7
  */
8
8
 
9
9
  /* --- Reduced Motion --- */
10
- .reduce-motion *,
11
- .reduce-motion *::before,
12
- .reduce-motion *::after {
10
+ [data-theme-motion="reduced"] *,
11
+ [data-theme-motion="reduced"] *::before,
12
+ [data-theme-motion="reduced"] *::after {
13
13
  animation-duration: 0.01ms !important;
14
14
  animation-iteration-count: 1 !important;
15
15
  transition-duration: 0.01ms !important;
16
16
  scroll-behavior: auto !important;
17
17
  }
18
18
 
19
+ [data-theme-motion="reduced"] {
20
+ --duration-base: 0ms;
21
+ }
22
+
19
23
  /* --- Reduced Transparency --- */
20
- /* Replace semi-transparent backgrounds with solid equivalents */
21
- .reduce-transparency {
24
+ [data-theme-transparency="reduced"] {
22
25
  --ui-bg-elevated: var(--ui-bg);
26
+ --opacity-glass: 1;
27
+ --backdrop-blur: 0px;
23
28
  }
24
29
 
25
- .reduce-transparency :where([class*='backdrop-blur']) {
30
+ [data-theme-transparency="reduced"] :where([class*='backdrop-blur']) {
26
31
  backdrop-filter: none !important;
27
32
  -webkit-backdrop-filter: none !important;
28
33
  }
29
34
 
30
- .reduce-transparency :where([class*='bg-default/'], [class*='bg-elevated/']) {
35
+ [data-theme-transparency="reduced"] :where([class*='bg-default/'], [class*='bg-elevated/']) {
31
36
  background-color: var(--ui-bg-elevated) !important;
32
37
  }
33
38
 
34
39
  /* --- High Contrast --- */
35
- .high-contrast {
40
+ [data-theme-contrast="high"] {
36
41
  --ui-border: var(--ui-border-accented);
37
42
  }
38
43
 
39
- .high-contrast :where(button, a, [role='button']) {
44
+ [data-theme-contrast="high"] :where(button, a, [role='button']) {
40
45
  outline-width: 2px;
41
46
  }
42
47
 
43
- .high-contrast .text-muted,
44
- .high-contrast .text-dimmed {
48
+ [data-theme-contrast="high"] .text-muted,
49
+ [data-theme-contrast="high"] .text-dimmed {
45
50
  opacity: 1;
46
51
  filter: contrast(1.25);
47
52
  }