mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 12:42:49 +00:00
Use the shared link and button primitives in two remaining spots (#2549)
This clears the last shared-template audit findings. Combined with #2546, the audit then passes. - **Backup export and import warnings:** the "migration guide" links were raw `target="_blank"` anchors. They now use `ExternalTextLink`. Its new `inlineInherit` variant keeps the warning's own amber or yellow colour and underline, so a link inside a tinted callout sentence doesn't switch to link blue. - **External watchdog:** the Refresh and Show/Hide buttons now use the shared outline `Button` instead of a copied border shell. On desktop they look and measure the same as on main (26px and 38px). On phones they become 44px touch targets. - `BackupTransferDialogs.tsx` is now a registered required consumer of `ExternalTextLink`, and the guardrail pins the new variant. Verification: - Checked live on a mock stack against main, in light and dark mode at 1440px and at 390px. - The links keep their callout colour, stay inline and open in a new tab with noopener. - The receipt is included.
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -6472,6 +6472,9 @@ must not hand-code raw `<a target="_blank">` 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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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" }
|
||||
|
||||
@@ -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<BackupTransferDialogsProps> = (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{' '}
|
||||
<a
|
||||
<ExternalTextLink
|
||||
href={getShippedDocUrl('MIGRATION.md')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="underline"
|
||||
variant="inlineInherit"
|
||||
>
|
||||
migration guide
|
||||
</a>{' '}
|
||||
</ExternalTextLink>{' '}
|
||||
before relying on this for recovery. Remember your{' '}
|
||||
{props.useCustomPassphrase() || !props.securityStatus()?.hasAuthentication
|
||||
? 'passphrase'
|
||||
@@ -296,14 +295,12 @@ export const BackupTransferDialogs: Component<BackupTransferDialogsProps> = (pro
|
||||
<strong>Warning:</strong> 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{' '}
|
||||
<a
|
||||
<ExternalTextLink
|
||||
href={getShippedDocUrl('MIGRATION.md')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="underline"
|
||||
variant="inlineInherit"
|
||||
>
|
||||
migration guide
|
||||
</a>{' '}
|
||||
</ExternalTextLink>{' '}
|
||||
before retiring the old server.
|
||||
</p>
|
||||
<p>
|
||||
|
||||
@@ -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(() => (
|
||||
<ExternalTextLink href="https://docs.example.test/migration" variant="inlineInherit">
|
||||
migration guide
|
||||
</ExternalTextLink>
|
||||
));
|
||||
|
||||
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();
|
||||
|
||||
@@ -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<ExternalTextLinkVariant,
|
||||
compactAction:
|
||||
'inline-flex min-h-10 items-center rounded-sm px-1 text-xs font-medium text-blue-700 hover:underline dark:text-blue-300 sm:min-h-9',
|
||||
compactInherit: 'inline-flex min-h-10 items-center rounded-sm px-1 underline sm:min-h-9',
|
||||
// A link inside a tinted callout sentence keeps the callout's text colour.
|
||||
inlineInherit: 'underline',
|
||||
inlineAction:
|
||||
'inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium text-blue-600 hover:underline dark:text-blue-300',
|
||||
};
|
||||
|
||||
@@ -1890,10 +1890,13 @@ describe('shared primitive guardrails', () => {
|
||||
'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({
|
||||
|
||||
@@ -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
|
||||
<span class={`rounded-full px-2.5 py-1 text-xs font-semibold ${presentation().class}`}>
|
||||
{unavailable() ? 'Status unavailable' : presentation().label}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-border px-2.5 py-1 text-xs font-medium text-base-content hover:bg-surface-hover disabled:opacity-50"
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
disabled={loading()}
|
||||
onClick={() => void loadStatus()}
|
||||
>
|
||||
{loading() ? 'Refreshing…' : 'Refresh'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
class="min-w-0"
|
||||
@@ -136,14 +137,14 @@ export function AlertDeadManDestinationSection(props: AlertDeadManDestinationSec
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-border px-3 py-2 text-sm font-medium text-base-content hover:bg-surface-hover"
|
||||
<Button
|
||||
variant="outline"
|
||||
size="settingsAction"
|
||||
aria-pressed={showUrl()}
|
||||
onClick={() => setShowUrl((value) => !value)}
|
||||
>
|
||||
{showUrl() ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
<p id={`${inputId}-help`} class="mt-2 text-xs leading-5 text-muted">
|
||||
|
||||
@@ -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' }));
|
||||
|
||||
Reference in New Issue
Block a user