mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-18 06:23:18 +00:00
370b67d7ec
* chore: ignore .superpowers/ brainstorm scratch dir
* feat(sidebar): add useStackMenuItems hook with grouped menu model
Pure transform hook that converts StackMenuCtx into four ordered MenuGroup
arrays (inspect, organize, lifecycle, destructive). Shared type contract in
sidebar-types.ts gives both the ContextMenu and DropdownMenu a single source
of truth so they cannot drift. Covered by 8 unit tests.
* refactor(sidebar): stabilize useStackMenuItems memoization deps
Destructure menuVisibility flags into primitive deps so inline object
literals from callers do not defeat memoization. Add a test confirming
isBusy disables all lifecycle items.
* feat(sidebar): add usePinnedStacks hook with per-node localStorage
* refactor(sidebar): stabilize usePinnedStacks eviction signal and isPinned dep
Change evictedOldest shape to { file, seq } so consumer effects re-fire on
repeated evictions. Narrow isPinned's useCallback dep to the current node's
pinned list so it only rebinds on local changes. Add test for eviction
side-effect and a second test for the seq counter.
* feat(sidebar): add useSidebarGroupCollapse hook with per-node keys
* refactor(sidebar): tighten useSidebarGroupCollapse effect ordering
Collapse the two write/read effects into a single skip-next-write ref
pattern so switching nodes no longer writes the previous node's map under
the new key before hydration. Also skip the no-op mount write. Add test
for setCollapsed.
* feat(sidebar): add row + group-header style helpers
* feat(sidebar): add SidebarBrand with mono kicker + serif hero
* feat(sidebar): add SidebarActions wrapper for create + scan
* feat(sidebar): extract SidebarSearch with kbd pill
* feat(sidebar): add StackRow with cyan-rail active state
* refactor(sidebar): dedupe tooltip markup in StackRow, widen test coverage
Extract a local RowTooltip helper so the update and git-pending branches
share the CursorProvider scaffolding. Add four behavioral tests covering
click, keyboard activation, kebab stop-propagation, and the busy loader
branch.
* feat(sidebar): unify context + kebab menus via useStackMenuItems
* feat(sidebar): add StackGroup with collapse and pinned variant
* feat(sidebar): add StackList with pinned + label groups
* feat(sidebar): add SidebarActivityTicker with idle fallback
* feat(sidebar): add StackSidebar container composing the regions
* feat(sidebar): replace sidebar block with StackSidebar composition
* docs(sidebar): add stack sidebar feature page with screenshots
* fix(sidebar): satisfy react-hooks purity and memoization rules
* fix(sidebar): restore "Sencho Logo" alt text for E2E selector
59 lines
2.1 KiB
TypeScript
59 lines
2.1 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { cn } from '@/lib/utils';
|
|
import { formatTimeAgo } from '@/lib/relativeTime';
|
|
import type { NotificationItem } from '@/components/dashboard/types';
|
|
|
|
const ONE_HOUR_S = 60 * 60;
|
|
const NOW_TICK_MS = 30_000;
|
|
|
|
interface SidebarActivityTickerProps {
|
|
notifications: NotificationItem[];
|
|
connected: boolean;
|
|
onNavigate: () => void;
|
|
}
|
|
|
|
export function SidebarActivityTicker({ notifications, connected, onNavigate }: SidebarActivityTickerProps) {
|
|
const [nowS, setNowS] = useState(() => Math.floor(Date.now() / 1000));
|
|
useEffect(() => {
|
|
const id = setInterval(() => setNowS(Math.floor(Date.now() / 1000)), NOW_TICK_MS);
|
|
return () => clearInterval(id);
|
|
}, []);
|
|
const latest = useMemo(() => {
|
|
return notifications
|
|
.filter(n => n.stack_name && nowS - n.timestamp <= ONE_HOUR_S)
|
|
.sort((a, b) => b.timestamp - a.timestamp)[0] ?? null;
|
|
}, [notifications, nowS]);
|
|
|
|
const idle = latest === null;
|
|
const dotClass = connected
|
|
? 'bg-success shadow-[0_0_6px_var(--success)] animate-pulse'
|
|
: 'bg-warning';
|
|
const kicker = idle ? 'IDLE · NO RECENT ACTIVITY' : 'LIVE · VIEW ACTIVITY →';
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={onNavigate}
|
|
className={cn(
|
|
'w-full flex flex-col gap-0.5 px-4 py-2 border-t border-glass-border',
|
|
'bg-sidebar/80 hover:bg-glass-highlight text-left',
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<span data-testid="ticker-dot" className={cn('w-1.5 h-1.5 rounded-full', dotClass)} />
|
|
{idle ? (
|
|
<span className="font-mono text-[11px] text-muted-foreground">No recent activity</span>
|
|
) : (
|
|
<span className="font-mono text-[11px] truncate">
|
|
<span className="text-brand">{latest.stack_name}</span>
|
|
<span className="text-muted-foreground"> · {latest.message} · {formatTimeAgo(latest.timestamp * 1000)}</span>
|
|
</span>
|
|
)}
|
|
</div>
|
|
<span className="font-mono text-[9px] tracking-[0.22em] uppercase text-stat-subtitle pl-3.5">
|
|
{kicker}
|
|
</span>
|
|
</button>
|
|
);
|
|
}
|