1
0
Fork 0

feat: v5 general improvements (#2948)

* feat(v5): General fixes and improvements

* add changeset

* fix: bundlesize
This commit is contained in:
Paul Jankowski 2026-08-03 15:14:02 -04:00 committed by GitHub
parent 8c01259d81
commit 5eac1fd6bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 152 additions and 93 deletions

View file

@ -0,0 +1,30 @@
---
"@docsearch/docusaurus-adapter": patch
"@docsearch/react": patch
"@docsearch/css": patch
---
feat(v5): General UI styling updates and fixes
- New `--docsearch-font-family` variable, used by the search button, keyboard
keys, modal, and sidepanel. It replaces the system font stacks that were
duplicated across `sidepanel.css` and `button.css`, so overriding one
variable now themes every DocSearch surface
- The search input and modal heading are `1rem` at every breakpoint
(previously `0.875rem` with a mobile-only override)
- `.DocSearch-Title` uses `line-height: 1.5em` instead of `0.5em` and adds
`overflow-wrap: anywhere`, so long titles wrap instead of overlapping (#2908)
- Hit icon `svg` sizing moved into `.DocSearch-Hit-icon`. The
`.DocSearch-Hit-icon--small` modifier is replaced by
`.DocSearch-Hit-icon--start`, which top-aligns the icon; recent
conversations use it
- The Ask AI button icon centers with `display: inline-flex` instead of
`margin-block-start`/`align-self` overrides
- Removed the 2px offset on the Ask AI sources action text
- `SourcesPanel` accepts `pluralTitleText`. `titleText` is now the singular
label, and the trigger renders `{count} {label}`
- `AskAiScreenTranslations` and `ConversationScreenTranslations` expose
`relatedSourcesTextPlural`
- Docusaurus adapter: `theme.SearchModal.askAiScreen.relatedSourcesText` is
now the singular "Source", and the new
`theme.SearchModal.askAiScreen.relatedSourcesTextPlural` provides "Sources"

View file

@ -39,6 +39,7 @@ const translations: DocSearchTranslations & {
askAiScreen: { askAiScreen: {
disclaimerText: string; disclaimerText: string;
relatedSourcesText: string; relatedSourcesText: string;
relatedSourcesTextPlural: string;
thinkingText: string; thinkingText: string;
copyButtonText: string; copyButtonText: string;
copyButtonCopiedText: string; copyButtonCopiedText: string;
@ -237,8 +238,13 @@ const translations: DocSearchTranslations & {
}), }),
relatedSourcesText: translate({ relatedSourcesText: translate({
id: 'theme.SearchModal.askAiScreen.relatedSourcesText', id: 'theme.SearchModal.askAiScreen.relatedSourcesText',
message: 'Related sources', message: 'Source',
description: 'The text for related sources', description: 'Text shown describing a singular related source',
}),
relatedSourcesTextPlural: translate({
id: 'theme.SearchModal.askAiScreen.relatedSourcesTextPlural',
message: 'Sources',
description: 'Text shown describing multiple related sources',
}), }),
thinkingText: translate({ thinkingText: translate({
id: 'theme.SearchModal.askAiScreen.thinkingText', id: 'theme.SearchModal.askAiScreen.thinkingText',

View file

@ -2,7 +2,7 @@
"files": [ "files": [
{ {
"path": "packages/docsearch-css/dist/style.css", "path": "packages/docsearch-css/dist/style.css",
"maxSize": "8 kB" "maxSize": "8.05 kB"
}, },
{ {
"path": "packages/docsearch-react/dist/umd/index.js", "path": "packages/docsearch-react/dist/umd/index.js",

View file

@ -1,12 +1,12 @@
import { test, expect } from './fixtures'; import { test, expect, gatherA11yViolations } from './fixtures';
test.describe('a11y', () => { test.describe('a11y > Modal', () => {
test.beforeEach(async ({ docSearch }) => { test.beforeEach(async ({ docSearch }) => {
await docSearch.goto(); await docSearch.goto();
await docSearch.waitForLoad(); await docSearch.waitForLoad();
}); });
test.fail('Smoke test > Modal', async ({ docSearch, axe }, testInfo) => { test('Smoke test', async ({ docSearch, axe }, testInfo) => {
await docSearch.openModal(); await docSearch.openModal();
const scanResults = await axe().include('.DocSearch-Container').analyze(); const scanResults = await axe().include('.DocSearch-Container').analyze();
@ -16,10 +16,42 @@ test.describe('a11y', () => {
contentType: 'application/json', contentType: 'application/json',
}); });
expect(scanResults.violations).toEqual([]); // 6 is the current number of reported violations
expect(
gatherA11yViolations(scanResults.violations).length
).toBeLessThanOrEqual(6);
}); });
test.fail('Smoke test > Sidepanel', async ({ sidepanel, axe }, testInfo) => { test('Search results', async ({ docSearch, axe }, testInfo) => {
await docSearch.openModal();
await docSearch.typeQueryMatching();
await expect(docSearch.hits).toBeVisible();
const scanResults = await axe()
.include('#docsearch-hits_docsearch-list')
.analyze();
await testInfo.attach('a11y-scan-results-modal-search-results', {
body: JSON.stringify(scanResults.violations, null, 2),
contentType: 'application/json',
});
// 24 is the current number of reported violations
expect(
gatherA11yViolations(scanResults.violations).length
).toBeLessThanOrEqual(24);
});
});
test.describe('a11y > Sidepanel', () => {
test.beforeEach(async ({ docSearch, sidepanel }) => {
await docSearch.goto();
await sidepanel.waitForLoad();
});
test('Smoke test', async ({ sidepanel, axe }, testInfo) => {
await sidepanel.openSidepanel(); await sidepanel.openSidepanel();
const scanResults = await axe() const scanResults = await axe()
@ -31,6 +63,9 @@ test.describe('a11y', () => {
contentType: 'application/json', contentType: 'application/json',
}); });
expect(scanResults.violations).toEqual([]); // 4 is the current number of reported violations
expect(
gatherA11yViolations(scanResults.violations).length
).toBeLessThanOrEqual(4);
}); });
}); });

View file

@ -102,6 +102,10 @@ export class SidepanelPage {
this.sidepanel = page.locator('.DocSearch-Sidepanel-Container'); this.sidepanel = page.locator('.DocSearch-Sidepanel-Container');
} }
async waitForLoad(): Promise<void> {
await expect(this.sidepanelButton).toBeVisible({ timeout: 10000 });
}
async openSidepanel(): Promise<void> { async openSidepanel(): Promise<void> {
await this.sidepanelButton.click(); await this.sidepanelButton.click();
await expect(this.sidepanel).toHaveClass(/is-open/, { await expect(this.sidepanel).toHaveClass(/is-open/, {
@ -137,4 +141,10 @@ export const test = base.extend<{
}, },
}); });
export { expect }; type AxeScanResults = Awaited<ReturnType<AxeBuilder['analyze']>>;
function gatherA11yViolations(violations: AxeScanResults['violations']) {
return violations.flatMap((v) => v.nodes);
}
export { expect, gatherA11yViolations };

View file

@ -84,14 +84,13 @@
/* Ask AI Button */ /* Ask AI Button */
.DocSearch-Hit-AskAIButton { .DocSearch-Hit-AskAIButton {
align-items: center;
height: 40px; height: 40px;
} }
.DocSearch-Hit-AskAIButton-icon { .DocSearch-Hit-AskAIButton-icon {
display: inline-flex;
align-items: center;
margin-inline-end: 8px; margin-inline-end: 8px;
margin-block-start: 0;
align-self: unset;
} }
.DocSearch-Hit-AskAIButton-title { .DocSearch-Hit-AskAIButton-title {
@ -1011,20 +1010,23 @@
gap: 1ch; gap: 1ch;
border-radius: 100vw; border-radius: 100vw;
border: 1px solid var(--docsearch-subtle-color); border: 1px solid var(--docsearch-subtle-color);
background: transparent; background-color: transparent;
padding-inline: 0.75em; padding-inline: 0.75em;
padding-block: 0.375em; padding-block: 0.375em;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
color: var(--docsearch-text-color); color: var(--docsearch-text-color);
transition: all 0.2s ease; transition:
background-color 0.2s ease,
border-color 0.2s ease,
color 0.2s ease;
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
transition: none; transition: none;
} }
&:hover { &:hover {
background: var(--docsearch-soft-primary-color); background-color: var(--docsearch-soft-primary-color);
color: var(--docsearch-highlight-color); color: var(--docsearch-highlight-color);
border-color: var(--docsearch-soft-primary-color); border-color: var(--docsearch-soft-primary-color);
} }
@ -1047,7 +1049,6 @@
} }
.DocSearch-AskAiScreen-Sources-Action-text { .DocSearch-AskAiScreen-Sources-Action-text {
margin-block-start: 2px;
font-size: 1em; font-size: 1em;
line-height: 1.15em; line-height: 1.15em;
white-space: nowrap; white-space: nowrap;

View file

@ -1,6 +1,8 @@
/* Variables */ /* Variables */
:root { :root {
/* stylelint-disable-next-line value-keyword-case */
--docsearch-font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
--docsearch-primary-color: rgb(0 61 255); --docsearch-primary-color: rgb(0 61 255);
--docsearch-primary-dark-color: rgb(2 46 185 / 100%); --docsearch-primary-dark-color: rgb(2 46 185 / 100%);
--docsearch-soft-primary-color: rgb(0 61 255 / 10%); --docsearch-soft-primary-color: rgb(0 61 255 / 10%);

View file

@ -11,6 +11,7 @@
justify-content: space-between; justify-content: space-between;
padding: 0 8px; padding: 0 8px;
user-select: none; user-select: none;
font-family: var(--docsearch-font-family);
} }
.DocSearch-Button-Container { .DocSearch-Button-Container {
@ -58,10 +59,7 @@
justify-content: center; justify-content: center;
position: relative; position: relative;
font-size: 14px; font-size: 14px;
font-family: font-family: var(--docsearch-font-family);
system-ui,
-apple-system,
sans-serif;
transition-property: all; transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 100ms; transition-duration: 100ms;

View file

@ -50,6 +50,7 @@
margin: 60px auto auto; margin: 60px auto auto;
max-width: var(--docsearch-modal-width); max-width: var(--docsearch-modal-width);
position: relative; position: relative;
font-family: var(--docsearch-font-family);
} }
.DocSearch-Popover-Positioner { .DocSearch-Popover-Positioner {
@ -442,7 +443,7 @@
color: var(--docsearch-text-color); color: var(--docsearch-text-color);
flex: 1; flex: 1;
font: inherit; font: inherit;
font-size: 0.875rem; font-size: 1rem;
font-weight: 400; font-weight: 400;
height: 100%; height: 100%;
outline: none; outline: none;
@ -656,8 +657,9 @@
color: var(--docsearch-text-color); color: var(--docsearch-text-color);
vertical-align: middle; vertical-align: middle;
font-size: 1.1em; font-size: 1.1em;
line-height: 0.5em; line-height: 1.5em;
font-weight: 300; font-weight: 300;
overflow-wrap: anywhere;
} }
.DocSearch-Title strong { .DocSearch-Title strong {
@ -777,23 +779,16 @@
height: 24px; height: 24px;
stroke-width: var(--docsearch-icon-stroke-width); stroke-width: var(--docsearch-icon-stroke-width);
width: 24px; width: 24px;
svg {
height: 20px;
width: 20px;
}
}
.DocSearch-Hit-icon--start {
align-self: flex-start; align-self: flex-start;
margin-block-start: 0.8em; margin-block-start: 0.5em;
}
.DocSearch-Hit-icon svg {
height: 20px;
width: 20px;
}
.DocSearch-Hit-icon--small {
height: 1rem;
width: 1rem;
}
.DocSearch-Hit-icon--small svg {
height: 1rem;
width: 1rem;
} }
.DocSearch-Hit-action, .DocSearch-Hit-action,
@ -1114,10 +1109,6 @@ assistive tech users */
--docsearch-footer-height: 48px; --docsearch-footer-height: 48px;
} }
.DocSearch-Input {
font-size: 1rem;
}
/* Prevent body scroll on modal-open for mobile */ /* Prevent body scroll on modal-open for mobile */
/* https://stackoverflow.com/a/24727206/6276948 */ /* https://stackoverflow.com/a/24727206/6276948 */

View file

@ -35,6 +35,7 @@
user-select: none; user-select: none;
border: 0; border: 0;
transition: all 150ms ease-in-out; transition: all 150ms ease-in-out;
font-family: var(--docsearch-font-family);
} }
.DocSearch-SidepanelButton.floating { .DocSearch-SidepanelButton.floating {
@ -90,18 +91,7 @@
all: unset; all: unset;
cursor: pointer; cursor: pointer;
transition: all 150ms ease-in-out; transition: all 150ms ease-in-out;
font-family: font-family: var(--docsearch-font-family);
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Open Sans',
'Helvetica Neue',
sans-serif;
line-height: normal; line-height: normal;
} }
@ -122,7 +112,6 @@
.DocSearch-Sidepanel-Container { .DocSearch-Sidepanel-Container {
position: fixed; position: fixed;
min-width: 0; min-width: 0;
will-change: transform, width, opacity;
overflow: hidden; overflow: hidden;
box-shadow: box-shadow:
0 0 0 1px rgb(35 38 59 / 5%), 0 0 0 1px rgb(35 38 59 / 5%),
@ -132,12 +121,6 @@
width: 100svw; width: 100svw;
} }
@media screen and (prefers-reduced-motion: reduce) {
.DocSearch-Sidepanel-Container {
transition: none;
}
}
.DocSearch-Sidepanel-Container.inline, .DocSearch-Sidepanel-Container.inline,
.DocSearch-Sidepanel-Container.floating { .DocSearch-Sidepanel-Container.floating {
top: 0; top: 0;
@ -166,7 +149,10 @@
@media screen and (width >= 769px) { @media screen and (width >= 769px) {
.DocSearch-Sidepanel-Container { .DocSearch-Sidepanel-Container {
width: var(--sp-width); width: var(--sp-width);
transition: all 280ms cubic-bezier(0.22, 1, 0.36, 1); transition:
transform 280ms cubic-bezier(0.22, 1, 0.36, 1),
opacity 280ms cubic-bezier(0.22, 1, 0.36, 1),
width 280ms cubic-bezier(0.22, 1, 0.36, 1);
} }
.DocSearch-Sidepanel-Container.floating { .DocSearch-Sidepanel-Container.floating {
@ -209,6 +195,12 @@
} }
} }
@media screen and (prefers-reduced-motion: reduce) {
.DocSearch-Sidepanel-Container {
transition: none;
}
}
.DocSearch-Sidepanel-Container.is-open { .DocSearch-Sidepanel-Container.is-open {
opacity: 1; opacity: 1;
} }
@ -233,18 +225,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: var(--docsearch-sidepanel-background); background-color: var(--docsearch-sidepanel-background);
font-family: font-family: var(--docsearch-font-family);
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Open Sans',
'Helvetica Neue',
sans-serif;
overflow: hidden; overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
height: 100%; height: 100%;
@ -442,18 +423,7 @@
min-height: 1.5rem; min-height: 1.5rem;
height: 1.5rem; height: 1.5rem;
overflow: hidden; overflow: hidden;
font-family: font-family: var(--docsearch-font-family);
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Open Sans',
'Helvetica Neue',
sans-serif;
} }
.DocSearch-Sidepanel-Prompt--textarea::placeholder { .DocSearch-Sidepanel-Prompt--textarea::placeholder {

View file

@ -31,7 +31,10 @@ export type AskAiScreenTranslations = Partial<
Omit<ToolCallTranslations, 'searchingText' | 'toolCallResultText'> & { Omit<ToolCallTranslations, 'searchingText' | 'toolCallResultText'> & {
// Misc texts // Misc texts
disclaimerText: string; disclaimerText: string;
/** Text shown describing a singular related source. */
relatedSourcesText: string; relatedSourcesText: string;
/** Text shown describing multiple related sources. */
relatedSourcesTextPlural: string;
thinkingText: string; thinkingText: string;
copyButtonText: string; copyButtonText: string;
copyButtonCopiedText: string; copyButtonCopiedText: string;
@ -187,6 +190,7 @@ function AskAiExchangeCard({
stoppedStreamingText = 'You stopped this response', stoppedStreamingText = 'You stopped this response',
errorTitleText = 'Chat error', errorTitleText = 'Chat error',
relatedSourcesText, relatedSourcesText,
relatedSourcesTextPlural,
suggestedPromptsTitleText = 'Suggested prompts', suggestedPromptsTitleText = 'Suggested prompts',
} = translations; } = translations;
@ -355,7 +359,11 @@ function AskAiExchangeCard({
)} )}
</div> </div>
<div className="DocSearch-AskAiScreen-Answer-Footer"> <div className="DocSearch-AskAiScreen-Answer-Footer">
<SourcesPanel links={urlsToDisplay} titleText={relatedSourcesText} /> <SourcesPanel
links={urlsToDisplay}
titleText={relatedSourcesText}
pluralTitleText={relatedSourcesTextPlural}
/>
<FeedbackActions <FeedbackActions
id={messageId} id={messageId}
showActions={showActions} showActions={showActions}

View file

@ -31,8 +31,10 @@ export type ConversationScreenTranslations = Partial<
reasoningText: string; reasoningText: string;
/** Text show while assistant is thinking. */ /** Text show while assistant is thinking. */
thinkingText: string; thinkingText: string;
/** Text shown describing related sources. */ /** Text shown describing a singular related source. */
relatedSourcesText: string; relatedSourcesText: string;
/** Text shown describing multiple related sources. */
relatedSourcesTextPlural: string;
/** Message that's shown when user has stopped the streaming of a message. */ /** Message that's shown when user has stopped the streaming of a message. */
stoppedStreamingText: string; stoppedStreamingText: string;
/** Text shown for copy button on code snippets. */ /** Text shown for copy button on code snippets. */
@ -119,6 +121,7 @@ const ConversationExchange = React.forwardRef<
thinkingText = 'Thinking...', thinkingText = 'Thinking...',
searchingText = 'Searching...', searchingText = 'Searching...',
relatedSourcesText, relatedSourcesText,
relatedSourcesTextPlural,
stoppedStreamingText = 'You stopped this response', stoppedStreamingText = 'You stopped this response',
preToolCallText = 'Searching...', preToolCallText = 'Searching...',
toolCallResultText = 'Searched for', toolCallResultText = 'Searched for',
@ -281,6 +284,7 @@ const ConversationExchange = React.forwardRef<
<SourcesPanel <SourcesPanel
links={urlsToDisplay} links={urlsToDisplay}
titleText={relatedSourcesText} titleText={relatedSourcesText}
pluralTitleText={relatedSourcesTextPlural}
/> />
<FeedbackActions <FeedbackActions
isSidepanel={true} isSidepanel={true}

View file

@ -8,16 +8,20 @@ import { Popover } from './ui/Popover';
interface SourcesProps { interface SourcesProps {
links: ExtractedLink[]; links: ExtractedLink[];
titleText?: string; titleText?: string;
pluralTitleText?: string;
} }
export function SourcesPanel({ export function SourcesPanel({
links, links,
titleText = 'Sources', titleText = 'Source',
pluralTitleText = 'Sources',
}: SourcesProps): JSX.Element | null { }: SourcesProps): JSX.Element | null {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const triggerRef = React.useRef<HTMLButtonElement>(null); const triggerRef = React.useRef<HTMLButtonElement>(null);
const [boundary, setBoundary] = React.useState<HTMLElement | null>(null); const [boundary, setBoundary] = React.useState<HTMLElement | null>(null);
const sourcesTitle = links.length > 1 ? pluralTitleText : titleText;
useEffect(() => { useEffect(() => {
if (!open) { if (!open) {
return undefined; return undefined;
@ -73,7 +77,7 @@ export function SourcesPanel({
<ContentIcon /> <ContentIcon />
</span> </span>
<span className="DocSearch-AskAiScreen-Sources-Action-text"> <span className="DocSearch-AskAiScreen-Sources-Action-text">
{links.length} sources {links.length} {sourcesTitle}
</span> </span>
</Popover.Trigger> </Popover.Trigger>
<Popover.Popup <Popover.Popup
@ -83,7 +87,7 @@ export function SourcesPanel({
alignOffset={-16} alignOffset={-16}
collisionBoundary={boundary ?? 'clipping-ancestors'} collisionBoundary={boundary ?? 'clipping-ancestors'}
> >
<Popover.Title>{titleText}</Popover.Title> <Popover.Title>{sourcesTitle}</Popover.Title>
<ul className="DocSearch-AskAiScreen-Sources"> <ul className="DocSearch-AskAiScreen-Sources">
{links.map((l) => ( {links.map((l) => (
<li key={l.url} className="DocSearch-AskAiScreen-Sources-source"> <li key={l.url} className="DocSearch-AskAiScreen-Sources-source">

View file

@ -38,7 +38,7 @@ export function RecentConversationsResults({
title={recentConversationsTitle} title={recentConversationsTitle}
collection={recentConversations} collection={recentConversations}
renderIcon={() => ( renderIcon={() => (
<div className="DocSearch-Hit-icon DocSearch-Hit-icon--small"> <div className="DocSearch-Hit-icon DocSearch-Hit-icon--start">
<MessageIcon /> <MessageIcon />
</div> </div>
)} )}