refactor(ui): isolate session components (#33670)

This commit is contained in:
Victor Navarro 2026-06-24 16:56:09 +02:00 committed by GitHub
commit 04c1730c90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
126 changed files with 294 additions and 142 deletions

View file

@ -1,163 +0,0 @@
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: geometricPrecision;
}
[data-component="basic-tool-v2"] {
--bt-title: var(--v2-text-text-base);
--bt-sep: var(--v2-text-text-muted);
--bt-subtitle: var(--v2-text-text-muted);
--bt-args: var(--v2-text-text-muted);
--bt-chevron: var(--v2-text-text-faint);
--bt-content: var(--v2-text-text-muted);
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
padding: 4px 0;
gap: 8px;
min-width: 0;
width: 100%;
font-variant-numeric: tabular-nums;
[data-slot="basic-tool-v2-trigger"] {
box-sizing: border-box;
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
width: 100%;
min-width: 0;
min-height: 20px;
padding: 0;
margin: 0;
text-align: left;
color: inherit;
outline: none;
}
[data-slot="basic-tool-v2-trigger"]:focus-visible {
outline: 2px solid var(--v2-border-border-focus);
outline-offset: 2px;
border-radius: 2px;
}
[data-slot="basic-tool-v2-labels"] {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 6px;
min-width: 0;
max-width: 100%;
}
[data-slot="basic-tool-v2-title"] {
display: flex;
align-items: center;
flex-shrink: 0;
font-size: 13px;
font-weight: 440;
line-height: 1;
letter-spacing: -0.04px;
color: var(--bt-title);
font-variation-settings: "slnt" 0;
user-select: none;
}
[data-slot="basic-tool-v2-sep"] {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 11px;
font-weight: 530;
line-height: 12px;
letter-spacing: 0.05px;
color: var(--bt-sep);
font-variation-settings: "slnt" 0;
user-select: none;
}
[data-slot="basic-tool-v2-subtitle"] {
display: flex;
align-items: center;
min-width: 0;
flex: 0 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
font-weight: 440;
line-height: 1;
letter-spacing: -0.04px;
color: var(--bt-subtitle);
font-variation-settings: "slnt" 0;
user-select: none;
}
[data-slot="basic-tool-v2-arg"] {
display: flex;
align-items: center;
flex-shrink: 0;
font-size: 13px;
font-weight: 440;
line-height: 1;
letter-spacing: -0.04px;
color: var(--bt-args);
font-variation-settings: "slnt" 0;
user-select: none;
}
[data-slot="basic-tool-v2-diff"] {
flex-shrink: 0;
}
[data-slot="basic-tool-v2-diff"] [data-component="diff-changes"] {
gap: 4px;
}
[data-slot="basic-tool-v2-chevron-wrap"] {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
flex-shrink: 0;
width: 20px;
height: 20px;
padding: 2px;
border-radius: 4px;
color: var(--bt-chevron);
user-select: none;
}
[data-slot="basic-tool-v2-chevron"] {
display: block;
flex-shrink: 0;
width: 16px;
height: 16px;
transition: transform 0.15s ease-out;
}
&[data-expanded] [data-slot="basic-tool-v2-chevron"] {
transform: rotate(90deg);
}
[data-slot="basic-tool-v2-content"] {
box-sizing: border-box;
min-width: 0;
width: 100%;
overflow: hidden;
}
[data-slot="basic-tool-v2-content-inner"] {
font-size: 15px;
font-weight: 440;
line-height: 24px;
letter-spacing: 0;
color: var(--bt-content);
font-variation-settings: "slnt" 0;
}
}

View file

@ -1,137 +0,0 @@
import { createSignal } from "solid-js"
import { BasicToolV2 } from "./basic-tool-v2"
const docs = `### Overview
Compact collapsible tool row showing title, subtitle, args, and diff changes, with an expand/collapse chevron.
### API
- \`BasicToolV2\` wraps Kobalte \`Collapsible\`. Pass \`open\`, \`defaultOpen\`, and \`onOpenChange\` for controlled/uncontrolled disclosure.
- \`trigger\` accepts either a \`BasicToolV2TriggerTitle\` object (title, subtitle, args, changes) or arbitrary JSX.
- When \`status\` is \`"pending"\` or \`"running"\`, subtitle/args/chevron hide and the title shows a shimmer animation.
- Pass \`children\` for expandable detail content.
### Theming
- Uses \`data-component="basic-tool-v2"\` and slot attributes; colors via \`--bt-*\` CSS variables.
`
export default {
title: "UI V2/BasicTool",
id: "components-basic-tool-v2",
component: BasicToolV2,
tags: ["autodocs"],
parameters: {
frameBackground: "#fff",
layout: "padded",
docs: {
description: {
component: docs,
},
},
},
}
export const Default = {
render: () => (
<BasicToolV2
trigger={{
title: "Read",
subtitle: "src/index.ts",
args: ["lines=1-50"],
changes: { additions: 12, deletions: 3 },
}}
defaultOpen={false}
>
File content appears here.
</BasicToolV2>
),
}
export const Expanded = {
render: () => (
<BasicToolV2
trigger={{
title: "Read",
subtitle: "src/index.ts",
args: ["lines=1-50"],
changes: { additions: 12, deletions: 3 },
}}
defaultOpen={true}
>
File content appears here.
</BasicToolV2>
),
}
export const Pending = {
render: () => (
<BasicToolV2
trigger={{
title: "Read",
subtitle: "src/index.ts",
args: ["lines=1-50"],
changes: { additions: 12, deletions: 3 },
}}
status="pending"
/>
),
}
export const NoChildren = {
render: () => (
<BasicToolV2
trigger={{
title: "Grep",
subtitle: "pattern=TODO",
args: ["recursive=true"],
}}
/>
),
}
export const CustomTrigger = {
render: () => (
<BasicToolV2
trigger={
<span style={{ color: "#161616", "font-size": "13px", "font-weight": "440" }}>Custom trigger content</span>
}
>
Expandable detail for custom trigger.
</BasicToolV2>
),
}
export const Controlled = {
render: () => {
const [open, setOpen] = createSignal(false)
return (
<div style={{ display: "flex", "flex-direction": "column", gap: "16px", "max-width": "420px" }}>
<button
type="button"
onClick={() => setOpen((o) => !o)}
style={{
padding: "4px 10px",
"font-size": "12px",
"border-radius": "6px",
border: "1px solid rgba(0,0,0,0.15)",
background: "#fff",
color: "#161616",
cursor: "pointer",
}}
>
Toggle from outside: {open() ? "Open" : "Closed"}
</button>
<BasicToolV2
trigger={{
title: "Write",
subtitle: "src/utils.ts",
changes: { additions: 8, deletions: 2 },
}}
open={open()}
onOpenChange={setOpen}
>
Controlled content.
</BasicToolV2>
</div>
)
},
}

View file

@ -1,139 +0,0 @@
import { Collapsible } from "@kobalte/core/collapsible"
import { type ComponentProps, type JSX, For, Show, createMemo, splitProps } from "solid-js"
import { DiffChanges } from "./diff-changes-v2"
import { TextShimmerV2 } from "./text-shimmer-v2"
import "./basic-tool-v2.css"
function ChevronIcon() {
return (
<svg
data-slot="basic-tool-v2-chevron"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M6.75194 10.6243C6.41861 10.8187 6 10.5783 6 10.1924V5.80837C6 5.42247 6.41861 5.18204 6.75194 5.37648L10.5096 7.56846C10.8404 7.7614 10.8404 8.2393 10.5096 8.43224L6.75194 10.6243Z"
fill="currentColor"
/>
</svg>
)
}
export interface BasicToolV2TriggerTitle {
title: string
subtitle?: string
args?: string[]
changes?: { additions: number; deletions: number } | { additions: number; deletions: number }[]
action?: JSX.Element
}
const isTriggerTitle = (val: unknown): val is BasicToolV2TriggerTitle =>
typeof val === "object" && val !== null && "title" in val && (typeof Node === "undefined" || !(val instanceof Node))
export interface BasicToolV2Props extends Omit<ComponentProps<"div">, "children" | "title"> {
trigger: BasicToolV2TriggerTitle | JSX.Element
children?: JSX.Element
status?: string
open?: boolean
defaultOpen?: boolean
onOpenChange?: (open: boolean) => void
onSubtitleClick?: () => void
}
export function BasicToolV2(props: BasicToolV2Props) {
const [local, rest] = splitProps(props, [
"trigger",
"children",
"status",
"open",
"defaultOpen",
"onOpenChange",
"onSubtitleClick",
"class",
"classList",
])
const pending = createMemo(() => local.status === "pending" || local.status === "running")
const hasChildren = createMemo(() => {
const c = local.children
if (c == null) return false
return true
})
const canExpand = createMemo(() => hasChildren() && !pending())
const handleOpenChange = (value: boolean) => {
if (pending()) return
local.onOpenChange?.(value)
}
return (
<Collapsible
{...rest}
data-component="basic-tool-v2"
open={local.open}
defaultOpen={local.defaultOpen}
onOpenChange={handleOpenChange}
disabled={!canExpand()}
classList={{
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
<Collapsible.Trigger as="div" role="button" data-slot="basic-tool-v2-trigger">
<div data-slot="basic-tool-v2-labels">
<Show when={isTriggerTitle(local.trigger) && local.trigger} fallback={local.trigger as JSX.Element}>
{(title) => (
<>
<span data-slot="basic-tool-v2-title">
<TextShimmerV2 text={title().title} active={pending()} />
</span>
<Show when={!pending() && title().subtitle}>
<span data-slot="basic-tool-v2-sep" aria-hidden="true">
·
</span>
<span
data-slot="basic-tool-v2-subtitle"
style={local.onSubtitleClick ? { cursor: "pointer" } : undefined}
onClick={(e) => {
if (local.onSubtitleClick) {
e.stopPropagation()
local.onSubtitleClick()
}
}}
>
{title().subtitle}
</span>
</Show>
<Show when={!pending() && title().args?.length}>
<For each={title().args}>{(arg) => <span data-slot="basic-tool-v2-arg">{arg}</span>}</For>
</Show>
<Show when={!pending() && title().changes}>
<span data-slot="basic-tool-v2-diff">
<DiffChanges changes={title().changes!} />
</span>
</Show>
<Show when={!pending() && title().action}>{(action) => action()}</Show>
</>
)}
</Show>
<Show when={canExpand()}>
<span data-slot="basic-tool-v2-chevron-wrap">
<ChevronIcon />
</span>
</Show>
</div>
</Collapsible.Trigger>
<Show when={canExpand()}>
<Collapsible.Content data-slot="basic-tool-v2-content">
<div data-slot="basic-tool-v2-content-inner">{local.children}</div>
</Collapsible.Content>
</Show>
</Collapsible>
)
}

View file

@ -1,875 +0,0 @@
[data-component="session-progress-indicator-v2"] {
--_duration: 1200ms;
display: block;
flex-shrink: 0;
color: var(--v2-icon-icon-muted, #808080);
}
[data-component="session-progress-indicator-v2"] [data-dot] {
fill: currentColor;
opacity: 0.2;
animation-duration: var(--_duration);
animation-timing-function: ease-out;
animation-iteration-count: infinite;
animation-fill-mode: both;
}
@keyframes session-progress-indicator-v2-dot-0 {
0% {
opacity: 0.2;
}
12.5% {
opacity: 0.2;
}
25% {
opacity: 0.2;
}
37.5% {
opacity: 1;
}
50% {
opacity: 0.5;
}
62.5% {
opacity: 0.2;
}
75% {
opacity: 0.2;
}
87.5% {
opacity: 0.2;
}
100% {
opacity: 0.2;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="0"] {
animation-name: session-progress-indicator-v2-dot-0;
}
@keyframes session-progress-indicator-v2-dot-5 {
0% {
opacity: 0.2;
}
12.5% {
opacity: 0.2;
}
25% {
opacity: 0.2;
}
37.5% {
opacity: 0.75;
}
50% {
opacity: 0.2;
}
62.5% {
opacity: 0.2;
}
75% {
opacity: 0.2;
}
87.5% {
opacity: 0.2;
}
100% {
opacity: 0.2;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="5"] {
animation-name: session-progress-indicator-v2-dot-5;
}
@keyframes session-progress-indicator-v2-dot-10 {
0% {
opacity: 0.2;
}
12.5% {
opacity: 0.2;
}
25% {
opacity: 1;
}
37.5% {
opacity: 0.5;
}
50% {
opacity: 0.2;
}
62.5% {
opacity: 0.2;
}
75% {
opacity: 0.2;
}
87.5% {
opacity: 0.2;
}
100% {
opacity: 0.2;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="10"] {
animation-name: session-progress-indicator-v2-dot-10;
}
@keyframes session-progress-indicator-v2-dot-15 {
0% {
opacity: 0.2;
}
12.5% {
opacity: 0.2;
}
25% {
opacity: 0.75;
}
37.5% {
opacity: 0.2;
}
50% {
opacity: 0.2;
}
62.5% {
opacity: 0.2;
}
75% {
opacity: 0.2;
}
87.5% {
opacity: 0.2;
}
100% {
opacity: 0.2;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="15"] {
animation-name: session-progress-indicator-v2-dot-15;
}
@keyframes session-progress-indicator-v2-dot-20 {
0% {
opacity: 0.2;
}
12.5% {
opacity: 1;
}
25% {
opacity: 0.5;
}
37.5% {
opacity: 0.2;
}
50% {
opacity: 0.2;
}
62.5% {
opacity: 0.2;
}
75% {
opacity: 0.2;
}
87.5% {
opacity: 0.2;
}
100% {
opacity: 0.2;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="20"] {
animation-name: session-progress-indicator-v2-dot-20;
}
@keyframes session-progress-indicator-v2-dot-1 {
0% {
opacity: 0.2;
}
12.5% {
opacity: 0.2;
}
25% {
opacity: 0.2;
}
37.5% {
opacity: 0.2;
}
50% {
opacity: 0.75;
}
62.5% {
opacity: 0.2;
}
75% {
opacity: 0.2;
}
87.5% {
opacity: 0.2;
}
100% {
opacity: 0.2;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="1"] {
animation-name: session-progress-indicator-v2-dot-1;
}
@keyframes session-progress-indicator-v2-dot-6 {
0% {
opacity: 0.2;
}
12.5% {
opacity: 0.2;
}
25% {
opacity: 0.2;
}
37.5% {
opacity: 1;
}
50% {
opacity: 0.5;
}
62.5% {
opacity: 0.2;
}
75% {
opacity: 0.2;
}
87.5% {
opacity: 0.2;
}
100% {
opacity: 0.2;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="6"] {
animation-name: session-progress-indicator-v2-dot-6;
}
@keyframes session-progress-indicator-v2-dot-11 {
0% {
opacity: 0.2;
}
12.5% {
opacity: 0.2;
}
25% {
opacity: 1;
}
37.5% {
opacity: 0.75;
}
50% {
opacity: 0.2;
}
62.5% {
opacity: 0.2;
}
75% {
opacity: 0.2;
}
87.5% {
opacity: 0.2;
}
100% {
opacity: 0.2;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="11"] {
animation-name: session-progress-indicator-v2-dot-11;
}
@keyframes session-progress-indicator-v2-dot-16 {
0% {
opacity: 0.2;
}
12.5% {
opacity: 1;
}
25% {
opacity: 0.5;
}
37.5% {
opacity: 0.2;
}
50% {
opacity: 0.2;
}
62.5% {
opacity: 0.2;
}
75% {
opacity: 0.2;
}
87.5% {
opacity: 0.2;
}
100% {
opacity: 0.2;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="16"] {
animation-name: session-progress-indicator-v2-dot-16;
}
@keyframes session-progress-indicator-v2-dot-21 {
0% {
opacity: 0.2;
}
12.5% {
opacity: 0.75;
}
25% {
opacity: 0.2;
}
37.5% {
opacity: 0.2;
}
50% {
opacity: 0.2;
}
62.5% {
opacity: 0.2;
}
75% {
opacity: 0.2;
}
87.5% {
opacity: 0.2;
}
100% {
opacity: 0.2;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="21"] {
animation-name: session-progress-indicator-v2-dot-21;
}
@keyframes session-progress-indicator-v2-dot-2 {
0% {
opacity: 0.2;
}
12.5% {
opacity: 0.2;
}
25% {
opacity: 0.2;
}
37.5% {
opacity: 0.2;
}
50% {
opacity: 1;
}
62.5% {
opacity: 0.5;
}
75% {
opacity: 0.2;
}
87.5% {
opacity: 0.2;
}
100% {
opacity: 0.2;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="2"] {
animation-name: session-progress-indicator-v2-dot-2;
}
@keyframes session-progress-indicator-v2-dot-7 {
0% {
opacity: 0.2;
}
12.5% {
opacity: 0.2;
}
25% {
opacity: 0.2;
}
37.5% {
opacity: 0.2;
}
50% {
opacity: 1;
}
62.5% {
opacity: 0.75;
}
75% {
opacity: 0.2;
}
87.5% {
opacity: 0.2;
}
100% {
opacity: 0.2;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="7"] {
animation-name: session-progress-indicator-v2-dot-7;
}
@keyframes session-progress-indicator-v2-dot-12 {
0% {
opacity: 1;
}
12.5% {
opacity: 1;
}
25% {
opacity: 1;
}
37.5% {
opacity: 1;
}
50% {
opacity: 1;
}
62.5% {
opacity: 1;
}
75% {
opacity: 1;
}
87.5% {
opacity: 1;
}
100% {
opacity: 1;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="12"] {
animation-name: session-progress-indicator-v2-dot-12;
}
@keyframes session-progress-indicator-v2-dot-17 {
0% {
opacity: 1;
}
12.5% {
opacity: 0.75;
}
25% {
opacity: 0.2;
}
37.5% {
opacity: 0.2;
}
50% {
opacity: 0.2;
}
62.5% {
opacity: 0.2;
}
75% {
opacity: 0.2;
}
87.5% {
opacity: 0.2;
}
100% {
opacity: 1;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="17"] {
animation-name: session-progress-indicator-v2-dot-17;
}
@keyframes session-progress-indicator-v2-dot-22 {
0% {
opacity: 1;
}
12.5% {
opacity: 0.5;
}
25% {
opacity: 0.2;
}
37.5% {
opacity: 0.2;
}
50% {
opacity: 0.2;
}
62.5% {
opacity: 0.2;
}
75% {
opacity: 0.2;
}
87.5% {
opacity: 0.2;
}
100% {
opacity: 1;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="22"] {
animation-name: session-progress-indicator-v2-dot-22;
}
@keyframes session-progress-indicator-v2-dot-3 {
0% {
opacity: 0.2;
}
12.5% {
opacity: 0.2;
}
25% {
opacity: 0.2;
}
37.5% {
opacity: 0.2;
}
50% {
opacity: 0.2;
}
62.5% {
opacity: 0.75;
}
75% {
opacity: 0.2;
}
87.5% {
opacity: 0.2;
}
100% {
opacity: 0.2;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="3"] {
animation-name: session-progress-indicator-v2-dot-3;
}
@keyframes session-progress-indicator-v2-dot-8 {
0% {
opacity: 0.2;
}
12.5% {
opacity: 0.2;
}
25% {
opacity: 0.2;
}
37.5% {
opacity: 0.2;
}
50% {
opacity: 0.2;
}
62.5% {
opacity: 1;
}
75% {
opacity: 0.75;
}
87.5% {
opacity: 0.2;
}
100% {
opacity: 0.2;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="8"] {
animation-name: session-progress-indicator-v2-dot-8;
}
@keyframes session-progress-indicator-v2-dot-13 {
0% {
opacity: 0.2;
}
12.5% {
opacity: 0.2;
}
25% {
opacity: 0.2;
}
37.5% {
opacity: 0.2;
}
50% {
opacity: 0.2;
}
62.5% {
opacity: 0.2;
}
75% {
opacity: 1;
}
87.5% {
opacity: 1;
}
100% {
opacity: 0.2;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="13"] {
animation-name: session-progress-indicator-v2-dot-13;
}
@keyframes session-progress-indicator-v2-dot-18 {
0% {
opacity: 0.5;
}
12.5% {
opacity: 0.2;
}
25% {
opacity: 0.2;
}
37.5% {
opacity: 0.2;
}
50% {
opacity: 0.2;
}
62.5% {
opacity: 0.2;
}
75% {
opacity: 0.2;
}
87.5% {
opacity: 0.75;
}
100% {
opacity: 0.5;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="18"] {
animation-name: session-progress-indicator-v2-dot-18;
}
@keyframes session-progress-indicator-v2-dot-23 {
0% {
opacity: 0.75;
}
12.5% {
opacity: 0.2;
}
25% {
opacity: 0.2;
}
37.5% {
opacity: 0.2;
}
50% {
opacity: 0.2;
}
62.5% {
opacity: 0.2;
}
75% {
opacity: 0.2;
}
87.5% {
opacity: 0.2;
}
100% {
opacity: 0.75;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="23"] {
animation-name: session-progress-indicator-v2-dot-23;
}
@keyframes session-progress-indicator-v2-dot-4 {
0% {
opacity: 0.2;
}
12.5% {
opacity: 0.2;
}
25% {
opacity: 0.2;
}
37.5% {
opacity: 0.2;
}
50% {
opacity: 0.2;
}
62.5% {
opacity: 1;
}
75% {
opacity: 0.5;
}
87.5% {
opacity: 0.2;
}
100% {
opacity: 0.2;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="4"] {
animation-name: session-progress-indicator-v2-dot-4;
}
@keyframes session-progress-indicator-v2-dot-9 {
0% {
opacity: 0.2;
}
12.5% {
opacity: 0.2;
}
25% {
opacity: 0.2;
}
37.5% {
opacity: 0.2;
}
50% {
opacity: 0.2;
}
62.5% {
opacity: 0.2;
}
75% {
opacity: 0.75;
}
87.5% {
opacity: 0.2;
}
100% {
opacity: 0.2;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="9"] {
animation-name: session-progress-indicator-v2-dot-9;
}
@keyframes session-progress-indicator-v2-dot-14 {
0% {
opacity: 0.2;
}
12.5% {
opacity: 0.2;
}
25% {
opacity: 0.2;
}
37.5% {
opacity: 0.2;
}
50% {
opacity: 0.2;
}
62.5% {
opacity: 0.2;
}
75% {
opacity: 1;
}
87.5% {
opacity: 1;
}
100% {
opacity: 0.2;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="14"] {
animation-name: session-progress-indicator-v2-dot-14;
}
@keyframes session-progress-indicator-v2-dot-19 {
0% {
opacity: 0.2;
}
12.5% {
opacity: 0.2;
}
25% {
opacity: 0.2;
}
37.5% {
opacity: 0.2;
}
50% {
opacity: 0.2;
}
62.5% {
opacity: 0.2;
}
75% {
opacity: 0.2;
}
87.5% {
opacity: 0.75;
}
100% {
opacity: 0.2;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="19"] {
animation-name: session-progress-indicator-v2-dot-19;
}
@keyframes session-progress-indicator-v2-dot-24 {
0% {
opacity: 0.5;
}
12.5% {
opacity: 0.2;
}
25% {
opacity: 0.2;
}
37.5% {
opacity: 0.2;
}
50% {
opacity: 0.2;
}
62.5% {
opacity: 0.2;
}
75% {
opacity: 0.2;
}
87.5% {
opacity: 0.5;
}
100% {
opacity: 0.5;
}
}
[data-component="session-progress-indicator-v2"] [data-dot="24"] {
animation-name: session-progress-indicator-v2-dot-24;
}
@media (prefers-reduced-motion: reduce) {
[data-component="session-progress-indicator-v2"] [data-dot] {
animation: none;
}
[data-component="session-progress-indicator-v2"] [data-dot="12"] {
opacity: 1;
}
}

View file

@ -1,66 +0,0 @@
// @ts-nocheck
import { SessionProgressIndicatorV2 } from "./session-progress-indicator-v2"
const docs = `### Overview
Animated 5×5 dot grid loader for in-progress session state.
Derived from Figma \`_sessionProgressIndicator\` with 8-frame rotation.
### API
- Accepts standard SVG props.
### Behavior
- CSS keyframes drive per-dot opacity across 8 frames (1.2s loop).
- Center dot stays at full opacity throughout the cycle.
### Accessibility
- Sets \`aria-hidden="true"\` by default.
### Theming
- Uses \`currentColor\` via \`--v2-icon-icon-muted\`.
`
export default {
title: "UI V2/SessionProgressIndicator",
id: "components-session-progress-indicator-v2",
component: SessionProgressIndicatorV2,
tags: ["autodocs"],
parameters: {
docs: {
description: {
component: docs,
},
},
},
}
export const Basic = {
render: () => <SessionProgressIndicatorV2 />,
}
export const Sizes = {
render: () => (
<div style={{ display: "flex", gap: "16px", "align-items": "center" }}>
<SessionProgressIndicatorV2 width={12} height={12} />
<SessionProgressIndicatorV2 />
<SessionProgressIndicatorV2 width={24} height={24} />
</div>
),
}
export const OnDark = {
render: () => (
<div
style={{
display: "flex",
gap: "16px",
"align-items": "center",
padding: "16px",
"background-color": "#171717",
color: "#c7c7c7",
}}
>
<SessionProgressIndicatorV2 />
</div>
),
}

View file

@ -1,32 +0,0 @@
import { For, splitProps, type ComponentProps } from "solid-js"
import "./session-progress-indicator-v2.css"
const grid = 5
const dot = 2
const gap = 1
const origin = 1.5
const dots = Array.from({ length: grid * grid }, (_, index) => ({
index,
x: origin + (index % grid) * (dot + gap),
y: origin + Math.floor(index / grid) * (dot + gap),
}))
export function SessionProgressIndicatorV2(props: ComponentProps<"svg">) {
const [local, rest] = splitProps(props, ["class", "classList", "width", "height"])
return (
<svg
{...rest}
class={local.class}
classList={local.classList}
width={local.width ?? 16}
height={local.height ?? 16}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
data-component="session-progress-indicator-v2"
aria-hidden={rest["aria-hidden"] ?? "true"}
>
<For each={dots}>{(cell) => <rect data-dot={cell.index} x={cell.x} y={cell.y} width={dot} height={dot} />}</For>
</svg>
)
}

View file

@ -1,200 +0,0 @@
[data-component="tool-error-card"] {
--tec-border: var(--v2-state-fg-danger);
--tec-title: var(--v2-text-text-base);
--tec-sep: var(--v2-text-text-muted);
--tec-subtitle: var(--v2-text-text-muted);
--tec-suffix: var(--v2-text-text-faint);
--tec-chevron: var(--v2-text-text-faint);
--tec-icon: var(--v2-icon-icon-base);
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: stretch;
min-width: 0;
width: 100%;
padding: 0 0 0 10px;
gap: 8px;
border-left: 2px solid var(--tec-border);
font-variant-numeric: tabular-nums;
[data-slot="tool-error-card-trigger"] {
box-sizing: border-box;
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
width: 100%;
min-width: 0;
min-height: 24px;
padding: 2px 0;
margin: 0;
text-align: left;
color: inherit;
outline: none;
border-radius: 2px;
}
[data-slot="tool-error-card-trigger"]:focus-visible {
outline: 2px solid var(--v2-border-border-focus);
outline-offset: 2px;
}
[data-slot="tool-error-card-icon-wrap"] {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 16px;
height: 20px;
box-sizing: border-box;
color: var(--tec-icon);
}
[data-slot="tool-error-card-ban"] {
display: block;
flex-shrink: 0;
}
[data-slot="tool-error-card-loader"] {
display: block;
flex-shrink: 0;
width: 16px;
height: 16px;
animation: tool-error-card-spin 0.65s linear infinite;
transform-origin: 50% 50%;
}
[data-slot="tool-error-card-main"] {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
min-width: 0;
min-height: 20px;
flex: 1 1 auto;
}
[data-slot="tool-error-card-labels"] {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 6px;
max-width: 100%;
min-width: 0;
overflow: hidden;
}
[data-slot="tool-error-card-title"] {
display: flex;
align-items: center;
flex-shrink: 0;
max-width: 100%;
height: 20px;
font-size: 13px;
font-weight: 530;
line-height: 20px;
letter-spacing: -0.04px;
color: var(--tec-title);
user-select: none;
}
[data-slot="tool-error-card-sep"] {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
height: 20px;
font-size: 11px;
font-weight: 530;
line-height: 20px;
letter-spacing: 0.05px;
color: var(--tec-sep);
user-select: none;
}
[data-slot="tool-error-card-subtitle"] {
display: flex;
align-items: center;
min-width: 0;
height: 20px;
flex: 0 1 auto;
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
font-weight: 440;
line-height: 20px;
letter-spacing: -0.04px;
color: var(--tec-subtitle);
user-select: none;
}
a[data-slot="tool-error-card-subtitle"] {
display: flex;
align-items: center;
min-width: 0;
height: 20px;
flex: 0 1 auto;
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
font-weight: 440;
line-height: 20px;
letter-spacing: -0.04px;
color: inherit;
text-decoration: underline;
text-underline-offset: 2px;
user-select: none;
}
[data-slot="tool-error-card-chevron-wrap"] {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 14px;
height: 20px;
color: var(--tec-chevron);
user-select: none;
}
[data-slot="tool-error-card-chevron"] {
display: block;
flex-shrink: 0;
width: 14px;
height: 14px;
color: var(--tec-chevron);
transition: transform 0.15s ease-out;
}
&[data-expanded] [data-slot="tool-error-card-chevron"] {
transform: rotate(90deg);
}
[data-slot="tool-error-card-content"] {
box-sizing: border-box;
min-width: 0;
}
[data-slot="tool-error-card-suffix"] {
padding: 0 0 0 24px;
font-size: 13px;
font-weight: 440;
line-height: 1;
letter-spacing: -0.04px;
color: var(--tec-suffix);
white-space: pre-wrap;
word-break: break-word;
}
}
@keyframes tool-error-card-spin {
to {
transform: rotate(360deg);
}
}

View file

@ -1,91 +0,0 @@
import { createSignal } from "solid-js"
import { ButtonV2 } from "./button-v2"
import { ToolErrorCardV2, type ToolErrorCardV2Props } from "./tool-error-card-v2"
const docs = `### Overview
Compact tool error row with optional expandable detail, aligned to the OpenCode design system spec.
### API
- \`ToolErrorCardV2\` wraps Kobalte \`Collapsible\` directly. Pass \`open\`, \`defaultOpen\`, and \`onOpenChange\` like any disclosure (controlled when \`open\` is defined).
- Without a non-empty \`suffix\`, the card is not expandable (\`disabled\` on the collapsible root).
### Theming
- Uses \`data-component="tool-error-card"\` and slot attributes; colors are CSS variables on the root (\`--tec-*\`).
`
export default {
title: "UI V2/ToolErrorCard",
id: "components-tool-error-card-v2",
component: ToolErrorCardV2,
tags: ["autodocs"],
parameters: {
frameBackground: "#fff",
layout: "padded",
docs: {
description: {
component: docs,
},
},
},
}
export const Default = {
args: {
title: "Read",
subtitle: "Permission denied",
suffix: "The tool could not access the requested path.",
defaultOpen: false,
} satisfies ToolErrorCardV2Props,
render: (args: ToolErrorCardV2Props) => <ToolErrorCardV2 {...args} />,
}
export const Loading = {
args: {
title: "Read",
subtitle: "Working",
suffix: "Details appear when the tool finishes.",
loading: true,
defaultOpen: false,
} satisfies ToolErrorCardV2Props,
render: (args: ToolErrorCardV2Props) => <ToolErrorCardV2 {...args} />,
}
export const SubtitleLink = {
args: {
title: "Task",
subtitle: "View logs",
subtitleHref: "https://example.com",
suffix: "Subagent exited with code 1.",
defaultOpen: false,
} satisfies ToolErrorCardV2Props,
render: (args: ToolErrorCardV2Props) => <ToolErrorCardV2 {...args} />,
}
export const NoSuffixDisabled = {
args: {
title: "List",
subtitle: "No detail",
defaultOpen: false,
} satisfies ToolErrorCardV2Props,
render: (args: ToolErrorCardV2Props) => <ToolErrorCardV2 {...args} />,
}
export const Controlled = {
render: () => {
const [open, setOpen] = createSignal(false)
return (
<div style={{ display: "flex", "flex-direction": "column", gap: "24px", "max-width": "420px" }}>
<ButtonV2 type="button" classList={{ "w-fit": true }} onClick={() => setOpen((o) => !o)}>
Toggle from outside: {open() ? "Open" : "Closed"}
</ButtonV2>
<ToolErrorCardV2
title="Grep"
subtitle="Timeout"
suffix="Operation exceeded 30s."
open={open()}
onOpenChange={setOpen}
/>
</div>
)
},
}

View file

@ -1,166 +0,0 @@
import { Collapsible } from "@kobalte/core/collapsible"
import { type ComponentProps, type JSX, Show, createMemo, splitProps } from "solid-js"
import "./tool-error-card-v2.css"
function BanIcon() {
return (
<svg
data-slot="tool-error-card-ban"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M3.44283 12.5575L12.5495 3.45081M14.4446 8.00011C14.4446 11.5593 11.5593 14.4446 8.00011 14.4446C4.44094 14.4446 1.55566 11.5593 1.55566 8.00011C1.55566 4.44094 4.44094 1.55566 8.00011 1.55566C11.5593 1.55566 14.4446 4.44094 14.4446 8.00011Z"
stroke="currentColor"
/>
</svg>
)
}
/** duo-progress-25: faint track ring + ~25% solid arc (Figma OpenCode DS) */
function LoaderIcon() {
const r = 5.9
return (
<svg
data-slot="tool-error-card-loader"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<g transform="translate(8 8)">
<circle
r={r}
fill="none"
stroke="var(--v2-icon-icon-base)"
stroke-width="1"
stroke-opacity="0.3"
transform="rotate(-90)"
/>
<circle
r={r}
fill="none"
stroke="var(--v2-icon-icon-base)"
stroke-width="1"
pathLength="100"
stroke-dasharray="25 75"
transform="rotate(-90)"
/>
</g>
</svg>
)
}
function ChevronIcon() {
return (
<svg
data-slot="tool-error-card-chevron"
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M5.90795 9.62425C5.61628 9.81865 5.25 9.57825 5.25 9.19235V4.80837C5.25 4.42247 5.61628 4.18204 5.90795 4.37648L9.1959 6.56846C9.48535 6.7614 9.48535 7.2393 9.1959 7.43224L5.90795 9.62425Z"
fill="currentColor"
/>
</svg>
)
}
export interface ToolErrorCardV2Props extends Omit<ComponentProps<"div">, "children" | "title"> {
title: JSX.Element | string
subtitle: JSX.Element | string
suffix?: JSX.Element | string
loading?: boolean
open?: boolean
defaultOpen?: boolean
onOpenChange?: (open: boolean) => void
/** When set, subtitle renders as a link (clicks do not toggle expand). */
subtitleHref?: string
}
export function ToolErrorCardV2(props: ToolErrorCardV2Props) {
const [local, rest] = splitProps(props, [
"title",
"subtitle",
"suffix",
"loading",
"open",
"defaultOpen",
"onOpenChange",
"subtitleHref",
"class",
"classList",
])
const hasSuffix = createMemo(() => {
const s = local.suffix
if (s == null) return false
if (typeof s === "string") return s.length > 0
return true
})
return (
<Collapsible
{...rest}
data-component="tool-error-card"
open={local.open}
defaultOpen={local.defaultOpen}
onOpenChange={local.onOpenChange}
disabled={!hasSuffix()}
aria-busy={local.loading ? true : undefined}
classList={{
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
<Collapsible.Trigger as="div" role="button" data-slot="tool-error-card-trigger">
<span data-slot="tool-error-card-icon-wrap">
<Show when={local.loading} fallback={<BanIcon />}>
<LoaderIcon />
</Show>
</span>
<div data-slot="tool-error-card-main">
<div data-slot="tool-error-card-labels">
<span data-slot="tool-error-card-title">{local.title}</span>
<span data-slot="tool-error-card-sep" aria-hidden="true">
·
</span>
<Show
when={local.subtitleHref}
fallback={<span data-slot="tool-error-card-subtitle">{local.subtitle}</span>}
>
<a
data-slot="tool-error-card-subtitle"
href={local.subtitleHref!}
onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
{local.subtitle}
</a>
</Show>
<Show when={hasSuffix()}>
<span data-slot="tool-error-card-chevron-wrap">
<ChevronIcon />
</span>
</Show>
</div>
</div>
</Collapsible.Trigger>
<Show when={hasSuffix()}>
<Collapsible.Content data-slot="tool-error-card-content">
<div data-slot="tool-error-card-suffix">{local.suffix}</div>
</Collapsible.Content>
</Show>
</Collapsible>
)
}