mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-21 05:23:19 +00:00
feat(app): two switches where the social glyphs were
The sidebar's bottom corner held About and a row of five brand glyphs. The glyphs were the least useful thing on that screen: they are visited once, and About already listed every one of them under Links. So they are only there now, and the corner carries the two things a Windows user does want at hand: whether CodeBurn sits in the notification area, and whether the Capacity Dock rail is on the screen edge. Both are on by default and neither renders at all until the main process says this build has a tray app staged, which is why there is no placeholder row and no greyed-out switch: on macOS, on Linux, and in a dev build with nothing staged, the corner is exactly what it always was. The switch is the shared one from Settings and onboarding, at 30x17 rather than 38x22 because a 186px sidebar has no room for the full size beside a label, and keyed off the theme tokens rather than the white overlays the bare shape uses on the dark onboarding sheet.
This commit is contained in:
parent
c625d020e6
commit
2bd44113c7
4 changed files with 205 additions and 31 deletions
|
|
@ -12,12 +12,23 @@ export type SocialLink = {
|
|||
icon: ReactNode
|
||||
}
|
||||
|
||||
/** Where CodeBurn lives on the web. These used to sit as a row of glyphs in the sidebar's
|
||||
* bottom corner, which is where the two Windows switches are now; About was already listing
|
||||
* them under Links, so that is the one place they appear. */
|
||||
export 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> },
|
||||
]
|
||||
|
||||
function openExternal(event: MouseEvent<HTMLAnchorElement>, url: string): void {
|
||||
event.preventDefault()
|
||||
void codeburn.openExternal(url)
|
||||
}
|
||||
|
||||
export function AboutModal({ socials, onClose }: { socials: SocialLink[]; onClose: () => void }) {
|
||||
export function AboutModal({ socials = SOCIALS, onClose }: { socials?: SocialLink[]; onClose: () => void }) {
|
||||
const status = useUpdateStatus()
|
||||
const [checked, setChecked] = useState(false)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,31 @@
|
|||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, it, expect, vi } from 'vitest'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, it, expect, vi } from 'vitest'
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||
|
||||
import { Sidebar } from './Sidebar'
|
||||
|
||||
// The preload bridge is read once at module load, so the switches need it mocked rather than
|
||||
// assigned onto `window` after the fact.
|
||||
const bridge = vi.hoisted(() => ({
|
||||
companionStatus: vi.fn(),
|
||||
setMenuBarEnabled: vi.fn(),
|
||||
setSidebarEnabled: vi.fn(),
|
||||
openExternal: vi.fn(),
|
||||
}))
|
||||
vi.mock('../lib/ipc', () => ({ codeburn: bridge, normalizeCliError: (err: unknown) => err }))
|
||||
|
||||
function setPlatform(platform: string): void {
|
||||
;(window as unknown as { codeburn?: { platform?: string } }).codeburn = { platform }
|
||||
}
|
||||
|
||||
describe('Sidebar', () => {
|
||||
beforeEach(() => {
|
||||
bridge.companionStatus.mockResolvedValue({ supported: false, menuBar: false, sidebar: false, store: false })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
delete (window as unknown as { codeburn?: { platform?: string } }).codeburn
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it.each([
|
||||
|
|
@ -48,4 +63,94 @@ describe('Sidebar', () => {
|
|||
// motionEnabled() is off under vitest, so the idle flicker never attaches.
|
||||
expect(container.querySelector('.fm-flicker')).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps About in the corner and the social glyphs out of it', async () => {
|
||||
const { container } = render(<Sidebar active="overview" onNavigate={() => {}} />)
|
||||
expect(screen.getByRole('link', { name: 'About' })).toBeInTheDocument()
|
||||
expect(container.querySelector('.foot .social')).toBeNull()
|
||||
// They moved into the About modal, which lists them under Links.
|
||||
fireEvent.click(screen.getByRole('link', { name: 'About' }))
|
||||
expect(await screen.findByRole('link', { name: /GitHub/ })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /LinkedIn/ })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Sidebar companion switches', () => {
|
||||
const SUPPORTED = { supported: true, menuBar: true, sidebar: true, store: false }
|
||||
|
||||
afterEach(() => { vi.clearAllMocks() })
|
||||
|
||||
async function renderSwitches(status = SUPPORTED) {
|
||||
bridge.companionStatus.mockResolvedValue(status)
|
||||
render(<Sidebar active="overview" onNavigate={() => {}} />)
|
||||
return screen.findByRole('switch', { name: 'Menu bar' })
|
||||
}
|
||||
|
||||
it('shows nothing where the main process reports no bundled tray app', async () => {
|
||||
bridge.companionStatus.mockResolvedValue({ supported: false, menuBar: false, sidebar: false, store: false })
|
||||
render(<Sidebar active="overview" onNavigate={() => {}} />)
|
||||
|
||||
await waitFor(() => expect(bridge.companionStatus).toHaveBeenCalled())
|
||||
expect(screen.queryByRole('switch')).toBeNull()
|
||||
})
|
||||
|
||||
it('survives a preload that has never heard of them', async () => {
|
||||
bridge.companionStatus.mockRejectedValue(new Error('no such channel'))
|
||||
render(<Sidebar active="overview" onNavigate={() => {}} />)
|
||||
|
||||
await waitFor(() => expect(bridge.companionStatus).toHaveBeenCalled())
|
||||
expect(screen.queryByRole('switch')).toBeNull()
|
||||
})
|
||||
|
||||
it('renders both switches on, in the sidebar corner', async () => {
|
||||
const menuBar = await renderSwitches()
|
||||
|
||||
expect(menuBar).toHaveAttribute('aria-checked', 'true')
|
||||
expect(screen.getByRole('switch', { name: 'Sidebar' })).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('turning Menu bar off sends false and renders the status that came back', async () => {
|
||||
const menuBar = await renderSwitches()
|
||||
bridge.setMenuBarEnabled.mockResolvedValue({ ...SUPPORTED, menuBar: false })
|
||||
|
||||
fireEvent.click(menuBar)
|
||||
|
||||
expect(bridge.setMenuBarEnabled).toHaveBeenCalledWith(false)
|
||||
await waitFor(() => expect(menuBar).toHaveAttribute('aria-checked', 'false'))
|
||||
expect(screen.getByRole('switch', { name: 'Sidebar' })).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('turning Sidebar off leaves Menu bar alone', async () => {
|
||||
await renderSwitches()
|
||||
bridge.setSidebarEnabled.mockResolvedValue({ ...SUPPORTED, sidebar: false })
|
||||
|
||||
fireEvent.click(screen.getByRole('switch', { name: 'Sidebar' }))
|
||||
|
||||
expect(bridge.setSidebarEnabled).toHaveBeenCalledWith(false)
|
||||
await waitFor(() => expect(screen.getByRole('switch', { name: 'Sidebar' })).toHaveAttribute('aria-checked', 'false'))
|
||||
expect(screen.getByRole('switch', { name: 'Menu bar' })).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
/// An install the person cancelled at the UAC prompt comes back unchanged, and the switch
|
||||
/// has to show that rather than the state it optimistically painted.
|
||||
it('stays where it was when the main process reports no change', async () => {
|
||||
const menuBar = await renderSwitches({ ...SUPPORTED, menuBar: false })
|
||||
bridge.setMenuBarEnabled.mockResolvedValue({ ...SUPPORTED, menuBar: false })
|
||||
|
||||
fireEvent.click(menuBar)
|
||||
|
||||
await waitFor(() => expect(bridge.setMenuBarEnabled).toHaveBeenCalledWith(true))
|
||||
expect(menuBar).toHaveAttribute('aria-checked', 'false')
|
||||
})
|
||||
|
||||
it('refuses a second click while one is still in flight', async () => {
|
||||
const menuBar = await renderSwitches()
|
||||
bridge.setMenuBarEnabled.mockReturnValue(new Promise(() => {}))
|
||||
|
||||
fireEvent.click(menuBar)
|
||||
fireEvent.click(screen.getByRole('switch', { name: 'Sidebar' }))
|
||||
|
||||
expect(bridge.setMenuBarEnabled).toHaveBeenCalledTimes(1)
|
||||
expect(bridge.setSidebarEnabled).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { useState, type ReactNode } from 'react'
|
||||
import { useEffect, useState, type ReactNode } from 'react'
|
||||
|
||||
import { codeburn } from '../lib/ipc'
|
||||
import { shortcutLabel } from '../lib/platform'
|
||||
import { AboutModal, type SocialLink } from './AboutModal'
|
||||
import type { CompanionStatus } from '../lib/types'
|
||||
import { AboutModal } from './AboutModal'
|
||||
import { FlameMark } from './FlameMark'
|
||||
|
||||
export type Section = 'overview' | 'sessions' | 'pullRequests' | 'spend' | 'optimize' | 'models' | 'compare' | 'plans' | 'settings' | 'plugins'
|
||||
|
|
@ -40,14 +41,6 @@ export const NAV_ITEMS: Array<{ id: Section; label: string; key: string; icon: R
|
|||
) },
|
||||
]
|
||||
|
||||
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({
|
||||
active,
|
||||
onNavigate,
|
||||
|
|
@ -83,24 +76,76 @@ export function Sidebar({
|
|||
</div>
|
||||
))}
|
||||
<div className="push" />
|
||||
<CompanionSwitches />
|
||||
<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>
|
||||
</nav>
|
||||
{aboutOpen ? <AboutModal socials={SOCIALS} onClose={() => setAboutOpen(false)} /> : null}
|
||||
{aboutOpen ? <AboutModal onClose={() => setAboutOpen(false)} /> : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The two surfaces the Windows desktop app carries besides its own window: the tray app
|
||||
* ("Menu bar") and the Capacity Dock rail it draws ("Sidebar"). Both are on by default and
|
||||
* live above About, where the social glyphs used to be.
|
||||
*
|
||||
* Nothing renders until the main process says this build has a tray app staged, which is why
|
||||
* there is no placeholder row and no disabled switch: on macOS, on Linux, and in a dev build
|
||||
* with nothing staged, the corner is exactly what it always was.
|
||||
*/
|
||||
function CompanionSwitches() {
|
||||
const [status, setStatus] = useState<CompanionStatus | null>(null)
|
||||
const [busy, setBusy] = useState<'menuBar' | 'sidebar' | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let live = true
|
||||
// `codeburn` is the preload bridge, absent in a plain browser and under tests, and
|
||||
// `companionStatus` is absent on a preload that predates these two switches.
|
||||
void codeburn?.companionStatus?.()
|
||||
.then(next => { if (live) setStatus(next) })
|
||||
.catch(() => {})
|
||||
return () => { live = false }
|
||||
}, [])
|
||||
|
||||
if (!status?.supported) return null
|
||||
|
||||
// Every setter answers with the whole status, so a switch shows what took rather than what
|
||||
// was asked for: an install the person cancelled at the UAC prompt leaves it where it was.
|
||||
const toggle = (key: 'menuBar' | 'sidebar') => {
|
||||
if (busy) return
|
||||
const call = key === 'menuBar' ? codeburn.setMenuBarEnabled : codeburn.setSidebarEnabled
|
||||
if (!call) return
|
||||
setBusy(key)
|
||||
void call.call(codeburn, !status[key])
|
||||
.then(setStatus)
|
||||
.catch(() => {})
|
||||
.finally(() => setBusy(null))
|
||||
}
|
||||
|
||||
const row = (key: 'menuBar' | 'sidebar', label: string, hint: string) => (
|
||||
<div className="companion-row">
|
||||
<span className="companion-label" title={hint}>{label}</span>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={status[key]}
|
||||
aria-label={label}
|
||||
title={hint}
|
||||
disabled={busy !== null}
|
||||
className={status[key] ? 'switch sm on' : 'switch sm'}
|
||||
onClick={() => toggle(key)}
|
||||
>
|
||||
<span className="switch-knob" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="companion">
|
||||
{row('menuBar', 'Menu bar', 'Show CodeBurn in the Windows notification area')}
|
||||
{row('sidebar', 'Sidebar', 'Show the Capacity Dock rail on the screen edge')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,10 +130,18 @@ body { overflow: hidden; background: var(--bg); color: var(--ink); }
|
|||
.foot { display: flex; align-items: center; gap: 10px; margin-top: 4px; padding: 11px 8px 2px; border-top: 1px solid var(--line); }
|
||||
.foot .about { color: var(--mut); text-decoration: none; font-size: 12px; font-weight: 520; cursor: pointer; }
|
||||
.foot .about:hover { color: var(--ink); }
|
||||
.foot .social { display: flex; gap: 2px; margin-left: auto; }
|
||||
.foot .social a { color: var(--mut2); display: flex; padding: 5px; border-radius: 6px; cursor: pointer; }
|
||||
.foot .social a:hover { color: var(--accent); background: var(--hover); }
|
||||
.foot .social svg { width: 15px; height: 15px; fill: currentColor; stroke: none; }
|
||||
/* Windows only: the tray app and the Capacity Dock rail, above About. Same 8px gutter and
|
||||
12.5px type as a nav item, so the corner reads as one column rather than two. */
|
||||
.companion { padding: 8px 8px 2px; border-top: 1px solid var(--line); }
|
||||
.companion-row { display: flex; align-items: center; gap: 8px; padding: 3px 0; }
|
||||
.companion-label { color: var(--mut); font-size: 12px; font-weight: 480; }
|
||||
.companion-row .switch { margin-left: auto; }
|
||||
/* The bare .switch is painted for the dark onboarding sheet; on the sidebar it keys off the
|
||||
theme tokens, exactly as the Settings surface does. */
|
||||
.companion .switch { border-color: var(--line2); background: var(--hover); }
|
||||
.companion .switch .switch-knob { background: var(--mut); }
|
||||
.companion .switch.on { background: var(--accent); border-color: var(--accent); }
|
||||
.companion .switch.on .switch-knob { background: #fff; }
|
||||
|
||||
/* About overlay: a compact, portal-free card using the reskin's surface tokens. */
|
||||
.about-modal-backdrop {
|
||||
|
|
@ -1093,6 +1101,11 @@ html.page-hidden *::after {
|
|||
.switch.on { background: #f2701c; border-color: #f2701c; }
|
||||
.switch.on .switch-knob { transform: translateX(16px); background: #fff; }
|
||||
.switch:focus-visible { outline: 1px solid #f2701c; outline-offset: 1px; }
|
||||
/* Compact variant for the 186px sidebar, where the full-size switch crowds the label out. */
|
||||
.switch.sm { width: 30px; height: 17px; }
|
||||
.switch.sm .switch-knob { width: 12px; height: 12px; }
|
||||
.switch.sm.on .switch-knob { transform: translateX(13px); }
|
||||
.switch:disabled { opacity: .55; cursor: default; }
|
||||
/* On themed (Settings) surfaces the same switch keys off theme tokens. */
|
||||
.set-p .switch { border-color: var(--line2); background: var(--hover); }
|
||||
.set-p .switch .switch-knob { background: var(--mut); }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue