@agent-native/toolkit 0.201.1 → 0.202.0-nightly-20261006033736

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 (229) hide show
  1. package/agent-native.eject.json +39 -12
  2. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  3. package/dist/app/agentkit/react/components.js +292 -23
  4. package/dist/app/agentkit/react/components.js.map +1 -1
  5. package/dist/app/agentkit/react/context.d.ts +5 -0
  6. package/dist/app/agentkit/react/context.d.ts.map +1 -1
  7. package/dist/app/agentkit/react/context.js +5 -0
  8. package/dist/app/agentkit/react/context.js.map +1 -1
  9. package/dist/app/agentkit/react/styles.css +78 -3
  10. package/dist/app/auth/AuthPage.d.ts.map +1 -1
  11. package/dist/app/auth/AuthPage.js +7 -14
  12. package/dist/app/auth/AuthPage.js.map +1 -1
  13. package/dist/app/auth/server.d.ts +6 -0
  14. package/dist/app/auth/server.d.ts.map +1 -0
  15. package/dist/app/auth/server.js +19 -0
  16. package/dist/app/auth/server.js.map +1 -0
  17. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  18. package/dist/app/chat/AgentKitAssistantChat.js +164 -83
  19. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  20. package/dist/app/chat/AgentSidebar.d.ts +2 -1
  21. package/dist/app/chat/AgentSidebar.d.ts.map +1 -1
  22. package/dist/app/chat/AgentSidebar.js +2 -2
  23. package/dist/app/chat/AgentSidebar.js.map +1 -1
  24. package/dist/app/chat/agentkit-chat/guided-questions.d.ts.map +1 -1
  25. package/dist/app/chat/agentkit-chat/guided-questions.js +13 -3
  26. package/dist/app/chat/agentkit-chat/guided-questions.js.map +1 -1
  27. package/dist/app/chat/chat/surface-types.d.ts +2 -0
  28. package/dist/app/chat/chat/surface-types.d.ts.map +1 -1
  29. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  30. package/dist/app/i18n/catalogs/ar-SA.js +41 -0
  31. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  32. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  33. package/dist/app/i18n/catalogs/de-DE.js +41 -0
  34. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  35. package/dist/app/i18n/catalogs/en-US.d.ts +41 -0
  36. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  37. package/dist/app/i18n/catalogs/en-US.js +41 -0
  38. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  39. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  40. package/dist/app/i18n/catalogs/es-ES.js +41 -0
  41. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  42. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  43. package/dist/app/i18n/catalogs/fr-FR.js +41 -0
  44. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  45. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  46. package/dist/app/i18n/catalogs/hi-IN.js +41 -0
  47. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  48. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  49. package/dist/app/i18n/catalogs/ja-JP.js +41 -0
  50. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  51. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  52. package/dist/app/i18n/catalogs/ko-KR.js +41 -0
  53. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  54. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  55. package/dist/app/i18n/catalogs/pt-BR.js +41 -0
  56. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  57. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  58. package/dist/app/i18n/catalogs/zh-CN.js +41 -0
  59. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  60. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  61. package/dist/app/i18n/catalogs/zh-TW.js +41 -0
  62. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  63. package/dist/app/shared/StarfieldBackground.d.ts +1 -6
  64. package/dist/app/shared/StarfieldBackground.d.ts.map +1 -1
  65. package/dist/app/shared/StarfieldBackground.js +1 -368
  66. package/dist/app/shared/StarfieldBackground.js.map +1 -1
  67. package/dist/app/shared/WaveBackground.d.ts +5 -0
  68. package/dist/app/shared/WaveBackground.d.ts.map +1 -0
  69. package/dist/app/shared/WaveBackground.js +34 -0
  70. package/dist/app/shared/WaveBackground.js.map +1 -0
  71. package/dist/app/shared/WebGlWaveBackground.d.ts +9 -0
  72. package/dist/app/shared/WebGlWaveBackground.d.ts.map +1 -0
  73. package/dist/app/shared/WebGlWaveBackground.js +433 -0
  74. package/dist/app/shared/WebGlWaveBackground.js.map +1 -0
  75. package/dist/app/shared/index.d.ts +1 -0
  76. package/dist/app/shared/index.d.ts.map +1 -1
  77. package/dist/app/shared/index.js +1 -0
  78. package/dist/app/shared/index.js.map +1 -1
  79. package/dist/app/shared/ocean/bloom-blur.wgsl +43 -0
  80. package/dist/app/shared/ocean/bloom-blur.wgsl.js +1 -0
  81. package/dist/app/shared/ocean/bloom-bright.wgsl +21 -0
  82. package/dist/app/shared/ocean/bloom-bright.wgsl.js +1 -0
  83. package/dist/app/shared/ocean/bloom-composite.wgsl +40 -0
  84. package/dist/app/shared/ocean/bloom-composite.wgsl.js +1 -0
  85. package/dist/app/shared/ocean/brand-colors.d.ts +9 -0
  86. package/dist/app/shared/ocean/brand-colors.d.ts.map +1 -0
  87. package/dist/app/shared/ocean/brand-colors.js +45 -0
  88. package/dist/app/shared/ocean/brand-colors.js.map +1 -0
  89. package/dist/app/shared/ocean/camera.d.ts +5 -0
  90. package/dist/app/shared/ocean/camera.d.ts.map +1 -0
  91. package/dist/app/shared/ocean/camera.js +24 -0
  92. package/dist/app/shared/ocean/camera.js.map +1 -0
  93. package/dist/app/shared/ocean/hero-ocean-background.d.ts +8 -0
  94. package/dist/app/shared/ocean/hero-ocean-background.d.ts.map +1 -0
  95. package/dist/app/shared/ocean/hero-ocean-background.js +186 -0
  96. package/dist/app/shared/ocean/hero-ocean-background.js.map +1 -0
  97. package/dist/app/shared/ocean/ifft-stage.wgsl +36 -0
  98. package/dist/app/shared/ocean/ifft-stage.wgsl.js +1 -0
  99. package/dist/app/shared/ocean/initial-spectrum.wgsl +36 -0
  100. package/dist/app/shared/ocean/initial-spectrum.wgsl.js +1 -0
  101. package/dist/app/shared/ocean/noise.wgsl +24 -0
  102. package/dist/app/shared/ocean/noise.wgsl.js +1 -0
  103. package/dist/app/shared/ocean/normal-foam.wgsl +32 -0
  104. package/dist/app/shared/ocean/normal-foam.wgsl.js +1 -0
  105. package/dist/app/shared/ocean/ocean-colors.d.ts +6 -0
  106. package/dist/app/shared/ocean/ocean-colors.d.ts.map +1 -0
  107. package/dist/app/shared/ocean/ocean-colors.js +6 -0
  108. package/dist/app/shared/ocean/ocean-colors.js.map +1 -0
  109. package/dist/app/shared/ocean/ocean-common.wgsl +15 -0
  110. package/dist/app/shared/ocean/ocean-common.wgsl.js +1 -0
  111. package/dist/app/shared/ocean/ocean-graph.d.ts +12 -0
  112. package/dist/app/shared/ocean/ocean-graph.d.ts.map +1 -0
  113. package/dist/app/shared/ocean/ocean-graph.js +16 -0
  114. package/dist/app/shared/ocean/ocean-graph.js.map +1 -0
  115. package/dist/app/shared/ocean/particles.wgsl +139 -0
  116. package/dist/app/shared/ocean/particles.wgsl.js +1 -0
  117. package/dist/app/shared/ocean/present.wgsl +40 -0
  118. package/dist/app/shared/ocean/present.wgsl.js +1 -0
  119. package/dist/app/shared/ocean/renderer.d.ts +66 -0
  120. package/dist/app/shared/ocean/renderer.d.ts.map +1 -0
  121. package/dist/app/shared/ocean/renderer.js +535 -0
  122. package/dist/app/shared/ocean/renderer.js.map +1 -0
  123. package/dist/app/shared/ocean/spectrum.wgsl +35 -0
  124. package/dist/app/shared/ocean/spectrum.wgsl.js +1 -0
  125. package/dist/app/shared/ocean/tuning.d.ts +48 -0
  126. package/dist/app/shared/ocean/tuning.d.ts.map +1 -0
  127. package/dist/app/shared/ocean/tuning.js +74 -0
  128. package/dist/app/shared/ocean/tuning.js.map +1 -0
  129. package/dist/app/shared/ocean/webgpu-support.d.ts +3 -0
  130. package/dist/app/shared/ocean/webgpu-support.d.ts.map +1 -0
  131. package/dist/app/shared/ocean/webgpu-support.js +13 -0
  132. package/dist/app/shared/ocean/webgpu-support.js.map +1 -0
  133. package/dist/app/sharing/AccessRequestApprovalPage.d.ts +23 -0
  134. package/dist/app/sharing/AccessRequestApprovalPage.d.ts.map +1 -0
  135. package/dist/app/sharing/AccessRequestApprovalPage.js +95 -0
  136. package/dist/app/sharing/AccessRequestApprovalPage.js.map +1 -0
  137. package/dist/app/sharing/AccessRequestsSection.d.ts +5 -0
  138. package/dist/app/sharing/AccessRequestsSection.d.ts.map +1 -0
  139. package/dist/app/sharing/AccessRequestsSection.js +47 -0
  140. package/dist/app/sharing/AccessRequestsSection.js.map +1 -0
  141. package/dist/app/sharing/ResourceAccessScreen.d.ts +53 -0
  142. package/dist/app/sharing/ResourceAccessScreen.d.ts.map +1 -0
  143. package/dist/app/sharing/ResourceAccessScreen.js +132 -0
  144. package/dist/app/sharing/ResourceAccessScreen.js.map +1 -0
  145. package/dist/app/sharing/ShareButton.d.ts.map +1 -1
  146. package/dist/app/sharing/ShareButton.js +4 -2
  147. package/dist/app/sharing/ShareButton.js.map +1 -1
  148. package/dist/app/sharing/ShareDialog.d.ts.map +1 -1
  149. package/dist/app/sharing/ShareDialog.js +7 -5
  150. package/dist/app/sharing/ShareDialog.js.map +1 -1
  151. package/dist/app/sharing/index.d.ts +2 -0
  152. package/dist/app/sharing/index.d.ts.map +1 -1
  153. package/dist/app/sharing/index.js +2 -0
  154. package/dist/app/sharing/index.js.map +1 -1
  155. package/dist/app/styles/agent-native.css +21 -1
  156. package/dist/app/styles/tokens/agent-kit.css +3 -1
  157. package/dist/composer/AgentSuggestionBar.d.ts +1 -1
  158. package/dist/composer/AgentSuggestionBar.d.ts.map +1 -1
  159. package/dist/composer/AgentSuggestionBar.js +75 -6
  160. package/dist/composer/AgentSuggestionBar.js.map +1 -1
  161. package/package.json +14 -4
  162. package/src/app/agentkit/react/components.tsx +492 -60
  163. package/src/app/agentkit/react/context.tsx +10 -0
  164. package/src/app/agentkit/react/lifecycle.spec.tsx +487 -1
  165. package/src/app/agentkit/react/styles.css +78 -3
  166. package/src/app/auth/AuthPage.spec.tsx +11 -7
  167. package/src/app/auth/AuthPage.tsx +16 -35
  168. package/src/app/auth/server.spec.ts +68 -0
  169. package/src/app/auth/server.ts +29 -0
  170. package/src/app/chat/AgentKitAssistantChat.spec.tsx +287 -290
  171. package/src/app/chat/AgentKitAssistantChat.tsx +219 -116
  172. package/src/app/chat/AgentSidebar.tsx +3 -0
  173. package/src/app/chat/agentkit-chat/guided-questions.flow.spec.tsx +85 -0
  174. package/src/app/chat/agentkit-chat/guided-questions.tsx +12 -5
  175. package/src/app/chat/chat/surface-types.ts +2 -0
  176. package/src/app/i18n/catalogs/ar-SA.ts +44 -0
  177. package/src/app/i18n/catalogs/de-DE.ts +55 -0
  178. package/src/app/i18n/catalogs/en-US.ts +46 -0
  179. package/src/app/i18n/catalogs/es-ES.ts +53 -0
  180. package/src/app/i18n/catalogs/fr-FR.ts +53 -0
  181. package/src/app/i18n/catalogs/hi-IN.ts +47 -0
  182. package/src/app/i18n/catalogs/ja-JP.ts +54 -0
  183. package/src/app/i18n/catalogs/ko-KR.ts +50 -0
  184. package/src/app/i18n/catalogs/pt-BR.ts +52 -0
  185. package/src/app/i18n/catalogs/zh-CN.ts +42 -0
  186. package/src/app/i18n/catalogs/zh-TW.ts +43 -0
  187. package/src/app/shared/StarfieldBackground.tsx +4 -423
  188. package/src/app/shared/WaveBackground.spec.tsx +104 -0
  189. package/src/app/shared/WaveBackground.tsx +60 -0
  190. package/src/app/shared/WebGlWaveBackground.spec.tsx +106 -0
  191. package/src/app/shared/WebGlWaveBackground.tsx +499 -0
  192. package/src/app/shared/index.ts +1 -0
  193. package/src/app/shared/ocean/bloom-blur.wgsl +43 -0
  194. package/src/app/shared/ocean/bloom-bright.wgsl +21 -0
  195. package/src/app/shared/ocean/bloom-composite.wgsl +40 -0
  196. package/src/app/shared/ocean/brand-colors.ts +49 -0
  197. package/src/app/shared/ocean/camera.ts +28 -0
  198. package/src/app/shared/ocean/hero-ocean-background.spec.tsx +369 -0
  199. package/src/app/shared/ocean/hero-ocean-background.tsx +238 -0
  200. package/src/app/shared/ocean/ifft-stage.wgsl +36 -0
  201. package/src/app/shared/ocean/initial-spectrum.wgsl +36 -0
  202. package/src/app/shared/ocean/noise.wgsl +24 -0
  203. package/src/app/shared/ocean/normal-foam.wgsl +32 -0
  204. package/src/app/shared/ocean/ocean-colors.ts +11 -0
  205. package/src/app/shared/ocean/ocean-common.wgsl +15 -0
  206. package/src/app/shared/ocean/ocean-graph.ts +29 -0
  207. package/src/app/shared/ocean/particles.wgsl +139 -0
  208. package/src/app/shared/ocean/present.wgsl +40 -0
  209. package/src/app/shared/ocean/renderer.spec.ts +100 -0
  210. package/src/app/shared/ocean/renderer.ts +693 -0
  211. package/src/app/shared/ocean/spectrum.wgsl +35 -0
  212. package/src/app/shared/ocean/tuning.ts +131 -0
  213. package/src/app/shared/ocean/webgpu-support.ts +12 -0
  214. package/src/app/sharing/AccessRequestApprovalPage.spec.tsx +276 -0
  215. package/src/app/sharing/AccessRequestApprovalPage.tsx +307 -0
  216. package/src/app/sharing/AccessRequestsSection.tsx +163 -0
  217. package/src/app/sharing/ResourceAccessScreen.spec.tsx +235 -0
  218. package/src/app/sharing/ResourceAccessScreen.tsx +331 -0
  219. package/src/app/sharing/ShareButton.spec.tsx +171 -5
  220. package/src/app/sharing/ShareButton.tsx +15 -2
  221. package/src/app/sharing/ShareDialog.spec.tsx +71 -2
  222. package/src/app/sharing/ShareDialog.tsx +24 -2
  223. package/src/app/sharing/index.ts +2 -0
  224. package/src/app/styles/agent-native.css +21 -1
  225. package/src/app/styles/agent-native.spec.ts +16 -3
  226. package/src/app/styles/tokens/agent-kit.css +3 -1
  227. package/src/composer/AgentSuggestionBar.overflow.spec.tsx +277 -0
  228. package/src/composer/AgentSuggestionBar.tsx +113 -8
  229. package/src/wgsl-env.d.ts +6 -0
