diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index e28d3a39f0..04f7ac7add 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -1635,6 +1635,21 @@ locale. Cards narrower than 600px, which a window of about 900px with the rail open produces, are still cramped; a container-driven switch to the card list is the remaining fix. +The history card's own width now chooses between the table and the cards, +not the viewport. `AlertHistoryTableSection` measures its wrapper with +`useObservedElementWidth` and shows the cards whenever the card is narrower +than `ALERT_HISTORY_TABLE_MIN_WIDTH` (800px, the width from which the three +row actions share one line); until the card is measured, the phone +breakpoint decides as before. A 1024px to roughly 1240px window with the +Alerts rail open, which gave the table a 600px to 800px card with stacked +actions and names broken mid-word, now shows the cards; a 1280px window +(an 837px card) keeps the table. The 600px density and the stacked-action +rule above therefore serve only a card that has not been measured. The cards +take `active` from that decision instead of a 768px media query, so an open +timeline or resource view moves into the cards' drawer when the cards take +over and back to the inline expansion when the table returns +(`AlertHistoryTableSection.test.tsx`, `AlertHistoryMobileList.test.tsx`). + Alert history row timestamps render clock time in the viewer's own locale and must carry the absolute date and time as a title. The date otherwise lives only in the day group header, which scrolls out of sight, and a hardcoded diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index a85645139e..cdbbac05de 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -8933,6 +8933,14 @@ placeholders for the row subject (`alerts.assistant.iconLabel`, `alerts.assistant.iconLabelAt`); every locale keeps those placeholders (`i18n.test.ts`). +### Layouts that swap a table for cards follow the container + +A surface that swaps a table for a card list decides from the width of the +element that holds them (`useObservedElementWidth`), with the viewport +breakpoint only as the fallback before the first measurement, because side +rails and panels leave a table far less room than the screen width suggests. +Alerts > History switches below an 800px card (`ALERT_HISTORY_TABLE_MIN_WIDTH`). + ### Mobile destinations are links and update evidence stays explicit Fixed mobile destinations and menu destinations that navigate are anchors with diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index 75b7ff1f62..59cf394727 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -2519,7 +2519,9 @@ "frontend-modern/src/components/Alerts/WebhookConfig.test.tsx", "frontend-modern/src/features/alerts/__tests__/AlertDeadManDestinationSection.test.tsx", "frontend-modern/src/features/alerts/__tests__/AlertHistoryFrequencyCard.test.tsx", + "frontend-modern/src/features/alerts/__tests__/AlertHistoryMobileList.test.tsx", "frontend-modern/src/features/alerts/__tests__/alertHistoryModel.test.ts", + "frontend-modern/src/features/alerts/__tests__/AlertHistoryTableSection.test.tsx", "frontend-modern/src/features/alerts/__tests__/AlertIncidentSynthesisSummary.test.tsx", "frontend-modern/src/features/alerts/__tests__/AlertIntentPolicyPanel.test.tsx", "frontend-modern/src/features/alerts/__tests__/AlertOverviewAlertCard.system-scope.test.tsx", diff --git a/frontend-modern/browser-verification/04d6d08630ddbb48.json b/frontend-modern/browser-verification/04d6d08630ddbb48.json new file mode 100644 index 0000000000..ff4ec31b0d --- /dev/null +++ b/frontend-modern/browser-verification/04d6d08630ddbb48.json @@ -0,0 +1,45 @@ +{ + "version": 1, + "base_sha": "fbf5b0c8beb89ce114b468cceb7f606255f9b33d", + "verified_at": "2026-10-15T13:00:00Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/features/alerts/AlertHistoryMobileList.tsx", + "frontend-modern/src/features/alerts/AlertHistoryTableSection.tsx" + ], + "content_sha256": { + "frontend-modern/src/features/alerts/AlertHistoryMobileList.tsx": "e2bfcc34a8a5bb93c38c1cda0e3d7da67a5593d28f89730b9356f91e92cedb4a", + "frontend-modern/src/features/alerts/AlertHistoryTableSection.tsx": "318f48211cf5fd1437299fa194e3a23becc64a788da143390f7b7cd473b15a5b" + }, + "routes": [ + "/alerts/history" + ], + "viewports": [ + { + "width": 1280, + "height": 800 + }, + { + "width": 1024, + "height": 768 + }, + { + "width": 900, + "height": 768 + }, + { + "width": 375, + "height": 812 + } + ], + "states": [ + "Before (main): a 1024px window with the Alerts rail open showed the table in a 602px card, names broken mid-word (\"inventory-/api\"), messages cut after two lines and the three row actions stacked", + "After: 1280px window, 837px card, table unchanged; 1024px, 602px card, cards with full names and messages; 900px (rail collapses to tabs), 774px card, cards; 375px phone, cards, no horizontal overflow", + "A timeline opened inline in the table at 1280px reappears as the card drawer at 1024px and returns to the inline expansion at 1280px", + "A card timeline opens the shared drawer at 1024px" + ], + "interactions": [ + "Resized between 1280, 1024, 900 and 375 and read the layout, card width and visibility of each layout", + "Opened a table timeline, narrowed to cards, widened back; opened and closed a card timeline drawer" + ] +} diff --git a/frontend-modern/src/features/alerts/AlertHistoryMobileList.tsx b/frontend-modern/src/features/alerts/AlertHistoryMobileList.tsx index beee22a902..28c0bb4f57 100644 --- a/frontend-modern/src/features/alerts/AlertHistoryMobileList.tsx +++ b/frontend-modern/src/features/alerts/AlertHistoryMobileList.tsx @@ -1,4 +1,4 @@ -import { Show, createMemo, createSignal, onCleanup, onMount } from 'solid-js'; +import { Show, createEffect, createMemo, createSignal, on, type Accessor } from 'solid-js'; import { aiChatStore } from '@/stores/aiChat'; import { useRelativeTimeNow } from '@/utils/relativeTimeClock'; @@ -20,6 +20,11 @@ import type { AlertHistoryState } from './useAlertHistoryState'; interface AlertHistoryMobileListProps { state: AlertHistoryState; + /** + * Whether the cards are the history layout on show. The table section + * decides from its card's width; a list rendered on its own is active. + */ + active?: Accessor; } type AlertHistoryGroup = ReturnType[number]; @@ -47,13 +52,16 @@ export function AlertHistoryMobileList(props: AlertHistoryMobileListProps) { return items; }); + // Only a change of layout may move expansions into the drawer: an accessor + // that reads a width would otherwise rerun this on every resized pixel and + // reopen a drawer the operator just closed. + const active = createMemo(() => props.active?.() ?? true); + // The shared expansion state survives layout changes. Project it into the - // phone's single drawer instead of leaving a hidden desktop expansion open. - onMount(() => { - if (typeof window.matchMedia !== 'function') return; - const viewport = window.matchMedia('(min-width: 768px)'); - const synchronizeLayout = () => { - if (viewport.matches || aiChatStore.isOpen) { + // cards' single drawer instead of leaving a hidden table expansion open. + createEffect( + on(active, (isActive) => { + if (!isActive || aiChatStore.isOpen) { setInvestigation(null); return; } @@ -76,11 +84,8 @@ export function AlertHistoryMobileList(props: AlertHistoryMobileListProps) { } } setInvestigation(null); - }; - synchronizeLayout(); - viewport.addEventListener('change', synchronizeLayout); - onCleanup(() => viewport.removeEventListener('change', synchronizeLayout)); - }); + }), + ); const getInvestigationReturnFocusTarget = ( current: MobileAlertHistoryInvestigation, @@ -177,7 +182,12 @@ export function AlertHistoryMobileList(props: AlertHistoryMobileListProps) { return ( <> -
+
[number]; type AlertHistoryAlert = AlertHistoryGroup['alerts'][number]; type AlertHistoryRenderItem = @@ -26,6 +35,16 @@ type AlertHistoryRenderItem = | { kind: 'alert'; key: string; alert: AlertHistoryAlert }; export function AlertHistoryTableSection(props: AlertHistoryTableSectionProps) { + // The card's own width decides, not the viewport: a laptop window with the + // Alerts rail open leaves the table far less room than the screen suggests. + // Until the card is measured, the phone breakpoint decides as before. + const cardWidth = useObservedElementWidth(); + const { isMobile } = useBreakpoint(); + // A memo, so readers hear about a change of layout, not every resized pixel. + const showCards = createMemo(() => { + const width = cardWidth.width(); + return width === null ? isMobile() : width < ALERT_HISTORY_TABLE_MIN_WIDTH; + }); const renderItems = createMemo(() => { const items: AlertHistoryRenderItem[] = []; for (const [groupIndex, group] of props.state.groupedAlerts().entries()) { @@ -52,9 +71,12 @@ export function AlertHistoryTableSection(props: AlertHistoryTableSectionProps) { } > - <> - -