diff --git a/client/src/Providers/__tests__/themeCache.spec.ts b/client/src/Providers/__tests__/themeCache.spec.ts index 93e2df5c8b6..962b59ee9e3 100644 --- a/client/src/Providers/__tests__/themeCache.spec.ts +++ b/client/src/Providers/__tests__/themeCache.spec.ts @@ -224,7 +224,7 @@ describe('theme cache storage', () => { * - definitions with one or both mode blocks absent. * A new role, or a new fallback, joins by construction. */ -const PIN = { fingerprint: '1.5.dxwdxg', digest: '1n3ywnj' }; +const PIN = { fingerprint: '1.5.1ln9dy9', digest: '1ohs8yp' }; const digestOf = (text: string): string => { let hash = 5381; diff --git a/client/src/components/Chat/Input/__tests__/panelRadius.spec.ts b/client/src/components/Chat/Input/__tests__/panelRadius.spec.ts index 2acc42657d0..14c77324589 100644 --- a/client/src/components/Chat/Input/__tests__/panelRadius.spec.ts +++ b/client/src/components/Chat/Input/__tests__/panelRadius.spec.ts @@ -7,14 +7,26 @@ const PACKAGE_COMPONENTS = path.resolve( __dirname, '../../../../../../packages/client/src/components', ); -const PACKAGE_PANELS = ['SendActions.tsx']; +const PACKAGE_PANELS = [ + 'SendActions.tsx', + 'TimePicker.tsx', + 'ControlCombobox.tsx', + 'MultiSelect.tsx', +]; +const COMPONENTS_DIR = path.resolve(CHAT_DIR, '..'); +const CLIENT_PANELS = [ + 'ui/LocalizedDateRangePicker.tsx', + 'Nav/AccountSettings.tsx', + 'Prompts/editor/VariablesDropdown.tsx', + 'Variables/Editor.tsx', +]; const SOURCE = /\.tsx$/; const SKIPPED = /(__tests__|\.spec\.|\.test\.)/; const PANEL_TAG = - /<(?:Ariakit\.(?:Menu|Popover|SelectPopover|ComboboxPopover|Hovercard)|Popover\.Content)\b(?:[^\n]*>$|[\s\S]*?\n\s*\/?>)/gm; + /<(?:(?:Ariakit\.)?(?:Popover|SelectPopover|ComboboxPopover|Hovercard)|(?:Ariakit|Menu)\.Menu|(?:Popover\.)?Content)(?![.\w])(?:[^\n]*>$|[\s\S]*?\n\s*\/?>)/gm; const PANEL_CLASS = /className="popover\b[^"]*"/g; const PANEL_CONSTANT = /const (?:menuClasses|panelClasses|popoverClasses)\b[^;]*;/g; -const PANEL_ROLE = /\brounded-theme-(?:menu-panel|popover)\b/; +const PANEL_ROLE = /\b(?:rounded-theme-(?:menu-panel|compact-panel|popover|surface)|popover-ui)\b/; const DELEGATED = /className=\{[A-Za-z.]+\}/; function sourcesUnder(dir: string): string[] { @@ -30,10 +42,11 @@ function sourcesUnder(dir: string): string[] { }); } -describe('composer menu and popover panels', () => { +describe('menu, popover and picker panels', () => { const files = [ ...SCAN_DIRS.flatMap((dir) => sourcesUnder(path.join(CHAT_DIR, dir))), ...PACKAGE_PANELS.map((file) => path.join(PACKAGE_COMPONENTS, file)), + ...CLIENT_PANELS.map((file) => path.join(COMPONENTS_DIR, file)), ]; it('finds the panels the theme roles are meant to cover', () => { @@ -45,7 +58,7 @@ describe('composer menu and popover panels', () => { ...(text.match(PANEL_CLASS) ?? []), ]; }); - expect(panels.length).toBeGreaterThanOrEqual(10); + expect(panels.length).toBeGreaterThanOrEqual(15); }); it.each(files.map((file) => [path.relative(CHAT_DIR, file), file]))( diff --git a/client/src/components/Nav/AccountSettings.tsx b/client/src/components/Nav/AccountSettings.tsx index e707df98c41..f08d6957142 100644 --- a/client/src/components/Nav/AccountSettings.tsx +++ b/client/src/components/Nav/AccountSettings.tsx @@ -55,7 +55,7 @@ function HelpSubmenu({ {hasHelpFaq && (
diff --git a/e2e/specs/mock/scenarios/panel-shape.spec.ts b/e2e/specs/mock/scenarios/panel-shape.spec.ts new file mode 100644 index 00000000000..ce674c55d94 --- /dev/null +++ b/e2e/specs/mock/scenarios/panel-shape.spec.ts @@ -0,0 +1,102 @@ +import { expect, test } from '@playwright/test'; +import type { Locator, Page } from '@playwright/test'; +import { clickHouseTheme } from '../../../../packages/client/src/theme/themes/clickhouse'; +import { NEW_CHAT_PATH } from '../helpers'; +import { probeStyle } from './style.helpers'; + +/** + * The account menu and its help submenu are `.popover-ui` panels, whose rule draws the menu radius + * role, so the `rounded-lg` they also carried never painted. The default theme keeps the menu + * corner they have always shown, ClickHouse brings them onto its menu corner, and a reference theme + * that names only `menuRadius` reshapes them, with no rule written for the account menu itself. + * Every expected corner is resolved by the browser from a probe. + */ +test.describe.configure({ timeout: 120_000 }); +test.use({ viewport: { width: 1280, height: 800 } }); + +const REFERENCE_RADIUS = '3px'; + +const referenceTheme = { + version: 1, + name: 'menu-radius-reference', + modes: { + light: { appearance: { menuRadius: REFERENCE_RADIUS } }, + dark: { appearance: { menuRadius: REFERENCE_RADIUS } }, + }, +}; + +type ThemeChoice = 'default' | 'clickhouse' | 'reference'; + +const definitions: Record, object> = { + clickhouse: clickHouseTheme, + reference: referenceTheme, +}; + +async function openNewChat(page: Page, theme: ThemeChoice) { + await page.addInitScript( + (definition) => { + localStorage.removeItem('theme-colors'); + localStorage.removeItem('theme-name'); + if (definition === null) { + localStorage.removeItem('theme-definition'); + localStorage.removeItem('theme-source'); + return; + } + localStorage.setItem('theme-definition', JSON.stringify(definition)); + localStorage.setItem('theme-source', 'definition'); + }, + theme === 'default' ? null : definitions[theme], + ); + await page.goto(NEW_CHAT_PATH, { timeout: 15000 }); + await expect(page.getByTestId('text-input')).toBeVisible({ timeout: 30000 }); +} + +const cornerOf = (locator: Locator) => + locator.evaluate((node) => getComputedStyle(node).borderTopLeftRadius); + +/** Open the account menu, then its help submenu, and read both panels' corners. */ +async function accountPanelCorners(page: Page) { + await page.getByTestId('nav-user').click(); + const accountMenu = page.getByRole('menu').filter({ has: page.getByTestId('nav-settings') }); + await expect(accountMenu).toBeVisible(); + const account = await cornerOf(accountMenu); + + await page.getByRole('menuitem', { name: 'Help' }).click(); + const helpMenu = page.getByRole('menu').filter({ hasNot: page.getByTestId('nav-settings') }); + await expect(helpMenu).toBeVisible(); + const help = await cornerOf(helpMenu); + return { account, help }; +} + +test.describe('account menu panel shape', () => { + test('the default theme keeps the account menu corners @scenario:default-theme-account-panels-unchanged', async ({ + page, + }) => { + await openNewChat(page, 'default'); + const menuCorner = await probeStyle(page, 'popover-ui', 'border-top-left-radius'); + const corners = await accountPanelCorners(page); + + expect(menuCorner).toBe('11.2px'); + expect(corners).toEqual({ account: menuCorner, help: menuCorner }); + }); + + test('ClickHouse brings the account menus onto its menu corner @scenario:clickhouse-account-panels-follow-menu-corner', async ({ + page, + }) => { + await openNewChat(page, 'clickhouse'); + const menuCorner = await probeStyle(page, 'popover-ui', 'border-top-left-radius'); + const corners = await accountPanelCorners(page); + + expect(menuCorner).toBe('4px'); + expect(corners).toEqual({ account: menuCorner, help: menuCorner }); + }); + + test('a theme naming only the menu radius role reshapes the account menus @scenario:reference-theme-reshapes-account-panels', async ({ + page, + }) => { + await openNewChat(page, 'reference'); + const corners = await accountPanelCorners(page); + + expect(corners).toEqual({ account: REFERENCE_RADIUS, help: REFERENCE_RADIUS }); + }); +}); diff --git a/packages/client/src/components/ControlCombobox.tsx b/packages/client/src/components/ControlCombobox.tsx index 87adbc51784..5fe81c5c212 100644 --- a/packages/client/src/components/ControlCombobox.tsx +++ b/packages/client/src/components/ControlCombobox.tsx @@ -242,7 +242,7 @@ function ControlCombobox({ portal={portal} portalElement={portalElement} className={cn( - 'border-border-light bg-surface-secondary overflow-hidden rounded-xl border shadow-lg', + 'border-border-light bg-surface-secondary rounded-theme-menu-panel overflow-hidden border shadow-lg', popoverMaxHeight != null && 'flex flex-col', popoverClassName ?? 'animate-popover', )} diff --git a/packages/client/src/components/MultiSelect.tsx b/packages/client/src/components/MultiSelect.tsx index 9c41f642079..cd485ec608d 100644 --- a/packages/client/src/components/MultiSelect.tsx +++ b/packages/client/src/components/MultiSelect.tsx @@ -180,7 +180,7 @@ export default function MultiSelect({ finalFocus={selectRef} className={cn( 'animate-popover z-40 flex max-h-[18.75rem]', - 'flex-col overflow-auto overscroll-contain rounded-xl', + 'rounded-theme-menu-panel flex-col overflow-auto overscroll-contain', 'bg-surface-secondary text-text-primary px-1.5 py-1 shadow-lg', 'border-border-light border', 'outline-hidden', diff --git a/packages/client/src/components/TimePicker.tsx b/packages/client/src/components/TimePicker.tsx index d4377a2c7c3..f4bdac18fda 100644 --- a/packages/client/src/components/TimePicker.tsx +++ b/packages/client/src/components/TimePicker.tsx @@ -193,7 +193,7 @@ function PickerShell({ align="start" sideOffset={6} className={cn( - 'border-border-light bg-surface-secondary z-[999] max-w-[min(calc(100vw-1rem),var(--radix-popover-content-available-width))] rounded-xl border p-2 shadow-lg outline-hidden', + 'border-border-light bg-surface-secondary rounded-theme-menu-panel z-[999] max-w-[min(calc(100vw-1rem),var(--radix-popover-content-available-width))] border p-2 shadow-lg outline-hidden', // Same enter/exit motion as the shared Radix primitives (Combobox, // Select, DropdownMenu): fade + zoom from the trigger edge, with Radix's // own transform origin so the zoom grows out of wherever it was placed. diff --git a/packages/client/src/theme/README.md b/packages/client/src/theme/README.md index 0648e5537d0..717d4c6b1d2 100644 --- a/packages/client/src/theme/README.md +++ b/packages/client/src/theme/README.md @@ -65,8 +65,9 @@ Three primitives keep corners of their own outside that scale: the menu panel (` `tabRadius` (0.185rem, through `rounded-theme-tab`). The defaults are the corners they always drew. The composer's popovers read `popoverRadius` (1rem, `rounded-theme-popover`), the model selector's -panel `menuPanelRadius` (0.75rem, `rounded-theme-menu-panel`) and the send and stop buttons -`composerActionRadius` (a full circle, `rounded-theme-composer-action`), so a theme can bring them +panel and the form-control pickers `menuPanelRadius` (0.75rem, `rounded-theme-menu-panel`), the +date range picker `compactPanelRadius` (0.5rem, `rounded-theme-compact-panel`) and the send and +stop buttons `composerActionRadius` (a full circle, `rounded-theme-composer-action`), so a theme can bring them onto `menuRadius` or a square corner without moving the control or surface radii. An inline code chip in Markdown takes its weight from `inlineCodeWeight` (600). diff --git a/packages/client/src/theme/defaults.css b/packages/client/src/theme/defaults.css index d056a6759c0..1d48c8e2390 100644 --- a/packages/client/src/theme/defaults.css +++ b/packages/client/src/theme/defaults.css @@ -260,6 +260,7 @@ html { --theme-menu-radius: 0.7rem; --theme-popover-radius: var(--theme-radius-2xl, 1rem); --theme-menu-panel-radius: var(--theme-radius-xl, 0.75rem); + --theme-compact-panel-radius: var(--theme-radius-lg, 0.5rem); --theme-composer-action-radius: var(--theme-round-control-radius, 9999px); --theme-inline-code-weight: 600; --theme-tooltip-radius: 0.275rem; diff --git a/packages/client/src/theme/defaults.spec.ts b/packages/client/src/theme/defaults.spec.ts index 52d60bad0b9..0c870365fbd 100644 --- a/packages/client/src/theme/defaults.spec.ts +++ b/packages/client/src/theme/defaults.spec.ts @@ -95,6 +95,7 @@ const stockAliases: Partial< menuShadow: 'shadowLg', popoverRadius: 'radius2xl', menuPanelRadius: 'radiusXl', + compactPanelRadius: 'radiusLg', composerActionRadius: 'roundControlRadius', authControlRadius: 'radius2xl', authActionHeight: 'buttonHeight', diff --git a/packages/client/src/theme/registry.spec.ts b/packages/client/src/theme/registry.spec.ts index 97b5b449640..f0dd6d11b79 100644 --- a/packages/client/src/theme/registry.spec.ts +++ b/packages/client/src/theme/registry.spec.ts @@ -289,7 +289,12 @@ describe('theme registry', () => { name: 'corner-reference', modes: { light: { - appearance: { radius2xl: '3px', radiusXl: '5px', roundControlRadius: '7px' }, + appearance: { + radius2xl: '3px', + radiusXl: '5px', + radiusLg: '4px', + roundControlRadius: '7px', + }, }, }, }, @@ -298,6 +303,7 @@ describe('theme registry', () => { expect(resolved.appearance.popoverRadius).toBe('3px'); expect(resolved.appearance.menuPanelRadius).toBe('5px'); + expect(resolved.appearance.compactPanelRadius).toBe('4px'); expect(resolved.appearance.composerActionRadius).toBe('7px'); }); diff --git a/packages/client/src/theme/registry.ts b/packages/client/src/theme/registry.ts index 199a966de8c..e2cc68e9c44 100644 --- a/packages/client/src/theme/registry.ts +++ b/packages/client/src/theme/registry.ts @@ -226,6 +226,7 @@ export const themeAppearanceProperties: Readonly< menuRadius: '--theme-menu-radius', popoverRadius: '--theme-popover-radius', menuPanelRadius: '--theme-menu-panel-radius', + compactPanelRadius: '--theme-compact-panel-radius', composerActionRadius: '--theme-composer-action-radius', inlineCodeWeight: '--theme-inline-code-weight', tooltipRadius: '--theme-tooltip-radius', @@ -339,6 +340,7 @@ export const defaultAppearance: IThemeAppearance = Object.freeze({ menuRadius: '0.7rem', popoverRadius: '1rem', menuPanelRadius: '0.75rem', + compactPanelRadius: '0.5rem', composerActionRadius: '9999px', inlineCodeWeight: '600', tooltipRadius: '0.275rem', @@ -624,7 +626,7 @@ function withFloorableLengths(appearance: IThemeAppearance): IThemeAppearance { /** * Roles split out of a broader one, each paired with the role it read before. Headings drew the UI * family before the display role existed, theme-sized controls were padded by the shared spacing, - * and dialog titles were set in the `text-lg` step and the display family, the composer's popovers, the model selector and the send button drew `rounded-2xl`, `rounded-xl` and the round control corner, and the sign-in controls drew `rounded-2xl` and the other sign-in actions the default Button's height and corner, and the checkbox `rounded-sm`, so a theme that names + * and dialog titles were set in the `text-lg` step and the display family, the composer's popovers, the model selector, the date range picker and the send button drew `rounded-2xl`, `rounded-xl`, `rounded-lg` and the round control corner, and the sign-in controls drew `rounded-2xl` and the other sign-in actions the default Button's height and corner, and the checkbox `rounded-sm`, so a theme that names * the broader role and not the split one keeps what it drew. Pairs resolve in order, so a role can * follow one that is itself inherited; the other sign-in actions resolve before the sign-in corner * is derived, so they follow only a corner the theme names and otherwise keep `radiusLg`. @@ -638,6 +640,7 @@ const inheritedAppearance: ReadonlyArray<[keyof IThemeAppearance, keyof IThemeAp ['dialogTitleFontFamily', 'displayFontFamily'], ['popoverRadius', 'radius2xl'], ['menuPanelRadius', 'radiusXl'], + ['compactPanelRadius', 'radiusLg'], ['composerActionRadius', 'roundControlRadius'], ['authActionRadius', 'authControlRadius'], ['authActionRadius', 'radiusLg'], diff --git a/packages/client/src/theme/tailwind.spec.js b/packages/client/src/theme/tailwind.spec.js index 0d3f681ac1b..67cf362c900 100644 --- a/packages/client/src/theme/tailwind.spec.js +++ b/packages/client/src/theme/tailwind.spec.js @@ -162,6 +162,11 @@ describe('LibreChat Tailwind preset', () => { ['rounded-theme-popover', '--theme-popover-radius', defaultAppearance.popoverRadius], ['rounded-theme-checkbox', '--theme-checkbox-radius', defaultAppearance.checkboxRadius], ['rounded-theme-menu-panel', '--theme-menu-panel-radius', defaultAppearance.menuPanelRadius], + [ + 'rounded-theme-compact-panel', + '--theme-compact-panel-radius', + defaultAppearance.compactPanelRadius, + ], [ 'rounded-theme-composer-action', '--theme-composer-action-radius', @@ -330,6 +335,7 @@ describe('LibreChat Tailwind preset', () => { menuShadow: 'shadowLg', popoverRadius: 'radius2xl', menuPanelRadius: 'radiusXl', + compactPanelRadius: 'radiusLg', composerActionRadius: 'roundControlRadius', authControlRadius: 'radius2xl', authActionHeight: 'buttonHeight', diff --git a/packages/client/src/theme/themes/clickhouse.spec.tsx b/packages/client/src/theme/themes/clickhouse.spec.tsx index cc43050a571..a2f63c83c57 100644 --- a/packages/client/src/theme/themes/clickhouse.spec.tsx +++ b/packages/client/src/theme/themes/clickhouse.spec.tsx @@ -594,6 +594,7 @@ describe('clickhouse theme definition', () => { composerActionRadius: '0.25rem', popoverRadius: appearance.menuRadius, menuPanelRadius: appearance.menuRadius, + compactPanelRadius: appearance.menuRadius, inlineCodeWeight: '500', }); }); diff --git a/packages/client/src/theme/themes/clickhouse.ts b/packages/client/src/theme/themes/clickhouse.ts index 2946af37480..12ee105bdc1 100644 --- a/packages/client/src/theme/themes/clickhouse.ts +++ b/packages/client/src/theme/themes/clickhouse.ts @@ -459,6 +459,7 @@ const clickHouseShape = { menuRadius: '0.25rem', // genericMenu.panel.radii.all popoverRadius: '0.25rem', // genericMenu.panel.radii.all, the panel the composer popovers are menuPanelRadius: '0.25rem', // genericMenu.panel.radii.all + compactPanelRadius: '0.25rem', // genericMenu.panel.radii.all composerActionRadius: '0.25rem', // button.radii.all, Click UI draws its icon buttons square inlineCodeWeight: '500', // typography.font.weights.2 tooltipRadius: '0.25rem', // tooltip.radii.all diff --git a/packages/client/src/theme/themes/clickui.spec.ts b/packages/client/src/theme/themes/clickui.spec.ts index 8a5995b6eab..ed65179edf9 100644 --- a/packages/client/src/theme/themes/clickui.spec.ts +++ b/packages/client/src/theme/themes/clickui.spec.ts @@ -610,6 +610,7 @@ const appearanceSources: Partial> = { menuRadius: 'click.genericMenu.panel.radii.all', popoverRadius: 'click.genericMenu.panel.radii.all', menuPanelRadius: 'click.genericMenu.panel.radii.all', + compactPanelRadius: 'click.genericMenu.panel.radii.all', composerActionRadius: 'click.button.radii.all', authControlRadius: 'click.button.radii.all', inlineCodeWeight: 'typography.font.weights.2', diff --git a/packages/client/src/theme/types/index.ts b/packages/client/src/theme/types/index.ts index af1180cfa83..dfa5b0cb0c0 100644 --- a/packages/client/src/theme/types/index.ts +++ b/packages/client/src/theme/types/index.ts @@ -655,6 +655,8 @@ export interface IThemeAppearance { * control or surface radii. */ popoverRadius: string; menuPanelRadius: string; + /** The corners of the date range picker panel, which drew `rounded-lg`. */ + compactPanelRadius: string; composerActionRadius: string; /** The weight of an inline code chip in rendered Markdown. */ inlineCodeWeight: string; diff --git a/packages/client/tailwind.preset.cjs b/packages/client/tailwind.preset.cjs index 6546f032447..246857fc344 100644 --- a/packages/client/tailwind.preset.cjs +++ b/packages/client/tailwind.preset.cjs @@ -157,6 +157,7 @@ module.exports = { 'theme-surface': 'var(--theme-surface-radius, 1rem)', 'theme-popover': 'var(--theme-popover-radius, 1rem)', 'theme-menu-panel': 'var(--theme-menu-panel-radius, 0.75rem)', + 'theme-compact-panel': 'var(--theme-compact-panel-radius, 0.5rem)', 'theme-composer-action': 'var(--theme-composer-action-radius, 9999px)', 'theme-auth-control': 'var(--theme-auth-control-radius, 1rem)', 'theme-auth-action': 'var(--theme-auth-action-radius, 0.5rem)', diff --git a/packages/data-provider/src/theme.ts b/packages/data-provider/src/theme.ts index 1146f0823c2..bdd8f4747e3 100644 --- a/packages/data-provider/src/theme.ts +++ b/packages/data-provider/src/theme.ts @@ -427,6 +427,7 @@ const appearanceValidators = { menuRadius: isLength, popoverRadius: isLength, menuPanelRadius: isLength, + compactPanelRadius: isLength, composerActionRadius: isLength, inlineCodeWeight: isFontWeight, tooltipRadius: isLength,