mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 22:12:23 +00:00
fix(ui): standardize mobile interaction targets
Contract-Neutral: responsive interaction sizing and popover positioning only; no API or payload changes
This commit is contained in:
@@ -1,46 +1,113 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"base_sha": "93e4b764ffae445e3b501d4bce17c50a9e5ec4fb",
|
"base_sha": "a94980797bba025285cb7fcb793a7866c4c62e0b",
|
||||||
"verified_at": "2026-08-06T15:37:44Z",
|
"verified_at": "2026-08-06T16:01:54Z",
|
||||||
"result": "passed",
|
"result": "passed",
|
||||||
"changed_paths": [
|
"changed_paths": [
|
||||||
"frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts"
|
"frontend-modern/src/components/AI/FindingsPanel.tsx",
|
||||||
|
"frontend-modern/src/components/Settings/SettingsPageShell.tsx",
|
||||||
|
"frontend-modern/src/components/shared/FilterToolbar.tsx",
|
||||||
|
"frontend-modern/src/components/shared/SearchField.tsx",
|
||||||
|
"frontend-modern/src/components/shared/SearchTipsPopover.tsx",
|
||||||
|
"frontend-modern/src/components/shared/Subtabs.tsx",
|
||||||
|
"frontend-modern/src/components/shared/SummaryRowActionButton.tsx",
|
||||||
|
"frontend-modern/src/components/shared/buttonModel.ts",
|
||||||
|
"frontend-modern/src/components/shared/searchInputEnhancementsModel.ts",
|
||||||
|
"frontend-modern/src/components/shared/searchTipsPopoverModel.ts",
|
||||||
|
"frontend-modern/src/components/shared/toggleModel.ts",
|
||||||
|
"frontend-modern/src/components/shared/useSearchTipsPopoverState.ts",
|
||||||
|
"frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx"
|
||||||
],
|
],
|
||||||
"content_sha256": {
|
"content_sha256": {
|
||||||
"frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts": "d64a38fee5fef5c62b04814738e7d9342123114d63c0b7559268dbf1d8df98ef"
|
"frontend-modern/src/components/AI/FindingsPanel.tsx": "fa76f7946c15b42e529338419dae5bc33395ae91440c404d3f50afb243050ed8",
|
||||||
|
"frontend-modern/src/components/Settings/SettingsPageShell.tsx": "cee289c0db5a36bbac8d323cf0435cd0b3ff75384c6e04818ebc71a357a29147",
|
||||||
|
"frontend-modern/src/components/shared/FilterToolbar.tsx": "7b18839d12aba7cef74d934d98658a56bcc26ad46d728f86207d2d0dc25c7dd0",
|
||||||
|
"frontend-modern/src/components/shared/SearchField.tsx": "9dab6fb6e8f35a8633a7777796df272263d375ee08ab6cf039c380c868fcdfd3",
|
||||||
|
"frontend-modern/src/components/shared/SearchTipsPopover.tsx": "22a8dfa12f5ac98c0dd4886d72f9d384805169d2b712cde9f2c8448b6387f1ad",
|
||||||
|
"frontend-modern/src/components/shared/Subtabs.tsx": "cebd908970f3512001f96ea065d24624496f2ed93182b52b8a4b615a35d3735d",
|
||||||
|
"frontend-modern/src/components/shared/SummaryRowActionButton.tsx": "e35943a58467e4fadc9c6d06fd6a484c0179b653c2d03851ee5bc51d988132a3",
|
||||||
|
"frontend-modern/src/components/shared/buttonModel.ts": "d6847c1d6e41165165c1a8617b7a5ea63d61fdc0ecc30c876d745949d551c7bd",
|
||||||
|
"frontend-modern/src/components/shared/searchInputEnhancementsModel.ts": "f1fc385fa542e59c14142d2eec6e5e5309b7f747341480de9c35d7c96240ce18",
|
||||||
|
"frontend-modern/src/components/shared/searchTipsPopoverModel.ts": "bd8e76fcdbf74c4111603729901b57f4e4ffff5d8b0660b17a4acb77338b4262",
|
||||||
|
"frontend-modern/src/components/shared/toggleModel.ts": "2f587b9f3bc2d2bf7c57ce4ed5406075bc4a856fd64262e674fa7507c86c3f85",
|
||||||
|
"frontend-modern/src/components/shared/useSearchTipsPopoverState.ts": "155d7479e39171f9f6c4dc324de2349a24b81b5a5c24989150dc6fc1b1b9b51d",
|
||||||
|
"frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx": "de74125176e6d7421cec5717c963cd8e992f0ef0f11bf9304bb7b75c6f844658"
|
||||||
},
|
},
|
||||||
"routes": [
|
"routes": [
|
||||||
"/vmware",
|
"/proxmox/overview",
|
||||||
"/proxmox?status=all"
|
"/proxmox/storage?tab=disks&source=proxmox-all",
|
||||||
|
"/proxmox/replication",
|
||||||
|
"/proxmox/backups?view=coverage",
|
||||||
|
"/proxmox/backups?view=date",
|
||||||
|
"/proxmox/ceph",
|
||||||
|
"/proxmox/mail",
|
||||||
|
"/docker/overview",
|
||||||
|
"/docker/images",
|
||||||
|
"/docker/storage",
|
||||||
|
"/docker/networks",
|
||||||
|
"/docker/swarm",
|
||||||
|
"/kubernetes/overview",
|
||||||
|
"/kubernetes/nodes",
|
||||||
|
"/kubernetes/workloads",
|
||||||
|
"/kubernetes/services",
|
||||||
|
"/kubernetes/storage",
|
||||||
|
"/kubernetes/configuration",
|
||||||
|
"/kubernetes/events",
|
||||||
|
"/truenas/overview",
|
||||||
|
"/truenas/storage",
|
||||||
|
"/truenas/services",
|
||||||
|
"/truenas/apps",
|
||||||
|
"/truenas/vms",
|
||||||
|
"/truenas/shares",
|
||||||
|
"/truenas/protection",
|
||||||
|
"/vmware/overview",
|
||||||
|
"/vmware/storage",
|
||||||
|
"/vmware/networks",
|
||||||
|
"/vmware/health",
|
||||||
|
"/vmware/activity",
|
||||||
|
"/standalone/machines",
|
||||||
|
"/standalone/availability",
|
||||||
|
"/alerts/thresholds",
|
||||||
|
"/actions",
|
||||||
|
"/patrol",
|
||||||
|
"/settings/monitoring/availability",
|
||||||
|
"/settings/pulse-intelligence/provider",
|
||||||
|
"/docs",
|
||||||
|
"/preview/setup-complete",
|
||||||
|
"/this-route-does-not-exist"
|
||||||
],
|
],
|
||||||
"viewports": [
|
"viewports": [
|
||||||
{
|
{
|
||||||
"width": 1900,
|
"width": 320,
|
||||||
"height": 1100
|
"height": 900
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"width": 1280,
|
"width": 1280,
|
||||||
"height": 800
|
"height": 900
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"width": 390,
|
"width": 1920,
|
||||||
"height": 844
|
"height": 1080
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"states": [
|
"states": [
|
||||||
"vSphere VM table with a saved workloadsHiddenColumns:vmware-vms preference that predates the Backup default-hide: before the fix the Backup column rendered on all 18 VM rows, every cell reading None",
|
"Mock-data tables across every Proxmox, Docker, Kubernetes, TrueNAS, vSphere, Machines, and Availability route",
|
||||||
"vSphere VM table after the one-time migration: Backup absent from the header, zero Backup cells, hidden list carries backup and the default-hidden-applied marker records it",
|
"Phone inventory controls with table expanders, search history, search tips, filters, saved views, sort controls, and row action menus",
|
||||||
"vSphere VM table with Backup manually re-shown from the Columns control: 18 rows, every cell None",
|
"Actions Open and History states plus a governed action review dialog with populated approval evidence",
|
||||||
"vSphere VM table reloaded after the manual re-show: Backup still visible, marker stops the migration re-hiding a column the user restored",
|
"Patrol runtime controls, attention filters, findings sort controls, issue review buttons, and compact toggle hit area",
|
||||||
"Proxmox workload table unaffected at desktop width: Backup column still visible with real ages (1d, 9d, 12d, 15d, 21d) alongside fresh and None cells",
|
"Settings Availability and Provider controls plus the compact Settings return action",
|
||||||
"Proxmox workload table at 390px: hidden list unchanged, backup never added to the Proxmox scope",
|
"Search tips popover at phone and desktop widths, including viewport-edge and page-scroll positioning",
|
||||||
"vSphere VM table at 390px: 32 rows render, Backup hidden, no horizontal body overflow"
|
"Machines table with default columns, every optional column enabled, responsive column priority, and restored user choices",
|
||||||
|
"Documentation, setup-complete preview, and not-found utility routes"
|
||||||
],
|
],
|
||||||
"interactions": [
|
"interactions": [
|
||||||
"Opened the View preferences popover on /vmware and read the Columns control reporting 4 hidden",
|
"Scanned every listed local route at 320px for horizontal document overflow and visible primary controls below a 44px phone target",
|
||||||
"Opened the Columns multiselect and clicked the Backup toggle to re-show the column",
|
"Opened and closed Docker and Machines search-tips popovers, measured their viewport bounds, and confirmed phone and desktop positioning remains fully visible",
|
||||||
"Reloaded /vmware after the manual re-show to confirm the migration marker holds",
|
"Opened Docker filters and confirmed the add-filter selector is 44px tall and remains inside the phone viewport",
|
||||||
"Seeded and cleared the workloadsHiddenColumns:vmware-vms scope keys to reproduce the pre-migration state at each viewport",
|
"Expanded and collapsed Docker host details and confirmed the disclosure control is 44px without introducing document overflow",
|
||||||
"Navigated between /vmware and /proxmox?status=all to compare the Backup column across scopes"
|
"Switched Actions between Open and History, opened a populated action review dialog, measured its 288px phone bounds and 44px controls, and confirmed close restores focus to the originating action",
|
||||||
|
"Measured Patrol phone actions, filters, selects, findings review controls, and the compact switch's extended hit area, then confirmed compact desktop density remains unchanged",
|
||||||
|
"Enabled all optional Machines columns, confirmed priority columns only at 320px, useful operational columns at 1280px, all 16 columns at 1920px, and no horizontal document scrolling at any width",
|
||||||
|
"Restored the original Machines column choices after the stress test, reset the viewport override, and finalized the browser session"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1336,7 +1336,7 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
|||||||
aria-expanded={expandedId() === finding.id}
|
aria-expanded={expandedId() === finding.id}
|
||||||
aria-controls={`finding-${finding.id}-details`}
|
aria-controls={`finding-${finding.id}-details`}
|
||||||
onClick={toggleExpanded}
|
onClick={toggleExpanded}
|
||||||
class="inline-flex items-center gap-1 rounded border border-border bg-surface px-2 py-1 text-xs font-medium text-muted transition-colors hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
|
class="inline-flex min-h-11 items-center gap-1 rounded border border-border bg-surface px-2 py-1 text-xs font-medium text-muted transition-colors hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 sm:min-h-0"
|
||||||
title={
|
title={
|
||||||
isPatrolFindingsSource()
|
isPatrolFindingsSource()
|
||||||
? expandedId() === finding.id
|
? expandedId() === finding.id
|
||||||
@@ -2049,7 +2049,7 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
|||||||
fieldBaseClass="contents"
|
fieldBaseClass="contents"
|
||||||
value={sortBy()}
|
value={sortBy()}
|
||||||
onChange={(e) => setSortBy(e.currentTarget.value as 'severity' | 'time')}
|
onChange={(e) => setSortBy(e.currentTarget.value as 'severity' | 'time')}
|
||||||
selectBaseClass="text-xs px-2 py-1 rounded border border-border bg-surface"
|
selectBaseClass="min-h-11 rounded border border-border bg-surface px-2 py-1 text-xs sm:min-h-0"
|
||||||
>
|
>
|
||||||
<option value="severity">By Severity</option>
|
<option value="severity">By Severity</option>
|
||||||
<option value="time">By Time</option>
|
<option value="time">By Time</option>
|
||||||
|
|||||||
@@ -942,6 +942,16 @@ describe('aiFindingPresentation', () => {
|
|||||||
expect(findingsPanelSource).not.toContain('<select');
|
expect(findingsPanelSource).not.toContain('<select');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps Patrol review and sort controls touch-sized on phones', () => {
|
||||||
|
expect(findingsPanelSource).toContain(
|
||||||
|
'class="inline-flex min-h-11 items-center gap-1 rounded border border-border',
|
||||||
|
);
|
||||||
|
expect(findingsPanelSource).toContain(
|
||||||
|
'selectBaseClass="min-h-11 rounded border border-border',
|
||||||
|
);
|
||||||
|
expect(findingsPanelSource).toContain('sm:min-h-0');
|
||||||
|
});
|
||||||
|
|
||||||
it('hides the filter bar when there are no Patrol findings or special buckets to navigate', () => {
|
it('hides the filter bar when there are no Patrol findings or special buckets to navigate', () => {
|
||||||
expect(findingsPanelSource).toContain('const showFilterControls = createMemo(');
|
expect(findingsPanelSource).toContain('const showFilterControls = createMemo(');
|
||||||
expect(findingsPanelSource).toContain('const hasUnknownRunSnapshot = createMemo(');
|
expect(findingsPanelSource).toContain('const hasUnknownRunSnapshot = createMemo(');
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ export const SettingsPageShell: Component<SettingsPageShellProps> = (props) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => props.setIsMobileMenuOpen(true)}
|
onClick={() => props.setIsMobileMenuOpen(true)}
|
||||||
class="flex min-h-10 items-center gap-1.5 rounded-md px-2 py-1.5 font-medium text-blue-600 transition-colors active:bg-blue-50 dark:text-blue-400 dark:active:bg-blue-900"
|
class="flex min-h-11 items-center gap-1.5 rounded-md px-2 py-1.5 font-medium text-blue-600 transition-colors active:bg-blue-50 dark:text-blue-400 dark:active:bg-blue-900"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
class="h-5 w-5 -ml-1 flex-shrink-0"
|
class="h-5 w-5 -ml-1 flex-shrink-0"
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ describe('SettingsPageShell mobile navigation', () => {
|
|||||||
|
|
||||||
expect(screen.getAllByText('Infrastructure')).toHaveLength(2);
|
expect(screen.getAllByText('Infrastructure')).toHaveLength(2);
|
||||||
expect(screen.getAllByText('API Access')).toHaveLength(1);
|
expect(screen.getAllByText('API Access')).toHaveLength(1);
|
||||||
|
expect(screen.getByRole('button', { name: 'Settings' })).toHaveClass('min-h-11');
|
||||||
|
|
||||||
setActiveTab('api');
|
setActiveTab('api');
|
||||||
|
|
||||||
|
|||||||
@@ -49,14 +49,18 @@ describe('Button', () => {
|
|||||||
);
|
);
|
||||||
expect(buttonModelSource).toContain('dangerOutline:');
|
expect(buttonModelSource).toContain('dangerOutline:');
|
||||||
expect(buttonModelSource).toContain('export const BUTTON_SIZE_CLASSES');
|
expect(buttonModelSource).toContain('export const BUTTON_SIZE_CLASSES');
|
||||||
expect(buttonModelSource).toContain("xs: 'px-2.5 py-1 text-xs'");
|
expect(buttonModelSource).toContain("xs: 'min-h-11 px-2.5 py-1 text-xs sm:min-h-0'");
|
||||||
expect(buttonModelSource).toContain("mdCompact: 'px-3 py-2 text-sm'");
|
expect(buttonModelSource).toContain(
|
||||||
|
"mdCompact: 'min-h-11 px-3 py-2 text-sm sm:min-h-0'",
|
||||||
|
);
|
||||||
expect(buttonModelSource).toContain("settingsAction: 'min-h-11 px-3 py-2 text-sm sm:min-h-9'");
|
expect(buttonModelSource).toContain("settingsAction: 'min-h-11 px-3 py-2 text-sm sm:min-h-9'");
|
||||||
expect(buttonModelSource).toContain(
|
expect(buttonModelSource).toContain(
|
||||||
"settingsActionXs: 'min-h-11 px-3 py-2 text-xs sm:min-h-9'",
|
"settingsActionXs: 'min-h-11 px-3 py-2 text-xs sm:min-h-9'",
|
||||||
);
|
);
|
||||||
expect(buttonModelSource).toContain("chip: 'gap-1 px-1.5 py-0.5 text-[10px]'");
|
expect(buttonModelSource).toContain("chip: 'gap-1 px-1.5 py-0.5 text-[10px]'");
|
||||||
expect(buttonModelSource).toContain("iconMd: 'h-9 w-9 p-0'");
|
expect(buttonModelSource).toContain(
|
||||||
|
"iconMd: 'h-9 w-9 min-h-11 min-w-11 p-0 sm:min-h-0 sm:min-w-0'",
|
||||||
|
);
|
||||||
expect(buttonModelSource).toContain('DRAWER_HEADER_ACTION_BUTTON_CLASS');
|
expect(buttonModelSource).toContain('DRAWER_HEADER_ACTION_BUTTON_CLASS');
|
||||||
expect(buttonModelSource).toContain('DRAWER_HEADER_ICON_BUTTON_CLASS');
|
expect(buttonModelSource).toContain('DRAWER_HEADER_ICON_BUTTON_CLASS');
|
||||||
expect(buttonModelSource).toContain('getDrawerHeaderActionButtonClass');
|
expect(buttonModelSource).toContain('getDrawerHeaderActionButtonClass');
|
||||||
@@ -76,6 +80,8 @@ describe('Button', () => {
|
|||||||
expect(button).toHaveClass('bg-surface');
|
expect(button).toHaveClass('bg-surface');
|
||||||
expect(button).toHaveClass('border-border');
|
expect(button).toHaveClass('border-border');
|
||||||
expect(button).toHaveClass('px-3');
|
expect(button).toHaveClass('px-3');
|
||||||
|
expect(button).toHaveClass('min-h-11');
|
||||||
|
expect(button).toHaveClass('sm:min-h-0');
|
||||||
|
|
||||||
button.click();
|
button.click();
|
||||||
expect(onClick).toHaveBeenCalledTimes(1);
|
expect(onClick).toHaveBeenCalledTimes(1);
|
||||||
@@ -157,6 +163,7 @@ describe('Button', () => {
|
|||||||
expect(removeMemberButton).toHaveClass('border-transparent');
|
expect(removeMemberButton).toHaveClass('border-transparent');
|
||||||
expect(removeMemberButton).toHaveClass('text-red-600');
|
expect(removeMemberButton).toHaveClass('text-red-600');
|
||||||
expect(removeMemberButton).toHaveClass('text-xs');
|
expect(removeMemberButton).toHaveClass('text-xs');
|
||||||
|
expect(removeMemberButton).toHaveClass('min-h-11');
|
||||||
|
|
||||||
const infoButton = screen.getByRole('button', { name: 'View reference' });
|
const infoButton = screen.getByRole('button', { name: 'View reference' });
|
||||||
expect(infoButton).toHaveClass('border-blue-200');
|
expect(infoButton).toHaveClass('border-blue-200');
|
||||||
|
|||||||
@@ -256,7 +256,8 @@ describe('FilterHeader', () => {
|
|||||||
expect(filterPanelClass).toContain('z-[80]');
|
expect(filterPanelClass).toContain('z-[80]');
|
||||||
expect(filterPanelClass).not.toContain('w-[min(40rem,calc(100vw-2rem))]');
|
expect(filterPanelClass).not.toContain('w-[min(40rem,calc(100vw-2rem))]');
|
||||||
expect(filterPanelDefaultWidthClass).toContain('w-[min(40rem,calc(100vw-2rem))]');
|
expect(filterPanelDefaultWidthClass).toContain('w-[min(40rem,calc(100vw-2rem))]');
|
||||||
expect(filterToolbarSource).toContain("'flex min-h-10 items-center gap-1.5");
|
expect(filterToolbarSource).toContain("'flex min-h-11 items-center gap-1.5");
|
||||||
|
expect(filterToolbarSource).toContain('sm:h-7 sm:min-h-0');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps peer popover triggers aligned and exposes their disclosure state', () => {
|
it('keeps peer popover triggers aligned and exposes their disclosure state', () => {
|
||||||
@@ -273,7 +274,9 @@ describe('FilterHeader', () => {
|
|||||||
|
|
||||||
const trigger = screen.getByRole('button', { name: 'Saved' });
|
const trigger = screen.getByRole('button', { name: 'Saved' });
|
||||||
const chevron = trigger.querySelector('svg');
|
const chevron = trigger.querySelector('svg');
|
||||||
expect(trigger).toHaveClass('h-7');
|
expect(trigger).toHaveClass('min-h-11');
|
||||||
|
expect(trigger).toHaveClass('sm:h-7');
|
||||||
|
expect(trigger).toHaveClass('sm:min-h-0');
|
||||||
expect(trigger).toHaveClass('text-base-content');
|
expect(trigger).toHaveClass('text-base-content');
|
||||||
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
||||||
expect(chevron).not.toHaveClass('rotate-180');
|
expect(chevron).not.toHaveClass('rotate-180');
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ export const filterGroupClass =
|
|||||||
'inline-flex items-center gap-1 rounded-md bg-surface-hover p-0.5 ring-1 ring-border-subtle';
|
'inline-flex items-center gap-1 rounded-md bg-surface-hover p-0.5 ring-1 ring-border-subtle';
|
||||||
export const filterLabelClass =
|
export const filterLabelClass =
|
||||||
'px-1.5 text-[9px] font-semibold uppercase tracking-wide text-muted';
|
'px-1.5 text-[9px] font-semibold uppercase tracking-wide text-muted';
|
||||||
export const filterToolbarControlClass = 'h-7 rounded-md text-xs font-medium';
|
export const filterToolbarControlClass =
|
||||||
|
'min-h-11 rounded-md text-xs font-medium sm:h-7 sm:min-h-0';
|
||||||
export const filterActionButtonClass = `inline-flex items-center gap-1.5 ${filterToolbarControlClass} bg-surface-hover px-2.5 text-muted ring-1 ring-border-subtle transition-colors hover:bg-surface hover:text-base-content`;
|
export const filterActionButtonClass = `inline-flex items-center gap-1.5 ${filterToolbarControlClass} bg-surface-hover px-2.5 text-muted ring-1 ring-border-subtle transition-colors hover:bg-surface hover:text-base-content`;
|
||||||
export const filterActionButtonActiveClass = 'bg-surface text-base-content shadow-sm';
|
export const filterActionButtonActiveClass = 'bg-surface text-base-content shadow-sm';
|
||||||
export const filterSelectClass = `${filterToolbarControlClass} border border-border bg-surface px-2 text-base-content outline-none focus:border-blue-500`;
|
export const filterSelectClass =
|
||||||
|
'min-h-11 rounded-md border border-border bg-surface px-2 text-xs font-medium text-base-content outline-none focus:border-blue-500 sm:h-7 sm:min-h-0';
|
||||||
export const filterDividerClass = 'hidden h-5 w-px bg-surface-hover sm:block';
|
export const filterDividerClass = 'hidden h-5 w-px bg-surface-hover sm:block';
|
||||||
export const filterPanelClass =
|
export const filterPanelClass =
|
||||||
'absolute right-0 top-[calc(100%+0.5rem)] z-[80] rounded-md border border-border bg-surface p-3 shadow-lg';
|
'absolute right-0 top-[calc(100%+0.5rem)] z-[80] rounded-md border border-border bg-surface p-3 shadow-lg';
|
||||||
@@ -28,7 +30,7 @@ export const filterPanelDefaultWidthClass = 'w-[min(40rem,calc(100vw-2rem))]';
|
|||||||
export const filterPanelTitleClass = 'text-sm font-medium text-base-content';
|
export const filterPanelTitleClass = 'text-sm font-medium text-base-content';
|
||||||
export const filterPanelDescriptionClass = 'text-xs text-muted';
|
export const filterPanelDescriptionClass = 'text-xs text-muted';
|
||||||
export const mobileFiltersButtonClass =
|
export const mobileFiltersButtonClass =
|
||||||
'flex min-h-10 items-center gap-1.5 rounded-md bg-surface-hover px-2.5 py-1.5 text-xs font-medium text-muted';
|
'flex min-h-11 items-center gap-1.5 rounded-md bg-surface-hover px-2.5 py-1.5 text-xs font-medium text-muted sm:min-h-10';
|
||||||
export const filterCountBadgeClass =
|
export const filterCountBadgeClass =
|
||||||
'ml-0.5 rounded-full bg-blue-500 px-1.5 py-0.5 text-[10px] font-semibold leading-none text-white';
|
'ml-0.5 rounded-full bg-blue-500 px-1.5 py-0.5 text-[10px] font-semibold leading-none text-white';
|
||||||
export const filterUtilityBadgeClass =
|
export const filterUtilityBadgeClass =
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const SearchField: Component<SearchFieldProps> = (props) => {
|
|||||||
onKeyDown={search.handleKeyDown}
|
onKeyDown={search.handleKeyDown}
|
||||||
onBlur={search.handleBlur}
|
onBlur={search.handleBlur}
|
||||||
aria-label={props.title ?? props.placeholder ?? 'Search'}
|
aria-label={props.title ?? props.placeholder ?? 'Search'}
|
||||||
class={`min-h-10 w-full pl-8 sm:pl-9 ${search.inputPaddingRight()} py-1.5 sm:py-2 text-sm border border-border rounded-md
|
class={`min-h-11 w-full pl-8 sm:min-h-10 sm:pl-9 ${search.inputPaddingRight()} py-1.5 sm:py-2 text-sm border border-border rounded-md
|
||||||
bg-surface text-base-content placeholder-muted
|
bg-surface text-base-content placeholder-muted
|
||||||
focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:focus:border-blue-400 outline-none transition-all disabled:opacity-60 disabled:cursor-not-allowed ${props.inputClass ?? ''}`}
|
focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:focus:border-blue-400 outline-none transition-all disabled:opacity-60 disabled:cursor-not-allowed ${props.inputClass ?? ''}`}
|
||||||
title={props.title}
|
title={props.title}
|
||||||
@@ -46,7 +46,7 @@ export const SearchField: Component<SearchFieldProps> = (props) => {
|
|||||||
<Show when={search.showClearButton()}>
|
<Show when={search.showClearButton()}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="inline-flex h-10 w-10 items-center justify-center rounded-full bg-surface-hover text-muted transition-all duration-150 hover:bg-red-100 hover:text-red-600 active:scale-90 sm:h-6 sm:w-6 dark:hover:bg-red-900 dark:hover:text-red-400"
|
class="inline-flex h-11 w-11 items-center justify-center rounded-full bg-surface-hover text-muted transition-all duration-150 hover:bg-red-100 hover:text-red-600 active:scale-90 sm:h-6 sm:w-6 dark:hover:bg-red-900 dark:hover:text-red-400"
|
||||||
onClick={() => props.onChange('')}
|
onClick={() => props.onChange('')}
|
||||||
onMouseDown={props.onClearMouseDown}
|
onMouseDown={props.onClearMouseDown}
|
||||||
aria-label="Clear search"
|
aria-label="Clear search"
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ export const SearchTipsPopover: Component<SearchTipsPopoverProps> = (props) => {
|
|||||||
id={popoverId()}
|
id={popoverId()}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-label={title()}
|
aria-label={title()}
|
||||||
class={`absolute ${positionClass()} z-50 mt-2 w-72 overflow-hidden rounded-md border bg-surface text-left shadow-sm`}
|
style={state.popoverStyle()}
|
||||||
|
class={`fixed ${positionClass()} z-50 w-auto overflow-hidden rounded-md border bg-surface text-left shadow-sm sm:absolute sm:mt-2 sm:w-72`}
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between border-b border-border-subtle px-3 py-2">
|
<div class="flex items-center justify-between border-b border-border-subtle px-3 py-2">
|
||||||
<span class="text-sm font-semibold text-base-content">{title()}</span>
|
<span class="text-sm font-semibold text-base-content">{title()}</span>
|
||||||
|
|||||||
@@ -8275,7 +8275,7 @@ describe('shared primitive guardrails', () => {
|
|||||||
|
|
||||||
expect(savedViewsMenuSource).toContain("from './useSavedViews';");
|
expect(savedViewsMenuSource).toContain("from './useSavedViews';");
|
||||||
expect(filterToolbarSource).toContain('export const FilterPopoverTrigger');
|
expect(filterToolbarSource).toContain('export const FilterPopoverTrigger');
|
||||||
expect(filterToolbarSource).toContain("filterToolbarControlClass = 'h-7");
|
expect(filterToolbarSource).toContain("'min-h-11 rounded-md text-xs font-medium sm:h-7 sm:min-h-0'");
|
||||||
expect(viewOptionsMenuSource).toContain('<FilterPopoverTrigger');
|
expect(viewOptionsMenuSource).toContain('<FilterPopoverTrigger');
|
||||||
expect(viewOptionsMenuSource).toContain('class="static ml-auto shrink-0 sm:relative sm:ml-0"');
|
expect(viewOptionsMenuSource).toContain('class="static ml-auto shrink-0 sm:relative sm:ml-0"');
|
||||||
expect(viewOptionsMenuSource).toContain('class="left-auto right-0');
|
expect(viewOptionsMenuSource).toContain('class="left-auto right-0');
|
||||||
@@ -8427,10 +8427,11 @@ describe('shared primitive guardrails', () => {
|
|||||||
it('keeps shared navigation and row actions usable at phone widths', () => {
|
it('keeps shared navigation and row actions usable at phone widths', () => {
|
||||||
expect(subtabsSource).toContain('overflow-x-auto');
|
expect(subtabsSource).toContain('overflow-x-auto');
|
||||||
expect(subtabsSource).toContain('whitespace-nowrap');
|
expect(subtabsSource).toContain('whitespace-nowrap');
|
||||||
expect(subtabsSource).toContain('min-h-10');
|
expect(subtabsSource).toContain('min-h-11');
|
||||||
expect(summaryRowActionButtonSource).toContain('h-10 w-10');
|
expect(subtabsSource).toContain('sm:min-h-10');
|
||||||
expect(searchFieldSource).toContain('min-h-10');
|
expect(summaryRowActionButtonSource).toContain('h-11 w-11');
|
||||||
expect(filterToolbarSource).toContain('min-h-10');
|
expect(searchFieldSource).toContain('min-h-11');
|
||||||
|
expect(filterToolbarSource).toContain('min-h-11');
|
||||||
expect(inlineDetailTableRowSource).toContain('max-w-[calc(100vw-3.5rem)]');
|
expect(inlineDetailTableRowSource).toContain('max-w-[calc(100vw-3.5rem)]');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const subtabsShellClass = 'border-b border-border';
|
|||||||
export const subtabsListClass = 'flex min-w-0 items-center gap-6 overflow-x-auto scrollbar-hide';
|
export const subtabsListClass = 'flex min-w-0 items-center gap-6 overflow-x-auto scrollbar-hide';
|
||||||
export const subtabsTrailingRowClass = 'flex flex-wrap items-center justify-between gap-3';
|
export const subtabsTrailingRowClass = 'flex flex-wrap items-center justify-between gap-3';
|
||||||
export const subtabButtonClass =
|
export const subtabButtonClass =
|
||||||
'inline-flex min-h-10 shrink-0 items-center whitespace-nowrap border-b-2 px-1 py-2 text-sm font-medium transition-colors';
|
'inline-flex min-h-11 shrink-0 items-center whitespace-nowrap border-b-2 px-1 py-2 text-sm font-medium transition-colors sm:min-h-10';
|
||||||
export const subtabButtonActiveClass = 'border-blue-600 text-base-content';
|
export const subtabButtonActiveClass = 'border-blue-600 text-base-content';
|
||||||
export const subtabButtonInactiveClass = 'border-transparent text-muted hover:text-base-content';
|
export const subtabButtonInactiveClass = 'border-transparent text-muted hover:text-base-content';
|
||||||
export const Subtabs: Component<SubtabsProps> = (props) => {
|
export const Subtabs: Component<SubtabsProps> = (props) => {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ type SummaryRowActionButtonProps =
|
|||||||
};
|
};
|
||||||
|
|
||||||
const DISCLOSURE_BUTTON_CLASS = [
|
const DISCLOSURE_BUTTON_CLASS = [
|
||||||
'inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-md text-muted transition-colors sm:h-6 sm:w-6',
|
'inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-md text-muted transition-colors sm:h-6 sm:w-6',
|
||||||
'hover:bg-surface hover:text-base-content',
|
'hover:bg-surface hover:text-base-content',
|
||||||
SUMMARY_ROW_ACTION_BUTTON_FOCUS_CLASS,
|
SUMMARY_ROW_ACTION_BUTTON_FOCUS_CLASS,
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ describe('Toggle', () => {
|
|||||||
expect(toggleModelSource).toContain('toggleSizeConfig');
|
expect(toggleModelSource).toContain('toggleSizeConfig');
|
||||||
expect(toggleModelSource).toContain('resolveToggleSize');
|
expect(toggleModelSource).toContain('resolveToggleSize');
|
||||||
expect(toggleModelSource).toContain('getToggleTrackClass');
|
expect(toggleModelSource).toContain('getToggleTrackClass');
|
||||||
|
expect(toggleModelSource).toContain("after:-inset-y-2.5 after:inset-x-0 after:content-['']");
|
||||||
expect(toggleModelSource).toContain('getToggleKnobClass');
|
expect(toggleModelSource).toContain('getToggleKnobClass');
|
||||||
expect(toggleModelSource).toContain('ToggleChangeEvent');
|
expect(toggleModelSource).toContain('ToggleChangeEvent');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ describe('SearchField', () => {
|
|||||||
));
|
));
|
||||||
|
|
||||||
expect(screen.getByText('Cmd+K')).toBeInTheDocument();
|
expect(screen.getByText('Cmd+K')).toBeInTheDocument();
|
||||||
expect(screen.getByPlaceholderText('Search field')).toHaveClass('min-h-10');
|
expect(screen.getByPlaceholderText('Search field')).toHaveClass('min-h-11');
|
||||||
|
expect(screen.getByPlaceholderText('Search field')).toHaveClass('sm:min-h-10');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reserves mobile input space for every visible trailing action', () => {
|
it('reserves mobile input space for every visible trailing action', () => {
|
||||||
@@ -109,8 +110,8 @@ describe('SearchField', () => {
|
|||||||
));
|
));
|
||||||
|
|
||||||
expect(screen.getByRole('button', { name: 'Clear search' })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: 'Clear search' })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: 'Clear search' })).toHaveClass('h-10');
|
expect(screen.getByRole('button', { name: 'Clear search' })).toHaveClass('h-11');
|
||||||
expect(screen.getByRole('button', { name: 'Clear search' })).toHaveClass('w-10');
|
expect(screen.getByRole('button', { name: 'Clear search' })).toHaveClass('w-11');
|
||||||
expect(screen.getByRole('button', { name: 'Extra' })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: 'Extra' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ describe('SearchInput', () => {
|
|||||||
expect(searchInputEnhancementsModelSource).toContain('SEARCH_HISTORY_MENU_CLASS');
|
expect(searchInputEnhancementsModelSource).toContain('SEARCH_HISTORY_MENU_CLASS');
|
||||||
expect(searchInputEnhancementsModelSource).toContain('w-full max-w-lg');
|
expect(searchInputEnhancementsModelSource).toContain('w-full max-w-lg');
|
||||||
expect(searchInputEnhancementsModelSource).not.toContain('left-0 right-0 top-full');
|
expect(searchInputEnhancementsModelSource).not.toContain('left-0 right-0 top-full');
|
||||||
expect(searchInputEnhancementsModelSource).toContain('h-10 w-10');
|
expect(searchInputEnhancementsModelSource).toContain('h-11 w-11');
|
||||||
expect(searchInputEnhancementsModelSource).toContain('sm:h-7 sm:w-7');
|
expect(searchInputEnhancementsModelSource).toContain('sm:h-7 sm:w-7');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -23,12 +23,18 @@ describe('SearchTipsPopover', () => {
|
|||||||
expect(searchTipsPopoverStateSource).toContain('createEffect');
|
expect(searchTipsPopoverStateSource).toContain('createEffect');
|
||||||
expect(searchTipsPopoverStateSource).toContain('window.addEventListener');
|
expect(searchTipsPopoverStateSource).toContain('window.addEventListener');
|
||||||
expect(searchTipsPopoverStateSource).toContain('pointerInside');
|
expect(searchTipsPopoverStateSource).toContain('pointerInside');
|
||||||
|
expect(searchTipsPopoverStateSource).toContain('window.innerWidth >= 640');
|
||||||
|
expect(searchTipsPopoverStateSource).toContain('window.innerWidth - viewportMargin - width');
|
||||||
|
expect(searchTipsPopoverStateSource).toContain("window.addEventListener('scroll', updatePopoverPosition, true)");
|
||||||
|
expect(searchTipsPopoverSource).toContain('style={state.popoverStyle()}');
|
||||||
|
expect(searchTipsPopoverSource).toContain('fixed ${positionClass()}');
|
||||||
|
expect(searchTipsPopoverSource).toContain('sm:absolute sm:mt-2 sm:w-72');
|
||||||
|
|
||||||
expect(searchTipsPopoverModelSource).toContain('getSearchTipsPopoverTriggerClass');
|
expect(searchTipsPopoverModelSource).toContain('getSearchTipsPopoverTriggerClass');
|
||||||
expect(searchTipsPopoverModelSource).toContain('getSearchTipsPopoverPositionClass');
|
expect(searchTipsPopoverModelSource).toContain('getSearchTipsPopoverPositionClass');
|
||||||
expect(searchTipsPopoverModelSource).toContain('getSearchTipsPopoverTriggerVariant');
|
expect(searchTipsPopoverModelSource).toContain('getSearchTipsPopoverTriggerVariant');
|
||||||
expect(searchTipsPopoverModelSource).toContain('shouldSearchTipsPopoverOpenOnHover');
|
expect(searchTipsPopoverModelSource).toContain('shouldSearchTipsPopoverOpenOnHover');
|
||||||
expect(searchTipsPopoverModelSource).toContain('h-10 w-10');
|
expect(searchTipsPopoverModelSource).toContain('h-11 w-11');
|
||||||
expect(searchTipsPopoverModelSource).toContain('sm:h-5 sm:w-5');
|
expect(searchTipsPopoverModelSource).toContain('sm:h-5 sm:w-5');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -13,15 +13,15 @@ const TRIGGER_BASE_CLASSES =
|
|||||||
describe('searchTipsPopoverModel.branchcov0712c', () => {
|
describe('searchTipsPopoverModel.branchcov0712c', () => {
|
||||||
describe('getSearchTipsPopoverPositionClass', () => {
|
describe('getSearchTipsPopoverPositionClass', () => {
|
||||||
it("returns 'left-0' when align === 'left' (truthy ternary arm)", () => {
|
it("returns 'left-0' when align === 'left' (truthy ternary arm)", () => {
|
||||||
expect(getSearchTipsPopoverPositionClass('left')).toBe('left-0');
|
expect(getSearchTipsPopoverPositionClass('left')).toBe('sm:left-0');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 'right-0' when align === 'right' (falsy ternary arm)", () => {
|
it("returns 'right-0' when align === 'right' (falsy ternary arm)", () => {
|
||||||
expect(getSearchTipsPopoverPositionClass('right')).toBe('right-0');
|
expect(getSearchTipsPopoverPositionClass('right')).toBe('sm:right-0');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 'right-0' when align is omitted (undefined -> falsy ternary arm)", () => {
|
it("returns 'right-0' when align is omitted (undefined -> falsy ternary arm)", () => {
|
||||||
expect(getSearchTipsPopoverPositionClass()).toBe('right-0');
|
expect(getSearchTipsPopoverPositionClass()).toBe('sm:right-0');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the else-arm value for an unexpected align value (defensive)', () => {
|
it('returns the else-arm value for an unexpected align value (defensive)', () => {
|
||||||
@@ -29,7 +29,7 @@ describe('searchTipsPopoverModel.branchcov0712c', () => {
|
|||||||
// wrong-typed value that slips past the union at runtime) must fall to
|
// wrong-typed value that slips past the union at runtime) must fall to
|
||||||
// the 'right-0' arm.
|
// the 'right-0' arm.
|
||||||
const bogus = 'center' as unknown as Parameters<typeof getSearchTipsPopoverPositionClass>[0];
|
const bogus = 'center' as unknown as Parameters<typeof getSearchTipsPopoverPositionClass>[0];
|
||||||
expect(getSearchTipsPopoverPositionClass(bogus)).toBe('right-0');
|
expect(getSearchTipsPopoverPositionClass(bogus)).toBe('sm:right-0');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ describe('searchTipsPopoverModel.branchcov0712c', () => {
|
|||||||
|
|
||||||
it("returns the icon variant classes when triggerVariant === 'icon' (both ifs false -> default return)", () => {
|
it("returns the icon variant classes when triggerVariant === 'icon' (both ifs false -> default return)", () => {
|
||||||
expect(getSearchTipsPopoverTriggerClass('icon')).toBe(
|
expect(getSearchTipsPopoverTriggerClass('icon')).toBe(
|
||||||
`flex h-10 w-10 items-center justify-center rounded-full transition-colors hover:text-muted sm:h-5 sm:w-5 ${TRIGGER_BASE_CLASSES}`,
|
`flex h-11 w-11 items-center justify-center rounded-full transition-colors hover:text-muted sm:h-5 sm:w-5 ${TRIGGER_BASE_CLASSES}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ describe('searchTipsPopoverModel.branchcov0712c', () => {
|
|||||||
// the trailing default (the icon) return.
|
// the trailing default (the icon) return.
|
||||||
const bogus = 'ghost' as unknown as Parameters<typeof getSearchTipsPopoverTriggerClass>[0];
|
const bogus = 'ghost' as unknown as Parameters<typeof getSearchTipsPopoverTriggerClass>[0];
|
||||||
expect(getSearchTipsPopoverTriggerClass(bogus)).toBe(
|
expect(getSearchTipsPopoverTriggerClass(bogus)).toBe(
|
||||||
`flex h-10 w-10 items-center justify-center rounded-full transition-colors hover:text-muted sm:h-5 sm:w-5 ${TRIGGER_BASE_CLASSES}`,
|
`flex h-11 w-11 items-center justify-center rounded-full transition-colors hover:text-muted sm:h-5 sm:w-5 ${TRIGGER_BASE_CLASSES}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -51,15 +51,15 @@ export const BUTTON_VARIANT_CLASSES: Record<ButtonVariant, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const BUTTON_SIZE_CLASSES: Record<ButtonSize, string> = {
|
export const BUTTON_SIZE_CLASSES: Record<ButtonSize, string> = {
|
||||||
xs: 'px-2.5 py-1 text-xs',
|
xs: 'min-h-11 px-2.5 py-1 text-xs sm:min-h-0',
|
||||||
sm: 'px-2.5 py-1.5 text-xs',
|
sm: 'min-h-11 px-2.5 py-1.5 text-xs sm:min-h-0',
|
||||||
mdCompact: 'px-3 py-2 text-sm',
|
mdCompact: 'min-h-11 px-3 py-2 text-sm sm:min-h-0',
|
||||||
settingsAction: 'min-h-11 px-3 py-2 text-sm sm:min-h-9',
|
settingsAction: 'min-h-11 px-3 py-2 text-sm sm:min-h-9',
|
||||||
settingsActionXs: 'min-h-11 px-3 py-2 text-xs sm:min-h-9',
|
settingsActionXs: 'min-h-11 px-3 py-2 text-xs sm:min-h-9',
|
||||||
md: 'px-4 py-2 text-sm',
|
md: 'min-h-11 px-4 py-2 text-sm sm:min-h-0',
|
||||||
lg: 'px-6 py-3 text-base',
|
lg: 'px-6 py-3 text-base',
|
||||||
icon: 'p-2',
|
icon: 'min-h-11 min-w-11 p-2 sm:min-h-0 sm:min-w-0',
|
||||||
iconMd: 'h-9 w-9 p-0',
|
iconMd: 'h-9 w-9 min-h-11 min-w-11 p-0 sm:min-h-0 sm:min-w-0',
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ButtonClassOptions = {
|
export type ButtonClassOptions = {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const SEARCH_HISTORY_ENTRY_BUTTON_CLASS =
|
|||||||
export const SEARCH_HISTORY_CLEAR_LABEL = 'Clear history';
|
export const SEARCH_HISTORY_CLEAR_LABEL = 'Clear history';
|
||||||
|
|
||||||
export function getSearchHistoryToggleButtonClass(isOpen: boolean): string {
|
export function getSearchHistoryToggleButtonClass(isOpen: boolean): string {
|
||||||
return `flex h-10 w-10 items-center justify-center rounded-md transition-colors sm:h-7 sm:w-7 ${
|
return `flex h-11 w-11 items-center justify-center rounded-md transition-colors sm:h-7 sm:w-7 ${
|
||||||
isOpen
|
isOpen
|
||||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
|
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
|
||||||
: 'text-muted hover:bg-surface-hover hover:text-base-content'
|
: 'text-muted hover:bg-surface-hover hover:text-base-content'
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export interface SearchTipsPopoverProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getSearchTipsPopoverPositionClass(align?: 'left' | 'right'): string {
|
export function getSearchTipsPopoverPositionClass(align?: 'left' | 'right'): string {
|
||||||
return align === 'left' ? 'left-0' : 'right-0';
|
return align === 'left' ? 'sm:left-0' : 'sm:right-0';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSearchTipsPopoverId(popoverId?: string): string {
|
export function getSearchTipsPopoverId(popoverId?: string): string {
|
||||||
@@ -57,5 +57,5 @@ export function getSearchTipsPopoverTriggerClass(
|
|||||||
return `rounded px-1 py-0.5 underline decoration-dotted underline-offset-4 transition-colors hover:text-base-content ${triggerBaseClasses}`;
|
return `rounded px-1 py-0.5 underline decoration-dotted underline-offset-4 transition-colors hover:text-base-content ${triggerBaseClasses}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `flex h-10 w-10 items-center justify-center rounded-full transition-colors hover:text-muted sm:h-5 sm:w-5 ${triggerBaseClasses}`;
|
return `flex h-11 w-11 items-center justify-center rounded-full transition-colors hover:text-muted sm:h-5 sm:w-5 ${triggerBaseClasses}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function getToggleTrackClass(
|
|||||||
className?: string,
|
className?: string,
|
||||||
): string {
|
): string {
|
||||||
const config = toggleSizeConfig[size];
|
const config = toggleSizeConfig[size];
|
||||||
return `relative inline-flex ${config.track} shrink-0 items-center rounded-full p-0.5 transition-all duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)] focus:outline-none focus:ring-0 ${
|
return `relative inline-flex ${config.track} shrink-0 items-center rounded-full p-0.5 transition-all duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)] after:absolute after:-inset-y-2.5 after:inset-x-0 after:content-[''] focus:outline-none focus:ring-0 ${
|
||||||
disabled ? TOGGLE_DISABLED_CLASS : checked ? TOGGLE_CHECKED_CLASS : TOGGLE_UNCHECKED_CLASS
|
disabled ? TOGGLE_DISABLED_CLASS : checked ? TOGGLE_CHECKED_CLASS : TOGGLE_UNCHECKED_CLASS
|
||||||
} ${className ?? ''}`.trim();
|
} ${className ?? ''}`.trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface SearchTipsPopoverState {
|
|||||||
handleMouseEnter: () => void;
|
handleMouseEnter: () => void;
|
||||||
handleMouseLeave: () => void;
|
handleMouseLeave: () => void;
|
||||||
isOpen: Accessor<boolean>;
|
isOpen: Accessor<boolean>;
|
||||||
|
popoverStyle: Accessor<string | undefined>;
|
||||||
setPopoverRef: (el: HTMLDivElement) => void;
|
setPopoverRef: (el: HTMLDivElement) => void;
|
||||||
setTriggerRef: (el: HTMLButtonElement) => void;
|
setTriggerRef: (el: HTMLButtonElement) => void;
|
||||||
}
|
}
|
||||||
@@ -22,11 +23,35 @@ export function useSearchTipsPopoverState(
|
|||||||
options: SearchTipsPopoverStateOptions,
|
options: SearchTipsPopoverStateOptions,
|
||||||
): SearchTipsPopoverState {
|
): SearchTipsPopoverState {
|
||||||
const [open, setOpen] = createSignal(false);
|
const [open, setOpen] = createSignal(false);
|
||||||
|
const [popoverStyle, setPopoverStyle] = createSignal<string>();
|
||||||
let popoverRef: HTMLDivElement | undefined;
|
let popoverRef: HTMLDivElement | undefined;
|
||||||
let triggerRef: HTMLButtonElement | undefined;
|
let triggerRef: HTMLButtonElement | undefined;
|
||||||
let pointerInside = false;
|
let pointerInside = false;
|
||||||
|
|
||||||
const close = () => setOpen(false);
|
const close = () => setOpen(false);
|
||||||
|
const updatePopoverPosition = () => {
|
||||||
|
if (!triggerRef || !popoverRef || window.innerWidth >= 640) {
|
||||||
|
setPopoverStyle(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewportMargin = 16;
|
||||||
|
const triggerGap = 8;
|
||||||
|
const width = Math.min(288, window.innerWidth - viewportMargin * 2);
|
||||||
|
const triggerRect = triggerRef.getBoundingClientRect();
|
||||||
|
const popoverHeight = popoverRef.getBoundingClientRect().height;
|
||||||
|
const left = Math.min(
|
||||||
|
Math.max(triggerRect.right - width, viewportMargin),
|
||||||
|
window.innerWidth - viewportMargin - width,
|
||||||
|
);
|
||||||
|
const belowTop = triggerRect.bottom + triggerGap;
|
||||||
|
const top =
|
||||||
|
belowTop + popoverHeight <= window.innerHeight - viewportMargin
|
||||||
|
? belowTop
|
||||||
|
: Math.max(viewportMargin, triggerRect.top - triggerGap - popoverHeight);
|
||||||
|
|
||||||
|
setPopoverStyle(`left:${left}px;top:${top}px;width:${width}px`);
|
||||||
|
};
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!open()) {
|
if (!open()) {
|
||||||
@@ -51,10 +76,15 @@ export function useSearchTipsPopoverState(
|
|||||||
|
|
||||||
window.addEventListener('pointerdown', handlePointerDown);
|
window.addEventListener('pointerdown', handlePointerDown);
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
window.addEventListener('resize', updatePopoverPosition);
|
||||||
|
window.addEventListener('scroll', updatePopoverPosition, true);
|
||||||
|
queueMicrotask(updatePopoverPosition);
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
window.removeEventListener('pointerdown', handlePointerDown);
|
window.removeEventListener('pointerdown', handlePointerDown);
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
window.removeEventListener('resize', updatePopoverPosition);
|
||||||
|
window.removeEventListener('scroll', updatePopoverPosition, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -82,8 +112,10 @@ export function useSearchTipsPopoverState(
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
},
|
},
|
||||||
isOpen: open,
|
isOpen: open,
|
||||||
|
popoverStyle,
|
||||||
setPopoverRef: (el) => {
|
setPopoverRef: (el) => {
|
||||||
popoverRef = el;
|
popoverRef = el;
|
||||||
|
queueMicrotask(updatePopoverPosition);
|
||||||
},
|
},
|
||||||
setTriggerRef: (el) => {
|
setTriggerRef: (el) => {
|
||||||
triggerRef = el;
|
triggerRef = el;
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
|
|||||||
<Show when={!state.shouldShowPatrolSetupOnly()}>
|
<Show when={!state.shouldShowPatrolSetupOnly()}>
|
||||||
<div class="flex flex-wrap items-center gap-2 sm:ml-auto">
|
<div class="flex flex-wrap items-center gap-2 sm:ml-auto">
|
||||||
{renderRunControl(
|
{renderRunControl(
|
||||||
'sm:hidden flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-surface-alt disabled:text-muted rounded-md transition-colors',
|
'flex min-h-11 items-center gap-2 rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:bg-surface-alt disabled:text-muted sm:hidden',
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Show when={!runBlockedByProviderSetup()}>
|
<Show when={!runBlockedByProviderSetup()}>
|
||||||
@@ -390,7 +390,7 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
|
|||||||
href={settingsTabPath('system-ai-patrol')}
|
href={settingsTabPath('system-ai-patrol')}
|
||||||
aria-label="Open Patrol settings"
|
aria-label="Open Patrol settings"
|
||||||
title="Open Patrol settings"
|
title="Open Patrol settings"
|
||||||
class="flex items-center gap-2 rounded-md border border-border px-3 py-1.5 text-sm font-medium text-base-content shadow-sm transition-colors hover:bg-surface-alt"
|
class="flex min-h-11 items-center gap-2 rounded-md border border-border px-3 py-1.5 text-sm font-medium text-base-content shadow-sm transition-colors hover:bg-surface-alt sm:min-h-0"
|
||||||
>
|
>
|
||||||
<SettingsIcon class="w-4 h-4" />
|
<SettingsIcon class="w-4 h-4" />
|
||||||
<span class="sr-only sm:not-sr-only">Settings</span>
|
<span class="sr-only sm:not-sr-only">Settings</span>
|
||||||
|
|||||||
@@ -91,6 +91,14 @@ describe('PatrolIntelligenceHeader', () => {
|
|||||||
expect(headerSource).not.toContain('Refresh Patrol');
|
expect(headerSource).not.toContain('Refresh Patrol');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps primary Patrol actions touch-sized on phones without inflating desktop chrome', () => {
|
||||||
|
expect(headerSource).toContain('flex min-h-11 items-center gap-2 rounded-md bg-blue-600');
|
||||||
|
expect(headerSource).toContain(
|
||||||
|
'flex min-h-11 items-center gap-2 rounded-md border border-border',
|
||||||
|
);
|
||||||
|
expect(headerSource).toContain('sm:min-h-0');
|
||||||
|
});
|
||||||
|
|
||||||
it('makes Patrol mode a simple four-level choice without rendering plan-locked paid modes', () => {
|
it('makes Patrol mode a simple four-level choice without rendering plan-locked paid modes', () => {
|
||||||
expect(PATROL_AUTONOMY_POLICY_PRESENTATION).toEqual({
|
expect(PATROL_AUTONOMY_POLICY_PRESENTATION).toEqual({
|
||||||
monitor: {
|
monitor: {
|
||||||
|
|||||||
Reference in New Issue
Block a user