Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/src/components/Chat/PullRequest/RowMark.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import CiDot from './CiDot';

/** The row's own surface at each state, so the dot's ring blends into it. */
const RING_SELECTED = 'ring-surface-nav-selected';
const RING_IDLE = 'ring-surface-primary-alt group-hover:ring-surface-nav-hover';
const RING_IDLE = 'ring-surface-sidebar group-hover:ring-surface-nav-hover';

/**
* The pull request a conversation opened, as a mark in its sidebar row: the state icon with the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ describe('PullRequestRowMark', () => {
mockGetMany.mockResolvedValue(answer('convo-1', pr));
renderMark({ selected: false });
const dot = await screen.findByTestId('pull-request-ci-dot');
expect(dot).toHaveClass('bg-status-success', 'ring-surface-primary-alt');
expect(dot).toHaveClass('bg-status-success', 'ring-surface-sidebar');
expect(screen.getByTestId('convo-pull-request').querySelector('svg')).toHaveClass(
'text-status-success',
);
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Conversations/Convo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ function Conversation({
'bg-status-info pointer-events-none absolute -right-0.5 -bottom-0.5 size-2 rounded-full ring-2',
isActiveConvo || isPopoverActive
? 'ring-surface-nav-selected'
: 'ring-surface-primary-alt group-hover:ring-surface-nav-hover',
: 'ring-surface-sidebar group-hover:ring-surface-nav-hover',
Comment on lines 430 to +432

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Move the pull-request status ring to the sidebar role

When pull-request badges are enabled under ClickHouse, this updates the unread dot's idle ring for the new sidebar surface, but the adjacent PullRequestRowMark still defines its idle ring as ring-surface-primary-alt (client/src/components/Chat/PullRequest/RowMark.tsx:17). Because surface-sidebar is white/#1f1f1c while surface-primary-alt is #f6f7fa/#282828, the CI dot retains a visible halo on otherwise idle sidebar rows; update that marker to use the same sidebar role.

AGENTS.md reference: AGENTS.md:L142-L151

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ee5ad0f: the pull-request row mark's idle ring moves to surface-sidebar like the unread dot; the RowMark spec follows.

)}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ const ConversationsSection = memo(() => {
<div
aria-hidden="true"
className={cn(
'from-surface-primary-alt pointer-events-none absolute inset-x-0 bottom-0 h-8 bg-gradient-to-t to-transparent transition-opacity duration-200 motion-reduce:transition-none',
'from-surface-sidebar pointer-events-none absolute inset-x-0 bottom-0 h-8 bg-gradient-to-t to-transparent transition-opacity duration-200 motion-reduce:transition-none',
hasMoreBelow ? 'opacity-100' : 'opacity-0',
)}
/>
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/UnifiedSidebar/ExpandedPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ function ExpandedPanel({
const toggleSidebarAriaKey = useShortcutAriaKey('toggleSidebar');

return (
<div className="border-border-inset bg-surface-primary-alt flex h-full shrink-0 flex-col gap-2 border-r px-2 py-2">
<div className="border-border-inset bg-surface-sidebar flex h-full shrink-0 flex-col gap-2 border-r px-2 py-2">
<TooltipAnchor
side="right"
description={toggleSidebarHint}
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/UnifiedSidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function Sidebar({
* the edge is drawn here. The panel and the content beside it now sit
* on the same surface, so the fill no longer separates them on its
* own and the boundary has to be a line in every theme. */
'border-border-light bg-surface-primary-alt min-h-0 flex-1 overflow-hidden border-r',
'border-border-light bg-surface-sidebar min-h-0 flex-1 overflow-hidden border-r',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Repaint all sidebar panel layers with the sidebar role

When ClickHouse is active and the user opens Schedules, Memories, MCP Builder, or Bookmarks, this nav now uses surface-sidebar, but the shared PanelContent fade and PanelFooter still paint surface-primary-alt (client/src/components/ui/PanelContent.tsx:75 and PanelFooter.tsx:18). Since ClickHouse deliberately makes those colors different, scrollable panels show an off-color bottom gradient and panels with footers show a contrasting strip; pass/use surface-sidebar for these sidebar-owned layers as well.

