mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Broaden shared Button settings governance
This commit is contained in:
parent
bbdd590a82
commit
60f153611b
11 changed files with 152 additions and 60 deletions
|
|
@ -463,9 +463,11 @@ not a replacement status card, CTA band, or page-local nested card.
|
|||
danger/outline/ghost button chrome, sizes, focus rings, disabled/loading
|
||||
behavior, and safe new-tab link behavior must come from `Button`,
|
||||
`ButtonLink`, and `buttonModel.ts`. Empty states, platform page notices,
|
||||
Patrol controls, settings panel actions, and other repeated command
|
||||
affordances must not copy local `inline-flex ... rounded-md ... bg-surface`
|
||||
Tailwind shells just because the page needs a one-off action. If a new
|
||||
Patrol controls, settings panel actions, infrastructure setup controls, and
|
||||
other repeated command affordances must not copy local `inline-flex ...
|
||||
rounded-md ... bg-surface` 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. 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.
|
||||
|
|
|
|||
|
|
@ -191,7 +191,11 @@
|
|||
"requiredConsumers": [
|
||||
{ "path": "src/components/AI/Chat/ChatMessages.tsx" },
|
||||
{ "path": "src/components/Infrastructure/ResourceDetailDrawerDebugTab.tsx" },
|
||||
{ "path": "src/components/Settings/AgentProfilesPanel.tsx" },
|
||||
{ "path": "src/components/Settings/AvailabilitySettingsPanel.tsx" },
|
||||
{ "path": "src/components/Settings/DataHandlingPanel.tsx" },
|
||||
{ "path": "src/components/Settings/InfrastructureInstallerSection.tsx" },
|
||||
{ "path": "src/components/Settings/InfrastructureSourceManager.tsx" },
|
||||
{ "path": "src/features/patrol/PatrolIntelligenceWorkspace.tsx" },
|
||||
{ "path": "src/features/standalone/StandalonePageSurface.tsx" }
|
||||
],
|
||||
|
|
@ -1056,6 +1060,28 @@
|
|||
"src/components/shared/Button.test.tsx",
|
||||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "button-secondary-settings-action-local-shell",
|
||||
"category": "action-button",
|
||||
"summary": "Settings and infrastructure surfaces must not copy the standard secondary px-3 py-2 action-button shell; use Button or ButtonLink with the mdCompact size.",
|
||||
"canonical": {
|
||||
"path": "src/components/shared/buttonModel.ts",
|
||||
"export": "getButtonClass"
|
||||
},
|
||||
"scopes": ["src/components/Settings", "src/features", "src/pages"],
|
||||
"extensions": [".tsx"],
|
||||
"allPatterns": [
|
||||
"rounded-md border border-border bg-surface px-3 py-2 text-sm font-medium text-base-content"
|
||||
],
|
||||
"legacyReason": "Retired migration debt. Standard settings-sized secondary command 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": [
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Component, For, Show } from 'solid-js';
|
||||
import { Button } from '@/components/shared/Button';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import SettingsPanel from '@/components/shared/SettingsPanel';
|
||||
import { Dialog } from '@/components/shared/Dialog';
|
||||
|
|
@ -89,9 +90,7 @@ export const AgentProfilesPanel: Component = () => {
|
|||
<Crown class="w-5 h-5 text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-base-content">
|
||||
{featureGateCopy.title}
|
||||
</h3>
|
||||
<h3 class="text-base font-semibold text-base-content">{featureGateCopy.title}</h3>
|
||||
<p class="text-sm text-muted">{featureGateCopy.subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -116,25 +115,29 @@ export const AgentProfilesPanel: Component = () => {
|
|||
description="Reusable agent configurations"
|
||||
action={
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
size="mdCompact"
|
||||
onClick={handleCreate}
|
||||
class="inline-flex min-h-10 sm:min-h-9 items-center gap-2 rounded-md bg-blue-600 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 sm:px-4 sm:py-2 sm:text-sm"
|
||||
class="min-h-10 gap-2 sm:min-h-9 sm:px-4"
|
||||
>
|
||||
<Plus class="w-4 h-4" />
|
||||
<span class="hidden sm:inline">New Profile</span>
|
||||
<span class="sm:hidden">New</span>
|
||||
</button>
|
||||
</Button>
|
||||
<Show when={aiAvailable()}>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="mdCompact"
|
||||
onClick={handleSuggest}
|
||||
title="Suggest profiles for your estate"
|
||||
class="inline-flex min-h-10 sm:min-h-9 min-w-10 items-center gap-1.5 rounded-md border border-border bg-surface px-3 py-2 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover sm:px-3 sm:py-2 sm:text-sm"
|
||||
class="min-h-10 min-w-10 gap-1.5 sm:min-h-9"
|
||||
>
|
||||
<Lightbulb class="w-3.5 h-3.5" />
|
||||
<span class="hidden sm:inline">Ideas</span>
|
||||
</button>
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import Plus from 'lucide-solid/icons/plus';
|
|||
import RotateCw from 'lucide-solid/icons/rotate-cw';
|
||||
import Activity from 'lucide-solid/icons/activity';
|
||||
import X from 'lucide-solid/icons/x';
|
||||
import { Button } from '@/components/shared/Button';
|
||||
import SettingsPanel from '@/components/shared/SettingsPanel';
|
||||
import { Dialog } from '@/components/shared/Dialog';
|
||||
import {
|
||||
|
|
@ -33,10 +34,6 @@ import {
|
|||
shouldOpenAvailabilityTargetAddDialog,
|
||||
} from './availabilitySettingsModel';
|
||||
|
||||
const primaryButtonClass =
|
||||
'inline-flex min-h-9 items-center justify-center gap-2 rounded-md bg-blue-600 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60';
|
||||
const secondaryButtonClass =
|
||||
'inline-flex min-h-9 items-center justify-center gap-2 rounded-md border border-border bg-surface px-3 py-2 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-60';
|
||||
const rowActionButtonClass =
|
||||
'inline-flex min-h-8 items-center justify-center rounded-md border border-border px-2.5 py-1 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-60';
|
||||
const closeButtonClass =
|
||||
|
|
@ -200,23 +197,27 @@ export const AvailabilitySettingsPanel: Component = () => {
|
|||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="mdCompact"
|
||||
class="min-h-9 gap-2"
|
||||
onClick={() => void loadTargets()}
|
||||
disabled={loading()}
|
||||
class={secondaryButtonClass}
|
||||
>
|
||||
<RotateCw class={`h-4 w-4 ${loading() ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
size="mdCompact"
|
||||
class="min-h-9 gap-2"
|
||||
onClick={() => openAddDialog('service')}
|
||||
class={primaryButtonClass}
|
||||
>
|
||||
<Plus class="h-4 w-4" />
|
||||
Add service/device check
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -253,14 +254,16 @@ export const AvailabilitySettingsPanel: Component = () => {
|
|||
endpoint that only needs reachability monitoring.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
size="mdCompact"
|
||||
class="min-h-9 gap-2"
|
||||
onClick={() => openAddDialog('service')}
|
||||
class={primaryButtonClass}
|
||||
>
|
||||
<Plus class="h-4 w-4" />
|
||||
Add service/device check
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import Lock from 'lucide-solid/icons/lock';
|
|||
import RefreshCw from 'lucide-solid/icons/refresh-cw';
|
||||
import ShieldCheck from 'lucide-solid/icons/shield-check';
|
||||
import { useUnifiedResources } from '@/hooks/useUnifiedResources';
|
||||
import Button from '@/components/shared/Button';
|
||||
import { Button, ButtonLink } from '@/components/shared/Button';
|
||||
import SettingsPanel from '@/components/shared/SettingsPanel';
|
||||
import { settingsTabPath } from './settingsNavigationModel';
|
||||
import {
|
||||
|
|
@ -100,12 +100,14 @@ const EmptyPolicyPostureState: Component = () => (
|
|||
organization does not have visible monitored resources.
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
<ButtonLink
|
||||
href={settingsTabPath('infrastructure-systems')}
|
||||
class="inline-flex w-fit items-center justify-center rounded-md border border-border bg-surface px-3 py-2 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
|
||||
variant="secondary"
|
||||
size="mdCompact"
|
||||
class="w-fit"
|
||||
>
|
||||
Open Infrastructure
|
||||
</a>
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { Component } from 'solid-js';
|
||||
import { For, Show, createEffect, createMemo, createSignal } from 'solid-js';
|
||||
import { Button } from '@/components/shared/Button';
|
||||
import { FormSelect } from '@/components/shared/FormSelect';
|
||||
import SettingsPanel from '@/components/shared/SettingsPanel';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
|
|
@ -472,15 +473,16 @@ export const InfrastructureInstallerSection: Component<InfrastructureInstallerSe
|
|||
or a specific install profile.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="mdCompact"
|
||||
onClick={() => setShowAdvancedOptions((current) => !current)}
|
||||
class="inline-flex items-center justify-center rounded-md border border-border bg-surface px-3 py-2 text-sm font-medium text-base-content transition-colors hover:bg-surface"
|
||||
>
|
||||
{showAdvancedOptions()
|
||||
? 'Hide advanced connection and install options'
|
||||
: 'Show advanced connection and install options'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
type Component,
|
||||
} from 'solid-js';
|
||||
import { AlertTriangle, Cpu, Plus, RotateCw, Search, SlidersHorizontal } from 'lucide-solid';
|
||||
import { Button } from '@/components/shared/Button';
|
||||
import SettingsPanel from '@/components/shared/SettingsPanel';
|
||||
import {
|
||||
Table,
|
||||
|
|
@ -66,18 +67,9 @@ const inlineButtonClass =
|
|||
// to an existing platform without competing with the primary CTA.
|
||||
const addSectionButtonClass =
|
||||
'inline-flex items-center justify-center gap-1 rounded px-2 py-0.5 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-50 dark:text-blue-300 dark:hover:bg-blue-950/30';
|
||||
const workspacePrimaryButtonClass =
|
||||
'inline-flex min-h-9 items-center justify-center gap-2 rounded-md bg-blue-600 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60';
|
||||
const workspaceSecondaryButtonClass =
|
||||
'inline-flex min-h-9 items-center justify-center gap-2 rounded-md border border-border bg-surface px-3 py-2 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-60';
|
||||
const discoveryPrimaryButtonClass =
|
||||
'inline-flex min-h-9 items-center justify-center gap-2 rounded-md bg-blue-600 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60';
|
||||
const discoverySecondaryButtonClass =
|
||||
'inline-flex min-h-9 items-center justify-center gap-2 rounded-md border border-border bg-surface px-3 py-2 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-60';
|
||||
const discoveryRowClass =
|
||||
'border-b border-border-subtle bg-blue-50/30 hover:bg-blue-50/40 dark:bg-blue-950/10 dark:hover:bg-blue-950/20';
|
||||
const discoveryScanTargetLabel =
|
||||
'Proxmox VE, Proxmox Backup Server, and Proxmox Mail Gateway APIs';
|
||||
const discoveryScanTargetLabel = 'Proxmox VE, Proxmox Backup Server, and Proxmox Mail Gateway APIs';
|
||||
// The system column shows just the name on a single line. The OS / version
|
||||
// (standalone) or cluster identity (cluster) descriptor that used to sit on a
|
||||
// second line moves into the name's hover title, so every row stays one line
|
||||
|
|
@ -598,14 +590,16 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
|
|||
<div class="border-b border-border bg-surface px-4 py-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Show when={props.onAddInfrastructure}>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
size="mdCompact"
|
||||
class="min-h-9 gap-2"
|
||||
onClick={props.onAddInfrastructure}
|
||||
class={workspacePrimaryButtonClass}
|
||||
>
|
||||
<Plus class="h-4 w-4" />
|
||||
Add infrastructure
|
||||
</button>
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -671,11 +665,13 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
|
|||
<Show when={!props.readOnly}>
|
||||
<div class="flex flex-wrap gap-2 lg:justify-end">
|
||||
<Show when={props.onRunDiscovery}>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
size="mdCompact"
|
||||
class="min-h-9 gap-2"
|
||||
onClick={props.onRunDiscovery}
|
||||
disabled={props.discoveryScanStatus().scanning || !props.discoveryEnabled}
|
||||
class={discoveryPrimaryButtonClass}
|
||||
title={`Scan configured networks for reachable ${discoveryScanTargetLabel}. Discovered candidates appear here for review before they are added.`}
|
||||
>
|
||||
<RotateCw
|
||||
|
|
@ -683,28 +679,32 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
|
|||
aria-hidden="true"
|
||||
/>
|
||||
{props.discoveryScanStatus().scanning ? 'Scanning...' : 'Run discovery'}
|
||||
</button>
|
||||
</Button>
|
||||
</Show>
|
||||
<Show when={props.onOpenDiscoverySettings}>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="mdCompact"
|
||||
class="min-h-9 gap-2"
|
||||
onClick={props.onOpenDiscoverySettings}
|
||||
class={discoverySecondaryButtonClass}
|
||||
title={`Configure which networks Pulse scans for ${discoveryScanTargetLabel}.`}
|
||||
>
|
||||
<SlidersHorizontal class="h-4 w-4" aria-hidden="true" />
|
||||
Discovery settings
|
||||
</button>
|
||||
</Button>
|
||||
</Show>
|
||||
<Show when={discoveredCandidateCount() > 0 && props.onReviewDiscoveredSource}>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="mdCompact"
|
||||
class="min-h-9 gap-2"
|
||||
onClick={() => props.onReviewDiscoveredSource?.(props.discoveredNodes()[0])}
|
||||
class={discoverySecondaryButtonClass}
|
||||
>
|
||||
<Search class="h-4 w-4" aria-hidden="true" />
|
||||
{discoveredCandidateCount() === 1 ? 'Review candidate' : 'Review first candidate'}
|
||||
</button>
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
|
@ -752,15 +752,17 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
|
|||
setupConfidenceAction().kind !== 'agent'
|
||||
}
|
||||
>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="mdCompact"
|
||||
onClick={() => setupConfidenceAction().onClick?.()}
|
||||
disabled={setupConfidenceAction().disabled}
|
||||
class={`${workspaceSecondaryButtonClass} self-start xl:self-center`}
|
||||
class="min-h-9 gap-2 self-start xl:self-center"
|
||||
>
|
||||
{setupConfidenceActionIcon(setupConfidenceAction().kind)}
|
||||
{setupConfidenceAction().label}
|
||||
</button>
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { cleanup, render, screen } from '@solidjs/testing-library';
|
||||
import { Route, Router } from '@solidjs/router';
|
||||
import { DataHandlingPanel } from '../DataHandlingPanel';
|
||||
|
||||
const unifiedResourcesState = vi.hoisted(() => ({
|
||||
|
|
@ -18,6 +19,13 @@ vi.mock('@/hooks/useUnifiedResources', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
const renderPanel = () =>
|
||||
render(() => (
|
||||
<Router>
|
||||
<Route path="/" component={() => <DataHandlingPanel />} />
|
||||
</Router>
|
||||
));
|
||||
|
||||
describe('DataHandlingPanel', () => {
|
||||
beforeEach(() => {
|
||||
unifiedResourcesState.error = null;
|
||||
|
|
@ -33,10 +41,11 @@ describe('DataHandlingPanel', () => {
|
|||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
window.history.replaceState({}, '', '/');
|
||||
});
|
||||
|
||||
it('explains the empty resource posture instead of leading with zero-value counters', () => {
|
||||
render(() => <DataHandlingPanel />);
|
||||
renderPanel();
|
||||
|
||||
expect(screen.getByText('Resource Data Policy')).toBeInTheDocument();
|
||||
expect(screen.getByText('Read-only resource privacy posture')).toBeInTheDocument();
|
||||
|
|
@ -68,7 +77,7 @@ describe('DataHandlingPanel', () => {
|
|||
},
|
||||
};
|
||||
|
||||
render(() => <DataHandlingPanel />);
|
||||
renderPanel();
|
||||
|
||||
expect(screen.getByText('Governed Resources')).toBeInTheDocument();
|
||||
expect(screen.getByText('Local-Only')).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ describe('Button', () => {
|
|||
"secondary: 'border border-border bg-surface text-base-content shadow-sm hover:bg-surface-hover'",
|
||||
);
|
||||
expect(buttonModelSource).toContain('export const BUTTON_SIZE_CLASSES');
|
||||
expect(buttonModelSource).toContain("mdCompact: 'px-3 py-2 text-sm'");
|
||||
});
|
||||
|
||||
it('renders command buttons with the shared secondary shell', () => {
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ import incidentTimelinePanelSource from '@/components/Alerts/IncidentTimelinePan
|
|||
import thresholdsTableDockerIgnoredPrefixesSectionSource from '@/components/Alerts/ThresholdsTableDockerIgnoredPrefixesSection.tsx?raw';
|
||||
import webhookConfigFormSource from '@/components/Alerts/WebhookConfigForm.tsx?raw';
|
||||
import reportMergeModalSource from '@/components/Infrastructure/ReportMergeModal.tsx?raw';
|
||||
import availabilitySettingsPanelSource from '@/components/Settings/AvailabilitySettingsPanel.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';
|
||||
|
|
@ -170,6 +171,7 @@ import diagnosticsResultsPanelSource from '@/components/Settings/DiagnosticsResu
|
|||
import dockerRuntimeSettingsCardSource from '@/components/Settings/DockerRuntimeSettingsCard.tsx?raw';
|
||||
import dataHandlingPanelSource from '@/components/Settings/DataHandlingPanel.tsx?raw';
|
||||
import generalSettingsPanelSource from '@/components/Settings/GeneralSettingsPanel.tsx?raw';
|
||||
import infrastructureInstallerSectionSource from '@/components/Settings/InfrastructureInstallerSection.tsx?raw';
|
||||
import organizationAccessMembersSectionSource from '@/components/Settings/OrganizationAccessMembersSection.tsx?raw';
|
||||
import organizationIncomingSharesSectionSource from '@/components/Settings/OrganizationIncomingSharesSection.tsx?raw';
|
||||
import organizationOutgoingSharesSectionSource from '@/components/Settings/OrganizationOutgoingSharesSection.tsx?raw';
|
||||
|
|
@ -1505,13 +1507,20 @@ describe('shared primitive guardrails', () => {
|
|||
const registeredGuard = registry.patternGuards?.find(
|
||||
(guard) => guard.id === 'button-secondary-command-local-shell',
|
||||
);
|
||||
const settingsActionGuard = registry.patternGuards?.find(
|
||||
(guard) => guard.id === 'button-secondary-settings-action-local-shell',
|
||||
);
|
||||
|
||||
expect(registeredRule?.canonical?.path).toBe('src/components/shared/Button.tsx');
|
||||
expect(registeredRule?.canonical?.export).toBe('Button');
|
||||
expect(registeredRule?.requiredConsumers?.map((consumer) => consumer.path)).toEqual([
|
||||
'src/components/AI/Chat/ChatMessages.tsx',
|
||||
'src/components/Infrastructure/ResourceDetailDrawerDebugTab.tsx',
|
||||
'src/components/Settings/AgentProfilesPanel.tsx',
|
||||
'src/components/Settings/AvailabilitySettingsPanel.tsx',
|
||||
'src/components/Settings/DataHandlingPanel.tsx',
|
||||
'src/components/Settings/InfrastructureInstallerSection.tsx',
|
||||
'src/components/Settings/InfrastructureSourceManager.tsx',
|
||||
'src/features/patrol/PatrolIntelligenceWorkspace.tsx',
|
||||
'src/features/standalone/StandalonePageSurface.tsx',
|
||||
]);
|
||||
|
|
@ -1523,6 +1532,18 @@ describe('shared primitive guardrails', () => {
|
|||
expect(registeredGuard?.scopes).toEqual(['src/components', 'src/features', 'src/pages']);
|
||||
expect(registeredGuard?.allowedPaths ?? []).toHaveLength(0);
|
||||
expect(registeredGuard?.ignoredPaths).toEqual(['src/components/shared/Button.test.tsx']);
|
||||
expect(settingsActionGuard?.canonical?.path).toBe('src/components/shared/buttonModel.ts');
|
||||
expect(settingsActionGuard?.canonical?.export).toBe('getButtonClass');
|
||||
expect(settingsActionGuard?.allPatterns).toEqual([
|
||||
'rounded-md border border-border bg-surface px-3 py-2 text-sm font-medium text-base-content',
|
||||
]);
|
||||
expect(settingsActionGuard?.scopes).toEqual([
|
||||
'src/components/Settings',
|
||||
'src/features',
|
||||
'src/pages',
|
||||
]);
|
||||
expect(settingsActionGuard?.allowedPaths ?? []).toHaveLength(0);
|
||||
expect(settingsActionGuard?.ignoredPaths).toEqual(['src/components/shared/Button.test.tsx']);
|
||||
|
||||
expect(buttonSource).toContain('export function Button');
|
||||
expect(buttonSource).toContain('export function ButtonLink');
|
||||
|
|
@ -1537,7 +1558,27 @@ describe('shared primitive guardrails', () => {
|
|||
expect(resourceDetailDrawerDebugTabSource).not.toContain(
|
||||
'rounded-md border border-border bg-surface px-3 py-1.5 text-xs font-medium text-base-content',
|
||||
);
|
||||
expect(agentProfilesPanelSource).toContain('@/components/shared/Button');
|
||||
expect(agentProfilesPanelSource).not.toContain(
|
||||
'rounded-md border border-border bg-surface px-3 py-2 text-sm font-medium text-base-content',
|
||||
);
|
||||
expect(availabilitySettingsPanelSource).toContain('@/components/shared/Button');
|
||||
expect(availabilitySettingsPanelSource).not.toContain(
|
||||
'rounded-md border border-border bg-surface px-3 py-2 text-sm font-medium text-base-content',
|
||||
);
|
||||
expect(dataHandlingPanelSource).toContain('@/components/shared/Button');
|
||||
expect(dataHandlingPanelSource).toContain('ButtonLink');
|
||||
expect(dataHandlingPanelSource).not.toContain(
|
||||
'rounded-md border border-border bg-surface px-3 py-2 text-sm font-medium text-base-content',
|
||||
);
|
||||
expect(infrastructureInstallerSectionSource).toContain('@/components/shared/Button');
|
||||
expect(infrastructureInstallerSectionSource).not.toContain(
|
||||
'rounded-md border border-border bg-surface px-3 py-2 text-sm font-medium text-base-content',
|
||||
);
|
||||
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',
|
||||
);
|
||||
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 = 'sm' | 'md' | 'lg' | 'icon';
|
||||
export type ButtonSize = 'sm' | 'mdCompact' | 'md' | 'lg' | 'icon';
|
||||
|
||||
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';
|
||||
|
|
@ -14,6 +14,7 @@ export const BUTTON_VARIANT_CLASSES: Record<ButtonVariant, string> = {
|
|||
|
||||
export const BUTTON_SIZE_CLASSES: Record<ButtonSize, string> = {
|
||||
sm: 'px-2.5 py-1.5 text-xs',
|
||||
mdCompact: 'px-3 py-2 text-sm',
|
||||
md: 'px-4 py-2 text-sm',
|
||||
lg: 'px-6 py-3 text-base',
|
||||
icon: 'p-2',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue