feat(site): design improvements for projects page (#30740)

<img width="1211" height="799" alt="Screenshot 2026-10-14 at 15 29 37"
src="https://github.com/user-attachments/assets/3a62b3e7-1e97-4d76-b643-9a6da9ae690d"
/>

1. Improves spacing and positioning of elements
2. Move meta data to details section
This commit is contained in:
Jaayden Halko
2026-10-14 16:40:40 +07:00
committed by GitHub
parent 01f78b37e3
commit 40c0adc7ac
15 changed files with 235 additions and 195 deletions
@@ -425,6 +425,7 @@ const AgentCreatePageContent: React.FC<AgentCreatePageContentProps> = ({
return (
<>
<AgentPageHeader
floating={project !== undefined}
chimeEnabled={chimeEnabled}
onToggleChime={handleChimeToggle}
webPush={webPush}
@@ -535,7 +535,7 @@ const AgentsPageLayout: React.FC = () => {
<div
data-testid="agents-main-panel"
className={cn(
"min-h-0 min-w-0 flex-1 flex-col bg-surface-primary",
"min-h-0 min-w-0 flex-1 flex-col bg-surface-primary sm:relative",
isSettingsIndex ? "hidden sm:flex" : "flex",
!agentId &&
!isSettingsDetail &&
@@ -1334,8 +1334,10 @@ export const AgentChatInput: React.FC<AgentChatInputProps> = ({
const content = (
<div
className={cn(
"mx-auto w-full pb-0 sm:pb-4",
fillWidth ? "max-w-full" : chatWidthClass(chatFullWidth),
"mx-auto w-full",
fillWidth
? "max-w-full"
: [chatWidthClass(chatFullWidth), "pb-0 sm:pb-4"],
isEditingHistoryMessage && "pt-1",
)}
>
@@ -1,3 +1,4 @@
import { cn } from "cn";
import {
ArrowLeftIcon,
BellIcon,
@@ -36,6 +37,8 @@ type AgentPageHeaderProps = {
/** When set, shows a back link on mobile instead of the logo
* and hides the mobile actions menu. */
mobileBack?: { to: To; label: string };
/** On desktop, overlays the page instead of taking its own row. */
floating?: boolean;
chimeEnabled?: boolean;
onToggleChime?: () => void;
webPush?: ReturnType<typeof useWebpushNotifications>;
@@ -45,6 +48,7 @@ type AgentPageHeaderProps = {
export const AgentPageHeader: React.FC<AgentPageHeaderProps> = ({
children,
mobileBack,
floating = false,
chimeEnabled: controlledChimeEnabled,
onToggleChime,
webPush: controlledWebPush,
@@ -112,7 +116,14 @@ export const AgentPageHeader: React.FC<AgentPageHeaderProps> = ({
};
return (
<div className="order-first flex shrink-0 items-center gap-2 pl-4 pr-2 pt-3 pb-0.5 sm:order-0 sm:px-4 sm:py-0.5">
<div
className={cn(
"order-first flex shrink-0 items-center gap-2 pl-4 pr-2 pt-3 pb-0.5 sm:order-0 sm:px-4 sm:py-0.5",
// The row spans the page width, so only its buttons take clicks.
floating &&
"sm:pointer-events-none sm:absolute sm:inset-x-0 sm:top-0 sm:z-10",
)}
>
{mobileBack ? (
<Button
asChild
@@ -138,14 +149,16 @@ export const AgentPageHeader: React.FC<AgentPageHeaderProps> = ({
size="icon"
onClick={onExpandSidebar}
aria-label="Expand sidebar"
className="hidden size-7 min-w-0 shrink-0 sm:inline-flex"
className="pointer-events-auto hidden size-7 min-w-0 shrink-0 sm:inline-flex"
>
<PanelLeftIcon />
</Button>
)}
<div className="min-w-0 flex-1" />
{children && isDesktop && (
<div className="hidden items-center gap-2 sm:flex">{children}</div>
<div className="pointer-events-auto hidden items-center gap-2 sm:flex">
{children}
</div>
)}
{/* Mobile: meatball menu with all actions */}
{!mobileBack && !isDesktop && (
@@ -66,7 +66,9 @@ export const ProjectChatsListView: React.FC<ProjectChatsListViewProps> = ({
)
) : chats.length === 0 ? (
<>
<div className="rounded-lg border border-dashed border-border px-4 py-8 text-center text-sm text-content-secondary">
{/* Fills the grid row so the box matches the details panel
beside it. */}
<div className="flex flex-1 items-center justify-center rounded-lg border border-dashed border-border px-4 py-8 text-center text-sm text-content-secondary">
No chats yet. Every chat will leverage the same settings and
history.
</div>
@@ -12,6 +12,7 @@ import type {
ChatProject,
ChatProjectInstructions,
} from "#/api/typesGenerated";
import { TooltipProvider } from "#/components/Tooltip/Tooltip";
import { ThemeOverride } from "#/contexts/ThemeProvider";
import {
MockChatProject,
@@ -23,6 +24,23 @@ import { createTestQueryClient } from "#/testHelpers/renderHelpers";
import themes, { DEFAULT_THEME } from "#/theme";
import { ProjectDetailsPanel } from "./ProjectDetailsPanel";
vi.mock("#/hooks/useAuthenticated", async () => {
const { MockUserOwner } = await import("#/testHelpers/entities");
return {
useAuthenticated: () => ({ user: MockUserOwner, permissions: {} }),
};
});
vi.mock("#/modules/dashboard/useDashboard", async () => {
const { MockDefaultOrganization } = await import("#/testHelpers/entities");
return {
useDashboard: () => ({
organizations: [MockDefaultOrganization],
showOrganizations: true,
}),
};
});
const mockInstructions = (instructions: ChatProjectInstructions) =>
vi
.spyOn(API.experimental, "getChatProjectInstructions")
@@ -57,16 +75,18 @@ const confirmDelete = async (user: ReturnType<typeof userEvent.setup>) => {
);
};
const renderPanel = (project: ChatProject = MockChatProject) => {
const renderPanel = () => {
const queryClient = createTestQueryClient();
const panel = (project: ChatProject) => (
<ThemeOverride theme={themes[DEFAULT_THEME]}>
<QueryClientProvider client={queryClient}>
<ProjectDetailsPanel project={project} />
<TooltipProvider>
<ProjectDetailsPanel project={project} />
</TooltipProvider>
</QueryClientProvider>
</ThemeOverride>
);
const { rerender } = render(panel(project));
const { rerender } = render(panel(MockChatProject));
return {
user: userEvent.setup(),
queryClient,
@@ -91,7 +111,9 @@ describe("ProjectDetailsPanel", () => {
const getInstructions = mockInstructions(MockUnsetChatProjectInstructions);
const { user, queryClient } = renderPanel();
await user.click(await screen.findByRole("button", { name: "Create" }));
await user.click(
await screen.findByRole("button", { name: "Create instructions" }),
);
await user.type(
screen.getByRole("textbox", { name: "Instructions" }),
text,
@@ -210,7 +232,9 @@ describe("ProjectDetailsPanel", () => {
mockInstructions(MockUnsetChatProjectInstructions);
const { user } = renderPanel();
await user.click(await screen.findByRole("button", { name: "Create" }));
await user.click(
await screen.findByRole("button", { name: "Create instructions" }),
);
const textbox = screen.getByRole("textbox", { name: "Instructions" });
await user.type(textbox, "Too long");
await user.click(screen.getByRole("button", { name: "Save" }));
@@ -265,7 +289,9 @@ describe("ProjectDetailsPanel", () => {
mockInstructions(MockUnsetChatProjectInstructions);
const { user } = renderPanel();
await user.click(await screen.findByRole("button", { name: "Create" }));
await user.click(
await screen.findByRole("button", { name: "Create instructions" }),
);
await user.type(
screen.getByRole("textbox", { name: "Instructions" }),
"Be brief.",
@@ -291,7 +317,9 @@ describe("ProjectDetailsPanel", () => {
mockInstructions(MockUnsetChatProjectInstructions);
const { user } = renderPanel();
await user.click(await screen.findByRole("button", { name: "Create" }));
await user.click(
await screen.findByRole("button", { name: "Create instructions" }),
);
await user.type(
screen.getByRole("textbox", { name: "Instructions" }),
"Be brief.",
@@ -5,7 +5,10 @@ import {
deleteChatProjectInstructions,
updateChatProjectInstructions,
} from "#/api/queries/chatProjects";
import { user } from "#/api/queries/users";
import type { ChatProject } from "#/api/typesGenerated";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { useDashboard } from "#/modules/dashboard/useDashboard";
import { ProjectDetailsPanelView } from "./ProjectDetailsPanelView";
import { ProjectInstructionsDialog } from "./ProjectInstructionsDialog";
@@ -27,6 +30,25 @@ const ProjectDetailsPanelContent: React.FC<ProjectDetailsPanelProps> = ({
project,
}) => {
const queryClient = useQueryClient();
const { user: me } = useAuthenticated();
const { organizations, showOrganizations } = useDashboard();
const isOwner = project.owner_id === me.id;
const ownerQuery = useQuery({ ...user(project.owner_id), enabled: !isOwner });
const ownerLabel = isOwner
? "You"
: ownerQuery.data
? ownerQuery.data.name || ownerQuery.data.username
: ownerQuery.isError
? "Unknown"
: undefined;
const organization = organizations.find(
(org) => org.id === project.organization_id,
);
const organizationLabel = showOrganizations
? organization
? organization.display_name || organization.name
: "Unknown"
: undefined;
const instructionsQuery = useQuery(chatProjectInstructions(project));
const updateMutation = useMutation(
updateChatProjectInstructions(queryClient, project),
@@ -64,6 +86,9 @@ const ProjectDetailsPanelContent: React.FC<ProjectDetailsPanelProps> = ({
return (
<>
<ProjectDetailsPanelView
ownerLabel={ownerLabel}
createdAt={project.created_at}
organizationLabel={organizationLabel}
instructions={instructionsQuery.data}
error={instructionsQuery.error}
onRetry={() => void instructionsQuery.refetch()}
@@ -1,10 +1,12 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { fn, within } from "storybook/test";
import {
MockChatProject,
MockChatProjectInstructions,
MockUnsetChatProjectInstructions,
mockApiError,
} from "#/testHelpers/entities";
import { DATE_FORMAT, formatDateTime } from "#/utils/time";
import { ProjectDetailsPanelView } from "./ProjectDetailsPanelView";
const meta: Meta<typeof ProjectDetailsPanelView> = {
@@ -12,12 +14,14 @@ const meta: Meta<typeof ProjectDetailsPanelView> = {
component: ProjectDetailsPanelView,
decorators: [
(Story) => (
<div className="max-w-[30rem]">
<div className="max-w-[310px]">
<Story />
</div>
),
],
args: {
ownerLabel: "You",
createdAt: MockChatProject.created_at,
instructions: MockChatProjectInstructions,
error: undefined,
onRetry: fn(),
@@ -28,6 +32,46 @@ const meta: Meta<typeof ProjectDetailsPanelView> = {
export default meta;
type Story = StoryObj<typeof ProjectDetailsPanelView>;
export const Default: Story = {};
export const WithOrganization: Story = {
args: { organizationLabel: "Coder" },
};
export const OwnerLoading: Story = {
args: { ownerLabel: undefined },
};
export const OtherOwner: Story = {
args: { ownerLabel: "Jane Doe" },
};
export const OwnerUnknown: Story = {
args: { ownerLabel: "Unknown" },
};
/** Long owner and organization names wrap so they stay readable. */
export const LongNames: Story = {
args: {
ownerLabel: "bartholomewmontgomeryfitzgerald1",
organizationLabel: "Platform-Infrastructure-Engineering-Europe",
},
};
/** Keyboard focus on the creation date shows the full timestamp. */
export const CreatedDateFocused: Story = {
play: async ({ canvasElement }) => {
within(canvasElement)
.getByRole("button", {
name: formatDateTime(
MockChatProject.created_at,
DATE_FORMAT.MEDIUM_DATE,
),
})
.focus();
},
};
export const Unset: Story = {
args: { instructions: MockUnsetChatProjectInstructions },
};
@@ -58,3 +102,12 @@ export const LoadError: Story = {
}),
},
};
/** A failed refetch keeps the cached instructions and shows the error. */
export const RefetchError: Story = {
args: {
error: mockApiError({
message: "Failed to get chat project instructions.",
}),
},
};
@@ -4,8 +4,19 @@ import type { ChatProjectInstructions } from "#/api/typesGenerated";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Button } from "#/components/Button/Button";
import { Skeleton } from "#/components/Skeleton/Skeleton";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "#/components/Tooltip/Tooltip";
import { DATE_FORMAT, formatDateTime } from "#/utils/time";
type ProjectDetailsPanelViewProps = {
/** The owner's display label. `undefined` while it loads. */
readonly ownerLabel: string | undefined;
readonly createdAt: string;
/** Omit to hide the organization row. */
readonly organizationLabel?: string;
/** `undefined` until the instructions load. */
readonly instructions: ChatProjectInstructions | undefined;
readonly error: unknown;
@@ -17,7 +28,15 @@ type ProjectDetailsPanelViewProps = {
/** The project's shared configuration, shown beside its chats. */
export const ProjectDetailsPanelView: React.FC<
ProjectDetailsPanelViewProps
> = ({ instructions, error, onRetry, onEditInstructions }) => {
> = ({
ownerLabel,
createdAt,
organizationLabel,
instructions,
error,
onRetry,
onEditInstructions,
}) => {
const headingId = useId();
const instructionsHeadingId = useId();
const errorAlert = Boolean(error) && (
@@ -40,13 +59,42 @@ export const ProjectDetailsPanelView: React.FC<
Details
</h2>
<div className="rounded-lg border border-border">
<dl className="m-0 flex flex-col gap-2 border-0 border-b border-solid border-border p-4 text-sm">
<DetailRow label="Project owner">
{ownerLabel ?? (
<>
<Skeleton className="my-1 ml-auto h-3 w-12" />
<span className="sr-only">Loading</span>
</>
)}
</DetailRow>
<DetailRow label="Created">
<Tooltip>
{/* A button so keyboard users can focus it to read the full time. */}
<TooltipTrigger
type="button"
className="cursor-default rounded-sm border-0 bg-transparent p-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-content-link"
>
<time dateTime={createdAt}>
{formatDateTime(createdAt, DATE_FORMAT.MEDIUM_DATE)}
</time>
</TooltipTrigger>
<TooltipContent>
{formatDateTime(createdAt, DATE_FORMAT.FULL_DATETIME)}
</TooltipContent>
</Tooltip>
</DetailRow>
{organizationLabel !== undefined && (
<DetailRow label="Organization">{organizationLabel}</DetailRow>
)}
</dl>
<section
aria-labelledby={instructionsHeadingId}
className="flex flex-col gap-3 p-4"
>
<h3
id={instructionsHeadingId}
className="m-0 text-sm font-medium text-content-primary"
className="m-0 text-sm font-medium text-content-secondary"
>
Instructions
</h3>
@@ -65,6 +113,7 @@ export const ProjectDetailsPanelView: React.FC<
<Button
size="sm"
variant="outline"
aria-label="Create instructions"
onClick={onEditInstructions}
>
Create
@@ -94,3 +143,17 @@ export const ProjectDetailsPanelView: React.FC<
</section>
);
};
type DetailRowProps = {
readonly label: string;
readonly children: React.ReactNode;
};
const DetailRow: React.FC<DetailRowProps> = ({ label, children }) => (
<div className="flex items-start justify-between gap-4">
<dt className="shrink-0 text-content-secondary">{label}</dt>
<dd className="m-0 min-w-0 break-words text-right font-mono text-content-primary">
{children}
</dd>
</div>
);
@@ -1,13 +1,12 @@
import { EllipsisVerticalIcon } from "lucide-react";
import { useRef, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { useMutation, useQueryClient } from "react-query";
import { useLocation, useNavigate } from "react-router";
import { getErrorMessage } from "#/api/errors";
import {
deleteChatProject,
updateChatProject,
} from "#/api/queries/chatProjects";
import { user } from "#/api/queries/users";
import type { ChatProject } from "#/api/typesGenerated";
import { Button } from "#/components/Button/Button";
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
@@ -19,45 +18,23 @@ import {
DropdownMenuTrigger,
} from "#/components/DropdownMenu/DropdownMenu";
import { toast } from "#/components/Toaster/toast";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { useDashboard } from "#/modules/dashboard/useDashboard";
import { draftStorageKeys } from "../AgentCreateForm";
import { ProjectActionsMenuItems } from "../ChatsSidebar/chats/ProjectActionsMenuItems";
import { ChatProjectDialog } from "../ChatsSidebar/dialogs/ChatProjectDialog";
import { normalizeLocationSearch } from "../ChatsSidebar/locationSearch";
import { ProjectMetadataBadges } from "./ProjectMetadataBadges";
import { ProjectPageHeader } from "./ProjectPageHeader";
type ProjectHeaderSectionProps = {
readonly project: ChatProject;
};
/** The project page header with its metadata and edit and delete actions. */
/** The project page header with its edit and delete actions. */
export const ProjectHeaderSection: React.FC<ProjectHeaderSectionProps> = ({
project,
}) => {
const queryClient = useQueryClient();
const navigate = useNavigate();
const location = useLocation();
const { user: me } = useAuthenticated();
const { organizations, showOrganizations } = useDashboard();
const isOwner = project.owner_id === me.id;
const ownerQuery = useQuery({ ...user(project.owner_id), enabled: !isOwner });
const ownerLabel = isOwner
? "you"
: ownerQuery.data
? ownerQuery.data.name || ownerQuery.data.username
: ownerQuery.isError
? "Unknown"
: undefined;
const organization = organizations.find(
(org) => org.id === project.organization_id,
);
const organizationLabel = showOrganizations
? organization
? organization.display_name || organization.name
: "Unknown"
: undefined;
const updateMutation = useMutation(updateChatProject(queryClient));
// The project list refetch after a delete unmounts this page, so the
@@ -130,13 +107,6 @@ export const ProjectHeaderSection: React.FC<ProjectHeaderSectionProps> = ({
</DropdownMenuContent>
</DropdownMenu>
}
metadata={
<ProjectMetadataBadges
ownerLabel={ownerLabel}
createdAt={project.created_at}
organizationLabel={organizationLabel}
/>
}
/>
<ChatProjectDialog
project={project}
@@ -1,66 +0,0 @@
import { Badge } from "#/components/Badge/Badge";
import { Skeleton } from "#/components/Skeleton/Skeleton";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "#/components/Tooltip/Tooltip";
import { DATE_FORMAT, formatDateTime } from "#/utils/time";
type ProjectMetadataBadgesProps = {
/** The owner's display label. `undefined` while it loads. */
readonly ownerLabel: string | undefined;
readonly createdAt: string;
/** Omit to hide the organization badge. */
readonly organizationLabel?: string;
};
export const ProjectMetadataBadges: React.FC<ProjectMetadataBadgesProps> = ({
ownerLabel,
createdAt,
organizationLabel,
}) => (
<ul className="m-0 flex list-none flex-wrap gap-2 p-0">
<MetadataBadge label="Project owner">
{ownerLabel ?? (
<>
<Skeleton className="h-2.5 w-12" />
<span className="sr-only">Loading</span>
</>
)}
</MetadataBadge>
<MetadataBadge label="Created">
<Tooltip>
{/* A button so keyboard users can focus it to read the full time. */}
<TooltipTrigger
type="button"
className="cursor-default rounded-sm border-0 bg-transparent p-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-content-link"
>
<time dateTime={createdAt}>
{formatDateTime(createdAt, DATE_FORMAT.MEDIUM_DATE)}
</time>
</TooltipTrigger>
<TooltipContent>
{formatDateTime(createdAt, DATE_FORMAT.FULL_DATETIME)}
</TooltipContent>
</Tooltip>
</MetadataBadge>
{organizationLabel !== undefined && (
<MetadataBadge label="Organization">{organizationLabel}</MetadataBadge>
)}
</ul>
);
type MetadataBadgeProps = {
readonly label: string;
readonly children: React.ReactNode;
};
const MetadataBadge: React.FC<MetadataBadgeProps> = ({ label, children }) => (
<li>
<Badge size="sm">
<span>{label}:</span>
<span className="text-content-primary">{children}</span>
</Badge>
</li>
);
@@ -1,11 +1,13 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { chatProjectInstructionsKey } from "#/api/queries/chatProjects";
import { chatCostTreeKey, projectChatsKey } from "#/api/queries/chats";
import { userKey } from "#/api/queries/users";
import type { Chat } from "#/api/typesGenerated";
import { MockChat, mockChatCost } from "#/testHelpers/chatEntities";
import {
MockChatProject,
MockChatProjectInstructions,
MockUserMember,
MockUserOwner,
} from "#/testHelpers/entities";
import {
@@ -91,3 +93,19 @@ export const Mobile: Story = {
],
},
};
/** Another user's project in a multi-organization deployment. */
export const OwnedByAnotherUser: Story = {
args: { project: { ...MockChatProject, owner_id: MockUserMember.id } },
parameters: {
showOrganizations: true,
queries: [
...chatQueries([{ ...MockChat, id: "chat-1", title: "Plan the launch" }]),
{
key: chatProjectInstructionsKey(MockChatProject.id),
data: MockChatProjectInstructions,
},
{ key: userKey(MockUserMember.id), data: MockUserMember },
],
},
};
@@ -14,15 +14,17 @@ export const ProjectPage: React.FC<ProjectPageProps> = ({
}) => (
// flex-1 and min-h-0 bound the page to the layout's height at every
// breakpoint, so a long chat list scrolls here instead of being clipped.
<div className="order-last flex min-h-0 flex-1 flex-col overflow-auto px-4 pt-8 sm:order-0 sm:pt-16">
<div className="order-last flex min-h-0 flex-1 flex-col overflow-auto px-4 pt-6 sm:order-0 sm:px-[85.5px]">
<div className="mx-auto flex w-full max-w-6xl flex-col gap-6 pb-8 @container">
<ProjectHeaderSection key={project.id} project={project} />
{children}
{/* Columns follow the page width rather than the viewport, which
the agents sidebar narrows. */}
<div className="grid grid-cols-1 gap-8 @4xl:grid-cols-[minmax(0,1fr)_30rem]">
<ProjectChatsList project={project} />
<ProjectDetailsPanel project={project} />
<div className="flex flex-col gap-14">
{children}
{/* Columns follow the page width rather than the viewport, which
the agents sidebar narrows. */}
<div className="grid grid-cols-1 gap-8 @4xl:grid-cols-[minmax(0,554fr)_minmax(0,310fr)]">
<ProjectChatsList project={project} />
<ProjectDetailsPanel project={project} />
</div>
</div>
</div>
</div>
@@ -1,10 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { EllipsisVerticalIcon } from "lucide-react";
import { within } from "storybook/test";
import { Button } from "#/components/Button/Button";
import { MockChatProject } from "#/testHelpers/entities";
import { DATE_FORMAT, formatDateTime } from "#/utils/time";
import { ProjectMetadataBadges } from "./ProjectMetadataBadges";
import { ProjectPageHeader } from "./ProjectPageHeader";
const actions = (
@@ -26,12 +23,6 @@ const meta: Meta<typeof ProjectPageHeader> = {
args: {
project: MockChatProject,
actions,
metadata: (
<ProjectMetadataBadges
ownerLabel="you"
createdAt={MockChatProject.created_at}
/>
),
},
};
@@ -55,62 +46,3 @@ export const LongNameAndDescription: Story = {
},
},
};
export const OwnerLoading: Story = {
args: {
metadata: (
<ProjectMetadataBadges
ownerLabel={undefined}
createdAt={MockChatProject.created_at}
/>
),
},
};
export const OtherOwner: Story = {
args: {
metadata: (
<ProjectMetadataBadges
ownerLabel="Jane Doe"
createdAt={MockChatProject.created_at}
/>
),
},
};
export const OwnerUnknown: Story = {
args: {
metadata: (
<ProjectMetadataBadges
ownerLabel="Unknown"
createdAt={MockChatProject.created_at}
/>
),
},
};
export const WithOrganization: Story = {
args: {
metadata: (
<ProjectMetadataBadges
ownerLabel="you"
createdAt={MockChatProject.created_at}
organizationLabel="Coder"
/>
),
},
};
/** Keyboard focus on the creation date shows the full timestamp. */
export const CreatedDateFocused: Story = {
play: async ({ canvasElement }) => {
within(canvasElement)
.getByRole("button", {
name: formatDateTime(
MockChatProject.created_at,
DATE_FORMAT.MEDIUM_DATE,
),
})
.focus();
},
};
@@ -4,15 +4,13 @@ import { ChatProjectIcon } from "../ChatProjectIcon";
type ProjectPageHeaderProps = {
readonly project: ChatProject;
readonly actions?: React.ReactNode;
readonly metadata?: React.ReactNode;
};
export const ProjectPageHeader: React.FC<ProjectPageHeaderProps> = ({
project,
actions,
metadata,
}) => (
<header className="flex min-w-0 flex-col gap-1">
<header className="mb-18 flex min-w-0 flex-col gap-1">
<div className="flex min-w-0 items-center gap-3">
<div className="flex size-7 shrink-0 items-center justify-center rounded-md border border-solid border-border bg-surface-secondary">
<ChatProjectIcon project={project} className="size-4" />
@@ -24,7 +22,6 @@ export const ProjectPageHeader: React.FC<ProjectPageHeaderProps> = ({
{actions}
</div>
</div>
{metadata}
{project.description && (
<p className="m-0 text-sm text-content-secondary [overflow-wrap:anywhere]">
{project.description}