Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 87 additions & 22 deletions packages/ui/src/components/card.css
Original file line number Diff line number Diff line change
@@ -1,29 +1,94 @@
[data-component="card"] {
--card-pad-y: 10px;
--card-pad-r: 12px;
--card-pad-l: 10px;

width: 100%;
display: flex;
flex-direction: column;
background-color: var(--surface-inset-base);
border: 1px solid var(--border-weaker-base);
transition: background-color 0.15s ease;
position: relative;
background: transparent;
border: none;
border-radius: var(--radius-md);
padding: 6px 12px;
overflow: clip;

&[data-variant="error"] {
background-color: var(--surface-critical-weak);
border: 1px solid var(--border-critical-base);
color: rgba(218, 51, 25, 0.6);

/* text-12-regular */
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
font-style: normal;
font-weight: var(--font-weight-regular);
line-height: var(--line-height-large); /* 166.667% */
letter-spacing: var(--letter-spacing-normal);

&[data-component="icon"] {
color: var(--icon-critical-active);
}
padding: var(--card-pad-y) var(--card-pad-r) var(--card-pad-y) var(--card-pad-l);

/* text-14-regular */
font-family: var(--font-family-sans);
font-size: var(--font-size-base);
font-style: normal;
font-weight: var(--font-weight-regular);
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
color: var(--text-strong);

--card-gap: 8px;
--card-icon: 16px;
--card-indent: 0px;
--card-line-pad: 8px;

--card-accent: var(--icon-active);

&:has([data-slot="card-title"]) {
gap: 8px;
}

&:has([data-slot="card-title-icon"]) {
--card-indent: calc(var(--card-icon) + var(--card-gap));
}

&::before {
content: "";
position: absolute;
left: 0;
top: var(--card-line-pad);
bottom: var(--card-line-pad);
width: 2px;
border-radius: 2px;
background-color: var(--card-accent);
}

:where([data-card="title"], [data-slot="card-title"]) {
color: var(--text-strong);
font-weight: var(--font-weight-medium);
}

:where([data-slot="card-title"]) {
display: flex;
align-items: center;
gap: var(--card-gap);
}

:where([data-slot="card-title"]) [data-component="icon"] {
color: var(--card-accent);
}

:where([data-slot="card-title-icon"]) {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--card-icon);
height: var(--card-icon);
flex: 0 0 auto;
}

:where([data-slot="card-title-icon"][data-placeholder]) [data-component="icon"] {
color: var(--text-weak);
}

:where([data-slot="card-title-icon"])
[data-slot="icon-svg"]
:is(path, line, polyline, polygon, rect, circle, ellipse)[stroke] {
stroke-width: 1.5px !important;
}

:where([data-card="description"], [data-slot="card-description"]) {
color: var(--text-base);
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;
}

:where([data-card="actions"], [data-slot="card-actions"]) {
padding-left: var(--card-indent);
}
}
14 changes: 6 additions & 8 deletions packages/ui/src/components/card.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @ts-nocheck
import { Card } from "./card"
import { Card, CardActions, CardDescription, CardTitle } from "./card"
import { Button } from "./button"

