feat(connectors): the create-connector header icons render to spec (#15055)

This commit is contained in:
Raunak Bhagat authored and GitHub committed 2026-09-23 21:38:26 +00:00
1 parent 7e54ce4fe3
commit 4c6217c70d
15 files changed
+103 -56

No files matched your search

@@ -27,7 +27,6 @@ interface ContentXlPresetConfig {
/** More-icon-1 width/height (CSS value). */
moreIcon1Size: string;
/** More-icon-2 width/height (CSS value). */
moreIcon2Size: string;
/** Button `size` prop for the edit button. */
editButtonSize: ContainerSizeVariants;
/** Tailwind padding class for the edit button container. */
@@ -67,7 +66,7 @@ interface ContentXlProps {
/** Optional secondary icon rendered in the icon row. */
moreIcon1?: IconFunctionComponent;
/** Optional tertiary icon rendered in the icon row. */
/** Optional tertiary icon rendered in the icon row, boxed and sized like `icon`. */
moreIcon2?: IconFunctionComponent;
/** Ref forwarded to the root `<div>`. */
@@ -84,7 +83,6 @@ const CONTENT_XL_PRESETS: Record<ContentXlSizePreset, ContentXlPresetConfig> = {
lineHeight: "2.25rem",
iconSize: "2rem",
moreIcon1Size: "1rem",
moreIcon2Size: "2rem",
editButtonSize: "md",
editButtonPadding: "p-1",
iconRowMarginBottom: "0rem",
@@ -94,7 +92,6 @@ const CONTENT_XL_PRESETS: Record<ContentXlSizePreset, ContentXlPresetConfig> = {
lineHeight: "1.75rem",
iconSize: "1.5rem",
moreIcon1Size: "0.75rem",
moreIcon2Size: "1.5rem",
editButtonSize: "sm",
editButtonPadding: "p-0.5",
iconRowMarginBottom: "0.25rem",
@@ -168,14 +165,16 @@ function ContentXl({
</div>
)}
{/* The tertiary icon is a peer of the primary one: same box, same
line-height floor, same size. */}
{MoreIcon2 && (
<div className="opal-content-xl-more-icon-container shrink-0">
<div
className="opal-content-xl-icon-container shrink-0"
style={{ minHeight: config.lineHeight }}
>
<MoreIcon2
className="opal-content-xl-icon"
style={{
width: config.moreIcon2Size,
height: config.moreIcon2Size,
}}
style={{ width: config.iconSize, height: config.iconSize }}
/>
</div>
)}
+2 -2
View File
@@ -62,8 +62,8 @@ Invalid combinations (e.g. `sizePreset="headline" + variant="body"`) are exclude
| `description` | `string` | — | Optional description below the title |
| `editable` | `boolean` | `false` | Enable inline editing of the title |
| `onTitleChange` | `(newTitle: string) => void` | — | Called when user commits an edit |
| `moreIcon1` | `IconFunctionComponent` | — | Secondary icon in icon row (ContentXl only) |
| `moreIcon2` | `IconFunctionComponent` | — | Tertiary icon in icon row (ContentXl only) |
| `moreIcon1` | `IconFunctionComponent` | — | Secondary icon in icon row (ContentXl only); colored with the secondary foreground (`text-03`) |
| `moreIcon2` | `IconFunctionComponent` | — | Tertiary icon in icon row (ContentXl only); boxed and sized exactly like `icon` |
| `color` | `ColorTypes` | `"default"` | Icon and title color pair. `"muted-success"` / `"muted-warning"` color only the icon and leave the text at `text-03` |
| `strikethrough` | `boolean` | `false` | Strike the title through, for a title whose option is switched off |
@@ -154,6 +154,12 @@
color: var(--content-foreground-icon);
}
/* The secondary icon between two marks reads as a connector, so it takes
the secondary foreground (text-03 in every mode), not the icon color. */
.opal-content-xl-more-icon-container > .opal-content-xl-icon {
color: var(--content-foreground-secondary);
}
/* ---------------------------------------------------------------------------
Title row — title (or input) + edit button
--------------------------------------------------------------------------- */
@@ -1,6 +1,6 @@
"use client";
import { Logo } from "@/lib/app/components";
import { FoldableLogo } from "@/lib/app/components";
import AgentAvatar from "@/refresh-components/avatars/AgentAvatar";
import Text from "@/refresh-components/texts/Text";
import { MinimalAgent } from "@/lib/agents/types";
@@ -65,7 +65,7 @@ export default function WelcomeMessage({
gap={2}
width="fit"
>
<Logo folded size={32} />
<FoldableLogo folded size={32} />
<Text as="p" dir="auto" headingH2>
{greeting}
</Text>
+6 -2
View File
@@ -6,7 +6,7 @@ import { Text } from "@opal/components";
import { Spacer } from "@opal/components";
import { RequestNewVerificationEmail } from "../waiting-on-verification/RequestNewVerificationEmail";
import { User } from "@/lib/types";
import { Logo } from "@/lib/app/components";
import { FoldableLogo } from "@/lib/app/components";
import { NEXT_PUBLIC_CLOUD_ENABLED } from "@/lib/constants";
import { useTranslations } from "next-intl";
@@ -62,7 +62,11 @@ export default function Verify({ user }: VerifyProps) {
return (
<main>
<div className="min-h-screen flex flex-col items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<Logo folded size={64} className="mx-auto w-fit animate-pulse" />
<FoldableLogo
folded
size={64}
className="mx-auto w-fit animate-pulse"
/>
{!error ? (
<>
<Spacer rem={0.5} />
@@ -4,7 +4,7 @@ import { AuthTypeMetadata } from "@/lib/auth/types";
import { redirect } from "next/navigation";
import { User } from "@/lib/types";
import { RequestNewVerificationEmail } from "./RequestNewVerificationEmail";
import { Logo } from "@/lib/app/components";
import { FoldableLogo } from "@/lib/app/components";
import { Text } from "@opal/components";
import { markdown, richNodes } from "@opal/utils";
import { getTranslations } from "next-intl/server";
@@ -36,7 +36,7 @@ export default async function Page() {
return (
<main>
<div className="min-h-screen flex flex-col items-center justify-center py-12 px-4 sm:px-6 lg:px-8 gap-4">
<Logo folded size={64} className="mx-auto w-fit" />
<FoldableLogo folded size={64} className="mx-auto w-fit" />
<div className="flex flex-col gap-2">
<Text as="span">
{markdown(
@@ -7,7 +7,7 @@ import type { BuildMessage } from "@/app/craft/types/streamingTypes";
import type { StreamItem } from "@/app/craft/types/displayTypes";
jest.mock("@/lib/app/components", () => ({
Logo: () => <div data-testid="onyx-logo" />,
FoldableLogo: () => <div data-testid="onyx-logo" />,
}));
jest.mock("@/components/chat/MinimalMarkdown", () => ({
@@ -7,7 +7,7 @@ import { CopyButton } from "@opal/components";
import { Hoverable } from "@opal/core";
import { SvgAlertCircle } from "@opal/icons";
import { AnimatePresence, motion } from "motion/react";
import { Logo } from "@/lib/app/components";
import { FoldableLogo } from "@/lib/app/components";
import SetupCard from "@/app/craft/components/setup-requests/SetupCard";
import { ExternalAppUserResponse } from "@/app/craft/v1/apps/registry";
import { errorHandlingFetcher } from "@/lib/fetcher";
@@ -308,7 +308,7 @@ export default function BuildMessageList({
<Hoverable.Root key={message.id} group="craftAgentMessage" width="full">
<div className="flex items-start gap-3 py-4">
<div className="shrink-0 h-9 flex items-center">
<Logo onyxBranded folded size={24} />
<FoldableLogo onyxBranded folded size={24} />
</div>
<div className="flex-1 flex flex-col gap-2 min-w-0">
{visibleSavedRender ? (
@@ -402,7 +402,7 @@ export default function BuildMessageList({
{showStreamingArea && (
<div className="flex items-start gap-3 py-4">
<div className="shrink-0 mt-2">
<Logo onyxBranded folded size={24} />
<FoldableLogo onyxBranded folded size={24} />
</div>
<div className="flex-1 flex flex-col gap-2 min-w-0">
{streamRender?.pinnedTodo && (
@@ -5,7 +5,7 @@ import { useTranslations } from "next-intl";
import { BuildFile } from "@/app/craft/contexts/UploadFilesContext";
import { useVideoBackgroundToggleClick } from "@/app/craft/components/video-background/useVideoBackgroundToggleClick";
import Text from "@/refresh-components/texts/Text";
import { Logo } from "@/lib/app/components";
import { FoldableLogo } from "@/lib/app/components";
import CraftInputBar, {
CraftInputBarHandle,
} from "@/app/craft/components/CraftInputBar";
@@ -73,7 +73,11 @@ export default function BuildWelcome({
className="flex flex-row items-baseline gap-2 select-none"
onClick={handleWordmarkClick}
>
<Logo onyxBranded size={28} className="translate-y-[6px]" />
<FoldableLogo
onyxBranded
size={28}
className="translate-y-[6px]"
/>
<Text
text05
style={{
@@ -4,7 +4,7 @@ import { useTranslations } from "next-intl";
import { Text } from "@opal/components";
import { SvgArrowUp, SvgPlus } from "@opal/icons";
import { cn } from "@opal/utils";
import { Logo } from "@/lib/app/components";
import { FoldableLogo } from "@/lib/app/components";
const PROMPT_PILL_KEYS = [
"engineering",
@@ -50,7 +50,11 @@ export default function WelcomePageMock({
<div className="w-full max-w-3xl">
<div className="flex flex-row items-center justify-between gap-4 pb-6">
<div className="flex flex-row items-baseline gap-2 select-none">
<Logo onyxBranded size={28} className="translate-y-[6px]" />
<FoldableLogo
onyxBranded
size={28}
className="translate-y-[6px]"
/>
<span
className="text-text-05"
style={{
@@ -1,6 +1,6 @@
"use client";
import { Logo } from "@/lib/app/components";
import { FoldableLogo } from "@/lib/app/components";
import { Button } from "@opal/components";
import { SvgEditBig, SvgExternalLink } from "@opal/icons";
import { useTranslations } from "next-intl";
@@ -24,7 +24,7 @@ export default function SidePanelHeader({
return (
<header className="flex items-center justify-between px-4 py-3 border-b border-border-01 bg-background">
<Logo />
<FoldableLogo />
<div className="flex items-center gap-1">
<Button
prominence="tertiary"
@@ -1,6 +1,6 @@
"use client";
import { Logo } from "@/lib/app/components";
import { FoldableLogo } from "@/lib/app/components";
import { useSettings } from "@/lib/settings/hooks";
import { useTranslations } from "next-intl";
@@ -10,7 +10,7 @@ export default function OnyxInitializingLoader() {
return (
<div className="mx-auto my-auto animate-pulse">
<Logo folded size={96} className="mx-auto mb-3" />
<FoldableLogo folded size={96} className="mx-auto mb-3" />
<p className="text-lg text-text font-semibold">
{t("initializing.text", { appName })}
</p>
+52 -20
View File
@@ -10,38 +10,41 @@ import { cn } from "@opal/utils";
import Text from "@/refresh-components/texts/Text";
import Truncated from "@/refresh-components/texts/Truncated";
import { SvgOnyxLogo, SvgOnyxLogoTyped } from "@opal/logos";
import { IconProps } from "@opal/types";
export interface LogoProps {
folded?: boolean;
size?: number;
className?: string;
// Always render the real Onyx logo, ignoring enterprise white-label settings
// (custom logo / application name). Used by Onyx-branded surfaces like Craft.
export interface LogoProps extends IconProps {
// Always render the real Onyx mark, ignoring the enterprise custom logo.
// Used by Onyx-branded surfaces like Craft.
onyxBranded?: boolean;
}
export function Logo({ folded, size, className, onyxBranded }: LogoProps) {
/**
* The app mark alone: the uploaded custom logo when one is set, the Onyx
* mark otherwise. Shaped like an Opal icon, so it fits any `icon` slot and
* honours the size or style the slot passes.
*/
export function Logo({ size, className, style, onyxBranded }: LogoProps) {
const t = useTranslations("common");
const resolvedSize = size ?? DEFAULT_LOGO_SIZE_PX;
const { enterprise, logoUrl } = useSettings();
const logoDisplayStyle = enterprise?.logo_display_style;
const applicationName = enterprise?.application_name;
const { logoUrl } = useSettings();
if (onyxBranded) {
return folded ? (
<SvgOnyxLogo size={resolvedSize} className={cn("shrink-0", className)} />
) : (
<SvgOnyxLogoTyped size={resolvedSize} className={className} />
if (onyxBranded || !logoUrl) {
return (
<SvgOnyxLogo
size={resolvedSize}
className={cn("shrink-0", className)}
style={style}
/>
);
}
const logo = logoUrl ? (
return (
<div
className={cn(
"aspect-square rounded-full overflow-hidden relative shrink-0",
className
)}
style={{ height: resolvedSize }}
style={{ height: resolvedSize, ...style }}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
@@ -50,9 +53,38 @@ export function Logo({ folded, size, className, onyxBranded }: LogoProps) {
className="object-cover object-center w-full h-full"
/>
</div>
) : (
<SvgOnyxLogo size={resolvedSize} className={cn("shrink-0", className)} />
);
}
export interface FoldableLogoProps extends LogoProps {
folded?: boolean;
}
/**
* The sidebar's brand block: the mark, and when unfolded the application
* name and the "powered by" line beside it, per the enterprise display style.
*/
export function FoldableLogo({
folded,
size,
className,
onyxBranded,
}: FoldableLogoProps) {
const t = useTranslations("common");
const resolvedSize = size ?? DEFAULT_LOGO_SIZE_PX;
const { enterprise } = useSettings();
const logoDisplayStyle = enterprise?.logo_display_style;
const applicationName = enterprise?.application_name;
if (onyxBranded) {
return folded ? (
<Logo onyxBranded size={resolvedSize} className={className} />
) : (
<SvgOnyxLogoTyped size={resolvedSize} className={className} />
);
}
const logo = <Logo size={resolvedSize} className={className} />;
const renderNameAndPoweredBy = (opts: {
includeLogo: boolean;
@@ -98,7 +130,7 @@ export function Logo({ folded, size, className, onyxBranded }: LogoProps) {
return applicationName ? (
renderNameAndPoweredBy({ includeLogo: true, includeName: true })
) : folded ? (
<SvgOnyxLogo size={resolvedSize} className={cn("shrink-0", className)} />
<Logo size={resolvedSize} className={className} />
) : (
<SvgOnyxLogoTyped size={resolvedSize} className={className} />
);
+2 -2
View File
@@ -5,11 +5,11 @@ import { toast } from "@opal/layouts";
import { ChatSession } from "@/app/app/interfaces";
import { DEFAULT_AGENT_ID } from "@/lib/constants";
import { LOCAL_STORAGE_KEYS } from "@/lib/sidebar/constants";
import { Logo } from "@/lib/app/components";
import { FoldableLogo } from "@/lib/app/components";
import type { IconFunctionComponent } from "@opal/types";
export function renderSidebarLogo(folded: boolean): IconFunctionComponent {
return (props) => React.createElement(Logo, { ...props, folded });
return (props) => React.createElement(FoldableLogo, { ...props, folded });
}
/** Parameters shared by move-operation helpers. */
@@ -6,8 +6,8 @@ import { Permission } from "@/lib/types";
import useSWR, { mutate } from "swr";
import { buildSimilarCredentialInfoURL } from "@/lib/connectors/utils";
import { getSourceDisplayName, getSourceMetadata } from "@/lib/sources";
import { useEffect, useMemo, useRef, useState } from "react";
import { renderSidebarLogo } from "@/lib/sidebar/utils";
import { useEffect, useRef, useState } from "react";
import { Logo } from "@/lib/app/components";
import { deleteCredential, linkCredential } from "@/lib/credential";
import { submitFiles } from "@/lib/connectors/svc";
import { submitGoogleSite } from "@/lib/connectors/svc";
@@ -159,8 +159,6 @@ export default function AddConnector({
const router = useRouter();
const settings = useSettings();
// The app icon honours white-labelling, like the sidebar's.
const AppIcon = useMemo(() => renderSidebarLogo(true), []);
const defaultPruneFreqHours = settings.default_pruning_freq
? settings.default_pruning_freq / 3600
: 600; // 25 days fallback until settings load
@@ -536,7 +534,7 @@ export default function AddConnector({
<SettingsLayouts.Header
icon={sourceMetadata.icon}
moreIcon1={SvgArrowExchange}
moreIcon2={AppIcon}
moreIcon2={Logo}
title={displayName}
description={t("header.description", {
source: displayName,