chati-dev 4.2.2 → 4.3.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 (220) hide show
  1. package/README.md +80 -53
  2. package/bin/chati.js +150 -5
  3. package/framework/agents/build/dev.md +509 -76
  4. package/framework/agents/deploy/devops.md +40 -25
  5. package/framework/agents/discover/brief.md +156 -22
  6. package/framework/agents/discover/brownfield-wu.md +24 -14
  7. package/framework/agents/discover/greenfield-wu.md +100 -14
  8. package/framework/agents/plan/architect-data-engineer.md +6 -6
  9. package/framework/agents/plan/architect-system.md +46 -12
  10. package/framework/agents/plan/architect.md +40 -20
  11. package/framework/agents/plan/detail.md +36 -24
  12. package/framework/agents/plan/phases.md +36 -26
  13. package/framework/agents/plan/tasks.md +114 -33
  14. package/framework/agents/plan/ux-brand-architect.md +240 -8
  15. package/framework/agents/plan/ux-component-engineer.md +28 -13
  16. package/framework/agents/plan/ux-researcher.md +7 -6
  17. package/framework/agents/plan/ux.md +55 -22
  18. package/framework/agents/quality/qa-implementation.md +143 -74
  19. package/framework/agents/quality/qa-planning.md +115 -42
  20. package/framework/agents/quality/qa-visual.md +439 -0
  21. package/framework/agents/shared/visualizer.md +128 -0
  22. package/framework/config.yaml +7 -6
  23. package/framework/constitution.md +127 -44
  24. package/framework/context/governance.md +12 -7
  25. package/framework/context/quality.md +6 -5
  26. package/framework/context/root.md +6 -6
  27. package/framework/data/entity-registry.yaml +377 -4
  28. package/framework/data/model-limits.json +19 -0
  29. package/framework/domains/agents/qa-visual.yaml +74 -0
  30. package/framework/domains/constitution.yaml +46 -2
  31. package/framework/domains/workflows/greenfield-fullstack.yaml +2 -2
  32. package/framework/hooks/advance-trigger.js +131 -0
  33. package/framework/hooks/brief-validator.js +83 -0
  34. package/framework/hooks/constitution-guard.js +24 -5
  35. package/framework/hooks/license-guard.js +62 -27
  36. package/framework/hooks/mode-governance.js +13 -2
  37. package/framework/hooks/model-governance.js +1 -0
  38. package/framework/hooks/post-dev.js +207 -0
  39. package/framework/hooks/prism-engine.js +274 -105
  40. package/framework/hooks/reasoning-escalator.js +371 -0
  41. package/framework/hooks/reference-trigger.js +117 -0
  42. package/framework/hooks/session-digest.js +50 -1
  43. package/framework/hooks/settings.json +32 -1
  44. package/framework/hooks/style-guard.js +25 -6
  45. package/framework/hooks/team-quality-gate.js +19 -12
  46. package/framework/hooks/undercover-guard.js +4 -2
  47. package/framework/i18n/en.yaml +3 -3
  48. package/framework/i18n/es.yaml +3 -3
  49. package/framework/i18n/fr.yaml +3 -3
  50. package/framework/i18n/pt.yaml +3 -3
  51. package/framework/intelligence/context-engine.md +4 -5
  52. package/framework/intelligence/decision-engine.md +1 -1
  53. package/framework/intelligence/hooks-performance.md +3 -3
  54. package/framework/migrations/v1.0-to-v1.1.yaml +1 -1
  55. package/framework/migrations/v1.4-to-v2.0.yaml +11 -11
  56. package/framework/migrations/v4.0-to-v4.1.yaml +2 -2
  57. package/framework/migrations/v4.2-to-v4.3.yaml +29 -0
  58. package/framework/orchestrator/chati-router.js +387 -0
  59. package/framework/orchestrator/chati-update.md +40 -40
  60. package/framework/orchestrator/chati.md +294 -94
  61. package/framework/scaffold/motion-premium/README.md +89 -0
  62. package/framework/scaffold/motion-premium/app/globals.css.template +400 -0
  63. package/framework/scaffold/motion-premium/app/layout.tsx.template +110 -0
  64. package/framework/scaffold/motion-premium/components/animation/BackgroundCrossfadeProvider.tsx.template +170 -0
  65. package/framework/scaffold/motion-premium/components/animation/LenisProvider.tsx.template +49 -0
  66. package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template +83 -0
  67. package/framework/scaffold/motion-premium/components/animation/Preloader.tsx.template +171 -0
  68. package/framework/scaffold/motion-premium/components/ui/Container.tsx.template +69 -0
  69. package/framework/scaffold/motion-premium/components/ui/PageSection.tsx.template +74 -0
  70. package/framework/scaffold/motion-premium/lib/animations/gsap.ts.template +112 -0
  71. package/framework/scaffold/motion-premium/lib/animations/refreshCoordinator.ts.template +75 -0
  72. package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +119 -0
  73. package/framework/scaffold/motion-premium/lib/animations/useGsapContext.ts.template +92 -0
  74. package/framework/scaffold/motion-premium/lib/animations/useScrollSnapStepper.ts.template +265 -0
  75. package/framework/scaffold/motion-premium/lib/animations/useSmoothScroll.ts.template +67 -0
  76. package/framework/scaffold/motion-premium/lib/brand.ts.template +43 -0
  77. package/framework/scaffold/motion-premium/scaffold.yaml +174 -0
  78. package/framework/scaffold/motion-premium-3d/README.md +80 -0
  79. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template +81 -0
  80. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template +75 -0
  81. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/page.tsx.template +26 -0
  82. package/framework/scaffold/motion-premium-3d/components/3d/CameraRig.tsx.template +100 -0
  83. package/framework/scaffold/motion-premium-3d/components/3d/CanvasProvider.tsx.template +85 -0
  84. package/framework/scaffold/motion-premium-3d/components/3d/InvalidateOnScroll.tsx.template +51 -0
  85. package/framework/scaffold/motion-premium-3d/components/3d/MeshCrossfade.tsx.template +79 -0
  86. package/framework/scaffold/motion-premium-3d/components/3d/ScrollCrossfade.tsx.template +88 -0
  87. package/framework/scaffold/motion-premium-3d/components/3d/ScrollScene.tsx.template +121 -0
  88. package/framework/scaffold/motion-premium-3d/components/webgl/SceneFallback.tsx.template +65 -0
  89. package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +68 -0
  90. package/framework/scaffold/motion-premium-3d/lib/webgl/detect.ts.template +69 -0
  91. package/framework/scaffold/motion-premium-3d/scaffold.yaml +133 -0
  92. package/framework/schemas/session.schema.json +109 -21
  93. package/framework/scripts/reference-capture.js +430 -0
  94. package/framework/scripts/visual-qa.js +674 -0
  95. package/framework/tasks/orchestrator-handoff.md +1 -1
  96. package/framework/tasks/orchestrator-resume.md +1 -1
  97. package/framework/tasks/orchestrator-route.md +1 -1
  98. package/framework/tasks/orchestrator-status.md +3 -3
  99. package/framework/tasks/qa-planning-gate-define.md +1 -1
  100. package/framework/templates/brandbook-html-tmpl.md +1 -1
  101. package/framework/templates/brandbook-tmpl.yaml +1 -1
  102. package/framework/templates/component-spec-tmpl.yaml +1 -1
  103. package/framework/templates/design-token-tmpl.yaml +1 -1
  104. package/framework/templates/icon-system-tmpl.yaml +1 -1
  105. package/framework/templates/team-planning-tasks.yaml +6 -5
  106. package/framework/workflows/brownfield-discovery.yaml +2 -2
  107. package/framework/workflows/brownfield-fullstack.yaml +15 -11
  108. package/framework/workflows/brownfield-service.yaml +14 -10
  109. package/framework/workflows/brownfield-ui.yaml +15 -11
  110. package/framework/workflows/greenfield-fullstack.yaml +16 -13
  111. package/framework/workflows/quick-flow.yaml +3 -3
  112. package/framework/workflows/standard-flow.yaml +12 -9
  113. package/package.json +10 -5
  114. package/src/autonomy/autonomous-gate.js +1 -0
  115. package/src/autonomy/build-state.js +1 -2
  116. package/src/autonomy/progress-reporter.js +1 -1
  117. package/src/config/agent-customizer.js +11 -3
  118. package/src/config/claude-settings-generator.js +27 -7
  119. package/src/config/context-file-generator.js +41 -21
  120. package/src/config/framework-adapter.js +1 -0
  121. package/src/config/gemini-hooks-generator.js +19 -7
  122. package/src/config/mcp-configs.js +1 -0
  123. package/src/context/layers/l1-global.js +2 -1
  124. package/src/dashboard/data-reader.js +4 -3
  125. package/src/dashboard/layout.js +2 -1
  126. package/src/decision/analyzer.js +6 -30
  127. package/src/decision/engine.js +4 -28
  128. package/src/decision/registry-healer.js +3 -2
  129. package/src/decision/registry-updater.js +23 -14
  130. package/src/extensions/loader.js +2 -8
  131. package/src/gates/g1-planning-complete.js +2 -1
  132. package/src/gates/g2-qa-planning.js +2 -1
  133. package/src/gates/g3-implementation.js +2 -1
  134. package/src/gates/g4-qa-implementation.js +3 -2
  135. package/src/gates/g5-deploy-ready.js +2 -1
  136. package/src/health/engine.js +4 -3
  137. package/src/installer/core.js +422 -81
  138. package/src/installer/preflight.js +131 -0
  139. package/src/installer/provider-overlay.js +3 -3
  140. package/src/installer/scaffold-applier.js +358 -0
  141. package/src/installer/templates.js +46 -29
  142. package/src/installer/validator.js +17 -12
  143. package/src/intelligence/registry-manager.js +22 -29
  144. package/src/intelligence/timeline.js +11 -6
  145. package/src/license/commands.js +1 -1
  146. package/src/license/wait.js +102 -0
  147. package/src/memory/agent-memory.js +81 -0
  148. package/src/memory/dream.js +32 -1
  149. package/src/merger/replace-merger.js +28 -15
  150. package/src/orchestrator/agent-selector.js +2 -1
  151. package/src/orchestrator/cli.js +1869 -71
  152. package/src/orchestrator/doctor.js +270 -0
  153. package/src/orchestrator/handoff-engine.js +4 -3
  154. package/src/orchestrator/index.js +2 -0
  155. package/src/orchestrator/pipeline-manager.js +306 -15
  156. package/src/orchestrator/session-manager.js +331 -6
  157. package/src/tasks/handoff.js +3 -2
  158. package/src/telemetry/config.js +4 -3
  159. package/src/telemetry/schema.js +1 -0
  160. package/src/terminal/collector.js +3 -2
  161. package/src/terminal/index.js +1 -2
  162. package/src/terminal/isolation.js +52 -18
  163. package/src/terminal/prompt-builder.js +42 -25
  164. package/src/terminal/run-parallel.js +1 -1
  165. package/src/terminal/run-team.js +3 -3
  166. package/src/terminal/team-task-list.js +43 -4
  167. package/src/upgrade/backup.js +3 -2
  168. package/src/upgrade/checker.js +3 -2
  169. package/src/upgrade/migrator.js +65 -7
  170. package/src/upgrade/tracked-files-detector.js +86 -0
  171. package/src/upgrade/user-messages.js +94 -0
  172. package/src/utils/config-parser.js +2 -1
  173. package/src/utils/feature-flags.js +2 -1
  174. package/src/utils/flatten-entities.js +69 -0
  175. package/src/utils/framework-dir.js +16 -0
  176. package/src/utils/model-id.js +85 -0
  177. package/src/utils/provider-limits.js +84 -23
  178. package/src/utils/schema-validator.js +1 -1
  179. package/src/wizard/i18n.js +5 -4
  180. package/src/wizard/index.js +14 -0
  181. package/assets/logo - c/303/263pia.png +0 -0
  182. package/assets/logo.svg +0 -42
  183. package/assets/logo2.png +0 -0
  184. package/assets/social-preview.png +0 -0
  185. package/scripts/bundle-framework.js +0 -69
  186. package/scripts/changelog-generator.js +0 -222
  187. package/scripts/codebase-mapper.js +0 -728
  188. package/scripts/commit-message-generator.js +0 -167
  189. package/scripts/coverage-analyzer.js +0 -260
  190. package/scripts/dependency-analyzer.js +0 -280
  191. package/scripts/doctor/checks/agents.js +0 -77
  192. package/scripts/doctor/checks/constitution.js +0 -41
  193. package/scripts/doctor/checks/domain-alignment.js +0 -58
  194. package/scripts/doctor/checks/prism-layers.js +0 -84
  195. package/scripts/doctor/checks/registry.js +0 -55
  196. package/scripts/doctor/checks/schemas.js +0 -61
  197. package/scripts/doctor/fixes/reference-fix.js +0 -100
  198. package/scripts/doctor/fixes/registry-fix.js +0 -56
  199. package/scripts/doctor/index.js +0 -212
  200. package/scripts/framework-analyzer.js +0 -308
  201. package/scripts/generate-constitution-domain.js +0 -253
  202. package/scripts/generate-signing-key.js +0 -33
  203. package/scripts/health-check.js +0 -481
  204. package/scripts/ide-sync.js +0 -326
  205. package/scripts/performance-analyzer.js +0 -325
  206. package/scripts/plan-tracker.js +0 -278
  207. package/scripts/populate-entity-registry.js +0 -481
  208. package/scripts/pr-review.js +0 -317
  209. package/scripts/rollback-manager.js +0 -310
  210. package/scripts/semantic-lint.js +0 -328
  211. package/scripts/sign-manifest.js +0 -53
  212. package/scripts/stuck-detector.js +0 -343
  213. package/scripts/test-quality-assessment.js +0 -257
  214. package/scripts/validate-agents.js +0 -368
  215. package/scripts/validate-package.js +0 -505
  216. package/scripts/validate-tasks.js +0 -465
  217. package/src/autonomy/worktree-manager.js +0 -250
  218. package/src/intelligence/decision-engine.js +0 -256
  219. package/src/intelligence/document-sharder.js +0 -221
  220. package/src/intelligence/elicitation.js +0 -265
