Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license

This commit is contained in:
yilsem committed 2026-09-05 13:26:46 +03:00
commit 03634b1ca3
11340 files changed
+3442369

No files matched your search

@@ -0,0 +1,24 @@
import type { ButtonHTMLAttributes, ReactNode } from 'react'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
interface OverlayIconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode
}
// Overlay chrome icon action — same titlebar-sized ghost button as the overlay
// close (X), so footer/header actions read identically across breakpoints.
export function OverlayIconButton({ children, className, type = 'button', ...props }: OverlayIconButtonProps) {
return (
<Button
className={cn('text-(--ui-text-tertiary) hover:bg-(--chrome-action-hover) hover:text-foreground', className)}
size="icon-titlebar"
type={type}
variant="ghost"
{...props}
>
{children}
</Button>
)
}
@@ -0,0 +1,241 @@
import { Fragment, memo, type ReactNode } from 'react'
import { TabDropdown } from '@/components/ui/tab-dropdown'
import type { IconComponent } from '@/lib/icons'
import { cn } from '@/lib/utils'
import { PAGE_MAX_W } from '../layout-constants'
import { OVERLAY_TOP_CLEARANCE } from './overlay-view'
// The wide rail and the narrow dropdown swap at exactly the width where
// OverlaySplitLayout drops to a single column, so the rail never stacks.
const RAIL_HIDDEN = 'max-[47.5rem]:hidden'
const BAR_HIDDEN = 'hidden max-[47.5rem]:flex'
interface OverlaySplitLayoutProps {
children: ReactNode
className?: string
}
interface OverlaySidebarProps {
children: ReactNode
className?: string
}
interface OverlayMainProps {
children: ReactNode
className?: string
}
interface OverlayNavItemProps {
active: boolean
icon: IconComponent
/** Stable identity for the row, used as its `data-tour` handle. */
id?: string
label: string
// Renders as an indented child of another nav item: smaller icon and a
// lighter active state so it never competes with the boxed parent item.
nested?: boolean
onClick: () => void
trailing?: ReactNode
}
export function OverlaySplitLayout({ children, className }: OverlaySplitLayoutProps) {
return (
<div
className={cn(
// Narrow: one column, and pin rows to [nav-bar auto | main 1fr] — without
// an explicit template the grid's default align-content:stretch splits the
// height evenly across the two rows, shoving the content to mid-screen.
'grid h-full min-h-0 flex-1 grid-cols-[13rem_minmax(0,1fr)] overflow-hidden bg-transparent max-[47.5rem]:grid-cols-1 max-[47.5rem]:grid-rows-[auto_minmax(0,1fr)]',
className
)}
>
{children}
</div>
)
}
export function OverlaySidebar({ children, className }: OverlaySidebarProps) {
return (
<aside
className={cn(
// The left links sit beside (not under) the floating close button, so
// they ride up via the shorter shared OVERLAY_TOP_CLEARANCE (same line
// as a Panel header) instead of main's taller X-clearance. The bg still
// fills from the card's top edge, so there's no gap above the sidebar.
'flex min-h-0 flex-col gap-0.5 overflow-y-auto bg-(--ui-sidebar-surface-background) px-2.5 pb-3',
OVERLAY_TOP_CLEARANCE,
className
)}
// Every overlay's left nav (settings, cron, profiles, agents) answers to
// one name, so a tour can point at "the nav" without knowing which
// overlay is open. See lib/tour.
data-tour="overlay-nav"
>
{children}
</aside>
)
}
export function OverlayMain({ children, className }: OverlayMainProps) {
return (
<main
className={cn(
// Main sits UNDER the floating close button (top-right), so it keeps the
// taller top pad to clear the X — unlike the sidebar / Panel header,
// which sit to its left and ride up via OVERLAY_TOP_CLEARANCE. All four
// paddings are 1/3 tighter than the raw values (×2/3): the wide/narrow
// top clearance, the bottom gutter, and the horizontal clamp gutter
// (inlined from PAGE_INSET_X so only overlay panes tighten, not the
// shared page gutter). Narrow top drops toward the OverlayNav bar.
'mx-auto flex min-h-0 w-full flex-1 flex-col overflow-hidden bg-transparent pb-2 pt-[calc((var(--titlebar-height)/2+1rem)*2/3)] max-[47.5rem]:pt-[calc(0.5rem*2/3)] px-[clamp(0.8333rem,2.6667vw,2.6667rem)]',
PAGE_MAX_W,
className
)}
>
{children}
</main>
)
}
export const OverlayNavItem = memo(function OverlayNavItem({
active,
icon: Icon,
id,
label,
nested,
onClick,
trailing
}: OverlayNavItemProps) {
return (
<button
className={cn(
'flex h-7 w-full items-center justify-start gap-2 rounded-md border px-2 text-left text-[length:var(--conversation-text-font-size)] font-normal transition-colors',
nested
? active
? 'border-transparent bg-(--chrome-action-hover) font-medium text-foreground'
: 'border-transparent bg-transparent text-(--ui-text-tertiary) hover:bg-(--chrome-action-hover) hover:text-foreground'
: active
? 'border-(--ui-stroke-tertiary) bg-(--ui-bg-tertiary) text-foreground'
: 'border-transparent bg-transparent text-(--ui-text-secondary) hover:bg-(--chrome-action-hover) hover:text-foreground'
)}
// Names the row by its own id, so a tour can address one link
// (`[data-tour="nav-models"]`) instead of guessing at nth-child.
data-tour={id ? `nav-${id}` : undefined}
onClick={onClick}
type="button"
>
<Icon
className={cn(
'shrink-0',
nested ? 'size-3.5' : 'size-4',
active ? 'text-foreground/80' : 'text-muted-foreground/80'
)}
/>
<span className="min-w-0 flex-1 truncate">{label}</span>
{trailing}
</button>
)
})
export interface OverlayNavLink {
active: boolean
icon: IconComponent
id: string
label: string
onSelect: () => void
}
export interface OverlayNavGroup extends OverlayNavLink {
/** Sub-links: expanded under the active group on the rail, always listed
* (flattened + indented) in the narrow dropdown. */
children?: OverlayNavLink[]
/** Visual break before this group — a spacer on the rail, a separator in
* the dropdown. */
gapBefore?: boolean
}
// Data-driven pane nav: one model renders a persistent left rail on wide
// viewports and a single dropdown bar on narrow ones (matching the tab
// dropdown in PageSearchShell), so every OverlaySplitLayout pane degrades the
// same way instead of stacking its whole sidebar. Drop it in as the first
// child of an OverlaySplitLayout, before OverlayMain.
export function OverlayNav({ footer, groups }: { footer?: ReactNode; groups: OverlayNavGroup[] }) {
return (
<>
<OverlaySidebar className={RAIL_HIDDEN}>
{groups.map(group => (
<Fragment key={group.id}>
{group.gapBefore && <div aria-hidden className="h-2" />}
<OverlayNavItem
active={group.active}
icon={group.icon}
id={group.id}
label={group.label}
onClick={group.onSelect}
/>
{group.children && group.active && (
<div className="ml-3.5 flex flex-col gap-0.5 pl-1.5">
{group.children.map(child => (
<OverlayNavItem
active={child.active}
icon={child.icon}
id={child.id}
key={child.id}
label={child.label}
nested
onClick={child.onSelect}
/>
))}
</div>
)}
</Fragment>
))}
{footer && <div className="mt-auto flex items-center gap-1 pt-2">{footer}</div>}
</OverlaySidebar>
{/* Narrow: ride the OverlayView titlebar strip so the dropdown shares the
close button's row instead of taking its own. The bar is
pointer-events-none (children opt back in) so the floating X underneath
stays clickable; pr clears it, no-drag beats the strip's drag region,
and the height matches the strip so the trigger lines up with the X. */}
<div
className={cn(
'pointer-events-none relative z-20 h-[calc(var(--titlebar-height)+0.1875rem)] items-center justify-between gap-2 pl-3 pr-12',
BAR_HIDDEN
)}
>
<div className="pointer-events-auto min-w-0 [-webkit-app-region:no-drag]">
<TabDropdown
align="start"
items={groups.flatMap(group => [
{
active: group.active && !group.children?.some(child => child.active),
icon: group.icon,
id: group.id,
label: group.label,
onSelect: group.onSelect,
separatorBefore: group.gapBefore
},
...(group.children ?? []).map(child => ({
active: child.active,
icon: child.icon,
id: child.id,
indent: true,
label: child.label,
onSelect: child.onSelect
}))
])}
/>
</div>
{footer && (
<div className="pointer-events-auto flex shrink-0 items-center gap-1 [-webkit-app-region:no-drag]">
{footer}
</div>
)}
</div>
</>
)
}
@@ -0,0 +1,151 @@
import { type CSSProperties, type ReactNode, useEffect } from 'react'
import { TITLEBAR_HEIGHT } from '@/app/shell/titlebar'
import { TitlebarIcon } from '@/app/shell/titlebar-icon'
import { Button } from '@/components/ui/button'
import { translateNow } from '@/i18n'
import { ESCAPE_PRIORITY, isTopEscapeLayer, pushEscapeLayer } from '@/lib/escape-layers'
import { triggerHaptic } from '@/lib/haptics'
import { cn } from '@/lib/utils'
// Shared top clearance for overlay content that sits *beside* the floating
// close button (which is absolute at `0.1875rem + titlebar/2`, -translate-y-1/2,
// so it costs no layout space): a Panel's header and the split layout's left
// sidebar links. They ride up next to the X on the same line across every
// overlay (settings, system, agents, cron, …) — change it here, not per-surface.
// Main content sits *under* the X (top-right) and keeps its own taller pad.
export const OVERLAY_TOP_CLEARANCE = 'pt-[calc(var(--titlebar-height)/2-0.4375rem)]'
interface OverlayViewProps {
children: ReactNode
onClose: () => void
closeLabel?: string
contentClassName?: string
/** Chrome pinned to the card's top edge, horizontally centered and riding
* the border half-in half-out (e.g. the Settings search pill). Rendered
* beside the card, not inside it — the card clips its own overflow. */
edgeBadge?: ReactNode
headerContent?: ReactNode
rootClassName?: string
/** Controls rendered on the close button's row, to its left. They ride the
* titlebar strip, so keep them titlebar-sized and quiet. */
titlebarActions?: ReactNode
}
export function OverlayView({
children,
onClose,
closeLabel = translateNow('common.close'),
contentClassName,
edgeBadge,
headerContent,
rootClassName,
titlebarActions
}: OverlayViewProps) {
const closeOverlay = () => {
triggerHaptic('close')
onClose()
}
// Esc dismisses every OverlayView-based overlay. Nested Radix dialogs
// stop propagation themselves, so opening (e.g.) the model picker inside
// Settings still closes the picker first instead of the underlying overlay.
useEffect(() => {
const releaseLayer = pushEscapeLayer(ESCAPE_PRIORITY.overlay)
const onKeyDown = (event: KeyboardEvent) => {
if (event.key !== 'Escape' || event.defaultPrevented || !isTopEscapeLayer(ESCAPE_PRIORITY.overlay)) {
return
}
event.preventDefault()
triggerHaptic('close')
onClose()
}
window.addEventListener('keydown', onKeyDown)
return () => {
window.removeEventListener('keydown', onKeyDown)
releaseLayer()
}
}, [onClose])
return (
<div
className={cn(
'fixed inset-0 z-50 bg-black/22 backdrop-blur-[0.125rem]',
// Equidistant inset on every side. The top value is driven by the
// titlebar height so the card clears the OS traffic-lights vertically;
// since the card top already sits below them, the left needs no extra
// inset — keeping all sides equal so the card is ~full-width at any size.
'p-[calc(var(--titlebar-height)+0.625rem)]',
'sm:p-[calc(var(--titlebar-height)+0.875rem)]'
)}
// Every OverlayView-based overlay (settings, command-center, agents, cron,
// profiles, star map, …) covers the chat while the composer stays mounted
// beneath it. This marker tells `composerFocusBlockedBySurface` to stand
// the global type-to-focus / soft `/` / Enter down, so keystrokes don't
// leak into the hidden composer (and the overlay's own bare-key shortcuts,
// e.g. star map's Space, keep working).
data-overlay-surface=""
onClick={event => {
if (event.target === event.currentTarget) {
closeOverlay()
}
}}
role="presentation"
// Window-level chrome: overlays always clear the real titlebar. The
// contrib shell zeroes --titlebar-height for CONTENT areas (panes sit
// below its in-flow title bar), and CSS vars inherit through the DOM —
// so a fixed overlay mounted inside a zone would read 0 and bleed to
// the edges. Re-pin the real height at the overlay root.
style={{ '--titlebar-height': `${TITLEBAR_HEIGHT}px` } as CSSProperties}
>
<div className="relative h-full min-h-0">
<div
className={cn(
'relative flex h-full min-h-0 flex-col overflow-hidden rounded-xl border border-(--ui-stroke-secondary) bg-(--ui-chat-surface-background) shadow-md',
rootClassName
)}
// Marks the card as a RAISED surface for window glass: while the field
// behind it thins to show the desktop, this card stays near-opaque
// (see the [data-glass-raised] rules in styles.css). Inert otherwise.
data-glass-raised=""
>
<div className="pointer-events-none absolute inset-x-0 top-0 z-10 h-[calc(var(--titlebar-height)+0.1875rem)] [-webkit-app-region:drag]">
{headerContent && (
<div className="pointer-events-auto absolute left-1/2 top-[calc(0.5rem+var(--titlebar-height)/2)] -translate-x-1/2 -translate-y-1/2 [-webkit-app-region:no-drag]">
{headerContent}
</div>
)}
<div className="pointer-events-auto absolute right-3 top-[calc(0.1875rem+var(--titlebar-height)/2)] flex -translate-y-1/2 items-center gap-1.5 [-webkit-app-region:no-drag]">
{titlebarActions}
<Button
aria-label={closeLabel}
className="text-(--ui-text-tertiary) hover:bg-(--chrome-action-hover) hover:text-foreground"
onClick={closeOverlay}
size="icon-titlebar"
variant="ghost"
>
<TitlebarIcon name="close" />
</Button>
</div>
</div>
{/* No top padding here: the split-layout columns own their own
titlebar clearance so their backgrounds run flush to the card top
(otherwise the card surface shows as a gap above the sidebar). */}
<div className={cn('min-h-0 flex flex-1 flex-col', contentClassName)}>{children}</div>
</div>
{/* Sibling of the card, not a child: the card clips its own overflow
(rounded corners), and the badge deliberately straddles the top
border — half above, half below. */}
{edgeBadge && <div className="absolute left-1/2 top-0 z-20 -translate-x-1/2 -translate-y-1/2">{edgeBadge}</div>}
</div>
</div>
)
}
@@ -0,0 +1,32 @@
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
import { PanelRowMenu } from './panel'
beforeAll(() => {
Element.prototype.hasPointerCapture ??= () => false
Element.prototype.releasePointerCapture ??= () => undefined
Element.prototype.setPointerCapture ??= () => undefined
HTMLElement.prototype.scrollIntoView ??= () => undefined
})
describe('PanelRowMenu', () => {
afterEach(() => {
cleanup()
})
it('opens its actions menu from the kebab without a tooltip', async () => {
const onSelect = vi.fn()
render(<PanelRowMenu items={[{ label: 'Rename', onSelect }]} />)
const trigger = screen.getByRole('button', { name: 'Actions' })
expect(trigger.closest('[data-slot="tooltip-trigger"]')).toBeNull()
fireEvent.pointerDown(trigger, { button: 0, ctrlKey: false, pointerType: 'mouse' })
fireEvent.click(await screen.findByRole('menuitem', { name: 'Rename' }))
expect(onSelect).toHaveBeenCalledOnce()
})
})
+430
View File
@@ -0,0 +1,430 @@
import type { ReactNode } from 'react'
import { ActionsContextMenu, ActionsMenu, type MenuKit, renderActionItem } from '@/components/ui/actions-menu'
import { Button } from '@/components/ui/button'
import { Codicon } from '@/components/ui/codicon'
import { RowButton } from '@/components/ui/row-button'
import { SearchField } from '@/components/ui/search-field'
import { Tip } from '@/components/ui/tooltip'
import { translateNow } from '@/i18n'
import { cn } from '@/lib/utils'
import { OVERLAY_TOP_CLEARANCE, OverlayView } from './overlay-view'
// Overlay "panel" primitive — the centered, capped card + framed chrome lifted
// straight from the trace / agents overlay so every non-settings overlay (cron,
// profiles, …) speaks the same visual language: tight type scale, muted
// opacities, NO container borders (rows separate via the row-hover/active bg
// vars + gaps, exactly like the trace waterfall labels).
//
// Compose it as:
// <Panel onClose>
// <PanelHeader title subtitle actions={…} />
// <PanelBody> // master/detail row
// <PanelList>…</PanelList>
// <PanelDetail>…</PanelDetail>
// </PanelBody>
// </Panel>
//
// Single-column views drop their content straight after the header.
interface PanelProps {
children: ReactNode
// Root layout override (the card already fills the equidistant inset).
className?: string
closeLabel?: string
contentClassName?: string
onClose: () => void
}
export function Panel({
children,
className,
closeLabel = translateNow('common.close'),
contentClassName,
onClose
}: PanelProps) {
return (
<OverlayView
closeLabel={closeLabel}
// Header title rides up next to the floating close button — see
// OVERLAY_TOP_CLEARANCE, the shared clearance every overlay column uses.
contentClassName={cn('flex h-full min-h-0 flex-col px-4 pb-4 sm:px-5', OVERLAY_TOP_CLEARANCE, contentClassName)}
onClose={onClose}
rootClassName={cn('flex h-full w-full flex-col', className)}
>
{children}
</OverlayView>
)
}
interface PanelHeaderProps {
// Right-aligned controls (search, "+ New", segmented control, …).
actions?: ReactNode
subtitle?: ReactNode
title: ReactNode
}
export function PanelHeader({ actions, subtitle, title }: PanelHeaderProps) {
return (
// The overlay's close (X) is absolutely positioned at right-3 and costs no
// layout space, so header actions would otherwise slide right up against it.
// Reserve clearance (button footprint from the card edge + a small gap) on
// the right whenever actions are present.
<header className={cn('mb-3 flex shrink-0 items-start justify-between gap-3', actions ? 'pr-8' : undefined)}>
<div className="min-w-0">
<h2 className="text-sm font-semibold text-foreground">{title}</h2>
{subtitle ? <p className="truncate text-xs text-muted-foreground/80">{subtitle}</p> : null}
</div>
{actions ? <div className="flex shrink-0 items-center gap-1.5">{actions}</div> : null}
</header>
)
}
export function PanelBody({ children, className }: { children: ReactNode; className?: string }) {
return (
<div
className={cn(
// Side-by-side master/detail on a wide card; once it narrows (same
// threshold the other overlays collapse at) stack the list above the
// detail so the detail keeps full width instead of being squished.
'flex min-h-0 flex-1 flex-col gap-4 overflow-hidden min-[47.5rem]:flex-row min-[47.5rem]:gap-5',
className
)}
>
{children}
</div>
)
}
interface PanelListProps {
children: ReactNode
className?: string
// Pass an onSearchChange to bake a full-bleed filter field in above the items
// (pinned; the rows scroll under it). Controlled via searchValue.
onSearchChange?: (value: string) => void
searchLabel?: string
searchPlaceholder?: string
/** Data-derived rotating placeholder nudges (see SearchField.hints). */
searchHints?: string[]
searchValue?: string
}
// Left master list. Dense + borderless, like the trace waterfall's label tree:
// single-line rows that touch, separated from the detail only by the body gap.
// An optional search field pins to the top, full-bleed, above the scroll.
export function PanelList({
children,
className,
onSearchChange,
searchLabel,
searchPlaceholder,
searchHints,
searchValue
}: PanelListProps) {
return (
// Full-width and height-capped when stacked (narrow); a fixed 13rem rail
// beside the detail when wide.
<div className={cn('flex w-full shrink-0 flex-col max-[47.5rem]:max-h-[40%] min-[47.5rem]:w-52', className)}>
{onSearchChange ? (
<SearchField
aria-label={searchLabel ?? searchPlaceholder ?? ''}
containerClassName="mb-1 w-full shrink-0"
hints={searchHints}
onChange={onSearchChange}
placeholder={searchPlaceholder ?? ''}
value={searchValue ?? ''}
/>
) : null}
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto overscroll-contain">{children}</div>
</div>
)
}
interface PanelListRowProps {
active: boolean
// Leading status dot color class (e.g. 'bg-emerald-500'); omit for none.
dotClassName?: string
// Leading codicon glyph name (used when there's no lead/dot).
icon?: string
// Custom leading element (colored swatch, avatar, …). Wins over dot/icon.
lead?: ReactNode
// Per-row actions. Pass `menuItems` to get BOTH the hover kebab and a matching
// right-click menu from one array (preferred). `menu` takes a raw node for the
// rare custom trigger; it gets no right-click parity.
menu?: ReactNode
menuItems?: PanelMenuItem[]
// aria/tooltip label for the kebab + right-click menu built from `menuItems`.
menuLabel?: string
// Short always-visible trailing meta (a tag/time, like the trace label's duration).
meta?: ReactNode
onSelect: () => void
rowKey?: string
title: ReactNode
}
// A row is a container (not a <button>) so it can host both the select target
// and a kebab menu without nesting interactive elements. Hover/active bg lives
// on the wrapper so the whole row highlights as one. When `menuItems` is passed,
// the whole row also answers right-click with the same actions as its kebab.
export function PanelListRow({
active,
dotClassName,
icon,
lead,
menu,
menuItems,
menuLabel,
meta,
onSelect,
rowKey,
title
}: PanelListRowProps) {
const row = (
<div
className={cn(
'group/row row-hover relative flex h-7 w-full items-center rounded-md text-[0.78rem] hover:text-foreground',
active ? 'bg-(--ui-row-active-background) text-foreground' : 'text-(--ui-text-secondary)'
)}
data-panel-row={rowKey}
>
<RowButton
className="flex h-full min-w-0 flex-1 items-center gap-2 rounded-md pl-2 pr-1 text-left"
onClick={onSelect}
>
{lead ??
(dotClassName ? (
<span aria-hidden="true" className={cn('size-1.5 shrink-0 rounded-full', dotClassName)} />
) : icon ? (
<Codicon className="shrink-0 text-muted-foreground/55" name={icon} size="0.85rem" />
) : null)}
<span className="min-w-0 flex-1 truncate font-medium text-foreground/85">{title}</span>
</RowButton>
{meta ? <span className="shrink-0 pr-2 text-[0.62rem] tabular-nums text-muted-foreground/45">{meta}</span> : null}
{menuItems ? (
<div className="shrink-0 pr-1">
<PanelRowMenu items={menuItems} label={menuLabel} />
</div>
) : menu ? (
<div className="shrink-0 pr-1">{menu}</div>
) : null}
</div>
)
// Right-click parity: same items as the kebab. `disabled` (no actionable
// items) renders the row bare.
return menuItems ? (
<ActionsContextMenu
ariaLabel={menuLabel}
contentClassName="w-40"
disabled={menuItems.length === 0}
items={renderPanelMenuItems(menuItems)}
>
{row}
</ActionsContextMenu>
) : (
row
)
}
export interface PanelMenuItem {
disabled?: boolean
icon?: string
label: string
onSelect: () => void
tone?: 'danger' | 'default'
}
// Bridge PanelMenuItem[] → the shared actions-menu render fn, so a panel row's
// kebab and its right-click menu render from one source.
function renderPanelMenuItems(items: PanelMenuItem[]) {
return (kit: MenuKit) =>
items.map(item =>
renderActionItem(kit, {
disabled: item.disabled,
icon: item.icon,
key: item.label,
label: item.label,
onSelect: item.onSelect,
variant: item.tone === 'danger' ? 'destructive' : 'default'
})
)
}
// Per-row "⋮" actions menu — mirrors the sidebar session row's settled pattern
// (size-5 ghost trigger + kebab-vertical codicon + w-40 content). Hidden until
// the row is hovered/focused (or the menu is open). Returns null with no items
// (e.g. the default profile, which can't be renamed/deleted).
export function PanelRowMenu({ items, label = 'Actions' }: { items: PanelMenuItem[]; label?: string }) {
if (items.length === 0) {
return null
}
return (
<ActionsMenu ariaLabel={label} contentClassName="w-40" items={renderPanelMenuItems(items)}>
<Button
aria-label={label}
className="size-5 rounded-[4px] bg-transparent text-(--ui-text-tertiary) opacity-0 transition-colors duration-100 hover:bg-(--ui-control-active-background) hover:text-foreground focus-visible:opacity-100 focus-visible:ring-0 group-hover/row:opacity-100 data-[state=open]:bg-(--ui-control-active-background) data-[state=open]:text-foreground data-[state=open]:opacity-100 [&_svg]:size-3.5!"
size="icon"
variant="ghost"
>
<Codicon name="kebab-vertical" size="0.875rem" />
</Button>
</ActionsMenu>
)
}
// Scrolling detail region. Fills the column (no right rail here, unlike the
// trace inspector), so the content stretches the full available width.
export function PanelDetail({ children, className }: { children: ReactNode; className?: string }) {
return (
<div className={cn('min-h-0 flex-1 overflow-y-auto overscroll-contain', className)}>
<div className="space-y-4 pb-6 pl-1 pr-2">{children}</div>
</div>
)
}
interface PanelEmptyProps {
action?: ReactNode
description?: ReactNode
// Codicon glyph name (e.g. 'hubot', 'warning', 'loading~spin').
icon?: string
title?: ReactNode
}
export function PanelEmpty({ action, description, icon = 'inbox', title }: PanelEmptyProps) {
return (
<div className="grid flex-1 place-items-center px-6 py-10 text-center">
<div className="flex flex-col items-center gap-2">
<Codicon className="text-muted-foreground/50" name={icon} size="1.25rem" />
{title ? <p className="text-sm font-medium text-foreground/90">{title}</p> : null}
{description ? (
<p className="max-w-sm text-xs leading-relaxed text-muted-foreground/70">{description}</p>
) : null}
{action ? <div className="mt-2">{action}</div> : null}
</div>
</div>
)
}
export function PanelSectionLabel({ children, className }: { children: ReactNode; className?: string }) {
return (
<div className={cn('text-[0.6rem] font-medium uppercase tracking-wider text-muted-foreground/50', className)}>
{children}
</div>
)
}
// Inspector-style key/value grid (mirrors the trace span inspector's <dl>).
export interface PanelMetaRow {
label: ReactNode
value: ReactNode
}
export function PanelMeta({ className, rows }: { className?: string; rows: PanelMetaRow[] }) {
return (
<dl className={cn('grid grid-cols-[5rem_1fr] gap-x-2 gap-y-1 text-[0.7rem]', className)}>
{rows.map((row, i) => (
<div className="contents" key={typeof row.label === 'string' ? row.label : i}>
<dt className="truncate text-muted-foreground/55">{row.label}</dt>
<dd className="min-w-0 break-words text-foreground/85">{row.value}</dd>
</div>
))}
</dl>
)
}
// Monospace content block (job prompt, etc.) — mirrors the inspector's
// input/output <pre> blocks: subtle bg, no border.
export function PanelBlock({ children, className }: { children: ReactNode; className?: string }) {
return (
<pre
className={cn(
'max-h-48 overflow-auto whitespace-pre-wrap break-words rounded bg-foreground/5 p-2.5 text-[0.68rem] leading-relaxed text-foreground/80',
className
)}
>
{children}
</pre>
)
}
export type PanelPillTone = 'bad' | 'good' | 'muted' | 'warn'
const PILL_TONE: Record<PanelPillTone, string> = {
bad: 'bg-destructive/10 text-destructive',
good: 'bg-primary/10 text-primary',
muted: 'bg-foreground/10 text-muted-foreground',
warn: 'bg-amber-500/10 text-amber-600 dark:text-amber-300'
}
export function PanelPill({ children, tone = 'muted' }: { children: ReactNode; tone?: PanelPillTone }) {
return (
<span
className={cn(
'inline-flex items-center rounded-full px-1.5 py-0.5 text-[0.62rem] font-medium capitalize',
PILL_TONE[tone]
)}
>
{children}
</span>
)
}
// Self-describing centered "+" that sits as the LAST item in a PanelList. The
// label rides aria/title only — no visible text.
export function PanelAddButton({
icon = 'add',
label,
onClick
}: {
icon?: string
label: string
onClick: () => void
}) {
return (
<Tip label={label}>
<Button
aria-label={label}
className="h-7 w-full shrink-0 justify-center text-muted-foreground/70 hover:bg-(--ui-row-hover-background) hover:text-foreground"
onClick={onClick}
size="sm"
variant="ghost"
>
<Codicon name={icon} size="0.875rem" />
</Button>
</Tip>
)
}
// Visible action for a detail header (cron pause/resume/trigger, …). Ghost by
// default; `primary` promotes the header's main action to a filled button.
export function PanelAction({
children,
disabled,
icon,
onClick,
primary
}: {
children: ReactNode
disabled?: boolean
icon: string
onClick: () => void
primary?: boolean
}) {
return (
<Button
className={cn(
'gap-1.5',
!primary && 'text-muted-foreground hover:bg-(--ui-row-hover-background) hover:text-foreground'
)}
disabled={disabled}
onClick={onClick}
size="sm"
variant={primary ? 'default' : 'ghost'}
>
<Codicon name={icon} size="0.875rem" />
{children}
</Button>
)
}
@@ -0,0 +1,31 @@
// @vitest-environment jsdom
/**
* The peek is scoped with :has() so only the settings overlay that owns the
* translucency row pays for an opacity transition. jsdom implements :has() in
* matches(), so the scoping claim is checkable directly: the marked overlay
* matches the peek selector and an unmarked one does not.
*/
import { describe, expect, it } from 'vitest'
const PEEK_SELECTOR = '[data-overlay-surface]:has([data-translucency-peek-scope])'
describe('peek scoping', () => {
it('matches only the overlay that armed the interaction', () => {
document.body.innerHTML = `
<div data-overlay-surface id="settings">
<div data-translucency-peek-scope></div>
</div>
<div data-overlay-surface id="command-center">
<div></div>
</div>
`
const settings = document.getElementById('settings')
const commandCenter = document.getElementById('command-center')
expect(settings?.matches(PEEK_SELECTOR)).toBe(true)
// The regression this guards: an unscoped rule gave EVERY overlay in the
// app a 420ms opacity transition for the life of the process.
expect(commandCenter?.matches(PEEK_SELECTOR)).toBe(false)
})
})