kmcom-nuxt-layers 1.3.1 → 1.4.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 (85) 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/package.json +5 -5
  10. package/layers/core/app/assets/css/main.css +5 -0
  11. package/layers/core/app/composables/useCache.ts +8 -4
  12. package/layers/core/app/composables/useErrorLog.ts +9 -5
  13. package/layers/core/app/composables/useScrollGuard.ts +4 -2
  14. package/layers/core/app/plugins/feature-detection.client.ts +1 -1
  15. package/layers/core/app/plugins/init.ts +2 -1
  16. package/layers/core/app/plugins/scroll-guard.client.ts +4 -1
  17. package/layers/core/app.config.ts +0 -9
  18. package/layers/forms/app/components/Form/Contact.vue +16 -7
  19. package/layers/forms/nuxt.config.ts +18 -0
  20. package/layers/forms/package.json +2 -0
  21. package/layers/layout/app/components/Layout/Container.vue +1 -4
  22. package/layers/layout/app/components/Layout/Grid/Debug.vue +0 -1
  23. package/layers/layout/app/components/Layout/Grid/Item.vue +12 -6
  24. package/layers/layout/app/components/Layout/Main.vue +1 -4
  25. package/layers/layout/app/components/Layout/Page/Container.vue +3 -1
  26. package/layers/layout/app/components/Layout/Page/Header.vue +16 -7
  27. package/layers/layout/app/components/Layout/Section/Grid.vue +1 -4
  28. package/layers/layout/app/components/Layout/Section/Sidebar.vue +6 -1
  29. package/layers/layout/app/components/Layout/Section/Stack.vue +1 -1
  30. package/layers/layout/app/composables/useGridConfig.ts +6 -1
  31. package/layers/motion/app/components/Motion/HorizontalScroll.vue +61 -0
  32. package/layers/motion/app/components/Motion/PinnedSection.vue +77 -0
  33. package/layers/motion/app/components/Motion/ScrollProgress.vue +8 -56
  34. package/layers/motion/app/components/Motion/ScrollScene.vue +121 -0
  35. package/layers/motion/app/components/Motion/ScrollStep.vue +45 -0
  36. package/layers/motion/app/components/Motion/TextReveal.vue +28 -63
  37. package/layers/motion/app/composables/useScrollSteps.ts +41 -0
  38. package/layers/motion/app/composables/useSectionProgress.ts +58 -0
  39. package/layers/motion/app/composables/useSmoothScroll.ts +3 -2
  40. package/layers/motion/app/plugins/locomotive-scroll.client.ts +6 -6
  41. package/layers/motion/nuxt.config.ts +6 -0
  42. package/layers/motion/package.json +2 -1
  43. package/layers/shader/app/components/Preset/ThemeAurora.client.vue +86 -0
  44. package/layers/shader/app/components/Preset/ThemeFlow.client.vue +86 -0
  45. package/layers/shader/app/components/Preset/ThemeGradient.client.vue +87 -0
  46. package/layers/shader/app/components/Shader/Background.client.vue +6 -0
  47. package/layers/shader/app/composables/useAmbientMaterials.ts +150 -0
  48. package/layers/shader/app/composables/useThemeColors.ts +43 -0
  49. package/layers/shader/app/utils/tsl/oklch.ts +12 -6
  50. package/layers/theme/app/assets/css/theme.css +19 -14
  51. package/layers/theme/app/components/ThemePicker/AccentButton.vue +2 -2
  52. package/layers/theme/app/components/ThemePicker/Colors.vue +2 -4
  53. package/layers/theme/app/components/ThemePicker/Menu.vue +4 -13
  54. package/layers/theme/app/components/ThemePicker/MenuButton.vue +1 -7
  55. package/layers/theme/app/composables/useAccentColor.ts +38 -0
  56. package/layers/theme/app/composables/useTheme.ts +14 -0
  57. package/layers/theme/app/composables/useThemeContrast.ts +34 -0
  58. package/layers/theme/app/composables/useThemeMotion.ts +34 -0
  59. package/layers/theme/app/composables/useThemePreferences.ts +3 -156
  60. package/layers/theme/app/composables/useThemeTransparency.ts +41 -0
  61. package/layers/theme/app/plugins/theme.client.ts +3 -3
  62. package/layers/theme/app/types/theme.ts +4 -0
  63. package/layers/theme/nuxt.config.ts +7 -0
  64. package/layers/ui/app/app.config.ts +44 -0
  65. package/layers/ui/app/assets/css/main.css +14 -0
  66. package/layers/ui/app/components/Accent/Blob.vue +29 -0
  67. package/layers/ui/app/components/Accent/Scene.vue +38 -0
  68. package/layers/ui/app/components/Gradient/Background.vue +22 -0
  69. package/layers/ui/app/components/Gradient/Text.vue +22 -0
  70. package/layers/ui/app/components/Progress/Bar.vue +25 -0
  71. package/layers/ui/app/components/Progress/Circular.vue +69 -0
  72. package/layers/ui/app/components/Tint/Overlay.vue +25 -0
  73. package/layers/ui/app/components/Typography/CodeBlock.vue +2 -1
  74. package/layers/ui/app/components/Typography/Headline.vue +2 -1
  75. package/layers/ui/app/components/Typography/QuoteBlock.vue +2 -1
  76. package/layers/ui/app/components/Typography/TextStroke.vue +18 -16
  77. package/layers/ui/app/composables/accent.ts +51 -0
  78. package/layers/ui/app/composables/gradient.ts +79 -0
  79. package/layers/ui/app/composables/tint.ts +20 -0
  80. package/layers/ui/app/types/accent.ts +17 -0
  81. package/layers/ui/app/types/gradient.ts +27 -0
  82. package/layers/ui/app/types/tint.ts +25 -0
  83. package/package.json +32 -30
  84. package/layers/motion/app/utils/gsapAnimations.ts +0 -122
  85. 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
