diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index cf47b6f445..f323f4290f 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -2944,6 +2944,9 @@ ping URL must never return to the browser after save; the configured state uses an explicit replacement placeholder and removal action. The watchdog remains active independently of alert activation, snooze, quiet hours, and notification delivery pause because those policies must not disable observation of Pulse. +Its Refresh and Show/Hide actions are the shared outline `Button` (xs and +`settingsAction` sizes), not a copied border shell, so they match the other +destination controls and keep a 44px touch target on phones. Watchdog transport and monitoring progress remain notifications- and monitoring-owned respectively. Alerts owns the system-alert projection: diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index d073fa9869..2eaa21ee0e 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -6472,6 +6472,9 @@ must not hand-code raw `` anchors for documentation links. The `settings-external-text-link-shell` and `settings-external-text-link-local-anchor` registry entries enforce that split, and the Button registry owns the `info` variant for blue documentation CTAs. +A documentation link inside a tinted callout sentence (the backup export and +import warnings) uses the `inlineInherit` variant, which keeps the callout's +text colour and underline instead of switching to link blue. Platform inline notices that sit inside platform pages but are not settings callouts must route through the shared `InlineNotice` primitive. Platform owners diff --git a/frontend-modern/browser-verification/5a6ce74ec7f4bcda.json b/frontend-modern/browser-verification/5a6ce74ec7f4bcda.json new file mode 100644 index 0000000000..58e68cfabe --- /dev/null +++ b/frontend-modern/browser-verification/5a6ce74ec7f4bcda.json @@ -0,0 +1,38 @@ +{ + "version": 1, + "base_sha": "b4ea3761e83e8b6cc2e50c7afdd0e6f5367116b6", + "verified_at": "2026-10-05T23:06:38Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/components/Settings/BackupTransferDialogs.tsx", + "frontend-modern/src/components/shared/ExternalTextLink.tsx", + "frontend-modern/src/features/alerts/AlertDeadManDestinationSection.tsx" + ], + "content_sha256": { + "frontend-modern/src/components/Settings/BackupTransferDialogs.tsx": "966ef9b08d0cc3b162e8a9a0978379cc4fd834d46090630428d176681bc8cf42", + "frontend-modern/src/components/shared/ExternalTextLink.tsx": "91a539a03b2846767f2bfd650bae40d336750cbf8abb8b377842617fd46d24be", + "frontend-modern/src/features/alerts/AlertDeadManDestinationSection.tsx": "73c23af492449c5d3eb09308162fb9169aaeffe70f067b226caa8296181f3beb" + }, + "routes": [ + "/settings/system-recovery", + "/alerts/destinations" + ], + "viewports": [ + { + "width": 1440, + "height": 1100 + }, + { + "width": 390, + "height": 1100 + } + ], + "states": [ + "Settings > Recovery > Create Backup and Restore Backup dialogs: the migration guide link inside the amber and yellow warnings is an ExternalTextLink (inlineInherit) that keeps the callout colour (rgb(180,83,9) / rgb(161,98,7) light, rgb(252,211,77) / rgb(253,224,71) dark), stays inline and underlined, and opens in a new tab with noopener; pixel-identical to main on desktop", + "Alerts > Destinations > External watchdog: Refresh and Show/Hide are shared outline Buttons; on desktop they match main (26px and 38px); on a 390px phone they become 44px touch targets (main: 26px and 38px)", + "Shared template audit: no findings remain in BackupTransferDialogs.tsx or AlertDeadManDestinationSection.tsx" + ], + "interactions": [ + "Opened both backup dialogs and the destinations page on main (reverse-applied patch) and on the change, light and dark at 1440 and at 390" + ] +} diff --git a/frontend-modern/scripts/shared-template-registry.json b/frontend-modern/scripts/shared-template-registry.json index 68cc8cdb44..ebe7107d1d 100644 --- a/frontend-modern/scripts/shared-template-registry.json +++ b/frontend-modern/scripts/shared-template-registry.json @@ -1599,6 +1599,7 @@ { "path": "src/components/Settings/AISettingsDialogs.tsx" }, { "path": "src/components/Settings/APITokenManager.tsx" }, { "path": "src/components/Settings/AgentIntegrationsPanel.tsx" }, + { "path": "src/components/Settings/BackupTransferDialogs.tsx" }, { "path": "src/components/Settings/GeneralSettingsPanel.tsx" }, { "path": "src/components/Settings/SecurityOverviewPanel.tsx" }, { "path": "src/components/Settings/SelfHostedCommercialRecoverySection.tsx" } diff --git a/frontend-modern/src/components/Settings/BackupTransferDialogs.tsx b/frontend-modern/src/components/Settings/BackupTransferDialogs.tsx index fbd3163b33..1fb03b3fb9 100644 --- a/frontend-modern/src/components/Settings/BackupTransferDialogs.tsx +++ b/frontend-modern/src/components/Settings/BackupTransferDialogs.tsx @@ -5,6 +5,7 @@ import { SectionHeader } from '@/components/shared/SectionHeader'; import { controlClass, formField, formHelpText, labelClass } from '@/components/shared/Form'; import type { SecurityStatus as SecurityStatusInfo } from '@/types/config'; import { getAPITokenManagementLocationMessage } from '@/utils/apiTokenPresentation'; +import { ExternalTextLink } from '@/components/shared/ExternalTextLink'; import { getShippedDocUrl } from '@/utils/docsLinks'; interface BackupTransferDialogsProps { @@ -145,14 +146,12 @@ export const BackupTransferDialogs: Component = (pro and API-token records, but not history, TrueNAS/vSphere connections or agent enrolment state. Local login credentials and sessions are not included. Check the{' '} - migration guide - {' '} + {' '} before relying on this for recovery. Remember your{' '} {props.useCustomPassphrase() || !props.securityStatus()?.hasAuthentication ? 'passphrase' @@ -296,14 +295,12 @@ export const BackupTransferDialogs: Component = (pro Warning: Import replaces the included settings and API-token records, not the whole installation. Back up the destination first. History and excluded settings are not restored. Check the{' '} - migration guide - {' '} + {' '} before retiring the old server.

