chati-dev 4.2.2 → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/README.md +80 -53
  2. package/bin/chati.js +170 -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 +158 -22
  6. package/framework/agents/discover/brownfield-wu.md +26 -14
  7. package/framework/agents/discover/greenfield-wu.md +102 -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 +38 -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 +448 -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 +441 -0
  59. package/framework/orchestrator/chati-update.md +40 -40
  60. package/framework/orchestrator/chati.md +295 -94
  61. package/framework/rules/human-writing-style.md +47 -0
  62. package/framework/scaffold/motion-premium/README.md +99 -0
  63. package/framework/scaffold/motion-premium/app/globals.css.template +400 -0
  64. package/framework/scaffold/motion-premium/app/layout.tsx.template +110 -0
  65. package/framework/scaffold/motion-premium/components/animation/BackgroundCrossfadeProvider.tsx.template +170 -0
  66. package/framework/scaffold/motion-premium/components/animation/LenisProvider.tsx.template +49 -0
  67. package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template +83 -0
  68. package/framework/scaffold/motion-premium/components/animation/Preloader.tsx.template +171 -0
  69. package/framework/scaffold/motion-premium/components/ui/Container.tsx.template +69 -0
  70. package/framework/scaffold/motion-premium/components/ui/PageSection.tsx.template +74 -0
  71. package/framework/scaffold/motion-premium/lib/animations/gsap.ts.template +112 -0
  72. package/framework/scaffold/motion-premium/lib/animations/refreshCoordinator.ts.template +75 -0
  73. package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +119 -0
  74. package/framework/scaffold/motion-premium/lib/animations/useGsapContext.ts.template +92 -0
  75. package/framework/scaffold/motion-premium/lib/animations/useScrollSnapStepper.ts.template +265 -0
  76. package/framework/scaffold/motion-premium/lib/animations/useSmoothScroll.ts.template +67 -0
  77. package/framework/scaffold/motion-premium/lib/brand.ts.template +43 -0
  78. package/framework/scaffold/motion-premium/scaffold.yaml +178 -0
  79. package/framework/scaffold/motion-premium-3d/README.md +90 -0
  80. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template +81 -0
  81. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template +75 -0
  82. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/page.tsx.template +26 -0
  83. package/framework/scaffold/motion-premium-3d/components/3d/CameraRig.tsx.template +100 -0
  84. package/framework/scaffold/motion-premium-3d/components/3d/CanvasProvider.tsx.template +85 -0
  85. package/framework/scaffold/motion-premium-3d/components/3d/InvalidateOnScroll.tsx.template +51 -0
  86. package/framework/scaffold/motion-premium-3d/components/3d/MeshCrossfade.tsx.template +79 -0
  87. package/framework/scaffold/motion-premium-3d/components/3d/ScrollCrossfade.tsx.template +88 -0
  88. package/framework/scaffold/motion-premium-3d/components/3d/ScrollScene.tsx.template +121 -0
  89. package/framework/scaffold/motion-premium-3d/components/webgl/SceneFallback.tsx.template +65 -0
  90. package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +68 -0
  91. package/framework/scaffold/motion-premium-3d/lib/webgl/detect.ts.template +69 -0
  92. package/framework/scaffold/motion-premium-3d/scaffold.yaml +136 -0
  93. package/framework/schemas/session.schema.json +109 -21
  94. package/framework/scripts/reference-capture.js +430 -0
  95. package/framework/scripts/visual-qa.js +686 -0
  96. package/framework/tasks/orchestrator-handoff.md +1 -1
  97. package/framework/tasks/orchestrator-resume.md +1 -1
  98. package/framework/tasks/orchestrator-route.md +1 -1
  99. package/framework/tasks/orchestrator-status.md +3 -3
  100. package/framework/tasks/qa-planning-gate-define.md +1 -1
  101. package/framework/templates/brandbook-html-tmpl.md +2 -2
  102. package/framework/templates/brandbook-tmpl.yaml +1 -1
  103. package/framework/templates/component-spec-tmpl.yaml +1 -1
  104. package/framework/templates/design-token-tmpl.yaml +1 -1
  105. package/framework/templates/icon-system-tmpl.yaml +1 -1
  106. package/framework/templates/team-planning-tasks.yaml +6 -5
  107. package/framework/workflows/brownfield-discovery.yaml +2 -2
  108. package/framework/workflows/brownfield-fullstack.yaml +15 -11
  109. package/framework/workflows/brownfield-service.yaml +14 -10
  110. package/framework/workflows/brownfield-ui.yaml +15 -11
  111. package/framework/workflows/greenfield-fullstack.yaml +16 -13
  112. package/framework/workflows/quick-flow.yaml +3 -3
  113. package/framework/workflows/standard-flow.yaml +12 -9
  114. package/package.json +12 -6
  115. package/src/autonomy/autonomous-gate.js +1 -0
  116. package/src/autonomy/build-state.js +1 -2
  117. package/src/autonomy/progress-reporter.js +1 -1
  118. package/src/config/agent-customizer.js +11 -3
  119. package/src/config/claude-settings-generator.js +27 -7
  120. package/src/config/context-file-generator.js +41 -21
  121. package/src/config/framework-adapter.js +1 -0
  122. package/src/config/gemini-hooks-generator.js +19 -7
  123. package/src/config/mcp-configs.js +1 -0
  124. package/src/context/layers/l1-global.js +2 -1
  125. package/src/dashboard/data-reader.js +4 -3
  126. package/src/dashboard/layout.js +2 -1
  127. package/src/decision/analyzer.js +6 -30
  128. package/src/decision/engine.js +4 -28
  129. package/src/decision/registry-healer.js +3 -2
  130. package/src/decision/registry-updater.js +23 -14
  131. package/src/extensions/loader.js +2 -8
  132. package/src/gates/g1-planning-complete.js +2 -1
  133. package/src/gates/g2-qa-planning.js +2 -1
  134. package/src/gates/g3-implementation.js +2 -1
  135. package/src/gates/g4-qa-implementation.js +3 -2
  136. package/src/gates/g5-deploy-ready.js +2 -1
  137. package/src/health/engine.js +4 -3
  138. package/src/installer/core.js +431 -81
  139. package/src/installer/preflight.js +131 -0
  140. package/src/installer/provider-overlay.js +3 -3
  141. package/src/installer/scaffold-applier.js +358 -0
  142. package/src/installer/templates.js +46 -29
  143. package/src/installer/validator.js +17 -12
  144. package/src/intelligence/registry-manager.js +22 -29
  145. package/src/intelligence/timeline.js +11 -6
  146. package/src/license/commands.js +1 -1
  147. package/src/license/wait.js +102 -0
  148. package/src/memory/agent-memory.js +81 -0
  149. package/src/memory/dream.js +32 -1
  150. package/src/merger/replace-merger.js +28 -15
  151. package/src/orchestrator/agent-selector.js +2 -1
  152. package/src/orchestrator/cli.js +1880 -71
  153. package/src/orchestrator/doctor.js +270 -0
  154. package/src/orchestrator/handoff-engine.js +4 -3
  155. package/src/orchestrator/index.js +2 -0
  156. package/src/orchestrator/pipeline-manager.js +306 -15
  157. package/src/orchestrator/session-manager.js +339 -6
  158. package/src/tasks/handoff.js +3 -2
  159. package/src/telemetry/config.js +4 -3
  160. package/src/telemetry/schema.js +1 -0
  161. package/src/terminal/collector.js +3 -2
  162. package/src/terminal/index.js +1 -2
  163. package/src/terminal/isolation.js +52 -18
  164. package/src/terminal/prompt-builder.js +42 -25
  165. package/src/terminal/run-parallel.js +1 -1
  166. package/src/terminal/run-team.js +3 -3
  167. package/src/terminal/team-task-list.js +43 -4
  168. package/src/upgrade/backup.js +3 -2
  169. package/src/upgrade/checker.js +3 -2
  170. package/src/upgrade/migrator.js +65 -7
  171. package/src/upgrade/tracked-files-detector.js +86 -0
  172. package/src/upgrade/user-messages.js +94 -0
  173. package/src/utils/config-parser.js +2 -1
  174. package/src/utils/feature-flags.js +2 -1
  175. package/src/utils/flatten-entities.js +69 -0
  176. package/src/utils/framework-dir.js +16 -0
  177. package/src/utils/model-id.js +85 -0
  178. package/src/utils/provider-limits.js +84 -23
  179. package/src/utils/schema-validator.js +1 -1
  180. package/src/wizard/i18n.js +5 -4
  181. package/src/wizard/index.js +14 -0
  182. package/assets/logo - c/303/263pia.png +0 -0
  183. package/assets/logo.svg +0 -42
  184. package/assets/logo2.png +0 -0
  185. package/assets/social-preview.png +0 -0
  186. package/scripts/bundle-framework.js +0 -69
  187. package/scripts/changelog-generator.js +0 -222
  188. package/scripts/codebase-mapper.js +0 -728
  189. package/scripts/commit-message-generator.js +0 -167
  190. package/scripts/coverage-analyzer.js +0 -260
  191. package/scripts/dependency-analyzer.js +0 -280
  192. package/scripts/doctor/checks/agents.js +0 -77
  193. package/scripts/doctor/checks/constitution.js +0 -41
  194. package/scripts/doctor/checks/domain-alignment.js +0 -58
  195. package/scripts/doctor/checks/prism-layers.js +0 -84
  196. package/scripts/doctor/checks/registry.js +0 -55
  197. package/scripts/doctor/checks/schemas.js +0 -61
  198. package/scripts/doctor/fixes/reference-fix.js +0 -100
  199. package/scripts/doctor/fixes/registry-fix.js +0 -56
  200. package/scripts/doctor/index.js +0 -212
  201. package/scripts/framework-analyzer.js +0 -308
  202. package/scripts/generate-constitution-domain.js +0 -253
  203. package/scripts/generate-signing-key.js +0 -33
  204. package/scripts/health-check.js +0 -481
  205. package/scripts/ide-sync.js +0 -326
  206. package/scripts/performance-analyzer.js +0 -325
  207. package/scripts/plan-tracker.js +0 -278
  208. package/scripts/populate-entity-registry.js +0 -481
  209. package/scripts/pr-review.js +0 -317
  210. package/scripts/rollback-manager.js +0 -310
  211. package/scripts/semantic-lint.js +0 -328
  212. package/scripts/sign-manifest.js +0 -53
  213. package/scripts/stuck-detector.js +0 -343
  214. package/scripts/test-quality-assessment.js +0 -257
  215. package/scripts/validate-agents.js +0 -368
  216. package/scripts/validate-package.js +0 -505
  217. package/scripts/validate-tasks.js +0 -465
  218. package/src/autonomy/worktree-manager.js +0 -250
  219. package/src/intelligence/decision-engine.js +0 -256
  220. package/src/intelligence/document-sharder.js +0 -221
  221. package/src/intelligence/elicitation.js +0 -265