+ createThemeAuroraColorNode,
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 = createThemeAuroraColorNode(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
+ createThemeFlowColorNode,
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 = createThemeFlowColorNode(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,87 @@
1
+ <script setup lang="ts">
2
+ // @ts-nocheck - TSL types
3
+ import {
4
+ createAmbientUniforms,
5
+ createThemeGradientColorNode,
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
+ // createAmbientUniforms hardcodes 0.5 — override with our prop value
41
+ if (props.mouseInteraction) {
42
+ uniforms.mouseStrength.value = props.mouseStrength
43
+ }
44
+
45
+ const c1 = useShaderColor(props.color1)
46
+ const c2 = useShaderColor(props.color2)
47
+ const c3 = useShaderColor(props.color3)
48
+ const c4 = useShaderColor(props.color4)
49
+
50
+ const colorNode = createThemeGradientColorNode(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>
@@ -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()
@@ -277,6 +277,156 @@ export function createGradientMeshColorNode(uniforms: AmbientUniforms): any {
277
277
  })()
278
278
  }
279
279
 
280
+ export interface ThemeColorUniforms {
281
+ color1: any // TSL uniform node wrapping a THREE.Color
282
+ color2: any
283
+ color3: any
284
+ color4: any
285
+ }
286
+
287
+ export function createThemeGradientColorNode(
288
+ uniforms: AmbientUniforms,
289
+ colors: ThemeColorUniforms,
290
+ ): any {
291
+ const { speed: uSpeed, intensity: uIntensity,
292
+ mouseX: uMouseX, mouseY: uMouseY, mouseStrength: uMouseStrength } = uniforms
293
+
294
+ return Fn(() => {
295
+ const t = mul(time, uSpeed, 0.2)
296
+ const uvCoord = uv()
297
+
298
+ // Gentle UV shift for p2-p4 area
299
+ const mouseOffset = vec2(
300
+ mul(sub(uMouseX, 0.5), uMouseStrength, 0.05),
301
+ mul(sub(uMouseY, 0.5), uMouseStrength, 0.05),
302
+ )
303
+ const adjustedUV = add(uvCoord, mouseOffset)
304
+
305
+ // p1 attracted toward mouse cursor
306
+ 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)))
307
+ const mousePos = vec2(uMouseX, uMouseY)
308
+ const p1 = mix(p1Base, mousePos, mul(uMouseStrength, 0.5))
309
+
310
+ 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)))
311
+ 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)))
312
+ 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)))
313
+
314
+ const nm1 = simplexNoise2D(add(mul(adjustedUV, 3.0), mul(t, 0.3))).mul(0.5).add(0.5)
315
+ const nm2 = simplexNoise2D(add(mul(adjustedUV, 4.5), mul(t, -0.2), 8.0)).mul(0.5).add(0.5)
316
+ const nm3 = fbm2D(add(adjustedUV, mul(t, 0.1)), { octaves: 3, frequency: 2.0 }).mul(0.5).add(0.5)
317
+ const nm4 = simplexNoise2D(add(mul(adjustedUV, 7.0), mul(t, 0.5))).mul(0.5).add(0.5)
318
+
319
+ const d1 = mul(sub(1.0, smoothstep(0.0, 0.7, length(sub(adjustedUV, p1)))), add(0.7, mul(nm1, 0.3)))
320
+ const d2 = mul(sub(1.0, smoothstep(0.0, 0.7, length(sub(adjustedUV, p2)))), add(0.7, mul(nm2, 0.3)))
321
+ const d3 = mul(sub(1.0, smoothstep(0.0, 0.7, length(sub(adjustedUV, p3)))), add(0.7, mul(nm3, 0.3)))
322
+ const d4 = mul(sub(1.0, smoothstep(0.0, 0.7, length(sub(adjustedUV, p4)))), add(0.7, mul(nm4, 0.3)))
323
+
324
+ let colorNode = mul(colors.color1, d1)
325
+ colorNode = add(colorNode, mul(colors.color2, d2))
326
+ colorNode = add(colorNode, mul(colors.color3, d3))
327
+ colorNode = add(colorNode, mul(colors.color4, d4))
328
+
329
+ // Normalise first, then vignette, then intensity
330
+ const totalWeight = add(d1, d2, d3, d4, 0.01)
331
+ colorNode = colorNode.div(totalWeight)
332
+
333
+ const dist = length(sub(uvCoord, 0.5))
334
+ colorNode = mul(colorNode, sub(1.0, mul(dist, 0.25)))
335
+ colorNode = mul(colorNode, uIntensity)
336
+
337
+ return colorNode
338
+ })()
339
+ }
340
+
341
+ export function createThemeFlowColorNode(
342
+ uniforms: AmbientUniforms,
343
+ colors: ThemeColorUniforms,
344
+ ): any {
345
+ const { speed: uSpeed, intensity: uIntensity, mouseX: uMouseX, mouseY: uMouseY, mouseStrength: uMouseStrength } = uniforms
346
+
347
+ return Fn(() => {
348
+ const t = mul(time, uSpeed, 0.15)
349
+ const uvCoord = uv()
350
+
351
+ const mouseOffset = vec2(
352
+ mul(sub(uMouseX, 0.5), uMouseStrength, 0.3),
353
+ mul(sub(uMouseY, 0.5), uMouseStrength, 0.3),
354
+ )
355
+
356
+ const warpCoarse1 = simplexNoise2D(add(mul(uvCoord, 1.5), t))
357
+ const warpCoarse2 = simplexNoise2D(add(mul(uvCoord, 1.5), mul(t, -0.5), 7.0))
358
+ const warpedUV1 = add(uvCoord, mul(vec2(warpCoarse1, warpCoarse2), 0.25), mouseOffset)
359
+
360
+ const warpMed1 = simplexNoise2D(add(mul(warpedUV1, 3.0), mul(t, 0.7)))
361
+ const warpMed2 = simplexNoise2D(add(mul(warpedUV1, 3.0), mul(t, -0.3), 15.0))
362
+ const warpedUV2 = add(warpedUV1, mul(vec2(warpMed1, warpMed2), 0.12))
363
+
364
+ const warpFine1 = simplexNoise2D(add(mul(warpedUV2, 5.0), mul(t, 1.2)))
365
+ const warpFine2 = simplexNoise2D(add(mul(warpedUV2, 5.0), mul(t, -0.8), 25.0))
366
+ const warpedUV = add(warpedUV2, mul(vec2(warpFine1, warpFine2), 0.05))
367
+
368
+ const n1 = fbm2D(warpedUV, { octaves: 5, frequency: 2.0 }).mul(0.5).add(0.5)
369
+ const n2 = ridgedFbm2d(warpedUV, { octaves: 4, frequency: 1.5 })
370
+
371
+ let colorNode = mix(colors.color1, colors.color2, n1)
372
+ colorNode = mix(colorNode, colors.color3, mul(n2, 0.5))
373
+
374
+ const iridescence = mix(colors.color3, colors.color4, add(mul(n1, 0.6), mul(n2, 0.4)))
375
+ colorNode = mix(colorNode, iridescence, 0.3)
376
+ colorNode = mix(colorNode, colors.color4, mul(smoothstep(0.6, 0.9, n2), 0.25))
377
+
378
+ const brightness = add(0.85, mul(0.15, sin(add(mul(n1, 6.28), t))))
379
+ colorNode = mul(colorNode, brightness, uIntensity)
380
+
381
+ const dist = length(sub(uvCoord, 0.5))
382
+ colorNode = mul(colorNode, sub(1.0, mul(dist, 0.3)))
383
+
384
+ return colorNode
385
+ })()
386
+ }
387
+
388
+ export function createThemeAuroraColorNode(
389
+ uniforms: AmbientUniforms,
390
+ colors: ThemeColorUniforms,
391
+ ): any {
392
+ const { speed: uSpeed, intensity: uIntensity, mouseX: uMouseX, mouseY: uMouseY, mouseStrength: uMouseStrength } = uniforms
393
+
394
+ return Fn(() => {
395
+ const t = mul(time, uSpeed, 0.2)
396
+ const uvCoord = uv()
397
+
398
+ const mouseOffset = vec2(
399
+ mul(sub(uMouseX, 0.5), uMouseStrength),
400
+ mul(sub(uMouseY, 0.5), uMouseStrength),
401
+ )
402
+
403
+ const curtainCoord = add(
404
+ vec2(mul(uvCoord.x, 3.0), mul(uvCoord.y, 0.5)),
405
+ vec2(t, mul(t, 0.3)),
406
+ mouseOffset,
407
+ )
408
+
409
+ const curtain1 = simplexNoise2D(curtainCoord).mul(0.5).add(0.5)
410
+ const curtain2 = simplexNoise2D(add(mul(curtainCoord, 1.5), vec2(mul(t, -0.2), 5.0))).mul(0.5).add(0.5)
411
+ const curtain3 = simplexNoise2D(add(mul(curtainCoord, 0.7), vec2(mul(t, 0.4), 12.0))).mul(0.5).add(0.5)
412
+ const detail = fbm2D(curtainCoord, { octaves: 4, frequency: 2.0 }).mul(0.5).add(0.5)
413
+ const curtain = mul(add(mul(curtain1, 0.4), mul(curtain2, 0.25), mul(curtain3, 0.2), mul(detail, 0.15)), 1.0)
414
+
415
+ const fade = mul(pow(sub(1.0, uvCoord.y), 1.2), smoothstep(0.0, 0.3, uvCoord.y))
416
+ const aurora = mul(smoothstep(0.2, 0.8, mul(curtain, fade)), uIntensity)
417
+
418
+ const shimmer = simplexNoise2D(add(mul(curtainCoord, 8.0), mul(t, 3.0))).mul(0.5).add(0.5)
419
+
420
+ const colorDriver = add(mul(curtain2, 0.6), mul(curtain3, 0.4))
421
+ const auroraColor = mix(colors.color1, colors.color2, colorDriver)
422
+
423
+ const skyColor = mul(colors.color1, 0.04)
424
+ const shimmerColor = mul(colors.color3, mul(shimmer, mul(aurora, 0.15)))
425
+
426
+ return add(mix(skyColor, auroraColor, aurora), shimmerColor)
427
+ })()
428
+ }
429
+
280
430
  export function createOceanColorNode(uniforms: AmbientUniforms): any {
281
431
  const { speed: uSpeed, intensity: uIntensity, mouseX: uMouseX, mouseY: uMouseY, mouseStrength: uMouseStrength } = uniforms
282
432
 
@@ -0,0 +1,43 @@
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 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
+ })
35
+ }
36
+
37
+ if (import.meta.client) {
38
+ watch(activeAccent, refresh, { immediate: true })
39
+ watch(() => colorMode.value, refresh)
40
+ }
41
+
42
+ return { primaryHex, secondaryHex, infoHex, primaryLightHex, clearColor, refresh }
43
+ }
@@ -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
  }
