@@ -11,12 +11,10 @@ import {
1111 useState ,
1212} from "react" ;
1313import { motion , useReducedMotion } from "framer-motion" ;
14+ import { CanvasTexture } from "three" ;
1415import ContactForm from "@/components/ContactForm" ;
1516import Markdown from "@/components/Markdown" ;
16- import TerminalCanvas , {
17- TerminalApi ,
18- } from "@/components/terminal/TerminalCanvas" ;
19- import type { SceneDebugInfo } from "@/components/hero/HeroScene" ;
17+ import { Terminal , type TerminalRef } from "@/components/terminal/Terminal" ;
2018import { usePreferences } from "@/components/PreferencesProvider" ;
2119import type { ContentData } from "@/lib/content" ;
2220import { getAlternateLanguage , languageMeta } from "@/lib/i18n" ;
@@ -27,10 +25,13 @@ function HeroLoading() {
2725 return < div className = "hero-loading" > { t . hero . loading } </ div > ;
2826}
2927
30- const HeroScene = dynamic ( ( ) => import ( "@/components/hero/HeroScene" ) , {
31- ssr : false ,
32- loading : ( ) => < HeroLoading /> ,
33- } ) ;
28+ const Scene = dynamic (
29+ ( ) => import ( "@/components/canvas/Scene" ) . then ( ( mod ) => mod . Scene ) ,
30+ {
31+ ssr : false ,
32+ loading : ( ) => < HeroLoading /> ,
33+ } ,
34+ ) ;
3435
3536type HomeClientProps = {
3637 content : ContentData ;
@@ -497,6 +498,17 @@ export default function HomeClient({
497498 const { profile, projects, theme, pages, capabilities } = content ;
498499 const { t, language, setLanguage } = useI18n ( ) ;
499500 const { theme : currentTheme , toggleTheme, isSwitching } = usePreferences ( ) ;
501+ const terminalTheme = useMemo (
502+ ( ) => ( {
503+ palette : {
504+ terminalBg : theme . palette . terminalBg ,
505+ terminalText : theme . palette . terminalText ,
506+ terminalDim : theme . palette . terminalDim ,
507+ } ,
508+ crt : theme . crt ,
509+ } ) ,
510+ [ theme ] ,
511+ ) ;
500512 const heroRef = useRef < HTMLElement > ( null ) ;
501513 const featuredRef = useRef < HTMLElement > ( null ) ;
502514 const featuredHeadingRef = useRef < HTMLHeadingElement > ( null ) ;
@@ -641,7 +653,9 @@ export default function HomeClient({
641653 const [ openProjectId , setOpenProjectId ] = useState < string | null > ( null ) ;
642654 const [ openCapabilityId , setOpenCapabilityId ] = useState < string | null > ( null ) ;
643655 const [ aboutOpen , setAboutOpen ] = useState ( false ) ;
644- const [ terminalApi , setTerminalApi ] = useState < TerminalApi | null > ( null ) ;
656+ const [ terminalTexture , setTerminalTexture ] = useState < CanvasTexture | null > (
657+ null ,
658+ ) ;
645659 const [ terminalFocused , setTerminalFocused ] = useState ( false ) ;
646660 const pendingTerminalFocusRef = useRef ( false ) ;
647661 const [ screenAspect , setScreenAspect ] = useState ( 1.33 ) ;
@@ -668,30 +682,33 @@ export default function HomeClient({
668682 alt : false ,
669683 } ) ;
670684 const mobileModifiersRef = useRef ( mobileModifiers ) ;
685+ const terminalRef = useRef < TerminalRef > ( null ) ;
686+ const terminalUser = useMemo (
687+ ( ) =>
688+ profile . terminal . prompt . split ( "@" ) [ 0 ] || t . terminal . system . userFallback ,
689+ [ profile . terminal . prompt , t . terminal . system . userFallback ] ,
690+ ) ;
691+ const homeDir = useMemo ( ( ) => `/home/${ terminalUser } ` , [ terminalUser ] ) ;
692+
671693 const handleSceneReady = useCallback ( ( ) => {
672694 setSceneReady ( true ) ;
673695 setSceneReadyAt ( Date . now ( ) ) ;
674- } , [ ] ) ;
675-
676- const handleSceneDebug = useCallback ( ( info : SceneDebugInfo ) => {
677- if ( info . modelLoaded || info . fallbackPlane ) {
678- setModelReady ( true ) ;
679- setModelReadyAt ( Date . now ( ) ) ;
680- }
696+ setModelReady ( true ) ;
697+ setModelReadyAt ( Date . now ( ) ) ;
681698 } , [ ] ) ;
682699
683700 const handleTerminalFocus = useCallback ( ( ) => {
684- if ( terminalApi ) {
685- terminalApi . focus ( ) ;
701+ if ( terminalRef . current ) {
702+ terminalRef . current . focusInput ( ) ;
686703 pendingTerminalFocusRef . current = false ;
687704 return ;
688705 }
689706 pendingTerminalFocusRef . current = true ;
690- } , [ terminalApi ] ) ;
707+ } , [ ] ) ;
691708
692709 const handleTerminalInputFocus = useCallback ( ( ) => {
693- terminalApi ?. focusInput ?. ( ) ;
694- } , [ terminalApi ] ) ;
710+ terminalRef . current ?. focusInput ( ) ;
711+ } , [ ] ) ;
695712
696713 const handleVirtualKey = useCallback (
697714 (
@@ -701,27 +718,18 @@ export default function HomeClient({
701718 if ( options ?. focus !== false ) {
702719 handleTerminalFocus ( ) ;
703720 }
704- const modifiers = mobileModifiersRef . current ;
705- const down = new KeyboardEvent ( "keydown" , {
706- key,
707- bubbles : true ,
708- ctrlKey : modifiers . ctrl ,
709- shiftKey : modifiers . shift ,
710- altKey : modifiers . alt ,
711- } ) ;
712- const up = new KeyboardEvent ( "keyup" , {
713- key,
714- bubbles : true ,
715- ctrlKey : modifiers . ctrl ,
716- shiftKey : modifiers . shift ,
717- altKey : modifiers . alt ,
718- } ) ;
719- window . dispatchEvent ( down ) ;
720- window . dispatchEvent ( up ) ;
721+ terminalRef . current ?. handleInput ( key ) ;
721722 } ,
722723 [ handleTerminalFocus ] ,
723724 ) ;
724725
726+ useEffect ( ( ) => {
727+ if ( terminalTexture && ! terminalBootReady ) {
728+ setTerminalBootReady ( true ) ;
729+ setTerminalBootReadyAt ( Date . now ( ) ) ;
730+ }
731+ } , [ terminalTexture , terminalBootReady ] ) ;
732+
725733 const toggleMobileModifier = useCallback ( ( key : "ctrl" | "shift" | "alt" ) => {
726734 setMobileModifiers ( ( prev ) => {
727735 const next = { ...prev , [ key ] : ! prev [ key ] } ;
@@ -1091,15 +1099,15 @@ export default function HomeClient({
10911099 } , [ bootDone ] ) ;
10921100
10931101 useEffect ( ( ) => {
1094- if ( ! terminalApi || ! pendingTerminalFocusRef . current ) {
1102+ if ( ! terminalRef . current || ! pendingTerminalFocusRef . current ) {
10951103 return ;
10961104 }
1097- terminalApi . focus ( ) ;
1105+ terminalRef . current . focusInput ( ) ;
10981106 pendingTerminalFocusRef . current = false ;
1099- } , [ terminalApi ] ) ;
1107+ } , [ ] ) ;
11001108
11011109 useEffect ( ( ) => {
1102- if ( ! bootDone || ! terminalApi || isMobile || isCoarsePointer ) {
1110+ if ( ! bootDone || ! terminalRef . current || isMobile || isCoarsePointer ) {
11031111 return ;
11041112 }
11051113 const active = document . activeElement ;
@@ -1110,8 +1118,8 @@ export default function HomeClient({
11101118 ) {
11111119 return ;
11121120 }
1113- terminalApi . focus ( ) ;
1114- } , [ bootDone , isCoarsePointer , isMobile , terminalApi ] ) ;
1121+ terminalRef . current . focusInput ( ) ;
1122+ } , [ bootDone , isCoarsePointer , isMobile ] ) ;
11151123
11161124 useEffect ( ( ) => {
11171125 mobileModifiersRef . current = mobileModifiers ;
@@ -1341,9 +1349,6 @@ export default function HomeClient({
13411349 } , [ latestProjects , t . projects . latestTitle ] ) ;
13421350
13431351 const files = useMemo ( ( ) => {
1344- const terminalUser =
1345- profile . terminal . prompt . split ( "@" ) [ 0 ] || t . terminal . system . userFallback ;
1346- const homeDir = `/home/${ terminalUser } ` ;
13471352 const docsDir = `${ homeDir } /docs` ;
13481353 const scriptsDir = `${ homeDir } /scripts` ;
13491354 const mediaDir = `${ homeDir } /media` ;
@@ -1588,14 +1593,7 @@ export default function HomeClient({
15881593 content : t . terminal . files . elfPlaceholder ,
15891594 } ,
15901595 ] ;
1591- } , [
1592- localizedPages ,
1593- profile . fullName ,
1594- profile . terminal . prompt ,
1595- projectsMd ,
1596- t . terminal . files ,
1597- t . terminal . system . userFallback ,
1598- ] ) ;
1596+ } , [ localizedPages , profile . fullName , projectsMd , t . terminal . files , homeDir ] ) ;
15991597
16001598 const scrollToSection = ( id : string ) => {
16011599 const element = document . getElementById ( id ) ;
@@ -1605,6 +1603,26 @@ export default function HomeClient({
16051603 setMenuOpen ( false ) ;
16061604 } ;
16071605
1606+ const terminalConfig = useMemo (
1607+ ( ) => ( {
1608+ prompt : profile . terminal . prompt ,
1609+ introLines : profile . introLines [ language ] ,
1610+ homePath : homeDir ,
1611+ files,
1612+ messages : t . terminal ,
1613+ onNavigateAction : scrollToSection ,
1614+ } ) ,
1615+ [
1616+ files ,
1617+ homeDir ,
1618+ language ,
1619+ profile . introLines ,
1620+ profile . terminal . prompt ,
1621+ scrollToSection ,
1622+ t . terminal ,
1623+ ] ,
1624+ ) ;
1625+
16081626 const navLabels : Record < string , string > = {
16091627 home : t . nav . home ,
16101628 intro : t . nav . intro ,
@@ -2222,45 +2240,10 @@ export default function HomeClient({
22222240 id = "home"
22232241 className = { `hero-shell ${ terminalFocused ? "focused" : "" } ` } >
22242242 < div className = "hero-sticky" >
2225- < HeroScene
2226- terminalApi = { terminalApi }
2243+ < Scene
2244+ texture = { terminalTexture }
22272245 scrollProgressRef = { scrollProgressRef }
2228- noteTexts = { { red : t . hero . noteRed , blue : t . hero . noteBlue } }
2229- active = { heroActive }
2230- onDebugAction = { handleSceneDebug }
2231- onScreenAspectAction = { ( aspect ) => {
2232- if ( ! screenAspectReady ) {
2233- setScreenAspect ( aspect ) ;
2234- setScreenAspectReady ( true ) ;
2235- setScreenAspectReadyAt ( Date . now ( ) ) ;
2236- screenAspectRef . current = aspect ;
2237- pendingAspectRef . current = null ;
2238- return ;
2239- }
2240- const diff = Math . abs ( aspect - screenAspectRef . current ) ;
2241- if ( diff < 0.03 ) {
2242- return ;
2243- }
2244- pendingAspectRef . current = aspect ;
2245- if ( interactionActiveRef . current ) {
2246- return ;
2247- }
2248- if ( screenAspectTimerRef . current ) {
2249- window . clearTimeout ( screenAspectTimerRef . current ) ;
2250- }
2251- screenAspectTimerRef . current = window . setTimeout ( ( ) => {
2252- if ( pendingAspectRef . current === null ) {
2253- return ;
2254- }
2255- const nextAspect = pendingAspectRef . current ;
2256- pendingAspectRef . current = null ;
2257- setScreenAspect ( nextAspect ) ;
2258- if ( ! screenAspectReady ) {
2259- setScreenAspectReady ( true ) ;
2260- setScreenAspectReadyAt ( Date . now ( ) ) ;
2261- }
2262- } , 140 ) ;
2263- } }
2246+ className = "hero-scene absolute-fill"
22642247 onReadyAction = { handleSceneReady }
22652248 />
22662249 < div className = "hero-overlay" >
@@ -2384,26 +2367,14 @@ export default function HomeClient({
23842367 className = { `hero-scroll-shield${ heroActive ? " is-active" : "" } ` }
23852368 aria-hidden = "true"
23862369 />
2387- < TerminalCanvas
2388- files = { files }
2389- introLines = { profile . introLines [ language ] }
2390- prompt = { profile . terminal . prompt }
2391- language = { language }
2392- messages = { t . terminal }
2393- theme = { theme }
2394- isMobile = { isMobile }
2395- isActive = { heroActive }
2396- screenAspect = { screenAspect }
2397- scrollProgressRef = { scrollProgressRef }
2398- onNavigateAction = { scrollToSection }
2399- onReadyAction = { ( api ) => {
2400- setTerminalApi ( api ) ;
2401- } }
2402- onBootReadyAction = { ( ) => {
2403- setTerminalBootReady ( true ) ;
2404- setTerminalBootReadyAt ( Date . now ( ) ) ;
2405- } }
2406- onFocusChangeAction = { ( focused ) => setTerminalFocused ( focused ) }
2370+ < Terminal
2371+ ref = { terminalRef }
2372+ config = { terminalConfig }
2373+ theme = { terminalTheme }
2374+ active = { heroActive }
2375+ focused = { terminalFocused }
2376+ onFocusChange = { setTerminalFocused }
2377+ onTextureReady = { setTerminalTexture }
24072378 />
24082379 </ section >
24092380
0 commit comments