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.
- package/README.md +80 -53
- package/bin/chati.js +150 -5
- package/framework/agents/build/dev.md +509 -76
- package/framework/agents/deploy/devops.md +40 -25
- package/framework/agents/discover/brief.md +156 -22
- package/framework/agents/discover/brownfield-wu.md +24 -14
- package/framework/agents/discover/greenfield-wu.md +100 -14
- package/framework/agents/plan/architect-data-engineer.md +6 -6
- package/framework/agents/plan/architect-system.md +46 -12
- package/framework/agents/plan/architect.md +40 -20
- package/framework/agents/plan/detail.md +36 -24
- package/framework/agents/plan/phases.md +36 -26
- package/framework/agents/plan/tasks.md +114 -33
- package/framework/agents/plan/ux-brand-architect.md +240 -8
- package/framework/agents/plan/ux-component-engineer.md +28 -13
- package/framework/agents/plan/ux-researcher.md +7 -6
- package/framework/agents/plan/ux.md +55 -22
- package/framework/agents/quality/qa-implementation.md +143 -74
- package/framework/agents/quality/qa-planning.md +115 -42
- package/framework/agents/quality/qa-visual.md +439 -0
- package/framework/agents/shared/visualizer.md +128 -0
- package/framework/config.yaml +7 -6
- package/framework/constitution.md +127 -44
- package/framework/context/governance.md +12 -7
- package/framework/context/quality.md +6 -5
- package/framework/context/root.md +6 -6
- package/framework/data/entity-registry.yaml +377 -4
- package/framework/data/model-limits.json +19 -0
- package/framework/domains/agents/qa-visual.yaml +74 -0
- package/framework/domains/constitution.yaml +46 -2
- package/framework/domains/workflows/greenfield-fullstack.yaml +2 -2
- package/framework/hooks/advance-trigger.js +131 -0
- package/framework/hooks/brief-validator.js +83 -0
- package/framework/hooks/constitution-guard.js +24 -5
- package/framework/hooks/license-guard.js +62 -27
- package/framework/hooks/mode-governance.js +13 -2
- package/framework/hooks/model-governance.js +1 -0
- package/framework/hooks/post-dev.js +207 -0
- package/framework/hooks/prism-engine.js +274 -105
- package/framework/hooks/reasoning-escalator.js +371 -0
- package/framework/hooks/reference-trigger.js +117 -0
- package/framework/hooks/session-digest.js +50 -1
- package/framework/hooks/settings.json +32 -1
- package/framework/hooks/style-guard.js +25 -6
- package/framework/hooks/team-quality-gate.js +19 -12
- package/framework/hooks/undercover-guard.js +4 -2
- package/framework/i18n/en.yaml +3 -3
- package/framework/i18n/es.yaml +3 -3
- package/framework/i18n/fr.yaml +3 -3
- package/framework/i18n/pt.yaml +3 -3
- package/framework/intelligence/context-engine.md +4 -5
- package/framework/intelligence/decision-engine.md +1 -1
- package/framework/intelligence/hooks-performance.md +3 -3
- package/framework/migrations/v1.0-to-v1.1.yaml +1 -1
- package/framework/migrations/v1.4-to-v2.0.yaml +11 -11
- package/framework/migrations/v4.0-to-v4.1.yaml +2 -2
- package/framework/migrations/v4.2-to-v4.3.yaml +29 -0
- package/framework/orchestrator/chati-router.js +387 -0
- package/framework/orchestrator/chati-update.md +40 -40
- package/framework/orchestrator/chati.md +294 -94
- package/framework/scaffold/motion-premium/README.md +89 -0
- package/framework/scaffold/motion-premium/app/globals.css.template +400 -0
- package/framework/scaffold/motion-premium/app/layout.tsx.template +110 -0
- package/framework/scaffold/motion-premium/components/animation/BackgroundCrossfadeProvider.tsx.template +170 -0
- package/framework/scaffold/motion-premium/components/animation/LenisProvider.tsx.template +49 -0
- package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template +83 -0
- package/framework/scaffold/motion-premium/components/animation/Preloader.tsx.template +171 -0
- package/framework/scaffold/motion-premium/components/ui/Container.tsx.template +69 -0
- package/framework/scaffold/motion-premium/components/ui/PageSection.tsx.template +74 -0
- package/framework/scaffold/motion-premium/lib/animations/gsap.ts.template +112 -0
- package/framework/scaffold/motion-premium/lib/animations/refreshCoordinator.ts.template +75 -0
- package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +119 -0
- package/framework/scaffold/motion-premium/lib/animations/useGsapContext.ts.template +92 -0
- package/framework/scaffold/motion-premium/lib/animations/useScrollSnapStepper.ts.template +265 -0
- package/framework/scaffold/motion-premium/lib/animations/useSmoothScroll.ts.template +67 -0
- package/framework/scaffold/motion-premium/lib/brand.ts.template +43 -0
- package/framework/scaffold/motion-premium/scaffold.yaml +174 -0
- package/framework/scaffold/motion-premium-3d/README.md +80 -0
- package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template +81 -0
- package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template +75 -0
- package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/page.tsx.template +26 -0
- package/framework/scaffold/motion-premium-3d/components/3d/CameraRig.tsx.template +100 -0
- package/framework/scaffold/motion-premium-3d/components/3d/CanvasProvider.tsx.template +85 -0
- package/framework/scaffold/motion-premium-3d/components/3d/InvalidateOnScroll.tsx.template +51 -0
- package/framework/scaffold/motion-premium-3d/components/3d/MeshCrossfade.tsx.template +79 -0
- package/framework/scaffold/motion-premium-3d/components/3d/ScrollCrossfade.tsx.template +88 -0
- package/framework/scaffold/motion-premium-3d/components/3d/ScrollScene.tsx.template +121 -0
- package/framework/scaffold/motion-premium-3d/components/webgl/SceneFallback.tsx.template +65 -0
- package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +68 -0
- package/framework/scaffold/motion-premium-3d/lib/webgl/detect.ts.template +69 -0
- package/framework/scaffold/motion-premium-3d/scaffold.yaml +133 -0
- package/framework/schemas/session.schema.json +109 -21
- package/framework/scripts/reference-capture.js +430 -0
- package/framework/scripts/visual-qa.js +674 -0
- package/framework/tasks/orchestrator-handoff.md +1 -1
- package/framework/tasks/orchestrator-resume.md +1 -1
- package/framework/tasks/orchestrator-route.md +1 -1
- package/framework/tasks/orchestrator-status.md +3 -3
- package/framework/tasks/qa-planning-gate-define.md +1 -1
- package/framework/templates/brandbook-html-tmpl.md +1 -1
- package/framework/templates/brandbook-tmpl.yaml +1 -1
- package/framework/templates/component-spec-tmpl.yaml +1 -1
- package/framework/templates/design-token-tmpl.yaml +1 -1
- package/framework/templates/icon-system-tmpl.yaml +1 -1
- package/framework/templates/team-planning-tasks.yaml +6 -5
- package/framework/workflows/brownfield-discovery.yaml +2 -2
- package/framework/workflows/brownfield-fullstack.yaml +15 -11
- package/framework/workflows/brownfield-service.yaml +14 -10
- package/framework/workflows/brownfield-ui.yaml +15 -11
- package/framework/workflows/greenfield-fullstack.yaml +16 -13
- package/framework/workflows/quick-flow.yaml +3 -3
- package/framework/workflows/standard-flow.yaml +12 -9
- package/package.json +10 -5
- package/src/autonomy/autonomous-gate.js +1 -0
- package/src/autonomy/build-state.js +1 -2
- package/src/autonomy/progress-reporter.js +1 -1
- package/src/config/agent-customizer.js +11 -3
- package/src/config/claude-settings-generator.js +27 -7
- package/src/config/context-file-generator.js +41 -21
- package/src/config/framework-adapter.js +1 -0
- package/src/config/gemini-hooks-generator.js +19 -7
- package/src/config/mcp-configs.js +1 -0
- package/src/context/layers/l1-global.js +2 -1
- package/src/dashboard/data-reader.js +4 -3
- package/src/dashboard/layout.js +2 -1
- package/src/decision/analyzer.js +6 -30
- package/src/decision/engine.js +4 -28
- package/src/decision/registry-healer.js +3 -2
- package/src/decision/registry-updater.js +23 -14
- package/src/extensions/loader.js +2 -8
- package/src/gates/g1-planning-complete.js +2 -1
- package/src/gates/g2-qa-planning.js +2 -1
- package/src/gates/g3-implementation.js +2 -1
- package/src/gates/g4-qa-implementation.js +3 -2
- package/src/gates/g5-deploy-ready.js +2 -1
- package/src/health/engine.js +4 -3
- package/src/installer/core.js +422 -81
- package/src/installer/preflight.js +131 -0
- package/src/installer/provider-overlay.js +3 -3
- package/src/installer/scaffold-applier.js +358 -0
- package/src/installer/templates.js +46 -29
- package/src/installer/validator.js +17 -12
- package/src/intelligence/registry-manager.js +22 -29
- package/src/intelligence/timeline.js +11 -6
- package/src/license/commands.js +1 -1
- package/src/license/wait.js +102 -0
- package/src/memory/agent-memory.js +81 -0
- package/src/memory/dream.js +32 -1
- package/src/merger/replace-merger.js +28 -15
- package/src/orchestrator/agent-selector.js +2 -1
- package/src/orchestrator/cli.js +1869 -71
- package/src/orchestrator/doctor.js +270 -0
- package/src/orchestrator/handoff-engine.js +4 -3
- package/src/orchestrator/index.js +2 -0
- package/src/orchestrator/pipeline-manager.js +306 -15
- package/src/orchestrator/session-manager.js +331 -6
- package/src/tasks/handoff.js +3 -2
- package/src/telemetry/config.js +4 -3
- package/src/telemetry/schema.js +1 -0
- package/src/terminal/collector.js +3 -2
- package/src/terminal/index.js +1 -2
- package/src/terminal/isolation.js +52 -18
- package/src/terminal/prompt-builder.js +42 -25
- package/src/terminal/run-parallel.js +1 -1
- package/src/terminal/run-team.js +3 -3
- package/src/terminal/team-task-list.js +43 -4
- package/src/upgrade/backup.js +3 -2
- package/src/upgrade/checker.js +3 -2
- package/src/upgrade/migrator.js +65 -7
- package/src/upgrade/tracked-files-detector.js +86 -0
- package/src/upgrade/user-messages.js +94 -0
- package/src/utils/config-parser.js +2 -1
- package/src/utils/feature-flags.js +2 -1
- package/src/utils/flatten-entities.js +69 -0
- package/src/utils/framework-dir.js +16 -0
- package/src/utils/model-id.js +85 -0
- package/src/utils/provider-limits.js +84 -23
- package/src/utils/schema-validator.js +1 -1
- package/src/wizard/i18n.js +5 -4
- package/src/wizard/index.js +14 -0
- package/assets/logo - c/303/263pia.png +0 -0
- package/assets/logo.svg +0 -42
- package/assets/logo2.png +0 -0
- package/assets/social-preview.png +0 -0
- package/scripts/bundle-framework.js +0 -69
- package/scripts/changelog-generator.js +0 -222
- package/scripts/codebase-mapper.js +0 -728
- package/scripts/commit-message-generator.js +0 -167
- package/scripts/coverage-analyzer.js +0 -260
- package/scripts/dependency-analyzer.js +0 -280
- package/scripts/doctor/checks/agents.js +0 -77
- package/scripts/doctor/checks/constitution.js +0 -41
- package/scripts/doctor/checks/domain-alignment.js +0 -58
- package/scripts/doctor/checks/prism-layers.js +0 -84
- package/scripts/doctor/checks/registry.js +0 -55
- package/scripts/doctor/checks/schemas.js +0 -61
- package/scripts/doctor/fixes/reference-fix.js +0 -100
- package/scripts/doctor/fixes/registry-fix.js +0 -56
- package/scripts/doctor/index.js +0 -212
- package/scripts/framework-analyzer.js +0 -308
- package/scripts/generate-constitution-domain.js +0 -253
- package/scripts/generate-signing-key.js +0 -33
- package/scripts/health-check.js +0 -481
- package/scripts/ide-sync.js +0 -326
- package/scripts/performance-analyzer.js +0 -325
- package/scripts/plan-tracker.js +0 -278
- package/scripts/populate-entity-registry.js +0 -481
- package/scripts/pr-review.js +0 -317
- package/scripts/rollback-manager.js +0 -310
- package/scripts/semantic-lint.js +0 -328
- package/scripts/sign-manifest.js +0 -53
- package/scripts/stuck-detector.js +0 -343
- package/scripts/test-quality-assessment.js +0 -257
- package/scripts/validate-agents.js +0 -368
- package/scripts/validate-package.js +0 -505
- package/scripts/validate-tasks.js +0 -465
- package/src/autonomy/worktree-manager.js +0 -250
- package/src/intelligence/decision-engine.js +0 -256
- package/src/intelligence/document-sharder.js +0 -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
|
+
}
|
package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template
ADDED
|
@@ -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
|
+
}
|