@@ -40,8 +40,8 @@ const bgStyle = computed(() => ({
40
40
  class="size-8 rounded-full ring-1 ring-offset-2 transition-shadow duration-200"
41
41
  :class="[
42
42
  active
43
- ? 'ring-2 ring-primary ring-offset-[var(--ui-bg)] shadow-lg'
44
- : 'ring-transparent hover:ring-muted ring-offset-[var(--ui-bg)] hover:shadow-md',
43
+ ? 'ring-2 ring-primary ring-offset-bg shadow-lg'
44
+ : 'ring-transparent hover:ring-muted ring-offset-bg hover:shadow-md',
45
45
  ]"
46
46
  :style="bgStyle"
47
47
  :aria-label="`Set accent color to ${color}`"
@@ -2,11 +2,9 @@
2
2
  import type { AccentColor } from '#layers/theme/app/types/theme'
3
3
 
4
4
  const appConfig = useAppConfig()
5
- const { activeAccent, setAccent } = useThemePreferences()
5
+ const { activeAccent, setAccent } = useAccentColor()
6
6
 
7
- const accents = computed(
8
- () => (appConfig as any).themeLayer?.accents as AccentColor[] ?? [],
9
- )
7
+ const accents = computed(() => ((appConfig as any).themeLayer?.accents as AccentColor[]) ?? [])
10
8
  </script>
11
9
 
12
10
  <template>
@@ -9,7 +9,7 @@ const {
9
9
  effectiveHighContrast,
10
10
  effectiveReducedMotion,
11
11
  effectiveReducedTransparency,
12
- } = useThemePreferences()
12
+ } = useTheme()
13
13
 
14
14
  const contrastModel = computed({
15
15
  get: () => effectiveHighContrast.value,
@@ -50,10 +50,7 @@ const transparencyModel = computed({
50
50
  <li class="flex w-full flex-row items-center justify-between py-1">
51
51
  <div>
52
52
  <h2 class="text-lg font-semibold">High Contrast</h2>
53
- <p
54
- v-if="contrastOverride === 'system'"
55
- class="text-sm text-muted"
56
- >
53
+ <p v-if="contrastOverride === 'system'" class="text-sm text-muted">
57
54
  Following system
58
55
  </p>
59
56
  </div>
@@ -68,10 +65,7 @@ const transparencyModel = computed({
68
65
  <li class="flex w-full flex-row items-center justify-between py-1">
69
66
  <div>
70
67
  <h2 class="text-lg font-semibold">Reduce Motion</h2>
71
- <p
72
- v-if="motionOverride === 'system'"
73
- class="text-sm text-muted"
74
- >
68
+ <p v-if="motionOverride === 'system'" class="text-sm text-muted">
75
69
  Following system
76
70
  </p>
77
71
  </div>
@@ -86,10 +80,7 @@ const transparencyModel = computed({
86
80
  <li class="flex w-full flex-row items-center justify-between py-1">
87
81
  <div>
88
82
  <h2 class="text-lg font-semibold">Reduce Transparency</h2>
89
- <p
90
- v-if="transparencyOverride === 'system'"
91
- class="text-sm text-muted"
92
- >
83
+ <p v-if="transparencyOverride === 'system'" class="text-sm text-muted">
93
84
  Following system
94
85
  </p>
95
86
  </div>
@@ -1,9 +1,3 @@
1
1
  <template>
2
- <UButton
3
- icon="i-lucide-swatch-book"
4
- variant="ghost"
5
- square
6
- aria-label="Theme picker"
7
- size="xl"
8
- />
2
+ <UButton icon="i-lucide-swatch-book" variant="ghost" square aria-label="Theme picker" size="xl" />
9
3
  </template>