Converge security auth actions on shared Button primitive

This commit is contained in:
rcourtman
2026-06-13 22:20:03 +01:00
parent e73420d4d5
commit a418b8da27
6 changed files with 74 additions and 12 deletions
@@ -572,6 +572,10 @@ not a replacement status card, CTA band, or page-local nested card.
cloud-paid surfaces own Suspend, Activate, Reload, tenant state, and mutation
semantics, while `Button` owns the row-action chrome through the secondary
`sm` and `xs` sizes.
Security authentication settings actions follow the same boundary:
security/privacy owns auth setup, password-change, credential-rotation, and
read-only semantics, while `Button` owns the warning, primary, secondary,
and settings-action chrome.
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.
@@ -221,6 +221,11 @@ controls as normal product settings.
primitive. Security/privacy owns scope authority, wildcard behavior, preset
membership, and custom scope toggles; frontend-primitives owns active and
inactive pill tone, focus, disabled treatment, and pressed-state wiring.
6c. Keep authentication setup, password-change, and credential-rotation actions
on the shared `Button` primitive. Security/privacy owns the auth authority,
setup/rotation semantics, and read-only capability state;
frontend-primitives owns warning, primary, secondary, focus, disabled, and
settings-action chrome.
6. Keep the shared storage-directory and secure storage-file hardening helper aligned with the crypto manager plus control-plane magic-link key and store handling whenever runtime data-root ownership assumptions change.
7. Keep auth-env ingestion, hosted commercial base URL validation, and shared
fingerprint-verifier TLS defaults aligned whenever runtime auth loading,
@@ -970,6 +970,7 @@
{ "path": "src/components/Settings/ReportingPanel.tsx" },
{ "path": "src/components/Settings/ResourcePicker.tsx" },
{ "path": "src/components/Settings/SelfHostedCommercialRecoverySection.tsx" },
{ "path": "src/components/Settings/SecurityAuthPanel.tsx" },
{ "path": "src/components/Settings/SSOProvidersPanel.tsx" },
{ "path": "src/components/UpdateConfirmationModal.tsx" },
{ "path": "src/components/UpdateProgressModal.tsx" },
@@ -1038,6 +1039,14 @@
"min-h-10 sm:min-h-9 px-4 py-2.5 text-sm font-medium rounded-md border border-border text-base-content hover:bg-surface-hover transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
]
},
{
"path": "src/components/Settings/SecurityAuthPanel.tsx",
"patterns": [
"w-full sm:w-auto px-3 py-2 text-xs font-medium rounded-md border border-amber-300 text-amber-800 bg-amber-100 hover:bg-amber-200 transition-colors dark:border-amber-700 dark:text-amber-200 dark:bg-amber-900 dark:hover:bg-amber-800",
"w-full sm:w-auto min-h-10 sm:min-h-10 px-4 py-2.5 text-sm font-medium bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors",
"w-full sm:w-auto min-h-10 sm:min-h-10 px-4 py-2.5 text-sm font-medium border border-border text-base-content rounded-md hover:bg-surface-hover transition-colors"
]
},
{
"path": "src/components/Settings/SSOProvidersPanel.tsx",
"patterns": [
@@ -1,4 +1,5 @@
import { Component, Show, Accessor, Setter } from 'solid-js';
import { Button } from '@/components/shared/Button';
import { CalloutCard } from '@/components/shared/CalloutCard';
import SettingsPanel from '@/components/shared/SettingsPanel';
import { Toggle } from '@/components/shared/Toggle';
@@ -75,14 +76,15 @@ export const SecurityAuthPanel: Component<SecurityAuthPanelProps> = (props) => {
{SECURITY_AUTH_DISABLED_READ_ONLY_MESSAGE}
</p>
</Show>
<button
type="button"
<Button
variant="warning"
size="settingsActionXs"
class="w-full sm:w-auto"
onClick={() => props.setShowQuickSecuritySetup(!props.showQuickSecuritySetup())}
disabled={!props.canManage}
class="w-full sm:w-auto px-3 py-2 text-xs font-medium rounded-md border border-amber-300 text-amber-800 bg-amber-100 hover:bg-amber-200 transition-colors dark:border-amber-700 dark:text-amber-200 dark:bg-amber-900 dark:hover:bg-amber-800"
>
{SECURITY_AUTH_SETUP_LABEL}
</button>
</Button>
</div>
<Show when={props.canManage && props.showQuickSecuritySetup()}>
@@ -107,26 +109,28 @@ export const SecurityAuthPanel: Component<SecurityAuthPanelProps> = (props) => {
</div>
</Show>
<div class="flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center">
<button
type="button"
<Button
variant="primary"
size="settingsAction"
class="w-full sm:w-auto"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
props.setShowPasswordModal(true);
}}
disabled={!props.canManage}
class="w-full sm:w-auto min-h-10 sm:min-h-10 px-4 py-2.5 text-sm font-medium bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
Change password
</button>
<button
type="button"
</Button>
<Button
variant="secondary"
size="settingsAction"
class="w-full sm:w-auto"
onClick={() => props.setShowQuickSecurityWizard(!props.showQuickSecurityWizard())}
disabled={!props.canManage}
class="w-full sm:w-auto min-h-10 sm:min-h-10 px-4 py-2.5 text-sm font-medium border border-border text-base-content rounded-md hover:bg-surface-hover transition-colors"
>
Rotate credentials
</button>
</Button>
</div>
<div class="text-xs text-muted">
<span class="font-medium text-base-content">User:</span>{' '}
@@ -327,6 +327,22 @@ describe('settings architecture guardrails', () => {
expect(ssoProvidersPanelSource).not.toContain(
'text-blue-600 hover:underline flex items-center gap-1',
);
expect(securityAuthPanelSource).toContain(
"import { Button } from '@/components/shared/Button';",
);
expect(securityAuthPanelSource).toContain('variant="warning"');
expect(securityAuthPanelSource).toContain('variant="primary"');
expect(securityAuthPanelSource).toContain('size="settingsAction"');
expect(securityAuthPanelSource).not.toContain(
'w-full sm:w-auto px-3 py-2 text-xs font-medium rounded-md border border-amber-300',
);
expect(securityAuthPanelSource).not.toContain(
'w-full sm:w-auto min-h-10 sm:min-h-10 px-4 py-2.5 text-sm font-medium bg-blue-600',
);
expect(securityAuthPanelSource).not.toContain(
'w-full sm:w-auto min-h-10 sm:min-h-10 px-4 py-2.5 text-sm font-medium border border-border',
);
});
it('keeps settings callouts on the shared CalloutCard primitive', () => {
@@ -3098,6 +3098,7 @@ describe('shared primitive guardrails', () => {
'src/components/Settings/ReportingPanel.tsx',
'src/components/Settings/ResourcePicker.tsx',
'src/components/Settings/SelfHostedCommercialRecoverySection.tsx',
'src/components/Settings/SecurityAuthPanel.tsx',
'src/components/Settings/SSOProvidersPanel.tsx',
'src/components/UpdateConfirmationModal.tsx',
'src/components/UpdateProgressModal.tsx',
@@ -3167,6 +3168,14 @@ describe('shared primitive guardrails', () => {
'min-h-10 sm:min-h-9 px-4 py-2.5 text-sm font-medium rounded-md border border-border text-base-content hover:bg-surface-hover transition-colors disabled:opacity-60 disabled:cursor-not-allowed',
]),
}),
expect.objectContaining({
path: 'src/components/Settings/SecurityAuthPanel.tsx',
patterns: expect.arrayContaining([
'w-full sm:w-auto px-3 py-2 text-xs font-medium rounded-md border border-amber-300 text-amber-800 bg-amber-100 hover:bg-amber-200 transition-colors dark:border-amber-700 dark:text-amber-200 dark:bg-amber-900 dark:hover:bg-amber-800',
'w-full sm:w-auto min-h-10 sm:min-h-10 px-4 py-2.5 text-sm font-medium bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors',
'w-full sm:w-auto min-h-10 sm:min-h-10 px-4 py-2.5 text-sm font-medium border border-border text-base-content rounded-md hover:bg-surface-hover transition-colors',
]),
}),
expect.objectContaining({
path: 'src/components/Settings/SSOProvidersPanel.tsx',
patterns: expect.arrayContaining([
@@ -3955,6 +3964,21 @@ describe('shared primitive guardrails', () => {
expect(selfHostedCommercialRecoverySectionSource).not.toContain(
'min-h-10 sm:min-h-9 px-4 py-2.5 text-sm font-medium rounded-md border border-border text-base-content hover:bg-surface-hover transition-colors disabled:opacity-60 disabled:cursor-not-allowed',
);
expect(securityAuthPanelSource).toContain('@/components/shared/Button');
expect(securityAuthPanelSource).toContain('<Button');
expect(securityAuthPanelSource).toContain('variant="warning"');
expect(securityAuthPanelSource).toContain('variant="primary"');
expect(securityAuthPanelSource).toContain('size="settingsAction"');
expect(securityAuthPanelSource).toContain('size="settingsActionXs"');
expect(securityAuthPanelSource).not.toContain(
'w-full sm:w-auto px-3 py-2 text-xs font-medium rounded-md border border-amber-300 text-amber-800 bg-amber-100 hover:bg-amber-200 transition-colors dark:border-amber-700 dark:text-amber-200 dark:bg-amber-900 dark:hover:bg-amber-800',
);
expect(securityAuthPanelSource).not.toContain(
'w-full sm:w-auto min-h-10 sm:min-h-10 px-4 py-2.5 text-sm font-medium bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors',
);
expect(securityAuthPanelSource).not.toContain(
'w-full sm:w-auto min-h-10 sm:min-h-10 px-4 py-2.5 text-sm font-medium border border-border text-base-content rounded-md hover:bg-surface-hover transition-colors',
);
expect(discoveryTabSource).toContain('@/components/shared/Button');
expect(discoveryTabSource).toContain('@/components/shared/CopyableCodeRow');
expect(discoveryTabSource).toContain('CopyValueButton');