feat(desktop): show unread count on the sessions sidebar toggle
A small overlay on the left sidebar icon (right when panes are flipped) so a closed sessions list still reports unfinished-unread chats.
This commit is contained in:
committed by
brooklyn!
parent
508b3cf65d
commit
bf4f5e17f8
@@ -151,6 +151,12 @@ Notes:
|
||||
- SVGs inherit `size-3.5` (`size-3` at `xs`). Don't re-set icon size.
|
||||
- Polymorph with `asChild` when the button must render as a link/Slot.
|
||||
|
||||
## Badges — one component
|
||||
|
||||
`src/components/ui/badge.tsx`. Variants: `default` (tinted primary), `muted`,
|
||||
`warn`, `destructive`, `outline`, `solid` (primary fill — icon-corner counts).
|
||||
Sizes: `default`, `xs`, `overlay` (titlebar glyph counts).
|
||||
|
||||
## Form controls
|
||||
|
||||
- **`controlVariants`** (`src/components/ui/control.ts`) is the shared shape for
|
||||
|
||||
@@ -5,9 +5,11 @@ import { useLocation, useNavigate } from 'react-router'
|
||||
import { hudTargetSessionId } from '@/app/hud/handoff'
|
||||
import { toggleLayoutEditMode } from '@/components/pane-shell/edit-mode'
|
||||
import { resetLayoutTree } from '@/components/pane-shell/tree/store'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tip, TipKeybindLabel } from '@/components/ui/tooltip'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { compactNumber } from '@/lib/format'
|
||||
import { triggerHaptic } from '@/lib/haptics'
|
||||
import { formatModifierToken } from '@/lib/keybinds/combo'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -15,11 +17,13 @@ import { $hapticsMuted, toggleHapticsMuted } from '@/store/haptics'
|
||||
import { toggleHud } from '@/store/hud'
|
||||
import {
|
||||
$fileBrowserOpen,
|
||||
$panesFlipped,
|
||||
$sidebarOpen,
|
||||
toggleFileBrowserOpen,
|
||||
togglePanesFlipped,
|
||||
toggleSidebarOpen
|
||||
} from '@/store/layout'
|
||||
import { $unreadSessionCount } from '@/store/session-dot-state'
|
||||
|
||||
import { appViewForPath, isOverlayView } from '../routes'
|
||||
|
||||
@@ -43,6 +47,8 @@ export interface TitlebarTool {
|
||||
onSelect?: (event?: MouseEvent) => void
|
||||
/** Keybind action id — when set, the tooltip shows the label + keybind hint. */
|
||||
actionId?: string
|
||||
/** Overlay count on the glyph (unread sessions). Hidden when 0/undefined. */
|
||||
badge?: number
|
||||
title?: string
|
||||
to?: string
|
||||
}
|
||||
@@ -79,6 +85,24 @@ function LayoutGlyph({ modHeld }: { modHeld: boolean }) {
|
||||
)
|
||||
}
|
||||
|
||||
/** Overlay count on a titlebar glyph. Hidden when count is 0/undefined. */
|
||||
function withCountBadge(icon: ReactNode, count: number | undefined): ReactNode {
|
||||
if (!count) {
|
||||
return icon
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="relative inline-flex">
|
||||
{icon}
|
||||
<span className="pointer-events-none absolute -top-2.5 -right-1.5 z-1">
|
||||
<Badge aria-hidden size="overlay" variant="solid">
|
||||
{compactNumber(count)}
|
||||
</Badge>
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/** Live ⌘/Ctrl tracking — mod-click affordances telegraph themselves (the
|
||||
* layout button morphs into its reset form while the modifier is down). */
|
||||
function useModifierHeld(): boolean {
|
||||
@@ -109,7 +133,11 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
|
||||
const modHeld = useModifierHeld()
|
||||
const hapticsMuted = useStore($hapticsMuted)
|
||||
const fileBrowserOpen = useStore($fileBrowserOpen)
|
||||
const panesFlipped = useStore($panesFlipped)
|
||||
const sidebarOpen = useStore($sidebarOpen)
|
||||
const unreadCount = useStore($unreadSessionCount)
|
||||
const unreadBadge = unreadCount > 0 ? unreadCount : undefined
|
||||
const unreadHint = unreadBadge ? ` · ${t.titlebar.unreadSessions(unreadBadge)}` : ''
|
||||
|
||||
const toggleHaptics = () => {
|
||||
if (!hapticsMuted) {
|
||||
@@ -130,13 +158,16 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
|
||||
// show/hide affordances.
|
||||
const leftEdge = { open: sidebarOpen, toggle: toggleSidebarOpen }
|
||||
const rightEdge = { open: fileBrowserOpen, toggle: toggleFileBrowserOpen }
|
||||
const leftLabel = leftEdge.open ? t.titlebar.hideSidebar : t.titlebar.showSidebar
|
||||
const rightLabel = rightEdge.open ? t.titlebar.hideRightSidebar : t.titlebar.showRightSidebar
|
||||
|
||||
const leftToolbarTools: TitlebarTool[] = [
|
||||
{
|
||||
actionId: 'view.toggleSidebar',
|
||||
badge: panesFlipped ? undefined : unreadBadge,
|
||||
icon: <TitlebarIcon name="layout-sidebar-left" />,
|
||||
id: 'sidebar',
|
||||
label: leftEdge.open ? t.titlebar.hideSidebar : t.titlebar.showSidebar,
|
||||
label: `${leftLabel}${panesFlipped ? '' : unreadHint}`,
|
||||
onSelect: () => {
|
||||
triggerHaptic('tap')
|
||||
leftEdge.toggle()
|
||||
@@ -157,9 +188,10 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
|
||||
|
||||
const rightSidebarTool: TitlebarTool = {
|
||||
actionId: 'view.toggleRightSidebar',
|
||||
badge: panesFlipped ? unreadBadge : undefined,
|
||||
icon: <TitlebarIcon name="layout-sidebar-right" />,
|
||||
id: 'right-sidebar',
|
||||
label: rightEdge.open ? t.titlebar.hideRightSidebar : t.titlebar.showRightSidebar,
|
||||
label: `${rightLabel}${panesFlipped ? unreadHint : ''}`,
|
||||
onSelect: () => {
|
||||
triggerHaptic('tap')
|
||||
rightEdge.toggle()
|
||||
@@ -306,7 +338,7 @@ function TitlebarToolButton({ navigate, tool }: { navigate: ReturnType<typeof us
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{tool.icon}
|
||||
{withCountBadge(tool.icon, tool.badge)}
|
||||
</a>
|
||||
</Button>
|
||||
</Tip>
|
||||
@@ -332,7 +364,7 @@ function TitlebarToolButton({ navigate, tool }: { navigate: ReturnType<typeof us
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
{tool.icon}
|
||||
{withCountBadge(tool.icon, tool.badge)}
|
||||
</Button>
|
||||
</Tip>
|
||||
)
|
||||
|
||||
@@ -15,11 +15,14 @@ const badgeVariants = cva(
|
||||
muted: 'bg-muted text-muted-foreground',
|
||||
warn: 'bg-amber-500/10 text-amber-600 dark:text-amber-300',
|
||||
destructive: 'bg-destructive/10 text-destructive',
|
||||
outline: 'border border-(--ui-stroke-secondary) text-muted-foreground'
|
||||
outline: 'border border-(--ui-stroke-secondary) text-muted-foreground',
|
||||
// Solid fill — icon-corner counts (titlebar unread, etc.).
|
||||
solid: 'bg-primary text-primary-foreground'
|
||||
},
|
||||
size: {
|
||||
default: 'px-1.5 py-0.5 text-[0.65rem] [&_svg]:size-3',
|
||||
xs: 'px-1 py-px text-[0.6rem] [&_svg]:size-2.5'
|
||||
xs: 'px-1 py-px text-[0.6rem] [&_svg]:size-2.5',
|
||||
overlay: 'h-2 min-w-2 justify-center rounded-[2px] px-px text-[7px] font-semibold tabular-nums'
|
||||
}
|
||||
},
|
||||
defaultVariants: { variant: 'default', size: 'default' }
|
||||
|
||||
@@ -181,6 +181,7 @@ export const ar = defineLocale({
|
||||
swapSidebarSides: 'تبديل جانبي الأشرطة',
|
||||
hideRightSidebar: 'إخفاء الشريط الأيمن',
|
||||
showRightSidebar: 'إظهار الشريط الأيمن',
|
||||
unreadSessions: count => (count === 1 ? 'جلسة واحدة غير مقروءة' : `${count} جلسات غير مقروءة`),
|
||||
muteHaptics: 'كتم الاهتزازات',
|
||||
unmuteHaptics: 'تفعيل الاهتزازات',
|
||||
openSettings: 'فتح الإعدادات',
|
||||
|
||||
@@ -211,6 +211,7 @@ export const en: Translations = {
|
||||
swapSidebarSides: 'Swap sidebar sides',
|
||||
hideRightSidebar: 'Hide right sidebar',
|
||||
showRightSidebar: 'Show right sidebar',
|
||||
unreadSessions: count => (count === 1 ? '1 unread session' : `${count} unread sessions`),
|
||||
muteHaptics: 'Mute haptics',
|
||||
unmuteHaptics: 'Unmute haptics',
|
||||
openSettings: 'Open settings',
|
||||
|
||||
@@ -212,6 +212,7 @@ export const ja = defineLocale({
|
||||
swapSidebarSides: 'サイドバーの向きを切り替え',
|
||||
hideRightSidebar: '右サイドバーを非表示',
|
||||
showRightSidebar: '右サイドバーを表示',
|
||||
unreadSessions: count => (count === 1 ? '未読セッション 1 件' : `未読セッション ${count} 件`),
|
||||
muteHaptics: '触覚フィードバックをオフ',
|
||||
unmuteHaptics: '触覚フィードバックをオン',
|
||||
openSettings: '設定を開く',
|
||||
|
||||
@@ -253,6 +253,7 @@ export interface Translations {
|
||||
swapSidebarSides: string
|
||||
hideRightSidebar: string
|
||||
showRightSidebar: string
|
||||
unreadSessions: (count: number) => string
|
||||
muteHaptics: string
|
||||
unmuteHaptics: string
|
||||
openSettings: string
|
||||
|
||||
@@ -206,6 +206,7 @@ export const zhHant = defineLocale({
|
||||
swapSidebarSides: '交換側邊欄位置',
|
||||
hideRightSidebar: '隱藏右側邊欄',
|
||||
showRightSidebar: '顯示右側邊欄',
|
||||
unreadSessions: count => (count === 1 ? '1 個未讀工作階段' : `${count} 個未讀工作階段`),
|
||||
muteHaptics: '靜音觸感回饋',
|
||||
unmuteHaptics: '開啟觸感回饋',
|
||||
openSettings: '開啟設定',
|
||||
|
||||
@@ -206,6 +206,7 @@ export const zh: Translations = {
|
||||
swapSidebarSides: '交换侧边栏位置',
|
||||
hideRightSidebar: '隐藏右侧栏',
|
||||
showRightSidebar: '显示右侧栏',
|
||||
unreadSessions: count => (count === 1 ? '1 个未读会话' : `${count} 个未读会话`),
|
||||
muteHaptics: '关闭触感反馈',
|
||||
unmuteHaptics: '开启触感反馈',
|
||||
openSettings: '打开设置',
|
||||
|
||||
Reference in New Issue
Block a user