|
| 1 | +/* Hero — the menu-bar popover mockup is the showpiece */ |
| 2 | + |
| 3 | +const MenuBarPopover = ({ active = 'A' }) => { |
| 4 | + const slots = [ |
| 5 | + { id: 'A', shortcut: ['⌘', 'C'], paste: ['⌘', 'V'], preview: 'github.com/RAKKUNN/DualClip', kind: 'link' }, |
| 6 | + { id: 'B', shortcut: ['⌥', '⌘', 'C'], paste: ['⌥', '⌘', 'V'], preview: 'const handlePaste = async () => {\n const text = await navigator.clipboard.readText()', kind: 'code' }, |
| 7 | + { id: 'C', shortcut: ['⌃', '⌘', 'C'], paste: ['⌃', '⌘', 'V'], preview: '— image —', kind: 'image' }, |
| 8 | + ]; |
| 9 | + return ( |
| 10 | + <div className="popover" role="dialog" aria-label="DualClip menu bar popover"> |
| 11 | + <div className="popover-header"> |
| 12 | + <div className="popover-header-left"> |
| 13 | + <div className="popover-app-dot" /> |
| 14 | + <span className="popover-app-name">DualClip</span> |
| 15 | + </div> |
| 16 | + <div className="popover-status mono">RAM-only · idle</div> |
| 17 | + </div> |
| 18 | + <div className="popover-slots"> |
| 19 | + {slots.map(s => ( |
| 20 | + <div key={s.id} className={`slot-row ${active === s.id ? 'is-active' : ''}`}> |
| 21 | + <div className="slot-row-left"> |
| 22 | + <span className={`slot-chip slot-${s.id}`}>{s.id}</span> |
| 23 | + <div className="slot-content"> |
| 24 | + <div className="slot-label mono">Slot {s.id}</div> |
| 25 | + <div className={`slot-preview ${s.kind === 'image' ? 'slot-preview-image' : ''}`}> |
| 26 | + {s.kind === 'image' ? ( |
| 27 | + <div className="slot-image-thumb"> |
| 28 | + <span className="slot-image-bars" /> |
| 29 | + <span className="mono">PNG · 1.2 MB</span> |
| 30 | + </div> |
| 31 | + ) : ( |
| 32 | + <span className="mono">{s.preview}</span> |
| 33 | + )} |
| 34 | + </div> |
| 35 | + </div> |
| 36 | + </div> |
| 37 | + <div className="slot-row-right"> |
| 38 | + <Shortcut keys={s.paste} sm /> |
| 39 | + </div> |
| 40 | + </div> |
| 41 | + ))} |
| 42 | + </div> |
| 43 | + <div className="popover-footer"> |
| 44 | + <div className="footer-hint mono"> |
| 45 | + <span className="hint-dot" /> Press <Shortcut keys={['⌥','⌘','C']} sm/> to stash · <Shortcut keys={['⌥','⌘','V']} sm/> to recall |
| 46 | + </div> |
| 47 | + </div> |
| 48 | + </div> |
| 49 | + ); |
| 50 | +}; |
| 51 | + |
| 52 | +const HEADLINE_VARIANTS = { |
| 53 | + "zero-history": <>Three clipboards.<br/><span className="headline-em">Zero history.</span></>, |
| 54 | + "where-not-when": <><span className="headline-em">Where</span>, not when.</>, |
| 55 | + "by-the-letter": <>Stash by the letter.<br/><span className="headline-em">A, B, or C.</span></>, |
| 56 | +}; |
| 57 | + |
| 58 | +const Hero = ({ headlineVariant = 'zero-history', showShortcuts = true }) => { |
| 59 | + const [activeSlot, setActiveSlot] = React.useState('A'); |
| 60 | + React.useEffect(() => { |
| 61 | + const i = setInterval(() => { |
| 62 | + setActiveSlot(s => (s === 'A' ? 'B' : s === 'B' ? 'C' : 'A')); |
| 63 | + }, 2400); |
| 64 | + return () => clearInterval(i); |
| 65 | + }, []); |
| 66 | + |
| 67 | + return ( |
| 68 | + <header className="hero" data-screen-label="Hero"> |
| 69 | + <div className="spotlight" /> |
| 70 | + <div className="grain" /> |
| 71 | + <Nav /> |
| 72 | + <div className="container hero-grid"> |
| 73 | + <div className="hero-copy"> |
| 74 | + <div className="eyebrow"><span className="dot"/> macOS · Menu bar</div> |
| 75 | + <h1 className="hero-headline"> |
| 76 | + {HEADLINE_VARIANTS[headlineVariant] || HEADLINE_VARIANTS["zero-history"]} |
| 77 | + </h1> |
| 78 | + <p className="lede hero-lede"> |
| 79 | + DualClip gives you three dedicated clipboard slots — A, B, C — with their own |
| 80 | + keyboard shortcuts. Decide where things go. Recall them instantly. Nothing ever |
| 81 | + touches the disk. |
| 82 | + </p> |
| 83 | + |
| 84 | + <div className="hero-install"> |
| 85 | + <div className="code-block hero-code"> |
| 86 | + <span><span className="prompt">$</span><span className="mono">brew install RAKKUNN/tap/dualclip</span></span> |
| 87 | + <CopyButton text="brew install RAKKUNN/tap/dualclip" /> |
| 88 | + </div> |
| 89 | + <div className="hero-cta-row"> |
| 90 | + <a className="btn btn-primary" href="https://github.com/RAKKUNN/DualClip/releases" target="_blank" rel="noreferrer"> |
| 91 | + <Icon.Download size={14}/> Download for macOS |
| 92 | + </a> |
| 93 | + <a className="btn btn-ghost" href="https://github.com/RAKKUNN/DualClip" target="_blank" rel="noreferrer"> |
| 94 | + <Icon.Github size={14}/> View source |
| 95 | + </a> |
| 96 | + </div> |
| 97 | + </div> |
| 98 | + |
| 99 | + <div className="hero-meta mono"> |
| 100 | + <span><Icon.Lock size={12}/> MIT licensed</span> |
| 101 | + <span className="meta-dot"/> |
| 102 | + <span>macOS 13+</span> |
| 103 | + <span className="meta-dot"/> |
| 104 | + <span>Apple Silicon</span> |
| 105 | + </div> |
| 106 | + </div> |
| 107 | + |
| 108 | + <div className="hero-visual" aria-hidden="false"> |
| 109 | + <div className="hero-menubar"> |
| 110 | + <div className="menubar-bar"> |
| 111 | + <span className="menubar-apple"> |
| 112 | + <svg width="13" height="14" viewBox="0 0 17 20" fill="currentColor"><path d="M13.97 10.6c-.02-2.43 1.99-3.6 2.08-3.65-1.13-1.66-2.9-1.89-3.53-1.91-1.5-.15-2.93.88-3.69.88-.77 0-1.94-.86-3.19-.84-1.64.02-3.16.96-4 2.42-1.71 2.97-.44 7.36 1.23 9.78.81 1.18 1.78 2.5 3.04 2.46 1.22-.05 1.69-.79 3.16-.79 1.47 0 1.89.79 3.18.77 1.31-.02 2.14-1.2 2.95-2.39.93-1.37 1.32-2.71 1.34-2.78-.03-.01-2.56-.98-2.58-3.95M11.5 3.41c.68-.82 1.13-1.96 1.01-3.1-.97.04-2.16.65-2.86 1.47-.62.72-1.17 1.88-1.02 3 1.08.08 2.19-.55 2.87-1.37"/></svg> |
| 113 | + </span> |
| 114 | + <span className="menubar-text">File</span> |
| 115 | + <span className="menubar-text">Edit</span> |
| 116 | + <span className="menubar-text">View</span> |
| 117 | + <span className="menubar-spacer" /> |
| 118 | + <span className="menubar-icon-group"> |
| 119 | + <span className="menubar-icon menubar-icon--app" aria-label="DualClip menu bar icon"> |
| 120 | + <span className="menubar-pill"> |
| 121 | + <span className="menubar-pill-half" /> |
| 122 | + </span> |
| 123 | + </span> |
| 124 | + <span className="menubar-icon"> |
| 125 | + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg> |
| 126 | + </span> |
| 127 | + <span className="menubar-icon mono menubar-time">10:12</span> |
| 128 | + </span> |
| 129 | + </div> |
| 130 | + <div className="menubar-arrow" /> |
| 131 | + <MenuBarPopover active={activeSlot} /> |
| 132 | + </div> |
| 133 | + {showShortcuts && <div className="hero-shortcuts"> |
| 134 | + <div className="shortcut-card"> |
| 135 | + <div className="shortcut-card-label mono">copy →</div> |
| 136 | + <div className="shortcut-card-keys"><Shortcut keys={['⌘','C']}/></div> |
| 137 | + <div className="shortcut-card-slot"><span className="slot-chip slot-A">A</span></div> |
| 138 | + </div> |
| 139 | + <div className="shortcut-card"> |
| 140 | + <div className="shortcut-card-label mono">copy →</div> |
| 141 | + <div className="shortcut-card-keys"><Shortcut keys={['⌥','⌘','C']}/></div> |
| 142 | + <div className="shortcut-card-slot"><span className="slot-chip slot-B">B</span></div> |
| 143 | + </div> |
| 144 | + <div className="shortcut-card"> |
| 145 | + <div className="shortcut-card-label mono">copy →</div> |
| 146 | + <div className="shortcut-card-keys"><Shortcut keys={['⌃','⌘','C']}/></div> |
| 147 | + <div className="shortcut-card-slot"><span className="slot-chip slot-C">C</span></div> |
| 148 | + </div> |
| 149 | + </div>} |
| 150 | + </div> |
| 151 | + </div> |
| 152 | + </header> |
| 153 | + ); |
| 154 | +}; |
| 155 | + |
| 156 | +const Nav = () => { |
| 157 | + const [theme, setTheme] = React.useState('dark'); |
| 158 | + React.useEffect(() => { |
| 159 | + document.documentElement.setAttribute('data-theme', theme); |
| 160 | + }, [theme]); |
| 161 | + return ( |
| 162 | + <nav className="nav container"> |
| 163 | + <a className="nav-brand" href="#top"> |
| 164 | + <img src="assets/icon.png" alt="DualClip" width="28" height="28" className="nav-icon"/> |
| 165 | + <span className="nav-name">DualClip</span> |
| 166 | + </a> |
| 167 | + <div className="nav-links"> |
| 168 | + <a href="#how">How it works</a> |
| 169 | + <a href="#features">Features</a> |
| 170 | + <a href="#compare">vs History</a> |
| 171 | + <a href="#install">Install</a> |
| 172 | + </div> |
| 173 | + <div className="nav-actions"> |
| 174 | + <button |
| 175 | + className="nav-theme" |
| 176 | + onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')} |
| 177 | + aria-label="Toggle theme" |
| 178 | + > |
| 179 | + {theme === 'dark' ? <Icon.Sun size={14}/> : <Icon.Moon size={14}/>} |
| 180 | + </button> |
| 181 | + <a className="nav-github" href="https://github.com/RAKKUNN/DualClip" target="_blank" rel="noreferrer"> |
| 182 | + <Icon.Github size={16}/> |
| 183 | + </a> |
| 184 | + </div> |
| 185 | + </nav> |
| 186 | + ); |
| 187 | +}; |
| 188 | + |
| 189 | +Object.assign(window, { Hero, Nav, MenuBarPopover }); |
0 commit comments