1+ import { useState , useEffect , useRef } from 'react'
12import { colors , fonts , shadows , radius } from '../tokens'
23import { Header } from '../components/Header'
34import { Hero } from '../components/Hero'
@@ -6,6 +7,25 @@ import { Footer } from '../components/Footer'
67import { projects } from '../data/projects'
78import { blogPosts } from '../data/blog-posts'
89
10+ // --- Intersection Observer Hook ---
11+
12+ function useInView ( ) {
13+ const ref = useRef < HTMLDivElement > ( null )
14+ const [ inView , setInView ] = useState ( false )
15+ useEffect ( ( ) => {
16+ if ( ! ref . current ) return
17+ const observer = new IntersectionObserver (
18+ ( [ entry ] ) => {
19+ if ( entry . isIntersecting ) setInView ( true )
20+ } ,
21+ { threshold : 0.15 } ,
22+ )
23+ observer . observe ( ref . current )
24+ return ( ) => observer . disconnect ( )
25+ } , [ ] )
26+ return { ref, inView }
27+ }
28+
929// --- Value Props ---
1030
1131const valuePropsStyles : Record < string , React . CSSProperties > = {
@@ -251,11 +271,77 @@ const inlineCode: React.CSSProperties = {
251271 fontFamily : fonts . mono ,
252272}
253273
254- function VerticalTrace ( ) {
274+ const driftStyles : Record < string , React . CSSProperties > = {
275+ container : {
276+ display : 'flex' ,
277+ alignItems : 'flex-start' ,
278+ gap : '0.6rem' ,
279+ maxWidth : '520px' ,
280+ margin : '1rem auto 0' ,
281+ background : `${ colors . accentRed } 0a` ,
282+ border : `1px solid ${ colors . accentRed } 30` ,
283+ borderRadius : radius ,
284+ padding : '0.75rem 1rem' ,
285+ fontFamily : fonts . mono ,
286+ fontSize : '0.78rem' ,
287+ color : colors . accentRed ,
288+ lineHeight : 1.5 ,
289+ } ,
290+ icon : {
291+ flexShrink : 0 ,
292+ fontSize : '0.9rem' ,
293+ } ,
294+ }
295+
296+ const feedbackStyles : Record < string , React . CSSProperties > = {
297+ container : {
298+ display : 'flex' ,
299+ flexDirection : 'column' as const ,
300+ alignItems : 'center' ,
301+ gap : '0.5rem' ,
302+ margin : '1.5rem 0' ,
303+ } ,
304+ label : {
305+ fontSize : '0.85rem' ,
306+ fontFamily : fonts . system ,
307+ color : colors . textMuted ,
308+ fontWeight : 500 ,
309+ } ,
310+ pills : {
311+ display : 'flex' ,
312+ gap : '0.5rem' ,
313+ flexWrap : 'wrap' as const ,
314+ justifyContent : 'center' ,
315+ } ,
316+ pill : {
317+ fontFamily : fonts . mono ,
318+ fontSize : '0.72rem' ,
319+ padding : '0.25rem 0.65rem' ,
320+ borderRadius : '100px' ,
321+ border : `1px solid ${ colors . borderColor } ` ,
322+ background : 'rgba(0,0,0,0.02)' ,
323+ color : colors . textMuted ,
324+ } ,
325+ arrow : {
326+ fontSize : '0.65rem' ,
327+ marginLeft : '0.2rem' ,
328+ } ,
329+ }
330+
331+ function VerticalTrace ( { inView } : { inView : boolean } ) {
255332 return (
256333 < div style = { traceStyles . wrapper } >
257334 { traceNodes . flatMap ( ( node , i ) => [
258- < div key = { node . id } style = { { ...traceStyles . card , borderLeft : `4px solid ${ node . color } ` } } >
335+ < div
336+ key = { node . id }
337+ style = { {
338+ ...traceStyles . card ,
339+ borderLeft : `4px solid ${ node . color } ` ,
340+ opacity : inView ? 1 : 0 ,
341+ transform : inView ? 'none' : 'translateY(12px)' ,
342+ transition : `opacity 0.5s ease ${ i * 0.2 } s, transform 0.5s ease ${ i * 0.2 } s` ,
343+ } }
344+ >
259345 < div style = { traceStyles . cardHeader } >
260346 < span style = { { ...traceStyles . typePill , color : node . color , background : `${ node . color } 14` } } >
261347 { node . type }
@@ -267,7 +353,14 @@ function VerticalTrace() {
267353 </ div > ,
268354 ...( i < traceEdges . length
269355 ? [
270- < div key = { `edge-${ i } ` } style = { traceStyles . connector } >
356+ < div
357+ key = { `edge-${ i } ` }
358+ style = { {
359+ ...traceStyles . connector ,
360+ opacity : inView ? 1 : 0 ,
361+ transition : `opacity 0.3s ease ${ i * 0.2 + 0.1 } s` ,
362+ } }
363+ >
271364 < div style = { traceStyles . connectorLine } />
272365 < span style = { traceStyles . connectorLabel } > { traceEdges [ i ] } </ span >
273366 < div style = { traceStyles . connectorLine } />
@@ -280,21 +373,64 @@ function VerticalTrace() {
280373 )
281374}
282375
376+ function DriftBadge ( { inView } : { inView : boolean } ) {
377+ return (
378+ < div
379+ style = { {
380+ ...driftStyles . container ,
381+ opacity : inView ? 1 : 0 ,
382+ transform : inView ? 'none' : 'translateY(8px)' ,
383+ transition : 'opacity 0.5s ease 0.9s, transform 0.5s ease 0.9s' ,
384+ } }
385+ >
386+ < span style = { driftStyles . icon } > { '\u26A0' } </ span >
387+ < span >
388+ drift detected — < strong > THX-VERSION-AWARE</ strong > changed v1.0.0 → v1.1.0 —{ ' ' }
389+ < strong > REQ-CORE-005</ strong > needs review
390+ </ span >
391+ </ div >
392+ )
393+ }
394+
395+ function FeedbackArc ( { inView } : { inView : boolean } ) {
396+ return (
397+ < div
398+ style = { {
399+ ...feedbackStyles . container ,
400+ opacity : inView ? 1 : 0 ,
401+ transition : 'opacity 0.5s ease 1.1s' ,
402+ } }
403+ >
404+ < span style = { feedbackStyles . label } > Knowledge flows upstream too</ span >
405+ < div style = { feedbackStyles . pills } >
406+ { [ 'challenges' , 'validates' , 'reveals_gap_in' ] . map ( ( edge ) => (
407+ < span key = { edge } style = { feedbackStyles . pill } >
408+ { edge }
409+ < span style = { feedbackStyles . arrow } > { '\u2191' } </ span >
410+ </ span >
411+ ) ) }
412+ </ div >
413+ </ div >
414+ )
415+ }
416+
283417function HowItWorks ( ) {
418+ const { ref, inView } = useInView ( )
284419 return (
285420 < section style = { howStyles . section } >
286- < div style = { howStyles . container } >
421+ < div ref = { ref } style = { howStyles . container } >
287422 < h2 style = { howStyles . sectionTitle } > How it works</ h2 >
288423 < p style = { howStyles . intro } >
289424 Lattice organizes knowledge into four layers — sources, theses, requirements, and implementations
290425 — connected by version-bound edges. Here’s a real trace from Lattice’s own knowledge graph:
291426 </ p >
292- < VerticalTrace />
427+ < VerticalTrace inView = { inView } />
293428 < p style = { howStyles . body } >
294- Every edge records the version it was bound to. When a source is updated or a thesis is revised,{ ' ' }
295- < code style = { inlineCode } > lattice drift</ code > tells you exactly which downstream requirements and
296- implementations need review.
429+ Every edge records the version it was bound to. When something changes,{ ' ' }
430+ < code style = { inlineCode } > lattice drift</ code > tells you what needs review:
297431 </ p >
432+ < DriftBadge inView = { inView } />
433+ < FeedbackArc inView = { inView } />
298434 < pre style = { howStyles . codeBlock } >
299435 < code > { `.lattice/
300436├── config.yaml
0 commit comments