@@ -0,0 +1,170 @@
1
+ /**
2
+ * components/animation/BackgroundCrossfadeProvider.tsx — scaffold motion-premium v1.0.0
3
+ *
4
+ * Scroll-driven crossfade between 6 background color stops, applied to the
5
+ * <html> element's `--scene-bg` CSS custom property. The compositor handles
6
+ * the actual paint, so we pay almost nothing per frame despite the animation
7
+ * running across the entire page height.
8
+ *
9
+ * Non-obvious choices:
10
+ *
11
+ * - Target is `document.documentElement` (<html>), NOT <body>. SAUCE PATTERN #4.
12
+ * The `--scene-bg` token cascades from <html> to <body> via globals.css's
13
+ * `body { background-color: var(--scene-bg, ...) }`. Mutating the property
14
+ * on <html> is a single CSS custom property write per frame; mutating body
15
+ * backgroundColor directly forces repaints of body + all transparent
16
+ * descendants. <html> keeps the repaint cost on the compositor layer only.
17
+ *
18
+ * - `scrub: 1` — weight-perception tuning. Lower scrub values (<1) feel
19
+ * "snappy", higher values (>1) feel "heavy/cinematic". 1 is the default
20
+ * for panel-ish crossfades; for heavier effects (horizontal pins of
21
+ * cards) prefer scrub: 2 (see useScrollSnapStepper callers).
22
+ *
23
+ * - `invalidateOnRefresh: true` — required because we anchor to
24
+ * `document.documentElement` (full page height). When ScrollTrigger
25
+ * refreshes (e.g. after a dynamic height change), start/end positions
26
+ * must be recalculated. Without invalidate, a window resize during a
27
+ * crossfade produces a jump in `--scene-bg` value.
28
+ *
29
+ * - `resolveColorVar` helper — GSAP cannot tween CSS `var(...)` references
30
+ * as colors. Defaults use concrete hex via resolution at mount time:
31
+ * `getComputedStyle(html).getPropertyValue('--color-bg-deep')`. Users
32
+ * who pass raw hex stops bypass the resolver (it is a no-op when the
33
+ * input does not start with "var(").
34
+ *
35
+ * - `inactiveColor` prop — on routes where the crossfade is inactive, we
36
+ * write a static color to `--scene-bg` so the <body> has a predictable
37
+ * background. Default: `var(--color-bg-deep)` which works across all
38
+ * dark-theme projects.
39
+ *
40
+ * - `ctx.revert()` on cleanup PLUS a 100ms refresh debounce — the debounce
41
+ * is because BackgroundCrossfadeProvider typically mounts alongside
42
+ * other ScrollTriggers on route entry. Letting them all register before
43
+ * the first refresh avoids a thrash.
44
+ */
45
+
46
+ 'use client';
47
+
48
+ import { useEffect, type ReactNode } from 'react';
49
+ import { usePathname } from 'next/navigation';
50
+ import { gsap, ScrollTrigger, prefersReducedMotion } from '@/lib/animations/gsap';
51
+
52
+ export interface ColorStop {
53
+ /** Scroll progress, 0..1. Stops should be sorted ascending. */
54
+ progress: number;
55
+ /** Color value — concrete hex preferred, or `var(--color-*)` (resolved at mount). */
56
+ color: string;
57
+ }
58
+
59
+ export interface BackgroundCrossfadeProviderProps {
60
+ children: ReactNode;
61
+ /**
62
+ * Color stops for the scroll-driven `--scene-bg` timeline. First and
63
+ * last stops SHOULD match (seamless loop on back-nav). Defaults to a
64
+ * dark-theme 6-stop curve that references --color-bg-* vars from
65
+ * globals.css (resolved to hex at mount).
66
+ */
67
+ stops?: ColorStop[];
68
+ /**
69
+ * Route path where the crossfade is active. On any other route,
70
+ * --scene-bg is set to `inactiveColor` statically. Default: '/'.
71
+ */
72
+ activeRoute?: string;
73
+ /**
74
+ * Fallback color for inactive routes. Default: 'var(--color-bg-deep)'.
75
+ */
76
+ inactiveColor?: string;
77
+ }
78
+
79
+ const DEFAULT_STOPS: ColorStop[] = [
80
+ { progress: 0, color: 'var(--color-bg-deep)' },
81
+ { progress: 0.15, color: 'var(--color-bg-deep)' },
82
+ { progress: 0.5, color: 'var(--color-bg-primary)' },
83
+ { progress: 0.7, color: 'var(--color-bg-elevated)' },
84
+ { progress: 0.9, color: 'var(--color-bg-primary)' },
85
+ { progress: 1, color: 'var(--color-bg-deep)' },
86
+ ];
87
+
88
+ /**
89
+ * Resolve `var(--token)` references to their concrete computed value.
90
+ * Returns the input unchanged if not a var() reference or if SSR.
91
+ */
92
+ function resolveColorVar(color: string): string {
93
+ if (typeof window === 'undefined') return color;
94
+ if (!color.startsWith('var(')) return color;
95
+ const match = color.match(/var\(\s*(--[\w-]+)\s*\)/);
96
+ if (!match) return color;
97
+ const resolved = getComputedStyle(document.documentElement)
98
+ .getPropertyValue(match[1])
99
+ .trim();
100
+ return resolved || color;
101
+ }
102
+
103
+ export function BackgroundCrossfadeProvider({
104
+ children,
105
+ stops = DEFAULT_STOPS,
106
+ activeRoute = '/',
107
+ inactiveColor = 'var(--color-bg-deep)',
108
+ }: BackgroundCrossfadeProviderProps) {
109
+ const pathname = usePathname();
110
+
111
+ useEffect(() => {
112
+ if (pathname !== activeRoute) {
113
+ document.documentElement.style.setProperty(
114
+ '--scene-bg',
115
+ resolveColorVar(inactiveColor),
116
+ );
117
+ return;
118
+ }
119
+
120
+ // Resolve any var(...) references to concrete colors GSAP can tween.
121
+ const resolvedStops = stops.map(s => ({
122
+ progress: s.progress,
123
+ color: resolveColorVar(s.color),
124
+ }));
125
+
126
+ if (prefersReducedMotion()) {
127
+ document.documentElement.style.setProperty('--scene-bg', resolvedStops[0].color);
128
+ return;
129
+ }
130
+
131
+ const ctx = gsap.context(() => {
132
+ const tl = gsap.timeline({
133
+ scrollTrigger: {
134
+ trigger: document.documentElement,
135
+ start: 'top top',
136
+ end: 'bottom bottom',
137
+ scrub: 1,
138
+ invalidateOnRefresh: true,
139
+ },
140
+ });
141
+
142
+ for (let i = 0; i < resolvedStops.length - 1; i++) {
143
+ const curr = resolvedStops[i];
144
+ const next = resolvedStops[i + 1];
145
+ const duration = next.progress - curr.progress;
146
+ tl.to(
147
+ document.documentElement,
148
+ {
149
+ '--scene-bg': next.color,
150
+ duration,
151
+ ease: 'none',
152
+ },
153
+ curr.progress,
154
+ );
155
+ }
156
+
157
+ gsap.set(document.documentElement, { '--scene-bg': resolvedStops[0].color });
158
+ });
159
+
160
+ // Debounced refresh — let sibling ScrollTriggers register before recomputing.
161
+ const refreshTimer = window.setTimeout(() => ScrollTrigger.refresh(), 100);
162
+
163
+ return () => {
164
+ window.clearTimeout(refreshTimer);
165
+ ctx.revert();
166
+ };
167
+ }, [pathname, stops, activeRoute, inactiveColor]);
168
+
169
+ return <>{children}</>;
170
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * components/animation/LenisProvider.tsx — scaffold motion-premium v1.0.0
3
+ *
4
+ * Top-level client component that initializes Lenis smooth scroll and
5
+ * enforces manual scroll-restoration. Must be mounted ONCE at the root of
6
+ * the component tree (see app/layout.tsx nesting order).
7
+ *
8
+ * Non-obvious choices:
9
+ *
10
+ * - Calls `useSmoothScroll()` for its side effect only; the returned ref is
11
+ * intentionally unused at this level. Children that need the Lenis
12
+ * instance should call `getLenis()` (from useScrollSnapStepper.ts) which
13
+ * reads `window.lenis` exposed by initLenis.
14
+ *
15
+ * - Second useEffect runs `scrollRestoration = 'manual'` AGAIN even though
16
+ * layout.tsx has an inline pre-hydration script that does the same.
17
+ * Redundant by design: the inline script runs pre-hydration (defends
18
+ * against SSR + back/forward cache); this hook runs post-hydration
19
+ * (defends against any React component that might have changed
20
+ * history.scrollRestoration after initial paint). Belt + suspenders.
21
+ *
22
+ * - Does not wrap children in a provider context — the Lenis instance is
23
+ * exposed via `window.lenis` (see initLenis in lib/animations/gsap.ts),
24
+ * not via React context, because ScrollTrigger, useScrollSnapStepper,
25
+ * and QA scripts all need access outside React render cycle.
26
+ */
27
+
28
+ 'use client';
29
+
30
+ import { useEffect, type ReactNode } from 'react';
31
+ import { useSmoothScroll } from '@/lib/animations/useSmoothScroll';
32
+
33
+ interface LenisProviderProps {
34
+ children: ReactNode;
35
+ }
36
+
37
+ export function LenisProvider({ children }: LenisProviderProps) {
38
+ useSmoothScroll();
39
+
40
+ useEffect(() => {
41
+ if (typeof window === 'undefined') return;
42
+ if ('scrollRestoration' in window.history) {
43
+ window.history.scrollRestoration = 'manual';
44
+ }
45
+ window.scrollTo(0, 0);
46
+ }, []);
47
+
48
+ return <>{children}</>;
49
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * components/animation/PageTransitionWrapper.tsx — scaffold motion-premium v1.0.0
3
+ *
4
+ * Wraps page content in Framer Motion's AnimatePresence for opacity fades
5
+ * on client-side route changes. The scroll-to-top runs via `onExitComplete`
6
+ * so the old page is already unmounted when the scroll jump happens —
7
+ * the user never sees the old page scroll up.
8
+ *
9
+ * Non-obvious choices:
10
+ *
11
+ * - `mode="wait"` — the exit animation of the outgoing page completes
12
+ * BEFORE the incoming page mounts. Without this, both pages render
13
+ * simultaneously briefly, producing a visual layering glitch and
14
+ * doubled scroll height.
15
+ *
16
+ * - `key={pathname}` — forces AnimatePresence to treat each route as a
17
+ * distinct child, triggering exit + enter. Without the key, route
18
+ * changes appear instant because React reuses the same child.
19
+ *
20
+ * - `onExitComplete={scrollToTop}` — scroll-to-top AFTER exit finishes,
21
+ * not before. Scrolling during exit makes the exit animation jerk
22
+ * along the scroll delta. Deferring until mount point = 0 keeps the
23
+ * transition smooth.
24
+ *
25
+ * - `scrollToTop` calls BOTH `window.scrollTo(0, 0)` AND `lenis.scrollTo(0,
26
+ * { immediate: true })`. Belt + suspenders: window.scrollTo defends
27
+ * against the moment before Lenis picks up, lenis.scrollTo ensures the
28
+ * virtualized scroll position also resets. Skipping lenis.scrollTo
29
+ * causes Lenis to "remember" the old scroll position and smooth-scroll
30
+ * back to it after the transition.
31
+ *
32
+ * - EASING_CURVES.inOut as 4-tuple, not string — Framer Motion's `ease`
33
+ * prop expects a number array, not a cubic-bezier() string. Importing
34
+ * from tokens.ts's EASING_CURVES (array form) avoids the "cubic-bezier
35
+ * is not a valid ease" runtime error.
36
+ */
37
+
38
+ 'use client';
39
+
40
+ import { type ReactNode } from 'react';
41
+ import { usePathname } from 'next/navigation';
42
+ import { AnimatePresence, motion } from 'framer-motion';
43
+ import { EASING_CURVES } from '@/lib/animations/tokens';
44
+
45
+ interface LenisWindow {
46
+ lenis?: {
47
+ scrollTo?: (target: number, opts?: { immediate?: boolean }) => void;
48
+ };
49
+ }
50
+
51
+ function scrollToTop() {
52
+ if (typeof window === 'undefined') return;
53
+ window.scrollTo(0, 0);
54
+ const lenis = (window as unknown as LenisWindow).lenis;
55
+ lenis?.scrollTo?.(0, { immediate: true });
56
+ }
57
+
58
+ export interface PageTransitionWrapperProps {
59
+ children: ReactNode;
60
+ /** Fade duration in seconds. Default 0.3. */
61
+ duration?: number;
62
+ }
63
+
64
+ export function PageTransitionWrapper({
65
+ children,
66
+ duration = 0.3,
67
+ }: PageTransitionWrapperProps) {
68
+ const pathname = usePathname();
69
+
70
+ return (
71
+ <AnimatePresence mode="wait" onExitComplete={scrollToTop}>
72
+ <motion.div
73
+ key={pathname}
74
+ initial={{ opacity: 0 }}
75
+ animate={{ opacity: 1 }}
76
+ exit={{ opacity: 0 }}
77
+ transition={{ duration, ease: EASING_CURVES.inOut }}
78
+ >
79
+ {children}
80
+ </motion.div>
81
+ </AnimatePresence>
82
+ );
83
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * components/animation/Preloader.tsx — scaffold motion-premium v1.0.0
3
+ *
4
+ * Fullscreen overlay preloader with RAF-driven progress, configurable
5
+ * readiness gate, and graceful fade-out. Body scroll is locked while
6
+ * visible.
7
+ *
8
+ * Non-obvious choices:
9
+ *
10
+ * - MIN_DURATION + MAX_DURATION dual gate — `minDuration` prevents a flash
11
+ * of preloader (if the page is already fully loaded from cache, the
12
+ * preloader would appear for 1 frame and disappear — worse than no
13
+ * preloader). `maxDuration` prevents an infinite hang when isReady()
14
+ * never returns true (e.g. broken video element). The pair guarantees
15
+ * the preloader is visible between minDuration and maxDuration ms.
16
+ *
17
+ * - Progress capped at 96% until finish — finishing to 100 only when
18
+ * actually done. Without the cap, the bar would reach 100% before the
19
+ * page is ready, producing a "stuck at 100" feeling.
20
+ *
21
+ * - `videoSelector` opt-in prop — heroes with large video backgrounds are
22
+ * the common bottleneck. Passing a selector gates readiness on
23
+ * `video.readyState >= 3` (HAVE_FUTURE_DATA, enough buffered to start)
24
+ * AND the video being visible (opacity >= 0.99, so it has faded in).
25
+ * When no selector provided, we fall back to document.readyState only.
26
+ *
27
+ * - `is-preloading` class on <html> — provided as a CSS hook so user
28
+ * styles can react to the preload state (e.g. hide hero content until
29
+ * preloader finishes to avoid FOUC). Not required by the scaffold;
30
+ * feel free to remove if unused.
31
+ *
32
+ * - `document.body.style.overflow = 'hidden'` in addition to class hook —
33
+ * belt + suspenders against users who try to scroll during preload.
34
+ * Restored in cleanup AND in finish handler (covers both unmount paths
35
+ * and normal completion).
36
+ *
37
+ * - Accessibility: `role="status"` + `aria-live="polite"` + `aria-hidden
38
+ * ={finishing}` — screen readers announce progress but are excluded
39
+ * during the fade-out so they don't re-announce "100%" as the overlay
40
+ * disappears.
41
+ */
42
+
43
+ 'use client';
44
+
45
+ import { useEffect, useState, type ReactNode } from 'react';
46
+
47
+ export interface PreloaderProps {
48
+ /**
49
+ * Custom readiness check. Return true when content is ready to reveal.
50
+ * Overridden by `videoSelector` when both are set. Default: always true
51
+ * (so preloader ends at minDuration if page is already loaded).
52
+ */
53
+ isReady?: () => boolean;
54
+ /**
55
+ * Video selector to gate readiness on `readyState >= 3` + visibility.
56
+ * Overrides `isReady` when set.
57
+ */
58
+ videoSelector?: string;
59
+ /** Min time (ms) preloader stays visible — prevents flash. Default 1400. */
60
+ minDuration?: number;
61
+ /** Max time (ms) before forced reveal. Default 5000. */
62
+ maxDuration?: number;
63
+ /** Fade-out duration (ms). Default 500. */
64
+ fadeOutMs?: number;
65
+ /** Slot content rendered above the progress bar (e.g. brand logo). */
66
+ children?: ReactNode;
67
+ /** Extra classes applied to the overlay root. */
68
+ className?: string;
69
+ }
70
+
71
+ export function Preloader({
72
+ isReady,
73
+ videoSelector,
74
+ minDuration = 1400,
75
+ maxDuration = 5000,
76
+ fadeOutMs = 500,
77
+ children,
78
+ className,
79
+ }: PreloaderProps) {
80
+ const [progress, setProgress] = useState(0);
81
+ const [finishing, setFinishing] = useState(false);
82
+ const [hidden, setHidden] = useState(false);
83
+
84
+ useEffect(() => {
85
+ if (typeof window === 'undefined') return;
86
+
87
+ document.documentElement.classList.add('is-preloading');
88
+ document.body.style.overflow = 'hidden';
89
+
90
+ let mounted = true;
91
+ let rafId = 0;
92
+ const start = performance.now();
93
+
94
+ const checkReady = () => {
95
+ if (document.readyState !== 'complete') return false;
96
+ if (videoSelector) {
97
+ const video = document.querySelector<HTMLVideoElement>(videoSelector);
98
+ if (!video) return true; // selector provided but not present — do not block
99
+ const hasFutureData = video.readyState >= 3;
100
+ const isVisible = parseFloat(getComputedStyle(video).opacity) >= 0.99;
101
+ return hasFutureData && isVisible;
102
+ }
103
+ if (isReady) return isReady();
104
+ return true;
105
+ };
106
+
107
+ const tick = () => {
108
+ if (!mounted) return;
109
+ const elapsed = performance.now() - start;
110
+ const ready = checkReady();
111
+ const finish = (elapsed >= minDuration && ready) || elapsed >= maxDuration;
112
+
113
+ if (finish) {
114
+ setProgress(100);
115
+ setFinishing(true);
116
+ window.setTimeout(() => {
117
+ if (!mounted) return;
118
+ setHidden(true);
119
+ document.documentElement.classList.remove('is-preloading');
120
+ document.body.style.overflow = '';
121
+ }, fadeOutMs);
122
+ return;
123
+ }
124
+ const pct = Math.min(96, Math.round((elapsed / maxDuration) * 100));
125
+ setProgress(pct);
126
+ rafId = window.requestAnimationFrame(tick);
127
+ };
128
+
129
+ rafId = window.requestAnimationFrame(tick);
130
+
131
+ return () => {
132
+ mounted = false;
133
+ window.cancelAnimationFrame(rafId);
134
+ document.documentElement.classList.remove('is-preloading');
135
+ document.body.style.overflow = '';
136
+ };
137
+ }, [isReady, videoSelector, minDuration, maxDuration, fadeOutMs]);
138
+
139
+ if (hidden) return null;
140
+
141
+ return (
142
+ <div
143
+ className={[
144
+ 'fixed inset-0 z-[100] flex flex-col items-center justify-center',
145
+ 'bg-bg-primary',
146
+ className ?? '',
147
+ ].filter(Boolean).join(' ')}
148
+ style={{
149
+ opacity: finishing ? 0 : 1,
150
+ transition: `opacity ${fadeOutMs}ms ease-out`,
151
+ pointerEvents: finishing ? 'none' : 'auto',
152
+ }}
153
+ aria-hidden={finishing}
154
+ role="status"
155
+ aria-live="polite"
156
+ >
157
+ {children && <div className="mb-10">{children}</div>}
158
+
159
+ <div className="relative w-[220px] h-[2px] bg-bg-elevated overflow-hidden">
160
+ <div
161
+ className="absolute inset-y-0 left-0 bg-primary transition-[width] duration-200 ease-linear"
162
+ style={{ width: `${progress}%` }}
163
+ />
164
+ </div>
165
+
166
+ <p className="typo-micro-label mt-4 text-xs text-primary tabular-nums">
167
+ {progress}%
168
+ </p>
169
+ </div>
170
+ );
171
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * components/ui/Container.tsx — scaffold motion-premium v1.0.0
3
+ *
4
+ * @contract: single authority for max-width + horizontal padding
5
+ *
6
+ * This is the ONE component in the entire app that is allowed to set
7
+ * `max-width` and horizontal padding. Every other section, hero, grid,
8
+ * flexbox, or card that needs readable content width MUST wrap its
9
+ * children in <Container> (or use <PageSection>, which wraps for you).
10
+ *
11
+ * Why the single-authority rule:
12
+ *
13
+ * - Two components setting max-width at different levels = compounding
14
+ * constraints. A 1200px Container inside a 960px card inside a 1040px
15
+ * section produces content that is 912px wide on a 1920px screen, for
16
+ * no obvious reason. This bug is invisible in 1280px viewports and
17
+ * lethal in 1920px ones — it caused multiple "why is my layout
18
+ * centered oddly?" incidents in the reference-project saga.
19
+ *
20
+ * - Tailwind's `max-w-*` utilities proliferate fast; without a single
21
+ * source, the max-widths diverge (1200, 1280, 7xl, 6xl, container).
22
+ * One authority, one width, one mental model.
23
+ *
24
+ * - Responsive horizontal padding (px-6 → px-16) is the exterior-most
25
+ * visual rhythm of the page. Letting any component set its own px on
26
+ * top of Container's defaults produces subtle double-padding that
27
+ * visual QA often misses.
28
+ *
29
+ * Default behavior:
30
+ * - max-width: 1200px (matches --container-max-width token)
31
+ * - centered: mx-auto
32
+ * - horizontal padding (mobile-first):
33
+ * base: px-6 (24px)
34
+ * sm: px-8 (32px)
35
+ * lg: px-12 (48px)
36
+ * xl: px-16 (64px)
37
+ *
38
+ * The xl: threshold benefits from the --breakpoint-xl: 75rem override in
39
+ * globals.css — laptop viewports (1200–1279px) get px-16 instead of
40
+ * being stuck at lg:px-12.
41
+ *
42
+ * Escape hatch: none, by design. If a full-bleed background is needed,
43
+ * compose with PageSection's `noContainer` prop — do NOT edit Container.
44
+ */
45
+
46
+ import { type HTMLAttributes, forwardRef } from 'react';
47
+
48
+ interface ContainerProps extends HTMLAttributes<HTMLDivElement> {
49
+ className?: string;
50
+ }
51
+
52
+ export const Container = forwardRef<HTMLDivElement, ContainerProps>(
53
+ ({ children, className, ...rest }, ref) => (
54
+ <div
55
+ ref={ref}
56
+ className={[
57
+ 'mx-auto w-full max-w-[1200px] px-6 sm:px-8 lg:px-12 xl:px-16',
58
+ className,
59
+ ]
60
+ .filter(Boolean)
61
+ .join(' ')}
62
+ {...rest}
63
+ >
64
+ {children}
65
+ </div>
66
+ ),
67
+ );
68
+
69
+ Container.displayName = 'Container';
@@ -0,0 +1,74 @@
1
+ /**
2
+ * components/ui/PageSection.tsx — scaffold motion-premium v1.0.0
3
+ *
4
+ * Full-bleed vertical wrapper that provides consistent section rhythm and
5
+ * automatically wraps children in <Container>. The opinionated partner to
6
+ * Container: every top-level page section should be a PageSection.
7
+ *
8
+ * Non-obvious choices:
9
+ *
10
+ * - Default vertical padding: `py-[clamp(64px,8vw,192px)]`.
11
+ * - Min 64px (mobile) = 4rem, readable breathing room on small screens.
12
+ * - Fluid 8vw in the middle = section rhythm grows with viewport.
13
+ * - Max 192px (desktop) = 12rem, prevents absurd whitespace on 4k.
14
+ * Using `clamp()` instead of responsive Tailwind utilities keeps this
15
+ * a single CSS declaration — no `py-16 md:py-24 lg:py-32 xl:py-48`
16
+ * cascade that forgets some breakpoint.
17
+ *
18
+ * - `noPadding` escape hatch — for sections that provide their own
19
+ * vertical rhythm (e.g. a hero pinned to 100vh). Opt-in, not default.
20
+ *
21
+ * - `noContainer` escape hatch — for sections that need full-bleed
22
+ * backgrounds (e.g. hero video, crossfaded panel) but still want the
23
+ * vertical rhythm. The user then places their own Container inside
24
+ * for constrained text, leaving the background free to span.
25
+ *
26
+ * DO NOT use `noContainer` to "bring your own max-width" — Container
27
+ * is the single authority for max-width (@contract). If you need
28
+ * more/less width somewhere, fix Container, not this escape.
29
+ *
30
+ * - Polymorphic `as` prop (section|div|article) — lets consumers pick
31
+ * the correct HTML landmark. Defaults to <section>; use <article> for
32
+ * standalone content pieces, <div> for purely visual wrapping.
33
+ */
34
+
35
+ import { type ReactNode, type HTMLAttributes } from 'react';
36
+ import { Container } from './Container';
37
+
38
+ interface PageSectionProps extends HTMLAttributes<HTMLElement> {
39
+ children: ReactNode;
40
+ className?: string;
41
+ /** Skip the default `py-[clamp(...)]` vertical padding. */
42
+ noPadding?: boolean;
43
+ /**
44
+ * Render children directly instead of wrapping in <Container>.
45
+ * Use for full-bleed backgrounds; place Container inside manually for text.
46
+ */
47
+ noContainer?: boolean;
48
+ /** Polymorphic element. Default: 'section'. */
49
+ as?: 'section' | 'div' | 'article';
50
+ }
51
+
52
+ export function PageSection({
53
+ children,
54
+ className,
55
+ noPadding = false,
56
+ noContainer = false,
57
+ as: Tag = 'section',
58
+ ...props
59
+ }: PageSectionProps) {
60
+ return (
61
+ <Tag
62
+ className={[
63
+ 'w-full',
64
+ !noPadding && 'py-[clamp(64px,8vw,192px)]',
65
+ className,
66
+ ]
67
+ .filter(Boolean)
68
+ .join(' ')}
69
+ {...props}
70
+ >
71
+ {noContainer ? children : <Container>{children}</Container>}
72
+ </Tag>
73
+ );
74
+ }