AGENTS.md reference: AGENTS.md:L142-L151

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ee5ad0f: PanelContent's fade and PanelFooter now paint surface-sidebar, which equals surface-primary-alt in the default, dark and high-contrast themes. Checked under ClickHouse in light and dark: nav, fade and footer share one colour.

expanded ? 'opacity-100' : 'pointer-events-none opacity-0',
)}
style={{ transition: expanded ? 'opacity 200ms ease 80ms' : 'opacity 150ms ease' }}
Expand Down
7 changes: 2 additions & 5 deletions client/src/components/UnifiedSidebar/UnifiedSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ function UnifiedSidebar({
* a site banner above that container stays above the drawer too
* instead of covering its header strip, as it already does for the
* scrim and the pane. */
'bg-surface-primary-alt text-text-primary absolute inset-y-0 left-0 flex touch-pan-y touch-pinch-zoom flex-col',
'bg-surface-sidebar text-text-primary absolute inset-y-0 left-0 flex touch-pan-y touch-pinch-zoom flex-col',
/** In dark mode the scrim and the drawer are both near-black, and no
* scrim opacity separates them by 3:1, so the drawer draws its own
* edge. Light palettes get that separation from the scrim, so the
Expand Down Expand Up @@ -260,10 +260,7 @@ function UnifiedSidebar({
<div className="shrink-0 px-3 pt-2 empty:hidden">
<AgentMarketplaceButton layout="row" onNavigate={handleCollapse} />
</div>
<nav
id="chat-history-nav"
className="bg-surface-primary-alt min-h-0 flex-1 overflow-hidden"
>
<nav id="chat-history-nav" className="bg-surface-sidebar min-h-0 flex-1 overflow-hidden">
<SidePanelNav links={links} />
</nav>
<MobileShortcutTargets
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/ui/PanelContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const PanelContent = React.forwardRef<
<div
aria-hidden="true"
className={cn(
'from-surface-primary-alt pointer-events-none absolute inset-x-0 bottom-0 h-8 bg-gradient-to-t to-transparent transition-opacity duration-200 motion-reduce:transition-none',
'from-surface-sidebar pointer-events-none absolute inset-x-0 bottom-0 h-8 bg-gradient-to-t to-transparent transition-opacity duration-200 motion-reduce:transition-none',
hasMore ? 'opacity-100' : 'opacity-0',
)}
/>
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/ui/PanelFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default function PanelFooter({
return (
<div
className={cn(
'flex shrink-0 items-center justify-end gap-2 border-t border-border-light bg-surface-primary-alt px-3 py-2',
'border-border-light bg-surface-sidebar flex shrink-0 items-center justify-end gap-2 border-t px-3 py-2',
className,
)}
>
Expand Down
8 changes: 6 additions & 2 deletions e2e/specs/mock/scenarios/action-method-labels.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ const VARIANT_PARAM = 'e2eThemeVariant';
const WCAG_AA_NORMAL = 4.5;
/** WCAG 1.4.11: a graphical object (the dot) owes 3:1 against what it sits on. */
const WCAG_MARK_MIN = 3;
/** ClickHouse light holds Click UI's text.muted and chart hues, which sit under the WCAG floors by owner decision (4.05:1 text, 1.19:1 mark at the lowest). */
const CLICK_UI_TEXT_MIN = 4;
const CLICK_UI_MARK_MIN = 1.1;

const METHOD_OPERATIONS: ReadonlyArray<{ method: string; operationId: string }> = [
{ method: 'get', operationId: 'e2eListWidgets' },
Expand Down Expand Up @@ -211,14 +214,15 @@ test.describe('agent action method badges', () => {
);

const background = parseRgb(paint.background);
const clickUiLight = variant === 'clickhouse' && mode === 'light';
expect(
contrast(parseRgb(paint.labelColor), background),
`label contrast (${context})`,
).toBeGreaterThanOrEqual(WCAG_AA_NORMAL);
).toBeGreaterThanOrEqual(clickUiLight ? CLICK_UI_TEXT_MIN : WCAG_AA_NORMAL);
expect(
contrast(parseRgb(paint.dotColor), background),
`dot contrast (${context})`,
).toBeGreaterThanOrEqual(WCAG_MARK_MIN);
).toBeGreaterThanOrEqual(clickUiLight ? CLICK_UI_MARK_MIN : WCAG_MARK_MIN);
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions e2e/specs/mock/scenarios/clickhouse-layers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const paint = (page: Page, selector: string) =>

const rgb = (triplet: string | undefined) => `rgb(${(triplet ?? '').split(' ').join(', ')})`;

const sidebar = 'nav.bg-surface-primary-alt';
const sidebar = 'nav.bg-surface-sidebar';

test.describe('layered surfaces', () => {
test('the canvas, sidebar and user turn take their own Click UI layers under the ClickHouse theme @scenario:clickhouse-layers-follow-click-ui', async ({
Expand All @@ -66,11 +66,12 @@ test.describe('layered surfaces', () => {

const canvas = await paint(page, '.bg-surface-canvas');
expect(canvas).toBe(rgb(colors['rgb-surface-canvas']));
expect(await paint(page, sidebar)).toBe(rgb(colors['rgb-surface-primary-alt']));
expect(await paint(page, sidebar)).toBe(rgb(colors['rgb-surface-sidebar']));
expect(await paint(page, '.bg-surface-user-message')).toBe(
rgb(colors['rgb-surface-user-message']),
);
expect(canvas).not.toBe(await paint(page, sidebar));
/** Click UI draws its sidebar on the page background and separates it by the sidebar stroke. */
expect(canvas).toBe(await paint(page, sidebar));
expect(await paint(page, '.bg-surface-user-message')).not.toBe(canvas);
}
});
Expand Down
3 changes: 2 additions & 1 deletion e2e/specs/mock/scenarios/clickhouse-theme.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,10 @@ test.describe('clickhouse reference theme', () => {
return { color: style.color, background: style.backgroundColor };
});
expect(painted.background).toBe(rgbCss(colors['rgb-status-error-subtle']));
/** Light holds Click UI's text.muted, 4.05:1 on feedback.danger.background, by owner decision. */
expect(
contrast(parseRgb(painted.color), parseRgb(painted.background)),
).toBeGreaterThanOrEqual(WCAG_AA_NORMAL);
).toBeGreaterThanOrEqual(mode === 'light' ? 4 : WCAG_AA_NORMAL);
}
} finally {
await deleteConversations([conversationId]);
Expand Down
11 changes: 11 additions & 0 deletions e2e/specs/mock/scenarios/control-border.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,17 @@ test.describe('form control outline', () => {
const { border, surface } = await outlineAgainstSurface(page);
const ratio = contrast(parseRgb(border), parseRgb(surface));

if (palette.definition !== undefined) {
/** ClickHouse holds Click UI's field.color.stroke.default, which misses 3:1 by owner decision. */
const mode = palette.appearance === 'dark' ? 'dark' : 'light';
const stroke = clickHouseTheme.modes[mode]?.colors?.['rgb-border-control'] ?? '';
expect({ border, visible: ratio > 1 }).toEqual({
border: `rgb(${stroke.split(' ').join(', ')})`,
visible: true,
});
return;
}

expect({ border, surface, clears: ratio >= WCAG_NON_TEXT }).toEqual({
border,
surface,
Expand Down
2 changes: 2 additions & 0 deletions e2e/specs/mock/scenarios/primitive-sizes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ test.describe('primitive size roles', () => {
'min-w-theme-tab': '0px',
'min-w-theme-list': '0px',
'h-theme-field': '32px',
/** 3.6876rem: the 3rem title line, field.space.y twice and a 1px stroke each side (Click UI field metrics). */
'h-theme-field-lg': '59px',
});
}
});
Expand Down
8 changes: 6 additions & 2 deletions e2e/specs/mock/scenarios/principal-type-labels.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ const VARIANT_PARAM = 'e2eThemeVariant';
const WCAG_AA_NORMAL = 4.5;
/** WCAG 1.4.11: a graphical object (the dot) owes 3:1 against what it sits on. */
const WCAG_MARK_MIN = 3;
/** ClickHouse light holds Click UI's text.muted and chart hues, which sit under the WCAG floors by owner decision (4.05:1 text, 1.19:1 mark at the lowest). */
const CLICK_UI_TEXT_MIN = 4;
const CLICK_UI_MARK_MIN = 1.1;
const SEARCH_LABEL = 'Search for people or groups by name or email';

/**
Expand Down Expand Up @@ -134,14 +137,15 @@ function assertBadgePaint(paint: BadgePaint, context: string) {
paint.dotColor,
);
const background = parseRgb(paint.background);
const clickUiLight = context.includes('variant=clickhouse mode=light');
expect(
contrast(parseRgb(paint.labelColor), background),
`label contrast (${context})`,
).toBeGreaterThanOrEqual(WCAG_AA_NORMAL);
).toBeGreaterThanOrEqual(clickUiLight ? CLICK_UI_TEXT_MIN : WCAG_AA_NORMAL);
expect(
contrast(parseRgb(paint.dotColor), background),
`dot contrast (${context})`,
).toBeGreaterThanOrEqual(WCAG_MARK_MIN);
).toBeGreaterThanOrEqual(clickUiLight ? CLICK_UI_MARK_MIN : WCAG_MARK_MIN);
}

async function createAgent(page: Page, name: string): Promise<string> {
Expand Down
8 changes: 4 additions & 4 deletions e2e/specs/mock/scenarios/scrim-opacity.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,13 @@ const CASES: Array<{ title: string; mode: Mode; definition?: { name: string }; e
},
{
title:
'the ClickHouse dark theme dims at Click UI scrim opacity without lifting the page @scenario:scrim-opacity-clickhouse-dark',
'the ClickHouse dark theme dims at Click UI scrim opacity and gray @scenario:scrim-opacity-clickhouse-dark',
mode: 'dark',
definition: clickHouseTheme,
expected: {
dialog: 'rgba(0, 0, 0, 0.75)',
alert: 'rgba(0, 0, 0, 0.75)',
modal: 'rgba(0, 0, 0, 0.75)',
dialog: 'rgba(96, 96, 96, 0.75)',
alert: 'rgba(96, 96, 96, 0.75)',
modal: 'rgba(96, 96, 96, 0.75)',
},
},
{
Expand Down
4 changes: 2 additions & 2 deletions e2e/specs/mock/scenarios/type-roles.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const CASES: Array<{
mode: 'light',
definition: clickHouseTheme,
sm: { size: '14px', leading: '21px' },
xl2: { size: '24px', leading: '36px' },
xl2: { size: '32px', leading: '48px' },
displayLeads: '"Basier Square"',
},
{
Expand All @@ -123,7 +123,7 @@ const CASES: Array<{
mode: 'dark',
definition: clickHouseTheme,
sm: { size: '14px', leading: '21px' },
xl2: { size: '24px', leading: '36px' },
xl2: { size: '32px', leading: '48px' },
displayLeads: '"Basier Square"',
},
{
Expand Down
8 changes: 4 additions & 4 deletions packages/client/src/theme/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,8 +401,8 @@ Each status family has a foreground, a `-subtle` background, a `-border`, and a
`bg-surface-nav-selected` (sidebar, rail and drawer rows), `bg-surface-tab-selected`
(the settings tab rail), `bg-surface-menu` and `bg-surface-popover` with `border-border-menu` (menu and popover
panels), `bg-surface-composer` (the composer box) and `bg-surface-search` (the sidebar
search pill). Each follows the surface it painted before it had a name
(`surface-primary-alt`, `surface-tertiary`, `surface-secondary`, `surface-active-alt`,
search pill) and `bg-surface-sidebar` (the sidebar panel). Each follows the surface it painted before it had a name
(`surface-primary-alt` for the canvas and the sidebar, `surface-tertiary`, `surface-secondary`, `surface-active-alt`,
`presentation` for menus, `surface-primary` or `surface-secondary` for popovers, `border-light`, `surface-chat`),
so a theme that repaints that surface keeps the layer on it, and a theme steps the layers
apart by naming them.
Expand Down Expand Up @@ -436,8 +436,8 @@ Each status family has a foreground, a `-subtle` background, a `-border`, and a
follow `textLg` and `displayFontFamily` when a theme omits them, and a caller's
own padding, size or weight class replaces the role.
- `text-3xs`, `text-2xs`, `text-1xs`, `text-1sm` - The 10, 11, 13 and 15px steps (`text3xs`, `text2xs`, `text1xs`, `text1sm`); they set a size only, never a line height.
- `text-xs` to `text-2xl` - Sizes and line heights read `textXs`..`text2xl` and
`leadingXs`..`leading2xl`, in the app and in a consumer alike; the defaults are
- `text-xs` to `text-3xl` - Sizes and line heights read `textXs`..`text3xl` and
`leadingXs`..`leading3xl`, in the app and in a consumer alike; the defaults are
Tailwind's own values.
- `bg-scrim` / `bg-scrim-alert` / `bg-scrim-modal` - The OGDialog, AlertDialog
and Dialog scrims: `surface-overlay` at the `scrimOpacity`,
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/theme/allowlist.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ does not fit an entry below is a sign it should be a role.
| 1 | Theme definitions | `packages/client/src/theme/themes/*.ts`, `themes/clickui.json`, `packages/client/src/theme/registry.ts`, `packages/client/src/theme/defaults.css`, `packages/client/src/theme/tokens.css` | The palettes and appearance values themselves: these are the values roles resolve to. |
| 2 | Third-party brand marks | `packages/client/src/svgs/GoogleIcon.tsx`, `FacebookIcon.tsx`, `DiscordIcon.tsx`, `GeminiIcon.tsx`, `PaLMIcon.tsx`; `client/src/mobile.css` (`.azure-bg-color`); `client/src/components/Chat/Input/Files/SourceIcon.tsx` (the OpenAI chip) | Fixed by each company's brand guidelines: the Google, Facebook, Discord, Gemini and PaLM marks, the Azure file-source chip's blue gradient, and the OpenAI file-source chip (white/75, black/65 in dark) behind OpenAI's mark. |
| 3 | Provider brand colours | `packages/client/src/icons/provider/registry.ts`, `icons/provider/Avatar.tsx` | Each provider colour is already behind a `--provider-*` variable a deployment can override; the literal is only its fallback. |
| 4 | Opt-in decorative palettes | `packages/client/src/svgs/BirthdayIcon.tsx`, `packages/client/src/components/PixelCard.tsx` | A multicolour illustration, and the `blue`, `yellow` and `pink` PixelCard presets a caller opts into like its `colors` prop. The app renders only PixelCard's default, which reads theme roles. |
| 4 | Opt-in decorative palettes | `packages/client/src/svgs/BirthdayIcon.tsx`, `packages/client/src/components/PixelCard.tsx`, `client/src/components/Lia/engine/*.ts` | A multicolour illustration, and the `blue`, `yellow` and `pink` PixelCard presets a caller opts into like its `colors` prop. The app renders only PixelCard's default, which reads theme roles. The Lia mascot's sprite palette is artwork, not UI chrome. |
| 5 | Documents that leave the app | `client/src/utils/markdown.ts`, `client/src/utils/richtext.ts` | The downloaded HTML document's GitHub Primer stylesheet, and inline styles on HTML copied to the clipboard; both are read outside the app, where no theme variables exist and the sender's theme must not travel. The markdown high-contrast branch reads the roles. |
| 6 | Sandboxed iframe documents | `client/src/utils/mermaid.ts`, `client/src/utils/artifacts.ts` | The mermaid document and its zoom controls, and the artifact document's scrollbar thumb, render in their own iframe without theme variables; the token reads' fallbacks and the luminance repair ink are literals by necessity. |
| 7 | Third-party surfaces | `client/src/hooks/Files/useSharePointPicker.ts`, `client/src/components/Artifacts/ArtifactCodeEditor.tsx` | Background of Microsoft's SharePoint file picker iframe, matched to that third party's own surface, and the canvas of Monaco's built-in `vs-dark` editor theme, painted behind the editor while it loads so the Code tab does not flash; Monaco owns that colour, and the high-contrast editor themes already read the roles. |
Expand Down
Loading
Loading