Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
504c8bd
react-ui: refresh the AgentInterface shell
shubham-thesys Oct 8, 2026
c265b2a
Drop the react-headless thread cache from this PR
shubham-thesys Oct 8, 2026
5733096
react-ui: soften the AgentInterface shell
Oct 8, 2026
9662f0a
react-ui: fix the AgentInterface in dark mode
Oct 8, 2026
9548288
react-ui: default typography on variable Inter
Oct 8, 2026
2556582
react-ui: ease dark-mode Regular to 420
Oct 8, 2026
5657cb6
react-ui: light/dark toggle in the sidebar footer
Oct 8, 2026
bd91e28
react-ui: Regular weight back to 400
Oct 9, 2026
c5f2847
react-ui: set Inter's wght axis with font-variation-settings
Oct 9, 2026
95da35d
fix: format
AbhinRustagi Oct 12, 2026
d07b290
fix: reorganize files
AbhinRustagi Oct 12, 2026
ff7844c
fix: update
AbhinRustagi Oct 12, 2026
011acd3
fix: css
AbhinRustagi Oct 12, 2026
c19cbda
fix: conditional rename button
AbhinRustagi Oct 12, 2026
3af4727
react-ui: redesigned tool call timeline and thread polish (#1332)
shubham-thesys Oct 12, 2026
0b1ccdc
fix: clarify saved sidebar state, drop dead loader CSS and duplicate …
AbhinRustagi Oct 12, 2026
464dee0
Merge main into agent-interface-redesign
AbhinRustagi Oct 12, 2026
45d5f6b
templates: keep openui-cloud on main's version
AbhinRustagi Oct 12, 2026
48825de
react-ui: rework the genui component library and system prompt
Oct 12, 2026
8d4cedf
react-ui: DatePicker accepts ISO strings, timestamps and range shapes
Oct 12, 2026
74d4e0c
react-ui: steer the prompt to CompositeCardBlock only as a fallback
Oct 12, 2026
c97a933
react-ui: neutral focus rings on tabs, accordion and clickable cards
Oct 12, 2026
34eea99
react-ui: prefer visual choices over Forms in the prompt
Oct 12, 2026
a936bf5
react-ui: add FollowUpBlock and a Sources block to openuiLibrary
Oct 12, 2026
7d6f067
react-ui: plus icon on follow-up items
Oct 12, 2026
91ebdb0
react-ui: collapsible Related Queries and Sources
Oct 12, 2026
d815421
react-ui: tighter follow-up rows in Medium weight
Oct 12, 2026
7754909
Merge remote-tracking branch 'origin/main' into genui-components
Oct 13, 2026
2426b0d
lang-core: cast parsed roots through unknown in the triple-quote tests
Oct 13, 2026
4e4c8b0
docs: lists preview uses ListItem's leading slot
Oct 13, 2026
92015c9
assistant-ui: test fixtures use Text and Heading
Oct 13, 2026
a8450aa
lang-core: move triple-quote support to its own PR
AbhinRustagi Oct 13, 2026
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
5 changes: 5 additions & 0 deletions .changeset/genui-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@openuidev/react-ui": minor
---

Rework the generative UI component library and its system prompt. New components: Section, Grid, Shell, Bleed, Heading, Metric, Avatar, AvatarStack, Emoji, Shape, Link, LeadingText, EntityTitle, ImageTitle, EntityListItem, Checklist, Timeline, InsightCard, ValueCard, ImageCard, ValueLayout and FormRow, plus a Sparkline chart. `TextContent` becomes `Text` and `MarkDownRenderer` becomes `Markdown` (with `==highlight==` and favicon links). `ContextCardBlock` and `SnippetCardBlock` are removed; `IconText` becomes the archived `IconTitle`, and CardHeader, InlineHeader, Label, BoldText, TextCallout, MetricIndicator, ImageBlock, ImageText, OverviewCardBlock and VisualCardBlock move to `openuiArchivedComponents`, out of the model-facing libraries. Cards, Callout, Tag, Chips, OptionCards, Table, EditableTable (now `EditableCol`), form fields, Calendar, Modal, Carousel, ImageGallery and lists are restyled, and the theme adds a Bold `wght` axis token.
27 changes: 16 additions & 11 deletions docs/app/components/blocks/lists/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from "@components/components/preview";
import { ListBlock, ListItem } from "@openuidev/react-ui";
import { ChevronRight, FileText, Globe, LayoutPanelTop } from "lucide-react";
import { useMemo, useState } from "react";
import { useMemo, useState, type ReactNode } from "react";
import styles from "./page.module.css";

type ListVariant = "number" | "icon" | "image";
Expand All @@ -24,6 +24,14 @@ function ListsPreview({
interactionMode: ListInteractionMode;
}) {
const descriptionFor = (text: string) => (showDescription ? text : undefined);
// ListItem shows a number, or a leading icon or image with variant "plain".
const itemVariant = variant === "number" ? "number" : "plain";
const leadingFor = (icon: ReactNode, src: string, alt: string) =>
variant === "icon" ? (
icon
) : variant === "image" ? (
<img src={src} alt={alt} width={24} height={24} />
) : undefined;

const commonActionProps = useMemo(() => {
if (interactionMode === "static") {
Expand All @@ -38,33 +46,30 @@ function ListsPreview({
return (
<div className={styles.previewWrapper}>
<div className={styles.previewInner}>
<ListBlock variant={variant}>
<ListBlock variant={itemVariant}>
<ListItem
variant={variant}
variant={itemVariant}
index={0}
listHasSubtitle={showDescription}
icon={variant === "icon" ? <FileText size={16} /> : undefined}
image={variant === "image" ? { src: "/file.svg", alt: "File" } : undefined}
leading={leadingFor(<FileText size={16} />, "/file.svg", "File")}
title="Connect your data"
subtitle={descriptionFor("Link your database or upload a CSV to get started.")}
{...commonActionProps}
/>
<ListItem
variant={variant}
variant={itemVariant}
index={1}
listHasSubtitle={showDescription}
icon={variant === "icon" ? <Globe size={16} /> : undefined}
image={variant === "image" ? { src: "/globe.svg", alt: "Globe" } : undefined}
leading={leadingFor(<Globe size={16} />, "/globe.svg", "Globe")}
title="Choose a goal"
subtitle={descriptionFor("Summarize, classify, extract, or generate UI from prompts.")}
{...commonActionProps}
/>
<ListItem
variant={variant}
variant={itemVariant}
index={2}
listHasSubtitle={showDescription}
icon={variant === "icon" ? <LayoutPanelTop size={16} /> : undefined}
image={variant === "image" ? { src: "/window.svg", alt: "Window" } : undefined}
leading={leadingFor(<LayoutPanelTop size={16} />, "/window.svg", "Window")}
title="Review and refine"
subtitle={descriptionFor("Edit the result and rerun with tighter instructions.")}
{...commonActionProps}
Expand Down
4 changes: 2 additions & 2 deletions packages/assistant-ui/src/__tests__/renderers.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ vi.mock("@assistant-ui/react", () => ({
}));

const FORM = `root = Card([title, form])
title = TextContent("Contact Us", "large-heavy")
title = Text("Contact Us", "h3")
form = Form("contact", btns, [nameField, emailField])
nameField = FormControl("Name", Input("name", "Your name", "text", { required: true, minLength: 2 }))
emailField = FormControl("Email", Input("email", "you@example.com", "email", { required: true, email: true }))
Expand Down Expand Up @@ -259,7 +259,7 @@ describe("OpenUIPrompt", () => {
});

it("does not show the fallback for a warning when the program renders", async () => {
const args = { ui: 'card = Card([h])\nh = CardHeader("Entry", "B")' };
const args = { ui: 'card = Card([h])\nh = Heading("Entry", "B")' };
await renderPrompt(makeProps({ args, argsText: JSON.stringify(args) }));

expect(container.textContent).toContain("Entry");
Expand Down
55 changes: 35 additions & 20 deletions packages/react-ui/src/components/Accordion/accordion.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,26 @@
border: 1px solid transparent;
box-shadow: none;
padding: 0;
.openui-accordion-trigger {
box-shadow: none;
padding-left: 0;
padding-right: 0;
padding-top: cssUtils.$space-l;
padding-bottom: cssUtils.$space-l;
}
// Every row is the same height: equal padding above and below each
// trigger, a 1px rule between rows and none after the last. An open row's
// content ends with the same space, so both ends of the list match.
.openui-accordion-item {
&:first-child {
.openui-accordion-trigger {
padding-top: 0;
}
}
overflow: visible;
&:last-child {
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
border-bottom: 1px solid transparent;
border-bottom: none;
}
}
.openui-accordion-item .openui-accordion-trigger,
.openui-accordion-item:first-child .openui-accordion-trigger {
box-shadow: none;
padding: calc(cssUtils.$space-m + 2px) 0;
}
.openui-accordion-content {
&-wrapper {
padding: 0;
padding-bottom: cssUtils.$space-l;
padding-bottom: calc(cssUtils.$space-m + 2px);
gap: cssUtils.$space-m;
}
}
Expand Down Expand Up @@ -107,7 +104,8 @@
z-index: 1;
}
}
&-header {
// Scoped so host heading styles (h3 margins) never add space under a row.
& &-header {
display: flex;
margin-block-start: 0;
margin-block-end: 0;
Expand All @@ -117,7 +115,8 @@
&-trigger {
border: none;
box-sizing: border-box;
@include cssUtils.typography("body", "default");
// Section titles in Medium, same size as before.
@include cssUtils.typography("body", "heavy");
background-color: transparent;
padding: cssUtils.$space-l 0;
width: 100%;
Expand All @@ -127,18 +126,34 @@
justify-content: space-between;
color: cssUtils.$text-neutral-primary;
cursor: pointer;
border-radius: cssUtils.$radius-m;

&:focus {
outline: none;
}
&:focus-visible {
outline: 2px solid cssUtils.$border-interactive-emphasis;
outline-offset: 2px;
}

&-content {
display: flex;
align-items: center;
color: cssUtils.$text-neutral-primary;
gap: cssUtils.$space-xs;
gap: 0.5em;
// Leading: an Icon at the text's icon size, or an inline Avatar or Emoji.
&-icon {
display: flex;
display: inline-flex;
flex: none;
align-items: center;
justify-content: center;
height: 16px;
width: 16px;
min-width: 1.15em;

> svg {
width: 1.15em;
height: 1.15em;
stroke-width: 1.5;
}
}
}

Expand Down
94 changes: 94 additions & 0 deletions packages/react-ui/src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import clsx from "clsx";
import { ImageOff } from "lucide-react";
import { useEffect, useRef, useState, type ReactNode } from "react";
import { getFaviconUrl } from "../Sources/SourceContext";

export type AvatarShape = "circle" | "rounded";
export type AvatarSize = "inline" | "md" | "lg";
export type AvatarColor =
"neutral" | "info" | "success" | "warning" | "danger" | "purple" | "pink" | "inverted";

export interface AvatarProps {
/** Person or company name, used as the accessible label. */
name: string;
/** Image URL (a photo or logo). */
src?: string;
/** A company's website; its favicon is used when there's no src. */
domain?: string;
/** circle for people (default), rounded for logos. */
shape?: AvatarShape;
/** inline (default) sits on a text line like an Icon; md 32px; lg 48px. */
size?: AvatarSize;
/** An icon shown inside the shape when there's no image. */
icon?: ReactNode;
/** Background tint behind the icon; the icon color matches it. */
color?: AvatarColor;
className?: string;
}

const faviconFor = (domain?: string) => {
if (!domain) return "";
const url = /^https?:\/\//i.test(domain) ? domain : `https://${domain}`;
return getFaviconUrl(url, 128);
};

/**
* A small photo or logo that can stand in for an Icon, or an Icon on a tinted
* shape. A broken image shows the icon if given, else a placeholder icon.
*/
export const Avatar = ({
name,
src,
domain,
shape = "circle",
size = "inline",
icon,
color = "neutral",
className,
}: AvatarProps) => {
const imageUrl = src || faviconFor(domain);
const [failed, setFailed] = useState(false);
const imgRef = useRef<HTMLImageElement>(null);
// An image can fail before hydration attaches onError, so also check once
// on mount whether it already finished loading with no pixels.
useEffect(() => {
const img = imgRef.current;
setFailed(Boolean(img && img.complete && img.naturalWidth === 0));
}, [imageUrl]);
const showImage = Boolean(imageUrl) && !failed;
// A broken image becomes a placeholder icon; with no image and no icon at
// all there's nothing to show.
const content = icon ?? (imageUrl ? <ImageOff /> : null);
if (!showImage && !content) return null;

return (
<span
className={clsx(
"openui-avatar",
`openui-avatar--${shape}`,
`openui-avatar--${size}`,
`openui-avatar--color-${color}`,
className,
)}
role="img"
aria-label={name}
title={name}
>
{showImage ? (
<img
ref={imgRef}
className="openui-avatar__image"
src={imageUrl}
alt=""
loading="lazy"
referrerPolicy="no-referrer"
onError={() => setFailed(true)}
/>
) : (
<span className="openui-avatar__icon" aria-hidden>
{content}
</span>
)}
</span>
);
};
Loading
Loading