Extend stella-quick-links with aside layout + descriptions, standardize usage

Component extension:
- Add 'description' field to StellaQuickLink interface
- Add 'layout' input: 'inline' (default, horizontal dots) or 'aside' (vertical
  with descriptions, border-left accent on hover)
- Aside layout shows label + description per link in a vertical list
- Full CSS for aside variant: hover states, focus ring, icon transitions

Dashboard page:
- Quick Links moved to aside panel with elevated background and border
- All 6 links now have descriptions (e.g., "Deployment timeline and run history")

Evidence Overview page:
- "Shortcuts" and "Related Domains" sections use layout="aside"
- All 10 links now have descriptions

AGENTS.md:
- New "Quick Links Convention (MANDATORY)" rule: must use stella-quick-links
  with layout="aside", descriptions, and right-aligned aside placement

Remaining pages (8+) to be updated in follow-up commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-28 12:39:27 +02:00
parent b7fc87893c
commit cc615f8e19
4 changed files with 192 additions and 50 deletions

View File

@@ -141,6 +141,31 @@ The Promote button on release detail pages must follow a three-state model:
Use `showPromote` (computed, boolean) for visibility and `canPromote` (computed, boolean) for the enabled/disabled state.
Use `promoteDisabledReason` (computed, string | null) for the disabled tooltip.
## Quick Links Convention (MANDATORY)
All pages that include navigational quick links **must** follow these rules:
1. Use the `<stella-quick-links>` component — never raw `<nav>` with dot separators
2. Pass `layout="aside"` for page-level quick links (use `inline` only for inline/contextual links)
3. Include a `description` for every link explaining what the user will find there
4. Place quick links in a right-aligned aside panel with a border and elevated background
5. Include a `label` (e.g., "Related", "Quick Links", "Shortcuts")
**Pattern:**
```html
<aside class="page-aside">
<stella-quick-links
[links]="quickLinks"
label="Quick Links"
layout="aside" />
</aside>
```
Each link must have `label`, `route`, and `description`:
```typescript
{ label: 'Security Posture', route: '/security', description: 'Risk posture and advisory freshness' }
```
## Metric / KPI Cards Convention (MANDATORY)
All metric badges, stat cards, KPI tiles, and summary indicators **must** use `<stella-metric-card>`.

View File

@@ -453,8 +453,10 @@ interface PendingAction {
</a>
</div>
<!-- 6. Quick Links -->
<stella-quick-links [links]="dashboardQuickLinks" label="Quick Links" />
<!-- 6. Quick Links (right aside) -->
<aside class="dashboard-aside">
<stella-quick-links [links]="dashboardQuickLinks" label="Quick Links" layout="aside" />
</aside>
}
</div>
`,
@@ -1112,6 +1114,14 @@ interface PendingAction {
text-decoration: underline;
}
.dashboard-aside {
margin-top: 1rem;
border: 1px solid var(--color-border-primary);
border-radius: var(--radius-md);
padding: 0.75rem 0;
background: var(--color-surface-elevated);
}
/* Status dot (reused in summary cards) */
.status-dot {
display: inline-block;
@@ -1372,12 +1382,12 @@ export class DashboardV3Component implements OnInit, AfterViewInit, OnDestroy {
readonly riskTableAtTop = signal(true);
readonly dashboardQuickLinks: StellaQuickLink[] = [
{ label: 'Release Runs', route: '/releases/runs' },
{ label: 'Security & Risk', route: '/security' },
{ label: 'Operations', route: '/ops/operations' },
{ label: 'Evidence', route: '/evidence' },
{ label: 'Platform Setup', route: '/ops/platform-setup' },
{ label: 'Diagnostics', route: '/ops/operations/doctor' },
{ label: 'Release Runs', route: '/releases/runs', description: 'Deployment timeline and run history' },
{ label: 'Security & Risk', route: '/security', description: 'Posture, findings, and reachability' },
{ label: 'Operations', route: '/ops/operations', description: 'Platform health and execution control' },
{ label: 'Evidence', route: '/evidence', description: 'Decision capsules and audit trail' },
{ label: 'Platform Setup', route: '/ops/platform-setup', description: 'Environments, integrations, topology' },
{ label: 'Diagnostics', route: '/ops/operations/doctor', description: 'Run health checks on your deployment' },
];
// -- Loading states -------------------------------------------------------

View File

@@ -109,10 +109,9 @@ type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
</div>
</section>
<section class="shortcuts-section" aria-label="Evidence home shortcuts">
<h2 class="section-title">Shortcuts</h2>
<stella-quick-links [links]="shortcutLinks" />
</section>
<aside class="evidence-aside" aria-label="Evidence shortcuts">
<stella-quick-links [links]="shortcutLinks" label="Shortcuts" layout="aside" />
</aside>
<!-- Quick Stats (auditor detail: audit events, proof chains) -->
<section class="stats-section" aria-label="Evidence statistics">
@@ -136,11 +135,9 @@ type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
</div>
</section>
<!-- Cross-domain links -->
<section class="cross-links" aria-label="Related domain links">
<h2 class="section-title">Related Domains</h2>
<stella-quick-links [links]="relatedDomainLinks" />
</section>
<aside class="evidence-aside" aria-label="Related domains">
<stella-quick-links [links]="relatedDomainLinks" label="Related" layout="aside" />
</aside>
<!-- Trust ownership note -->
<aside class="ownership-note" role="note">
@@ -443,19 +440,19 @@ export class EvidenceAuditOverviewComponent {
readonly mode = signal<EvidenceHomeMode>('normal');
readonly shortcutLinks: StellaQuickLink[] = [
{ label: 'Audit Log', route: '/evidence/audit-log' },
{ label: 'Export Center', route: '/evidence/exports' },
{ label: 'Evidence Bundles', route: '/releases/bundles' },
{ label: 'Replay & Verify', route: '/evidence/verify-replay' },
{ label: 'Proof Chains', route: '/evidence/capsules' },
{ label: 'Trust & Signing', route: '/setup/trust-signing' },
{ label: 'Audit Log', route: '/evidence/audit-log', description: 'Cross-module audit trail for compliance' },
{ label: 'Export Center', route: '/evidence/exports', description: 'Export profiles and StellaBundle generation' },
{ label: 'Evidence Bundles', route: '/releases/bundles', description: 'Sealed evidence bundles for auditors' },
{ label: 'Replay & Verify', route: '/evidence/verify-replay', description: 'Deterministic replay of past decisions' },
{ label: 'Proof Chains', route: '/evidence/capsules', description: 'Signed decision capsules with evidence' },
{ label: 'Trust & Signing', route: '/setup/trust-signing', description: 'Signing keys and certificate management' },
];
readonly relatedDomainLinks: StellaQuickLink[] = [
{ label: 'Release Control', route: '/releases/runs', hint: 'Evidence attached to releases and promotions' },
{ label: 'Trust & Signing', route: '/setup/trust-signing', hint: 'Key management and signing policy' },
{ label: 'Policy Governance', route: '/ops/policy/governance', hint: 'Policy packs driving evidence requirements' },
{ label: 'Findings', route: '/security/findings', hint: 'Findings linked to evidence records' },
{ label: 'Release Control', route: '/releases/runs', description: 'Evidence attached to releases and promotions' },
{ label: 'Trust & Signing', route: '/setup/trust-signing', description: 'Key management and signing policy' },
{ label: 'Policy Governance', route: '/ops/policy/governance', description: 'Policy packs driving evidence requirements' },
{ label: 'Findings', route: '/security/findings', description: 'Findings linked to evidence records' },
];
readonly quickViews = computed((): EvidenceQuickViewTile[] => {

View File

@@ -25,6 +25,8 @@ export interface StellaQuickLink {
icon?: string;
/** Tooltip text */
hint?: string;
/** Short description shown below the label in aside layout */
description?: string;
}
@Component({
@@ -36,32 +38,59 @@ export interface StellaQuickLink {
@if (label) {
<span class="sql-label">{{ label }}</span>
}
<nav class="sql-nav" [attr.aria-label]="label || 'Quick links'">
@for (link of links; track link.label; let last = $last) {
<a
class="sql-link"
[routerLink]="link.route"
[title]="link.hint || link.label"
>
@if (link.icon) {
<svg class="sql-icon" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
@if (layout === 'aside') {
<nav class="sql-aside" [attr.aria-label]="label || 'Quick links'">
@for (link of links; track link.label) {
<a class="sql-aside-link" [routerLink]="link.route" [title]="link.hint || link.label">
@if (link.icon) {
<svg class="sql-aside-icon" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path [attr.d]="link.icon"/>
</svg>
}
<div class="sql-aside-content">
<span class="sql-aside-title">{{ link.label }}</span>
@if (link.description) {
<span class="sql-aside-desc">{{ link.description }}</span>
}
</div>
<svg class="sql-arrow" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2.5"
stroke-linecap="round" stroke-linejoin="round">
<path [attr.d]="link.icon"/>
<path d="M9 6l6 6-6 6"/>
</svg>
}
<span class="sql-text">{{ link.label }}</span>
<svg class="sql-arrow" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2.5"
stroke-linecap="round" stroke-linejoin="round">
<path d="M9 6l6 6-6 6"/>
</svg>
</a>
@if (!last) {
<span class="sql-sep" aria-hidden="true"></span>
</a>
}
}
</nav>
</nav>
} @else {
<nav class="sql-nav" [attr.aria-label]="label || 'Quick links'">
@for (link of links; track link.label; let last = $last) {
<a
class="sql-link"
[routerLink]="link.route"
[title]="link.hint || link.label"
>
@if (link.icon) {
<svg class="sql-icon" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path [attr.d]="link.icon"/>
</svg>
}
<span class="sql-text">{{ link.label }}</span>
<svg class="sql-arrow" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2.5"
stroke-linecap="round" stroke-linejoin="round">
<path d="M9 6l6 6-6 6"/>
</svg>
</a>
@if (!last) {
<span class="sql-sep" aria-hidden="true"></span>
}
}
</nav>
}
`,
styles: [`
:host {
@@ -152,6 +181,84 @@ export interface StellaQuickLink {
font-size: 0.875rem;
line-height: 1;
}
/* Aside layout — vertical list with descriptions */
:host([layout=aside]) {
border-top: none;
padding-top: 0;
margin-top: 0;
}
.sql-aside {
display: flex;
flex-direction: column;
gap: 0;
}
.sql-aside-link {
display: flex;
align-items: flex-start;
gap: 0.5rem;
padding: 0.625rem 0.75rem;
text-decoration: none;
color: var(--color-text-secondary);
border-left: 2px solid transparent;
transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
cursor: pointer;
}
.sql-aside-link:hover {
background: var(--color-surface-secondary);
border-left-color: var(--color-brand-primary);
color: var(--color-text-primary);
}
.sql-aside-link:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: -2px;
border-radius: 2px;
}
.sql-aside-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
margin-top: 1px;
opacity: 0.5;
}
.sql-aside-link:hover .sql-aside-icon {
opacity: 0.8;
}
.sql-aside-content {
flex: 1;
min-width: 0;
}
.sql-aside-title {
display: block;
font-size: 0.8125rem;
font-weight: 600;
line-height: 1.3;
}
.sql-aside-desc {
display: block;
font-size: 0.6875rem;
line-height: 1.4;
color: var(--color-text-muted);
margin-top: 0.125rem;
}
.sql-aside-link:hover .sql-aside-desc {
color: var(--color-text-secondary);
}
.sql-aside-link .sql-arrow {
margin-top: 2px;
flex-shrink: 0;
}
`],
})
export class StellaQuickLinksComponent {
@@ -160,4 +267,7 @@ export class StellaQuickLinksComponent {
/** Optional heading label (e.g. "Related", "Shortcuts", "Jump to"). */
@Input() label?: string;
/** Layout variant: 'inline' (horizontal dots) or 'aside' (vertical with descriptions). */
@Input() layout: 'inline' | 'aside' = 'inline';
}