@workerdeck/ui 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +3 -2
  2. package/build/{SessionPanel-BjNAnWMF.d.mts → SessionPanel-13l25ubU.d.mts} +15 -5
  3. package/build/{SessionPanel-DNuPKg5c.mjs → SessionPanel-DgwjH4Ve.mjs} +624 -221
  4. package/build/SessionPanel-DgwjH4Ve.mjs.map +1 -0
  5. package/build/index.d.mts +54 -21
  6. package/build/index.mjs +7 -6
  7. package/build/index.mjs.map +1 -1
  8. package/build/scoped.css +3547 -0
  9. package/build/workspace.d.mts +1 -1
  10. package/build/workspace.mjs +1 -1
  11. package/package.json +10 -6
  12. package/src/components/agent/Composer.tsx +10 -10
  13. package/src/components/agent/ContextDialog.tsx +3 -2
  14. package/src/components/agent/Conversation.tsx +1 -1
  15. package/src/components/agent/McpDialog.tsx +3 -1
  16. package/src/components/agent/Scrubber.tsx +248 -0
  17. package/src/components/agent/SessionList.tsx +3 -1
  18. package/src/components/agent/SessionPanel.tsx +20 -7
  19. package/src/components/agent/SessionSteps.tsx +10 -4
  20. package/src/components/agent/SkillsDialog.tsx +3 -2
  21. package/src/components/agent/StatusBar.tsx +1 -1
  22. package/src/components/agent/Transcript.tsx +147 -73
  23. package/src/components/agent/UsageDialog.tsx +3 -1
  24. package/src/components/agent/scrubber-marks.ts +277 -0
  25. package/src/components/terminal/PermissionPrompt.tsx +3 -0
  26. package/src/components/terminal/QuestionPrompt.tsx +3 -0
  27. package/src/components/terminal/StatusLine.tsx +3 -1
  28. package/src/components/ui/AlertDialog.tsx +23 -20
  29. package/src/components/ui/Dialog.tsx +26 -23
  30. package/src/components/ui/Menu.tsx +20 -17
  31. package/src/components/ui/PortalScope.tsx +27 -0
  32. package/src/components/ui/Select.tsx +19 -16
  33. package/src/components/ui/Tooltip.tsx +13 -10
  34. package/src/styles/scoped.entry.css +16 -0
  35. package/src/styles/terminal.css +1 -1
  36. package/src/styles/theme.css +160 -0
  37. package/build/SessionPanel-DNuPKg5c.mjs.map +0 -1
@@ -27,12 +27,14 @@ export interface TerminalPermissionPromptProps {
27
27
  /** `message` is fed back to the agent, which can then try something else;
28
28
  * `interrupt` also stops the turn. */
29
29
  onDeny: (requestId: string, message?: string, interrupt?: boolean) => void
30
+ className?: string
30
31
  }
31
32
 
