Add dynamic tab-aware subtitles to all policy shell pages

Follows the releases-unified-page pattern: static h1 title with a
computed() subtitle signal that changes when switching tabs.

Shell components updated:
- Governance: static subtitle → computed per 10 tabs
- Simulation: added h1 + computed subtitle for 9 tabs
- VEX & Exceptions: added h1 + computed subtitle for 8 tabs
- Policy Audit: added h1 + computed subtitle for 3 tabs
- Packs: replaced card-style header with releases pattern

Child h1 headers removed from 13 simulation sub-components to
eliminate double titles (action buttons preserved as standalone divs).

CSS aligned to releases pattern: title 1.5rem, subtitle 0.8125rem
with --color-text-secondary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-28 13:02:28 +02:00
parent fc6aa7c4b5
commit 7748c75934
18 changed files with 181 additions and 526 deletions

View File

@@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
computed,
DestroyRef,
inject,
signal,
@@ -33,6 +34,13 @@ const PAGE_TABS: readonly StellaPageTab[] = [
imports: [CommonModule, RouterOutlet, StellaPageTabsComponent],
template: `
<section class="policy-audit-shell" data-testid="policy-audit-shell">
<header class="audit-shell__header">
<div>
<h1 class="audit-shell__title">Policy Audit</h1>
<p class="audit-shell__subtitle">{{ activeSubtitle() }}</p>
</div>
</header>
<stella-page-tabs
[tabs]="pageTabs"
[activeTab]="activeSubview()"
@@ -49,6 +57,9 @@ const PAGE_TABS: readonly StellaPageTab[] = [
gap: 0.85rem;
}
.audit-shell__header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 1rem; }
.audit-shell__title { font-size: 1.5rem; font-weight: var(--font-weight-bold, 700); color: var(--color-text-heading); margin: 0 0 0.25rem; }
.audit-shell__subtitle { font-size: 0.8125rem; color: var(--color-text-secondary); margin: 0; }
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
@@ -60,6 +71,15 @@ export class PolicyDecisioningAuditShellComponent {
readonly pageTabs = PAGE_TABS;
readonly activeSubview = signal<AuditSubview>(this.readSubview());
protected readonly activeSubtitle = computed(() => {
switch (this.activeSubview()) {
case 'policy': return 'Policy promotions, simulations, approvals, and lint events.';
case 'vex': return 'VEX statement creation, revocation, and consensus events.';
case 'log': return 'Unified audit log across all policy actions.';
default: return 'Policy promotions, simulations, approvals, and lint events.';
}
});
constructor() {
this.router.events
.pipe(

View File

@@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
computed,
DestroyRef,
inject,
signal,
@@ -46,6 +47,13 @@ const PAGE_TABS: readonly StellaPageTab[] = [
imports: [CommonModule, RouterOutlet, StellaPageTabsComponent],
template: `
<section class="policy-vex-shell" data-testid="policy-vex-shell">
<header class="vex-shell__header">
<div>
<h1 class="vex-shell__title">VEX & Exceptions</h1>
<p class="vex-shell__subtitle">{{ activeSubtitle() }}</p>
</div>
</header>
<stella-page-tabs
[tabs]="pageTabs"
[activeTab]="activeSubview()"
@@ -62,6 +70,9 @@ const PAGE_TABS: readonly StellaPageTab[] = [
gap: 0.85rem;
}
.vex-shell__header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 1rem; }
.vex-shell__title { font-size: 1.5rem; font-weight: var(--font-weight-bold, 700); color: var(--color-text-heading); margin: 0 0 0.25rem; }
.vex-shell__subtitle { font-size: 0.8125rem; color: var(--color-text-secondary); margin: 0; }
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
@@ -73,6 +84,20 @@ export class PolicyDecisioningVexShellComponent {
readonly pageTabs = PAGE_TABS;
readonly activeSubview = signal<VexSubview>(this.readSubview());
protected readonly activeSubtitle = computed(() => {
switch (this.activeSubview()) {
case 'dashboard': return 'VEX statement overview and activity feed.';
case 'search': return 'Search VEX statements across all sources.';
case 'create': return 'Create new VEX statements for findings.';
case 'stats': return 'VEX coverage and statement metrics.';
case 'consensus': return 'Multi-source consensus resolution.';
case 'explorer': return 'Browse and inspect VEX data.';
case 'conflicts': return 'VEX statement conflicts and resolution.';
case 'exceptions': return 'Policy exception queue management.';
default: return 'VEX statement overview and activity feed.';
}
});
constructor() {
this.router.events
.pipe(

View File

@@ -47,17 +47,10 @@ const PACK_DETAIL_TABS: readonly StellaPageTab[] = [
imports: [CommonModule, RouterOutlet, StellaPageTabsComponent],
template: `
<section class="policy-pack-shell" data-testid="policy-pack-shell">
<header class="section-header">
<header class="pack-shell__header">
<div>
<p class="section-header__eyebrow">Packs</p>
<h2>{{ packId() ? 'Pack ' + packId() : 'Policy Pack Workspace' }}</h2>
<p>
{{
packId()
? 'Edit rules, YAML, approvals, and simulations for the selected pack.'
: 'Browse deterministic pack inventory and open a pack into authoring mode.'
}}
</p>
<h1 class="pack-shell__title">{{ packId() ? 'Pack ' + packId() : 'Policy Pack Workspace' }}</h1>
<p class="pack-shell__subtitle">{{ activeSubtitle() }}</p>
</div>
</header>
@@ -78,31 +71,9 @@ const PACK_DETAIL_TABS: readonly StellaPageTab[] = [
gap: 0.85rem;
}
.section-header {
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-lg);
background: var(--color-surface-primary);
padding: 1rem;
}
.section-header__eyebrow {
margin: 0 0 0.25rem;
color: var(--color-status-success);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.section-header h2 {
margin: 0;
color: var(--color-text-heading);
}
.section-header p {
margin: 0.35rem 0 0;
color: var(--color-text-secondary);
}
.pack-shell__header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 1rem; }
.pack-shell__title { font-size: 1.5rem; font-weight: var(--font-weight-bold, 700); color: var(--color-text-heading); margin: 0 0 0.25rem; }
.pack-shell__subtitle { font-size: 0.8125rem; color: var(--color-text-secondary); margin: 0; }
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
@@ -118,6 +89,23 @@ export class PolicyPackShellComponent {
return this.packId() ? PACK_DETAIL_TABS : WORKSPACE_TABS;
});
protected readonly activeSubtitle = computed(() => {
if (!this.packId()) {
return 'Browse deterministic pack inventory and open a pack into authoring mode.';
}
switch (this.activeSubview()) {
case 'dashboard': return 'Overview of the selected policy pack.';
case 'edit': return 'Edit rules and configuration for this pack.';
case 'rules': return 'Manage individual rules within this pack.';
case 'yaml': return 'View and edit the raw YAML definition.';
case 'approvals': return 'Review and manage approval workflows.';
case 'simulate': return 'Run simulations against this pack.';
case 'explain': return 'Explain evaluation results for a simulation run.';
case 'workspace': return 'Browse deterministic pack inventory and open a pack into authoring mode.';
default: return 'Overview of the selected policy pack.';
}
});
constructor() {
this.router.events
.pipe(

View File

@@ -1,5 +1,5 @@
import { Component, ChangeDetectionStrategy, signal, inject, OnInit, DestroyRef } from '@angular/core';
import { Component, ChangeDetectionStrategy, signal, computed, inject, OnInit, DestroyRef } from '@angular/core';
import { Router, RouterOutlet, NavigationEnd, ActivatedRoute } from '@angular/router';
import { filter } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@@ -33,7 +33,7 @@ const GOVERNANCE_TABS: readonly StellaPageTab[] = [
<section class="governance">
<div class="governance__header">
<h1 class="governance__title">Policy Governance</h1>
<p class="governance__subtitle">Configure risk budgets, trust weights, staleness rules, sealed mode, and risk profiles.</p>
<p class="governance__subtitle">{{ activeSubtitle() }}</p>
</div>
<stella-page-tabs
@@ -61,20 +61,23 @@ const GOVERNANCE_TABS: readonly StellaPageTab[] = [
}
.governance__header {
margin-bottom: 1.5rem;
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 1rem;
}
.governance__title {
margin: 0.25rem 0 0;
font-size: 1.75rem;
font-weight: var(--font-weight-bold);
font-size: 1.5rem;
font-weight: var(--font-weight-bold, 700);
color: var(--color-text-heading);
margin: 0 0 0.25rem;
}
.governance__subtitle {
margin: 0.25rem 0 0;
color: var(--color-text-muted);
font-size: 0.95rem;
font-size: 0.8125rem;
color: var(--color-text-secondary);
margin: 0;
}
`]
@@ -116,6 +119,22 @@ export class PolicyGovernanceComponent implements OnInit {
protected readonly activeTab = signal<string>('budget');
protected readonly GOVERNANCE_TABS = GOVERNANCE_TABS;
protected readonly activeSubtitle = computed(() => {
switch (this.activeTab()) {
case 'budget': return 'Monitor budget consumption and manage risk thresholds.';
case 'trust': return 'Configure trust weights for vulnerability sources and issuers.';
case 'staleness': return 'Configure data freshness thresholds and enforcement rules.';
case 'sealed': return 'Manage air-gapped operation mode and trusted source overrides.';
case 'profiles': return 'Manage risk evaluation profiles and signal weights.';
case 'validator': return 'Validate policy documents against the schema.';
case 'audit': return 'Track all governance configuration changes.';
case 'conflicts': return 'Identify and resolve rule overlaps and precedence issues.';
case 'schema-playground': return 'Test and validate risk profile schemas interactively.';
case 'schema-docs': return 'Reference documentation for risk profile configuration schemas.';
default: return 'Monitor budget consumption and manage risk thresholds.';
}
});
ngOnInit(): void {
this.syncTabFromRoute();
this.router.events

View File

@@ -23,15 +23,7 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section class="batch-evaluation">
<header class="batch-evaluation__header">
<div>
<p class="batch-evaluation__eyebrow">Policy Simulation Studio</p>
<h1>Batch Evaluation</h1>
<p class="batch-evaluation__lede">
Evaluate multiple artifacts against policy rules simultaneously.
</p>
</div>
<div class="header-tabs">
<div class="batch-evaluation__actions">
<button
class="tab-btn"
[class.tab-btn--active]="activeTab() === 'new'"
@@ -47,7 +39,6 @@ import {
History
</button>
</div>
</header>
<!-- New Evaluation Tab -->
@if (activeTab() === 'new') {
@@ -431,35 +422,11 @@ import {
padding: 1.5rem;
}
.batch-evaluation__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1.5rem;
}
.batch-evaluation__eyebrow {
margin: 0;
color: var(--color-status-success);
font-size: 0.8rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.batch-evaluation__header h1 {
margin: 0.25rem 0 0;
color: var(--color-text-heading);
font-size: 1.5rem;
}
.batch-evaluation__lede {
margin: 0.25rem 0 0;
color: var(--color-text-muted);
}
.header-tabs {
.batch-evaluation__actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-bottom: 1rem;
}
.tab-btn {

View File

@@ -24,15 +24,7 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section class="conflict-detection" [attr.aria-busy]="loading()">
<header class="conflict-detection__header">
<div>
<p class="conflict-detection__eyebrow">Policy Simulation Studio</p>
<h1>Conflict Detection</h1>
<p class="conflict-detection__lede">
Detect policy conflicts and get AI-assisted resolution suggestions.
</p>
</div>
<div class="header-actions">
<div class="conflict-detection__actions">
<button class="btn btn--primary" (click)="analyzeConflicts()" [disabled]="selectedPolicies().length < 2 || loading()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8" />
@@ -41,7 +33,6 @@ import {
Analyze Conflicts
</button>
</div>
</header>
<!-- Policy Selection -->
<div class="policy-selection">
@@ -344,35 +335,11 @@ import {
padding: 1.5rem;
}
.conflict-detection__header {
.conflict-detection__actions {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1.5rem;
}
.conflict-detection__eyebrow {
margin: 0;
color: var(--color-status-warning);
font-size: 0.8rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.conflict-detection__header h1 {
margin: 0.25rem 0 0;
color: var(--color-text-heading);
font-size: 1.5rem;
}
.conflict-detection__lede {
margin: 0.25rem 0 0;
color: var(--color-text-muted);
}
.header-actions {
display: flex;
gap: 0.75rem;
justify-content: flex-end;
gap: 0.5rem;
margin-bottom: 1rem;
}
.btn {

View File

@@ -29,23 +29,14 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section class="coverage" [attr.aria-busy]="loading()">
<header class="coverage__header">
<div>
<p class="coverage__eyebrow">Policy Simulation Studio</p>
<h1>Test Coverage</h1>
<p class="coverage__lede">
View coverage percentage per policy rule and identify missing test cases.
</p>
</div>
<div class="coverage__actions">
<button class="btn btn--secondary" (click)="loadCoverage()" [disabled]="loading()">
Refresh
</button>
<button class="btn btn--primary" (click)="runTests()" [disabled]="loading()">
{{ loading() ? 'Running...' : 'Run Tests' }}
</button>
</div>
</header>
<div class="coverage__actions">
<button class="btn btn--secondary" (click)="loadCoverage()" [disabled]="loading()">
Refresh
</button>
<button class="btn btn--primary" (click)="runTests()" [disabled]="loading()">
{{ loading() ? 'Running...' : 'Run Tests' }}
</button>
</div>
<!-- Coverage Summary -->
@if (result()) {
@@ -261,35 +252,11 @@ import {
padding: 1.5rem;
}
.coverage__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1.5rem;
}
.coverage__eyebrow {
margin: 0;
color: var(--color-status-success);
font-size: 0.8rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.coverage__header h1 {
margin: 0.25rem 0 0;
color: var(--color-text-heading);
font-size: 1.5rem;
}
.coverage__lede {
margin: 0.25rem 0 0;
color: var(--color-text-muted);
}
.coverage__actions {
display: flex;
gap: 0.75rem;
justify-content: flex-end;
gap: 0.5rem;
margin-bottom: 1rem;
}
.btn {

View File

@@ -25,18 +25,11 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section class="effective-policy" [attr.aria-busy]="loading()">
<header class="effective-policy__header">
<div>
<p class="effective-policy__eyebrow">Policy Simulation Studio</p>
<h1>Effective Policies</h1>
<p class="effective-policy__lede">
View which policies apply to each resource based on inheritance and overrides.
</p>
</div>
<div class="effective-policy__actions">
<button class="btn btn--primary" (click)="loadPolicies()" [disabled]="loading()">
{{ loading() ? 'Loading...' : 'Refresh' }}
</button>
</header>
</div>
<!-- Filters -->
<div class="effective-policy__filters">
@@ -167,31 +160,7 @@ import {
padding: 1.5rem;
}
.effective-policy__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1.5rem;
}
.effective-policy__eyebrow {
margin: 0;
color: var(--color-status-excepted);
font-size: 0.8rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.effective-policy__header h1 {
margin: 0.25rem 0 0;
color: var(--color-text-heading);
font-size: 1.5rem;
}
.effective-policy__lede {
margin: 0.25rem 0 0;
color: var(--color-text-muted);
}
.effective-policy__actions { display: flex; justify-content: flex-end; gap: 0.5rem; margin-bottom: 1rem; }
.btn {
padding: 0.6rem 1.25rem;

View File

@@ -25,18 +25,11 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section class="audit-log" [attr.aria-busy]="loading()">
<header class="audit-log__header">
<div>
<p class="audit-log__eyebrow">Policy Simulation Studio</p>
<h1>Audit Log</h1>
<p class="audit-log__lede">
View change history for policy packs with actor and timestamp details.
</p>
</div>
<div class="audit-log__actions">
<button class="btn btn--primary" (click)="loadAuditLog()" [disabled]="loading()">
Refresh
</button>
</header>
</div>
<!-- Filters -->
<div class="audit-log__filters">
@@ -193,31 +186,7 @@ import {
padding: 1.5rem;
}
.audit-log__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1.5rem;
}
.audit-log__eyebrow {
margin: 0;
color: var(--color-status-excepted);
font-size: 0.8rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.audit-log__header h1 {
margin: 0.25rem 0 0;
color: var(--color-text-heading);
font-size: 1.5rem;
}
.audit-log__lede {
margin: 0.25rem 0 0;
color: var(--color-text-muted);
}
.audit-log__actions { display: flex; justify-content: flex-end; gap: 0.5rem; margin-bottom: 1rem; }
.btn {
padding: 0.6rem 1.25rem;

View File

@@ -29,15 +29,7 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section class="diff-viewer" [attr.aria-busy]="loading()">
<header class="diff-viewer__header">
<div>
<p class="diff-viewer__eyebrow">Policy Simulation Studio</p>
<h1>Policy Diff</h1>
<p class="diff-viewer__lede">
Compare policy versions to see what changed.
</p>
</div>
@if (result()) {
@if (result()) {
<div class="diff-viewer__versions">
<span class="version-badge version-badge--from">v{{ result()?.fromVersion }}</span>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -47,7 +39,6 @@ import {
<span class="version-badge version-badge--to">v{{ result()?.toVersion }}</span>
</div>
}
</header>
<!-- Stats Summary -->
@if (result()) {
@@ -175,32 +166,6 @@ import {
padding: 1.5rem;
}
.diff-viewer__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1.5rem;
}
.diff-viewer__eyebrow {
margin: 0;
color: var(--color-status-info);
font-size: 0.8rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.diff-viewer__header h1 {
margin: 0.25rem 0 0;
color: var(--color-text-heading);
font-size: 1.5rem;
}
.diff-viewer__lede {
margin: 0.25rem 0 0;
color: var(--color-text-muted);
}
.diff-viewer__versions {
display: flex;
align-items: center;

View File

@@ -24,18 +24,11 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section class="policy-exception" [attr.aria-busy]="loading()">
<header class="policy-exception__header">
<div>
<p class="policy-exception__eyebrow">Policy Simulation Studio</p>
<h1>Policy Exceptions</h1>
<p class="policy-exception__lede">
Manage policy exceptions for specific resources or vulnerabilities.
</p>
</div>
<div class="policy-exception__actions">
<button class="btn btn--primary" (click)="showCreateForm.set(true)" [disabled]="showCreateForm()">
Create Exception
</button>
</header>
</div>
<!-- Create Exception Form -->
@if (showCreateForm()) {
@@ -254,31 +247,7 @@ import {
padding: 1.5rem;
}
.policy-exception__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1.5rem;
}
.policy-exception__eyebrow {
margin: 0;
color: var(--color-severity-high);
font-size: 0.8rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.policy-exception__header h1 {
margin: 0.25rem 0 0;
color: var(--color-text-heading);
font-size: 1.5rem;
}
.policy-exception__lede {
margin: 0.25rem 0 0;
color: var(--color-text-muted);
}
.policy-exception__actions { display: flex; justify-content: flex-end; gap: 0.5rem; margin-bottom: 1rem; }
.btn {
padding: 0.6rem 1.25rem;

View File

@@ -29,20 +29,11 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section class="policy-lint" [attr.aria-busy]="loading()">
<header class="policy-lint__header">
<div>
<p class="policy-lint__eyebrow">Policy Simulation Studio</p>
<h1>Policy Lint</h1>
<p class="policy-lint__lede">
Check policy syntax, semantics, and best practices.
</p>
</div>
<div class="policy-lint__actions">
<button class="btn btn--primary" (click)="runLint()" [disabled]="loading()">
{{ loading() ? 'Linting...' : 'Run Lint' }}
</button>
</div>
</header>
<div class="policy-lint__actions">
<button class="btn btn--primary" (click)="runLint()" [disabled]="loading()">
{{ loading() ? 'Linting...' : 'Run Lint' }}
</button>
</div>
<!-- Compilation Status -->
@if (result()) {
@@ -232,31 +223,7 @@ import {
padding: 1.5rem;
}
.policy-lint__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1.5rem;
}
.policy-lint__eyebrow {
margin: 0;
color: var(--color-status-warning);
font-size: 0.8rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.policy-lint__header h1 {
margin: 0.25rem 0 0;
color: var(--color-text-heading);
font-size: 1.5rem;
}
.policy-lint__lede {
margin: 0.25rem 0 0;
color: var(--color-text-muted);
}
.policy-lint__actions { display: flex; justify-content: flex-end; gap: 0.5rem; margin-bottom: 1rem; }
.btn {
padding: 0.6rem 1.25rem;

View File

@@ -24,16 +24,6 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section class="merge-preview" [attr.aria-busy]="loading()">
<header class="merge-preview__header">
<div>
<p class="merge-preview__eyebrow">Policy Simulation Studio</p>
<h1>Merge Preview</h1>
<p class="merge-preview__lede">
Preview the result of merging multiple policy packs.
</p>
</div>
</header>
<!-- Source Selection -->
<div class="merge-preview__sources">
<form [formGroup]="sourceForm" (ngSubmit)="generatePreview()">
@@ -212,29 +202,6 @@ import {
padding: 1.5rem;
}
.merge-preview__header {
margin-bottom: 1.5rem;
}
.merge-preview__eyebrow {
margin: 0;
color: var(--color-status-success);
font-size: 0.8rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.merge-preview__header h1 {
margin: 0.25rem 0 0;
color: var(--color-text-heading);
font-size: 1.5rem;
}
.merge-preview__lede {
margin: 0.25rem 0 0;
color: var(--color-text-muted);
}
.merge-preview__sources {
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-primary);

View File

@@ -33,18 +33,11 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section class="promotion-gate" [attr.aria-busy]="loading()">
<header class="promotion-gate__header">
<div>
<p class="promotion-gate__eyebrow">Policy Simulation Studio</p>
<h1>Promotion Gate</h1>
<p class="promotion-gate__lede">
Checklist of requirements before promoting policy to production.
</p>
</div>
<div class="promotion-gate__actions">
<button class="btn btn--primary" (click)="checkGate()" [disabled]="loading()">
{{ loading() ? 'Checking...' : 'Check Requirements' }}
</button>
</header>
</div>
<!-- Policy Info -->
@if (result()) {
@@ -239,31 +232,7 @@ import {
padding: 1.5rem;
}
.promotion-gate__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1.5rem;
}
.promotion-gate__eyebrow {
margin: 0;
color: var(--color-status-success);
font-size: 0.8rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.promotion-gate__header h1 {
margin: 0.25rem 0 0;
color: var(--color-text-heading);
font-size: 1.5rem;
}
.promotion-gate__lede {
margin: 0.25rem 0 0;
color: var(--color-text-muted);
}
.promotion-gate__actions { display: flex; justify-content: flex-end; gap: 0.5rem; margin-bottom: 1rem; }
.btn {
padding: 0.6rem 1.25rem;

View File

@@ -17,23 +17,13 @@ import { ShadowModeStateService } from './shadow-mode-state.service';
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section class="shadow-dashboard" [attr.aria-busy]="loading()">
<header class="shadow-dashboard__header">
<div>
<p class="shadow-dashboard__eyebrow">Policy Simulation Studio</p>
<h1>Shadow Mode Dashboard</h1>
<p class="shadow-dashboard__lede">
Compare shadow policy evaluations against active production policy.
</p>
</div>
<app-shadow-mode-indicator
[config]="config()"
[loading]="loading()"
(enable)="onEnableShadowMode()"
(disable)="onDisableShadowMode()"
(viewResults)="loadResults()"
/>
</header>
<app-shadow-mode-indicator
[config]="config()"
[loading]="loading()"
(enable)="onEnableShadowMode()"
(disable)="onDisableShadowMode()"
(viewResults)="loadResults()"
/>
<!-- Summary Cards -->
@if (results()) {
@@ -213,33 +203,6 @@ import { ShadowModeStateService } from './shadow-mode-state.service';
padding: 1.5rem;
}
.shadow-dashboard__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
margin-bottom: 1.5rem;
}
.shadow-dashboard__eyebrow {
margin: 0;
color: var(--color-status-excepted-border);
font-size: 0.8rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.shadow-dashboard__header h1 {
margin: 0.25rem 0 0;
color: var(--color-text-heading);
font-size: 1.5rem;
}
.shadow-dashboard__lede {
margin: 0.25rem 0 0;
color: var(--color-text-muted);
}
.shadow-dashboard__summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));

View File

@@ -27,15 +27,8 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section class="sim-console" [attr.aria-busy]="loading()">
<header class="sim-console__header">
<div>
<p class="sim-console__eyebrow">Policy Simulation Studio</p>
<h1>Simulation Console</h1>
<p class="sim-console__lede">
Run policy simulations against SBOMs to preview evaluation outcomes.
</p>
</div>
@if (result()) {
@if (result()) {
<div class="sim-console__actions">
<div class="sim-console__status">
<span
class="status-pill"
@@ -51,8 +44,8 @@ import {
</span>
}
</div>
}
</header>
</div>
}
<div class="sim-console__layout">
<!-- Input Form -->
@@ -332,31 +325,7 @@ import {
padding: 1.5rem;
}
.sim-console__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1.5rem;
}
.sim-console__eyebrow {
margin: 0;
color: var(--color-status-info);
font-size: 0.8rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.sim-console__header h1 {
margin: 0.25rem 0 0;
color: var(--color-text-heading);
font-size: 1.5rem;
}
.sim-console__lede {
margin: 0.25rem 0 0;
color: var(--color-text-muted);
}
.sim-console__actions { display: flex; justify-content: flex-end; gap: 0.5rem; margin-bottom: 1rem; }
.sim-console__status {
display: flex;

View File

@@ -1,5 +1,5 @@

import { Component, ChangeDetectionStrategy, signal, inject, OnInit } from '@angular/core';
import { Component, ChangeDetectionStrategy, signal, computed, inject, OnInit } from '@angular/core';
import { RouterModule, Router } from '@angular/router';
import { ShadowModeIndicatorComponent } from './shadow-mode-indicator.component';
@@ -33,6 +33,13 @@ const SIMULATION_TABS: readonly StellaPageTab[] = [
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section class="simulation">
<header class="simulation__header">
<div>
<h1 class="simulation__title">Policy Simulation</h1>
<p class="simulation__subtitle">{{ activeSubtitle() }}</p>
</div>
</header>
<!-- MANDATORY: Shadow Mode Indicator - Visible on all policy views -->
<div class="simulation__shadow-banner">
<app-shadow-mode-indicator
@@ -181,6 +188,10 @@ const SIMULATION_TABS: readonly StellaPageTab[] = [
padding: 1.5rem;
}
.simulation__header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 1rem; }
.simulation__title { font-size: 1.5rem; font-weight: var(--font-weight-bold, 700); color: var(--color-text-heading); margin: 0 0 0.25rem; }
.simulation__subtitle { font-size: 0.8125rem; color: var(--color-text-secondary); margin: 0; }
.simulation__shadow-banner {
margin-bottom: 1rem;
}
@@ -284,6 +295,23 @@ export class SimulationDashboardComponent implements OnInit {
private readonly shadowModeState = inject(ShadowModeStateService);
protected readonly activeTab = signal<string>('shadow');
private static readonly SUBTITLES: Record<string, string> = {
shadow: 'Compare shadow policy evaluations against active production policy.',
console: 'Run policies against test data and review evaluations.',
lint: 'Syntax and semantic validation for policy documents.',
coverage: 'Test coverage metrics per rule and policy pack.',
effective: 'Which policies apply to which environments and artifacts.',
audit: 'Simulation and policy change history.',
exceptions: 'Manage temporary policy exception waivers.',
promotion: 'Pre-promotion checklist and gate enforcement.',
merge: 'Preview pack merge results before applying.',
};
protected readonly activeSubtitle = computed(() =>
SimulationDashboardComponent.SUBTITLES[this.activeTab()] ?? ''
);
protected readonly shadowConfig = this.shadowModeState.config;
protected readonly shadowLoading = this.shadowModeState.loading;

View File

@@ -28,15 +28,7 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section class="sim-history" [attr.aria-busy]="loading()">
<header class="sim-history__header">
<div>
<p class="sim-history__eyebrow">Policy Simulation Studio</p>
<h1>Simulation History</h1>
<p class="sim-history__lede">
View past simulation runs, verify reproducibility, and compare results.
</p>
</div>
<div class="header-actions">
<div class="sim-history__actions">
<button
class="btn btn--secondary"
[disabled]="selectedForComparison().length !== 2"
@@ -48,7 +40,6 @@ import {
Refresh
</button>
</div>
</header>
<!-- Filters -->
<div class="sim-history__filters">
@@ -384,35 +375,11 @@ import {
padding: 1.5rem;
}
.sim-history__header {
.sim-history__actions {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1.5rem;
}
.sim-history__eyebrow {
margin: 0;
color: var(--color-status-excepted);
font-size: 0.8rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.sim-history__header h1 {
margin: 0.25rem 0 0;
color: var(--color-text-heading);
font-size: 1.5rem;
}
.sim-history__lede {
margin: 0.25rem 0 0;
color: var(--color-text-muted);
}
.header-actions {
display: flex;
gap: 0.75rem;
justify-content: flex-end;
gap: 0.5rem;
margin-bottom: 1rem;
}
.btn {