11'use client'
22
3- import React , { useEffect , useMemo , useRef , useState } from 'react' ;
3+ import React , { useEffect , useMemo , useRef , useState } from 'react' ; from 'react' ;
44
55const STORAGE_KEYS = {
66 history : 'ptc_history_v1' ,
@@ -260,6 +260,7 @@ function commandSuggestions(query) {
260260function App ( ) {
261261 const [ prefs , setPrefs ] = useState ( DEFAULT_PREFS ) ;
262262 const [ input , setInput ] = useState ( '' ) ;
263+ const [ selectedSuggestion , setSelectedSuggestion ] = uconst [ isPaletteOpen , setIsPaletteOpen ] = useState ( false ) ; useState ( false ) ;
263264 const [ result , setResult ] = useState ( null ) ;
264265 const [ history , setHistory ] = useState ( [ ] ) ;
265266 const [ pinned , setPinned ] = useState ( [ ] ) ;
@@ -400,14 +401,10 @@ function App() {
400401 }
401402 }
402403
403- function runSuggestion ( text ) {
404- setInput ( text ) ;
404+ function runSuggessetInput ( text ) ;
405405 inputRef . current ?. focus ( ) ;
406- executeCommand ( text ) ;
407- }
408-
409- function runSpeedTest ( ) {
410- const testUrl = 'https://speed.hetzner.de/10MB.bin' ;
406+ setIsPaletteOpen ( false ) ;
407+ executeCommand ( text ) ; https://speed.hetzner.de/10MB.bin';
411408 const started = performance . now ( ) ;
412409 setSpeedState ( { running : true , dl : null , ul : null , ping : null , message : 'Downloading a test payload…' } ) ;
413410 fetch ( testUrl , { cache : 'no-store' } )
@@ -447,35 +444,38 @@ function App() {
447444
448445 const suggestions = useMemo ( ( ) => commandSuggestions ( input ) , [ input ] ) ;
449446
447+ useEffect ( ( ) => {
448+ setSelectedSuggestion ( 0 ) ;
449+ } , [ input ] ) ;
450+
450451 useEffect ( ( ) => {
451452 const onKeyDown = ( e ) => {
452453 if ( e . key === '/' && document . activeElement !== inputRef . current ) {
453454 e . preventDefault ( ) ;
454455 inputRef . current ?. focus ( ) ;
455456 }
456- if ( e . key === 'Enter' && document . activeElement === inputRef . current ) {
457- e . preventDefault ( ) ;
458- if ( normalize ( input ) === 'speed test' || normalize ( input ) === 'speed' ) runSpeedTest ( ) ;
459- else executeCommand ( input ) ;
457+ if ( document . activeElement === inputRef . current ) {
458+ if ( e . key === 'ArrowDown' ) {
459+ e . pprev ) => Math . min ( prev + 1 , Math . max ( suggestions . length - 1 , 0 ) ) ) ;
460+ }
461+
462+ if ( e . key === 'ArrowUp' ) {
463+ e . preventDefault ( ) ;
464+ setSelectedSuggestion ( ( prev ) => Math . max ( prev - 1 , 0 ) ) ;
465+ }
466+
467+ if ( e . key === 'Tab' ) {
468+ e . preventDefault ( ) ;
469+ if ( suggestions [ selectedSuggestion ] ) {
470+ setInput ( suggestions [ selectedSuggestid test ' || normalize ( finalCommand ) === 'speed' ) runSpeedTest ( ) ;
471+ else executeCommand ( finalCommand ) ;
472+ }
460473 }
461474 } ;
462475 window . addEventListener ( 'keydown' , onKeyDown ) ;
463- return ( ) => window . removeEventListener ( 'keydown' , onKeyDown ) ;
464- } , [ input , prefs ] ) ;
465-
466- const commandExamples = [
467- '100 usd to myr' ,
468- '10 km to mi' ,
469- '16 * 24 + 10' ,
470- 'gen password 16 strong' ,
471- 'qr https://example.com' ,
472- 'speed test' ,
473- ] ;
474-
475- return (
476- < div className = "min-h-screen bg-slate-950 text-slate-100" >
476+ return ( ) = > window . removeEventListh - screen bg - slate - 950 text - slate - 100 ">
477477 < div className = "mx-auto max-w-7xl px-4 py-6 lg:px-8" >
478- < header className = "mb-6 flex flex-col gap-3 rounded-3xl border border-slate-800 bg-slate-900/70 p-5 shadow-2xl shadow-black/20 backdrop-blur" >
478+ < header className = "sticky top-4 z-20 mb-6 flex flex-col gap-3 rounded-3xl border border-slate-800 bg-slate-900/90 p-5 shadow-2xl shadow-black/30 backdrop-blur-xl " >
479479 < div className = "flex flex-wrap items-center justify-between gap-3" >
480480 < div >
481481 < h1 className = "text-2xl font-semibold tracking-tight" > Personal Tool Console</ h1 >
@@ -488,31 +488,68 @@ function App() {
488488 </ div >
489489
490490 < div className = "grid gap-3 lg:grid-cols-[1fr_auto]" >
491- < div className = "relative" >
491+ < div className = "relative group " >
492492 < input
493493 ref = { inputRef }
494494 value = { input }
495- onChange = { ( e ) => setInput ( e . target . value ) }
495+ onChange = { ( e ) => {
496+ setInput ( e . target . value ) ;
497+ setIsPaletteOpen ( true ) ;
498+ } }
496499 onKeyDown = { ( e ) => {
500+
501+ } }
502+ placeholder = "Search or run a command… 100 usd to myr, password 20, qr https://..., 16*24+10"
503+ className = "w-full rounded-2xl border border-slate-700 bg-black/70 px-5 py-5 pr-28 text-lg outline-none ring-0 transition placeholder:text-slate-500 focus:border-slate-400 focus:bg-black"
504+ />
505+ < onChange = { ( e ) => {
506+ setInput ( e . target . value ) ;
507+ setIsPaletteOpen ( true ) ;
508+ } } e inset-y-0 right-3 flex items-center gap-2 text-xs texif ( e . key = = = 'Escape' ) {
509+ e . preventDefault ( ) ;
510+ setIsPaletteOpen ( false ) ;
511+ return ;
512+ }
513+
497514 if ( e . key = = = 'Enter' ) {
498515 e . preventDefault ( ) ;
499516 if ( normalize ( input ) === 'speed test' || normalize ( input ) === 'speed' ) runSpeedTest ( ) ;
500517 else executeCommand ( input ) ;
501- }
502- } }
503- placeholder = "Type a command: 100 usd to myr, gen password 16 strong, qr https://..., 16*24+10"
504- className = "w-full rounded-2xl border border-slate-700 bg-slate-950/80 px-4 py-4 pr-24 text-base outline-none ring-0 placeholder:text-slate-500 focus:border-slate-500"
505- />
506- < div className = "pointer-events-none absolute inset-y-0 right-3 flex items-center gap-2 text-xs text-slate-500" >
507- < span className = "rounded-lg border border-slate-800 px-2 py-1" > Cmd</ span >
508- </ div >
518+ setIsPaletteOpen ( false ) ;
519+ } - 0 top- [ calc ( 100 % + 12 px ) ] z-30 overflow-hidden rounded-2xl border border-slate-800 bg-slate-950 / 95 shadow-2xl shadow-black / 50 backdrop-blur-xl ">
520+ < div className = "border-b border-slate-800 px-4 py-2 text-xs uppercase tracking-[0.2em] text-slate-500" >
521+ Command palette
522+ </ div >
523+
524+ < div className = "max-h-80 overflow-auto p-2" >
525+ { suggestions . map ( ( s , idx ) => (
526+ < button
527+ key = { s }
528+ onClick = { ( ) => runSuggestion ( s ) }
529+ className = { `flex isPaletteOpens-center justify-between rounded-xl px-4 py-3 text-left transition ${ idx === selectedSuggestion ? 'bg-slate-800 text-white' : 'text-slate-300 hover:bg-slate-900' } ` }
530+ >
531+ < div className = "flex flex-col" >
532+ < span className = "font-mono text-sm" > { s } </ span >
533+ < span className = "mt-1 text-xs text-slate-500" >
534+ { s . includes ( 'to' ) ? 'Conversion command' : s . includes ( 'password' ) ? 'Password generation' : s . includes ( 'qr' ) ? 'QR generation' : s . includes ( 'speed' ) ? 'Network test' : 'Calculator' }
535+ </ span >
536+ </ div >
537+
538+ < div className = "rounded-lg border border-slate-700 px-2 py-1 text-[10px] text-slate-500" >
539+ ↵
540+ </ div >
541+ </ button >
542+ ) ) }
543+ </ div >
544+ </ div >
545+ )}
509546 </ div >
510547 < div className = "flex gap-2" >
511548 < button
512549 onClick = { ( ) => ( normalize ( input ) === 'speed test' || normalize ( input ) === 'speed' ? runSpeedTest ( ) : executeCommand ( input ) ) }
513550 className = "rounded-2xl bg-slate-100 px-4 py-3 font-medium text-slate-950 transition hover:bg-white"
514551 >
515- Run
552+ Execute
516553 </ button >
517554 < button
518555 onClick = { ( ) => setInput ( '' ) }
@@ -523,16 +560,11 @@ function App() {
523560 </ div >
524561 </ div >
525562
526- < div className = "flex flex-wrap gap-2" >
527- { suggestions . map ( ( s ) => (
528- < button
529- key = { s }
530- onClick = { ( ) => runSuggestion ( s ) }
531- className = "rounded-full border border-slate-700 bg-slate-950/80 px-3 py-1.5 text-sm text-slate-300 transition hover:border-slate-500 hover:bg-slate-800"
532- >
533- { s }
534- </ button >
535- ) ) }
563+ < div className = "flex flex-wrap items-center gap-2 pt-2 text-xs text-slate-500" >
564+ < span className = "rounded-full border border-slate-800 px-3 py-1" > ↑↓ navigate</ span >
565+ < span className = "rounded-full border border-slate-800 px-3 py-1" > Tab autocomplete</ span >
566+ < span className = "rounded-full border border-slate-800 px-3 py-1" > Enter execute</ span >
567+ < span className = "rounded-full border border-slate-800 px-3 py-1" > / focus</ span >
536568 </ div >
537569 </ header >
538570
@@ -556,7 +588,7 @@ function App() {
556588
557589 { ! result && (
558590 < div className = "rounded-2xl border border-dashed border-slate-700 bg-slate-950/50 p-6 text-sm text-slate-400" >
559- Run a command to see a structured result here.
591+ Execute a command to see a structured result here.
560592 </ div >
561593 ) }
562594
@@ -635,7 +667,7 @@ function App() {
635667 onClick = { runSpeedTest }
636668 className = "rounded-2xl bg-slate-100 px-4 py-3 font-medium text-slate-950 transition hover:bg-white"
637669 >
638- Run download test
670+ Execute download test
639671 </ button >
640672 </ div >
641673 ) }
0 commit comments