Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -1238,16 +1238,39 @@ summary::-webkit-details-marker {
min-width: 0;
}

.smart-search-rotating-text,
.smart-search-prompt-row,
.smart-search-suggestion-row {
display: none;
}

@media (min-width: 1024px) {
.smart-search-rotating-text {
display: flex;
}

.smart-search-prompt-row,
.smart-search-suggestion-row {
display: flex;
}
}

.smart-search-rotating-text {
min-height: 1.5rem;
align-items: center;
justify-content: center;
gap: 0.35rem;
padding-inline: 0.25rem;
color: var(--text-soft);
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.01em;
}

.smart-search-rotating-query {
color: var(--clinical-accent);
}

.answer-suggestion-label {
color: var(--text-soft);
font-size: 0.6875rem;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
const focusRing =
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]";

const SMART_HINT_ROTATION_MS = 3200;

type DropdownItem = {
id: string;
label: string;
Expand Down Expand Up @@ -54,15 +56,40 @@ function OptionShell({ active, children, hint }: { active: boolean; children: Re

export type CommandSurfacePlacement = "bottom-dock" | "inline";

function ContextHintRow({ examples, onPickExample }: { examples: string[]; onPickExample: (example: string) => void }) {
function SmartRotatingHint({ examples, modeLabel }: { examples: string[]; modeLabel: string }) {
const [activeExampleIndex, setActiveExampleIndex] = useState(0);
const activeExample = examples[activeExampleIndex % examples.length];

useEffect(() => {
if (examples.length <= 1) return;
const intervalId = window.setInterval(() => {
setActiveExampleIndex((current) => (current + 1) % examples.length);
}, SMART_HINT_ROTATION_MS);
return () => window.clearInterval(intervalId);
}, [examples]);

if (!activeExample) return null;

return (
<div data-testid="smart-search-rotating-text" className="smart-search-rotating-text" aria-live="polite">
<span>Smart search</span>
<span aria-hidden="true">·</span>
<span>
Try <span className="smart-search-rotating-query">&ldquo;{activeExample}&rdquo;</span> in {modeLabel}.
</span>
</div>
);
}

function SmartPromptRow({ examples, onPickExample }: { examples: string[]; onPickExample: (example: string) => void }) {
return (
<AnswerSuggestionChips
suggestions={examples}
onPick={onPickExample}
label="Suggested"
testId="smart-search-suggestion-row"
label="Prompts"
testId="smart-search-prompt-row"
layout="scroll"
className="smart-search-suggestion-row"
className="smart-search-prompt-row"
/>
);
}
Expand Down Expand Up @@ -545,14 +572,7 @@ export function UniversalSearchCommandSurface({
placement === "bottom-dock" ? "gap-1" : "gap-2",
)}
>
<ContextHintRow
examples={config.examples}
onPickExample={(example) => {
onQueryChange(example);
onDropdownOpenChange(true);
onFocusSearchInput?.();
}}
/>
<SmartRotatingHint examples={config.examples} modeLabel={mode.label} />
<div
className="relative w-full"
onKeyDownCapture={(event) => {
Expand Down Expand Up @@ -585,6 +605,14 @@ export function UniversalSearchCommandSurface({
/>
) : null}
</div>
<SmartPromptRow
examples={config.examples}
onPickExample={(example) => {
onQueryChange(example);
onDropdownOpenChange(true);
onFocusSearchInput?.();
}}
/>
<ScopeChipRow scopes={config.scopes} activeScopes={commandScopes} onToggle={toggleScope} modeLabel={mode.label} />
<style>{`@keyframes universal-command-fade { from { opacity: 0; transform: translateY(2px); } to { opacity: 1; transform: none; } }`}</style>
</div>
Expand Down
48 changes: 31 additions & 17 deletions tests/ui-overlap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,41 +154,55 @@ test.describe("Header element overlap coverage", () => {
});
}

test("desktop smart search keeps suggested words above the composer", async ({ page }) => {
test("desktop smart search keeps rotating text above and prompts below the composer", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 900 });
await mockDemoDashboard(page);
await gotoHome(page);

const suggestionRow = page.getByTestId("smart-search-suggestion-row");
await expect(suggestionRow).toBeVisible();
await expect(suggestionRow.getByRole("button", { name: "lithium level timing" })).toBeVisible();
await expect(suggestionRow.getByRole("button", { name: "clozapine ANC monitoring" })).toBeVisible();
const rotatingText = page.getByTestId("smart-search-rotating-text");
const promptRow = page.getByTestId("smart-search-prompt-row");
await expect(rotatingText).toBeVisible();
await expect(rotatingText).toContainText("Smart search");
await expect(promptRow).toBeVisible();
await expect(promptRow.getByRole("button", { name: "lithium level timing" })).toBeVisible();
await expect(promptRow.getByRole("button", { name: "clozapine ANC monitoring" })).toBeVisible();

const geometry = await page.evaluate(() => {
const row = document.querySelector('[data-testid="smart-search-suggestion-row"]');
const input = document.querySelector('[data-testid="global-search-input"]');
if (!row || !input) return null;
const rowRect = row.getBoundingClientRect();
const inputRect = input.getBoundingClientRect();
return { rowBottom: rowRect.bottom, inputTop: inputRect.top };
const hint = document.querySelector('[data-testid="smart-search-rotating-text"]');
const prompt = document.querySelector('[data-testid="smart-search-prompt-row"]');
const pill = document.querySelector(".answer-footer-search-pill");
if (!hint || !prompt || !pill) return null;
const hintRect = hint.getBoundingClientRect();
const promptRect = prompt.getBoundingClientRect();
const pillRect = pill.getBoundingClientRect();
return {
hintBottom: hintRect.bottom,
pillTop: pillRect.top,
pillBottom: pillRect.bottom,
promptTop: promptRect.top,
};
});

expect(geometry, "suggestion row and smart search input must render").not.toBeNull();
expect(geometry!.rowBottom, "suggestions should sit above the smart search bar").toBeLessThanOrEqual(
geometry!.inputTop + 1,
expect(geometry, "smart search hint, composer, and prompt row must render").not.toBeNull();
expect(geometry!.hintBottom, "rotating text should sit above the smart search bar").toBeLessThanOrEqual(
geometry!.pillTop + 1,
);
expect(geometry!.promptTop, "smart prompts should sit below the smart search bar").toBeGreaterThanOrEqual(
geometry!.pillBottom - 1,
);

await suggestionRow.getByRole("button", { name: "lithium level timing" }).click();
await promptRow.getByRole("button", { name: "lithium level timing" }).click();
await expect(page.locator('[data-testid="global-search-input"]:visible').first()).toHaveValue(
"lithium level timing",
);
});

test("phone smart search does not show the desktop suggested words row", async ({ page }) => {
test("phone smart search does not show the desktop rotating text or prompt row", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 820 });
await mockDemoDashboard(page);
await gotoHome(page);

await expect(page.getByTestId("smart-search-suggestion-row")).toBeHidden();
await expect(page.getByTestId("smart-search-rotating-text")).toBeHidden();
await expect(page.getByTestId("smart-search-prompt-row")).toBeHidden();
});
});
Loading