feat(search): add SmartModeToggle pill component (#739)
Toggle pill with aria-pressed, active/resting styles matching the AND/OR operator button pattern, and mobile-expanded KI/Text labels. 4 vitest-browser-svelte specs (red/green). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
38
frontend/src/routes/search/SmartModeToggle.svelte
Normal file
38
frontend/src/routes/search/SmartModeToggle.svelte
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
|
|
||||||
|
let { smartMode = $bindable(false), onToggle }: { smartMode?: boolean; onToggle?: () => void } =
|
||||||
|
$props();
|
||||||
|
|
||||||
|
const label = $derived(smartMode ? m.search_toggle_smart_label() : m.search_toggle_keyword_label());
|
||||||
|
const labelSuffix = $derived(
|
||||||
|
smartMode ? m.search_toggle_smart_label_suffix() : m.search_toggle_keyword_label_suffix()
|
||||||
|
);
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
smartMode = !smartMode;
|
||||||
|
onToggle?.();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-pressed={smartMode}
|
||||||
|
onclick={toggle}
|
||||||
|
class="pointer-events-auto absolute top-1/2 right-2 flex -translate-y-1/2 cursor-pointer items-center gap-1.5 rounded-full px-2.5 py-1 text-[7.5px] font-bold uppercase outline-none focus-visible:ring-2 focus-visible:ring-brand-navy {smartMode
|
||||||
|
? 'border border-primary bg-primary text-primary-fg'
|
||||||
|
: 'border border-line bg-muted text-ink-2'}"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="h-2.5 w-2.5"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M12 2l2.09 6.26L20 10l-5.91 1.74L12 18l-2.09-6.26L4 10l5.91-1.74L12 2z" />
|
||||||
|
</svg>
|
||||||
|
<span>
|
||||||
|
{label}<span class="sm:hidden">{labelSuffix}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
36
frontend/src/routes/search/SmartModeToggle.svelte.spec.ts
Normal file
36
frontend/src/routes/search/SmartModeToggle.svelte.spec.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, expect, it, afterEach } from 'vitest';
|
||||||
|
import { cleanup, render } from 'vitest-browser-svelte';
|
||||||
|
import { page } from 'vitest/browser';
|
||||||
|
import SmartModeToggle from './SmartModeToggle.svelte';
|
||||||
|
|
||||||
|
afterEach(() => cleanup());
|
||||||
|
|
||||||
|
describe('SmartModeToggle', () => {
|
||||||
|
it('renders aria-pressed="false" by default and toggles on click', async () => {
|
||||||
|
render(SmartModeToggle, { smartMode: false });
|
||||||
|
const btn = page.getByRole('button');
|
||||||
|
await expect.element(btn).toHaveAttribute('aria-pressed', 'false');
|
||||||
|
await btn.click();
|
||||||
|
await expect.element(btn).toHaveAttribute('aria-pressed', 'true');
|
||||||
|
await btn.click();
|
||||||
|
await expect.element(btn).toHaveAttribute('aria-pressed', 'false');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the smart label when smartMode is true', async () => {
|
||||||
|
render(SmartModeToggle, { smartMode: true });
|
||||||
|
const btn = page.getByRole('button');
|
||||||
|
await expect.element(btn).toHaveTextContent('KI');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the keyword label when smartMode is false', async () => {
|
||||||
|
render(SmartModeToggle, { smartMode: false });
|
||||||
|
const btn = page.getByRole('button');
|
||||||
|
await expect.element(btn).toHaveTextContent('Text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies the active pill style only in smart mode', async () => {
|
||||||
|
render(SmartModeToggle, { smartMode: true });
|
||||||
|
const btn = page.getByRole('button');
|
||||||
|
await expect.element(btn).toHaveClass(/bg-primary/);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user