mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Canonicalize settings dialog close buttons
This commit is contained in:
parent
ce2cc8948b
commit
c57993adb9
9 changed files with 92 additions and 32 deletions
|
|
@ -467,11 +467,12 @@ not a replacement status card, CTA band, or page-local nested card.
|
|||
compact row actions, and other repeated command affordances must not copy
|
||||
local `inline-flex ... rounded-md ...` Tailwind shells just because the page
|
||||
needs a one-off action. The shared `mdCompact` size owns the common
|
||||
settings/action `px-3 py-2 text-sm` shape, and the shared `xs` size owns the
|
||||
compact settings row-action `px-2.5 py-1 text-xs` shape. If a new
|
||||
surface needs a variant that the shared primitive does not expose, extend
|
||||
the primitive and registry guard rather than adding a page-local class
|
||||
string.
|
||||
settings/action `px-3 py-2 text-sm` shape, the shared `xs` size owns the
|
||||
compact settings row-action `px-2.5 py-1 text-xs` shape, and the shared
|
||||
`iconMd` size owns the settings dialog close-button `h-9 w-9` icon shape.
|
||||
If a new surface needs a variant that the shared primitive does not expose,
|
||||
extend the primitive and registry guard rather than adding a page-local
|
||||
class string.
|
||||
Framed product table surfaces must consume the shared `TableCard` frame and
|
||||
`TableCardHeader` title/action band instead of composing page-local `Card`
|
||||
border, background, overflow, or table-title chrome. Feature owners may own
|
||||
|
|
|
|||
|
|
@ -195,6 +195,8 @@
|
|||
{ "path": "src/components/Settings/AvailabilitySettingsPanel.tsx" },
|
||||
{ "path": "src/components/Settings/ConnectionEditor/ConnectionEditor.tsx" },
|
||||
{ "path": "src/components/Settings/DataHandlingPanel.tsx" },
|
||||
{ "path": "src/components/Settings/InfrastructureAgentUpdatesDialog.tsx" },
|
||||
{ "path": "src/components/Settings/InfrastructureDiscoverySettingsDialog.tsx" },
|
||||
{ "path": "src/components/Settings/InfrastructureInstallerSection.tsx" },
|
||||
{ "path": "src/components/Settings/InfrastructureSourceManager.tsx" },
|
||||
{ "path": "src/components/Settings/InfrastructureWorkspace.tsx" },
|
||||
|
|
@ -1106,6 +1108,28 @@
|
|||
"src/components/shared/Button.test.tsx",
|
||||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "button-outline-settings-dialog-close-local-shell",
|
||||
"category": "action-button",
|
||||
"summary": "Settings dialog close buttons must not copy the h-9 w-9 bordered icon shell; use Button with the outline variant and iconMd size.",
|
||||
"canonical": {
|
||||
"path": "src/components/shared/buttonModel.ts",
|
||||
"export": "getButtonClass"
|
||||
},
|
||||
"scopes": ["src/components/Settings", "src/features", "src/pages"],
|
||||
"extensions": [".tsx"],
|
||||
"allPatterns": [
|
||||
"h-9 w-9 items-center justify-center rounded-md border border-border text-base-content transition-colors hover:bg-surface-hover"
|
||||
],
|
||||
"legacyReason": "Retired migration debt. Settings dialog close buttons belong to the shared Button primitive family.",
|
||||
"allowedPaths": [],
|
||||
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
|
||||
"proof": [
|
||||
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
||||
"src/components/shared/Button.test.tsx",
|
||||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
}
|
||||
],
|
||||
"requiredPatternGuards": [
|
||||
|
|
|
|||
|
|
@ -34,9 +34,6 @@ import {
|
|||
shouldOpenAvailabilityTargetAddDialog,
|
||||
} from './availabilitySettingsModel';
|
||||
|
||||
const closeButtonClass =
|
||||
'inline-flex h-9 w-9 items-center justify-center rounded-md border border-border text-base-content transition-colors hover:bg-surface-hover';
|
||||
|
||||
type AvailabilityDialogState =
|
||||
| { mode: 'add'; initialTargetKind?: AvailabilityTargetKind }
|
||||
| { mode: 'edit'; target: AvailabilityTarget }
|
||||
|
|
@ -348,14 +345,15 @@ export const AvailabilitySettingsPanel: Component = () => {
|
|||
<h2 class="text-base font-semibold text-base-content">{dialogTitle()}</h2>
|
||||
<p class="text-sm text-muted">{dialogDescription()}</p>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="iconMd"
|
||||
onClick={() => closeDialog(true)}
|
||||
class={closeButtonClass}
|
||||
aria-label="Close availability check dialog"
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-y-auto p-4">
|
||||
<AvailabilityTargetSlot
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { For, Show, createMemo, type Component } from 'solid-js';
|
||||
import { Copy, X } from 'lucide-solid';
|
||||
import { Dialog } from '@/components/shared/Dialog';
|
||||
import { Button } from '@/components/shared/Button';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
import { notificationStore } from '@/stores/notifications';
|
||||
import {
|
||||
|
|
@ -16,9 +17,6 @@ interface InfrastructureAgentUpdatesDialogProps {
|
|||
onClose: () => void;
|
||||
}
|
||||
|
||||
const closeButtonClass =
|
||||
'inline-flex h-9 w-9 items-center justify-center rounded-md border border-border text-base-content transition-colors hover:bg-surface-hover';
|
||||
|
||||
const copyButtonClass =
|
||||
'absolute right-2 top-2 inline-flex min-h-10 min-w-10 items-center justify-center rounded-md bg-surface-hover p-2 text-muted transition-colors hover:text-base-content disabled:cursor-not-allowed disabled:opacity-50 sm:min-h-9 sm:min-w-9';
|
||||
|
||||
|
|
@ -60,14 +58,15 @@ export const InfrastructureAgentUpdatesDialog: Component<InfrastructureAgentUpda
|
|||
Copy the update command for each host that is behind the current Pulse Agent target.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="iconMd"
|
||||
onClick={props.onClose}
|
||||
class={closeButtonClass}
|
||||
aria-label="Close agent update commands"
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 flex-1 space-y-4 overflow-y-auto p-4 sm:p-6">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Component, Show } from 'solid-js';
|
||||
import X from 'lucide-solid/icons/x';
|
||||
import { Dialog } from '@/components/shared/Dialog';
|
||||
import { Button } from '@/components/shared/Button';
|
||||
import { DiscoverySettingsForm } from './DiscoverySettingsForm';
|
||||
import type { DiscoverySettingsFormProps } from './discoverySettingsModel';
|
||||
|
||||
|
|
@ -9,9 +10,6 @@ interface InfrastructureDiscoverySettingsDialogProps extends DiscoverySettingsFo
|
|||
onClose: () => void;
|
||||
}
|
||||
|
||||
const closeButtonClass =
|
||||
'inline-flex h-9 w-9 items-center justify-center rounded-md border border-border text-base-content transition-colors hover:bg-surface-hover';
|
||||
|
||||
export const InfrastructureDiscoverySettingsDialog: Component<
|
||||
InfrastructureDiscoverySettingsDialogProps
|
||||
> = (props) => {
|
||||
|
|
@ -32,14 +30,15 @@ export const InfrastructureDiscoverySettingsDialog: Component<
|
|||
source discovery.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="iconMd"
|
||||
onClick={props.onClose}
|
||||
class={closeButtonClass}
|
||||
aria-label="Close discovery settings dialog"
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-y-auto px-4 py-4 sm:px-6">
|
||||
|
|
|
|||
|
|
@ -91,8 +91,6 @@ const ADD_STEP_TO_INSTALLER_FOCUS: Partial<
|
|||
kubernetes: 'kubernetes',
|
||||
};
|
||||
|
||||
const closeButtonClass =
|
||||
'inline-flex h-9 w-9 items-center justify-center rounded-md border border-border text-base-content transition-colors hover:bg-surface-hover';
|
||||
const buttonClass =
|
||||
'inline-flex min-h-10 sm:min-h-9 items-center justify-center rounded-md border border-border px-3 py-2 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-60';
|
||||
|
||||
|
|
@ -934,14 +932,15 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
|||
<h2 class="text-base font-semibold text-base-content">{addDialogTitle()}</h2>
|
||||
<p class="text-sm text-muted">{addDialogDescription()}</p>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="iconMd"
|
||||
onClick={closeAddFlow}
|
||||
class={closeButtonClass}
|
||||
aria-label="Close add infrastructure dialog"
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-y-auto">
|
||||
|
|
@ -1003,14 +1002,15 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
|||
<h2 class="text-base font-semibold text-base-content">{editDialogTitle()}</h2>
|
||||
<p class="text-sm text-muted">{editDialogDescription()}</p>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="iconMd"
|
||||
onClick={closeEditFlow}
|
||||
class={closeButtonClass}
|
||||
aria-label="Close edit infrastructure dialog"
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-y-auto">
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ describe('Button', () => {
|
|||
expect(buttonModelSource).toContain('export const BUTTON_SIZE_CLASSES');
|
||||
expect(buttonModelSource).toContain("xs: 'px-2.5 py-1 text-xs'");
|
||||
expect(buttonModelSource).toContain("mdCompact: 'px-3 py-2 text-sm'");
|
||||
expect(buttonModelSource).toContain("iconMd: 'h-9 w-9 p-0'");
|
||||
});
|
||||
|
||||
it('renders command buttons with the shared secondary shell', () => {
|
||||
|
|
|
|||
|
|
@ -108,6 +108,8 @@ import webhookConfigFormSource from '@/components/Alerts/WebhookConfigForm.tsx?r
|
|||
import reportMergeModalSource from '@/components/Infrastructure/ReportMergeModal.tsx?raw';
|
||||
import availabilitySettingsPanelSource from '@/components/Settings/AvailabilitySettingsPanel.tsx?raw';
|
||||
import connectionEditorSource from '@/components/Settings/ConnectionEditor/ConnectionEditor.tsx?raw';
|
||||
import infrastructureAgentUpdatesDialogSource from '@/components/Settings/InfrastructureAgentUpdatesDialog.tsx?raw';
|
||||
import infrastructureDiscoverySettingsDialogSource from '@/components/Settings/InfrastructureDiscoverySettingsDialog.tsx?raw';
|
||||
import selfHostedCommercialRecoverySectionSource from '@/components/Settings/SelfHostedCommercialRecoverySection.tsx?raw';
|
||||
import suggestProfileModalSource from '@/components/Settings/SuggestProfileModal.tsx?raw';
|
||||
import alertAppriseDestinationsSectionSource from '@/features/alerts/AlertAppriseDestinationsSection.tsx?raw';
|
||||
|
|
@ -1515,6 +1517,9 @@ describe('shared primitive guardrails', () => {
|
|||
const settingsRowActionGuard = registry.patternGuards?.find(
|
||||
(guard) => guard.id === 'button-outline-settings-row-action-local-shell',
|
||||
);
|
||||
const settingsDialogCloseGuard = registry.patternGuards?.find(
|
||||
(guard) => guard.id === 'button-outline-settings-dialog-close-local-shell',
|
||||
);
|
||||
|
||||
expect(registeredRule?.canonical?.path).toBe('src/components/shared/Button.tsx');
|
||||
expect(registeredRule?.canonical?.export).toBe('Button');
|
||||
|
|
@ -1525,6 +1530,8 @@ describe('shared primitive guardrails', () => {
|
|||
'src/components/Settings/AvailabilitySettingsPanel.tsx',
|
||||
'src/components/Settings/ConnectionEditor/ConnectionEditor.tsx',
|
||||
'src/components/Settings/DataHandlingPanel.tsx',
|
||||
'src/components/Settings/InfrastructureAgentUpdatesDialog.tsx',
|
||||
'src/components/Settings/InfrastructureDiscoverySettingsDialog.tsx',
|
||||
'src/components/Settings/InfrastructureInstallerSection.tsx',
|
||||
'src/components/Settings/InfrastructureSourceManager.tsx',
|
||||
'src/components/Settings/InfrastructureWorkspace.tsx',
|
||||
|
|
@ -1563,6 +1570,22 @@ describe('shared primitive guardrails', () => {
|
|||
]);
|
||||
expect(settingsRowActionGuard?.allowedPaths ?? []).toHaveLength(0);
|
||||
expect(settingsRowActionGuard?.ignoredPaths).toEqual(['src/components/shared/Button.test.tsx']);
|
||||
expect(settingsDialogCloseGuard?.canonical?.path).toBe(
|
||||
'src/components/shared/buttonModel.ts',
|
||||
);
|
||||
expect(settingsDialogCloseGuard?.canonical?.export).toBe('getButtonClass');
|
||||
expect(settingsDialogCloseGuard?.allPatterns).toEqual([
|
||||
'h-9 w-9 items-center justify-center rounded-md border border-border text-base-content transition-colors hover:bg-surface-hover',
|
||||
]);
|
||||
expect(settingsDialogCloseGuard?.scopes).toEqual([
|
||||
'src/components/Settings',
|
||||
'src/features',
|
||||
'src/pages',
|
||||
]);
|
||||
expect(settingsDialogCloseGuard?.allowedPaths ?? []).toHaveLength(0);
|
||||
expect(settingsDialogCloseGuard?.ignoredPaths).toEqual([
|
||||
'src/components/shared/Button.test.tsx',
|
||||
]);
|
||||
|
||||
expect(buttonSource).toContain('export function Button');
|
||||
expect(buttonSource).toContain('export function ButtonLink');
|
||||
|
|
@ -1588,6 +1611,9 @@ describe('shared primitive guardrails', () => {
|
|||
expect(availabilitySettingsPanelSource).not.toContain(
|
||||
'rounded-md border border-border px-2.5 py-1 text-xs font-medium text-base-content',
|
||||
);
|
||||
expect(availabilitySettingsPanelSource).not.toContain(
|
||||
'h-9 w-9 items-center justify-center rounded-md border border-border text-base-content transition-colors hover:bg-surface-hover',
|
||||
);
|
||||
expect(connectionEditorSource).toContain('@/components/shared/Button');
|
||||
expect(connectionEditorSource).not.toContain(
|
||||
'rounded-md border border-border px-2.5 py-1 text-xs font-medium text-base-content',
|
||||
|
|
@ -1601,6 +1627,14 @@ describe('shared primitive guardrails', () => {
|
|||
expect(infrastructureInstallerSectionSource).not.toContain(
|
||||
'rounded-md border border-border bg-surface px-3 py-2 text-sm font-medium text-base-content',
|
||||
);
|
||||
expect(infrastructureAgentUpdatesDialogSource).toContain('@/components/shared/Button');
|
||||
expect(infrastructureAgentUpdatesDialogSource).not.toContain(
|
||||
'h-9 w-9 items-center justify-center rounded-md border border-border text-base-content transition-colors hover:bg-surface-hover',
|
||||
);
|
||||
expect(infrastructureDiscoverySettingsDialogSource).toContain('@/components/shared/Button');
|
||||
expect(infrastructureDiscoverySettingsDialogSource).not.toContain(
|
||||
'h-9 w-9 items-center justify-center rounded-md border border-border text-base-content transition-colors hover:bg-surface-hover',
|
||||
);
|
||||
expect(infrastructureSourceManagerSource).toContain('@/components/shared/Button');
|
||||
expect(infrastructureSourceManagerSource).not.toContain(
|
||||
'rounded-md border border-border bg-surface px-3 py-2 text-sm font-medium text-base-content',
|
||||
|
|
@ -1612,6 +1646,9 @@ describe('shared primitive guardrails', () => {
|
|||
expect(infrastructureWorkspaceSource).not.toContain(
|
||||
'rounded-md border border-border px-2.5 py-1 text-xs font-medium text-base-content',
|
||||
);
|
||||
expect(infrastructureWorkspaceSource).not.toContain(
|
||||
'h-9 w-9 items-center justify-center rounded-md border border-border text-base-content transition-colors hover:bg-surface-hover',
|
||||
);
|
||||
expect(patrolIntelligenceWorkspaceSource).toContain('@/components/shared/Button');
|
||||
expect(patrolIntelligenceWorkspaceSource).not.toContain(
|
||||
'rounded-md border border-border bg-surface px-3 py-1.5 text-xs font-medium text-base-content',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost' | 'outline';
|
||||
export type ButtonSize = 'xs' | 'sm' | 'mdCompact' | 'md' | 'lg' | 'icon';
|
||||
export type ButtonSize = 'xs' | 'sm' | 'mdCompact' | 'md' | 'lg' | 'icon' | 'iconMd';
|
||||
|
||||
export const BUTTON_BASE_CLASS =
|
||||
'inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed';
|
||||
|
|
@ -19,6 +19,7 @@ export const BUTTON_SIZE_CLASSES: Record<ButtonSize, string> = {
|
|||
md: 'px-4 py-2 text-sm',
|
||||
lg: 'px-6 py-3 text-base',
|
||||
icon: 'p-2',
|
||||
iconMd: 'h-9 w-9 p-0',
|
||||
};
|
||||
|
||||
export type ButtonClassOptions = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue