diff --git a/app/electron/main.test.ts b/app/electron/main.test.ts index c7616c1b..88cd1352 100644 --- a/app/electron/main.test.ts +++ b/app/electron/main.test.ts @@ -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' diff --git a/app/electron/main.ts b/app/electron/main.ts index 75fa2c35..43a41747 100644 --- a/app/electron/main.ts +++ b/app/electron/main.ts @@ -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[] { diff --git a/app/electron/preload.ts b/app/electron/preload.ts index af40399c..e835a867 100644 --- a/app/electron/preload.ts +++ b/app/electron/preload.ts @@ -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, } diff --git a/app/renderer/components/AboutModal.tsx b/app/renderer/components/AboutModal.tsx new file mode 100644 index 00000000..fb4d196b --- /dev/null +++ b/app/renderer/components/AboutModal.tsx @@ -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, 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 ( +
+
event.stopPropagation()} + > + +
+
+ +
CodeBurn
+
v{version}
+
Know where every token goes, across every AI coding tool.
+
+
+ +
+
Updates
+ +
+
+
+
Developed by Resham Joshi · github.com/iamtoruk
+
+
+ ) +} diff --git a/app/renderer/components/Sidebar.tsx b/app/renderer/components/Sidebar.tsx index ee429034..3881db18 100644 --- a/app/renderer/components/Sidebar.tsx +++ b/app/renderer/components/Sidebar.tsx @@ -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: }, - { label: 'Discord', icon: }, - { label: 'X', icon: }, - { label: 'YouTube', icon: }, +const SOCIALS: SocialLink[] = [ + { label: 'GitHub', url: 'https://github.com/getagentseal/codeburn', icon: }, + { label: 'Discord', url: 'https://discord.com/invite/w2sw8mCqep', icon: }, + { label: 'X', url: 'https://x.com/_codeburn', icon: }, + { label: 'YouTube', url: 'https://www.youtube.com/@codeburnn', icon: }, + { label: 'LinkedIn', url: 'https://www.linkedin.com/showcase/codeburnn/', icon: }, ] export function Sidebar({ @@ -46,34 +49,47 @@ export function Sidebar({ onNavigate: (section: Section) => void status?: ReactNode }) { + const [aboutOpen, setAboutOpen] = useState(false) + return ( - + {aboutOpen ? setAboutOpen(false)} /> : null} + ) } diff --git a/app/renderer/lib/types.ts b/app/renderer/lib/types.ts index e53b4d59..3b718602 100644 --- a/app/renderer/lib/types.ts +++ b/app/renderer/lib/types.ts @@ -461,4 +461,5 @@ export interface CodeburnBridge { exportData(format: string, provider: string, outPath: string): Promise chooseDirectory(): Promise cliStatus(): Promise<{ found: boolean; path: string | null; error?: string }> + openExternal(url: string): Promise } diff --git a/app/renderer/styles/plain.css b/app/renderer/styles/plain.css index ba398337..751ebb44 100644 --- a/app/renderer/styles/plain.css +++ b/app/renderer/styles/plain.css @@ -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; }