32
33
  export function TerminalPermissionPrompt({
33
34
  request,
34
35
  onApprove,
35
36
  onDeny,
37
+ className,
36
38
  }: TerminalPermissionPromptProps) {
37
39
  const [focused, setFocused] = useState(0)
38
40
  const [denying, setDenying] = useState(false)
@@ -75,6 +77,7 @@ export function TerminalPermissionPrompt({
75
77
  return (
76
78
  <div
77
79
  data-slot='permission-prompt'
80
+ className={className}
78
81
  onKeyDown={(event) => {
79
82
  if (event.key === 'Escape') {
80
83
  event.preventDefault()
@@ -40,12 +40,14 @@ export interface TerminalQuestionPromptProps {
40
40
  onAnswer: (requestId: string, updatedInput: Record<string, unknown>) => void
41
41
  /** Deny the tool — the model proceeds without an answer. */
42
42
  onDismiss: (requestId: string, message?: string) => void
43
+ className?: string
43
44
  }
44
45
 
45
46
  export function TerminalQuestionPrompt({
46
47
  request,
47
48
  onAnswer,
48
49
  onDismiss,
50
+ className,
49
51
  }: TerminalQuestionPromptProps) {
50
52
  const questions = parseUserQuestions(request.input)
51
53
  const [selections, setSelections] = useState<Selection[]>(() => questions.map(() => EMPTY))
@@ -97,6 +99,7 @@ export function TerminalQuestionPrompt({
97
99
  return (
98
100
  <div
99
101
  data-slot='question-prompt'
102
+ className={className}
100
103
  onKeyDown={(event) => {
101
104
  if (event.key === 'Escape') {
102
105
  event.preventDefault()
@@ -38,6 +38,7 @@ const SEVERITY_TONE: Record<StatusSeverity, Tone> = {
38
38
  }
39
39
 
40
40
  export interface TerminalStatusLineProps {
41
+ className?: string
41
42
  state: TranscriptState
42
43
  /** Plan windows to read from, when they should not be the session's own — the
43
44
  * gateway's per-profile state merged over this transcript's. See
@@ -75,6 +76,7 @@ function Reading({
75
76
  }
76
77
 
77
78
  export function TerminalStatusLine({
79
+ className,
78
80
  state,
79
81
  rateLimits,
80
82
  connection,
@@ -147,7 +149,7 @@ export function TerminalStatusLine({
147
149
  }
148
150
 
149
151
  return (
150
- <Row data-slot='status-line' tone='dim'>
152
+ <Row data-slot='status-line' tone='dim' className={className}>
151
153
  {parts.map((part, index) => (
152
154
  <span key={index}>
153
155
  {index > 0 ? <Ink tone='faint'> · </Ink> : null}
@@ -1,6 +1,7 @@
1
1
  import { type FunctionComponent } from 'react'
2
2
  import { AlertDialog as AlertDialogPrimitive } from '@base-ui/react/alert-dialog'
3
3
  import { cn } from '../../lib/utils.ts'
4
+ import { PortalScope } from './PortalScope.tsx'
4
5
 
5
6
  export const AlertDialog = AlertDialogPrimitive.Root
6
7
  export const AlertDialogTrigger = AlertDialogPrimitive.Trigger
@@ -12,26 +13,28 @@ export const AlertDialogContent: FunctionComponent<AlertDialogPrimitive.Popup.Pr
12
13
  ...props
13
14
  }) => (
14
15
  <AlertDialogPrimitive.Portal>
15
- <AlertDialogPrimitive.Backdrop
16
- className={cn(
17
- 'fixed inset-0 z-70 bg-black/40 backdrop-blur-[1px]',
18
- 'transition-opacity duration-(--motion-base)',
19
- 'data-starting-style:opacity-0 data-ending-style:opacity-0',
20
- )}
21
- />
22
- <AlertDialogPrimitive.Popup
23
- data-slot='alert-dialog-content'
24
- className={cn(
25
- 'fixed top-1/2 left-1/2 z-70 w-[min(28rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2',
26
- 'rounded-lg border border-border bg-surface p-5 shadow-(--shadow-lg) outline-none',
27
- 'transition-[opacity,transform] duration-(--motion-base)',
28
- 'data-starting-style:scale-95 data-starting-style:opacity-0',
29
- 'data-ending-style:scale-95 data-ending-style:opacity-0',
30
- className,
31
- )}
32
- {...props}>
33
- {children}
34
- </AlertDialogPrimitive.Popup>
16
+ <PortalScope>
17
+ <AlertDialogPrimitive.Backdrop
18
+ className={cn(
19
+ 'fixed inset-0 z-70 bg-black/40 backdrop-blur-[1px]',
20
+ 'transition-opacity duration-(--motion-base)',
21
+ 'data-starting-style:opacity-0 data-ending-style:opacity-0',
22
+ )}
23
+ />
24
+ <AlertDialogPrimitive.Popup
25
+ data-slot='alert-dialog-content'
26
+ className={cn(
27
+ 'fixed top-1/2 left-1/2 z-70 w-[min(28rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2',
28
+ 'rounded-lg border border-border bg-surface p-5 shadow-(--shadow-lg) outline-none',
29
+ 'transition-[opacity,transform] duration-(--motion-base)',
30
+ 'data-starting-style:scale-95 data-starting-style:opacity-0',
31
+ 'data-ending-style:scale-95 data-ending-style:opacity-0',
32
+ className,
33
+ )}
34
+ {...props}>
35
+ {children}
36
+ </AlertDialogPrimitive.Popup>
37
+ </PortalScope>
35
38
  </AlertDialogPrimitive.Portal>
36
39
  )
37
40
 
@@ -2,6 +2,7 @@ import { type FunctionComponent } from 'react'
2
2
  import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
3
3
  import { X } from 'lucide-react'
4
4
  import { cn } from '../../lib/utils.ts'
5
+ import { PortalScope } from './PortalScope.tsx'
5
6
 
6
7
  export const Dialog = DialogPrimitive.Root
7
8
  export const DialogTrigger = DialogPrimitive.Trigger
@@ -18,29 +19,31 @@ export const DialogContent: FunctionComponent<
18
19
  DialogPrimitive.Popup.Props & { size?: 'sm' | 'md' | 'lg' }
19
20
  > = ({ className, children, size = 'md', ...props }) => (
20
21
  <DialogPrimitive.Portal>
21
- <DialogPrimitive.Backdrop
22
- className={cn(
23
- 'fixed inset-0 z-70 bg-black/40 backdrop-blur-[1px]',
24
- 'transition-opacity duration-(--motion-base)',
25
- 'data-starting-style:opacity-0 data-ending-style:opacity-0',
26
- )}
27
- />
28
- <DialogPrimitive.Popup
29
- data-slot='dialog-content'
30
- className={cn(
31
- 'fixed top-1/2 left-1/2 z-70 flex max-h-[min(42rem,calc(100dvh-3rem))] -translate-x-1/2 -translate-y-1/2 flex-col',
32
- size === 'sm' && 'w-[min(24rem,calc(100vw-2rem))]',
33
- size === 'md' && 'w-[min(32rem,calc(100vw-2rem))]',
34
- size === 'lg' && 'w-[min(46rem,calc(100vw-2rem))]',
35
- 'rounded-lg border border-border bg-surface shadow-(--shadow-lg) outline-none',
36
- 'transition-[opacity,transform] duration-(--motion-base)',
37
- 'data-starting-style:scale-95 data-starting-style:opacity-0',
38
- 'data-ending-style:scale-95 data-ending-style:opacity-0',
39
- className,
40
- )}
41
- {...props}>
42
- {children}
43
- </DialogPrimitive.Popup>
22
+ <PortalScope>
23
+ <DialogPrimitive.Backdrop
24
+ className={cn(
25
+ 'fixed inset-0 z-70 bg-black/40 backdrop-blur-[1px]',
26
+ 'transition-opacity duration-(--motion-base)',
27
+ 'data-starting-style:opacity-0 data-ending-style:opacity-0',
28
+ )}
29
+ />
30
+ <DialogPrimitive.Popup
31
+ data-slot='dialog-content'
32
+ className={cn(
33
+ 'fixed top-1/2 left-1/2 z-70 flex max-h-[min(42rem,calc(100dvh-3rem))] -translate-x-1/2 -translate-y-1/2 flex-col',
34
+ size === 'sm' && 'w-[min(24rem,calc(100vw-2rem))]',
35
+ size === 'md' && 'w-[min(32rem,calc(100vw-2rem))]',
36
+ size === 'lg' && 'w-[min(46rem,calc(100vw-2rem))]',
37
+ 'rounded-lg border border-border bg-surface shadow-(--shadow-lg) outline-none',
38
+ 'transition-[opacity,transform] duration-(--motion-base)',
39
+ 'data-starting-style:scale-95 data-starting-style:opacity-0',
40
+ 'data-ending-style:scale-95 data-ending-style:opacity-0',
41
+ className,
42
+ )}
43
+ {...props}>
44
+ {children}
45
+ </DialogPrimitive.Popup>
46
+ </PortalScope>
44
47
  </DialogPrimitive.Portal>
45
48
  )
46
49
 
@@ -1,6 +1,7 @@
1
1
  import { type FunctionComponent } from 'react'
2
2
  import { Menu as MenuPrimitive } from '@base-ui/react/menu'
3
3
  import { cn } from '../../lib/utils.ts'
4
+ import { PortalScope } from './PortalScope.tsx'
4
5
 
5
6
  export const Menu = MenuPrimitive.Root
6
7
  export const MenuTrigger = MenuPrimitive.Trigger
@@ -10,23 +11,25 @@ export const MenuContent: FunctionComponent<
10
11
  Pick<MenuPrimitive.Positioner.Props, 'align' | 'side' | 'sideOffset'>
11
12
  > = ({ className, align = 'end', side = 'bottom', sideOffset = 6, ...props }) => (
12
13
  <MenuPrimitive.Portal>
13
- <MenuPrimitive.Positioner
14
- align={align}
15
- side={side}
16
- sideOffset={sideOffset}
17
- className='isolate z-80 outline-none'>
18
- <MenuPrimitive.Popup
19
- data-slot='menu-content'
20
- className={cn(
21
- 'min-w-48 rounded-md border border-border bg-surface p-1 text-fg-1 shadow-(--shadow-lg) outline-none',
22
- 'transition-[opacity,transform] duration-(--motion-base)',
23
- 'data-starting-style:scale-95 data-starting-style:opacity-0',
24
- 'data-ending-style:scale-95 data-ending-style:opacity-0',
25
- className,
26
- )}
27
- {...props}
28
- />
29
- </MenuPrimitive.Positioner>
14
+ <PortalScope>
15
+ <MenuPrimitive.Positioner
16
+ align={align}
17
+ side={side}
18
+ sideOffset={sideOffset}
19
+ className='isolate z-80 outline-none'>
20
+ <MenuPrimitive.Popup
21
+ data-slot='menu-content'
22
+ className={cn(
23
+ 'min-w-48 rounded-md border border-border bg-surface p-1 text-fg-1 shadow-(--shadow-lg) outline-none',
24
+ 'transition-[opacity,transform] duration-(--motion-base)',
25
+ 'data-starting-style:scale-95 data-starting-style:opacity-0',
26
+ 'data-ending-style:scale-95 data-ending-style:opacity-0',
27
+ className,
28
+ )}
29
+ {...props}
30
+ />
31
+ </MenuPrimitive.Positioner>
32
+ </PortalScope>
30
33
  </MenuPrimitive.Portal>
31
34
  )
32
35
 
@@ -0,0 +1,27 @@
1
+ import { type FunctionComponent, type ReactNode } from 'react'
2
+
3
+ /**
4
+ * Re-establishes the `.wd-root` styling scope inside a portal.
5
+ *
6
+ * The scoped stylesheet (`@workerdeck/ui/scoped.css`) rewrites every rule to
7
+ * live under `.wd-root`, so an embedder's page keeps its own design system and
8
+ * the panel keeps this one. Base UI popups (Menu, Dialog, AlertDialog, Select,
9
+ * Tooltip) portal to `document.body` — OUTSIDE the embedder's wrapper — so
10
+ * without this they would render unstyled, or worse, styled by the host.
11
+ *
12
+ * `display: contents` (inline, so no stylesheet needs to load for it) makes the
13
+ * element generate no box: positioning, hit-testing and the popup's own layout
14
+ * are untouched. It still carries the class, which is all the scope needs —
15
+ * design tokens declared on `.wd-root` inherit through it, and every
16
+ * `.wd-root <x>` rule matches below it. Theme keeps working because the dark
17
+ * token block matches `[data-theme='dark']` on ANY ancestor, and `document.body`
18
+ * sits under the host's themed `<html>` just like the panel does.
19
+ *
20
+ * Under the classic `theme.css` integration the class matches no rule and the
21
+ * element is inert, so the wrapper is unconditional.
22
+ */
23
+ export const PortalScope: FunctionComponent<{ children?: ReactNode }> = ({ children }) => (
24
+ <div className='wd-root' style={{ display: 'contents' }}>
25
+ {children}
26
+ </div>
27
+ )
@@ -2,6 +2,7 @@ import { type FunctionComponent } from 'react'
2
2
  import { Select as SelectPrimitive } from '@base-ui/react/select'
3
3
  import { Check, ChevronsUpDown } from 'lucide-react'
4
4
  import { cn } from '../../lib/utils.ts'
5
+ import { PortalScope } from './PortalScope.tsx'
5
6
 
6
7
  export const Select = SelectPrimitive.Root
7
8
  export const SelectValue = SelectPrimitive.Value
@@ -40,22 +41,24 @@ export const SelectContent: FunctionComponent<
40
41
  ...props
41
42
  }) => (
42
43
  <SelectPrimitive.Portal>
43
- <SelectPrimitive.Positioner
44
- align={align}
45
- alignItemWithTrigger={alignItemWithTrigger}
46
- side={side}
47
- sideOffset={sideOffset}
48
- className='isolate z-80 outline-none'>
49
- <SelectPrimitive.Popup
50
- data-slot='select-content'
51
- className={cn(
52
- 'max-h-[min(24rem,var(--available-height))] min-w-[var(--anchor-width)] overflow-y-auto',
53
- 'rounded-md border border-border bg-surface p-1 text-fg-1 shadow-(--shadow-lg) outline-none',
54
- className,
55
- )}
56
- {...props}
57
- />
58
- </SelectPrimitive.Positioner>
44
+ <PortalScope>
45
+ <SelectPrimitive.Positioner
46
+ align={align}
47
+ alignItemWithTrigger={alignItemWithTrigger}
48
+ side={side}
49
+ sideOffset={sideOffset}
50
+ className='isolate z-80 outline-none'>
51
+ <SelectPrimitive.Popup
52
+ data-slot='select-content'
53
+ className={cn(
54
+ 'max-h-[min(24rem,var(--available-height))] min-w-[var(--anchor-width)] overflow-y-auto',
55
+ 'rounded-md border border-border bg-surface p-1 text-fg-1 shadow-(--shadow-lg) outline-none',
56
+ className,
57
+ )}
58
+ {...props}
59
+ />
60
+ </SelectPrimitive.Positioner>
61
+ </PortalScope>
59
62
  </SelectPrimitive.Portal>
60
63
  )
61
64
 
@@ -1,6 +1,7 @@
1
1
  import { type FunctionComponent, type ReactElement, type ReactNode } from 'react'
2
2
  import { Tooltip as TooltipPrimitive } from '@base-ui/react/tooltip'
3
3
  import { cn } from '../../lib/utils.ts'
4
+ import { PortalScope } from './PortalScope.tsx'
4
5
 
5
6
  export const TooltipProvider = TooltipPrimitive.Provider
6
7
 
@@ -8,16 +9,18 @@ export const TooltipContent: FunctionComponent<
8
9
  TooltipPrimitive.Popup.Props & Pick<TooltipPrimitive.Positioner.Props, 'side' | 'sideOffset'>
9
10
  > = ({ className, side = 'top', sideOffset = 6, ...props }) => (
10
11
  <TooltipPrimitive.Portal>
11
- <TooltipPrimitive.Positioner side={side} sideOffset={sideOffset} className='isolate z-90'>
12
- <TooltipPrimitive.Popup
13
- data-slot='tooltip-content'
14
- className={cn(
15
- 'rounded-md border border-border bg-surface px-2 py-1 text-label text-fg-2 shadow-(--shadow-md) outline-none',
16
- className,
17
- )}
18
- {...props}
19
- />
20
- </TooltipPrimitive.Positioner>
12
+ <PortalScope>
13
+ <TooltipPrimitive.Positioner side={side} sideOffset={sideOffset} className='isolate z-90'>
14
+ <TooltipPrimitive.Popup
15
+ data-slot='tooltip-content'
16
+ className={cn(
17
+ 'rounded-md border border-border bg-surface px-2 py-1 text-label text-fg-2 shadow-(--shadow-md) outline-none',
18
+ className,
19
+ )}
20
+ {...props}
21
+ />
22
+ </TooltipPrimitive.Positioner>
23
+ </PortalScope>
21
24
  </TooltipPrimitive.Portal>
22
25
  )
23
26
 
@@ -0,0 +1,16 @@
1
+ /*
2
+ * Build entry for the SCOPED stylesheet (`@workerdeck/ui/scoped.css`) — see
3
+ * `scripts/build-scoped-css.mjs`, which compiles this with the Tailwind CLI and
4
+ * then rewrites every selector under `.wd-root`. Not shipped as-is and not an
5
+ * export: embedders import the compiled `scoped.css` artifact.
6
+ *
7
+ * `source(none)` + explicit `@source`: the utilities compiled here are exactly
8
+ * the ones this package's own components (and streamdown, whose markdown
9
+ * renderer ships Tailwind-classed markup) use — hermetic, independent of any
10
+ * host app's Tailwind build.
11
+ */
12
+ @import 'tailwindcss' source(none);
13
+ @source '../components';
14
+ @source '../lib';
15
+ @source '../../node_modules/streamdown/dist';
16
+ @import './theme.css';
@@ -595,7 +595,7 @@
595
595
  [data-terminal] .term-composer-body {
596
596
  margin-inline: auto;
597
597
  width: 100%;
598
- max-width: var(--wd-content-max-w, 48rem);
598
+ max-width: var(--wd-transcript-max-width);
599
599
  padding-inline: var(--term-bleed, 0px);
600
600
  /* Air on each side of the prompt, and a plain pixel value rather than a
601
601
  fraction of `--term-line`: this is chrome between two rules, not a
@@ -151,6 +151,10 @@
151
151
  --shine: #dcdcdc;
152
152
  --frame-shadow: var(--shadow-xs);
153
153
 
154
+ /* The overview rail's bookmark magenta — the app palette has no magenta,
155
+ and every other colour on the rail is already spoken for. */
156
+ --wd-scrub-bookmark: #8b3a8b;
157
+
154
158
  /* ---------- aliases (bridge for the @theme utilities) ---------- */
155
159
  --surface: var(--bg-surface);
156
160
  --sidebar: #f4f4f4;
@@ -234,6 +238,9 @@
234
238
  0 4px 24px rgba(0, 0, 0, 0.55),
235
239
  0 2px 6px rgba(0, 0, 0, 0.45);
236
240
 
241
+ /* See the light block — lifted for the dark ground. */
242
+ --wd-scrub-bookmark: #c586c0;
243
+
237
244
  /* ---------- aliases ---------- */
238
245
  --surface: var(--bg-surface);
239
246
  --sidebar: #131313;
@@ -267,6 +274,12 @@
267
274
  --motion-base: 150ms;
268
275
  --motion-slow: 220ms;
269
276
 
277
+ /* ---------- Geometry (embedder-overridable layout knobs) ---------- */
278
+ --wd-status-bar-height: 38px;
279
+ --wd-composer-padding: 12px;
280
+ --wd-transcript-row-gap: 0px;
281
+ --wd-transcript-max-width: 48rem;
282
+
270
283
  /* Stock-name bridge → raw custom properties (shadcn idiom compatibility). */
271
284
  --primary: var(--accent);
272
285
  --primary-foreground: var(--accent-fg);
@@ -611,3 +624,150 @@
611
624
  transition: none !important;
612
625
  }
613
626
  }
627
+
628
+ /* ── Sticky prompt (cards variant) ─────────────────────────────────────────
629
+ *
630
+ * The same lane/head/sentinel mechanism as the terminal theme's pinned prompt
631
+ * (terminal.css scopes its rules under `[data-terminal]`; these key on the
632
+ * lane's own `data-sticky-lane='cards'` value) — but the head's CONTENT is
633
+ * different in kind, and that is the whole design. The terminal head is the
634
+ * row rendered again, clipped to one line — exact, because that theme is a
635
+ * monospace grid. A proportional message card clipped by height is a sliced
636
+ * bubble, so the cards head never renders the Message component at all:
637
+ * `StickyPromptLane` (Transcript.tsx) hands it the prompt as one run of plain
638
+ * text, and these rules only draw a bar. Nothing to flatten, nothing to
639
+ * un-style, nothing to `!important` over.
640
+ *
641
+ * Alignment is by construction, not restatement: the lane sits inside
642
+ * ConversationContent's centred, padded column (`max-w` + `mx-auto` + `px-4`)
643
+ * and spans it edge to edge, so the bar's text starts on the same left edge
644
+ * as the response paragraphs and the ellipsis lands on the column's right
645
+ * edge — no max-width or inline padding repeated here. The bar carries no
646
+ * gap class either (the measured row keeps it): it is not an overlay of the
647
+ * row's first line, so it has no in-flow y to match, and the 1st prompt (no
648
+ * gap) and every later one (pt-4/pt-2) share these rules unchanged. */
649
+ [data-slot='transcript-rows'] > [data-sticky-lane='cards'] {
650
+ z-index: 3;
651
+ pointer-events: none;
652
+ }
653
+
654
+ /* The head's own strip: the full lane, out of flow — see terminal.css for why
655
+ sticky confinement wants the border box, not a cancelled margin box. */
656
+ [data-sticky-lane='cards'] > [data-sticky-headlane] {
657
+ position: absolute;
658
+ inset: 0;
659
+ pointer-events: none;
660
+ }
661
+
662
+ /* The bar: ~28px, one line centred by its own line-height, truncated at the
663
+ column's right edge, on a frosted ground so the rows passing beneath read
664
+ as beneath. Hidden until actually stuck (`data-stuck`, set by the lane's
665
+ sentinel observer): unstuck it sits at the lane's top, over the real
666
+ bubble. */
667
+ [data-sticky-lane='cards'] > [data-sticky-headlane] > [data-sticky-head] {
668
+ position: sticky;
669
+ top: 0;
670
+ height: 1.75rem;
671
+ line-height: 1.75rem;
672
+ overflow: hidden;
673
+ white-space: nowrap;
674
+ text-overflow: ellipsis;
675
+ font-size: var(--text-body-sm);
676
+ color: var(--fg-2);
677
+ background: color-mix(in srgb, var(--bg) 88%, transparent);
678
+ backdrop-filter: blur(12px);
679
+ -webkit-backdrop-filter: blur(12px);
680
+ border-bottom: 1px solid var(--border);
681
+ user-select: none;
682
+ visibility: hidden;
683
+ }
684
+
685
+ [data-sticky-lane='cards'] > [data-sticky-headlane] > [data-sticky-head][data-stuck] {
686
+ visibility: visible;
687
+ }
688
+
689
+ /* ── Overview scrubber (cards variant) ──────────────────────────────────────
690
+ *
691
+ * The proportional rail (`components/agent/Scrubber.tsx`): same geometry as the
692
+ * terminal rail — 12px, two 6px lanes plus full width — and the same palette
693
+ * mapped onto the app tokens. It is an annotation rail, never a scrollbar: the
694
+ * container takes no pointer events, so the native scrollbar keeps working
695
+ * straight through the paint. */
696
+ .wd-scrubber {
697
+ position: absolute;
698
+ top: 0;
699
+ right: 0;
700
+ bottom: 0;
701
+ width: 12px;
702
+ z-index: 10;
703
+ pointer-events: none;
704
+ }
705
+ .wd-scrubber .wd-scrub-mark {
706
+ position: absolute;
707
+ min-height: 2px;
708
+ }
709
+ .wd-scrubber[data-interactive] .wd-scrub-mark {
710
+ pointer-events: auto;
711
+ cursor: pointer;
712
+ }
713
+ .wd-scrubber[data-interactive] .wd-scrub-mark:hover {
714
+ outline: 1px solid color-mix(in srgb, var(--fg-1) 40%, transparent);
715
+ }
716
+ .wd-scrubber .wd-scrub-mark[data-lane='l'] { left: 0; width: 6px; }
717
+ .wd-scrubber .wd-scrub-mark[data-lane='r'] { left: 6px; width: 6px; }
718
+ .wd-scrubber .wd-scrub-mark[data-lane='f'] { left: 0; width: 12px; }
719
+ .wd-scrubber .wd-scrub-mark[data-kind='user'] { background: var(--accent); }
720
+ .wd-scrubber .wd-scrub-mark[data-kind='subagent'] { background: var(--success); }
721
+ .wd-scrubber .wd-scrub-mark[data-kind='turn'] { background: var(--fg-3); }
722
+ .wd-scrubber .wd-scrub-mark[data-kind='turnFailed'] { background: var(--danger); }
723
+ .wd-scrubber .wd-scrub-mark[data-kind='error'] { background: var(--danger); }
724
+ .wd-scrubber .wd-scrub-mark[data-kind='toolFailed'] {
725
+ background: color-mix(in srgb, var(--danger) 55%, transparent);
726
+ }
727
+ .wd-scrubber .wd-scrub-mark[data-kind='bookmark'] { background: var(--wd-scrub-bookmark); }
728
+ .wd-scrubber .wd-scrub-mark[data-kind='approval'] {
729
+ background: var(--warning);
730
+ animation: wd-scrub-pulse 1.2s infinite;
731
+ }
732
+ .wd-scrubber .wd-scrub-mark[data-kind='recap'] {
733
+ background: repeating-linear-gradient(to right, var(--fg-4) 0 3px, transparent 3px 6px)
734
+ left center / 100% 1px no-repeat;
735
+ }
736
+ @keyframes wd-scrub-pulse {
737
+ 50% { opacity: 0.3; }
738
+ }
739
+ @media (prefers-reduced-motion: reduce) {
740
+ .wd-scrubber .wd-scrub-mark[data-kind='approval'] {
741
+ animation: none;
742
+ }
743
+ }
744
+
745
+ .wd-scrubber .wd-scrub-peek {
746
+ position: absolute;
747
+ right: calc(100% + 6px);
748
+ width: 320px;
749
+ max-width: 60vw;
750
+ display: flex;
751
+ flex-direction: column;
752
+ gap: 2px;
753
+ padding: 8px 10px;
754
+ border: 1px solid var(--border-strong);
755
+ border-radius: var(--radius-md, 6px);
756
+ background: var(--bg);
757
+ box-shadow: var(--shadow-md);
758
+ font-family: var(--cw-font-sans);
759
+ font-size: 12px;
760
+ line-height: 1.45;
761
+ color: var(--fg-1);
762
+ pointer-events: none;
763
+ z-index: 11;
764
+ }
765
+ .wd-scrubber .wd-scrub-peek [data-tone='muted'] { color: var(--fg-3); }
766
+ .wd-scrubber .wd-scrub-peek [data-tone='danger'] { color: var(--danger); }
767
+ .wd-scrubber .wd-scrub-ex {
768
+ display: -webkit-box;
769
+ -webkit-line-clamp: 4;
770
+ -webkit-box-orient: vertical;
771
+ overflow: hidden;
772
+ overflow-wrap: anywhere;
773
+ }