@@ -0,0 +1,43 @@
1
+
2
+
3
+ const KERNEL_RADIUS: u32 = 22u;
4
+ struct BlurUniforms {
5
+ direction: vec2f,
6
+ invSize: vec2f,
7
+ gaussianCoefficients0: vec4f,
8
+ gaussianCoefficients1: vec4f,
9
+ gaussianCoefficients2: vec4f,
10
+ gaussianCoefficients3: vec4f,
11
+ gaussianCoefficients4: vec4f,
12
+ gaussianCoefficients5: vec4f,
13
+ };
14
+ @group(0) @binding(0) var<uniform> uniforms: BlurUniforms;
15
+ @group(0) @binding(1) var colorTexture: texture_2d<f32>;
16
+ @group(0) @binding(2) var linearSampler: sampler;
17
+
18
+ fn coefficient(i: u32) -> f32 {
19
+ let packed = array<vec4f, 6>(
20
+ uniforms.gaussianCoefficients0,
21
+ uniforms.gaussianCoefficients1,
22
+ uniforms.gaussianCoefficients2,
23
+ uniforms.gaussianCoefficients3,
24
+ uniforms.gaussianCoefficients4,
25
+ uniforms.gaussianCoefficients5
26
+ );
27
+ return packed[i / 4u][i % 4u];
28
+ }
29
+
30
+ @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
31
+
32
+ var weightSum = coefficient(0u);
33
+ var diffuseSum = textureSample(colorTexture, linearSampler, uv).rgb * weightSum;
34
+ for (var i = 1u; i < KERNEL_RADIUS; i = i + 1u) {
35
+ let x = f32(i);
36
+ let w = coefficient(i);
37
+ let uvOffset = uniforms.direction * uniforms.invSize * x;
38
+ let sample1 = textureSample(colorTexture, linearSampler, uv + uvOffset).rgb;
39
+ let sample2 = textureSample(colorTexture, linearSampler, uv - uvOffset).rgb;
40
+ diffuseSum = diffuseSum + (sample1 + sample2) * w;
41
+ }
42
+ return vec4f(diffuseSum, 1.0);
43
+ }
@@ -0,0 +1,21 @@
1
+ struct BrightUniforms {
2
+ luminosityThreshold: f32,
3
+ smoothWidth: f32,
4
+ _pad0: vec2f,
5
+ };
6
+ @group(0) @binding(0) var<uniform> uniforms: BrightUniforms;
7
+ @group(0) @binding(1) var tDiffuse: texture_2d<f32>;
8
+ @group(0) @binding(2) var linearSampler: sampler;
9
+
10
+ fn luminance(rgb: vec3f) -> f32 {
11
+ return dot(rgb, vec3f(0.299, 0.587, 0.114));
12
+ }
13
+
14
+ @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
15
+
16
+ let texel = textureSample(tDiffuse, linearSampler, uv);
17
+ let v = luminance(texel.xyz);
18
+ let outputColor = vec4f(vec3f(0.0), 0.0);
19
+ let alpha = smoothstep(uniforms.luminosityThreshold, uniforms.luminosityThreshold + uniforms.smoothWidth, v);
20
+ return mix(outputColor, texel, alpha);
21
+ }
@@ -0,0 +1,40 @@
1
+ struct CompositeUniforms {
2
+ bloomStrength: f32,
3
+ bloomRadius: f32,
4
+ _pad0: vec2f,
5
+ bloomFactors0: vec4f,
6
+ bloomFactors1: vec4f,
7
+ };
8
+ @group(0) @binding(0) var<uniform> uniforms: CompositeUniforms;
9
+ @group(0) @binding(1) var blurTexture1: texture_2d<f32>;
10
+ @group(0) @binding(2) var blurTexture2: texture_2d<f32>;
11
+ @group(0) @binding(3) var blurTexture3: texture_2d<f32>;
12
+ @group(0) @binding(4) var blurTexture4: texture_2d<f32>;
13
+ @group(0) @binding(5) var blurTexture5: texture_2d<f32>;
14
+ @group(0) @binding(6) var linearSampler: sampler;
15
+
16
+ fn factor(i: u32) -> f32 {
17
+ let v = array<f32, 8>(
18
+ uniforms.bloomFactors0.x, uniforms.bloomFactors0.y, uniforms.bloomFactors0.z, uniforms.bloomFactors0.w,
19
+ uniforms.bloomFactors1.x, uniforms.bloomFactors1.y, uniforms.bloomFactors1.z, uniforms.bloomFactors1.w
20
+ );
21
+ return v[i];
22
+ }
23
+
24
+ fn lerpBloomFactor(f: f32) -> f32 {
25
+ let mirrorFactor = 1.2 - f;
26
+ return mix(f, mirrorFactor, uniforms.bloomRadius);
27
+ }
28
+
29
+ @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
30
+
31
+ let bloom = 3.0 * uniforms.bloomStrength * (
32
+ lerpBloomFactor(factor(0u)) * textureSample(blurTexture1, linearSampler, uv).rgb +
33
+ lerpBloomFactor(factor(1u)) * textureSample(blurTexture2, linearSampler, uv).rgb +
34
+ lerpBloomFactor(factor(2u)) * textureSample(blurTexture3, linearSampler, uv).rgb +
35
+ lerpBloomFactor(factor(3u)) * textureSample(blurTexture4, linearSampler, uv).rgb +
36
+ lerpBloomFactor(factor(4u)) * textureSample(blurTexture5, linearSampler, uv).rgb
37
+ );
38
+ let bloomAlpha = max(bloom.r, max(bloom.g, bloom.b));
39
+ return vec4f(bloom, bloomAlpha);
40
+ }
@@ -0,0 +1,49 @@
1
+ import { DEFAULT_OCEAN_COLORS, type OceanColors } from "./ocean-colors.js";
2
+
3
+ // guard:allow-raw-color - These fixed values calibrate the standalone GPU shader to the docs brand.
4
+ const DARK_COLORS = { fg: "#aeadac", bg: "#0a0a0a" };
5
+ // guard:allow-raw-color - These fixed values calibrate the standalone GPU shader to the docs brand.
6
+ const LIGHT_COLORS = { fg: "#00677f", bg: "#faf9f5" };
7
+
8
+ /**
9
+ * Returns null for anything that is not a full six-digit hex. Callers fall back
10
+ * to the packaged defaults -- an unreadable token and a legitimately dark token
11
+ * must not produce the same silent black.
12
+ */
13
+ export function hexToLinearRgb(
14
+ hex: string,
15
+ ): readonly [number, number, number] | null {
16
+ const normalized = hex.trim().replace("#", "");
17
+ if (!/^[0-9a-fA-F]{6}$/.test(normalized)) return null;
18
+ const value = Number.parseInt(normalized, 16);
19
+ return [
20
+ srgbToLinear(((value >> 16) & 255) / 255),
21
+ srgbToLinear(((value >> 8) & 255) / 255),
22
+ srgbToLinear((value & 255) / 255),
23
+ ];
24
+ }
25
+
26
+ function srgbToLinear(channel: number): number {
27
+ return channel <= 0.04045
28
+ ? channel / 12.92
29
+ : ((channel + 0.055) / 1.055) ** 2.4;
30
+ }
31
+
32
+ export function readOceanColors(element: Element): OceanColors {
33
+ const styles = getComputedStyle(element);
34
+ const fg = hexToLinearRgb(styles.getPropertyValue("--b-hero-shader-fg"));
35
+ const bg = hexToLinearRgb(styles.getPropertyValue("--b-bg-page"));
36
+ if (fg && bg) return { fg, bg };
37
+
38
+ const root = document.documentElement;
39
+ const dark = root.classList.contains("dark")
40
+ ? true
41
+ : root.classList.contains("light")
42
+ ? false
43
+ : root.getAttribute("data-theme") === "dark";
44
+ const colors = dark ? DARK_COLORS : LIGHT_COLORS;
45
+ return {
46
+ fg: hexToLinearRgb(colors.fg) ?? DEFAULT_OCEAN_COLORS.fg,
47
+ bg: hexToLinearRgb(colors.bg) ?? DEFAULT_OCEAN_COLORS.bg,
48
+ };
49
+ }
@@ -0,0 +1,28 @@
1
+ import { OCEAN_TUNING } from "./tuning.js";
2
+
3
+ export function oceanCamera(size: readonly [number, number]) {
4
+ const { eye, target, pitchDegrees, fovDegrees, near, far } =
5
+ OCEAN_TUNING.camera;
6
+ const angle =
7
+ Math.atan2(eye[1] - target[1], eye[2] - target[2]) -
8
+ (pitchDegrees * Math.PI) / 180;
9
+ const c = Math.cos(angle);
10
+ const s = Math.sin(angle);
11
+ const f = 1 / Math.tan((fovDegrees * Math.PI) / 360);
12
+
13
+ const view = new Float32Array(16);
14
+ view[0] = view[15] = 1;
15
+ view[5] = view[10] = c;
16
+ view[6] = s;
17
+ view[9] = -s;
18
+ view[13] = -(c * eye[1] - s * eye[2]);
19
+ view[14] = -(s * eye[1] + c * eye[2]);
20
+
21
+ const projection = new Float32Array(16);
22
+ projection[0] = f / (size[0] / Math.max(1, size[1]));
23
+ projection[5] = f;
24
+ projection[10] = far / (near - far);
25
+ projection[11] = -1;
26
+ projection[14] = (far * near) / (near - far);
27
+ return { view, projection };
28
+ }
@@ -0,0 +1,369 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { act, cleanup, render, waitFor } from "@testing-library/react";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ import { hexToLinearRgb } from "./brand-colors.js";
7
+ import { HeroOceanBackground } from "./hero-ocean-background.js";
8
+
9
+ const FIRST_FRAME_TIMEOUT_MS = 6000;
10
+
11
+ const { createRenderer, renderer, importSpy } = vi.hoisted(() => {
12
+ const renderer = {
13
+ ready: Promise.resolve(),
14
+ firstFrame: Promise.resolve(),
15
+ dispose: vi.fn(),
16
+ setColors: vi.fn(),
17
+ setPaused: vi.fn(),
18
+ setPointer: vi.fn(),
19
+ };
20
+ const importSpy = vi.fn();
21
+ return { createRenderer: vi.fn(() => renderer), renderer, importSpy };
22
+ });
23
+
24
+ vi.mock("./renderer", async () => {
25
+ importSpy();
26
+ const actual =
27
+ await vi.importActual<typeof import("./renderer.js")>("./renderer.js");
28
+ return { ...actual, createRenderer };
29
+ });
30
+
31
+ let intersectionCallbacks: ((entries: unknown[]) => void)[] = [];
32
+ let mutationCallbacks: (() => void)[] = [];
33
+ let disconnected: string[] = [];
34
+
35
+ beforeEach(() => {
36
+ intersectionCallbacks = [];
37
+ mutationCallbacks = [];
38
+ disconnected = [];
39
+ vi.stubGlobal(
40
+ "IntersectionObserver",
41
+ class {
42
+ constructor(cb: (entries: unknown[]) => void) {
43
+ intersectionCallbacks.push(cb);
44
+ }
45
+ observe() {}
46
+ disconnect() {
47
+ disconnected.push("intersection");
48
+ }
49
+ },
50
+ );
51
+ vi.stubGlobal(
52
+ "MutationObserver",
53
+ class {
54
+ constructor(cb: () => void) {
55
+ mutationCallbacks.push(cb);
56
+ }
57
+ observe() {}
58
+ disconnect() {
59
+ disconnected.push("mutation");
60
+ }
61
+ },
62
+ );
63
+ });
64
+
65
+ afterEach(() => {
66
+ cleanup();
67
+ vi.useRealTimers();
68
+ vi.unstubAllGlobals();
69
+ Object.defineProperty(document, "hidden", {
70
+ configurable: true,
71
+ value: false,
72
+ });
73
+ renderer.ready = Promise.resolve();
74
+ renderer.firstFrame = Promise.resolve();
75
+ createRenderer.mockClear();
76
+ importSpy.mockClear();
77
+ renderer.dispose.mockClear();
78
+ renderer.setColors.mockClear();
79
+ renderer.setPaused.mockClear();
80
+ renderer.setPointer.mockClear();
81
+ });
82
+
83
+ describe("hexToLinearRgb", () => {
84
+ it("converts a six-digit hex to linear RGB", () => {
85
+ expect(hexToLinearRgb("#ffffff")).toEqual([1, 1, 1]);
86
+ expect(hexToLinearRgb("#000000")).toEqual([0, 0, 0]);
87
+ });
88
+
89
+ it("linearizes rather than passing sRGB straight through", () => {
90
+ const [r] = hexToLinearRgb("#808080")!;
91
+ expect(r).toBeCloseTo(0.2158, 3);
92
+ });
93
+
94
+ it("returns null for anything that is not a six-digit hex", () => {
95
+ for (const input of [
96
+ "",
97
+ " ",
98
+ "#fff",
99
+ "rgb(0,0,0)",
100
+ "#ggghhh",
101
+ "#1234567",
102
+ ]) {
103
+ expect(hexToLinearRgb(input)).toBeNull();
104
+ }
105
+ });
106
+ });
107
+
108
+ describe("HeroOceanBackground", () => {
109
+ it("fills the hero section behind the grid and is hidden from assistive tech", async () => {
110
+ const { container } = render(
111
+ <HeroOceanBackground onError={vi.fn()} onReady={vi.fn()} />,
112
+ );
113
+ const box = container.firstElementChild as HTMLElement;
114
+ expect(box.getAttribute("aria-hidden")).toBe("true");
115
+ expect(box.className).toContain("absolute");
116
+ expect(box.className).toContain("inset-0");
117
+ expect(box.className).toContain("z-[-1]");
118
+ const canvas = box.querySelector("canvas");
119
+ expect(canvas).not.toBeNull();
120
+ expect(canvas?.style.width).toBe("100%");
121
+ expect(canvas?.style.height).toBe("100%");
122
+ expect(box.style.opacity).toBe("0");
123
+ await waitFor(() => expect(box.style.opacity).toBe("0.3"));
124
+ await waitFor(() => expect(createRenderer).toHaveBeenCalled());
125
+ });
126
+
127
+ it("loads the GPU runtime in an effect, not during render", async () => {
128
+ const onError = vi.fn();
129
+ render(<HeroOceanBackground onError={onError} onReady={vi.fn()} />);
130
+ expect(createRenderer).not.toHaveBeenCalled();
131
+ await waitFor(() => {
132
+ if (onError.mock.calls.length) throw onError.mock.calls[0]![0];
133
+ expect(createRenderer).toHaveBeenCalled();
134
+ });
135
+ });
136
+
137
+ it("starts the first-frame deadline after renderer initialization", async () => {
138
+ vi.useFakeTimers();
139
+ let finishInitialization: (() => void) | undefined;
140
+ const ready = new Promise<void>((resolve) => {
141
+ finishInitialization = resolve;
142
+ });
143
+ createRenderer.mockReturnValueOnce({ ...renderer, ready });
144
+ const onError = vi.fn();
145
+ const onReady = vi.fn();
146
+
147
+ render(<HeroOceanBackground onError={onError} onReady={onReady} />);
148
+ await act(async () => {
149
+ for (let i = 0; i < 10; i++) await Promise.resolve();
150
+ });
151
+ expect(createRenderer).toHaveBeenCalledOnce();
152
+
153
+ await act(async () => {
154
+ await vi.advanceTimersByTimeAsync(7_000);
155
+ });
156
+ expect(onError).not.toHaveBeenCalled();
157
+
158
+ await act(async () => {
159
+ finishInitialization?.();
160
+ await ready;
161
+ for (let i = 0; i < 10; i++) await Promise.resolve();
162
+ });
163
+ expect(onReady).toHaveBeenCalledOnce();
164
+ });
165
+
166
+ it("pauses the first-frame deadline while the document is hidden", async () => {
167
+ vi.useFakeTimers();
168
+ Object.defineProperty(document, "hidden", {
169
+ configurable: true,
170
+ value: false,
171
+ });
172
+ renderer.firstFrame = new Promise<void>(() => {});
173
+ const onError = vi.fn();
174
+ render(<HeroOceanBackground onError={onError} onReady={vi.fn()} />);
175
+
176
+ await act(async () => {
177
+ for (let tick = 0; tick < 10; tick += 1) await Promise.resolve();
178
+ });
179
+ expect(createRenderer).toHaveBeenCalledOnce();
180
+
181
+ await act(async () => {
182
+ await vi.advanceTimersByTimeAsync(1000);
183
+ Object.defineProperty(document, "hidden", {
184
+ configurable: true,
185
+ value: true,
186
+ });
187
+ document.dispatchEvent(new Event("visibilitychange"));
188
+ await vi.advanceTimersByTimeAsync(7000);
189
+ expect(onError).not.toHaveBeenCalled();
190
+ Object.defineProperty(document, "hidden", {
191
+ configurable: true,
192
+ value: false,
193
+ });
194
+ document.dispatchEvent(new Event("visibilitychange"));
195
+ await vi.advanceTimersByTimeAsync(FIRST_FRAME_TIMEOUT_MS - 1001);
196
+ expect(onError).not.toHaveBeenCalled();
197
+ await vi.advanceTimersByTimeAsync(1);
198
+ });
199
+
200
+ expect(onError).toHaveBeenCalledWith(
201
+ expect.objectContaining({
202
+ message: "The ocean wave did not draw a frame",
203
+ }),
204
+ );
205
+ });
206
+
207
+ it("pushes brand colours through on a theme change", async () => {
208
+ render(<HeroOceanBackground onError={vi.fn()} onReady={vi.fn()} />);
209
+ await waitFor(() => expect(createRenderer).toHaveBeenCalled());
210
+
211
+ for (const cb of mutationCallbacks) cb();
212
+ expect(renderer.setColors).toHaveBeenCalledWith(
213
+ expect.objectContaining({
214
+ fg: expect.any(Array),
215
+ bg: expect.any(Array),
216
+ }),
217
+ );
218
+ });
219
+
220
+ it("pauses when scrolled out of view and resumes when back", async () => {
221
+ render(<HeroOceanBackground onError={vi.fn()} onReady={vi.fn()} />);
222
+ await waitFor(() => expect(createRenderer).toHaveBeenCalled());
223
+
224
+ for (const cb of intersectionCallbacks) cb([{ isIntersecting: false }]);
225
+ expect(renderer.setPaused).toHaveBeenCalledWith(true);
226
+
227
+ for (const cb of intersectionCallbacks) cb([{ isIntersecting: true }]);
228
+ expect(renderer.setPaused).toHaveBeenCalledWith(false);
229
+ });
230
+
231
+ it("tracks body mouse movement relative to the hero bounds", async () => {
232
+ const { container } = render(
233
+ <HeroOceanBackground onError={vi.fn()} onReady={vi.fn()} />,
234
+ );
235
+ const box = container.firstElementChild as HTMLElement;
236
+ vi.spyOn(box, "getBoundingClientRect").mockReturnValue({
237
+ left: 10,
238
+ top: 20,
239
+ width: 200,
240
+ height: 100,
241
+ } as DOMRect);
242
+
243
+ await waitFor(() => expect(createRenderer).toHaveBeenCalled());
244
+ renderer.setPointer.mockClear();
245
+
246
+ document.body.dispatchEvent(
247
+ new MouseEvent("mousemove", {
248
+ bubbles: true,
249
+ clientX: 110,
250
+ clientY: 70,
251
+ }),
252
+ );
253
+ expect(renderer.setPointer).toHaveBeenLastCalledWith([0, 0, 1]);
254
+
255
+ document.body.dispatchEvent(
256
+ new MouseEvent("mousemove", {
257
+ bubbles: true,
258
+ clientX: 310,
259
+ clientY: 70,
260
+ }),
261
+ );
262
+ expect(renderer.setPointer).toHaveBeenLastCalledWith([2, 0, 0]);
263
+ });
264
+
265
+ it("remaps the active pointer on scroll and fades it on window blur", async () => {
266
+ const { container } = render(
267
+ <HeroOceanBackground onError={vi.fn()} onReady={vi.fn()} />,
268
+ );
269
+ const box = container.firstElementChild as HTMLElement;
270
+ const rect = {
271
+ left: 10,
272
+ top: 20,
273
+ width: 200,
274
+ height: 100,
275
+ } as DOMRect;
276
+ vi.spyOn(box, "getBoundingClientRect").mockReturnValue(rect);
277
+
278
+ await waitFor(() => expect(createRenderer).toHaveBeenCalled());
279
+ renderer.setPointer.mockClear();
280
+
281
+ document.body.dispatchEvent(
282
+ new MouseEvent("mousemove", {
283
+ bubbles: true,
284
+ clientX: 110,
285
+ clientY: 70,
286
+ }),
287
+ );
288
+ expect(renderer.setPointer).toHaveBeenLastCalledWith([0, 0, 1]);
289
+
290
+ vi.spyOn(box, "getBoundingClientRect").mockReturnValue({
291
+ ...rect,
292
+ left: 60,
293
+ } as DOMRect);
294
+ window.dispatchEvent(new Event("scroll"));
295
+ expect(renderer.setPointer).toHaveBeenLastCalledWith([-0.5, 0, 1]);
296
+
297
+ window.dispatchEvent(new Event("blur"));
298
+ expect(renderer.setPointer).toHaveBeenLastCalledWith([-0.5, 0, 0]);
299
+
300
+ window.dispatchEvent(new Event("scroll"));
301
+ expect(renderer.setPointer).toHaveBeenLastCalledWith([-0.5, 0, 0]);
302
+ });
303
+
304
+ it("disposes the GPU and both observers on unmount", async () => {
305
+ const { unmount } = render(
306
+ <HeroOceanBackground onError={vi.fn()} onReady={vi.fn()} />,
307
+ );
308
+ await waitFor(() => expect(createRenderer).toHaveBeenCalled());
309
+
310
+ const before = disconnected.length;
311
+ unmount();
312
+ expect(renderer.dispose).toHaveBeenCalled();
313
+ expect([...disconnected.slice(before)].sort()).toEqual([
314
+ "intersection",
315
+ "mutation",
316
+ ]);
317
+ });
318
+
319
+ it("does not construct a renderer when unmounted before the import lands", async () => {
320
+ const { unmount } = render(
321
+ <HeroOceanBackground onError={vi.fn()} onReady={vi.fn()} />,
322
+ );
323
+ unmount();
324
+ await Promise.resolve();
325
+ expect(createRenderer).not.toHaveBeenCalled();
326
+ });
327
+
328
+ it("stays fully transparent until the first frame is drawn", async () => {
329
+ let drawFirstFrame: () => void = () => {};
330
+ renderer.firstFrame = new Promise<void>((resolve) => {
331
+ drawFirstFrame = resolve;
332
+ });
333
+ const onReady = vi.fn();
334
+ const { container } = render(
335
+ <HeroOceanBackground onError={vi.fn()} onReady={onReady} />,
336
+ );
337
+ const box = container.firstElementChild as HTMLElement;
338
+
339
+ await waitFor(() => expect(createRenderer).toHaveBeenCalled());
340
+ expect(box.style.opacity).toBe("0");
341
+
342
+ drawFirstFrame();
343
+ await waitFor(() => expect(box.style.opacity).toBe("0.3"));
344
+ expect(onReady).toHaveBeenCalledOnce();
345
+ renderer.firstFrame = Promise.resolve();
346
+ });
347
+
348
+ it("never fades in when the renderer fails before drawing", async () => {
349
+ renderer.firstFrame = Promise.reject(new Error("device lost"));
350
+ const { container } = render(
351
+ <HeroOceanBackground onError={vi.fn()} onReady={vi.fn()} />,
352
+ );
353
+ const box = container.firstElementChild as HTMLElement;
354
+
355
+ await waitFor(() => expect(createRenderer).toHaveBeenCalled());
356
+ await Promise.resolve();
357
+ expect(box.style.opacity).toBe("0");
358
+ renderer.firstFrame = Promise.resolve();
359
+ });
360
+
361
+ it("reports a construction failure to the caller instead of throwing", async () => {
362
+ const onError = vi.fn();
363
+ createRenderer.mockImplementationOnce(() => {
364
+ throw new Error("no device");
365
+ });
366
+ render(<HeroOceanBackground onError={onError} onReady={vi.fn()} />);
367
+ await waitFor(() => expect(onError).toHaveBeenCalled());
368
+ });
369
+ });