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:
@@ -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>`.
|
||||
|
||||
@@ -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 -------------------------------------------------------
|
||||
|
||||
@@ -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[] => {
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user