From 9199828dbfb4ef89053cd5be9327c973770e7519 Mon Sep 17 00:00:00 2001 From: pr3khar Date: Tue, 7 Apr 2026 16:12:04 +0530 Subject: [PATCH 1/8] styling upfates --- .../src/components/Shell/NewChatButton.tsx | 15 +- .../react-ui/src/components/Shell/Sidebar.tsx | 130 ++++++++++++++++-- .../src/components/Shell/ThreadList.tsx | 26 ++-- .../components/Shell/components/Composer.tsx | 2 +- .../components/DesktopWelcomeComposer.tsx | 2 +- .../components/Shell/components/composer.scss | 6 +- .../components/desktopWelcomeComposer.scss | 6 +- .../components/Shell/conversationStarter.scss | 15 +- .../react-ui/src/components/Shell/shell.scss | 4 +- .../src/components/Shell/sidebar.scss | 105 ++++++++++---- .../react-ui/src/components/Shell/thread.scss | 3 + .../src/components/Shell/threadlist.scss | 75 ++++++---- .../components/ThemeProvider/defaultTheme.ts | 4 +- .../react-ui/src/hooks/useScrollToBottom.ts | 8 +- 14 files changed, 304 insertions(+), 97 deletions(-) diff --git a/packages/react-ui/src/components/Shell/NewChatButton.tsx b/packages/react-ui/src/components/Shell/NewChatButton.tsx index a17ef7067..c2a373cbb 100644 --- a/packages/react-ui/src/components/Shell/NewChatButton.tsx +++ b/packages/react-ui/src/components/Shell/NewChatButton.tsx @@ -1,22 +1,27 @@ import { useThreadList } from "@openuidev/react-headless"; import clsx from "clsx"; -import { Plus, SquarePen } from "lucide-react"; +import { SquarePen } from "lucide-react"; import { Button } from "../Button"; import { IconButton } from "../IconButton"; import { useShellStore } from "../_shared/store"; +import { useOptionalSidebarVisualState } from "./Sidebar"; export const NewChatButton = ({ className }: { className?: string }) => { const switchToNewThread = useThreadList((s) => s.switchToNewThread); const { isSidebarOpen } = useShellStore((state) => ({ isSidebarOpen: state.isSidebarOpen, })); + const sidebarVisualState = useOptionalSidebarVisualState(); + const showExpandedButton = sidebarVisualState + ? sidebarVisualState.visualState === "expanded" + : isSidebarOpen; - if (!isSidebarOpen) { + if (!showExpandedButton) { return ( } onClick={switchToNewThread} - variant="primary" + variant="secondary" size="small" className={clsx("openui-shell-new-chat-button_collapsed", className)} /> @@ -26,8 +31,8 @@ export const NewChatButton = ({ className }: { className?: string }) => { return ( diff --git a/packages/react-ui/src/components/Shell/components/Composer.tsx b/packages/react-ui/src/components/Shell/components/Composer.tsx index b141a0e83..d109e03ee 100644 --- a/packages/react-ui/src/components/Shell/components/Composer.tsx +++ b/packages/react-ui/src/components/Shell/components/Composer.tsx @@ -68,7 +68,7 @@ export const Composer = ({ className, placeholder = "Type your query here" }: Co : } - size="medium" + size="extra-small" variant="primary" className="openui-shell-thread-composer__submit-button" /> diff --git a/packages/react-ui/src/components/Shell/components/DesktopWelcomeComposer.tsx b/packages/react-ui/src/components/Shell/components/DesktopWelcomeComposer.tsx index a4debec01..3ace7e7bf 100644 --- a/packages/react-ui/src/components/Shell/components/DesktopWelcomeComposer.tsx +++ b/packages/react-ui/src/components/Shell/components/DesktopWelcomeComposer.tsx @@ -65,7 +65,7 @@ export const DesktopWelcomeComposer = ({ disabled={!textContent.trim() && !isRunning} aria-label={isRunning ? "Cancel" : "Send"} icon={isRunning ? : } - size="medium" + size="extra-small" variant="primary" className="openui-shell-desktop-welcome-composer__submit-button" /> diff --git a/packages/react-ui/src/components/Shell/components/composer.scss b/packages/react-ui/src/components/Shell/components/composer.scss index bfe4431d2..223f46b6c 100644 --- a/packages/react-ui/src/components/Shell/components/composer.scss +++ b/packages/react-ui/src/components/Shell/components/composer.scss @@ -21,13 +21,13 @@ $center-align-spacing: calc(32px + cssUtils.$space-s); &__input-wrapper { background-color: cssUtils.$foreground; border: 1px solid cssUtils.$border-interactive; - border-radius: cssUtils.$radius-2xl; - box-shadow: cssUtils.$shadow-s; + border-radius: cssUtils.$radius-3xl; + box-shadow: cssUtils.$shadow-m; overflow: clip; display: flex; flex-direction: column; gap: cssUtils.$space-s; - padding: cssUtils.$space-s; + padding: cssUtils.$space-s-m; } &__input { diff --git a/packages/react-ui/src/components/Shell/components/desktopWelcomeComposer.scss b/packages/react-ui/src/components/Shell/components/desktopWelcomeComposer.scss index 93636644f..c99dd7696 100644 --- a/packages/react-ui/src/components/Shell/components/desktopWelcomeComposer.scss +++ b/packages/react-ui/src/components/Shell/components/desktopWelcomeComposer.scss @@ -3,13 +3,13 @@ .openui-shell-desktop-welcome-composer { background-color: cssUtils.$foreground; border: 1px solid cssUtils.$border-interactive; - border-radius: cssUtils.$radius-2xl; - box-shadow: cssUtils.$shadow-s; + border-radius: cssUtils.$radius-3xl; + box-shadow: cssUtils.$shadow-m; overflow: clip; display: flex; flex-direction: column; gap: cssUtils.$space-m; - padding: cssUtils.$space-m; + padding: cssUtils.$space-s-m; width: 100%; max-width: 800px; diff --git a/packages/react-ui/src/components/Shell/conversationStarter.scss b/packages/react-ui/src/components/Shell/conversationStarter.scss index 5b6bdc627..5f16790de 100644 --- a/packages/react-ui/src/components/Shell/conversationStarter.scss +++ b/packages/react-ui/src/components/Shell/conversationStarter.scss @@ -13,7 +13,7 @@ $center-align-spacing: calc(32px + cssUtils.$space-s); &--short { display: flex; flex-wrap: wrap; - gap: cssUtils.$space-s; + gap: cssUtils.$space-xs; } // Long variant - vertical stacked list with separators @@ -46,13 +46,13 @@ $center-align-spacing: calc(32px + cssUtils.$space-s); align-items: center; gap: cssUtils.$space-xs; width: fit-content; - padding: cssUtils.$space-s cssUtils.$space-m; + padding: cssUtils.$space-s cssUtils.$space-s-m; background-color: cssUtils.$foreground; border: 1px solid cssUtils.$border-default; - border-radius: cssUtils.$radius-m; + border-radius: cssUtils.$radius-l; cursor: pointer; transition: all 0.15s ease; - @include cssUtils.typography(body, default); + @include cssUtils.typography(body, small); color: cssUtils.$text-neutral-primary; text-align: left; @@ -63,6 +63,11 @@ $center-align-spacing: calc(32px + cssUtils.$space-s); justify-content: center; flex-shrink: 0; color: cssUtils.$text-neutral-primary; + + & > svg { + width: 14px; + height: 14px; + } } // Text @@ -81,7 +86,7 @@ $center-align-spacing: calc(32px + cssUtils.$space-s); } @media (max-width: 480px) { - padding: cssUtils.$space-s cssUtils.$space-m; + padding: cssUtils.$space-s cssUtils.$space-s-m; } } diff --git a/packages/react-ui/src/components/Shell/shell.scss b/packages/react-ui/src/components/Shell/shell.scss index 542413566..8cdc6c73e 100644 --- a/packages/react-ui/src/components/Shell/shell.scss +++ b/packages/react-ui/src/components/Shell/shell.scss @@ -29,7 +29,9 @@ .openui-shell-new-chat-button { width: 100%; - justify-content: space-between; + justify-content: flex-start; + padding-left: cssUtils.$space-s; + padding-right: cssUtils.$space-s; .openui-shell-sidebar-header--collapsed & { width: auto; diff --git a/packages/react-ui/src/components/Shell/sidebar.scss b/packages/react-ui/src/components/Shell/sidebar.scss index 638ed7305..51d0dc5f2 100644 --- a/packages/react-ui/src/components/Shell/sidebar.scss +++ b/packages/react-ui/src/components/Shell/sidebar.scss @@ -4,10 +4,14 @@ $sidebar-width: 272px; $sidebar-padding: cssUtils.$space-m; $sidebar-mobile-width: 294px; $sidebar-mobile-padding: cssUtils.$space-l; +$sidebar-fade-duration: 90ms; +$sidebar-resize-duration: 160ms; +$sidebar-motion-ease: cubic-bezier(0.22, 1, 0.36, 1); .openui-shell-sidebar-container { border-right: 1px solid cssUtils.$border-default; height: 100%; + background-color: cssUtils.$foreground; width: $sidebar-width; padding: $sidebar-padding; display: flex; @@ -15,9 +19,10 @@ $sidebar-mobile-padding: cssUtils.$space-l; gap: cssUtils.$space-m; z-index: 999; transition: - width 0.4s ease-in-out, - height 0.4s ease-in-out, - transform 0.4s ease-in-out; + width $sidebar-resize-duration $sidebar-motion-ease, + height $sidebar-resize-duration $sidebar-motion-ease, + transform $sidebar-resize-duration $sidebar-motion-ease, + background-color $sidebar-resize-duration $sidebar-motion-ease; .openui-shell-container--mobile & { width: $sidebar-mobile-width; @@ -28,49 +33,88 @@ $sidebar-mobile-padding: cssUtils.$space-l; z-index: 1000; background-color: cssUtils.$foreground; border-radius: 0; - transition: left 0.3s ease-in-out; + transition: left $sidebar-resize-duration $sidebar-motion-ease; } &--collapsed { width: 52px; transform: translateX(0); gap: cssUtils.$space-xs; + cursor: e-resize; + background-color: cssUtils.$background; .openui-shell-container--mobile & { width: $sidebar-mobile-width; height: 100%; left: -$sidebar-mobile-width; transform: none; + cursor: default; + } + } + + &--collapsed[data-sidebar-visual-state="collapsed"]:hover { + .openui-shell-sidebar-header__logo { + opacity: 0; + pointer-events: none; + } + + .openui-shell-sidebar-header__toggle-button { + opacity: 1; + pointer-events: auto; } } &--hidden { display: none; } + + &[data-sidebar-visual-state="collapsing"], + &[data-sidebar-visual-state="expanding"] { + .openui-shell-sidebar-header__top-row, + .openui-shell-sidebar-content, + .openui-shell-sidebar-header > :not(.openui-shell-sidebar-header__top-row) { + opacity: 0; + pointer-events: none; + } + } + + &[data-sidebar-visual-state="expanding"] { + .openui-shell-sidebar-header > :not(.openui-shell-sidebar-header__top-row) { + opacity: 0; + pointer-events: none; + } + } } .openui-shell-sidebar-header { display: flex; flex-direction: column; - gap: cssUtils.$space-s; + gap: cssUtils.$space-l; + + & > :not(.openui-shell-sidebar-header__top-row) { + transition: opacity $sidebar-fade-duration $sidebar-motion-ease; + } &__top-row { display: flex; align-items: center; gap: cssUtils.$space-s; + transition: opacity $sidebar-fade-duration $sidebar-motion-ease; } &__logo { - width: 36px; - height: 36px; - border-radius: cssUtils.$radius-s; + width: 32px; + height: 32px; + margin-left: cssUtils.$space-3xs; + border-radius: cssUtils.$radius-m; transition: - width 0.3s ease-in-out, - height 0.3s ease-in-out; + width $sidebar-resize-duration $sidebar-motion-ease, + height $sidebar-resize-duration $sidebar-motion-ease, + opacity $sidebar-fade-duration $sidebar-motion-ease; } &__agent-name { - @include cssUtils.typography(primary, default); + @include cssUtils.typography(label, small); flex-grow: 1; color: cssUtils.$text-neutral-primary; @@ -83,13 +127,26 @@ $sidebar-mobile-padding: cssUtils.$space-l; align-items: center; .openui-shell-sidebar-header__top-row { - flex-direction: column; - align-items: center; + display: grid; + min-height: 32px; + place-items: center; + } + + .openui-shell-sidebar-header__logo, + .openui-shell-sidebar-header__toggle-button { + grid-area: 1 / 1; + margin-left: auto; } .openui-shell-sidebar-header__logo { - width: 28px; - height: 28px; + width: 32px; + height: 32px; + } + + .openui-shell-sidebar-header__toggle-button { + opacity: 0; + pointer-events: none; + transition: opacity $sidebar-fade-duration $sidebar-motion-ease; } } } @@ -106,16 +163,16 @@ $sidebar-mobile-padding: cssUtils.$space-l; gap: cssUtils.$space-m; overflow: hidden; opacity: 1; - transition: - opacity 0.2s ease-in-out 0.3s, - display 0.4s 0.5s; + transition: opacity $sidebar-fade-duration $sidebar-motion-ease; &--collapsed { - opacity: 0; - display: none; - transition: - opacity 0.2s ease-in-out, - display 0s 0.5s; + width: 100%; + align-items: center; + gap: cssUtils.$space-s; + + .openui-shell-thread-list { + display: none; + } } } @@ -138,7 +195,7 @@ $sidebar-mobile-padding: cssUtils.$space-l; width: 100%; height: 100%; background-color: cssUtils.$overlay; - transition: opacity 0.3s ease-in-out; + transition: opacity $sidebar-resize-duration $sidebar-motion-ease; z-index: 99; &--collapsed { diff --git a/packages/react-ui/src/components/Shell/thread.scss b/packages/react-ui/src/components/Shell/thread.scss index 3ef323fef..9d20d2fec 100644 --- a/packages/react-ui/src/components/Shell/thread.scss +++ b/packages/react-ui/src/components/Shell/thread.scss @@ -112,6 +112,7 @@ $center-align-spacing: calc(32px + cssUtils.$space-s); height: 100%; overflow: auto; padding: cssUtils.$space-m $center-align-spacing cssUtils.$space-xl; + scroll-padding-top: cssUtils.$space-2xl; scrollbar-width: thin; scrollbar-color: cssUtils.$highlight-strong transparent; @@ -140,6 +141,7 @@ $center-align-spacing: calc(32px + cssUtils.$space-s); .openui-shell-container--mobile & { padding: cssUtils.$space-l; + scroll-padding-top: cssUtils.$space-l; } // When artifact is active, reduce padding to maximize space @@ -162,6 +164,7 @@ $center-align-spacing: calc(32px + cssUtils.$space-s); .openui-shell-thread-messages { max-width: 880px; margin: 0 auto; + padding-top: cssUtils.$space-l; display: flex; flex-direction: column; gap: calc(2 * cssUtils.$space-l); diff --git a/packages/react-ui/src/components/Shell/threadlist.scss b/packages/react-ui/src/components/Shell/threadlist.scss index 80ee98c92..569405e32 100644 --- a/packages/react-ui/src/components/Shell/threadlist.scss +++ b/packages/react-ui/src/components/Shell/threadlist.scss @@ -3,15 +3,17 @@ .openui-shell-thread-list { display: flex; flex-direction: column; - gap: cssUtils.$space-s; + gap: cssUtils.$space-2xs; overflow: auto; + margin-top: cssUtils.$space-m; } .openui-shell-thread-list-group { display: flex; - padding-left: cssUtils.$space-xs; - color: cssUtils.$text-neutral-secondary; - @include cssUtils.typography(label, default); + padding-left: cssUtils.$space-s; + color: cssUtils.$text-neutral-tertiary; + @include cssUtils.typography(label, small); + margin-bottom: cssUtils.$space-2xs; } .openui-shell-thread-button { @@ -19,12 +21,16 @@ align-items: center; justify-content: space-between; width: 100%; - border-radius: cssUtils.$radius-s; + border-radius: calc(cssUtils.$radius-s + 1px); border: 1px solid transparent; + transition: all 0.12s ease; &--selected { - background-color: cssUtils.$sunk; - border-color: cssUtils.$border-default; + background-color: cssUtils.$highlight; + } + + &:hover { + background-color: cssUtils.$highlight; } &:hover { @@ -36,9 +42,9 @@ .openui-shell-thread-button-title { @include cssUtils.button-reset; - @include cssUtils.typography(primary, default); + @include cssUtils.typography(body, small); color: cssUtils.$text-neutral-primary; - padding: cssUtils.$space-2xs cssUtils.$space-xs; + padding: calc(cssUtils.$space-xs - 1.75px) cssUtils.$space-s; width: 100%; text-align: left; cursor: pointer; @@ -50,9 +56,9 @@ .openui-shell-thread-button-dropdown-trigger { @include cssUtils.button-reset; outline: none; - @include cssUtils.typography(primary, default); + @include cssUtils.typography(body, small); color: cssUtils.$text-neutral-primary; - padding: cssUtils.$space-2xs cssUtils.$space-xs; + padding: calc(cssUtils.$space-s - 1.75px) cssUtils.$space-s; flex-shrink: 0; min-height: 28px; cursor: pointer; @@ -68,27 +74,42 @@ } .openui-shell-thread-button-dropdown-menu { - display: flex; - flex-direction: column; - padding: cssUtils.$space-xs; + box-sizing: border-box; + position: relative; + z-index: 99999; + min-width: 160px; + overflow: hidden; border: 1px solid cssUtils.$border-default; - border-radius: cssUtils.$radius-m; + border-radius: cssUtils.$radius-xl; background-color: cssUtils.$foreground; - z-index: 9999; + box-shadow: cssUtils.$shadow-m; + color: cssUtils.$text-neutral-primary; + transform-origin: top center; + animation: openui-shell-thread-menu-show 0.18s ease-out forwards; + padding: 4px; } .openui-shell-thread-button-dropdown-menu-item { - @include cssUtils.button-reset; - @include cssUtils.typography(primary, default); - outline: none; - color: cssUtils.$text-neutral-primary; - padding: cssUtils.$space-2xs cssUtils.$space-xs; - display: flex; - align-items: center; - gap: cssUtils.$space-2xs; - cursor: pointer; + width: 100%; + justify-content: flex-start; + padding-left: cssUtils.$space-s; + padding-right: cssUtils.$space-s; + + &:focus, + &:focus-visible { + outline: none; + box-shadow: none; + } } -.openui-shell-thread-button-dropdown-menu-item-icon { - color: cssUtils.$text-neutral-secondary; +@keyframes openui-shell-thread-menu-show { + from { + opacity: 0; + transform: scale(0.95); + } + + to { + opacity: 1; + transform: scale(1); + } } diff --git a/packages/react-ui/src/components/ThemeProvider/defaultTheme.ts b/packages/react-ui/src/components/ThemeProvider/defaultTheme.ts index 47ed06188..d3dc9bde9 100644 --- a/packages/react-ui/src/components/ThemeProvider/defaultTheme.ts +++ b/packages/react-ui/src/components/ThemeProvider/defaultTheme.ts @@ -249,8 +249,8 @@ const createColorTheme = ({ chatUserResponseText: accentText, // Border - borderDefault: withAlpha(overlayBase, isDark ? 0.12 : 0.06), - borderInteractive: withAlpha(overlayBase, isDark ? 0.2 : 0.12), + borderDefault: withAlpha(overlayBase, isDark ? 0.06 : 0.06), + borderInteractive: withAlpha(overlayBase, isDark ? 0.12 : 0.12), borderInteractiveEmphasis: withAlpha(overlayBase, isDark ? 0.4 : 0.3), borderInteractiveSelected: isDark ? swatch(neutral, 50) : swatch(neutral, 1000), borderAccent: withAlpha(brandSolid, isDark ? 0.2 : 0.08), diff --git a/packages/react-ui/src/hooks/useScrollToBottom.ts b/packages/react-ui/src/hooks/useScrollToBottom.ts index 6295bd916..e6dd25359 100644 --- a/packages/react-ui/src/hooks/useScrollToBottom.ts +++ b/packages/react-ui/src/hooks/useScrollToBottom.ts @@ -110,13 +110,17 @@ export const useScrollToBottom = Date: Thu, 9 Apr 2026 03:35:04 +0530 Subject: [PATCH 2/8] making shells concistent and improving styling --- .../src/components/BottomTray/Container.tsx | 8 +- .../BottomTray/ConversationStarter.tsx | 55 ++++++- .../src/components/BottomTray/Thread.tsx | 23 +-- .../BottomTray/ThreadListContainer.tsx | 27 +++- .../BottomTray/components/Composer.tsx | 2 +- .../BottomTray/components/composer.scss | 11 +- .../src/components/BottomTray/container.scss | 2 +- .../BottomTray/conversationStarter.scss | 59 ++++++-- .../src/components/BottomTray/header.scss | 4 +- .../src/components/BottomTray/thread.scss | 40 +++-- .../src/components/BottomTray/threadList.scss | 95 +++++++----- .../components/BottomTray/welcomeScreen.scss | 6 +- .../src/components/CopilotShell/Container.tsx | 15 +- .../CopilotShell/ConversationStarter.tsx | 55 ++++++- .../src/components/CopilotShell/Header.tsx | 39 ++++- .../src/components/CopilotShell/Thread.tsx | 34 +++-- .../CopilotShell/ThreadListContainer.tsx | 118 +++++++++++++++ .../CopilotShell/components/Composer.tsx | 2 +- .../CopilotShell/components/composer.scss | 13 +- .../CopilotShell/conversationStarter.scss | 61 ++++++-- .../components/CopilotShell/copilotShell.scss | 1 + .../src/components/CopilotShell/header.scss | 6 +- .../CopilotShell/stories/Shell.stories.tsx | 6 +- .../src/components/CopilotShell/thread.scss | 43 +++++- .../components/CopilotShell/threadList.scss | 143 ++++++++++++++++++ .../CopilotShell/welcomeScreen.scss | 6 +- .../OpenUIChat/ComposedBottomTray.tsx | 8 +- .../components/OpenUIChat/ComposedCopilot.tsx | 3 +- .../OpenUIChat/ComposedStandalone.tsx | 3 +- .../src/components/OpenUIChat/types.ts | 1 + .../src/components/Shell/Container.tsx | 15 +- .../components/Shell/ConversationStarter.tsx | 55 ++++++- .../react-ui/src/components/Shell/Thread.tsx | 96 +++++++----- .../src/components/Shell/ThreadList.tsx | 13 +- .../src/components/Shell/WelcomeScreen.tsx | 15 +- .../components/Shell/conversationStarter.scss | 31 +++- .../react-ui/src/components/Shell/thread.scss | 32 +++- .../src/components/Shell/threadlist.scss | 22 ++- .../src/components/Shell/welcomeScreen.scss | 26 +++- .../components/ThemeProvider/defaultTheme.ts | 4 +- .../src/components/_shared/store/store.tsx | 29 +++- .../react-ui/src/hooks/useComposerState.ts | 42 ++++- 42 files changed, 1040 insertions(+), 229 deletions(-) create mode 100644 packages/react-ui/src/components/CopilotShell/ThreadListContainer.tsx create mode 100644 packages/react-ui/src/components/CopilotShell/threadList.scss diff --git a/packages/react-ui/src/components/BottomTray/Container.tsx b/packages/react-ui/src/components/BottomTray/Container.tsx index d92521399..2226ca54a 100644 --- a/packages/react-ui/src/components/BottomTray/Container.tsx +++ b/packages/react-ui/src/components/BottomTray/Container.tsx @@ -7,6 +7,7 @@ interface ContainerProps { logoUrl: string; agentName: string; className?: string; + showAssistantLogo?: boolean; /** Control the open state of the tray */ isOpen?: boolean; } @@ -16,10 +17,15 @@ export const Container = ({ logoUrl, agentName, className, + showAssistantLogo = false, isOpen = false, }: ContainerProps) => { return ( - +
{ return icon; }; +const hasRenderableIcon = (icon: ReactNode): boolean => { + if (icon === null || icon === undefined || icon === false) { + return false; + } + + if (isValidElement(icon) && icon.type === Fragment) { + return Boolean(icon.props.children); + } + + return true; +}; + const ConversationStarterItem = ({ displayText, prompt, @@ -33,6 +47,7 @@ const ConversationStarterItem = ({ icon, }: ConversationStarterItemProps) => { const renderedIcon = renderIcon(icon); + const shouldRenderIcon = hasRenderableIcon(renderedIcon); if (variant === "short") { return ( @@ -41,7 +56,7 @@ const ConversationStarterItem = ({ className="openui-bottom-tray-conversation-starter-item-short" onClick={() => onClick(prompt)} > - {renderedIcon && ( + {shouldRenderIcon && ( {renderedIcon} @@ -61,7 +76,7 @@ const ConversationStarterItem = ({ onClick={() => onClick(prompt)} >
- {renderedIcon && ( + {shouldRenderIcon && ( {renderedIcon} @@ -93,10 +108,12 @@ export const ConversationStarter = ({ className, variant = "short", }: ConversationStarterContainerProps) => { + const { textContent } = useComposerState(); const processMessage = useThread((s) => s.processMessage); const isRunning = useThread((s) => s.isRunning); const messages = useThread((s) => s.messages); const isLoadingMessages = useThread((s) => s.isLoadingMessages); + const isDrafting = textContent.length > 0; const handleClick = (prompt: string) => { if (isRunning) return; @@ -115,11 +132,43 @@ export const ConversationStarter = ({ return null; } + if (variant === "short") { + return ( + + + {starters.map((item, index) => ( + + ))} + + + ); + } + return (
diff --git a/packages/react-ui/src/components/BottomTray/Thread.tsx b/packages/react-ui/src/components/BottomTray/Thread.tsx index 712e9f190..c777d729c 100644 --- a/packages/react-ui/src/components/BottomTray/Thread.tsx +++ b/packages/react-ui/src/components/BottomTray/Thread.tsx @@ -2,6 +2,7 @@ import type { AssistantMessage, Message, ToolMessage } from "@openuidev/react-he import { MessageProvider, useThread } from "@openuidev/react-headless"; import clsx from "clsx"; import React, { memo, useRef } from "react"; +import { ComposerStateProvider } from "../../hooks/useComposerState"; import { ScrollVariant, useScrollToBottom } from "../../hooks/useScrollToBottom"; import { ArtifactOverlay } from "../_shared/artifact"; import type { AssistantMessageComponent, UserMessageComponent } from "../_shared/types"; @@ -20,15 +21,17 @@ export const ThreadContainer = ({ const isLoadingMessages = useThread((s) => s.isLoadingMessages); return ( -
- {children} - -
+ +
+ {children} + +
+
); }; @@ -79,8 +82,6 @@ export const ScrollArea = ({ > {children}
- {/* Gradient to hide the bottom of the scroll area */} -
); }; diff --git a/packages/react-ui/src/components/BottomTray/ThreadListContainer.tsx b/packages/react-ui/src/components/BottomTray/ThreadListContainer.tsx index e497f8755..39d420db9 100644 --- a/packages/react-ui/src/components/BottomTray/ThreadListContainer.tsx +++ b/packages/react-ui/src/components/BottomTray/ThreadListContainer.tsx @@ -3,6 +3,7 @@ import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import clsx from "clsx"; import { EllipsisVerticalIcon, MenuIcon, Trash2Icon } from "lucide-react"; import { useEffect } from "react"; +import { Button } from "../Button"; import { IconButton } from "../IconButton"; const ThreadItem = ({ @@ -27,9 +28,13 @@ const ThreadItem = ({ - + } + aria-label={`More actions for ${title}`} + variant="tertiary" + size="extra-small" + className="openui-bottom-tray-thread-item-menu-trigger" + /> { e.stopPropagation(); onDelete(); }} > - - Delete + @@ -64,7 +77,7 @@ export const ThreadListContainer = () => { useEffect(() => { loadThreads(); - }, []); + }, [loadThreads]); return ( diff --git a/packages/react-ui/src/components/BottomTray/components/Composer.tsx b/packages/react-ui/src/components/BottomTray/components/Composer.tsx index af5667086..a33b43fa9 100644 --- a/packages/react-ui/src/components/BottomTray/components/Composer.tsx +++ b/packages/react-ui/src/components/BottomTray/components/Composer.tsx @@ -68,7 +68,7 @@ export const Composer = ({ className, placeholder = "Type your message..." }: Co : } - size="medium" + size="extra-small" variant="primary" className="openui-bottom-tray-thread-composer__submit-button" /> diff --git a/packages/react-ui/src/components/BottomTray/components/composer.scss b/packages/react-ui/src/components/BottomTray/components/composer.scss index 73f28ba53..d21e42aa9 100644 --- a/packages/react-ui/src/components/BottomTray/components/composer.scss +++ b/packages/react-ui/src/components/BottomTray/components/composer.scss @@ -1,19 +1,22 @@ @use "../../../cssUtils" as cssUtils; .openui-bottom-tray-thread-composer { + box-sizing: border-box; + flex-shrink: 0; width: 100%; - padding: 0 cssUtils.$space-s cssUtils.$space-s; + margin: 0 0 cssUtils.$space-m; + padding: 0 cssUtils.$space-m; &__input-wrapper { background-color: cssUtils.$foreground; border: 1px solid cssUtils.$border-interactive; - border-radius: cssUtils.$radius-l; - box-shadow: cssUtils.$shadow-s; + border-radius: cssUtils.$radius-2xl; + box-shadow: cssUtils.$shadow-m; overflow: clip; display: flex; flex-direction: column; gap: cssUtils.$space-s; - padding: cssUtils.$space-s; + padding: cssUtils.$space-s-m; } &__input { diff --git a/packages/react-ui/src/components/BottomTray/container.scss b/packages/react-ui/src/components/BottomTray/container.scss index 413791e1c..1187eedee 100644 --- a/packages/react-ui/src/components/BottomTray/container.scss +++ b/packages/react-ui/src/components/BottomTray/container.scss @@ -20,7 +20,7 @@ clip-path 0.25s ease-in-out; border: 1px solid cssUtils.$border-default; - border-radius: cssUtils.$radius-2xl cssUtils.$radius-2xl cssUtils.$radius-3xl cssUtils.$radius-3xl; + border-radius: cssUtils.$radius-4xl; box-shadow: cssUtils.$shadow-2xl; background: cssUtils.$chat-container-bg; diff --git a/packages/react-ui/src/components/BottomTray/conversationStarter.scss b/packages/react-ui/src/components/BottomTray/conversationStarter.scss index 29b09cb7d..98e07b33a 100644 --- a/packages/react-ui/src/components/BottomTray/conversationStarter.scss +++ b/packages/react-ui/src/components/BottomTray/conversationStarter.scss @@ -3,13 +3,24 @@ // Container styles .openui-bottom-tray-conversation-starter { display: flex; - padding: 0 cssUtils.$space-s; + flex-shrink: 0; + width: 100%; + padding: 0 cssUtils.$space-m; margin-bottom: cssUtils.$space-s; + box-sizing: border-box; + max-height: 480px; + overflow: hidden; + opacity: 1; + transform: translateY(0); + transition: + opacity 0.2s ease, + transform 0.2s ease, + max-height 0.2s ease, + margin-bottom 0.2s ease; // Short variant - horizontal pill buttons &--short { - flex-direction: column; - gap: cssUtils.$space-s; + display: block; } // Long variant - vertical list items with separators @@ -22,18 +33,31 @@ &__separator { padding: cssUtils.$space-3xs cssUtils.$space-xs; } + + &__carousel-content { + gap: cssUtils.$space-xs; + } + + &--hidden { + opacity: 0; + transform: translateY(-4px); + max-height: 0; + margin-bottom: 0; + pointer-events: none; + } } // Short variant item (pill-style buttons) .openui-bottom-tray-conversation-starter-item-short { display: flex; - align-items: flex-start; + align-items: center; gap: cssUtils.$space-xs; width: fit-content; - padding: cssUtils.$space-s cssUtils.$space-m; + flex: 0 0 auto; + padding: cssUtils.$space-s cssUtils.$space-s-m; background-color: cssUtils.$foreground; - border: 1px solid cssUtils.$border-default; - border-radius: cssUtils.$radius-m; + border: 1px solid cssUtils.$border-interactive; + border-radius: cssUtils.$radius-l; cursor: pointer; transition: all 0.15s ease; @include cssUtils.typography(body, small); @@ -43,29 +67,34 @@ // Icon container &__icon { display: flex; - align-items: flex-start; + align-items: center; justify-content: center; flex-shrink: 0; - padding-top: cssUtils.$space-3xs; color: cssUtils.$text-neutral-primary; + + & > svg { + width: 14px; + height: 14px; + } } // Text &__text { flex: 1; + white-space: nowrap; } &:not(:disabled):hover { - background-color: cssUtils.$sunk; + background-color: cssUtils.$highlight; border-color: cssUtils.$border-interactive-emphasis; } &:not(:disabled):active { - background-color: cssUtils.$sunk; + background-color: cssUtils.$highlight-subtle; } @media (max-width: 480px) { - padding: cssUtils.$space-xs cssUtils.$space-s; + padding: cssUtils.$space-s cssUtils.$space-s-m; } } @@ -75,10 +104,10 @@ align-items: center; gap: cssUtils.$space-000; width: 100%; - padding: cssUtils.$space-xs cssUtils.$space-xs; + padding: cssUtils.$space-xs cssUtils.$space-s-m; background-color: transparent; border: none; - border-radius: cssUtils.$radius-m; + border-radius: cssUtils.$radius-l; cursor: pointer; transition: background-color 0.15s ease; @include cssUtils.typography(body, default); @@ -116,9 +145,11 @@ // Arrow icon (shown on hover) &__arrow { display: flex; + align-self: flex-start; align-items: center; justify-content: center; flex-shrink: 0; + padding-top: cssUtils.$space-2xs; opacity: 0; transition: opacity 0.15s ease; color: cssUtils.$text-neutral-primary; diff --git a/packages/react-ui/src/components/BottomTray/header.scss b/packages/react-ui/src/components/BottomTray/header.scss index 5d320fb9a..a7fc6b7bb 100644 --- a/packages/react-ui/src/components/BottomTray/header.scss +++ b/packages/react-ui/src/components/BottomTray/header.scss @@ -5,7 +5,7 @@ align-items: center; justify-content: space-between; padding: cssUtils.$space-m; - border-radius: cssUtils.$radius-l cssUtils.$radius-l 0 0; + border-radius: cssUtils.$radius-4xl cssUtils.$radius-4xl 0 0; // Mobile @media (max-width: 768px) { @@ -34,5 +34,5 @@ .openui-bottom-tray-header-actions { display: flex; align-items: center; - gap: cssUtils.$space-s; + gap: cssUtils.$space-2xs; } diff --git a/packages/react-ui/src/components/BottomTray/thread.scss b/packages/react-ui/src/components/BottomTray/thread.scss index 6cc739987..5dfd8cf43 100644 --- a/packages/react-ui/src/components/BottomTray/thread.scss +++ b/packages/react-ui/src/components/BottomTray/thread.scss @@ -13,17 +13,30 @@ flex: 1; position: relative; overflow: hidden; -} -.openui-bottom-tray-thread-scroll-gradient { - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 40px; - z-index: 1; - pointer-events: none; - background: linear-gradient(to top, cssUtils.$background 0%, transparent); + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: cssUtils.$space-xl; + background: linear-gradient(to bottom, cssUtils.$background 0%, transparent 100%); + pointer-events: none; + z-index: 1; + } + + &::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: cssUtils.$space-xl; + background: linear-gradient(to top, cssUtils.$background 0%, transparent 100%); + pointer-events: none; + z-index: 1; + } } .openui-bottom-tray-thread-scroll-area { @@ -31,6 +44,8 @@ height: 100%; overflow: auto; padding: cssUtils.$space-m; + scroll-padding-top: calc(cssUtils.$space-m + cssUtils.$space-xs); + &--user-message-anchor { & .openui-bottom-tray-thread-messages > *:last-child { min-height: calc(-200px + 100dvh); @@ -40,6 +55,7 @@ .openui-bottom-tray-thread-messages { margin: 0 auto; + padding-top: cssUtils.$space-s; display: flex; flex-direction: column; gap: cssUtils.$space-xl; @@ -70,10 +86,10 @@ &__content { @include cssUtils.typography(primary, default); - padding: cssUtils.$space-m cssUtils.$space-l; + padding: cssUtils.$space-s cssUtils.$space-m; background-color: cssUtils.$chat-user-response-bg; color: cssUtils.$chat-user-response-text; - border-radius: cssUtils.$radius-2xl; + border-radius: cssUtils.$radius-xl; overflow-wrap: break-word; max-width: 100%; height: fit-content; diff --git a/packages/react-ui/src/components/BottomTray/threadList.scss b/packages/react-ui/src/components/BottomTray/threadList.scss index 43f88cf58..f2de6bc3c 100644 --- a/packages/react-ui/src/components/BottomTray/threadList.scss +++ b/packages/react-ui/src/components/BottomTray/threadList.scss @@ -9,7 +9,7 @@ max-height: 296px; padding: cssUtils.$space-s; border: 1px solid cssUtils.$border-default; - border-radius: cssUtils.$radius-l; + border-radius: cssUtils.$radius-2xl; background-color: cssUtils.$foreground; box-shadow: cssUtils.$shadow-l; z-index: 9999; @@ -18,7 +18,7 @@ .openui-bottom-tray-thread-list-header { @include cssUtils.typography(label, small); - color: cssUtils.$text-neutral-secondary; + color: cssUtils.$text-neutral-tertiary; padding: cssUtils.$space-xs cssUtils.$space-s; padding-bottom: cssUtils.$space-s; } @@ -37,23 +37,38 @@ text-align: center; } +.openui-bottom-tray-thread-list-trigger { + &[data-state="open"], + &[aria-expanded="true"] { + background-color: cssUtils.$highlight; + border-color: cssUtils.$border-interactive; + } +} + // Thread Item .openui-bottom-tray-thread-item { display: flex; align-items: center; justify-content: space-between; width: 100%; - border-radius: cssUtils.$radius-s; + border-radius: calc(cssUtils.$radius-s + 1px); border: 1px solid transparent; - padding-right: cssUtils.$space-xs; + transition: all 0.12s ease; &--selected { - background-color: cssUtils.$sunk; - border-color: cssUtils.$border-default; + background-color: cssUtils.$highlight; } &:hover { - background-color: cssUtils.$sunk; + background-color: cssUtils.$highlight; + + .openui-bottom-tray-thread-item-menu-trigger { + opacity: 1; + } + } + + &:has(.openui-bottom-tray-thread-item-menu-trigger[data-state="open"]) { + background-color: cssUtils.$highlight; .openui-bottom-tray-thread-item-menu-trigger { opacity: 1; @@ -65,8 +80,8 @@ @include cssUtils.button-reset; @include cssUtils.typography(body, small); color: cssUtils.$text-neutral-primary; - padding: cssUtils.$space-xs cssUtils.$space-s; - flex: 1; + padding: calc(cssUtils.$space-xs - 1.75px) cssUtils.$space-s; + width: 100%; text-align: left; cursor: pointer; white-space: nowrap; @@ -75,52 +90,56 @@ } .openui-bottom-tray-thread-item-menu-trigger { - @include cssUtils.button-reset; - outline: none; - color: cssUtils.$text-neutral-secondary; - padding: cssUtils.$space-xs; - flex-shrink: 0; - cursor: pointer; opacity: 0; - border-radius: cssUtils.$radius-xs; - &:hover { - background-color: cssUtils.$highlight; + &:focus-visible { + opacity: 1; } &[data-state="open"] { opacity: 1; + background-color: cssUtils.$highlight; + border-color: cssUtils.$border-interactive; } } .openui-bottom-tray-thread-item-menu { - display: flex; - flex-direction: column; - padding: cssUtils.$space-xs; + box-sizing: border-box; + position: relative; + z-index: 99999; + min-width: 160px; + overflow: hidden; border: 1px solid cssUtils.$border-default; - border-radius: cssUtils.$radius-m; + border-radius: cssUtils.$radius-xl; background-color: cssUtils.$foreground; box-shadow: cssUtils.$shadow-m; - z-index: 10000; + color: cssUtils.$text-neutral-primary; + transform-origin: top center; + animation: openui-bottom-tray-thread-menu-show 0.18s ease-out forwards; + padding: 4px; } .openui-bottom-tray-thread-item-menu-action { - @include cssUtils.button-reset; - @include cssUtils.typography(body, small); - outline: none; - color: cssUtils.$text-neutral-primary; - padding: cssUtils.$space-xs cssUtils.$space-s; - display: flex; - align-items: center; - gap: cssUtils.$space-xs; - cursor: pointer; - border-radius: cssUtils.$radius-xs; - - &:hover { - background-color: cssUtils.$sunk; + width: 100%; + justify-content: flex-start; + padding-left: cssUtils.$space-s; + padding-right: cssUtils.$space-s; + + &:focus, + &:focus-visible { + outline: none; + box-shadow: none; } } -.openui-bottom-tray-thread-item-menu-icon { - color: cssUtils.$text-neutral-secondary; +@keyframes openui-bottom-tray-thread-menu-show { + from { + opacity: 0; + transform: scale(0.95); + } + + to { + opacity: 1; + transform: scale(1); + } } diff --git a/packages/react-ui/src/components/BottomTray/welcomeScreen.scss b/packages/react-ui/src/components/BottomTray/welcomeScreen.scss index 556062e27..e679c3988 100644 --- a/packages/react-ui/src/components/BottomTray/welcomeScreen.scss +++ b/packages/react-ui/src/components/BottomTray/welcomeScreen.scss @@ -3,7 +3,7 @@ .openui-bottom-tray-welcome-screen { display: flex; flex-direction: column; - gap: cssUtils.$space-l; + gap: cssUtils.$space-xl; height: 100%; justify-content: center; align-items: center; @@ -29,13 +29,13 @@ display: flex; flex-direction: column; align-items: center; - gap: cssUtils.$space-xs; + gap: cssUtils.$space-s; text-align: center; } // Title/greeting text &__title { - @include cssUtils.typography(heading, small); + @include cssUtils.typography(heading, medium); color: cssUtils.$text-neutral-primary; margin: 0; } diff --git a/packages/react-ui/src/components/CopilotShell/Container.tsx b/packages/react-ui/src/components/CopilotShell/Container.tsx index 13e820d5c..89c8885a6 100644 --- a/packages/react-ui/src/components/CopilotShell/Container.tsx +++ b/packages/react-ui/src/components/CopilotShell/Container.tsx @@ -7,11 +7,22 @@ interface ContainerProps { logoUrl: string; agentName: string; className?: string; + showAssistantLogo?: boolean; } -export const Container = ({ children, logoUrl, agentName, className }: ContainerProps) => { +export const Container = ({ + children, + logoUrl, + agentName, + className, + showAssistantLogo = false, +}: ContainerProps) => { return ( - +
{children}
diff --git a/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx b/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx index b16c63f19..a080fc481 100644 --- a/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx +++ b/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx @@ -1,8 +1,10 @@ import { useThread } from "@openuidev/react-headless"; import clsx from "clsx"; import { ArrowUp, Lightbulb } from "lucide-react"; -import { Fragment, ReactNode } from "react"; +import { Fragment, ReactNode, isValidElement } from "react"; +import { useComposerState } from "../../hooks/useComposerState"; import { ConversationStarterIcon, ConversationStarterProps } from "../../types/ConversationStarter"; +import { Carousel, CarouselContent } from "../Carousel"; import { isChatEmpty } from "../_shared/utils"; import { Separator } from "../Separator"; @@ -25,6 +27,18 @@ const renderIcon = (icon: ConversationStarterIcon | undefined): ReactNode => { return icon; }; +const hasRenderableIcon = (icon: ReactNode): boolean => { + if (icon === null || icon === undefined || icon === false) { + return false; + } + + if (isValidElement(icon) && icon.type === Fragment) { + return Boolean(icon.props.children); + } + + return true; +}; + const ConversationStarterItem = ({ displayText, prompt, @@ -33,6 +47,7 @@ const ConversationStarterItem = ({ icon, }: ConversationStarterItemProps) => { const renderedIcon = renderIcon(icon); + const shouldRenderIcon = hasRenderableIcon(renderedIcon); if (variant === "short") { return ( @@ -41,7 +56,7 @@ const ConversationStarterItem = ({ className="openui-copilot-shell-conversation-starter-item-short" onClick={() => onClick(prompt)} > - {renderedIcon && ( + {shouldRenderIcon && ( {renderedIcon} @@ -61,7 +76,7 @@ const ConversationStarterItem = ({ onClick={() => onClick(prompt)} >
- {renderedIcon && ( + {shouldRenderIcon && ( {renderedIcon} @@ -93,10 +108,12 @@ export const ConversationStarter = ({ className, variant = "short", }: ConversationStarterContainerProps) => { + const { textContent } = useComposerState(); const processMessage = useThread((s) => s.processMessage); const isRunning = useThread((s) => s.isRunning); const messages = useThread((s) => s.messages); const isLoadingMessages = useThread((s) => s.isLoadingMessages); + const isDrafting = textContent.length > 0; const handleClick = (prompt: string) => { if (isRunning) return; @@ -115,11 +132,43 @@ export const ConversationStarter = ({ return null; } + if (variant === "short") { + return ( + + + {starters.map((item, index) => ( + + ))} + + + ); + } + return (
diff --git a/packages/react-ui/src/components/CopilotShell/Header.tsx b/packages/react-ui/src/components/CopilotShell/Header.tsx index ce4f0ccfc..469fe6e04 100644 --- a/packages/react-ui/src/components/CopilotShell/Header.tsx +++ b/packages/react-ui/src/components/CopilotShell/Header.tsx @@ -1,27 +1,60 @@ +import { useThreadList } from "@openuidev/react-headless"; import clsx from "clsx"; +import { SquarePen } from "lucide-react"; import { ReactNode } from "react"; +import { IconButton } from "../IconButton"; import { useShellStore } from "../_shared/store"; +import { ThreadListContainer } from "./ThreadListContainer"; + +export const CopilotNewChatButton = () => { + const switchToNewThread = useThreadList((s) => s.switchToNewThread); + + return ( + } + onClick={switchToNewThread} + variant="tertiary" + aria-label="New chat" + className="openui-copilot-shell-header-new-chat-button" + /> + ); +}; interface HeaderProps { className?: string; /** Custom content to render on the rightmost side of the logo container */ rightChildren?: ReactNode; + /** Hide the new chat button */ + hideNewChatButton?: boolean; + /** Hide the thread list container */ + hideThreadListContainer?: boolean; } -export const Header = ({ className, rightChildren }: HeaderProps) => { +export const Header = ({ + className, + rightChildren, + hideNewChatButton = false, + hideThreadListContainer = false, +}: HeaderProps) => { const { logoUrl, agentName } = useShellStore((state) => ({ logoUrl: state.logoUrl, agentName: state.agentName, })); + const shouldRenderActions = rightChildren || !hideThreadListContainer || !hideNewChatButton; + return (
Logo {agentName}
- {rightChildren && ( -
{rightChildren}
+ {shouldRenderActions && ( +
+ {rightChildren} + {!hideThreadListContainer && } + {!hideNewChatButton && } +
)}
); diff --git a/packages/react-ui/src/components/CopilotShell/Thread.tsx b/packages/react-ui/src/components/CopilotShell/Thread.tsx index 28f0bdcc6..5f585dfad 100644 --- a/packages/react-ui/src/components/CopilotShell/Thread.tsx +++ b/packages/react-ui/src/components/CopilotShell/Thread.tsx @@ -2,8 +2,10 @@ import type { AssistantMessage, Message, ToolMessage } from "@openuidev/react-he import { MessageProvider, useThread } from "@openuidev/react-headless"; import clsx from "clsx"; import React, { memo, useRef } from "react"; +import { ComposerStateProvider } from "../../hooks/useComposerState"; import { ScrollVariant, useScrollToBottom } from "../../hooks/useScrollToBottom"; import { ArtifactOverlay } from "../_shared/artifact"; +import { useShellStore } from "../_shared/store"; import type { AssistantMessageComponent, UserMessageComponent } from "../_shared/types"; import { MarkDownRenderer } from "../MarkDownRenderer"; import { MessageLoading as MessageLoadingComponent } from "../MessageLoading"; @@ -20,15 +22,17 @@ export const ThreadContainer = ({ const isLoadingMessages = useThread((s) => s.isLoadingMessages); return ( -
- {children} - -
+ +
+ {children} + +
+
); }; @@ -90,8 +94,20 @@ export const AssistantMessageContainer = ({ children?: React.ReactNode; className?: string; }) => { + const { logoUrl, showAssistantLogo } = useShellStore((store) => ({ + logoUrl: store.logoUrl, + showAssistantLogo: store.showAssistantLogo, + })); + return (
+ {showAssistantLogo && ( + Assistant + )}
{children}
); diff --git a/packages/react-ui/src/components/CopilotShell/ThreadListContainer.tsx b/packages/react-ui/src/components/CopilotShell/ThreadListContainer.tsx new file mode 100644 index 000000000..373abba80 --- /dev/null +++ b/packages/react-ui/src/components/CopilotShell/ThreadListContainer.tsx @@ -0,0 +1,118 @@ +import { useThreadList } from "@openuidev/react-headless"; +import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; +import clsx from "clsx"; +import { EllipsisVerticalIcon, MenuIcon, Trash2Icon } from "lucide-react"; +import { useEffect } from "react"; +import { Button } from "../Button"; +import { IconButton } from "../IconButton"; + +const ThreadItem = ({ + title, + isSelected, + onSelect, + onDelete, +}: { + title: string; + isSelected: boolean; + onSelect: () => void; + onDelete: () => void; +}) => { + return ( +
+ + + + } + aria-label={`More actions for ${title}`} + variant="tertiary" + size="extra-small" + className="openui-copilot-shell-thread-item-menu-trigger" + /> + + + + { + e.stopPropagation(); + onDelete(); + }} + > + + + + + +
+ ); +}; + +export const ThreadListContainer = () => { + const threads = useThreadList((s) => s.threads); + const selectedThreadId = useThreadList((s) => s.selectedThreadId); + const loadThreads = useThreadList((s) => s.loadThreads); + const selectThread = useThreadList((s) => s.selectThread); + const deleteThread = useThreadList((s) => s.deleteThread); + + useEffect(() => { + loadThreads(); + }, [loadThreads]); + + return ( + + + } + variant="tertiary" + aria-label="Thread list" + className="openui-copilot-shell-thread-list-trigger" + /> + + + +
All threads
+
+ {threads.map((thread) => ( + selectThread(thread.id)} + onDelete={() => deleteThread(thread.id)} + /> + ))} + {threads.length === 0 && ( +
No threads yet
+ )} +
+
+
+
+ ); +}; diff --git a/packages/react-ui/src/components/CopilotShell/components/Composer.tsx b/packages/react-ui/src/components/CopilotShell/components/Composer.tsx index dfa225135..0986a9ed2 100644 --- a/packages/react-ui/src/components/CopilotShell/components/Composer.tsx +++ b/packages/react-ui/src/components/CopilotShell/components/Composer.tsx @@ -68,7 +68,7 @@ export const Composer = ({ className, placeholder = "Type your message..." }: Co : } - size="medium" + size="extra-small" variant="primary" className="openui-copilot-shell-thread-composer__submit-button" /> diff --git a/packages/react-ui/src/components/CopilotShell/components/composer.scss b/packages/react-ui/src/components/CopilotShell/components/composer.scss index e19e7d912..e79da538d 100644 --- a/packages/react-ui/src/components/CopilotShell/components/composer.scss +++ b/packages/react-ui/src/components/CopilotShell/components/composer.scss @@ -1,19 +1,22 @@ @use "../../../cssUtils" as cssUtils; .openui-copilot-shell-thread-composer { + box-sizing: border-box; + flex-shrink: 0; width: 100%; - padding: cssUtils.$space-m cssUtils.$space-l cssUtils.$space-l; - background-color: cssUtils.$foreground; + margin: 0 0 cssUtils.$space-m; + padding: 0 cssUtils.$space-m; &__input-wrapper { + background-color: cssUtils.$foreground; border: 1px solid cssUtils.$border-interactive; - border-radius: cssUtils.$radius-l; - box-shadow: cssUtils.$shadow-s; + border-radius: cssUtils.$radius-3xl; + box-shadow: cssUtils.$shadow-m; overflow: clip; display: flex; flex-direction: column; gap: cssUtils.$space-s; - padding: cssUtils.$space-s; + padding: cssUtils.$space-s-m; } &__input { diff --git a/packages/react-ui/src/components/CopilotShell/conversationStarter.scss b/packages/react-ui/src/components/CopilotShell/conversationStarter.scss index 5d7ec88cf..b7712e332 100644 --- a/packages/react-ui/src/components/CopilotShell/conversationStarter.scss +++ b/packages/react-ui/src/components/CopilotShell/conversationStarter.scss @@ -3,13 +3,24 @@ // Container styles .openui-copilot-shell-conversation-starter { display: flex; - padding: 0 cssUtils.$space-l; + flex-shrink: 0; + width: 100%; + padding: 0 cssUtils.$space-m; margin-bottom: cssUtils.$space-s; + box-sizing: border-box; + max-height: 480px; + overflow: hidden; + opacity: 1; + transform: translateY(0); + transition: + opacity 0.2s ease, + transform 0.2s ease, + max-height 0.2s ease, + margin-bottom 0.2s ease; // Short variant - horizontal pill buttons &--short { - flex-direction: column; - gap: cssUtils.$space-s; + display: block; } // Long variant - vertical list items with separators @@ -22,18 +33,31 @@ &__separator { padding: cssUtils.$space-3xs cssUtils.$space-xs; } + + &__carousel-content { + gap: cssUtils.$space-xs; + } + + &--hidden { + opacity: 0; + transform: translateY(-4px); + max-height: 0; + margin-bottom: 0; + pointer-events: none; + } } // Short variant item (pill-style buttons) .openui-copilot-shell-conversation-starter-item-short { display: flex; - align-items: flex-start; + align-items: center; gap: cssUtils.$space-xs; width: fit-content; - padding: cssUtils.$space-s cssUtils.$space-m; + flex: 0 0 auto; + padding: cssUtils.$space-s cssUtils.$space-s-m; background-color: cssUtils.$foreground; - border: 1px solid cssUtils.$border-default; - border-radius: cssUtils.$radius-m; + border: 1px solid cssUtils.$border-interactive; + border-radius: cssUtils.$radius-l; cursor: pointer; transition: all 0.15s ease; @include cssUtils.typography(body, small); @@ -43,29 +67,34 @@ // Icon container &__icon { display: flex; - align-items: flex-start; + align-items: center; justify-content: center; flex-shrink: 0; - padding-top: cssUtils.$space-3xs; color: cssUtils.$text-neutral-primary; + + & > svg { + width: 14px; + height: 14px; + } } // Text &__text { flex: 1; + white-space: nowrap; } &:not(:disabled):hover { - background-color: cssUtils.$sunk; + background-color: cssUtils.$highlight; border-color: cssUtils.$border-interactive-emphasis; } &:not(:disabled):active { - background-color: cssUtils.$sunk; + background-color: cssUtils.$highlight-subtle; } @media (max-width: 480px) { - padding: cssUtils.$space-xs cssUtils.$space-s; + padding: cssUtils.$space-s cssUtils.$space-s-m; } } @@ -75,10 +104,10 @@ align-items: center; gap: cssUtils.$space-000; width: 100%; - padding: cssUtils.$space-xs cssUtils.$space-s; + padding: cssUtils.$space-xs cssUtils.$space-s-m; background-color: transparent; border: none; - border-radius: cssUtils.$radius-m; + border-radius: cssUtils.$radius-l; cursor: pointer; transition: background-color 0.15s ease; @include cssUtils.typography(body, default); @@ -90,7 +119,7 @@ &__content { display: flex; align-items: flex-start; - gap: cssUtils.$space-s; + gap: cssUtils.$space-xs; flex: 1; min-width: 0; } @@ -116,9 +145,11 @@ // Arrow icon (shown on hover) &__arrow { display: flex; + align-self: flex-start; align-items: center; justify-content: center; flex-shrink: 0; + padding-top: cssUtils.$space-2xs; opacity: 0; transition: opacity 0.15s ease; color: cssUtils.$text-neutral-primary; diff --git a/packages/react-ui/src/components/CopilotShell/copilotShell.scss b/packages/react-ui/src/components/CopilotShell/copilotShell.scss index 1b1aa246c..c77b040f9 100644 --- a/packages/react-ui/src/components/CopilotShell/copilotShell.scss +++ b/packages/react-ui/src/components/CopilotShell/copilotShell.scss @@ -3,6 +3,7 @@ @use "./components/composer.scss"; @use "./header.scss"; @use "./conversationStarter.scss"; +@use "./threadList.scss"; @use "./welcomeScreen.scss"; .openui-copilot-shell-container { diff --git a/packages/react-ui/src/components/CopilotShell/header.scss b/packages/react-ui/src/components/CopilotShell/header.scss index 416938640..0faa6d430 100644 --- a/packages/react-ui/src/components/CopilotShell/header.scss +++ b/packages/react-ui/src/components/CopilotShell/header.scss @@ -4,7 +4,7 @@ display: flex; align-items: center; justify-content: space-between; - padding: cssUtils.$space-m cssUtils.$space-l; + padding: cssUtils.$space-m; border-bottom: 1px solid cssUtils.$border-default; background-color: cssUtils.$foreground; } @@ -13,7 +13,6 @@ display: flex; align-items: center; gap: cssUtils.$space-s; - @include cssUtils.typography(title, medium); } .openui-copilot-shell-header-logo { @@ -23,10 +22,11 @@ } .openui-copilot-shell-header-agent-name { - @include cssUtils.typography(title, medium); + @include cssUtils.typography(label, small); color: cssUtils.$text-neutral-primary; } +.openui-copilot-shell-header-actions, .openui-copilot-shell-header-right-content { display: flex; align-items: center; diff --git a/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx b/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx index 260bc1b9a..c20773b94 100644 --- a/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx +++ b/packages/react-ui/src/components/CopilotShell/stories/Shell.stories.tsx @@ -44,17 +44,17 @@ export default { const SAMPLE_STARTERS = [ { - displayText: "Tell me about my portfolio", + displayText: "My portfolio", prompt: "Tell me about the latest stock market trends and how they affect my portfolio", icon: , }, { - displayText: "Who is the president of Venezuela and where is he currently located?", + displayText: "Venezuela president", prompt: "Who is the president of Venezuela and where is he currently located?", // icon undefined = shows default lightbulb }, { - displayText: "Tell me about major stock (no icon)", + displayText: "Major stocks", prompt: "Tell me about major stock", icon: <>, // Empty fragment = no icon }, diff --git a/packages/react-ui/src/components/CopilotShell/thread.scss b/packages/react-ui/src/components/CopilotShell/thread.scss index 12b1c29ce..9d79c7f17 100644 --- a/packages/react-ui/src/components/CopilotShell/thread.scss +++ b/packages/react-ui/src/components/CopilotShell/thread.scss @@ -13,13 +13,39 @@ flex: 1; position: relative; overflow: hidden; + + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: cssUtils.$space-xl; + background: linear-gradient(to bottom, cssUtils.$background 0%, transparent 100%); + pointer-events: none; + z-index: 1; + } + + &::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: cssUtils.$space-xl; + background: linear-gradient(to top, cssUtils.$background 0%, transparent 100%); + pointer-events: none; + z-index: 1; + } } .openui-copilot-shell-thread-scroll-area { width: 100%; height: 100%; overflow: auto; - padding: cssUtils.$space-l; + padding: cssUtils.$space-m-l; + scroll-padding-top: cssUtils.$space-m-l; + &--user-message-anchor { & .openui-copilot-shell-thread-messages > *:last-child { min-height: calc(-200px + 100dvh); @@ -28,6 +54,8 @@ } .openui-copilot-shell-thread-messages { + width: 100%; + max-width: 880px; margin: 0 auto; display: flex; flex-direction: column; @@ -36,6 +64,8 @@ .openui-copilot-shell-thread-message-assistant { width: 100%; + display: flex; + gap: cssUtils.$space-s; overflow: hidden; &__content { @@ -47,6 +77,13 @@ flex-grow: 1; } + &__logo { + width: 32px; + height: 32px; + border-radius: cssUtils.$radius-m; + flex-shrink: 0; + } + &__text { @include cssUtils.typography(primary, default); color: cssUtils.$text-neutral-primary; @@ -59,10 +96,10 @@ &__content { @include cssUtils.typography(primary, default); - padding: cssUtils.$space-m cssUtils.$space-l; + padding: cssUtils.$space-s cssUtils.$space-m; background-color: cssUtils.$chat-user-response-bg; color: cssUtils.$chat-user-response-text; - border-radius: cssUtils.$radius-2xl; + border-radius: cssUtils.$radius-xl; overflow-wrap: break-word; max-width: 100%; height: fit-content; diff --git a/packages/react-ui/src/components/CopilotShell/threadList.scss b/packages/react-ui/src/components/CopilotShell/threadList.scss new file mode 100644 index 000000000..95a5bd572 --- /dev/null +++ b/packages/react-ui/src/components/CopilotShell/threadList.scss @@ -0,0 +1,143 @@ +@use "../../cssUtils" as cssUtils; + +.openui-copilot-shell-thread-list-dropdown { + display: flex; + flex-direction: column; + min-width: 240px; + max-width: 320px; + max-height: 296px; + padding: cssUtils.$space-s; + border: 1px solid cssUtils.$border-default; + border-radius: cssUtils.$radius-2xl; + background-color: cssUtils.$foreground; + box-shadow: cssUtils.$shadow-l; + z-index: 9999; + overflow: hidden; +} + +.openui-copilot-shell-thread-list-header { + @include cssUtils.typography(label, small); + color: cssUtils.$text-neutral-tertiary; + padding: cssUtils.$space-xs cssUtils.$space-s; + padding-bottom: cssUtils.$space-s; +} + +.openui-copilot-shell-thread-list-items { + display: flex; + flex-direction: column; + gap: cssUtils.$space-2xs; + overflow-y: auto; +} + +.openui-copilot-shell-thread-list-empty { + @include cssUtils.typography(body, small); + color: cssUtils.$text-neutral-secondary; + padding: cssUtils.$space-m; + text-align: center; +} + +.openui-copilot-shell-thread-list-trigger { + &[data-state="open"], + &[aria-expanded="true"] { + background-color: cssUtils.$highlight; + border-color: cssUtils.$border-interactive; + } +} + +.openui-copilot-shell-thread-item { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + border-radius: calc(cssUtils.$radius-s + 1px); + border: 1px solid transparent; + transition: all 0.12s ease; + + &--selected { + background-color: cssUtils.$highlight; + } + + &:hover { + background-color: cssUtils.$highlight; + + .openui-copilot-shell-thread-item-menu-trigger { + opacity: 1; + } + } + + &:has(.openui-copilot-shell-thread-item-menu-trigger[data-state="open"]) { + background-color: cssUtils.$highlight; + + .openui-copilot-shell-thread-item-menu-trigger { + opacity: 1; + } + } +} + +.openui-copilot-shell-thread-item-title { + @include cssUtils.button-reset; + @include cssUtils.typography(body, small); + color: cssUtils.$text-neutral-primary; + padding: calc(cssUtils.$space-xs - 1.75px) cssUtils.$space-s; + width: 100%; + text-align: left; + cursor: pointer; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.openui-copilot-shell-thread-item-menu-trigger { + opacity: 0; + + &:focus-visible { + opacity: 1; + } + + &[data-state="open"] { + opacity: 1; + background-color: cssUtils.$highlight; + border-color: cssUtils.$border-interactive; + } +} + +.openui-copilot-shell-thread-item-menu { + box-sizing: border-box; + position: relative; + z-index: 99999; + min-width: 160px; + overflow: hidden; + border: 1px solid cssUtils.$border-default; + border-radius: cssUtils.$radius-xl; + background-color: cssUtils.$foreground; + box-shadow: cssUtils.$shadow-m; + color: cssUtils.$text-neutral-primary; + transform-origin: top center; + animation: openui-copilot-shell-thread-menu-show 0.18s ease-out forwards; + padding: 4px; +} + +.openui-copilot-shell-thread-item-menu-action { + width: 100%; + justify-content: flex-start; + padding-left: cssUtils.$space-s; + padding-right: cssUtils.$space-s; + + &:focus, + &:focus-visible { + outline: none; + box-shadow: none; + } +} + +@keyframes openui-copilot-shell-thread-menu-show { + from { + opacity: 0; + transform: scale(0.95); + } + + to { + opacity: 1; + transform: scale(1); + } +} diff --git a/packages/react-ui/src/components/CopilotShell/welcomeScreen.scss b/packages/react-ui/src/components/CopilotShell/welcomeScreen.scss index 443cf7ff5..61a135941 100644 --- a/packages/react-ui/src/components/CopilotShell/welcomeScreen.scss +++ b/packages/react-ui/src/components/CopilotShell/welcomeScreen.scss @@ -3,7 +3,7 @@ .openui-copilot-shell-welcome-screen { display: flex; flex-direction: column; - gap: cssUtils.$space-l; + gap: cssUtils.$space-xl; height: 100%; justify-content: center; align-items: center; @@ -29,13 +29,13 @@ display: flex; flex-direction: column; align-items: center; - gap: cssUtils.$space-xs; + gap: cssUtils.$space-s; text-align: center; } // Title/greeting text &__title { - @include cssUtils.typography(heading, small); + @include cssUtils.typography(heading, medium); color: cssUtils.$text-neutral-primary; margin: 0; } diff --git a/packages/react-ui/src/components/OpenUIChat/ComposedBottomTray.tsx b/packages/react-ui/src/components/OpenUIChat/ComposedBottomTray.tsx index 148fb6fc0..7ea231ab1 100644 --- a/packages/react-ui/src/components/OpenUIChat/ComposedBottomTray.tsx +++ b/packages/react-ui/src/components/OpenUIChat/ComposedBottomTray.tsx @@ -72,6 +72,7 @@ const ConversationStartersRenderer = ({ const BottomTrayInner = ({ logoUrl = "https://www.openui.com/favicon.svg", agentName = "My Agent", + showAssistantLogo = false, messageLoading: MessageLoadingComponent = MessageLoading, scrollVariant = "user-message-anchor", isOpen: controlledIsOpen, @@ -108,7 +109,12 @@ const BottomTrayInner = ({ ) : null} - +
handleOpenChange(false)} diff --git a/packages/react-ui/src/components/OpenUIChat/ComposedCopilot.tsx b/packages/react-ui/src/components/OpenUIChat/ComposedCopilot.tsx index f856b439a..83fcefbdd 100644 --- a/packages/react-ui/src/components/OpenUIChat/ComposedCopilot.tsx +++ b/packages/react-ui/src/components/OpenUIChat/ComposedCopilot.tsx @@ -67,6 +67,7 @@ interface CopilotSpecificProps extends SharedChatUIProps { const CopilotInner = ({ logoUrl = "https://www.openui.com/favicon.svg", agentName = "My Agent", + showAssistantLogo = false, messageLoading: MessageLoadingComponent = MessageLoading, scrollVariant = "user-message-anchor", welcomeMessage, @@ -82,7 +83,7 @@ const CopilotInner = ({ ) : null; return ( - +
+ diff --git a/packages/react-ui/src/components/OpenUIChat/types.ts b/packages/react-ui/src/components/OpenUIChat/types.ts index c76857581..6708d0078 100644 --- a/packages/react-ui/src/components/OpenUIChat/types.ts +++ b/packages/react-ui/src/components/OpenUIChat/types.ts @@ -92,6 +92,7 @@ export type ComposerComponent = React.ComponentType; export interface SharedChatUIProps { logoUrl?: string; agentName?: string; + showAssistantLogo?: boolean; messageLoading?: React.ComponentType; scrollVariant?: ScrollVariant; welcomeMessage?: WelcomeMessageConfig; diff --git a/packages/react-ui/src/components/Shell/Container.tsx b/packages/react-ui/src/components/Shell/Container.tsx index 728b80e38..bf2feb314 100644 --- a/packages/react-ui/src/components/Shell/Container.tsx +++ b/packages/react-ui/src/components/Shell/Container.tsx @@ -9,9 +9,16 @@ interface ContainerProps { logoUrl: string; agentName: string; className?: string; + showAssistantLogo?: boolean; } -export const Container = ({ children, logoUrl, agentName, className }: ContainerProps) => { +export const Container = ({ + children, + logoUrl, + agentName, + className, + showAssistantLogo = false, +}: ContainerProps) => { const ref = useRef(null); const { width } = useElementSize({ ref }) || {}; // TODO: revisit this logic @@ -20,7 +27,11 @@ export const Container = ({ children, logoUrl, agentName, className }: Container const layout = isMobile ? "mobile" : isFullScreen ? "fullscreen" : "tray"; return ( - +
{ return icon; }; +const hasRenderableIcon = (icon: ReactNode): boolean => { + if (icon === null || icon === undefined || icon === false) { + return false; + } + + if (isValidElement(icon) && icon.type === Fragment) { + return Boolean(icon.props.children); + } + + return true; +}; + const ConversationStarterItem = ({ displayText, prompt, @@ -33,6 +47,7 @@ const ConversationStarterItem = ({ icon, }: ConversationStarterItemProps) => { const renderedIcon = renderIcon(icon); + const shouldRenderIcon = hasRenderableIcon(renderedIcon); if (variant === "short") { return ( @@ -41,7 +56,7 @@ const ConversationStarterItem = ({ className="openui-shell-conversation-starter-item-short" onClick={() => onClick(prompt)} > - {renderedIcon && ( + {shouldRenderIcon && ( {renderedIcon} )} {displayText} @@ -57,7 +72,7 @@ const ConversationStarterItem = ({ onClick={() => onClick(prompt)} >
- {renderedIcon && ( + {shouldRenderIcon && ( {renderedIcon} )} {displayText} @@ -85,10 +100,12 @@ export const ConversationStarter = ({ className, variant = "short", }: ConversationStarterContainerProps) => { + const { textContent } = useComposerState(); const processMessage = useThread((s) => s.processMessage); const isRunning = useThread((s) => s.isRunning); const messages = useThread((s) => s.messages); const isLoadingMessages = useThread((s) => s.isLoadingMessages); + const isDrafting = textContent.length > 0; const handleClick = (prompt: string) => { if (isRunning) return; @@ -107,11 +124,43 @@ export const ConversationStarter = ({ return null; } + if (variant === "short") { + return ( + + + {starters.map((item, index) => ( + + ))} + + + ); + } + return (
diff --git a/packages/react-ui/src/components/Shell/Thread.tsx b/packages/react-ui/src/components/Shell/Thread.tsx index d1dd8f883..8a36b155e 100644 --- a/packages/react-ui/src/components/Shell/Thread.tsx +++ b/packages/react-ui/src/components/Shell/Thread.tsx @@ -3,6 +3,7 @@ import { MessageProvider, useActiveArtifact, useThread } from "@openuidev/react- import clsx from "clsx"; import React, { memo, useRef } from "react"; import { useLayoutContext } from "../../context/LayoutContext"; +import { ComposerStateProvider } from "../../hooks/useComposerState"; import { ScrollVariant, useScrollToBottom } from "../../hooks/useScrollToBottom"; import { separateContentAndContext } from "../../utils/contentParser"; import { ArtifactOverlay, ArtifactPortalTarget } from "../_shared/artifact"; @@ -48,46 +49,48 @@ export const ThreadContainer = ({ }); return ( -
-
- {/* Chat panel - always visible */} -
- {children} - {isMobile && } -
+ +
+
+ {/* Chat panel - always visible */} +
+ {children} + {isMobile && } +
- {/* Desktop only: Resizable separator and artifact panel */} - {!isMobile && isArtifactActive && ( - <> - -
- -
- - )} + {/* Desktop only: Resizable separator and artifact panel */} + {!isMobile && isArtifactActive && ( + <> + +
+ +
+ + )} +
-
+ ); }; @@ -149,13 +152,24 @@ export const AssistantMessageContainer = ({ children?: React.ReactNode; className?: string; }) => { - const { logoUrl } = useShellStore((store) => ({ + const { logoUrl, showAssistantLogo } = useShellStore((store) => ({ logoUrl: store.logoUrl, + showAssistantLogo: store.showAssistantLogo, })); return ( -
- Assistant +
+ {showAssistantLogo && ( + Assistant + )}
{children}
); diff --git a/packages/react-ui/src/components/Shell/ThreadList.tsx b/packages/react-ui/src/components/Shell/ThreadList.tsx index 4dd4434ba..e8d863aba 100644 --- a/packages/react-ui/src/components/Shell/ThreadList.tsx +++ b/packages/react-ui/src/components/Shell/ThreadList.tsx @@ -5,8 +5,9 @@ import clsx from "clsx"; import { EllipsisVerticalIcon, Trash2Icon } from "lucide-react"; import { Fragment, useEffect } from "react"; import { useLayoutContext } from "../../context/LayoutContext"; -import { Button } from "../Button"; import { useShellStore } from "../_shared/store"; +import { Button } from "../Button"; +import { IconButton } from "../IconButton"; import { useTheme } from "../ThemeProvider"; export const ThreadButton = ({ @@ -51,9 +52,13 @@ export const ThreadButton = ({ - + } + aria-label={`More actions for ${title}`} + variant="tertiary" + size="extra-small" + className="openui-shell-thread-button-dropdown-trigger" + /> { const { className, starters = [], starterVariant = "long" } = props; + const { textContent } = useComposerState(); const messages = useThread((s) => s.messages); const isLoadingMessages = useThread((s) => s.isLoadingMessages); + const isDrafting = textContent.length > 0; // Only show when there are no messages if (!isChatEmpty({ isLoadingMessages, messages })) { @@ -117,13 +120,21 @@ export const WelcomeScreen = (props: WelcomeScreenProps) => { )}
{/* Desktop-only welcome composer */} -
+
{/* Desktop-only conversation starters */} {starters.length > 0 && ( -
+
)} diff --git a/packages/react-ui/src/components/Shell/conversationStarter.scss b/packages/react-ui/src/components/Shell/conversationStarter.scss index 5f16790de..493901b87 100644 --- a/packages/react-ui/src/components/Shell/conversationStarter.scss +++ b/packages/react-ui/src/components/Shell/conversationStarter.scss @@ -8,12 +8,20 @@ $center-align-spacing: calc(32px + cssUtils.$space-s); max-width: 880px; margin: 0 auto; padding: 0 $center-align-spacing cssUtils.$space-m; + box-sizing: border-box; + max-height: 480px; + overflow: hidden; + opacity: 1; + transform: translateY(0); + transition: + opacity 0.2s ease, + transform 0.2s ease, + max-height 0.2s ease, + padding-bottom 0.2s ease; // Short variant - horizontal wrapping buttons &--short { - display: flex; - flex-wrap: wrap; - gap: cssUtils.$space-xs; + display: block; } // Long variant - vertical stacked list with separators @@ -28,6 +36,10 @@ $center-align-spacing: calc(32px + cssUtils.$space-s); padding: cssUtils.$space-3xs cssUtils.$space-xs; } + &__carousel-content { + gap: cssUtils.$space-xs; + } + // Mobile adjustments .openui-shell-container--mobile & { padding: 0 cssUtils.$space-l; @@ -38,6 +50,14 @@ $center-align-spacing: calc(32px + cssUtils.$space-s); padding-left: 0; padding-right: cssUtils.$space-m; } + + &--hidden { + opacity: 0; + transform: translateY(-4px); + max-height: 0; + padding-bottom: 0; + pointer-events: none; + } } // Short variant item (pill-style buttons that wrap) @@ -46,9 +66,10 @@ $center-align-spacing: calc(32px + cssUtils.$space-s); align-items: center; gap: cssUtils.$space-xs; width: fit-content; + flex: 0 0 auto; padding: cssUtils.$space-s cssUtils.$space-s-m; background-color: cssUtils.$foreground; - border: 1px solid cssUtils.$border-default; + border: 1px solid cssUtils.$border-interactive; border-radius: cssUtils.$radius-l; cursor: pointer; transition: all 0.15s ease; @@ -137,9 +158,11 @@ $center-align-spacing: calc(32px + cssUtils.$space-s); // Arrow icon (shown on hover) &__arrow { display: flex; + align-self: flex-start; align-items: center; justify-content: center; flex-shrink: 0; + padding-top: cssUtils.$space-2xs; opacity: 0; transition: opacity 0.15s ease; color: cssUtils.$text-neutral-primary; diff --git a/packages/react-ui/src/components/Shell/thread.scss b/packages/react-ui/src/components/Shell/thread.scss index 9d20d2fec..cd822485d 100644 --- a/packages/react-ui/src/components/Shell/thread.scss +++ b/packages/react-ui/src/components/Shell/thread.scss @@ -101,6 +101,30 @@ $center-align-spacing: calc(32px + cssUtils.$space-s); position: relative; overflow: hidden; + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: cssUtils.$space-xl; + background: linear-gradient(to bottom, cssUtils.$background 0%, transparent 100%); + pointer-events: none; + z-index: 1; + } + + &::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: cssUtils.$space-xl; + background: linear-gradient(to top, cssUtils.$background 0%, transparent 100%); + pointer-events: none; + z-index: 1; + } + // When WelcomeScreen is present, don't grow so WelcomeScreen can take the space .openui-shell-thread-container:has(.openui-shell-welcome-screen) & { flex-grow: 0; @@ -177,6 +201,10 @@ $center-align-spacing: calc(32px + cssUtils.$space-s); padding-right: $center-align-spacing; overflow: hidden; + &--without-logo { + padding-left: $center-align-spacing; + } + .openui-shell-container--mobile & { padding: 0; } @@ -235,10 +263,10 @@ $center-align-spacing: calc(32px + cssUtils.$space-s); &__content { @include cssUtils.typography(primary, default); - padding: cssUtils.$space-m cssUtils.$space-l; + padding: cssUtils.$space-s cssUtils.$space-m; background-color: cssUtils.$chat-user-response-bg; color: cssUtils.$chat-user-response-text; - border-radius: cssUtils.$radius-2xl; + border-radius: cssUtils.$radius-xl; overflow-wrap: break-word; max-width: 100%; height: fit-content; diff --git a/packages/react-ui/src/components/Shell/threadlist.scss b/packages/react-ui/src/components/Shell/threadlist.scss index 569405e32..2edfcc76f 100644 --- a/packages/react-ui/src/components/Shell/threadlist.scss +++ b/packages/react-ui/src/components/Shell/threadlist.scss @@ -38,6 +38,14 @@ opacity: 1; } } + + &:has(.openui-shell-thread-button-dropdown-trigger[data-state="open"]) { + background-color: cssUtils.$highlight; + + .openui-shell-thread-button-dropdown-trigger { + opacity: 1; + } + } } .openui-shell-thread-button-title { @@ -54,22 +62,20 @@ } .openui-shell-thread-button-dropdown-trigger { - @include cssUtils.button-reset; - outline: none; - @include cssUtils.typography(body, small); - color: cssUtils.$text-neutral-primary; - padding: calc(cssUtils.$space-s - 1.75px) cssUtils.$space-s; - flex-shrink: 0; - min-height: 28px; - cursor: pointer; opacity: 0; .openui-shell-container--mobile & { opacity: 1; } + &:focus-visible { + opacity: 1; + } + &[data-state="open"] { opacity: 1; + background-color: cssUtils.$highlight; + border-color: cssUtils.$border-interactive; } } diff --git a/packages/react-ui/src/components/Shell/welcomeScreen.scss b/packages/react-ui/src/components/Shell/welcomeScreen.scss index ece74b596..798f26e91 100644 --- a/packages/react-ui/src/components/Shell/welcomeScreen.scss +++ b/packages/react-ui/src/components/Shell/welcomeScreen.scss @@ -31,7 +31,7 @@ $center-align-spacing: calc(32px + cssUtils.$space-s); display: flex; flex-direction: column; align-items: center; - gap: cssUtils.$space-l; + gap: cssUtils.$space-xl; } // Image container - minimal wrapper, styling handled by consumer @@ -55,13 +55,13 @@ $center-align-spacing: calc(32px + cssUtils.$space-s); display: flex; flex-direction: column; align-items: center; - gap: cssUtils.$space-xs; + gap: cssUtils.$space-s; text-align: center; } // Title/greeting text &__title { - @include cssUtils.typography(heading, small); + @include cssUtils.typography(heading, medium); color: cssUtils.$text-neutral-primary; margin: 0; } @@ -96,6 +96,12 @@ $center-align-spacing: calc(32px + cssUtils.$space-s); // Desktop conversation starters container &__desktop-starters { width: 100%; + overflow: hidden; + opacity: 1; + transform: translateY(0); + transition: + opacity 0.2s ease, + transform 0.2s ease; // Hide on mobile - keep the external ConversationStarter on mobile .openui-shell-container--mobile & { @@ -107,10 +113,24 @@ $center-align-spacing: calc(32px + cssUtils.$space-s); padding: 0; } + // Keep the embedded starter block in flow while the welcome-screen wrapper hides it, + // otherwise the desktop composer shifts when drafting begins. + .openui-shell-conversation-starter--hidden { + max-height: 480px; + transform: translateY(0); + } + // Center align short variant buttons .openui-shell-conversation-starter--short { justify-content: center; } + + &--hidden { + opacity: 0; + transform: translateY(-4px); + visibility: hidden; + pointer-events: none; + } } // When welcome screen has the desktop composer variant diff --git a/packages/react-ui/src/components/ThemeProvider/defaultTheme.ts b/packages/react-ui/src/components/ThemeProvider/defaultTheme.ts index d3dc9bde9..714b4634d 100644 --- a/packages/react-ui/src/components/ThemeProvider/defaultTheme.ts +++ b/packages/react-ui/src/components/ThemeProvider/defaultTheme.ts @@ -245,8 +245,8 @@ const createColorTheme = ({ interactiveDestructiveAccentDisabled: withAlpha(swatch(dangerSwatch, 600), 0.4), // Chat - chatUserResponseBg: brandSolid, - chatUserResponseText: accentText, + chatUserResponseBg: withAlpha(brandSolid, 0.08), + chatUserResponseText: neutralPrimary, // Border borderDefault: withAlpha(overlayBase, isDark ? 0.06 : 0.06), diff --git a/packages/react-ui/src/components/_shared/store/store.tsx b/packages/react-ui/src/components/_shared/store/store.tsx index cab14e1c2..daf47436a 100644 --- a/packages/react-ui/src/components/_shared/store/store.tsx +++ b/packages/react-ui/src/components/_shared/store/store.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useEffect, useMemo } from "react"; +import { createContext, useContext, useEffect, useRef } from "react"; import { create } from "zustand"; import { useShallow } from "zustand/react/shallow"; @@ -6,19 +6,31 @@ interface ShellState { isSidebarOpen: boolean; agentName: string; logoUrl: string; + showAssistantLogo: boolean; setIsSidebarOpen: (isOpen: boolean) => void; setAgentName: (name: string) => void; setLogoUrl: (url: string) => void; + setShowAssistantLogo: (show: boolean) => void; } -export const createShellStore = ({ logoUrl, agentName }: { logoUrl: string; agentName: string }) => +export const createShellStore = ({ + logoUrl, + agentName, + showAssistantLogo, +}: { + logoUrl: string; + agentName: string; + showAssistantLogo: boolean; +}) => create((set) => ({ isSidebarOpen: true, agentName: agentName, logoUrl: logoUrl, + showAssistantLogo, setIsSidebarOpen: (isOpen: boolean) => set({ isSidebarOpen: isOpen }), setAgentName: (name: string) => set({ agentName: name }), setLogoUrl: (url: string) => set({ logoUrl: url }), + setShowAssistantLogo: (show: boolean) => set({ showAssistantLogo: show }), })); export const ShellStoreContext = createContext | null>(null); @@ -36,18 +48,25 @@ export const ShellStoreProvider = ({ children, agentName, logoUrl, + showAssistantLogo = false, }: { children: React.ReactNode; logoUrl: string; agentName: string; + showAssistantLogo?: boolean; }) => { - const shellStore = useMemo(() => createShellStore({ agentName, logoUrl }), []); + const shellStoreRef = useRef | null>(null); + if (!shellStoreRef.current) { + shellStoreRef.current = createShellStore({ agentName, logoUrl, showAssistantLogo }); + } + const shellStore = shellStoreRef.current; useEffect(() => { - const { setAgentName, setLogoUrl } = shellStore.getState(); + const { setAgentName, setLogoUrl, setShowAssistantLogo } = shellStore.getState(); setAgentName(agentName); setLogoUrl(logoUrl); - }, [agentName, logoUrl]); + setShowAssistantLogo(showAssistantLogo); + }, [agentName, logoUrl, shellStore, showAssistantLogo]); return {children}; }; diff --git a/packages/react-ui/src/hooks/useComposerState.ts b/packages/react-ui/src/hooks/useComposerState.ts index 902cb7fb4..f94285299 100644 --- a/packages/react-ui/src/hooks/useComposerState.ts +++ b/packages/react-ui/src/hooks/useComposerState.ts @@ -1,7 +1,43 @@ -import { useState } from "react"; +import { + createElement, + createContext, + Dispatch, + PropsWithChildren, + SetStateAction, + useContext, + useMemo, + useState, +} from "react"; -export const useComposerState = () => { +interface ComposerStateValue { + textContent: string; + setTextContent: Dispatch>; +} + +const ComposerStateContext = createContext(null); + +export const ComposerStateProvider = ({ children }: PropsWithChildren) => { const [textContent, setTextContent] = useState(""); - return { textContent, setTextContent }; + const value = useMemo( + () => ({ + textContent, + setTextContent, + }), + [textContent], + ); + + return createElement(ComposerStateContext.Provider, { value }, children); +}; + +export const useComposerState = () => { + const context = useContext(ComposerStateContext); + const [localTextContent, localSetTextContent] = useState(""); + + return ( + context ?? { + textContent: localTextContent, + setTextContent: localSetTextContent, + } + ); }; From 53092d895aacb30bb78917b97976c2f59dce3824 Mon Sep 17 00:00:00 2001 From: pr3khar Date: Fri, 10 Apr 2026 15:20:47 +0530 Subject: [PATCH 3/8] styling updates --- .../src/components/CopilotShell/header.scss | 2 -- .../react-ui/src/components/Shell/NewChatButton.tsx | 7 +++++-- .../react-ui/src/components/Shell/ThreadList.tsx | 2 +- .../react-ui/src/components/Shell/mobileHeader.scss | 1 - packages/react-ui/src/components/Shell/sidebar.scss | 6 +++++- .../react-ui/src/components/Shell/threadlist.scss | 13 +++++++++++++ 6 files changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/react-ui/src/components/CopilotShell/header.scss b/packages/react-ui/src/components/CopilotShell/header.scss index 0faa6d430..e1cc75a8a 100644 --- a/packages/react-ui/src/components/CopilotShell/header.scss +++ b/packages/react-ui/src/components/CopilotShell/header.scss @@ -5,8 +5,6 @@ align-items: center; justify-content: space-between; padding: cssUtils.$space-m; - border-bottom: 1px solid cssUtils.$border-default; - background-color: cssUtils.$foreground; } .openui-copilot-shell-header-logo-container { diff --git a/packages/react-ui/src/components/Shell/NewChatButton.tsx b/packages/react-ui/src/components/Shell/NewChatButton.tsx index c2a373cbb..da7d3f04c 100644 --- a/packages/react-ui/src/components/Shell/NewChatButton.tsx +++ b/packages/react-ui/src/components/Shell/NewChatButton.tsx @@ -1,6 +1,7 @@ import { useThreadList } from "@openuidev/react-headless"; import clsx from "clsx"; import { SquarePen } from "lucide-react"; +import { useLayoutContext } from "../../context/LayoutContext"; import { Button } from "../Button"; import { IconButton } from "../IconButton"; import { useShellStore } from "../_shared/store"; @@ -11,10 +12,12 @@ export const NewChatButton = ({ className }: { className?: string }) => { const { isSidebarOpen } = useShellStore((state) => ({ isSidebarOpen: state.isSidebarOpen, })); + const { layout } = useLayoutContext(); const sidebarVisualState = useOptionalSidebarVisualState(); const showExpandedButton = sidebarVisualState ? sidebarVisualState.visualState === "expanded" : isSidebarOpen; + const isMobile = layout === "mobile"; if (!showExpandedButton) { return ( @@ -22,7 +25,7 @@ export const NewChatButton = ({ className }: { className?: string }) => { icon={} onClick={switchToNewThread} variant="secondary" - size="small" + size={isMobile ? "medium" : "small"} className={clsx("openui-shell-new-chat-button_collapsed", className)} /> ); @@ -33,7 +36,7 @@ export const NewChatButton = ({ className }: { className?: string }) => { className={clsx("openui-shell-new-chat-button", className)} iconLeft={} variant="secondary" - size="small" + size={isMobile ? "medium" : "small"} onClick={switchToNewThread} > New Chat diff --git a/packages/react-ui/src/components/Shell/ThreadList.tsx b/packages/react-ui/src/components/Shell/ThreadList.tsx index e8d863aba..e15acbcaa 100644 --- a/packages/react-ui/src/components/Shell/ThreadList.tsx +++ b/packages/react-ui/src/components/Shell/ThreadList.tsx @@ -56,7 +56,7 @@ export const ThreadButton = ({ icon={} aria-label={`More actions for ${title}`} variant="tertiary" - size="extra-small" + size={layout === "mobile" ? "small" : "extra-small"} className="openui-shell-thread-button-dropdown-trigger" /> diff --git a/packages/react-ui/src/components/Shell/mobileHeader.scss b/packages/react-ui/src/components/Shell/mobileHeader.scss index a5487875b..4a34aa869 100644 --- a/packages/react-ui/src/components/Shell/mobileHeader.scss +++ b/packages/react-ui/src/components/Shell/mobileHeader.scss @@ -8,7 +8,6 @@ align-items: center; justify-content: space-between; padding: cssUtils.$space-m cssUtils.$space-l; - background-color: cssUtils.$foreground; } } diff --git a/packages/react-ui/src/components/Shell/sidebar.scss b/packages/react-ui/src/components/Shell/sidebar.scss index 51d0dc5f2..a65c4ad0c 100644 --- a/packages/react-ui/src/components/Shell/sidebar.scss +++ b/packages/react-ui/src/components/Shell/sidebar.scss @@ -3,7 +3,7 @@ $sidebar-width: 272px; $sidebar-padding: cssUtils.$space-m; $sidebar-mobile-width: 294px; -$sidebar-mobile-padding: cssUtils.$space-l; +$sidebar-mobile-padding: cssUtils.$space-m; $sidebar-fade-duration: 90ms; $sidebar-resize-duration: 160ms; $sidebar-motion-ease: cubic-bezier(0.22, 1, 0.36, 1); @@ -118,6 +118,10 @@ $sidebar-motion-ease: cubic-bezier(0.22, 1, 0.36, 1); flex-grow: 1; color: cssUtils.$text-neutral-primary; + .openui-shell-container--mobile & { + @include cssUtils.typography(label, default); + } + .openui-shell-sidebar-container--collapsed & { display: none; } diff --git a/packages/react-ui/src/components/Shell/threadlist.scss b/packages/react-ui/src/components/Shell/threadlist.scss index 2edfcc76f..bb10c7b74 100644 --- a/packages/react-ui/src/components/Shell/threadlist.scss +++ b/packages/react-ui/src/components/Shell/threadlist.scss @@ -14,6 +14,14 @@ color: cssUtils.$text-neutral-tertiary; @include cssUtils.typography(label, small); margin-bottom: cssUtils.$space-2xs; + + .openui-shell-container--mobile & { + @include cssUtils.typography(label, default); + } + + &:not(:first-child) { + margin-top: cssUtils.$space-m; + } } .openui-shell-thread-button { @@ -59,6 +67,11 @@ white-space: nowrap; text-overflow: ellipsis; overflow: hidden; + + .openui-shell-container--mobile & { + @include cssUtils.typography(body, default); + padding: calc(cssUtils.$space-s - 1.75px) cssUtils.$space-s; + } } .openui-shell-thread-button-dropdown-trigger { From 9ddfdb3c07dad38ff77edb8ee79c4c130365426e Mon Sep 17 00:00:00 2001 From: pr3khar Date: Mon, 20 Apr 2026 14:03:43 +0530 Subject: [PATCH 4/8] styling updates & fixes --- .../BottomTray/ConversationStarter.tsx | 7 ---- .../BottomTray/conversationStarter.scss | 7 +--- .../src/components/BottomTray/threadList.scss | 4 +-- .../CopilotShell/ConversationStarter.tsx | 7 ---- .../CopilotShell/conversationStarter.scss | 7 +--- .../components/CopilotShell/threadList.scss | 4 +-- .../components/Shell/ConversationStarter.tsx | 7 ---- .../react-ui/src/components/Shell/Sidebar.tsx | 7 ++-- .../components/Shell/conversationStarter.scss | 7 +--- .../src/components/Shell/mobileHeader.scss | 2 +- .../react-ui/src/components/Shell/shell.scss | 4 +++ .../src/components/Shell/sidebar.scss | 32 +++++++++++-------- .../src/components/Shell/threadlist.scss | 2 +- 13 files changed, 35 insertions(+), 62 deletions(-) diff --git a/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx b/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx index f2d350dab..d48d6fcce 100644 --- a/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx +++ b/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx @@ -6,7 +6,6 @@ import { useComposerState } from "../../hooks/useComposerState"; import { ConversationStarterIcon, ConversationStarterProps } from "../../types/ConversationStarter"; import { Carousel, CarouselContent } from "../Carousel"; import { isChatEmpty } from "../_shared/utils"; -import { Separator } from "../Separator"; export type ConversationStarterVariant = "short" | "long"; @@ -181,12 +180,6 @@ export const ConversationStarter = ({ onClick={handleClick} variant={variant} /> - {/* Add separator between items in long variant */} - {variant === "long" && index < starters.length - 1 && ( -
- -
- )} ))}
diff --git a/packages/react-ui/src/components/BottomTray/conversationStarter.scss b/packages/react-ui/src/components/BottomTray/conversationStarter.scss index 98e07b33a..417a20999 100644 --- a/packages/react-ui/src/components/BottomTray/conversationStarter.scss +++ b/packages/react-ui/src/components/BottomTray/conversationStarter.scss @@ -23,17 +23,12 @@ display: block; } - // Long variant - vertical list items with separators + // Long variant - vertical list items &--long { flex-direction: column; gap: cssUtils.$space-2xs; } - // Separator wrapper for long variant - &__separator { - padding: cssUtils.$space-3xs cssUtils.$space-xs; - } - &__carousel-content { gap: cssUtils.$space-xs; } diff --git a/packages/react-ui/src/components/BottomTray/threadList.scss b/packages/react-ui/src/components/BottomTray/threadList.scss index f2de6bc3c..fd576ddeb 100644 --- a/packages/react-ui/src/components/BottomTray/threadList.scss +++ b/packages/react-ui/src/components/BottomTray/threadList.scss @@ -7,7 +7,7 @@ min-width: 240px; max-width: 320px; max-height: 296px; - padding: cssUtils.$space-s; + padding: cssUtils.$space-xs; border: 1px solid cssUtils.$border-default; border-radius: cssUtils.$radius-2xl; background-color: cssUtils.$foreground; @@ -26,7 +26,7 @@ .openui-bottom-tray-thread-list-items { display: flex; flex-direction: column; - gap: cssUtils.$space-2xs; + gap: cssUtils.$space-3xs; overflow-y: auto; } diff --git a/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx b/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx index a080fc481..70ddf2e4b 100644 --- a/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx +++ b/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx @@ -6,7 +6,6 @@ import { useComposerState } from "../../hooks/useComposerState"; import { ConversationStarterIcon, ConversationStarterProps } from "../../types/ConversationStarter"; import { Carousel, CarouselContent } from "../Carousel"; import { isChatEmpty } from "../_shared/utils"; -import { Separator } from "../Separator"; export type ConversationStarterVariant = "short" | "long"; @@ -181,12 +180,6 @@ export const ConversationStarter = ({ onClick={handleClick} variant={variant} /> - {/* Add separator between items in long variant */} - {variant === "long" && index < starters.length - 1 && ( -
- -
- )} ))}
diff --git a/packages/react-ui/src/components/CopilotShell/conversationStarter.scss b/packages/react-ui/src/components/CopilotShell/conversationStarter.scss index b7712e332..b938c2178 100644 --- a/packages/react-ui/src/components/CopilotShell/conversationStarter.scss +++ b/packages/react-ui/src/components/CopilotShell/conversationStarter.scss @@ -23,17 +23,12 @@ display: block; } - // Long variant - vertical list items with separators + // Long variant - vertical list items &--long { flex-direction: column; gap: cssUtils.$space-2xs; } - // Separator wrapper for long variant - &__separator { - padding: cssUtils.$space-3xs cssUtils.$space-xs; - } - &__carousel-content { gap: cssUtils.$space-xs; } diff --git a/packages/react-ui/src/components/CopilotShell/threadList.scss b/packages/react-ui/src/components/CopilotShell/threadList.scss index 95a5bd572..2cdef6408 100644 --- a/packages/react-ui/src/components/CopilotShell/threadList.scss +++ b/packages/react-ui/src/components/CopilotShell/threadList.scss @@ -6,7 +6,7 @@ min-width: 240px; max-width: 320px; max-height: 296px; - padding: cssUtils.$space-s; + padding: cssUtils.$space-xs; border: 1px solid cssUtils.$border-default; border-radius: cssUtils.$radius-2xl; background-color: cssUtils.$foreground; @@ -25,7 +25,7 @@ .openui-copilot-shell-thread-list-items { display: flex; flex-direction: column; - gap: cssUtils.$space-2xs; + gap: cssUtils.$space-3xs; overflow-y: auto; } diff --git a/packages/react-ui/src/components/Shell/ConversationStarter.tsx b/packages/react-ui/src/components/Shell/ConversationStarter.tsx index e968f2fb4..625bbfa51 100644 --- a/packages/react-ui/src/components/Shell/ConversationStarter.tsx +++ b/packages/react-ui/src/components/Shell/ConversationStarter.tsx @@ -6,7 +6,6 @@ import { useComposerState } from "../../hooks/useComposerState"; import { ConversationStarterIcon, ConversationStarterProps } from "../../types/ConversationStarter"; import { Carousel, CarouselContent } from "../Carousel"; import { isChatEmpty } from "../_shared/utils"; -import { Separator } from "../Separator"; export type ConversationStarterVariant = "short" | "long"; @@ -173,12 +172,6 @@ export const ConversationStarter = ({ onClick={handleClick} variant={variant} /> - {/* Add separator between items in long variant */} - {variant === "long" && index < starters.length - 1 && ( -
- -
- )} ))}
diff --git a/packages/react-ui/src/components/Shell/Sidebar.tsx b/packages/react-ui/src/components/Shell/Sidebar.tsx index dd92f631f..5d3765683 100644 --- a/packages/react-ui/src/components/Shell/Sidebar.tsx +++ b/packages/react-ui/src/components/Shell/Sidebar.tsx @@ -1,6 +1,6 @@ import { useActiveArtifact } from "@openuidev/react-headless"; import clsx from "clsx"; -import { ArrowLeftFromLine, ArrowRightFromLine } from "lucide-react"; +import { PanelLeftClose, PanelLeftOpen } from "lucide-react"; import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react"; import { useLayoutContext } from "../../context/LayoutContext"; import { IconButton } from "../IconButton"; @@ -185,12 +185,11 @@ export const SidebarHeader = ({ {agentName}
{agentName}
: - } + icon={showExpandedIcon ? : } onClick={() => { setIsSidebarOpen(!isSidebarOpen); }} + aria-label={showExpandedIcon ? "Close sidebar" : "Open sidebar"} size="small" variant="tertiary" className="openui-shell-sidebar-header__toggle-button" diff --git a/packages/react-ui/src/components/Shell/conversationStarter.scss b/packages/react-ui/src/components/Shell/conversationStarter.scss index 493901b87..e3df48412 100644 --- a/packages/react-ui/src/components/Shell/conversationStarter.scss +++ b/packages/react-ui/src/components/Shell/conversationStarter.scss @@ -24,18 +24,13 @@ $center-align-spacing: calc(32px + cssUtils.$space-s); display: block; } - // Long variant - vertical stacked list with separators + // Long variant - vertical stacked list &--long { display: flex; flex-direction: column; gap: cssUtils.$space-2xs; } - // Separator wrapper for long variant - &__separator { - padding: cssUtils.$space-3xs cssUtils.$space-xs; - } - &__carousel-content { gap: cssUtils.$space-xs; } diff --git a/packages/react-ui/src/components/Shell/mobileHeader.scss b/packages/react-ui/src/components/Shell/mobileHeader.scss index 4a34aa869..9ab8b3a95 100644 --- a/packages/react-ui/src/components/Shell/mobileHeader.scss +++ b/packages/react-ui/src/components/Shell/mobileHeader.scss @@ -7,7 +7,7 @@ display: flex; align-items: center; justify-content: space-between; - padding: cssUtils.$space-m cssUtils.$space-l; + padding: cssUtils.$space-m-l; } } diff --git a/packages/react-ui/src/components/Shell/shell.scss b/packages/react-ui/src/components/Shell/shell.scss index 8cdc6c73e..e6f9f27a9 100644 --- a/packages/react-ui/src/components/Shell/shell.scss +++ b/packages/react-ui/src/components/Shell/shell.scss @@ -37,3 +37,7 @@ width: auto; } } + +.openui-shell-new-chat-button_collapsed { + align-self: center; +} diff --git a/packages/react-ui/src/components/Shell/sidebar.scss b/packages/react-ui/src/components/Shell/sidebar.scss index a65c4ad0c..7a651b13c 100644 --- a/packages/react-ui/src/components/Shell/sidebar.scss +++ b/packages/react-ui/src/components/Shell/sidebar.scss @@ -2,10 +2,14 @@ $sidebar-width: 272px; $sidebar-padding: cssUtils.$space-m; +$sidebar-collapsed-width: calc( + 32px + #{cssUtils.$space-m} + #{cssUtils.$space-m} + #{cssUtils.$space-3xs} + #{cssUtils.$space-3xs} +); $sidebar-mobile-width: 294px; -$sidebar-mobile-padding: cssUtils.$space-m; +$sidebar-mobile-padding: cssUtils.$space-m-l; $sidebar-fade-duration: 90ms; $sidebar-resize-duration: 160ms; +$sidebar-mobile-slide-duration: 400ms; $sidebar-motion-ease: cubic-bezier(0.22, 1, 0.36, 1); .openui-shell-sidebar-container { @@ -33,11 +37,11 @@ $sidebar-motion-ease: cubic-bezier(0.22, 1, 0.36, 1); z-index: 1000; background-color: cssUtils.$foreground; border-radius: 0; - transition: left $sidebar-resize-duration $sidebar-motion-ease; + transition: left $sidebar-mobile-slide-duration $sidebar-motion-ease; } &--collapsed { - width: 52px; + width: $sidebar-collapsed-width; transform: translateX(0); gap: cssUtils.$space-xs; cursor: e-resize; @@ -70,7 +74,6 @@ $sidebar-motion-ease: cubic-bezier(0.22, 1, 0.36, 1); &[data-sidebar-visual-state="collapsing"], &[data-sidebar-visual-state="expanding"] { - .openui-shell-sidebar-header__top-row, .openui-shell-sidebar-content, .openui-shell-sidebar-header > :not(.openui-shell-sidebar-header__top-row) { opacity: 0; @@ -103,14 +106,12 @@ $sidebar-motion-ease: cubic-bezier(0.22, 1, 0.36, 1); } &__logo { + display: block; width: 32px; height: 32px; margin-left: cssUtils.$space-3xs; border-radius: cssUtils.$radius-m; - transition: - width $sidebar-resize-duration $sidebar-motion-ease, - height $sidebar-resize-duration $sidebar-motion-ease, - opacity $sidebar-fade-duration $sidebar-motion-ease; + transition: opacity $sidebar-fade-duration $sidebar-motion-ease; } &__agent-name { @@ -128,18 +129,21 @@ $sidebar-motion-ease: cubic-bezier(0.22, 1, 0.36, 1); } &--collapsed { - align-items: center; + align-items: stretch; .openui-shell-sidebar-header__top-row { - display: grid; + display: block; + position: relative; min-height: 32px; - place-items: center; + width: 100%; } .openui-shell-sidebar-header__logo, .openui-shell-sidebar-header__toggle-button { - grid-area: 1 / 1; - margin-left: auto; + position: absolute; + top: 0; + left: cssUtils.$space-3xs; + margin-left: 0; } .openui-shell-sidebar-header__logo { @@ -187,6 +191,8 @@ $sidebar-motion-ease: cubic-bezier(0.22, 1, 0.36, 1); } .openui-shell-sidebar-header__toggle-button { + transition: opacity $sidebar-fade-duration $sidebar-motion-ease; + .openui-shell-container--mobile & { display: none; } diff --git a/packages/react-ui/src/components/Shell/threadlist.scss b/packages/react-ui/src/components/Shell/threadlist.scss index bb10c7b74..adc6a2444 100644 --- a/packages/react-ui/src/components/Shell/threadlist.scss +++ b/packages/react-ui/src/components/Shell/threadlist.scss @@ -3,7 +3,7 @@ .openui-shell-thread-list { display: flex; flex-direction: column; - gap: cssUtils.$space-2xs; + gap: cssUtils.$space-3xs; overflow: auto; margin-top: cssUtils.$space-m; } From 6e8fe00e5f53ef751c2a0767a8d4abcf076c67b8 Mon Sep 17 00:00:00 2001 From: pr3khar Date: Mon, 20 Apr 2026 14:41:51 +0530 Subject: [PATCH 5/8] build fixes --- packages/react-ui/src/components/Shell/sidebar.scss | 3 ++- packages/react-ui/src/hooks/useComposerState.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react-ui/src/components/Shell/sidebar.scss b/packages/react-ui/src/components/Shell/sidebar.scss index 7a651b13c..a5084a341 100644 --- a/packages/react-ui/src/components/Shell/sidebar.scss +++ b/packages/react-ui/src/components/Shell/sidebar.scss @@ -3,7 +3,8 @@ $sidebar-width: 272px; $sidebar-padding: cssUtils.$space-m; $sidebar-collapsed-width: calc( - 32px + #{cssUtils.$space-m} + #{cssUtils.$space-m} + #{cssUtils.$space-3xs} + #{cssUtils.$space-3xs} + 32px + #{cssUtils.$space-m} + #{cssUtils.$space-m} + #{cssUtils.$space-3xs} + + #{cssUtils.$space-3xs} ); $sidebar-mobile-width: 294px; $sidebar-mobile-padding: cssUtils.$space-m-l; diff --git a/packages/react-ui/src/hooks/useComposerState.ts b/packages/react-ui/src/hooks/useComposerState.ts index f94285299..938f60857 100644 --- a/packages/react-ui/src/hooks/useComposerState.ts +++ b/packages/react-ui/src/hooks/useComposerState.ts @@ -1,6 +1,6 @@ import { - createElement, createContext, + createElement, Dispatch, PropsWithChildren, SetStateAction, From 0e3909cf14cbf08ac6010ea9623c8e4406a1e005 Mon Sep 17 00:00:00 2001 From: pr3khar Date: Tue, 28 Apr 2026 13:55:39 +0530 Subject: [PATCH 6/8] updates --- packages/react-ui/src/components/Select/select.scss | 6 +++--- packages/react-ui/src/genui-lib/Select/index.tsx | 1 + packages/react-ui/src/genui-lib/Select/schema.ts | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/react-ui/src/components/Select/select.scss b/packages/react-ui/src/components/Select/select.scss index a596ecb65..d10f41ce5 100644 --- a/packages/react-ui/src/components/Select/select.scss +++ b/packages/react-ui/src/components/Select/select.scss @@ -120,7 +120,7 @@ .openui-select-label { box-sizing: border-box; padding: cssUtils.$space-3xs cssUtils.$space-xs; - @include cssUtils.typography(primary, default); + @include cssUtils.typography(body, default); color: cssUtils.$text-neutral-tertiary; } @@ -135,7 +135,7 @@ border-radius: cssUtils.$radius-s; padding-top: cssUtils.$space-2xs; padding-bottom: cssUtils.$space-2xs; - @include cssUtils.typography(primary, default); + @include cssUtils.typography(body, default); color: cssUtils.$text-neutral-primary; outline: 0; @@ -192,7 +192,7 @@ // Text content &-text { flex: 1; - @include cssUtils.typography(primary, default); + @include cssUtils.typography(body, default); color: cssUtils.$text-neutral-secondary; } diff --git a/packages/react-ui/src/genui-lib/Select/index.tsx b/packages/react-ui/src/genui-lib/Select/index.tsx index a47147775..d3a5ba06e 100644 --- a/packages/react-ui/src/genui-lib/Select/index.tsx +++ b/packages/react-ui/src/genui-lib/Select/index.tsx @@ -68,6 +68,7 @@ export const Select = defineComponent({ value={value} onValueChange={handleChange} disabled={isStreaming} + size={(props.size as "sm" | "md" | "lg" | undefined) ?? "md"} > diff --git a/packages/react-ui/src/genui-lib/Select/schema.ts b/packages/react-ui/src/genui-lib/Select/schema.ts index 7de8c6b67..dd300bb21 100644 --- a/packages/react-ui/src/genui-lib/Select/schema.ts +++ b/packages/react-ui/src/genui-lib/Select/schema.ts @@ -16,5 +16,6 @@ export function createSelectSchema(SelectItem: RefComponent) { placeholder: z.string().optional(), rules: rulesSchema, value: reactive(z.string().optional()), + size: z.enum(["sm", "md", "lg"]).optional(), }); } From b41d3e82793af484a0a5eff4448b9ff9e200baeb Mon Sep 17 00:00:00 2001 From: ankit-thesys Date: Wed, 13 May 2026 17:12:59 +0530 Subject: [PATCH 7/8] fix(react-ui): address functional foot-guns in shell updates - Sidebar: stop click propagation on toggle button and collapsed NewChatButton so toggle/new-chat intent always wins over the container's click-to-expand handler - Sidebar: collapse the two isMobile effects into one, using a previousIsMobileRef to detect viewport transitions and avoid the stale-closure race between the open-state setter and animation state machine - useComposerState: emit a one-shot dev-mode warning when the hook is used outside a ComposerStateProvider (silent fallback to local state otherwise breaks cross-component features like isDrafting) - ThreadListContainer (BottomTray, CopilotShell): apply portalThemeClassName to portaled DropdownMenu.Content so non-default themes propagate into the dropdown subtrees - ConversationStarter (Shell, BottomTray, CopilotShell): type isValidElement<{ children?: ReactNode }>(icon) so icon.props.children is accessible under React 19's stricter ReactElement typing Co-Authored-By: Claude Opus 4.7 (1M context) --- .../BottomTray/ConversationStarter.tsx | 2 +- .../BottomTray/ThreadListContainer.tsx | 7 ++++-- .../CopilotShell/ConversationStarter.tsx | 2 +- .../CopilotShell/ThreadListContainer.tsx | 7 ++++-- .../components/Shell/ConversationStarter.tsx | 2 +- .../src/components/Shell/NewChatButton.tsx | 5 +++- .../react-ui/src/components/Shell/Sidebar.tsx | 25 ++++++++++++------- .../react-ui/src/hooks/useComposerState.ts | 19 ++++++++++++++ 8 files changed, 52 insertions(+), 17 deletions(-) diff --git a/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx b/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx index d48d6fcce..2253fcde9 100644 --- a/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx +++ b/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx @@ -31,7 +31,7 @@ const hasRenderableIcon = (icon: ReactNode): boolean => { return false; } - if (isValidElement(icon) && icon.type === Fragment) { + if (isValidElement<{ children?: ReactNode }>(icon) && icon.type === Fragment) { return Boolean(icon.props.children); } diff --git a/packages/react-ui/src/components/BottomTray/ThreadListContainer.tsx b/packages/react-ui/src/components/BottomTray/ThreadListContainer.tsx index 39d420db9..1e43da432 100644 --- a/packages/react-ui/src/components/BottomTray/ThreadListContainer.tsx +++ b/packages/react-ui/src/components/BottomTray/ThreadListContainer.tsx @@ -5,6 +5,7 @@ import { EllipsisVerticalIcon, MenuIcon, Trash2Icon } from "lucide-react"; import { useEffect } from "react"; import { Button } from "../Button"; import { IconButton } from "../IconButton"; +import { useTheme } from "../ThemeProvider"; const ThreadItem = ({ title, @@ -17,6 +18,7 @@ const ThreadItem = ({ onSelect: () => void; onDelete: () => void; }) => { + const { portalThemeClassName } = useTheme(); return (
{ const loadThreads = useThreadList((s) => s.loadThreads); const selectThread = useThreadList((s) => s.selectThread); const deleteThread = useThreadList((s) => s.deleteThread); + const { portalThemeClassName } = useTheme(); useEffect(() => { loadThreads(); @@ -91,7 +94,7 @@ export const ThreadListContainer = () => { { return false; } - if (isValidElement(icon) && icon.type === Fragment) { + if (isValidElement<{ children?: ReactNode }>(icon) && icon.type === Fragment) { return Boolean(icon.props.children); } diff --git a/packages/react-ui/src/components/CopilotShell/ThreadListContainer.tsx b/packages/react-ui/src/components/CopilotShell/ThreadListContainer.tsx index 373abba80..5cd903695 100644 --- a/packages/react-ui/src/components/CopilotShell/ThreadListContainer.tsx +++ b/packages/react-ui/src/components/CopilotShell/ThreadListContainer.tsx @@ -5,6 +5,7 @@ import { EllipsisVerticalIcon, MenuIcon, Trash2Icon } from "lucide-react"; import { useEffect } from "react"; import { Button } from "../Button"; import { IconButton } from "../IconButton"; +import { useTheme } from "../ThemeProvider"; const ThreadItem = ({ title, @@ -17,6 +18,7 @@ const ThreadItem = ({ onSelect: () => void; onDelete: () => void; }) => { + const { portalThemeClassName } = useTheme(); return (
{ const loadThreads = useThreadList((s) => s.loadThreads); const selectThread = useThreadList((s) => s.selectThread); const deleteThread = useThreadList((s) => s.deleteThread); + const { portalThemeClassName } = useTheme(); useEffect(() => { loadThreads(); @@ -91,7 +94,7 @@ export const ThreadListContainer = () => { { return false; } - if (isValidElement(icon) && icon.type === Fragment) { + if (isValidElement<{ children?: ReactNode }>(icon) && icon.type === Fragment) { return Boolean(icon.props.children); } diff --git a/packages/react-ui/src/components/Shell/NewChatButton.tsx b/packages/react-ui/src/components/Shell/NewChatButton.tsx index da7d3f04c..f06fa7d5c 100644 --- a/packages/react-ui/src/components/Shell/NewChatButton.tsx +++ b/packages/react-ui/src/components/Shell/NewChatButton.tsx @@ -23,7 +23,10 @@ export const NewChatButton = ({ className }: { className?: string }) => { return ( } - onClick={switchToNewThread} + onClick={(e) => { + e.stopPropagation(); + switchToNewThread(); + }} variant="secondary" size={isMobile ? "medium" : "small"} className={clsx("openui-shell-new-chat-button_collapsed", className)} diff --git a/packages/react-ui/src/components/Shell/Sidebar.tsx b/packages/react-ui/src/components/Shell/Sidebar.tsx index 603d863d9..343b83cba 100644 --- a/packages/react-ui/src/components/Shell/Sidebar.tsx +++ b/packages/react-ui/src/components/Shell/Sidebar.tsx @@ -45,20 +45,13 @@ export const SidebarContainer = ({ isSidebarOpen ? "expanded" : "collapsed", ); const animationTimeoutsRef = useRef>>([]); + const previousIsMobileRef = useRef(null); const clearAnimationTimeouts = () => { animationTimeoutsRef.current.forEach((timeoutId) => clearTimeout(timeoutId)); animationTimeoutsRef.current = []; }; - useEffect(() => { - if (isMobile) { - setIsSidebarOpen(false); - } else { - setIsSidebarOpen(true); - } - }, [isMobile]); - useEffect(() => { return () => { clearAnimationTimeouts(); @@ -68,6 +61,19 @@ export const SidebarContainer = ({ useEffect(() => { clearAnimationTimeouts(); + const justSwitchedLayout = previousIsMobileRef.current !== isMobile; + previousIsMobileRef.current = isMobile; + + // On viewport breakpoint change, force sidebar open state and let the effect + // re-run with the new `isSidebarOpen` to drive the animation. + if (justSwitchedLayout) { + const targetOpen = !isMobile; + if (isSidebarOpen !== targetOpen) { + setIsSidebarOpen(targetOpen); + return; + } + } + if (isMobile) { setIsCollapsedLayout(!isSidebarOpen); setVisualState(isSidebarOpen ? "expanded" : "collapsed"); @@ -186,7 +192,8 @@ export const SidebarHeader = ({
{agentName}
: } - onClick={() => { + onClick={(e) => { + e.stopPropagation(); setIsSidebarOpen(!isSidebarOpen); }} size="small" diff --git a/packages/react-ui/src/hooks/useComposerState.ts b/packages/react-ui/src/hooks/useComposerState.ts index 938f60857..94de32968 100644 --- a/packages/react-ui/src/hooks/useComposerState.ts +++ b/packages/react-ui/src/hooks/useComposerState.ts @@ -5,7 +5,9 @@ import { PropsWithChildren, SetStateAction, useContext, + useEffect, useMemo, + useRef, useState, } from "react"; @@ -33,6 +35,23 @@ export const ComposerStateProvider = ({ children }: PropsWithChildren) => { export const useComposerState = () => { const context = useContext(ComposerStateContext); const [localTextContent, localSetTextContent] = useState(""); + const hasWarnedRef = useRef(false); + + useEffect(() => { + const isProduction = + typeof process !== "undefined" && process.env && process.env["NODE_ENV"] === "production"; + if (!isProduction && !context && !hasWarnedRef.current) { + hasWarnedRef.current = true; + + console.warn( + "[openui] useComposerState was called without a in scope. " + + "Each call site will get its own isolated state, so cross-component features " + + "(e.g. hiding conversation starters while drafting) will not work. " + + "Wrap your layout in (or use a built-in ) " + + "to share composer state across siblings.", + ); + } + }, [context]); return ( context ?? { From 66e203b44fc615264f070972026c2771b3344306 Mon Sep 17 00:00:00 2001 From: i-subham Date: Sat, 16 May 2026 14:27:43 +0530 Subject: [PATCH 8/8] refactor(react-ui): replace ComposerStateProvider with CSS :has() for drafting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WHY: This PR introduced a React Context (ComposerStateProvider + useComposerState) to share composer draft state across siblings so that conversation starters can hide while the user is typing. With no external consumer of the new API yet, the same visual behavior can be expressed declaratively in CSS — which removes ~80 lines of provider/warning/memoization plumbing, eliminates the per-keystroke re-renders of every reader (WelcomeScreen + three ConversationStarter components), and removes a public API surface that the PR would otherwise have to support indefinitely. WHAT: - hooks/useComposerState.ts: revert to a 4-line useState wrapper. Removes ComposerStateContext, ComposerStateProvider, the dev warning, the local fallback path, useRef/useEffect/useMemo. - Composer roots (Shell, BottomTray, CopilotShell, DesktopWelcomeComposer): add `data-drafting={textContent.length > 0 || undefined}` on the root div. React skips the attribute when undefined, so the DOM only carries it while the input is non-empty. - Thread containers (3 shells): drop the wrap and its import. - WelcomeScreen + three ConversationStarters: stop reading useComposerState(); drop the `isDrafting` derivation and the `--drafting` / `--hidden` modifier classes. - thread.scss (3 shells): add a `:has([data-drafting])` rule on the thread container (plus the welcome-screen on Shell) that fades out the conversation starters via opacity + pointer-events, with a 150ms transition. - conversationStarter.scss (3 shells) + welcomeScreen.scss: remove the now-dead `&--hidden` modifier rules. Verified end-to-end in openui-chat dev server: - data-drafting toggles "true" / absent as the user types and clears. - Starters fade to opacity 0 / pointer-events none and back. - No --hidden modifier class lingers anywhere in the DOM. - Old dev-warning string is no longer present in any of the 42 bundled JS files served by Next.js. Net: 19 files changed, -92 LOC. Type-check clean. Build clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/generated/system-prompt.txt | 2 +- .../BottomTray/ConversationStarter.tsx | 9 --- .../src/components/BottomTray/Thread.tsx | 21 +++-- .../BottomTray/components/Composer.tsx | 1 + .../BottomTray/conversationStarter.scss | 8 -- .../src/components/BottomTray/thread.scss | 13 +++ .../CopilotShell/ConversationStarter.tsx | 9 --- .../src/components/CopilotShell/Thread.tsx | 21 +++-- .../CopilotShell/components/Composer.tsx | 1 + .../CopilotShell/conversationStarter.scss | 8 -- .../src/components/CopilotShell/thread.scss | 13 +++ .../components/Shell/ConversationStarter.tsx | 9 --- .../react-ui/src/components/Shell/Thread.tsx | 79 +++++++++---------- .../src/components/Shell/WelcomeScreen.tsx | 15 +--- .../components/Shell/components/Composer.tsx | 1 + .../components/DesktopWelcomeComposer.tsx | 5 +- .../components/Shell/conversationStarter.scss | 8 -- .../react-ui/src/components/Shell/thread.scss | 17 ++++ .../src/components/Shell/welcomeScreen.scss | 14 ---- .../react-ui/src/hooks/useComposerState.ts | 62 +-------------- 20 files changed, 112 insertions(+), 204 deletions(-) diff --git a/examples/openui-chat/src/generated/system-prompt.txt b/examples/openui-chat/src/generated/system-prompt.txt index e98309971..9a097a075 100644 --- a/examples/openui-chat/src/generated/system-prompt.txt +++ b/examples/openui-chat/src/generated/system-prompt.txt @@ -58,7 +58,7 @@ FormControl(label: string, input: Input | TextArea | Select | DatePicker | Slide Label(text: string) — Text label Input(name: string, placeholder?: string, type?: "text" | "email" | "password" | "number" | "url", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) TextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) -Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) +Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: "sm" | "md" | "lg") SelectItem(value: string, label: string) — Option for Select DatePicker(name: string, mode?: "single" | "range", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) Slider(name: string, variant: "continuous" | "discrete", min: number, max: number, step?: number, defaultValue?: number[], label?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) — Numeric slider input; supports continuous and discrete (stepped) variants diff --git a/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx b/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx index 2253fcde9..a82d262f0 100644 --- a/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx +++ b/packages/react-ui/src/components/BottomTray/ConversationStarter.tsx @@ -2,7 +2,6 @@ import { useThread } from "@openuidev/react-headless"; import clsx from "clsx"; import { ArrowUp, Lightbulb } from "lucide-react"; import { Fragment, ReactNode, isValidElement } from "react"; -import { useComposerState } from "../../hooks/useComposerState"; import { ConversationStarterIcon, ConversationStarterProps } from "../../types/ConversationStarter"; import { Carousel, CarouselContent } from "../Carousel"; import { isChatEmpty } from "../_shared/utils"; @@ -107,12 +106,10 @@ export const ConversationStarter = ({ className, variant = "short", }: ConversationStarterContainerProps) => { - const { textContent } = useComposerState(); const processMessage = useThread((s) => s.processMessage); const isRunning = useThread((s) => s.isRunning); const messages = useThread((s) => s.messages); const isLoadingMessages = useThread((s) => s.isLoadingMessages); - const isDrafting = textContent.length > 0; const handleClick = (prompt: string) => { if (isRunning) return; @@ -138,9 +135,6 @@ export const ConversationStarter = ({ className={clsx( "openui-bottom-tray-conversation-starter", "openui-bottom-tray-conversation-starter--short", - { - "openui-bottom-tray-conversation-starter--hidden": isDrafting, - }, className, )} > @@ -165,9 +159,6 @@ export const ConversationStarter = ({ className={clsx( "openui-bottom-tray-conversation-starter", `openui-bottom-tray-conversation-starter--${variant}`, - { - "openui-bottom-tray-conversation-starter--hidden": isDrafting, - }, className, )} > diff --git a/packages/react-ui/src/components/BottomTray/Thread.tsx b/packages/react-ui/src/components/BottomTray/Thread.tsx index c777d729c..0a9578d13 100644 --- a/packages/react-ui/src/components/BottomTray/Thread.tsx +++ b/packages/react-ui/src/components/BottomTray/Thread.tsx @@ -2,7 +2,6 @@ import type { AssistantMessage, Message, ToolMessage } from "@openuidev/react-he import { MessageProvider, useThread } from "@openuidev/react-headless"; import clsx from "clsx"; import React, { memo, useRef } from "react"; -import { ComposerStateProvider } from "../../hooks/useComposerState"; import { ScrollVariant, useScrollToBottom } from "../../hooks/useScrollToBottom"; import { ArtifactOverlay } from "../_shared/artifact"; import type { AssistantMessageComponent, UserMessageComponent } from "../_shared/types"; @@ -21,17 +20,15 @@ export const ThreadContainer = ({ const isLoadingMessages = useThread((s) => s.isLoadingMessages); return ( - -
- {children} - -
-
+
+ {children} + +
); }; diff --git a/packages/react-ui/src/components/BottomTray/components/Composer.tsx b/packages/react-ui/src/components/BottomTray/components/Composer.tsx index a33b43fa9..291010c1f 100644 --- a/packages/react-ui/src/components/BottomTray/components/Composer.tsx +++ b/packages/react-ui/src/components/BottomTray/components/Composer.tsx @@ -43,6 +43,7 @@ export const Composer = ({ className, placeholder = "Type your message..." }: Co return (
0 || undefined} onClick={(e) => { if (!(e.target as HTMLElement).closest("button, a, [role='button']")) { inputRef.current?.focus(); diff --git a/packages/react-ui/src/components/BottomTray/conversationStarter.scss b/packages/react-ui/src/components/BottomTray/conversationStarter.scss index 417a20999..9cf48fa62 100644 --- a/packages/react-ui/src/components/BottomTray/conversationStarter.scss +++ b/packages/react-ui/src/components/BottomTray/conversationStarter.scss @@ -32,14 +32,6 @@ &__carousel-content { gap: cssUtils.$space-xs; } - - &--hidden { - opacity: 0; - transform: translateY(-4px); - max-height: 0; - margin-bottom: 0; - pointer-events: none; - } } // Short variant item (pill-style buttons) diff --git a/packages/react-ui/src/components/BottomTray/thread.scss b/packages/react-ui/src/components/BottomTray/thread.scss index 5dfd8cf43..cc0ce23f4 100644 --- a/packages/react-ui/src/components/BottomTray/thread.scss +++ b/packages/react-ui/src/components/BottomTray/thread.scss @@ -8,6 +8,19 @@ position: relative; } +// Hide conversation starters while the user is drafting in the composer. +// The drafting flag is set on the composer root via `data-drafting`. +.openui-bottom-tray-thread-container:has([data-drafting]) { + .openui-bottom-tray-conversation-starter { + opacity: 0; + pointer-events: none; + } +} + +.openui-bottom-tray-conversation-starter { + transition: opacity 150ms ease; +} + .openui-bottom-tray-thread-scroll-container { width: 100%; flex: 1; diff --git a/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx b/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx index 9367fdef8..1d229a42b 100644 --- a/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx +++ b/packages/react-ui/src/components/CopilotShell/ConversationStarter.tsx @@ -2,7 +2,6 @@ import { useThread } from "@openuidev/react-headless"; import clsx from "clsx"; import { ArrowUp, Lightbulb } from "lucide-react"; import { Fragment, ReactNode, isValidElement } from "react"; -import { useComposerState } from "../../hooks/useComposerState"; import { ConversationStarterIcon, ConversationStarterProps } from "../../types/ConversationStarter"; import { Carousel, CarouselContent } from "../Carousel"; import { isChatEmpty } from "../_shared/utils"; @@ -107,12 +106,10 @@ export const ConversationStarter = ({ className, variant = "short", }: ConversationStarterContainerProps) => { - const { textContent } = useComposerState(); const processMessage = useThread((s) => s.processMessage); const isRunning = useThread((s) => s.isRunning); const messages = useThread((s) => s.messages); const isLoadingMessages = useThread((s) => s.isLoadingMessages); - const isDrafting = textContent.length > 0; const handleClick = (prompt: string) => { if (isRunning) return; @@ -138,9 +135,6 @@ export const ConversationStarter = ({ className={clsx( "openui-copilot-shell-conversation-starter", "openui-copilot-shell-conversation-starter--short", - { - "openui-copilot-shell-conversation-starter--hidden": isDrafting, - }, className, )} > @@ -165,9 +159,6 @@ export const ConversationStarter = ({ className={clsx( "openui-copilot-shell-conversation-starter", `openui-copilot-shell-conversation-starter--${variant}`, - { - "openui-copilot-shell-conversation-starter--hidden": isDrafting, - }, className, )} > diff --git a/packages/react-ui/src/components/CopilotShell/Thread.tsx b/packages/react-ui/src/components/CopilotShell/Thread.tsx index 5f585dfad..ffc103890 100644 --- a/packages/react-ui/src/components/CopilotShell/Thread.tsx +++ b/packages/react-ui/src/components/CopilotShell/Thread.tsx @@ -2,7 +2,6 @@ import type { AssistantMessage, Message, ToolMessage } from "@openuidev/react-he import { MessageProvider, useThread } from "@openuidev/react-headless"; import clsx from "clsx"; import React, { memo, useRef } from "react"; -import { ComposerStateProvider } from "../../hooks/useComposerState"; import { ScrollVariant, useScrollToBottom } from "../../hooks/useScrollToBottom"; import { ArtifactOverlay } from "../_shared/artifact"; import { useShellStore } from "../_shared/store"; @@ -22,17 +21,15 @@ export const ThreadContainer = ({ const isLoadingMessages = useThread((s) => s.isLoadingMessages); return ( - -
- {children} - -
-
+
+ {children} + +
); }; diff --git a/packages/react-ui/src/components/CopilotShell/components/Composer.tsx b/packages/react-ui/src/components/CopilotShell/components/Composer.tsx index 03b432173..578d152bc 100644 --- a/packages/react-ui/src/components/CopilotShell/components/Composer.tsx +++ b/packages/react-ui/src/components/CopilotShell/components/Composer.tsx @@ -43,6 +43,7 @@ export const Composer = ({ className, placeholder = "Type your message..." }: Co return (
0 || undefined} onClick={(e) => { if (!(e.target as HTMLElement).closest("button, a, [role='button']")) { inputRef.current?.focus(); diff --git a/packages/react-ui/src/components/CopilotShell/conversationStarter.scss b/packages/react-ui/src/components/CopilotShell/conversationStarter.scss index b938c2178..c6fb90500 100644 --- a/packages/react-ui/src/components/CopilotShell/conversationStarter.scss +++ b/packages/react-ui/src/components/CopilotShell/conversationStarter.scss @@ -32,14 +32,6 @@ &__carousel-content { gap: cssUtils.$space-xs; } - - &--hidden { - opacity: 0; - transform: translateY(-4px); - max-height: 0; - margin-bottom: 0; - pointer-events: none; - } } // Short variant item (pill-style buttons) diff --git a/packages/react-ui/src/components/CopilotShell/thread.scss b/packages/react-ui/src/components/CopilotShell/thread.scss index 9d79c7f17..889a52520 100644 --- a/packages/react-ui/src/components/CopilotShell/thread.scss +++ b/packages/react-ui/src/components/CopilotShell/thread.scss @@ -8,6 +8,19 @@ position: relative; } +// Hide conversation starters while the user is drafting in the composer. +// The drafting flag is set on the composer root via `data-drafting`. +.openui-copilot-shell-thread-container:has([data-drafting]) { + .openui-copilot-shell-conversation-starter { + opacity: 0; + pointer-events: none; + } +} + +.openui-copilot-shell-conversation-starter { + transition: opacity 150ms ease; +} + .openui-copilot-shell-thread-scroll-container { width: 100%; flex: 1; diff --git a/packages/react-ui/src/components/Shell/ConversationStarter.tsx b/packages/react-ui/src/components/Shell/ConversationStarter.tsx index 663a0236b..dc9d20869 100644 --- a/packages/react-ui/src/components/Shell/ConversationStarter.tsx +++ b/packages/react-ui/src/components/Shell/ConversationStarter.tsx @@ -2,7 +2,6 @@ import { useThread } from "@openuidev/react-headless"; import clsx from "clsx"; import { ArrowUp, Lightbulb } from "lucide-react"; import { Fragment, ReactNode, isValidElement } from "react"; -import { useComposerState } from "../../hooks/useComposerState"; import { ConversationStarterIcon, ConversationStarterProps } from "../../types/ConversationStarter"; import { Carousel, CarouselContent } from "../Carousel"; import { isChatEmpty } from "../_shared/utils"; @@ -99,12 +98,10 @@ export const ConversationStarter = ({ className, variant = "short", }: ConversationStarterContainerProps) => { - const { textContent } = useComposerState(); const processMessage = useThread((s) => s.processMessage); const isRunning = useThread((s) => s.isRunning); const messages = useThread((s) => s.messages); const isLoadingMessages = useThread((s) => s.isLoadingMessages); - const isDrafting = textContent.length > 0; const handleClick = (prompt: string) => { if (isRunning) return; @@ -130,9 +127,6 @@ export const ConversationStarter = ({ className={clsx( "openui-shell-conversation-starter", "openui-shell-conversation-starter--short", - { - "openui-shell-conversation-starter--hidden": isDrafting, - }, className, )} > @@ -157,9 +151,6 @@ export const ConversationStarter = ({ className={clsx( "openui-shell-conversation-starter", `openui-shell-conversation-starter--${variant}`, - { - "openui-shell-conversation-starter--hidden": isDrafting, - }, className, )} > diff --git a/packages/react-ui/src/components/Shell/Thread.tsx b/packages/react-ui/src/components/Shell/Thread.tsx index 8a36b155e..df22d6717 100644 --- a/packages/react-ui/src/components/Shell/Thread.tsx +++ b/packages/react-ui/src/components/Shell/Thread.tsx @@ -3,7 +3,6 @@ import { MessageProvider, useActiveArtifact, useThread } from "@openuidev/react- import clsx from "clsx"; import React, { memo, useRef } from "react"; import { useLayoutContext } from "../../context/LayoutContext"; -import { ComposerStateProvider } from "../../hooks/useComposerState"; import { ScrollVariant, useScrollToBottom } from "../../hooks/useScrollToBottom"; import { separateContentAndContext } from "../../utils/contentParser"; import { ArtifactOverlay, ArtifactPortalTarget } from "../_shared/artifact"; @@ -49,48 +48,46 @@ export const ThreadContainer = ({ }); return ( - -
-
- {/* Chat panel - always visible */} -
- {children} - {isMobile && } -
- - {/* Desktop only: Resizable separator and artifact panel */} - {!isMobile && isArtifactActive && ( - <> - -
- -
- - )} +
+
+ {/* Chat panel - always visible */} +
+ {children} + {isMobile && }
+ + {/* Desktop only: Resizable separator and artifact panel */} + {!isMobile && isArtifactActive && ( + <> + +
+ +
+ + )}
- +
); }; diff --git a/packages/react-ui/src/components/Shell/WelcomeScreen.tsx b/packages/react-ui/src/components/Shell/WelcomeScreen.tsx index ee6e515f1..edff6e183 100644 --- a/packages/react-ui/src/components/Shell/WelcomeScreen.tsx +++ b/packages/react-ui/src/components/Shell/WelcomeScreen.tsx @@ -1,7 +1,6 @@ import { useThread } from "@openuidev/react-headless"; import clsx from "clsx"; import { ReactNode } from "react"; -import { useComposerState } from "../../hooks/useComposerState"; import { ConversationStarterProps } from "../../types/ConversationStarter"; import { isChatEmpty } from "../_shared/utils"; import { DesktopWelcomeComposer } from "./components"; @@ -67,11 +66,9 @@ const isImageUrl = (image: { url: string } | ReactNode): image is { url: string export const WelcomeScreen = (props: WelcomeScreenProps) => { const { className, starters = [], starterVariant = "long" } = props; - const { textContent } = useComposerState(); const messages = useThread((s) => s.messages); const isLoadingMessages = useThread((s) => s.isLoadingMessages); - const isDrafting = textContent.length > 0; // Only show when there are no messages if (!isChatEmpty({ isLoadingMessages, messages })) { @@ -120,21 +117,13 @@ export const WelcomeScreen = (props: WelcomeScreenProps) => { )}
{/* Desktop-only welcome composer */} -
+
{/* Desktop-only conversation starters */} {starters.length > 0 && ( -
+
)} diff --git a/packages/react-ui/src/components/Shell/components/Composer.tsx b/packages/react-ui/src/components/Shell/components/Composer.tsx index 9f33c5180..0ce1e6fca 100644 --- a/packages/react-ui/src/components/Shell/components/Composer.tsx +++ b/packages/react-ui/src/components/Shell/components/Composer.tsx @@ -43,6 +43,7 @@ export const Composer = ({ className, placeholder = "Type your query here" }: Co return (
0 || undefined} onClick={(e) => { if (!(e.target as HTMLElement).closest("button, a, [role='button']")) { inputRef.current?.focus(); diff --git a/packages/react-ui/src/components/Shell/components/DesktopWelcomeComposer.tsx b/packages/react-ui/src/components/Shell/components/DesktopWelcomeComposer.tsx index 3ace7e7bf..d97168390 100644 --- a/packages/react-ui/src/components/Shell/components/DesktopWelcomeComposer.tsx +++ b/packages/react-ui/src/components/Shell/components/DesktopWelcomeComposer.tsx @@ -44,7 +44,10 @@ export const DesktopWelcomeComposer = ({ }, [textContent]); return ( -
+
0 || undefined} + >