mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-20 05:54:25 +00:00
feat(app): About panel + working social links (+ LinkedIn)
- add a shell.openExternal bridge (main ipc handler + preload + renderer type) so links open in the default browser (requires a full relaunch) - wire the sidebar social icons to real URLs and add LinkedIn - AboutModal: flame + wordmark, version, tagline, the five social links, and a "Check for updates" link to the GitHub releases page typecheck clean; 128/128 tests pass.
This commit is contained in:
parent
428caab6fd
commit
6f4b2bef38
7 changed files with 194 additions and 34 deletions
|
|
@ -8,6 +8,7 @@ vi.mock('electron', () => ({
|
|||
dialog: { showOpenDialog: vi.fn() },
|
||||
ipcMain: { handle: () => {} },
|
||||
Menu: { buildFromTemplate: (template: unknown) => template, setApplicationMenu: () => {} },
|
||||
shell: { openExternal: vi.fn() },
|
||||
}))
|
||||
|
||||
import { createApplicationMenuTemplate, createBridgeHandlers } from './main'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { app, BrowserWindow, dialog, ipcMain, Menu, nativeTheme, type MenuItemConstructorOptions } from 'electron'
|
||||
import { app, BrowserWindow, dialog, ipcMain, Menu, nativeTheme, shell, type MenuItemConstructorOptions } from 'electron'
|
||||
import path from 'node:path'
|
||||
|
||||
import { CliError, resolveCodeburnPath, spawnCli, spawnCliAction, type ActionResult } from './cli'
|
||||
|
|
@ -107,6 +107,7 @@ function registerHandlers(): void {
|
|||
const res = await dialog.showOpenDialog({ properties: ['openDirectory', 'createDirectory'] })
|
||||
return { ok: true, value: res.canceled ? null : (res.filePaths[0] ?? null) }
|
||||
})
|
||||
ipcMain.handle('open-external', (_event, url: string) => shell.openExternal(url))
|
||||
}
|
||||
|
||||
export function createApplicationMenuTemplate(isDev = Boolean(process.env.VITE_DEV_SERVER_URL)): MenuItemConstructorOptions[] {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ const bridge = {
|
|||
exportData: (format: string, provider: string, outPath: string) => invoke('codeburn:exportData', format, provider, outPath),
|
||||
chooseDirectory: () => invoke('codeburn:chooseDirectory'),
|
||||
cliStatus: () => invoke('codeburn:cliStatus'),
|
||||
openExternal: (url: string) => ipcRenderer.invoke('open-external', url),
|
||||
platform: process.platform,
|
||||
}
|
||||
|
||||
|
|
|
|||
79
app/renderer/components/AboutModal.tsx
Normal file
79
app/renderer/components/AboutModal.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { useEffect, type MouseEvent, type ReactNode } from 'react'
|
||||
|
||||
import { version } from '../../package.json'
|
||||
import flameLogo from '../assets/flame.png'
|
||||
import { codeburn } from '../lib/ipc'
|
||||
|
||||
export type SocialLink = {
|
||||
label: string
|
||||
url: string
|
||||
icon: ReactNode
|
||||
}
|
||||
|
||||
const RELEASES_URL = 'https://github.com/getagentseal/codeburn/releases'
|
||||
|
||||
function openExternal(event: MouseEvent<HTMLAnchorElement>, url: string): void {
|
||||
event.preventDefault()
|
||||
void codeburn.openExternal(url)
|
||||
}
|
||||
|
||||
export function AboutModal({ socials, onClose }: { socials: SocialLink[]; onClose: () => void }) {
|
||||
useEffect(() => {
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') onClose()
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', onKeyDown)
|
||||
return () => document.removeEventListener('keydown', onKeyDown)
|
||||
}, [onClose])
|
||||
|
||||
return (
|
||||
<div className="about-modal-backdrop" onClick={onClose}>
|
||||
<div
|
||||
className="about-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="about-modal-title"
|
||||
onClick={event => event.stopPropagation()}
|
||||
>
|
||||
<button className="about-modal-close" type="button" aria-label="Close About" onClick={onClose}>×</button>
|
||||
<div className="about-modal-grid">
|
||||
<div className="about-modal-hero">
|
||||
<img className="about-modal-logo" src={flameLogo} alt="" />
|
||||
<div className="about-modal-name" id="about-modal-title">CodeBurn</div>
|
||||
<div className="about-modal-version">v{version}</div>
|
||||
<div className="about-modal-tagline">Know where every token goes, across every AI coding tool.</div>
|
||||
</div>
|
||||
<div className="about-modal-side">
|
||||
<div className="about-modal-section">
|
||||
<div className="about-modal-section-title">Links</div>
|
||||
{socials.map(social => (
|
||||
<a
|
||||
className="about-modal-link"
|
||||
href={social.url}
|
||||
key={social.label}
|
||||
onClick={event => openExternal(event, social.url)}
|
||||
>
|
||||
{social.icon}
|
||||
<span>{social.label}</span>
|
||||
<span className="about-modal-external" aria-hidden="true">↗</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div className="about-modal-section about-modal-updates">
|
||||
<div className="about-modal-section-title">Updates</div>
|
||||
<button
|
||||
className="about-modal-update-button"
|
||||
type="button"
|
||||
onClick={() => { void codeburn.openExternal(RELEASES_URL) }}
|
||||
>
|
||||
Check for updates
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="about-modal-credit">Developed by Resham Joshi · github.com/iamtoruk</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import type { ReactNode } from 'react'
|
||||
import { useState, type ReactNode } from 'react'
|
||||
|
||||
import flameLogo from '../assets/flame.png'
|
||||
import { codeburn } from '../lib/ipc'
|
||||
import { AboutModal, type SocialLink } from './AboutModal'
|
||||
|
||||
export type Section = 'overview' | 'sessions' | 'spend' | 'optimize' | 'models' | 'compare' | 'plans' | 'settings'
|
||||
|
||||
|
|
@ -31,11 +33,12 @@ export const NAV_ITEMS: Array<{ id: Section; label: string; key: string; icon: R
|
|||
) },
|
||||
]
|
||||
|
||||
const SOCIALS: Array<{ label: string; icon: ReactNode }> = [
|
||||
{ label: 'GitHub', icon: <svg viewBox="0 0 24 24"><path d="M12 .5C5.37.5 0 5.87 0 12.5c0 5.3 3.44 9.8 8.21 11.39.6.11.82-.26.82-.58 0-.29-.01-1.05-.02-2.06-3.34.73-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.09-.75.08-.73.08-.73 1.2.09 1.84 1.24 1.84 1.24 1.07 1.83 2.81 1.3 3.5.99.11-.78.42-1.3.76-1.6-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.12-.3-.54-1.52.12-3.18 0 0 1.01-.32 3.3 1.23a11.5 11.5 0 0 1 6 0c2.29-1.55 3.3-1.23 3.3-1.23.66 1.66.24 2.88.12 3.18.77.84 1.23 1.91 1.23 3.22 0 4.61-2.8 5.62-5.48 5.92.43.37.81 1.1.81 2.22 0 1.6-.01 2.9-.01 3.29 0 .32.22.7.83.58A12 12 0 0 0 24 12.5C24 5.87 18.63.5 12 .5z" /></svg> },
|
||||
{ label: 'Discord', icon: <svg viewBox="0 0 24 24"><path d="M20.32 4.37A19.8 19.8 0 0 0 15.45 3c-.21.38-.46.9-.63 1.31a18.3 18.3 0 0 0-5.47 0C8.71 3.9 8.45 3.38 8.24 3a19.7 19.7 0 0 0-4.88 1.37C.86 8.75.05 13.02.45 17.23a19.9 19.9 0 0 0 6 3.03c.48-.66.91-1.36 1.28-2.11-.7-.26-1.37-.58-2-.96.17-.12.33-.25.49-.38a14.2 14.2 0 0 0 12.16 0c.16.14.32.26.49.38-.63.38-1.31.7-2 .96.37.75.8 1.45 1.28 2.11a19.8 19.8 0 0 0 6-3.03c.47-4.87-.8-9.1-3.83-12.86zM8.02 14.65c-1.18 0-2.15-1.08-2.15-2.41 0-1.33.95-2.42 2.15-2.42 1.2 0 2.17 1.09 2.15 2.42 0 1.33-.95 2.41-2.15 2.41zm7.96 0c-1.18 0-2.15-1.08-2.15-2.41 0-1.33.95-2.42 2.15-2.42 1.2 0 2.17 1.09 2.15 2.42 0 1.33-.95 2.41-2.15 2.41z" /></svg> },
|
||||
{ label: 'X', icon: <svg viewBox="0 0 24 24"><path d="M18.9 1.15h3.68l-8.04 9.19L24 22.85h-7.4l-5.8-7.58-6.63 7.58H.49l8.6-9.83L0 1.15h7.59l5.24 6.93 6.07-6.93zm-1.29 19.5h2.04L6.49 3.24H4.3l13.31 17.41z" /></svg> },
|
||||
{ label: 'YouTube', icon: <svg viewBox="0 0 24 24"><path d="M23.5 6.2a3.02 3.02 0 0 0-2.12-2.14C19.5 3.5 12 3.5 12 3.5s-7.5 0-9.38.56A3.02 3.02 0 0 0 .5 6.2 31.5 31.5 0 0 0 0 12a31.5 31.5 0 0 0 .5 5.8 3.02 3.02 0 0 0 2.12 2.14C4.5 20.5 12 20.5 12 20.5s7.5 0 9.38-.56A3.02 3.02 0 0 0 23.5 17.8 31.5 31.5 0 0 0 24 12a31.5 31.5 0 0 0-.5-5.8zM9.55 15.57V8.43L15.82 12l-6.27 3.57z" /></svg> },
|
||||
const SOCIALS: SocialLink[] = [
|
||||
{ label: 'GitHub', url: 'https://github.com/getagentseal/codeburn', icon: <svg viewBox="0 0 24 24"><path d="M12 .5C5.37.5 0 5.87 0 12.5c0 5.3 3.44 9.8 8.21 11.39.6.11.82-.26.82-.58 0-.29-.01-1.05-.02-2.06-3.34.73-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.09-.75.08-.73.08-.73 1.2.09 1.84 1.24 1.84 1.24 1.07 1.83 2.81 1.3 3.5.99.11-.78.42-1.3.76-1.6-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.12-.3-.54-1.52.12-3.18 0 0 1.01-.32 3.3 1.23a11.5 11.5 0 0 1 6 0c2.29-1.55 3.3-1.23 3.3-1.23.66 1.66.24 2.88.12 3.18.77.84 1.23 1.91 1.23 3.22 0 4.61-2.8 5.62-5.48 5.92.43.37.81 1.1.81 2.22 0 1.6-.01 2.9-.01 3.29 0 .32.22.7.83.58A12 12 0 0 0 24 12.5C24 5.87 18.63.5 12 .5z" /></svg> },
|
||||
{ label: 'Discord', url: 'https://discord.com/invite/w2sw8mCqep', icon: <svg viewBox="0 0 24 24"><path d="M20.32 4.37A19.8 19.8 0 0 0 15.45 3c-.21.38-.46.9-.63 1.31a18.3 18.3 0 0 0-5.47 0C8.71 3.9 8.45 3.38 8.24 3a19.7 19.7 0 0 0-4.88 1.37C.86 8.75.05 13.02.45 17.23a19.9 19.9 0 0 0 6 3.03c.48-.66.91-1.36 1.28-2.11-.7-.26-1.37-.58-2-.96.17-.12.33-.25.49-.38a14.2 14.2 0 0 0 12.16 0c.16.14.32.26.49.38-.63.38-1.31.7-2 .96.37.75.8 1.45 1.28 2.11a19.8 19.8 0 0 0 6-3.03c.47-4.87-.8-9.1-3.83-12.86zM8.02 14.65c-1.18 0-2.15-1.08-2.15-2.41 0-1.33.95-2.42 2.15-2.42 1.2 0 2.17 1.09 2.15 2.42 0 1.33-.95 2.41-2.15 2.41zm7.96 0c-1.18 0-2.15-1.08-2.15-2.41 0-1.33.95-2.42 2.15-2.42 1.2 0 2.17 1.09 2.15 2.42 0 1.33-.95 2.41-2.15 2.41z" /></svg> },
|
||||
{ label: 'X', url: 'https://x.com/_codeburn', icon: <svg viewBox="0 0 24 24"><path d="M18.9 1.15h3.68l-8.04 9.19L24 22.85h-7.4l-5.8-7.58-6.63 7.58H.49l8.6-9.83L0 1.15h7.59l5.24 6.93 6.07-6.93zm-1.29 19.5h2.04L6.49 3.24H4.3l13.31 17.41z" /></svg> },
|
||||
{ label: 'YouTube', url: 'https://www.youtube.com/@codeburnn', icon: <svg viewBox="0 0 24 24"><path d="M23.5 6.2a3.02 3.02 0 0 0-2.12-2.14C19.5 3.5 12 3.5 12 3.5s-7.5 0-9.38.56A3.02 3.02 0 0 0 .5 6.2 31.5 31.5 0 0 0 0 12a31.5 31.5 0 0 0 .5 5.8 3.02 3.02 0 0 0 2.12 2.14C4.5 20.5 12 20.5 12 20.5s7.5 0 9.38-.56A3.02 3.02 0 0 0 23.5 17.8 31.5 31.5 0 0 0 24 12a31.5 31.5 0 0 0-.5-5.8zM9.55 15.57V8.43L15.82 12l-6.27 3.57z" /></svg> },
|
||||
{ label: 'LinkedIn', url: 'https://www.linkedin.com/showcase/codeburnn/', icon: <svg viewBox="0 0 256 256"><path fill="currentColor" d="M218.123 218.127h-37.931v-59.403c0-14.165-.253-32.4-19.728-32.4-19.756 0-22.779 15.434-22.779 31.369v60.43h-37.93V95.967h36.413v16.694h.51a39.907 39.907 0 0 1 35.928-19.733c38.445 0 45.533 25.288 45.533 58.186l-.016 67.013ZM56.955 79.27c-12.157.002-22.014-9.852-22.016-22.009-.002-12.157 9.851-22.014 22.008-22.016 12.157-.003 22.014 9.851 22.016 22.008A22.013 22.013 0 0 1 56.955 79.27m18.966 138.858H37.95V95.967h37.97v122.16ZM237.033.018H18.89C8.58-.098.125 8.161-.001 18.471v219.053c.122 10.315 8.576 18.582 18.89 18.474h218.144c10.336.128 18.823-8.139 18.966-18.474V18.454c-.147-10.33-8.635-18.588-18.966-18.453" /></svg> },
|
||||
]
|
||||
|
||||
export function Sidebar({
|
||||
|
|
@ -46,34 +49,47 @@ export function Sidebar({
|
|||
onNavigate: (section: Section) => void
|
||||
status?: ReactNode
|
||||
}) {
|
||||
const [aboutOpen, setAboutOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<nav className="sb">
|
||||
<div className="app"><img className="logo" src={flameLogo} alt="" /><b>CodeBurn</b></div>
|
||||
{NAV_ITEMS.map(item => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={item.id === active ? 'ni on' : 'ni'}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onNavigate(item.id)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter' || e.key === ' ') onNavigate(item.id)
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
<span className="k">{item.key}</span>
|
||||
<>
|
||||
<nav className="sb">
|
||||
<div className="app"><img className="logo" src={flameLogo} alt="" /><b>CodeBurn</b></div>
|
||||
{NAV_ITEMS.map(item => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={item.id === active ? 'ni on' : 'ni'}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onNavigate(item.id)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter' || e.key === ' ') onNavigate(item.id)
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
<span className="k">{item.key}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="push" />
|
||||
<div className="foot">
|
||||
<a className="about" href="#about" onClick={event => { event.preventDefault(); setAboutOpen(true) }}>About</a>
|
||||
<div className="social">
|
||||
{SOCIALS.map(social => (
|
||||
<a
|
||||
key={social.label}
|
||||
href={social.url}
|
||||
title={social.label}
|
||||
aria-label={social.label}
|
||||
onClick={event => { event.preventDefault(); void codeburn.openExternal(social.url) }}
|
||||
>
|
||||
{social.icon}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="push" />
|
||||
<div className="foot">
|
||||
<a className="about">About</a>
|
||||
<div className="social">
|
||||
{SOCIALS.map(social => (
|
||||
<a key={social.label} title={social.label} aria-label={social.label}>{social.icon}</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</nav>
|
||||
{aboutOpen ? <AboutModal socials={SOCIALS} onClose={() => setAboutOpen(false)} /> : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -461,4 +461,5 @@ export interface CodeburnBridge {
|
|||
exportData(format: string, provider: string, outPath: string): Promise<ActionResult>
|
||||
chooseDirectory(): Promise<string | null>
|
||||
cliStatus(): Promise<{ found: boolean; path: string | null; error?: string }>
|
||||
openExternal(url: string): Promise<void>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,6 +156,67 @@ body { overflow: hidden; background: var(--bg); color: var(--ink); }
|
|||
.foot .social a:hover { color: var(--accent); background: var(--hover); }
|
||||
.foot .social svg { width: 15px; height: 15px; fill: currentColor; stroke: none; }
|
||||
|
||||
/* About overlay: a compact, portal-free card using the reskin's surface tokens. */
|
||||
.about-modal-backdrop {
|
||||
position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; padding: 24px;
|
||||
background: rgba(8, 10, 14, .56);
|
||||
}
|
||||
.about-modal {
|
||||
position: relative; width: min(720px, 100%); overflow: hidden; border: 1px solid var(--line);
|
||||
border-radius: 12px; background: var(--panel); color: var(--ink);
|
||||
box-shadow: 0 18px 50px rgba(0, 0, 0, .28), 0 3px 12px rgba(0, 0, 0, .16);
|
||||
}
|
||||
.about-modal-close {
|
||||
position: absolute; top: 10px; right: 10px; z-index: 1; width: 28px; height: 28px; padding: 0;
|
||||
border: 0; border-radius: 7px; background: transparent; color: var(--mut); font: inherit;
|
||||
font-size: 20px; line-height: 1; cursor: pointer;
|
||||
}
|
||||
.about-modal-close:hover { background: var(--hover); color: var(--ink); }
|
||||
.about-modal-close:focus-visible,
|
||||
.about-modal-link:focus-visible,
|
||||
.about-modal-update-button:focus-visible { outline: 1px solid var(--accent); outline-offset: 2px; }
|
||||
.about-modal-grid { display: grid; grid-template-columns: 1fr 1fr; }
|
||||
.about-modal-hero {
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 32px 24px;
|
||||
border-right: 1px solid var(--line2); text-align: center;
|
||||
}
|
||||
.about-modal-logo { width: 52px; height: 52px; margin-bottom: 12px; object-fit: contain; }
|
||||
.about-modal-name { font-size: 16px; font-weight: 650; letter-spacing: -.01em; }
|
||||
.about-modal-version { margin-top: 5px; color: var(--mut2); font-size: 11.5px; font-variant-numeric: tabular-nums; }
|
||||
.about-modal-tagline { max-width: 32ch; margin: 11px auto 0; color: var(--mut); font-size: 12.5px; line-height: 1.5; }
|
||||
.about-modal-side { display: flex; flex-direction: column; }
|
||||
.about-modal-section { padding: 14px 18px; border-bottom: 1px solid var(--line2); }
|
||||
.about-modal-section:last-child { border-bottom: 0; }
|
||||
.about-modal-section-title {
|
||||
margin-bottom: 8px; color: var(--mut2); font-size: 10.5px; font-weight: 600;
|
||||
letter-spacing: .05em; text-transform: uppercase;
|
||||
}
|
||||
.about-modal-link {
|
||||
position: relative; display: flex; align-items: center; gap: 11px; padding: 8px 0;
|
||||
color: var(--ink); font-size: 12.5px; text-decoration: none;
|
||||
}
|
||||
.about-modal-link + .about-modal-link::before {
|
||||
content: ''; position: absolute; top: 0; right: 0; left: 27px; height: 1px; background: var(--line2);
|
||||
}
|
||||
.about-modal-link svg { width: 16px; height: 16px; flex: 0 0 auto; fill: currentColor; stroke: none; }
|
||||
.about-modal-link:hover { color: var(--accent); }
|
||||
.about-modal-external { margin-left: auto; color: var(--mut2); }
|
||||
.about-modal-updates { margin-top: auto; }
|
||||
.about-modal-update-button {
|
||||
display: inline-flex; padding: 6px 11px; border: 1px solid var(--line); border-radius: 7px;
|
||||
background: var(--panel); color: var(--ink); font: inherit; font-size: 11.5px; font-weight: 540; cursor: pointer;
|
||||
}
|
||||
.about-modal-update-button:hover { border-color: var(--accent); color: var(--accent); }
|
||||
.about-modal-credit {
|
||||
padding: 13px 18px; border-top: 1px solid var(--line2); color: var(--mut2);
|
||||
font-size: 10.5px; text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.about-modal-grid { grid-template-columns: 1fr; }
|
||||
.about-modal-hero { padding: 24px; border-right: 0; border-bottom: 1px solid var(--line2); }
|
||||
}
|
||||
|
||||
/* macOS integrated title bar (hiddenInset): clear the traffic lights and make
|
||||
the top chrome draggable, keeping interactive controls clickable. */
|
||||
:root[data-platform='darwin'] .sb { padding-top: 34px; -webkit-app-region: drag; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue