Converge Patrol approval actions on shared Button primitives

This commit is contained in:
rcourtman
2026-06-13 22:59:58 +01:00
parent 85ae9ad540
commit 05f1a3298e
10 changed files with 321 additions and 70 deletions
@@ -540,6 +540,10 @@ not a replacement status card, CTA band, or page-local nested card.
and reporting exports use the shared `success`, `successOutline`, and
`successGhost` Button variants instead of carrying page-local emerald action
shells.
Patrol approval and remediation controls use that same primitive family:
Patrol owns approval/reapproval/denial/review/Assistant handoff behavior,
while `Button` owns success, warning-solid, primary, secondary, ghost,
disabled, focus, and compact action chrome.
Shared error-boundary fallback actions are also command buttons: reset,
reload, and retry controls must compose `Button` so emergency UI does not
become a separate local button vocabulary.
@@ -73,6 +73,11 @@ Patrol-specific presentation helpers.
tone, loading indicator variant, or empty-state shell variant, extend the
shared primitive and registry guard instead of adding page-local rounded pill
spans, spinner spans, or empty-state wrappers.
Patrol approval and remediation actions own approval, denial, reapproval,
review, and Assistant handoff semantics, but their visible action chrome must
compose the shared `Button` primitive for success, warning-solid, primary,
secondary, ghost, disabled, focus, and compact action behavior instead of
page-local button shells.
3. Keep remediation execution badge copy and severity styling aligned through `frontend-modern/src/components/patrol/RemediationStatus.tsx` and `frontend-modern/src/utils/remediationPresentation.ts`
4. Add or change Patrol header, summary, status runtime-state presentation, or runtime provider action presentation through `frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx`, `frontend-modern/src/features/patrol/PatrolIntelligenceSummary.tsx`, `frontend-modern/src/components/patrol/PatrolStatusBar.tsx`, `frontend-modern/src/utils/patrolRuntimePresentation.ts`, and `frontend-modern/src/utils/patrolRuntimeActions.ts`
Patrol summary presentation may show recent activity mix and trigger mode as
@@ -938,6 +938,8 @@
},
"requiredConsumers": [
{ "path": "src/components/AI/Chat/ChatMessages.tsx" },
{ "path": "src/components/patrol/ApprovalBanner.tsx" },
{ "path": "src/components/patrol/ApprovalSection.tsx" },
{ "path": "src/components/ErrorBoundary.tsx" },
{ "path": "src/components/Infrastructure/ResourceDetailDrawer.tsx" },
{ "path": "src/components/Infrastructure/ResourceDetailDrawerDebugTab.tsx" },
@@ -988,6 +990,23 @@
{ "path": "src/features/standalone/StandalonePageSurface.tsx" }
],
"forbiddenPatterns": [
{
"path": "src/components/patrol/ApprovalBanner.tsx",
"patterns": [
"px-3 py-1.5 bg-green-600 hover:bg-green-700",
"px-3 py-1.5 bg-surface-alt hover:bg-surface-hover",
"px-3 py-1.5 bg-amber-600 hover:bg-amber-700"
]
},
{
"path": "src/components/patrol/ApprovalSection.tsx",
"patterns": [
"px-3 py-1.5 bg-green-600 hover:bg-green-700",
"px-3 py-1.5 bg-amber-600 hover:bg-amber-700",
"px-3 py-1.5 bg-blue-600 hover:bg-blue-700",
"px-3 py-1.5 hover:bg-surface-hover disabled:opacity-50 text-muted"
]
},
{
"path": "src/components/Settings/GeneralSettingsPanel.tsx",
"patterns": [
@@ -5528,6 +5547,98 @@
"scripts/shared-template-audit.mjs"
]
},
{
"id": "button-success-patrol-approval-action-local-shell",
"category": "action-button",
"summary": "Patrol approval surfaces must not copy green execute-action button shells; use Button with the success variant and sm size.",
"canonical": {
"path": "src/components/shared/buttonModel.ts",
"export": "getButtonClass"
},
"scopes": ["src/components/patrol", "src/features/patrol"],
"extensions": [".tsx"],
"allPatterns": [
"px-3 py-1.5 bg-green-600 hover:bg-green-700",
"text-white text-xs font-medium rounded"
],
"legacyReason": "Retired migration debt. Patrol approval execute actions 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"
]
},
{
"id": "button-warning-solid-patrol-approval-action-local-shell",
"category": "action-button",
"summary": "Patrol approval surfaces must not copy amber solid review or reapprove button shells; use Button with the warningSolid variant and sm size.",
"canonical": {
"path": "src/components/shared/buttonModel.ts",
"export": "getButtonClass"
},
"scopes": ["src/components/patrol", "src/features/patrol"],
"extensions": [".tsx"],
"allPatterns": [
"px-3 py-1.5 bg-amber-600 hover:bg-amber-700",
"text-white text-xs font-medium rounded"
],
"legacyReason": "Retired migration debt. Patrol approval warning actions 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"
]
},
{
"id": "button-primary-patrol-approval-action-local-shell",
"category": "action-button",
"summary": "Patrol approval surfaces must not copy blue assistant-action button shells; use Button with the primary variant and sm size.",
"canonical": {
"path": "src/components/shared/buttonModel.ts",
"export": "getButtonClass"
},
"scopes": ["src/components/patrol", "src/features/patrol"],
"extensions": [".tsx"],
"allPatterns": [
"px-3 py-1.5 bg-blue-600 hover:bg-blue-700",
"text-white text-xs font-medium rounded"
],
"legacyReason": "Retired migration debt. Patrol Assistant handoff actions 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"
]
},
{
"id": "button-neutral-patrol-approval-action-local-shell",
"category": "action-button",
"summary": "Patrol approval surfaces must not copy neutral deny button shells; use Button with the secondary or ghost variant and sm size.",
"canonical": {
"path": "src/components/shared/buttonModel.ts",
"export": "getButtonClass"
},
"scopes": ["src/components/patrol", "src/features/patrol"],
"extensions": [".tsx"],
"allPatterns": [
"px-3 py-1.5 bg-surface-alt hover:bg-surface-hover",
"text-base-content text-xs font-medium rounded-md"
],
"legacyReason": "Retired migration debt. Patrol approval deny actions 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"
]
},
{
"id": "button-success-outline-settings-action-local-shell",
"category": "action-button",
@@ -16,6 +16,7 @@ import {
getApprovalExpiryStatusLabel,
getApprovalRiskPresentation,
} from '@/utils/approvalRiskPresentation';
import { Button } from '@/components/shared/Button';
import { LoadingSpinner } from '@/components/shared/LoadingSpinner';
import { MetadataBadge } from '@/components/shared/MetadataBadge';
import ShieldAlertIcon from 'lucide-solid/icons/shield-alert';
@@ -128,11 +129,13 @@ export const ApprovalBanner: Component<ApprovalBannerProps> = (props) => {
<div class="flex items-center gap-2">
<Show when={pending().length === 1 && firstApproval()}>
<button
<Button
type="button"
variant="success"
size="sm"
onClick={() => handleApprove(firstApproval()!)}
disabled={actionLoading() === firstApproval()!.id}
class="flex items-center gap-1.5 px-3 py-1.5 bg-green-600 hover:bg-green-700 disabled:bg-green-400 text-white text-xs font-medium rounded-md transition-colors"
class="gap-1.5"
>
<Show when={actionLoading() === firstApproval()!.id}>
<LoadingSpinner size="sm" tone="inverse" />
@@ -141,25 +144,29 @@ export const ApprovalBanner: Component<ApprovalBannerProps> = (props) => {
<CheckIcon class="w-3.5 h-3.5" />
</Show>
Approve & Execute
</button>
<button
</Button>
<Button
type="button"
variant="secondary"
size="sm"
onClick={() => handleDeny(firstApproval()!)}
disabled={actionLoading() === firstApproval()!.id}
class="flex items-center gap-1.5 px-3 py-1.5 bg-surface-alt hover:bg-surface-hover disabled:opacity-50 text-base-content text-xs font-medium rounded-md transition-colors"
class="gap-1.5"
>
<XIcon class="w-3.5 h-3.5" />
Deny
</button>
</Button>
</Show>
<Show when={pending().length > 1}>
<button
<Button
type="button"
variant="warningSolid"
size="sm"
onClick={handleReview}
class="flex items-center gap-1.5 px-3 py-1.5 bg-amber-600 hover:bg-amber-700 text-white text-xs font-medium rounded-md transition-colors"
class="gap-1.5"
>
Review
</button>
</Button>
</Show>
</div>
</div>
@@ -5,12 +5,15 @@
* States: Pending, Expired, Executed, Denied, Failed, Verified, VerificationFailed.
*/
import CheckIcon from 'lucide-solid/icons/check';
import MessageSquareIcon from 'lucide-solid/icons/message-square';
import { Component, Show, createSignal, createResource, createMemo } from 'solid-js';
import { aiIntelligenceStore } from '@/stores/aiIntelligence';
import { notificationStore } from '@/stores/notifications';
import { aiChatStore } from '@/stores/aiChat';
import { hasFeature } from '@/stores/license';
import { AIAPI, type ApprovalRequest, type ApprovalExecutionResult } from '@/api/ai';
import { Button } from '@/components/shared/Button';
import { LoadingSpinner } from '@/components/shared/LoadingSpinner';
import { MetadataBadge } from '@/components/shared/MetadataBadge';
import { getApprovalRiskPresentation } from '@/utils/approvalRiskPresentation';
@@ -234,44 +237,34 @@ export const ApprovalSection: Component<ApprovalSectionProps> = (props) => {
const renderRecoveryActions = (assistantLabel: string, onAssistantClick: (e: Event) => void) => (
<div class="flex items-center gap-2 mt-3 pt-3 border-t border-border-subtle">
<Show when={canAutoFix()}>
<button
<Button
type="button"
variant="warningSolid"
size="sm"
onClick={handleReapprove}
disabled={actionLoading() === 'reapprove'}
class="flex-1 px-3 py-1.5 bg-amber-600 hover:bg-amber-700 disabled:bg-amber-400 text-white text-xs font-medium rounded flex items-center justify-center gap-1.5"
class="flex-1 gap-1.5"
>
<Show when={actionLoading() === 'reapprove'}>
<LoadingSpinner size="sm" tone="inverse" />
</Show>
<Show when={actionLoading() !== 'reapprove'}>
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
<CheckIcon class="w-3.5 h-3.5" />
</Show>
Re-approve & Execute
</button>
</Button>
</Show>
<Show when={!canAutoFix()}>
<button
<Button
type="button"
variant="primary"
size="sm"
onClick={onAssistantClick}
class="flex-1 px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-xs font-medium rounded flex items-center justify-center gap-1.5"
class="flex-1 gap-1.5"
>
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
<MessageSquareIcon class="w-3.5 h-3.5" />
{assistantLabel}
</button>
</Button>
</Show>
</div>
);
@@ -301,10 +294,7 @@ export const ApprovalSection: Component<ApprovalSectionProps> = (props) => {
/>
</svg>
<span class="text-sm font-medium text-base-content">Fix Available</span>
<MetadataBadge
{...APPROVAL_SECTION_BADGE_PROPS}
tone={approvalRisk.badgeTone}
>
<MetadataBadge {...APPROVAL_SECTION_BADGE_PROPS} tone={approvalRisk.badgeTone}>
{approvalRisk.label} risk
</MetadataBadge>
</div>
@@ -316,62 +306,44 @@ export const ApprovalSection: Component<ApprovalSectionProps> = (props) => {
</div>
<div class="flex items-center gap-2 mt-3 pt-3 border-t border-border-subtle">
<Show when={canAutoFix()}>
<button
<Button
type="button"
variant="success"
size="sm"
onClick={(e) => handleApprove(approval, e)}
disabled={actionLoading() === approval.id}
class="flex-1 px-3 py-1.5 bg-green-600 hover:bg-green-700 disabled:bg-green-400 text-white text-xs font-medium rounded flex items-center justify-center gap-1.5"
class="flex-1 gap-1.5"
>
<Show when={actionLoading() === approval.id}>
<LoadingSpinner size="sm" tone="inverse" />
</Show>
<Show when={actionLoading() !== approval.id}>
<svg
class="w-3.5 h-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
<CheckIcon class="w-3.5 h-3.5" />
</Show>
Approve & Execute
</button>
<button
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={(e) => handleDeny(approval, e)}
disabled={actionLoading() === approval.id}
class="px-3 py-1.5 hover:bg-surface-hover disabled:opacity-50 text-muted text-xs font-medium rounded"
class="text-muted"
>
Deny
</button>
</Button>
</Show>
<Show when={!canAutoFix()}>
<button
<Button
type="button"
variant="primary"
size="sm"
onClick={(e) => handleFixWithAssistant(approval, null, e)}
class="flex-1 px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-xs font-medium rounded flex items-center justify-center gap-1.5"
class="flex-1 gap-1.5"
>
<svg
class="w-3.5 h-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
<MessageSquareIcon class="w-3.5 h-3.5" />
Fix with Assistant
</button>
</Button>
</Show>
</div>
</>
@@ -55,6 +55,17 @@ describe('ApprovalBanner', () => {
expect(approvalBannerSource).not.toMatch(/px-1\.5 py-0\.5 text-\[10px\] font-medium rounded/);
});
it('keeps approval action controls on the shared Button primitive', () => {
expect(approvalBannerSource).toContain('@/components/shared/Button');
expect(approvalBannerSource).toContain('<Button');
expect(approvalBannerSource).toContain('variant="success"');
expect(approvalBannerSource).toContain('variant="secondary"');
expect(approvalBannerSource).toContain('variant="warningSolid"');
expect(approvalBannerSource).not.toContain('px-3 py-1.5 bg-green-600 hover:bg-green-700');
expect(approvalBannerSource).not.toContain('px-3 py-1.5 bg-surface-alt hover:bg-surface-hover');
expect(approvalBannerSource).not.toContain('px-3 py-1.5 bg-amber-600 hover:bg-amber-700');
});
it('reviews the first approval-linked finding in canonical urgency order', () => {
state.pendingApprovals = [
{
@@ -92,6 +92,21 @@ describe('ApprovalSection', () => {
expect(approvalSectionSource).toContain('LoadingSpinner');
});
it('keeps approval action controls on the shared Button primitive', () => {
expect(approvalSectionSource).toContain('@/components/shared/Button');
expect(approvalSectionSource).toContain('<Button');
expect(approvalSectionSource).toContain('variant="success"');
expect(approvalSectionSource).toContain('variant="warningSolid"');
expect(approvalSectionSource).toContain('variant="primary"');
expect(approvalSectionSource).toContain('variant="ghost"');
expect(approvalSectionSource).not.toContain('px-3 py-1.5 bg-green-600 hover:bg-green-700');
expect(approvalSectionSource).not.toContain('px-3 py-1.5 bg-amber-600 hover:bg-amber-700');
expect(approvalSectionSource).not.toContain('px-3 py-1.5 bg-blue-600 hover:bg-blue-700');
expect(approvalSectionSource).not.toContain(
'px-3 py-1.5 hover:bg-surface-hover disabled:opacity-50 text-muted',
);
});
afterEach(() => {
cleanup();
});
@@ -37,6 +37,7 @@ describe('Button', () => {
);
expect(buttonModelSource).toContain('primaryFlat:');
expect(buttonModelSource).toContain('success:');
expect(buttonModelSource).toContain('warningSolid:');
expect(buttonModelSource).toContain('successOutline:');
expect(buttonModelSource).toContain('successGhost:');
expect(buttonModelSource).toContain('dangerGhost:');
@@ -105,6 +106,9 @@ describe('Button', () => {
<Button variant="success" size="settingsAction">
Open infrastructure
</Button>
<Button variant="warningSolid" size="sm">
Review approvals
</Button>
<Button variant="successOutline" size="settingsAction">
Open inventory
</Button>
@@ -137,6 +141,10 @@ describe('Button', () => {
expect(openInfrastructureButton).toHaveClass('bg-emerald-600');
expect(openInfrastructureButton).toHaveClass('text-white');
const reviewApprovalsButton = screen.getByRole('button', { name: 'Review approvals' });
expect(reviewApprovalsButton).toHaveClass('bg-amber-600');
expect(reviewApprovalsButton).toHaveClass('text-white');
const openInventoryButton = screen.getByRole('button', { name: 'Open inventory' });
expect(openInventoryButton).toHaveClass('border-emerald-300');
expect(openInventoryButton).toHaveClass('text-emerald-900');
@@ -3003,6 +3003,18 @@ describe('shared primitive guardrails', () => {
const settingsSuccessActionGuard = registry.patternGuards?.find(
(guard) => guard.id === 'button-success-settings-action-local-shell',
);
const patrolSuccessApprovalActionGuard = registry.patternGuards?.find(
(guard) => guard.id === 'button-success-patrol-approval-action-local-shell',
);
const patrolWarningApprovalActionGuard = registry.patternGuards?.find(
(guard) => guard.id === 'button-warning-solid-patrol-approval-action-local-shell',
);
const patrolPrimaryApprovalActionGuard = registry.patternGuards?.find(
(guard) => guard.id === 'button-primary-patrol-approval-action-local-shell',
);
const patrolNeutralApprovalActionGuard = registry.patternGuards?.find(
(guard) => guard.id === 'button-neutral-patrol-approval-action-local-shell',
);
const settingsSuccessOutlineActionGuard = registry.patternGuards?.find(
(guard) => guard.id === 'button-success-outline-settings-action-local-shell',
);
@@ -3081,6 +3093,8 @@ describe('shared primitive guardrails', () => {
expect(registeredRule?.canonical?.export).toBe('Button');
expect(registeredRule?.requiredConsumers?.map((consumer) => consumer.path)).toEqual([
'src/components/AI/Chat/ChatMessages.tsx',
'src/components/patrol/ApprovalBanner.tsx',
'src/components/patrol/ApprovalSection.tsx',
'src/components/ErrorBoundary.tsx',
'src/components/Infrastructure/ResourceDetailDrawer.tsx',
'src/components/Infrastructure/ResourceDetailDrawerDebugTab.tsx',
@@ -3126,6 +3140,23 @@ describe('shared primitive guardrails', () => {
]);
expect(registeredRule?.forbiddenPatterns).toEqual(
expect.arrayContaining([
expect.objectContaining({
path: 'src/components/patrol/ApprovalBanner.tsx',
patterns: expect.arrayContaining([
'px-3 py-1.5 bg-green-600 hover:bg-green-700',
'px-3 py-1.5 bg-surface-alt hover:bg-surface-hover',
'px-3 py-1.5 bg-amber-600 hover:bg-amber-700',
]),
}),
expect.objectContaining({
path: 'src/components/patrol/ApprovalSection.tsx',
patterns: expect.arrayContaining([
'px-3 py-1.5 bg-green-600 hover:bg-green-700',
'px-3 py-1.5 bg-amber-600 hover:bg-amber-700',
'px-3 py-1.5 bg-blue-600 hover:bg-blue-700',
'px-3 py-1.5 hover:bg-surface-hover disabled:opacity-50 text-muted',
]),
}),
expect.objectContaining({
path: 'src/components/Settings/GeneralSettingsPanel.tsx',
patterns: expect.arrayContaining([
@@ -3331,6 +3362,26 @@ describe('shared primitive guardrails', () => {
expect(billingAdminOrganizationsTableSource).not.toContain(
'px-2 py-1 text-xs rounded-md border border-border bg-surface hover:bg-surface-hover',
);
expect(approvalBannerSource).toContain('@/components/shared/Button');
expect(approvalBannerSource).toContain('<Button');
expect(approvalBannerSource).toContain('variant="success"');
expect(approvalBannerSource).toContain('variant="secondary"');
expect(approvalBannerSource).toContain('variant="warningSolid"');
expect(approvalBannerSource).not.toContain('px-3 py-1.5 bg-green-600 hover:bg-green-700');
expect(approvalBannerSource).not.toContain('px-3 py-1.5 bg-surface-alt hover:bg-surface-hover');
expect(approvalBannerSource).not.toContain('px-3 py-1.5 bg-amber-600 hover:bg-amber-700');
expect(approvalSectionSource).toContain('@/components/shared/Button');
expect(approvalSectionSource).toContain('<Button');
expect(approvalSectionSource).toContain('variant="success"');
expect(approvalSectionSource).toContain('variant="warningSolid"');
expect(approvalSectionSource).toContain('variant="primary"');
expect(approvalSectionSource).toContain('variant="ghost"');
expect(approvalSectionSource).not.toContain('px-3 py-1.5 bg-green-600 hover:bg-green-700');
expect(approvalSectionSource).not.toContain('px-3 py-1.5 bg-amber-600 hover:bg-amber-700');
expect(approvalSectionSource).not.toContain('px-3 py-1.5 bg-blue-600 hover:bg-blue-700');
expect(approvalSectionSource).not.toContain(
'px-3 py-1.5 hover:bg-surface-hover disabled:opacity-50 text-muted',
);
expect(registeredGuard?.canonical?.path).toBe('src/components/shared/buttonModel.ts');
expect(registeredGuard?.canonical?.export).toBe('getButtonClass');
expect(registeredGuard?.allPatterns).toEqual([
@@ -3439,6 +3490,70 @@ describe('shared primitive guardrails', () => {
expect(settingsSuccessActionGuard?.ignoredPaths).toEqual([
'src/components/shared/Button.test.tsx',
]);
expect(patrolSuccessApprovalActionGuard?.canonical?.path).toBe(
'src/components/shared/buttonModel.ts',
);
expect(patrolSuccessApprovalActionGuard?.canonical?.export).toBe('getButtonClass');
expect(patrolSuccessApprovalActionGuard?.allPatterns).toEqual([
'px-3 py-1.5 bg-green-600 hover:bg-green-700',
'text-white text-xs font-medium rounded',
]);
expect(patrolSuccessApprovalActionGuard?.scopes).toEqual([
'src/components/patrol',
'src/features/patrol',
]);
expect(patrolSuccessApprovalActionGuard?.allowedPaths ?? []).toHaveLength(0);
expect(patrolSuccessApprovalActionGuard?.ignoredPaths).toEqual([
'src/components/shared/Button.test.tsx',
]);
expect(patrolWarningApprovalActionGuard?.canonical?.path).toBe(
'src/components/shared/buttonModel.ts',
);
expect(patrolWarningApprovalActionGuard?.canonical?.export).toBe('getButtonClass');
expect(patrolWarningApprovalActionGuard?.allPatterns).toEqual([
'px-3 py-1.5 bg-amber-600 hover:bg-amber-700',
'text-white text-xs font-medium rounded',
]);
expect(patrolWarningApprovalActionGuard?.scopes).toEqual([
'src/components/patrol',
'src/features/patrol',
]);
expect(patrolWarningApprovalActionGuard?.allowedPaths ?? []).toHaveLength(0);
expect(patrolWarningApprovalActionGuard?.ignoredPaths).toEqual([
'src/components/shared/Button.test.tsx',
]);
expect(patrolPrimaryApprovalActionGuard?.canonical?.path).toBe(
'src/components/shared/buttonModel.ts',
);
expect(patrolPrimaryApprovalActionGuard?.canonical?.export).toBe('getButtonClass');
expect(patrolPrimaryApprovalActionGuard?.allPatterns).toEqual([
'px-3 py-1.5 bg-blue-600 hover:bg-blue-700',
'text-white text-xs font-medium rounded',
]);
expect(patrolPrimaryApprovalActionGuard?.scopes).toEqual([
'src/components/patrol',
'src/features/patrol',
]);
expect(patrolPrimaryApprovalActionGuard?.allowedPaths ?? []).toHaveLength(0);
expect(patrolPrimaryApprovalActionGuard?.ignoredPaths).toEqual([
'src/components/shared/Button.test.tsx',
]);
expect(patrolNeutralApprovalActionGuard?.canonical?.path).toBe(
'src/components/shared/buttonModel.ts',
);
expect(patrolNeutralApprovalActionGuard?.canonical?.export).toBe('getButtonClass');
expect(patrolNeutralApprovalActionGuard?.allPatterns).toEqual([
'px-3 py-1.5 bg-surface-alt hover:bg-surface-hover',
'text-base-content text-xs font-medium rounded-md',
]);
expect(patrolNeutralApprovalActionGuard?.scopes).toEqual([
'src/components/patrol',
'src/features/patrol',
]);
expect(patrolNeutralApprovalActionGuard?.allowedPaths ?? []).toHaveLength(0);
expect(patrolNeutralApprovalActionGuard?.ignoredPaths).toEqual([
'src/components/shared/Button.test.tsx',
]);
expect(settingsSuccessOutlineActionGuard?.canonical?.path).toBe(
'src/components/shared/buttonModel.ts',
);
@@ -2,6 +2,7 @@ export type ButtonVariant =
| 'primary'
| 'primaryFlat'
| 'warning'
| 'warningSolid'
| 'info'
| 'success'
| 'successOutline'
@@ -31,6 +32,8 @@ export const BUTTON_VARIANT_CLASSES: Record<ButtonVariant, string> = {
primaryFlat: 'border border-transparent bg-blue-600 text-white hover:bg-blue-700',
warning:
'border border-amber-300 bg-amber-100 text-amber-800 hover:bg-amber-200 dark:border-amber-700 dark:bg-amber-900 dark:text-amber-100 dark:hover:bg-amber-800',
warningSolid:
'border border-transparent bg-amber-600 text-white shadow-sm hover:bg-amber-700 dark:bg-amber-500 dark:hover:bg-amber-600',
info: 'border border-blue-200 bg-blue-50 text-blue-700 hover:bg-blue-100 dark:border-blue-700 dark:bg-blue-900 dark:text-blue-200',
success: 'border border-transparent bg-emerald-600 text-white shadow-sm hover:bg-emerald-700',
successOutline: