mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-18 22:36:19 +00:00
feat(sidebar): §14 sidebar orchestration, filter chips, pinned rail, trailing column (#850)
* feat(sidebar): §14 sidebar orchestration (filter chips, pinned rail, trailing column) - Add All / Up / Down / Updates filter chips with live counts; active chip filters the list; chip-filtered files computed in EditorLayout with useMemo - Surface the PINNED group with a 3px cyan left rail and glow via the sidebarPinnedGroupRail token; reuses the brand token already on the active row - Compact brand row from three stacked elements to a single 44px horizontal bar - Restructure StackRow trailing column as fixed slots: label dots (max 3 + +N overflow), update-dot | git-pending icon (priority order), kebab; add reserved invisible checkbox slot for PR2 bulk mode - Export statusText / statusColor from StackRow and reuse them in StackList remote-results section to remove the duplicate inline logic - Lift filterChip state and chip-filtered files to EditorLayout; remove filterChip from StackListProps to eliminate the dual-path redundancy - Remove unused labels param from buildGroups and the void labels workaround - Wrap filteredFiles in useMemo so filterCounts memo is not defeated on every render * fix(sidebar): move statusText and statusColor to stack-status-utils react-refresh/only-export-components requires component files to export only components. Move the two utility functions and StackRowStatus type to a dedicated stack-status-utils.ts so StackRow.tsx is a pure component module. Update StackList.tsx and EditorLayout.tsx to import from the new source directly.
This commit is contained in:
@@ -4,9 +4,9 @@ import { Cursor, CursorContainer, CursorFollow, CursorProvider } from '@/compone
|
||||
import { LabelDot } from '@/components/LabelPill';
|
||||
import type { Label } from '@/components/label-types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { sidebarRowActive, sidebarRowBase } from './sidebar-styles';
|
||||
|
||||
export type StackRowStatus = 'running' | 'exited' | 'unknown';
|
||||
import { sidebarRowActive, sidebarRowBase, sidebarRowCheckboxSlot } from './sidebar-styles';
|
||||
import { statusText, statusColor } from './stack-status-utils';
|
||||
import type { StackRowStatus } from './stack-status-utils';
|
||||
|
||||
interface StackRowProps {
|
||||
file: string;
|
||||
@@ -22,19 +22,6 @@ interface StackRowProps {
|
||||
kebabSlot: ReactNode;
|
||||
}
|
||||
|
||||
function statusText(status: StackRowStatus): string {
|
||||
if (status === 'running') return 'UP';
|
||||
if (status === 'exited') return 'DN';
|
||||
return '--';
|
||||
}
|
||||
|
||||
function statusColor(status: StackRowStatus, isBusy: boolean): string {
|
||||
if (isBusy) return 'text-muted-foreground';
|
||||
if (status === 'running') return 'text-success';
|
||||
if (status === 'exited') return 'text-destructive';
|
||||
return 'text-stat-icon';
|
||||
}
|
||||
|
||||
function RowTooltip({ trigger, label }: { trigger: ReactNode; label: string }) {
|
||||
return (
|
||||
<CursorProvider>
|
||||
@@ -49,9 +36,14 @@ function RowTooltip({ trigger, label }: { trigger: ReactNode; label: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
const MAX_VISIBLE_LABELS = 3;
|
||||
|
||||
export function StackRow(props: StackRowProps) {
|
||||
const { file, displayName, status, isBusy, isActive, isPaid, labels, hasUpdate, hasGitPending, onSelect, kebabSlot } = props;
|
||||
|
||||
const visibleLabels = isPaid ? labels.slice(0, MAX_VISIBLE_LABELS) : [];
|
||||
const overflowCount = isPaid ? Math.max(0, labels.length - MAX_VISIBLE_LABELS) : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="stack-row"
|
||||
@@ -61,32 +53,48 @@ export function StackRow(props: StackRowProps) {
|
||||
onClick={() => onSelect(file)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onSelect(file); } }}
|
||||
>
|
||||
{/* Reserved checkbox slot — revealed in bulk mode (PR2) */}
|
||||
<span className={sidebarRowCheckboxSlot} aria-hidden="true" />
|
||||
|
||||
{/* Status pill */}
|
||||
<span className={cn('font-mono text-[10px] shrink-0 w-[22px] flex items-center', statusColor(status, isBusy))}>
|
||||
{isBusy ? <Loader2 className="w-3 h-3 animate-spin" strokeWidth={2} /> : statusText(status)}
|
||||
</span>
|
||||
<span className="flex-1 truncate font-mono text-[13px]">{displayName}</span>
|
||||
{isPaid && labels.length > 0 && (
|
||||
|
||||
{/* Stack name */}
|
||||
<span className="flex-1 truncate font-mono text-[13px] min-w-0">{displayName}</span>
|
||||
|
||||
{/* Trailing: label dots (max 3 + overflow count) */}
|
||||
{visibleLabels.length > 0 && (
|
||||
<span className="flex items-center gap-0.5 shrink-0">
|
||||
{labels.map(l => <LabelDot key={l.id} color={l.color} />)}
|
||||
{visibleLabels.map(l => <LabelDot key={l.id} color={l.color} />)}
|
||||
{overflowCount > 0 && (
|
||||
<span className="font-mono text-[8px] text-stat-icon leading-none">+{overflowCount}</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{hasUpdate && (
|
||||
<RowTooltip
|
||||
trigger={(
|
||||
<span className="relative inline-flex w-2 h-2 shrink-0">
|
||||
<span className="absolute inset-0 rounded-full bg-update opacity-75 animate-ping" />
|
||||
<span className="relative w-2 h-2 rounded-full bg-update" />
|
||||
</span>
|
||||
)}
|
||||
label="Update available"
|
||||
/>
|
||||
)}
|
||||
{hasGitPending && (
|
||||
<RowTooltip
|
||||
trigger={<GitBranch className="w-3 h-3 text-brand" strokeWidth={1.5} />}
|
||||
label="Git source update pending"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Fixed trailing icon slot: update dot takes priority over git pending */}
|
||||
<span className="w-3.5 h-3.5 flex items-center justify-center shrink-0">
|
||||
{hasUpdate ? (
|
||||
<RowTooltip
|
||||
trigger={(
|
||||
<span className="relative inline-flex w-2 h-2">
|
||||
<span className="absolute inset-0 rounded-full bg-update opacity-75 animate-ping" />
|
||||
<span className="relative w-2 h-2 rounded-full bg-update" />
|
||||
</span>
|
||||
)}
|
||||
label="Update available"
|
||||
/>
|
||||
) : hasGitPending ? (
|
||||
<RowTooltip
|
||||
trigger={<GitBranch className="w-3 h-3 text-brand" strokeWidth={1.5} />}
|
||||
label="Git source update pending"
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
|
||||
{/* Kebab — always rightmost */}
|
||||
<div
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
||||
Reference in New Issue
Block a user