diff --git a/frontend-modern/src/components/shared/ExternalTextLink.test.tsx b/frontend-modern/src/components/shared/ExternalTextLink.test.tsx index c5d8a6498e..9d783fadb7 100644 --- a/frontend-modern/src/components/shared/ExternalTextLink.test.tsx +++ b/frontend-modern/src/components/shared/ExternalTextLink.test.tsx @@ -29,6 +29,20 @@ describe('ExternalTextLink', () => { expect(link.className).toContain('sm:min-h-9'); }); + it("keeps a callout's text colour for links inside a tinted sentence", () => { + render(() => ( + + migration guide + + )); + + const link = screen.getByRole('link', { name: 'migration guide' }); + expect(link).toHaveAttribute('target', '_blank'); + expect(link.className).toContain('underline'); + expect(link.className).not.toContain('text-blue'); + expect(link.className).not.toContain('min-h-'); + }); + it('centralizes the preserve-opener exception', () => { expect(getExternalTextLinkRel()).toBe('noopener noreferrer'); expect(getExternalTextLinkRel(true)).toBeUndefined(); diff --git a/frontend-modern/src/components/shared/ExternalTextLink.tsx b/frontend-modern/src/components/shared/ExternalTextLink.tsx index 17d62be01c..24a0c1af12 100644 --- a/frontend-modern/src/components/shared/ExternalTextLink.tsx +++ b/frontend-modern/src/components/shared/ExternalTextLink.tsx @@ -7,6 +7,7 @@ export type ExternalTextLinkVariant = | 'compact' | 'compactAction' | 'compactInherit' + | 'inlineInherit' | 'inlineAction'; interface ExternalTextLinkProps extends Omit< @@ -33,6 +34,8 @@ export const EXTERNAL_TEXT_LINK_VARIANT_CLASSES: Record { 'src/components/Settings/AISettingsDialogs.tsx', 'src/components/Settings/APITokenManager.tsx', 'src/components/Settings/AgentIntegrationsPanel.tsx', + 'src/components/Settings/BackupTransferDialogs.tsx', 'src/components/Settings/GeneralSettingsPanel.tsx', 'src/components/Settings/SecurityOverviewPanel.tsx', 'src/components/Settings/SelfHostedCommercialRecoverySection.tsx', ]); + // A link inside a tinted callout sentence keeps the callout's colour. + expect(externalTextLinkSource).toContain("inlineInherit: 'underline'"); expect(registeredRule?.forbiddenPatterns).toEqual( expect.arrayContaining([ expect.objectContaining({ diff --git a/frontend-modern/src/features/alerts/AlertDeadManDestinationSection.tsx b/frontend-modern/src/features/alerts/AlertDeadManDestinationSection.tsx index 3dc701bdd4..83174ca881 100644 --- a/frontend-modern/src/features/alerts/AlertDeadManDestinationSection.tsx +++ b/frontend-modern/src/features/alerts/AlertDeadManDestinationSection.tsx @@ -1,6 +1,7 @@ import { createSignal, createUniqueId, onMount, Show, type Accessor } from 'solid-js'; import { AlertsAPI } from '@/api/alerts'; +import { Button } from '@/components/shared/Button'; import { SettingsPanel } from '@/components/shared/SettingsPanel'; import type { DeadManStatus } from '@/types/alerts'; import { formatRelativeTime } from '@/utils/format'; @@ -86,14 +87,14 @@ export function AlertDeadManDestinationSection(props: AlertDeadManDestinationSec {unavailable() ? 'Status unavailable' : presentation().label} - + } class="min-w-0" @@ -136,14 +137,14 @@ export function AlertDeadManDestinationSection(props: AlertDeadManDestinationSec } > - +

diff --git a/frontend-modern/src/features/alerts/__tests__/AlertDeadManDestinationSection.test.tsx b/frontend-modern/src/features/alerts/__tests__/AlertDeadManDestinationSection.test.tsx index ce5d3f3a7a..7b06fb4601 100644 --- a/frontend-modern/src/features/alerts/__tests__/AlertDeadManDestinationSection.test.tsx +++ b/frontend-modern/src/features/alerts/__tests__/AlertDeadManDestinationSection.test.tsx @@ -80,6 +80,10 @@ describe('AlertDeadManDestinationSection', () => { expect(input).toHaveValue('https://watchdog.example.test/ping/new-token'); expect(container.textContent).not.toContain('***REDACTED***'); + // The section's actions are the shared outline Buttons, which give a + // phone a 44px touch target. + expect(screen.getByRole('button', { name: 'Show' })).toHaveClass('min-h-11', 'sm:min-h-9'); + expect(screen.getByRole('button', { name: /^Refresh/ })).toHaveClass('min-h-11', 'sm:min-h-0'); fireEvent.click(screen.getByRole('button', { name: 'Show' })); expect(input).toHaveAttribute('type', 'text'); fireEvent.click(screen.getByRole('button', { name: 'Hide' }));