Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 33 additions & 7 deletions apps/electron/src/renderer/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -875,7 +875,7 @@ button:disabled { cursor: not-allowed; }
.trail-sidebar-brand-kicker { margin-top: .35rem; color: var(--foreground-subtle); font-family: var(--font-numeric-stack); font-size: .82rem; font-weight: 400; letter-spacing: 0; line-height: 1.3; text-transform: uppercase; }
.trail-sidebar-new-analysis { min-height: 2.7rem; font-size: .9rem; border-radius: var(--radius); background: var(--primary); color: var(--primary-foreground); transition: background-color var(--motion-base) ease; }
.trail-sidebar-new-analysis:hover { background: color-mix(in srgb, var(--primary) 82%, var(--foreground-muted)); }
.trail-sidebar-nav { min-height: 0; overflow-y: auto; }
.trail-sidebar-nav { min-height: 48px; overflow-y: auto; }
.trail-sidebar-nav-item { position: relative; display: flex; min-height: 2.65rem; flex: 0 0 auto; align-items: center; gap: .7rem; border-radius: var(--radius); padding: .55rem 1rem; color: var(--foreground-muted); font-size: .93rem; font-weight: 400; text-align: left; transition: background-color var(--motion-base) ease, color var(--motion-base) ease; }
.trail-sidebar-nav-item:nth-child(7) { margin-top: 1.1rem; }
.trail-sidebar-nav-item:nth-child(7)::before { position: absolute; top: -.65rem; left: .6rem; right: .6rem; height: 1px; background: var(--border); content: ""; }
Expand All @@ -884,16 +884,39 @@ button:disabled { cursor: not-allowed; }
.trail-sidebar-nav-item:hover { background: var(--surface-muted); color: var(--foreground); }
.trail-sidebar-nav-item--active { box-shadow: inset 3px 0 0 var(--accent); background: var(--surface-muted); color: var(--accent); font-weight: 650; }
.trail-sidebar-alert-dot { margin-left: auto; height: .4rem; width: .4rem; border-radius: 999px; background: var(--negative); }
.trail-sidebar-section-label { color: var(--foreground-subtle); font-size: .57rem; font-weight: 700; letter-spacing: .11em; text-transform: uppercase; }
.trail-sidebar-context-heading { color: var(--foreground-muted); font-size: .68rem; }
.trail-sidebar-section-label { color: var(--foreground-muted); font-size: .75rem; font-weight: 600; }
.trail-sidebar-context-heading { flex-shrink: 0; min-height: 2rem; color: var(--foreground); font-size: .8125rem; font-weight: 600; }
.trail-sidebar-context-heading:hover { color: var(--foreground); }
.trail-sidebar-context { display: flex; flex: 0 0 auto; flex-direction: column; height: clamp(13rem, 30vh, 20rem); overflow: hidden; }
.trail-sidebar-brand, .trail-sidebar-new-analysis, .trail-sidebar-footer { flex-shrink: 0; }
.trail-sidebar-panels { display: flex; flex: 1; min-height: 0; flex-direction: column; overflow: hidden; }
.trail-sidebar-upper { display: flex; flex: 1; min-height: 0; flex-direction: column; overflow: hidden; }
.trail-sidebar-context { display: flex; flex: 0 1 auto; min-height: 0; max-height: calc(100% - 56px); flex-direction: column; margin-top: .5rem; padding-top: .5rem; overflow: hidden; }
.trail-sidebar-context > div:first-child { flex-shrink: 0; }
.trail-sidebar-sessions { max-height: 3.5rem; flex-shrink: 0; }
.trail-sidebar-sessions { min-height: 0; max-height: 5.5rem; overflow-y: auto; }
.trail-sidebar-watchlist-panel { display: flex; flex: 0 0 auto; min-height: 0; flex-direction: column; overflow: hidden; }
.trail-sidebar-watchlist-count { margin-left: auto; padding: .1rem .4rem; border-radius: 4px; background: var(--surface-muted); color: var(--foreground-muted); font-size: .7rem; }
.trail-sidebar-watchlist .trail-watchlist { min-height: 0; }
.trail-sidebar-watchlist .trail-watchlist-list { min-height: 0; overscroll-behavior-y: contain; }
.trail-sidebar-watchlist .trail-watchlist-header { flex-shrink: 0; }
.trail-sidebar-context-heading { flex-shrink: 0; }
.trail-sidebar-resize-handle { position: relative; display: flex; height: 12px; flex: 0 0 12px; align-items: center; justify-content: center; cursor: row-resize; touch-action: none; user-select: none; outline: none; }
.trail-sidebar-resize-handle::before { position: absolute; left: 0; right: 0; height: 1px; background: var(--border); content: ''; }
.trail-sidebar-resize-handle > span { z-index: 1; width: 30px; height: 4px; border-radius: 999px; background: var(--foreground-subtle); box-shadow: 0 0 0 4px var(--surface); }
.trail-sidebar-resize-handle:hover > span, .trail-sidebar-resize-handle:focus-visible > span, .trail-sidebar-panels--resizing .trail-sidebar-resize-handle > span { background: var(--accent); }
.trail-sidebar-resize-handle:focus-visible { border-radius: 4px; box-shadow: inset 0 0 0 2px var(--accent); }
.trail-sidebar-panels--resizing, .trail-sidebar-panels--resizing * { cursor: row-resize !important; user-select: none; }
.trail-sidebar-settings-panel { left: 12px; top: auto; bottom: 74px; }
.trail-sidebar-support { min-height: 2rem; }
@media (max-height: 700px) and (min-width: 900px) {
.trail-sidebar-content { padding-top: .75rem; padding-bottom: .75rem; }
.trail-sidebar-brand { gap: .75rem; padding-bottom: .5rem; margin-bottom: .75rem; }
.trail-sidebar-brand .trail-logo { width: 3.5rem; height: 3.5rem; }
.trail-sidebar-brand-name { font-size: 1.3rem; }
.trail-sidebar-brand-chinese { margin-top: .1rem; font-size: 1rem; }
.trail-sidebar-brand-kicker { margin-top: .2rem; font-size: .75rem; }
.trail-sidebar-new-analysis { margin-bottom: .75rem; }
.trail-sidebar-footer { padding-top: .5rem; }
.trail-sidebar-support { min-height: 1.75rem; padding-block: .25rem; }
}
.trail-workspace-topbar { height: 3.1rem; gap: .8rem; padding-inline: 1.4rem; border-bottom-color: var(--border); }
.trail-workspace-center { container-type: inline-size; }
@container (max-width: 600px) { .trail-workspace-topbar-tabs { display: none; } }
Expand Down Expand Up @@ -947,9 +970,12 @@ button:disabled { cursor: not-allowed; }
.trail-sidebar-nav-label,
.trail-sidebar-new-analysis span,
.trail-sidebar-support,
.trail-sidebar-context { display: none; }
.trail-sidebar-context,
.trail-sidebar-resize-handle,
.trail-sidebar-watchlist-panel { display: none; }
.trail-sidebar-new-analysis { width: 2.25rem; margin: 0 0 1rem; padding: 0; }
.trail-sidebar-nav { width: 100%; flex: 1; }
.trail-sidebar-panels, .trail-sidebar-upper { width: 100%; }
.trail-sidebar-nav-item { justify-content: center; padding: .55rem 0; }
.trail-sidebar-alert-dot { position: absolute; top: .35rem; right: .35rem; margin: 0; }
.trail-sidebar-footer { width: 100%; }
Expand Down
Binary file added docs/screenshots/sidebar-watchlist-expanded.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions packages/i18n/src/locales/en-US/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export const navigation = {
news: 'News',
reports: 'Reports',
watchlist: 'Watchlist',
resizeWatchlist: 'Resize watchlist height',
resizeWatchlistHint: 'Drag up or down to resize; double-click to reset. Use arrow keys to resize or Home to reset.',
sessions: 'Sessions',
newSession: 'New Session',
noSessions: 'No sessions yet',
Expand Down
2 changes: 2 additions & 0 deletions packages/i18n/src/locales/zh-CN/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export const navigation = {
news: '新闻',
reports: '报告',
watchlist: '自选',
resizeWatchlist: '调整自选区域高度',
resizeWatchlistHint: '上下拖动调整自选区域高度;双击恢复默认。也可使用上下方向键,Home 恢复默认。',
sessions: '会话',
newSession: '新建会话',
noSessions: '暂无会话',
Expand Down
120 changes: 66 additions & 54 deletions packages/ui/src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ import {
createSessionAtom,
deleteSessionAtom,
navSectionAtom,
watchlistAtom,
type NavSection,
} from '../../atoms';
import { useFinagentClient } from '../../client';
import { Watchlist } from '../stock/Watchlist';
import { TrailLogo } from '../brand/TrailIdentity';
import { SidebarWorkspaceSplit } from './SidebarWorkspaceSplit';

type SidebarItem = { key: NavSection; labelKey: string; icon: LucideIcon };

Expand Down Expand Up @@ -79,6 +81,7 @@ export const Sidebar: React.FC = () => {
const createSession = useSetAtom(createSessionAtom);
const deleteSession = useSetAtom(deleteSessionAtom);
const [navSection, setNavSection] = useAtom(navSectionAtom);
const [watchlist] = useAtom(watchlistAtom);
const showWorkspaceContext = navSection === 'sessions' || navSection === 'watchlist';

return (
Expand All @@ -103,61 +106,70 @@ export const Sidebar: React.FC = () => {
<span>{t('navigation.newSession')}</span>
</button>

<nav aria-label={t('navigation.globalNavAria')} className="trail-sidebar-nav flex min-h-0 flex-1 flex-col gap-1">
{SIDEBAR_ITEMS.map((item) => (
<SidebarNavButton
key={item.key}
item={item}
label={t(`navigation.${item.labelKey}`)}
active={navSection === item.key}
onClick={() => setNavSection(item.key)}
/>
))}
<SidebarNavButton
item={{ key: 'profile', labelKey: 'profile', icon: UserRound }}
label={t('navigation.profile')}
active={navSection === 'profile'}
onClick={() => setNavSection('profile')}
/>
</nav>
<SidebarWorkspaceSplit
enabled={showWorkspaceContext}
upper={
<>
<nav aria-label={t('navigation.globalNavAria')} className="trail-sidebar-nav flex min-h-0 flex-1 flex-col gap-1">
{SIDEBAR_ITEMS.map((item) => (
<SidebarNavButton
key={item.key}
item={item}
label={t(`navigation.${item.labelKey}`)}
active={navSection === item.key}
onClick={() => setNavSection(item.key)}
/>
))}
<SidebarNavButton
item={{ key: 'profile', labelKey: 'profile', icon: UserRound }}
label={t('navigation.profile')}
active={navSection === 'profile'}
onClick={() => setNavSection('profile')}
/>
</nav>

{showWorkspaceContext && (
<section className="trail-sidebar-context mt-4 min-h-0 border-t border-border pt-3">
<div className="flex items-center justify-between px-2 pb-2">
<span className="trail-sidebar-section-label">{t('navigation.sessions')}</span>
<span className="tnum text-[10px] text-foreground/40">{sessions.length}</span>
</div>
<div className="trail-sidebar-sessions max-h-28 overflow-y-auto">
{sessions.map((session) => (
<div key={session.id} className="group relative">
<button
type="button"
onClick={() => setActiveSessionId(session.id)}
className={`flex w-full items-center rounded-[4px] px-2 py-1.5 pr-7 text-left text-[11px] ${session.id === activeSessionId ? 'bg-surface-muted text-foreground' : 'text-foreground/60 hover:bg-surface-muted'}`}
>
<span className="min-w-0 truncate">{session.title}</span>
</button>
<button
type="button"
onClick={(event) => { event.stopPropagation(); void deleteSession(client, session.id); }}
aria-label={t('navigation.deleteSession', { title: session.title })}
className="absolute right-1 top-1/2 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-[4px] text-foreground/35 opacity-0 group-hover:opacity-100 hover:bg-negative/10 hover:text-negative"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
))}
{sessions.length === 0 && <div className="px-2 py-2 text-[11px] text-foreground/40">{t('navigation.noSessions')}</div>}
</div>
<button type="button" onClick={() => setNavSection('watchlist')} className="trail-sidebar-context-heading mt-3 flex w-full items-center gap-2 px-2 text-left">
<Activity className="h-3.5 w-3.5" />
<span>{t('navigation.watchlist')}</span>
</button>
<div className="trail-sidebar-watchlist mt-1 min-h-0 flex-1">
<Watchlist showHeader={false} />
</div>
</section>
)}
{showWorkspaceContext && (
<section className="trail-sidebar-context min-h-0 border-t border-border pt-3">
<div className="flex items-center justify-between px-2 pb-2">
<span className="trail-sidebar-section-label">{t('navigation.sessions')}</span>
<span className="tnum text-[10px] text-foreground/40">{sessions.length}</span>
</div>
<div className="trail-sidebar-sessions max-h-28 overflow-y-auto">
{sessions.map((session) => (
<div key={session.id} className="group relative">
<button
type="button"
onClick={() => setActiveSessionId(session.id)}
className={`flex w-full items-center rounded-[4px] px-2 py-1.5 pr-7 text-left text-[11px] ${session.id === activeSessionId ? 'bg-surface-muted text-foreground' : 'text-foreground/60 hover:bg-surface-muted'}`}
>
<span className="min-w-0 truncate">{session.title}</span>
</button>
<button
type="button"
onClick={(event) => { event.stopPropagation(); void deleteSession(client, session.id); }}
aria-label={t('navigation.deleteSession', { title: session.title })}
className="absolute right-1 top-1/2 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-[4px] text-foreground/35 opacity-0 group-hover:opacity-100 hover:bg-negative/10 hover:text-negative"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
))}
{sessions.length === 0 && <div className="px-2 py-2 text-[11px] text-foreground/40">{t('navigation.noSessions')}</div>}
</div>
</section>
)}
</>
}
>
<button type="button" onClick={() => setNavSection('watchlist')} className="trail-sidebar-context-heading flex w-full items-center gap-2 px-2 text-left">
<Activity className="h-3.5 w-3.5" />
<span>{t('navigation.watchlist')}</span>
<span className="trail-sidebar-watchlist-count tnum">{watchlist.length}</span>
</button>
<div className="trail-sidebar-watchlist mt-1 min-h-0 flex-1">
<Watchlist showHeader={false} />
</div>
</SidebarWorkspaceSplit>

<div className="trail-sidebar-footer mt-auto border-t border-border pt-3">
<SidebarNavButton
Expand Down
Loading
Loading