ui: trim terminal commands, open every link in a new tab
Keep /clear, /email, /copy, /linkedin, /github, /places, /mode, /lock, /exit. Email (terminal and page) and links in answers open in a new tab.
3 files changed, +11 -52
+6-51src/lib/gcs/console.ts
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | // Docked terminal, bottom right, styled after the Claude CLI. Plain text is a |
| 2 | 2 | // question about Lane: it streams from DeepSeek through the worker |
| 3 | -// (/__ai/chat), which grounds the model in /llms.txt. Slash commands navigate | |
| 4 | -// the page, open contact links, and drive the camera feed. "/" or "`" opens | |
| 3 | +// (/__ai/chat), which grounds the model in /llms.txt. Slash commands open | |
| 4 | +// contact links (always in a new tab), list places, and drive the camera feed. "/" or "`" opens | |
| 5 | 5 | // it; Esc closes the slash menu, then interrupts an answer, then closes. |
| 6 | 6 | // The conversation survives reloads for the tab session (sessionStorage). |
| 7 | 7 | import { track } from '../track'; |
| @@ -41,10 +41,8 @@ function linkify(el: HTMLElement, text: string) { | ||
| 41 | 41 | a.href = isMail ? `mailto:${m[0]}` : m[0]; |
| 42 | 42 | a.textContent = m[0]; |
| 43 | 43 | a.className = 'term-link'; |
| 44 | - if (!isMail) { | |
| 45 | - a.target = '_blank'; | |
| 46 | - a.rel = 'noopener'; | |
| 47 | - } | |
| 44 | + a.target = '_blank'; | |
| 45 | + a.rel = 'noopener'; | |
| 48 | 46 | a.addEventListener('click', () => track('console_link', { kind: isMail ? 'email' : 'url' })); |
| 49 | 47 | parts.push(a); |
| 50 | 48 | last = m.index! + m[0].length; |
| @@ -150,30 +148,8 @@ export function initConsole() { | ||
| 150 | 148 | }; |
| 151 | 149 | |
| 152 | 150 | const isOpen = () => !panel.hidden; |
| 153 | - const narrow = () => window.matchMedia('(max-width: 767px)').matches; | |
| 154 | - | |
| 155 | - // Scroll to a section. Skills falls back to Work where the Skills panel is | |
| 156 | - // replaced by the right-hand column. Small screens close the terminal so the | |
| 157 | - // section is visible. | |
| 158 | - const goTo = (id: string) => { | |
| 159 | - let el = document.getElementById(id); | |
| 160 | - if (el && getComputedStyle(el).display === 'none') el = document.getElementById('work'); | |
| 161 | - if (narrow()) close(); | |
| 162 | - el?.scrollIntoView({ behavior: reduce ? 'auto' : 'smooth' }); | |
| 163 | - print(`Scrolled to ${id}.`, 'muted'); | |
| 164 | - }; | |
| 165 | 151 | |
| 166 | 152 | const commands: Record<string, Command> = { |
| 167 | - help: { | |
| 168 | - help: 'list commands and shortcuts', | |
| 169 | - run: () => { | |
| 170 | - print('Type a question to ask about Lane, or a command:', 'muted'); | |
| 171 | - for (const [name, c] of Object.entries(commands)) { | |
| 172 | - if (c.help) print(` /${(c.usage ?? name).padEnd(20)} ${c.help}`, 'muted'); | |
| 173 | - } | |
| 174 | - print(' ↑ ↓ history tab completes esc interrupts or closes', 'muted'); | |
| 175 | - }, | |
| 176 | - }, | |
| 177 | 153 | clear: { |
| 178 | 154 | help: 'start a new conversation', |
| 179 | 155 | run: () => { |
| @@ -187,7 +163,7 @@ export function initConsole() { | ||
| 187 | 163 | run: () => { |
| 188 | 164 | print(`Opening mail to ${EMAIL}`, 'vol'); |
| 189 | 165 | track('contact_click', { channel: 'email', location: 'console' }); |
| 190 | - window.location.href = `mailto:${EMAIL}`; | |
| 166 | + window.open(`mailto:${EMAIL}`, '_blank', 'noopener'); | |
| 191 | 167 | }, |
| 192 | 168 | }, |
| 193 | 169 | copy: { |
| @@ -219,16 +195,6 @@ export function initConsole() { | ||
| 219 | 195 | window.open(GITHUB, '_blank', 'noopener'); |
| 220 | 196 | }, |
| 221 | 197 | }, |
| 222 | - work: { help: 'go to work and experience', run: () => goTo('work') }, | |
| 223 | - education: { help: 'go to education', run: () => goTo('education') }, | |
| 224 | - skills: { help: 'go to skills', run: () => goTo('skills') }, | |
| 225 | - contact: { help: 'go to contact', run: () => goTo('contact') }, | |
| 226 | - tracks: { | |
| 227 | - help: 'list every project', | |
| 228 | - run: () => { | |
| 229 | - for (const r of document.querySelectorAll<HTMLElement>('[data-trk]')) print(` ${r.dataset.trk} ${r.dataset.trkName}`); | |
| 230 | - }, | |
| 231 | - }, | |
| 232 | 198 | places: { |
| 233 | 199 | help: 'places Lane has lived and worked', |
| 234 | 200 | run: () => { |
| @@ -255,25 +221,14 @@ export function initConsole() { | ||
| 255 | 221 | print('DEV 01 reacquired', 'vol'); |
| 256 | 222 | }, |
| 257 | 223 | }, |
| 258 | - status: { | |
| 259 | - help: 'system status', | |
| 260 | - run: () => { | |
| 261 | - const feed = document.querySelector<HTMLElement>('.feed'); | |
| 262 | - print(`LINK OK UTC ${new Date().toISOString().slice(11, 19)}Z`); | |
| 263 | - print(`SENSOR 1 ${(feed?.dataset.mode ?? 'eo').toUpperCase()} ${document.querySelector('[data-tgt]')?.textContent ?? 'TRK 01'}`); | |
| 264 | - print(`CONTEXT ${convo.length} messages`); | |
| 265 | - }, | |
| 266 | - }, | |
| 267 | 224 | exit: { help: 'close the terminal', run: () => close() }, |
| 268 | - // Not listed in help. | |
| 269 | - giga: { run: () => (window.location.href = '/giga') }, | |
| 270 | 225 | }; |
| 271 | 226 | |
| 272 | 227 | function runCommand(raw: string) { |
| 273 | 228 | const [name, ...args] = raw.slice(1).toLowerCase().split(/\s+/); |
| 274 | 229 | const cmd = commands[name]; |
| 275 | 230 | track('console_command', { cmd: cmd ? name : 'unknown' }); |
| 276 | - if (!cmd) return void print(`Unknown command /${name}. Type /help for the list.`, 'err'); | |
| 231 | + if (!cmd) return void print(`Unknown command /${name}. Type / to see the commands.`, 'err'); | |
| 277 | 232 | try { |
| 278 | 233 | cmd.run(args); |
| 279 | 234 | } catch (err) { |
+4-0src/pages/index.astro
| @@ -349,6 +349,8 @@ const pad = (n: number) => String(n).padStart(2, '0'); | ||
| 349 | 349 | <div class="mt-9 flex flex-wrap items-center gap-3"> |
| 350 | 350 | <a |
| 351 | 351 | href={`mailto:${email}`} |
| 352 | + target="_blank" | |
| 353 | + rel="noopener" | |
| 352 | 354 | class={`inline-flex items-center rounded-[2px] bg-vol px-5 py-3 ${mono} font-bold text-black transition-colors hover:bg-vol-deep`} |
| 353 | 355 | data-track="contact_click" |
| 354 | 356 | data-track-channel="email" |
| @@ -524,6 +526,8 @@ const pad = (n: number) => String(n).padStart(2, '0'); | ||
| 524 | 526 | <a |
| 525 | 527 | class={`text-[clamp(1.4rem,6vw,2.25rem)] font-semibold font-stretch-[112%] text-vol ${textLink}`} |
| 526 | 528 | href={`mailto:${email}`} |
| 529 | + target="_blank" | |
| 530 | + rel="noopener" | |
| 527 | 531 | data-track="contact_click" |
| 528 | 532 | data-track-channel="email" |
| 529 | 533 | data-track-location="contact" |
+1-1worker/index.ts
| @@ -39,7 +39,7 @@ Rules: | ||
| 39 | 39 | - Never share a phone number or home address. |
| 40 | 40 | - For hiring, contracting, or contact questions, point to lane@henslee.me. |
| 41 | 41 | - Stay on topic: Lane, Lane's work and skills, and how to reach Lane. Politely decline unrelated tasks. |
| 42 | -- The terminal also has slash commands (/help lists them, /email opens mail, /copy copies the address). Mention them only when useful. | |
| 42 | +- The terminal also has slash commands (typing / lists them, /email opens mail, /copy copies the address). Mention them only when useful. | |
| 43 | 43 | |
| 44 | 44 | PROFILE |
| 45 | 45 | `; |