@@ -0,0 +1,85 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * CanvasProvider — the ONLY <Canvas> mount point in the app.
5
+ *
6
+ * Design invariants (ADR-3D-01 through ADR-3D-04):
7
+ *
8
+ * 1) Client-only mount. The module is imported through a
9
+ * next/dynamic({ ssr: false }) wrapper at the page boundary, so
10
+ * Three.js / R3F code never executes on the server.
11
+ *
12
+ * 2) Single RAF. <Canvas frameloop="demand"> disables R3F's default
13
+ * always-on 60FPS loop. Rendering happens ONLY when a component
14
+ * calls invalidate() — InvalidateOnScroll (see sibling file) ties
15
+ * that to the Lenis/GSAP master tick established by motion-premium's
16
+ * ADR-LENIS-GSAP. Zero competing RAFs across the whole app.
17
+ *
18
+ * 3) WebGL gate. If WebGLContext reports unavailable (no WebGL OR
19
+ * prefers-reduced-motion), the Canvas never mounts and the caller's
20
+ * <fallback> prop renders in its place.
21
+ *
22
+ * 4) Responsible defaults for premium sites:
23
+ * dpr: [1, 2] — Retina-aware without pathological 3x scaling
24
+ * gl.antialias: true — crisp lines matter at scroll speeds
25
+ * gl.alpha: true — canvas blends with page background
26
+ * gl.powerPreference: 'high-performance'
27
+ *
28
+ * Usage:
29
+ * <CanvasProvider fallback={<SceneFallback fullBleed />}>
30
+ * <CameraRig />
31
+ * <MyScene />
32
+ * </CanvasProvider>
33
+ */
34
+
35
+ import { Canvas } from '@react-three/fiber';
36
+ import type { ReactNode } from 'react';
37
+ import { useWebGLReady } from '@/components/webgl/WebGLContext';
38
+
39
+ export interface CanvasProviderProps {
40
+ children: ReactNode;
41
+ fallback: ReactNode;
42
+ /** Override the DPR range. Default [1, 2] — Retina-aware. */
43
+ dpr?: [number, number];
44
+ /** Additional className merged onto the <Canvas> host. */
45
+ className?: string;
46
+ /** Mount a DOM wrapper around the Canvas (useful for pinning, absolute positioning). */
47
+ wrapperClassName?: string;
48
+ }
49
+
50
+ export function CanvasProvider({
51
+ children,
52
+ fallback,
53
+ dpr = [1, 2],
54
+ className = '',
55
+ wrapperClassName = '',
56
+ }: CanvasProviderProps) {
57
+ const ready = useWebGLReady();
58
+
59
+ if (!ready) {
60
+ // Fallback path — no Canvas, no R3F tree, no bundle cost beyond the
61
+ // detector itself. The caller supplies a SceneFallback so the shape
62
+ // of the viewport stays stable.
63
+ return <>{fallback}</>;
64
+ }
65
+
66
+ return (
67
+ <div className={`relative ${wrapperClassName}`.trim()} data-canvas-host>
68
+ <Canvas
69
+ className={className}
70
+ frameloop="demand"
71
+ dpr={dpr}
72
+ gl={{
73
+ antialias: true,
74
+ alpha: true,
75
+ powerPreference: 'high-performance',
76
+ // stencil/depth defaults are fine for scroll-choreography use
77
+ // cases; advanced effects (post-processing) can request overrides
78
+ // via <Canvas> prop drilling in user code.
79
+ }}
80
+ >
81
+ {children}
82
+ </Canvas>
83
+ </div>
84
+ );
85
+ }
@@ -0,0 +1,51 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * InvalidateOnScroll — ties R3F's demand-mode rendering to the Lenis
5
+ * smooth-scroll master tick (ADR-3D-03, ADR-LENIS-GSAP).
6
+ *
7
+ * Background: motion-premium establishes a single RAF loop where Lenis
8
+ * drives gsap.ticker via `gsap.ticker.add((t) => lenis.raf(t * 1000))`.
9
+ * The 2D side (ScrollTrigger, page transitions) piggybacks on this.
10
+ * The 3D side must NOT start a second RAF — doing so fragments the
11
+ * frame budget and drops FPS on 120Hz displays.
12
+ *
13
+ * This component mounts INSIDE <CanvasProvider> so it has access to
14
+ * R3F's invalidate() function. On every GSAP tick, it calls invalidate().
15
+ * R3F then renders exactly ONE frame, but only if any demand-mode
16
+ * signal (scroll delta, component invalidation, tween update) was
17
+ * raised since the last render.
18
+ *
19
+ * The net effect:
20
+ * - Idle: zero renders (no scroll, no animation → no invalidate).
21
+ * - Scroll-in-progress: one render per Lenis tick (≈16ms on 60Hz,
22
+ * ≈8ms on 120Hz), matched to the 2D motion tick exactly.
23
+ * - Non-scroll animation (hover effects, looping meshes): components
24
+ * call useFrame, which R3F also honours under frameloop="demand"
25
+ * as long as they call invalidate() explicitly.
26
+ *
27
+ * Must be rendered INSIDE a <Canvas>. useThree() will throw outside.
28
+ */
29
+
30
+ import { useEffect } from 'react';
31
+ import { useThree } from '@react-three/fiber';
32
+ import { gsap } from 'gsap';
33
+
34
+ export function InvalidateOnScroll() {
35
+ const invalidate = useThree((state) => state.invalidate);
36
+
37
+ useEffect(() => {
38
+ // gsap.ticker.add returns the handler itself for unregistration.
39
+ const tick = () => {
40
+ invalidate();
41
+ };
42
+ gsap.ticker.add(tick);
43
+
44
+ return () => {
45
+ gsap.ticker.remove(tick);
46
+ };
47
+ }, [invalidate]);
48
+
49
+ // Renders nothing — this is a RAF-coordination concern, not a scene node.
50
+ return null;
51
+ }
@@ -0,0 +1,79 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * MeshCrossfade — per-mesh variant of ScrollCrossfade. Use when a single
5
+ * object should morph through a sequence of states (different geometry,
6
+ * material, or texture) as scroll advances, without rebuilding the
7
+ * scene graph.
8
+ *
9
+ * Differs from ScrollCrossfade:
10
+ * - ScrollCrossfade: multiple independent scene groups, pick one.
11
+ * - MeshCrossfade: single mesh slot, swap its contents — no GPU
12
+ * cost for inactive variants because unselected
13
+ * meshes are not instantiated.
14
+ *
15
+ * The active variant is determined by nearest `at` position to the
16
+ * current scroll progress. Hysteresis is a small threshold (default
17
+ * 2 percent of the pinned range) that prevents rapid flip-flop near
18
+ * segment boundaries.
19
+ *
20
+ * Usage:
21
+ * <MeshCrossfade
22
+ * variants={[
23
+ * { at: 0.0, render: () => <BoxMesh /> },
24
+ * { at: 0.5, render: () => <SphereMesh /> },
25
+ * { at: 1.0, render: () => <TorusMesh /> },
26
+ * ]}
27
+ * />
28
+ */
29
+
30
+ import { useContext, useMemo, useRef, useState } from 'react';
31
+ import { useFrame } from '@react-three/fiber';
32
+ import { ScrollSceneContext } from './ScrollScene';
33
+
34
+ export interface MeshVariant {
35
+ /** Progress position (0..1) where this variant is dominant. */
36
+ at: number;
37
+ /** Render function for the variant. Returning null skips rendering. */
38
+ render: () => JSX.Element | null;
39
+ }
40
+
41
+ export interface MeshCrossfadeProps {
42
+ variants: MeshVariant[];
43
+ /** Hysteresis band width around segment boundaries (0..1). Default 0.02. */
44
+ hysteresis?: number;
45
+ }
46
+
47
+ export function MeshCrossfade({ variants, hysteresis = 0.02 }: MeshCrossfadeProps) {
48
+ const progressRef = useContext(ScrollSceneContext);
49
+ const sorted = useMemo(() => [...variants].sort((a, b) => a.at - b.at), [variants]);
50
+ const [activeIdx, setActiveIdx] = useState(0);
51
+ const activeIdxRef = useRef(0);
52
+ activeIdxRef.current = activeIdx;
53
+
54
+ useFrame(() => {
55
+ if (!progressRef || sorted.length === 0) return;
56
+ const t = Math.max(0, Math.min(1, progressRef.current.progress));
57
+
58
+ // Find the variant whose `at` is closest to t.
59
+ let best = 0;
60
+ let bestDist = Math.abs(sorted[0].at - t);
61
+ for (let i = 1; i < sorted.length; i++) {
62
+ const d = Math.abs(sorted[i].at - t);
63
+ if (d < bestDist) { best = i; bestDist = d; }
64
+ }
65
+
66
+ // Hysteresis: only switch if the new best beats the current by more
67
+ // than `hysteresis`. Without this, positions near a boundary flip
68
+ // between variants on every frame.
69
+ if (best !== activeIdxRef.current) {
70
+ const currentDist = Math.abs(sorted[activeIdxRef.current].at - t);
71
+ if (currentDist - bestDist > hysteresis) {
72
+ setActiveIdx(best);
73
+ }
74
+ }
75
+ });
76
+
77
+ const active = sorted[activeIdx];
78
+ return active ? active.render() : null;
79
+ }
@@ -0,0 +1,88 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * ScrollCrossfade — fades between an ordered sequence of scene "stops"
5
+ * as scroll progress advances. The 3D analogue of motion-premium's 2D
6
+ * BackgroundCrossfadeProvider.
7
+ *
8
+ * Mounts inside <CanvasProvider> + <ScrollSceneContext>. Each stop is
9
+ * a React node (typically a <group> containing meshes). Stops whose
10
+ * index corresponds to the current progress segment animate their
11
+ * material opacity in; neighbour stops animate out; distant stops stay
12
+ * fully transparent.
13
+ *
14
+ * Internal material handling: the component sets `visible` false on
15
+ * groups whose opacity is effectively zero so the GPU skips them
16
+ * entirely.
17
+ *
18
+ * Usage:
19
+ * <ScrollCrossfade stops={[
20
+ * <HeroScene key="hero" />,
21
+ * <ZoomScene key="zoom" />,
22
+ * <OutroScene key="outro" />,
23
+ * ]} />
24
+ *
25
+ * The number of stops determines the segment boundaries evenly across
26
+ * the pinned progress range. For uneven spacing, supply the optional
27
+ * `at` array (same length) with 0..1 positions.
28
+ */
29
+
30
+ import { Children, useContext, useMemo, useRef, type ReactNode } from 'react';
31
+ import { useFrame } from '@react-three/fiber';
32
+ import { Group } from 'three';
33
+ import { ScrollSceneContext } from './ScrollScene';
34
+
35
+ export interface ScrollCrossfadeProps {
36
+ stops: ReactNode[];
37
+ /** Optional progress positions per stop; defaults to evenly spaced. */
38
+ at?: number[];
39
+ /** How wide the crossfade band is (0..1). Default 0.15 = 15 percent of the pinned range. */
40
+ fade?: number;
41
+ }
42
+
43
+ const MIN_VISIBLE = 0.01;
44
+
45
+ export function ScrollCrossfade({ stops, at, fade = 0.15 }: ScrollCrossfadeProps) {
46
+ const progressRef = useContext(ScrollSceneContext);
47
+ const groupRefs = useRef<(Group | null)[]>([]);
48
+
49
+ const positions = useMemo(() => {
50
+ if (at && at.length === stops.length) return at;
51
+ if (stops.length === 1) return [0.5];
52
+ return stops.map((_, i) => i / (stops.length - 1));
53
+ }, [at, stops]);
54
+
55
+ useFrame(() => {
56
+ if (!progressRef) return;
57
+ const t = Math.max(0, Math.min(1, progressRef.current.progress));
58
+
59
+ for (let i = 0; i < stops.length; i++) {
60
+ const group = groupRefs.current[i];
61
+ if (!group) continue;
62
+ const distance = Math.abs(t - positions[i]);
63
+ const opacity = Math.max(0, 1 - distance / fade);
64
+ group.visible = opacity > MIN_VISIBLE;
65
+
66
+ // Cascade opacity into any material that has it. This is a
67
+ // best-effort pass — authors who need per-material control can
68
+ // skip this component and drive opacity from their own useFrame.
69
+ group.traverse((obj) => {
70
+ const mat = (obj as unknown as { material?: { opacity?: number; transparent?: boolean } }).material;
71
+ if (mat && typeof mat.opacity === 'number') {
72
+ mat.opacity = opacity;
73
+ mat.transparent = true;
74
+ }
75
+ });
76
+ }
77
+ });
78
+
79
+ return (
80
+ <>
81
+ {Children.map(stops, (child, i) => (
82
+ <group ref={(el) => { groupRefs.current[i] = el; }}>
83
+ {child}
84
+ </group>
85
+ ))}
86
+ </>
87
+ );
88
+ }
@@ -0,0 +1,121 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * ScrollScene — a pinned R3F scene whose animation progress is driven
5
+ * by GSAP ScrollTrigger (the same engine that drives the 2D side of
6
+ * motion-premium). This keeps the 3D scene on the single-RAF contract
7
+ * (ADR-3D-03) and lets scenes share the same scroll source of truth as
8
+ * pinned 2D sections.
9
+ *
10
+ * Pattern:
11
+ * 1. User wraps a section of the page in <ScrollScene>. The component
12
+ * creates a ScrollTrigger pinning that section for `length` of
13
+ * viewport height.
14
+ * 2. ScrollTrigger's `onUpdate` writes the 0..1 progress into a ref
15
+ * exposed through ScrollSceneContext.
16
+ * 3. Descendants (CameraRig, ScrollCrossfade, MeshCrossfade) read the
17
+ * ref via useFrame on every R3F render (which InvalidateOnScroll
18
+ * triggers once per GSAP tick).
19
+ *
20
+ * This design keeps R3F decoupled from any extra state library —
21
+ * progress lives in a plain ref. React re-renders are NOT used to
22
+ * propagate progress (that would tank framerate); only three-js scene
23
+ * mutations happen inside useFrame.
24
+ *
25
+ * Consumers must mount this OUTSIDE <Canvas> (it lays out a scrolling
26
+ * section). The scene children it yields via `renderScene` mount INSIDE
27
+ * the caller's CanvasProvider.
28
+ *
29
+ * Usage:
30
+ * const sceneRef = useRef<ScrollSceneRef>(null);
31
+ * return (
32
+ * <>
33
+ * <ScrollScene ref={sceneRef} length={3}>
34
+ * <Container>...overlay HTML...</Container>
35
+ * </ScrollScene>
36
+ * <CanvasProvider fallback={<SceneFallback fullBleed />}>
37
+ * <InvalidateOnScroll />
38
+ * <ScrollSceneContext.Provider value={sceneRef.current?.progress}>
39
+ * <CameraRig />
40
+ * <MyMesh />
41
+ * </ScrollSceneContext.Provider>
42
+ * </CanvasProvider>
43
+ * </>
44
+ * );
45
+ */
46
+
47
+ import { createContext, forwardRef, useEffect, useImperativeHandle, useRef, type ReactNode, type MutableRefObject } from 'react';
48
+ import { gsap } from 'gsap';
49
+ import { ScrollTrigger } from 'gsap/ScrollTrigger';
50
+
51
+ export type ScrollPhase = 'before' | 'entering' | 'pinned' | 'exiting' | 'after';
52
+
53
+ export interface ScrollProgressRef {
54
+ /** 0..1 across the pinned range. */
55
+ progress: number;
56
+ /** Coarse lifecycle phase for gating sub-animations. */
57
+ phase: ScrollPhase;
58
+ }
59
+
60
+ export interface ScrollSceneRef {
61
+ /** Ref object R3F components read during useFrame. */
62
+ progress: MutableRefObject<ScrollProgressRef>;
63
+ }
64
+
65
+ export interface ScrollSceneProps {
66
+ /** Pin length as a multiple of viewport height (e.g. 3 = pin for 3 viewports of scroll). */
67
+ length?: number;
68
+ /** Extra scrub smoothing (0 = linear, >0 = eased). */
69
+ scrub?: number | boolean;
70
+ /** Additional className for the outer section. */
71
+ className?: string;
72
+ /** Overlay HTML (anything non-3D that should scroll-pin with the canvas). */
73
+ children?: ReactNode;
74
+ }
75
+
76
+ /** Context read by scene-children under CanvasProvider. Null until ScrollScene mounts. */
77
+ export const ScrollSceneContext = createContext<MutableRefObject<ScrollProgressRef> | null>(null);
78
+
79
+ export const ScrollScene = forwardRef<ScrollSceneRef, ScrollSceneProps>(function ScrollScene(
80
+ { length = 2, scrub = true, className = '', children },
81
+ ref,
82
+ ) {
83
+ const sectionRef = useRef<HTMLElement | null>(null);
84
+ const progressRef = useRef<ScrollProgressRef>({ progress: 0, phase: 'before' });
85
+
86
+ useImperativeHandle(ref, () => ({ progress: progressRef }), []);
87
+
88
+ useEffect(() => {
89
+ if (!sectionRef.current) return;
90
+ gsap.registerPlugin(ScrollTrigger);
91
+
92
+ const trigger = ScrollTrigger.create({
93
+ trigger: sectionRef.current,
94
+ start: 'top top',
95
+ end: `+=${length * 100}%`,
96
+ pin: true,
97
+ scrub,
98
+ onEnter: () => { progressRef.current.phase = 'entering'; },
99
+ onLeave: () => { progressRef.current.phase = 'after'; },
100
+ onEnterBack: () => { progressRef.current.phase = 'entering'; },
101
+ onLeaveBack: () => { progressRef.current.phase = 'before'; },
102
+ onUpdate: (self) => {
103
+ progressRef.current.progress = self.progress;
104
+ if (self.progress > 0 && self.progress < 1) progressRef.current.phase = 'pinned';
105
+ else if (self.progress >= 1) progressRef.current.phase = 'exiting';
106
+ },
107
+ });
108
+
109
+ return () => { trigger.kill(); };
110
+ }, [length, scrub]);
111
+
112
+ return (
113
+ <section
114
+ ref={sectionRef}
115
+ className={`relative h-svh w-full ${className}`.trim()}
116
+ data-scroll-scene
117
+ >
118
+ {children}
119
+ </section>
120
+ );
121
+ });
@@ -0,0 +1,65 @@
1
+ /**
2
+ * SceneFallback — rendered when WebGL is unavailable or the user asked
3
+ * for reduced motion.
4
+ *
5
+ * The design intent (ADR-3D-05, ADR-3D-08): users who cannot see a
6
+ * canvas should see a considered static design, not a broken-looking
7
+ * blank area. The fallback reads like an artistic still — a single
8
+ * gradient background with optional children (headline, logo, CTA)
9
+ * that still honour the brand palette.
10
+ *
11
+ * Props:
12
+ * children — optional foreground content (headline, CTA)
13
+ * className — extra Tailwind classes to merge
14
+ * fullBleed — when true, fills viewport (use for heroes)
15
+ * accessibleLabel — passed to role="img" aria-label. When the scene
16
+ * is decorative, set to '' and add aria-hidden instead.
17
+ *
18
+ * Usage:
19
+ * <SceneFallback fullBleed accessibleLabel="Product showcase backdrop">
20
+ * <h1 className="text-display-lg">Welcome</h1>
21
+ * </SceneFallback>
22
+ */
23
+
24
+ import type { ReactNode } from 'react';
25
+
26
+ export interface SceneFallbackProps {
27
+ children?: ReactNode;
28
+ className?: string;
29
+ fullBleed?: boolean;
30
+ accessibleLabel?: string;
31
+ }
32
+
33
+ export function SceneFallback({
34
+ children,
35
+ className = '',
36
+ fullBleed = false,
37
+ accessibleLabel = '',
38
+ }: SceneFallbackProps) {
39
+ const baseClass = fullBleed
40
+ ? 'relative w-screen h-svh overflow-hidden'
41
+ : 'relative w-full h-full min-h-[60vh] overflow-hidden';
42
+
43
+ // Brand palette placeholder — resolved at scaffold apply from brand.ts.
44
+ // The gradient runs from deep to primary for a cinematic-stills feel
45
+ // that echoes the WebGL scene it replaces.
46
+ const gradient = 'bg-[radial-gradient(ellipse_at_center,{{PRIMARY_HEX}}_0%,{{BG_DEEP_HEX}}_70%)]';
47
+
48
+ const a11y = accessibleLabel
49
+ ? { role: 'img', 'aria-label': accessibleLabel }
50
+ : { 'aria-hidden': true };
51
+
52
+ return (
53
+ <div
54
+ className={`${baseClass} ${gradient} ${className}`.trim()}
55
+ {...a11y}
56
+ data-scene-fallback
57
+ >
58
+ {children && (
59
+ <div className="relative z-10 h-full w-full flex items-center justify-center">
60
+ {children}
61
+ </div>
62
+ )}
63
+ </div>
64
+ );
65
+ }
@@ -0,0 +1,68 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * WebGLContext — React context that exposes runtime WebGL availability.
5
+ *
6
+ * Pattern (ADR-3D-05): the detect.ts probe runs ONCE on mount. The
7
+ * result lives in context so every 3D template checks the same source
8
+ * of truth without re-probing. Downstream <ScrollScene> and
9
+ * <CanvasProvider> short-circuit to <SceneFallback> when availability
10
+ * is false — no <Canvas> ever mounts, no Three.js bundle pays its
11
+ * cost on devices that cannot render it.
12
+ *
13
+ * State shape:
14
+ * status: 'pending' | 'available' | 'unavailable'
15
+ * reason: 'no-webgl' | 'reduced-motion' | 'no-window' | null
16
+ *
17
+ * 'pending' allows the first render to paint a static fallback; the
18
+ * effect then updates the context. This avoids the "canvas flashes for
19
+ * one frame before falling back" hydration bug.
20
+ */
21
+
22
+ import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
23
+ import { detectWebGL, type WebGLAvailability } from '@/lib/webgl/detect';
24
+
25
+ export interface WebGLContextValue {
26
+ status: 'pending' | 'available' | 'unavailable';
27
+ reason: WebGLAvailability extends { reason: infer R } ? R | null : null;
28
+ webglVersion: 1 | 2 | null;
29
+ }
30
+
31
+ const DEFAULT_VALUE: WebGLContextValue = {
32
+ status: 'pending',
33
+ reason: null,
34
+ webglVersion: null,
35
+ };
36
+
37
+ const WebGLContext = createContext<WebGLContextValue>(DEFAULT_VALUE);
38
+
39
+ export function WebGLProvider({ children }: { children: ReactNode }) {
40
+ const [value, setValue] = useState<WebGLContextValue>(DEFAULT_VALUE);
41
+
42
+ useEffect(() => {
43
+ const result = detectWebGL();
44
+ if (result.available) {
45
+ setValue({ status: 'available', reason: null, webglVersion: result.version });
46
+ } else {
47
+ setValue({ status: 'unavailable', reason: result.reason, webglVersion: null });
48
+ }
49
+ }, []);
50
+
51
+ return <WebGLContext.Provider value={value}>{children}</WebGLContext.Provider>;
52
+ }
53
+
54
+ /**
55
+ * Hook — returns the full context value. Prefer `useWebGLReady()` when
56
+ * you only need the boolean.
57
+ */
58
+ export function useWebGLContext(): WebGLContextValue {
59
+ return useContext(WebGLContext);
60
+ }
61
+
62
+ /**
63
+ * Hook — boolean "may we mount a Canvas now?". Returns false on pending
64
+ * so the first render is always safe (fallback renders, then upgrades).
65
+ */
66
+ export function useWebGLReady(): boolean {
67
+ return useContext(WebGLContext).status === 'available';
68
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * WebGL availability detection — client-only.
3
+ *
4
+ * Runs a one-shot probe: try to acquire a WebGL2 context, fall back to
5
+ * WebGL1, return null when neither is available (Safari ITP with
6
+ * strict mode, corporate browser lockdowns, very old hardware).
7
+ *
8
+ * ADR-3D-05: treat reduced-motion as WebGL-off from a UX perspective.
9
+ * A user who has asked for reduced motion does not want a scroll-driven
10
+ * 3D scene — even if WebGL is technically available. Returning false in
11
+ * this case lets downstream components render the SceneFallback without
12
+ * a second check.
13
+ *
14
+ * ADR-3D-08 coordinates with the hook at the component layer: the
15
+ * canvas provider calls detect() once and stores the result in a React
16
+ * context (see WebGLContext.tsx).
17
+ *
18
+ * Pure on the browser: does NOT render, mount, or dispatch events.
19
+ * Safe to call inside useEffect.
20
+ */
21
+
22
+ export type WebGLAvailability =
23
+ | { available: true; version: 2 | 1 }
24
+ | { available: false; reason: 'no-webgl' | 'reduced-motion' | 'no-window' };
25
+
26
+ export function detectWebGL(): WebGLAvailability {
27
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
28
+ // SSR / Node: pretend WebGL is unavailable so client-only mounts handle it.
29
+ return { available: false, reason: 'no-window' };
30
+ }
31
+
32
+ // Reduced-motion opt-out comes first — cheap, and a pref miss saves us
33
+ // from ever probing a context we will not use.
34
+ if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {
35
+ return { available: false, reason: 'reduced-motion' };
36
+ }
37
+
38
+ let canvas: HTMLCanvasElement | null = null;
39
+ try {
40
+ canvas = document.createElement('canvas');
41
+ const gl2 = canvas.getContext('webgl2');
42
+ if (gl2) {
43
+ // Best-effort cleanup so we do not leak the probe context.
44
+ (gl2 as WebGL2RenderingContext).getExtension('WEBGL_lose_context')?.loseContext();
45
+ return { available: true, version: 2 };
46
+ }
47
+ const gl1 = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
48
+ if (gl1) {
49
+ (gl1 as WebGLRenderingContext).getExtension('WEBGL_lose_context')?.loseContext();
50
+ return { available: true, version: 1 };
51
+ }
52
+ return { available: false, reason: 'no-webgl' };
53
+ } catch {
54
+ return { available: false, reason: 'no-webgl' };
55
+ } finally {
56
+ if (canvas) {
57
+ // Drop the probe node.
58
+ canvas.width = 0;
59
+ canvas.height = 0;
60
+ }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Shorthand boolean — use when caller only cares if Canvas should mount.
66
+ */
67
+ export function isWebGLAvailable(): boolean {
68
+ return detectWebGL().available;
69
+ }