const docs = `### Overview
Expand Down Expand Up @@ -49,15 +49,13 @@ export default {
render: (props: { variant?: "normal" | "error" | "warning" | "success" | "info" }) => {
return (
<Card variant={props.variant}>
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 500 }}>Card title</div>
<div style={{ color: "var(--text-weak)", fontSize: "13px" }}>Small supporting text.</div>
</div>
<Button size="small" variant="ghost">
<CardTitle variant={props.variant}>Card title</CardTitle>
<CardDescription>Small supporting text.</CardDescription>
<CardActions>
<Button size="small" variant="secondary">
Action
</Button>
</div>
</CardActions>
</Card>
)
},
Expand Down
107 changes: 104 additions & 3 deletions packages/ui/src/components/card.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,57 @@
import { type ComponentProps, splitProps } from "solid-js"
import { Icon, type IconProps } from "./icon"

type Variant = "normal" | "error" | "warning" | "success" | "info"

export interface CardProps extends ComponentProps<"div"> {
variant?: "normal" | "error" | "warning" | "success" | "info"
variant?: Variant
}

export interface CardTitleProps extends ComponentProps<"div"> {
variant?: Variant

/**
* Optional title icon.
*
* - `undefined`: picks a default icon based on `variant` (error/warning/success/info)
* - `false`/`null`: disables the icon
* - `Icon` name: forces a specific icon
*/
icon?: IconProps["name"] | false | null
}

function pick(variant: Variant) {
if (variant === "error") return "circle-ban-sign" as const
if (variant === "warning") return "warning" as const
if (variant === "success") return "circle-check" as const
if (variant === "info") return "help" as const
return
}

function mix(style: ComponentProps<"div">["style"], value?: string) {
if (!value) return style
if (!style) return { "--card-accent": value }
if (typeof style === "string") return `${style};--card-accent:${value};`
return { ...(style as Record<string, string | number>), "--card-accent": value }
}

export function Card(props: CardProps) {
const [split, rest] = splitProps(props, ["variant", "class", "classList"])
const [split, rest] = splitProps(props, ["variant", "style", "class", "classList"])
const variant = () => split.variant ?? "normal"
const accent = () => {
const v = variant()
if (v === "error") return "var(--icon-critical-base)"
if (v === "warning") return "var(--icon-warning-active)"
if (v === "success") return "var(--icon-success-active)"
if (v === "info") return "var(--icon-info-active)"
return
}
return (
<div
{...rest}
data-component="card"
data-variant={split.variant || "normal"}
data-variant={variant()}
style={mix(split.style, accent())}
classList={{
...(split.classList ?? {}),
[split.class ?? ""]: !!split.class,
Expand All @@ -20,3 +61,63 @@ export function Card(props: CardProps) {
</div>
)
}

export function CardTitle(props: CardTitleProps) {
const [split, rest] = splitProps(props, ["variant", "icon", "class", "classList", "children"])
const show = () => split.icon !== false && split.icon !== null
const name = () => {
if (split.icon === false || split.icon === null) return
if (typeof split.icon === "string") return split.icon
return pick(split.variant ?? "normal")
}
const placeholder = () => !name()
return (
<div
{...rest}
data-slot="card-title"
classList={{
...(split.classList ?? {}),
[split.class ?? ""]: !!split.class,
}}
>
{show() ? (
<span data-slot="card-title-icon" data-placeholder={placeholder() || undefined}>
<Icon name={name() ?? "dash"} size="small" />
</span>
) : null}
{split.children}
</div>
)
}

export function CardDescription(props: ComponentProps<"div">) {
const [split, rest] = splitProps(props, ["class", "classList", "children"])
return (
<div
{...rest}
data-slot="card-description"
classList={{
...(split.classList ?? {}),
[split.class ?? ""]: !!split.class,
}}
>
{split.children}
</div>
)
}

export function CardActions(props: ComponentProps<"div">) {
const [split, rest] = splitProps(props, ["class", "classList", "children"])
return (
<div
{...rest}
data-slot="card-actions"
classList={{
...(split.classList ?? {}),
[split.class ?? ""]: !!split.class,
}}
>
{split.children}
</div>
)
}
6 changes: 6 additions & 0 deletions packages/ui/src/components/markdown.css
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
ol {
margin-top: 0.5rem;
margin-bottom: 1rem;
margin-left: 0;
padding-left: 1.5rem;
list-style-position: outside;
}
Expand All @@ -70,6 +71,7 @@

ol {
list-style-type: decimal;
padding-left: 2.25rem;
}

li {
Expand Down Expand Up @@ -98,6 +100,10 @@
padding-left: 1rem; /* Minimal indent for nesting only */
}

li > ol {
padding-left: 1.75rem;
}

/* Blockquotes */
blockquote {
border-left: 2px solid var(--border-weak-base);
Expand Down
36 changes: 0 additions & 36 deletions packages/ui/src/components/message-part.css
Original file line number Diff line number Diff line change
Expand Up @@ -305,41 +305,6 @@
}
}

[data-component="tool-error"] {
display: flex;
align-items: start;
gap: 8px;

[data-slot="icon-svg"] {
color: var(--icon-critical-base);
margin-top: 4px;
}

[data-slot="message-part-tool-error-content"] {
display: flex;
align-items: start;
gap: 8px;
}

[data-slot="message-part-tool-error-title"] {
font-family: var(--font-family-sans);
font-size: var(--font-size-base);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
color: var(--text-on-critical-base);
white-space: nowrap;
}

[data-slot="message-part-tool-error-message"] {
color: var(--text-on-critical-weak);
max-height: 240px;
overflow-y: auto;
word-break: break-word;
}
}

[data-component="tool-output"] {
white-space: pre;
padding: 0;
Expand Down Expand Up @@ -713,7 +678,6 @@
[data-component="user-message"] [data-slot="user-message-text"],
[data-component="text-part"],
[data-component="reasoning-part"],
[data-component="tool-error"],
[data-component="tool-output"],
[data-component="bash-output"],
[data-component="edit-content"],
Expand Down
21 changes: 2 additions & 19 deletions packages/ui/src/components/message-part.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { Card } from "./card"
import { Collapsible } from "./collapsible"
import { FileIcon } from "./file-icon"
import { Icon } from "./icon"
import { ToolErrorCard } from "./tool-error-card"
import { Checkbox } from "./checkbox"
import { DiffChanges } from "./diff-changes"
import { Markdown } from "./markdown"
Expand Down Expand Up @@ -1189,25 +1190,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
</div>
)
}
const [title, ...rest] = cleaned.split(": ")
return (
<Card variant="error">
<div data-component="tool-error">
<Icon name="circle-ban-sign" size="small" />
<Switch>
<Match when={title && title.length < 30}>
<div data-slot="message-part-tool-error-content">
<div data-slot="message-part-tool-error-title">{title}</div>
<span data-slot="message-part-tool-error-message">{rest.join(": ")}</span>
</div>
</Match>
<Match when={true}>
<span data-slot="message-part-tool-error-message">{cleaned}</span>
</Match>
</Switch>
</div>
</Card>
)
return <ToolErrorCard tool={part().tool} error={error()} />
}}
</Match>
<Match when={true}>
Expand Down
Loading
Loading