mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-09 18:15:50 +00:00
Converge Patrol approval actions on shared Button primitives
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user