Merge pull request #90920 from NousResearch/bb/session-row-trailing
Sidebar rows own their right edge
This commit is contained in:
@@ -23,9 +23,19 @@ export function SidebarSectionMeta({ children }: { children: React.ReactNode })
|
||||
// Height lives ONLY on SidebarRowShell (min-h-[1.625rem]). Inset children
|
||||
// stretch to fill the cell and center content internally — never items-center
|
||||
// on the shell grid, or short clusters (projects) float 1–2px off sessions.
|
||||
//
|
||||
// `rowPadX` is the BODY's padding: the lead's inset, plus the gap the label
|
||||
// keeps from the actions column, both inside the row's click target.
|
||||
// `rowPadTrail` is the row's own trailing inset and belongs to the SHELL — the
|
||||
// only box containing both the actions column AND the card's in-body cluster,
|
||||
// so one class insets every trailing thing a row can render. Owned anywhere
|
||||
// else, the age / chips / kebab sit flush on the border box, which is exactly
|
||||
// where a working row paints its arc (`.arc-row` has zero standoff) — the ring
|
||||
// ran through the text.
|
||||
|
||||
const rowMinH = 'min-h-[1.625rem]'
|
||||
const rowPadX = 'pl-2 pr-1'
|
||||
const rowPadX = 'pl-2 pr-2'
|
||||
const rowPadTrail = 'pr-2'
|
||||
const rowGap = 'gap-1.5'
|
||||
const rowLead = 'grid size-3.5 shrink-0 place-items-center'
|
||||
const rowInset = cn(rowPadX, rowGap, 'flex h-full min-w-0 items-center self-stretch py-0.5')
|
||||
@@ -71,9 +81,11 @@ export function SidebarDateDivider({
|
||||
)
|
||||
}
|
||||
|
||||
/** Outer grid — sole owner of row height. The trailing `actions` slot is
|
||||
* marked `data-row-actions` so a row-wide drag gesture can exclude it with
|
||||
* one selector: it holds real controls, never grab surface. */
|
||||
/** Outer grid — sole owner of row height and of the trailing inset. The
|
||||
* `actions` slot is marked `data-row-actions` so a row-wide drag gesture can
|
||||
* exclude it with one selector: it holds real controls, never grab surface.
|
||||
* It stretches so that exclusion covers the column, not just its tallest
|
||||
* control. */
|
||||
export function SidebarRowShell({
|
||||
actions,
|
||||
actionsClassName,
|
||||
@@ -82,10 +94,13 @@ export function SidebarRowShell({
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & { actions?: React.ReactNode; actionsClassName?: string }) {
|
||||
return (
|
||||
<div className={cn(rowMinH, 'grid grid-cols-[minmax(0,1fr)_auto] items-stretch rounded-md', className)} {...props}>
|
||||
<div
|
||||
className={cn(rowMinH, rowPadTrail, 'grid grid-cols-[minmax(0,1fr)_auto] items-stretch rounded-md', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{actions ? (
|
||||
<div className={cn('flex shrink-0 items-center self-center', actionsClassName)} data-row-actions>
|
||||
<div className={cn('flex shrink-0 items-center self-stretch', actionsClassName)} data-row-actions>
|
||||
{actions}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -23,6 +23,7 @@ import type { CronJob } from '@/types/hermes'
|
||||
import { jobState, jobTitle, STATE_DOT } from '../../cron/job-state'
|
||||
import { SidebarPanelLabel } from '../../shell/sidebar-label'
|
||||
|
||||
import { SidebarRowBody, SidebarRowLabel, SidebarRowLead, SidebarRowShell } from './chrome'
|
||||
import { SidebarLoadMoreRow } from './load-more-row'
|
||||
|
||||
const INACTIVE_STATES = new Set(['completed', 'disabled', 'error', 'paused'])
|
||||
@@ -304,21 +305,57 @@ function CronJobSidebarRow({
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* The shared row chrome, not a copy of it: a cron job and a session sit
|
||||
in the same list, so they line up only if one place owns the geometry. */}
|
||||
<ActionsContextMenu ariaLabel={c.actionsTitle} contentClassName="w-44" items={items}>
|
||||
<div className="group/cron relative grid min-h-[1.625rem] grid-cols-[minmax(0,1fr)_auto] items-center rounded-md hover:bg-(--chrome-action-hover)">
|
||||
{/* Lead with the dot in the same w-3.5 cell + pl-2 the session rows use
|
||||
so the cron dots line up with the sessions above; the caret sits next
|
||||
to the label (matching the other sidebar disclosures) and the whole
|
||||
label area toggles the run peek. */}
|
||||
<SidebarRowShell
|
||||
actions={
|
||||
/* Trailing cluster: countdown by default, quick actions on hover. */
|
||||
<div className="flex items-center gap-0.5">
|
||||
<span className="text-[0.6875rem] text-(--ui-text-tertiary) tabular-nums group-hover/cron:hidden">
|
||||
{meta}
|
||||
</span>
|
||||
<div className="hidden items-center gap-0.5 group-hover/cron:flex">
|
||||
<Tip label={c.triggerNow}>
|
||||
<button
|
||||
aria-label={c.triggerNow}
|
||||
className="grid size-5 place-items-center rounded-sm text-(--ui-text-tertiary) hover:bg-(--ui-control-hover-background) hover:text-foreground disabled:cursor-wait disabled:opacity-60"
|
||||
disabled={busy}
|
||||
onClick={onTrigger}
|
||||
type="button"
|
||||
>
|
||||
{busy ? (
|
||||
<GlyphSpinner ariaLabel={c.triggerNow} className="text-[0.75rem]" />
|
||||
) : (
|
||||
<Codicon name="zap" size="0.75rem" />
|
||||
)}
|
||||
</button>
|
||||
</Tip>
|
||||
<Tip label={c.manage}>
|
||||
<button
|
||||
aria-label={c.manage}
|
||||
className="grid size-5 place-items-center rounded-sm text-(--ui-text-tertiary) hover:bg-(--ui-control-hover-background) hover:text-foreground"
|
||||
onClick={onManage}
|
||||
type="button"
|
||||
>
|
||||
<Codicon name="watch" size="0.75rem" />
|
||||
</button>
|
||||
</Tip>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
className="group/cron relative hover:bg-(--chrome-action-hover)"
|
||||
>
|
||||
{/* The caret sits next to the label (matching the other sidebar
|
||||
disclosures) and the whole label area toggles the run peek. */}
|
||||
<Tip label={label}>
|
||||
<button
|
||||
<SidebarRowBody
|
||||
aria-expanded={expanded}
|
||||
aria-label={expanded ? c.hideRuns : c.showRuns}
|
||||
className="flex min-w-0 items-center gap-1.5 bg-transparent py-0.5 pl-2 pr-1 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
|
||||
onClick={onTogglePeek}
|
||||
type="button"
|
||||
>
|
||||
<span className="grid w-3.5 shrink-0 place-items-center">
|
||||
<SidebarRowLead>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
@@ -327,10 +364,8 @@ function CronJobSidebarRow({
|
||||
state === 'running' && 'size-1.5 animate-pulse'
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
<span className="min-w-0 truncate text-[0.8125rem] text-(--ui-text-secondary) group-hover/cron:text-foreground">
|
||||
{label}
|
||||
</span>
|
||||
</SidebarRowLead>
|
||||
<SidebarRowLabel className="group-hover/cron:text-foreground">{label}</SidebarRowLabel>
|
||||
<DisclosureCaret
|
||||
className={cn(
|
||||
'shrink-0 text-(--ui-text-tertiary) transition',
|
||||
@@ -338,42 +373,9 @@ function CronJobSidebarRow({
|
||||
)}
|
||||
open={expanded}
|
||||
/>
|
||||
</button>
|
||||
</SidebarRowBody>
|
||||
</Tip>
|
||||
{/* Trailing cluster: countdown by default, quick actions on hover. */}
|
||||
<div className="flex items-center gap-0.5 justify-self-end pr-1">
|
||||
<span className="text-[0.6875rem] text-(--ui-text-tertiary) tabular-nums group-hover/cron:hidden">
|
||||
{meta}
|
||||
</span>
|
||||
<div className="hidden items-center gap-0.5 group-hover/cron:flex">
|
||||
<Tip label={c.triggerNow}>
|
||||
<button
|
||||
aria-label={c.triggerNow}
|
||||
className="grid size-5 place-items-center rounded-sm text-(--ui-text-tertiary) hover:bg-(--ui-control-hover-background) hover:text-foreground disabled:cursor-wait disabled:opacity-60"
|
||||
disabled={busy}
|
||||
onClick={onTrigger}
|
||||
type="button"
|
||||
>
|
||||
{busy ? (
|
||||
<GlyphSpinner ariaLabel={c.triggerNow} className="text-[0.75rem]" />
|
||||
) : (
|
||||
<Codicon name="zap" size="0.75rem" />
|
||||
)}
|
||||
</button>
|
||||
</Tip>
|
||||
<Tip label={c.manage}>
|
||||
<button
|
||||
aria-label={c.manage}
|
||||
className="grid size-5 place-items-center rounded-sm text-(--ui-text-tertiary) hover:bg-(--ui-control-hover-background) hover:text-foreground"
|
||||
onClick={onManage}
|
||||
type="button"
|
||||
>
|
||||
<Codicon name="watch" size="0.75rem" />
|
||||
</button>
|
||||
</Tip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarRowShell>
|
||||
</ActionsContextMenu>
|
||||
{expanded && <CronJobSidebarRuns jobId={job.id} onOpenRun={onOpenRun} />}
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,9 @@ interface SidebarLoadMoreRowProps {
|
||||
|
||||
// Compact "load more" affordance shared by recents, messaging, and cron. Kept
|
||||
// intentionally identical to workspace "show more" controls (ellipsis button)
|
||||
// so pagination reads as one interaction everywhere.
|
||||
// so pagination reads as one interaction everywhere. It hangs off the list
|
||||
// instead of sitting in a row, so it repeats the row's trailing inset
|
||||
// (SidebarRowShell's `pr-2`) to stay on the edge the rows stop at.
|
||||
export function SidebarLoadMoreRow({ step, onClick, loading = false }: SidebarLoadMoreRowProps) {
|
||||
const { t } = useI18n()
|
||||
const label = loading ? t.sidebar.loading : step > 0 ? t.sidebar.loadCount(step) : t.sidebar.loadMore
|
||||
@@ -20,7 +22,7 @@ export function SidebarLoadMoreRow({ step, onClick, loading = false }: SidebarLo
|
||||
<Tip label={label}>
|
||||
<button
|
||||
aria-label={label}
|
||||
className="ml-auto grid size-5 place-items-center rounded-sm bg-transparent text-(--ui-text-tertiary) transition-colors hover:bg-(--ui-control-hover-background) hover:text-foreground disabled:cursor-default disabled:opacity-60 disabled:hover:bg-transparent disabled:hover:text-(--ui-text-tertiary)"
|
||||
className="mr-2 ml-auto grid size-5 place-items-center rounded-sm bg-transparent text-(--ui-text-tertiary) transition-colors hover:bg-(--ui-control-hover-background) hover:text-foreground disabled:cursor-default disabled:opacity-60 disabled:hover:bg-transparent disabled:hover:text-(--ui-text-tertiary)"
|
||||
disabled={loading}
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
|
||||
@@ -48,6 +48,8 @@ export function WorkspaceAddButton({ label, onClick }: { label: string; onClick:
|
||||
}
|
||||
|
||||
// Reveals the next page of already-loaded rows within a workspace/worktree.
|
||||
// Hangs off the lane instead of sitting in a row, so it repeats the row's
|
||||
// trailing inset (SidebarRowShell's `pr-2`) to stay on the edge the rows stop at.
|
||||
export function WorkspaceShowMoreButton({
|
||||
count,
|
||||
label,
|
||||
@@ -64,7 +66,7 @@ export function WorkspaceShowMoreButton({
|
||||
<Tip label={text}>
|
||||
<button
|
||||
aria-label={text}
|
||||
className="ml-auto grid size-5 place-items-center rounded-sm bg-transparent text-(--ui-text-tertiary) transition-colors hover:bg-(--ui-control-hover-background) hover:text-foreground"
|
||||
className="mr-2 ml-auto grid size-5 place-items-center rounded-sm bg-transparent text-(--ui-text-tertiary) transition-colors hover:bg-(--ui-control-hover-background) hover:text-foreground"
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
>
|
||||
|
||||
@@ -4,19 +4,22 @@ import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import { SidebarRowCluster, SidebarRowShell, SidebarRowStack } from './chrome'
|
||||
|
||||
// Stands in for session rows, so it borrows their chrome instead of copying
|
||||
// the grid — a placeholder on a different edge than the rows it resolves into
|
||||
// makes the list step sideways on load.
|
||||
export function SidebarSessionSkeletons() {
|
||||
return (
|
||||
<div aria-hidden="true" className="grid gap-px">
|
||||
<SidebarRowStack aria-hidden="true">
|
||||
{['w-32', 'w-40', 'w-28', 'w-36', 'w-24'].map((width, i) => (
|
||||
<div
|
||||
className="grid min-h-[1.625rem] grid-cols-[minmax(0,1fr)_1.375rem] items-center rounded-md pl-2"
|
||||
key={`${width}-${i}`}
|
||||
>
|
||||
<Skeleton className={cn('h-3 rounded-sm', width)} />
|
||||
<Skeleton className="mx-auto size-3.5 rounded-sm opacity-60" />
|
||||
</div>
|
||||
<SidebarRowShell actions={<Skeleton className="size-3.5 rounded-sm opacity-60" />} key={`${width}-${i}`}>
|
||||
<SidebarRowCluster>
|
||||
<Skeleton className={cn('h-3 rounded-sm', width)} />
|
||||
</SidebarRowCluster>
|
||||
</SidebarRowShell>
|
||||
))}
|
||||
</div>
|
||||
</SidebarRowStack>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -405,9 +405,13 @@ function SidebarSessionRowImpl({
|
||||
<SidebarRowBody
|
||||
// Every trailing figure lives in the actions slot, which the row
|
||||
// measures — so the title needs a gap from it and nothing else. Hover
|
||||
// changes what you can see in that slot, never how wide it is.
|
||||
// changes what you can see in that slot, never how wide it is. The
|
||||
// card has no such column to clear (its cluster is INSIDE the body,
|
||||
// ending at the shell's own trailing inset), and keeping the gap
|
||||
// would pull the header in past every line below it.
|
||||
className={cn(
|
||||
'z-0 pr-2',
|
||||
'z-0',
|
||||
card && 'pr-0',
|
||||
branchStem && 'pl-3.5',
|
||||
// The card is a grid with ONE spacing knob: --card-gap. Every row
|
||||
// gap is gap-y-(--card-gap); the title/preview group opts out
|
||||
|
||||
Reference in New Issue
Block a user