一键导入
add-scrollspy-toc
Add a scrollspy Table of Contents sidebar to a page component with signal-based scroll tracking.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Add a scrollspy Table of Contents sidebar to a page component with signal-based scroll tracking.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | add-scrollspy-toc |
| description | Add a scrollspy Table of Contents sidebar to a page component with signal-based scroll tracking. |
| allowed-tools | ["Read","Glob","Grep","Edit","Write"] |
| argument-hint | <ComponentName> |
When the user invokes /add-scrollspy-toc <ComponentName>:
This skill adds a scrollspy Table of Contents (TOC) sidebar to a page component. The TOC highlights the current section as the user scrolls and allows click-to-scroll navigation.
All scrollspy implementations should use the inline signal-based approach for consistency:
activeSectionId, activeParentSectionId)fromEvent(window, "scroll") with debounceTime(10) for scroll trackinggetBoundingClientRect().top to determine active section[class.active]="activeSectionId() === section.id"DO NOT use the directive-based approach (ScrollSpySectionDirective, ScrollSpyLinkDirective) for new implementations.
import { Component, OnInit, OnDestroy, signal } from "@angular/core";
import { fromEvent, Subscription, debounceTime } from "rxjs";
For flat sections (no hierarchy):
private scrollSubscription: Subscription | null = null;
// Define your sections
sections = [
{ id: "section-1", title: "First Section" },
{ id: "section-2", title: "Second Section" },
{ id: "section-3", title: "Third Section" },
];
activeSectionId = signal<string | null>(null);
For hierarchical sections (parent/child):
private scrollSubscription: Subscription | null = null;
private sectionIds: string[] = [];
private sectionToParentMap = new Map<string, string>();
activeSectionId = signal<string | null>(null);
activeParentSectionId = signal<string | null>(null);
ngOnInit(): void {
// For static sections:
this.setupScrollSpy();
// For dynamic sections (from API), call after data loads:
// this.buildSectionMap(data);
// setTimeout(() => this.setupScrollSpy(), 150);
}
ngOnDestroy(): void {
this.scrollSubscription?.unsubscribe();
}
For flat sections:
private setupScrollSpy(): void {
this.scrollSubscription?.unsubscribe();
this.scrollSubscription = fromEvent(window, "scroll")
.pipe(debounceTime(10))
.subscribe(() => this.updateActiveSection());
this.updateActiveSection();
}
private updateActiveSection(): void {
const offset = 120; // Adjust based on header height
let activeId: string | null = null;
for (const section of this.sections) {
const el = document.getElementById(section.id);
if (el) {
const rect = el.getBoundingClientRect();
if (rect.top <= offset) {
activeId = section.id;
}
}
}
if (activeId !== null && activeId !== this.activeSectionId()) {
this.activeSectionId.set(activeId);
}
}
scrollToSection(sectionId: string): void {
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "start" });
this.activeSectionId.set(sectionId);
}
}
For hierarchical sections:
private buildSectionMap(sections: YourSectionType[]): void {
this.sectionToParentMap.clear();
this.sectionIds = [];
sections.forEach((parent) => {
const parentId = `section-${parent.ID}`;
this.sectionIds.push(parentId);
this.sectionToParentMap.set(parentId, parentId);
if (parent.ChildSections) {
parent.ChildSections.forEach((child) => {
const childId = `section-${child.ID}`;
this.sectionIds.push(childId);
this.sectionToParentMap.set(childId, parentId);
});
}
});
}
private setupScrollSpy(): void {
this.scrollSubscription?.unsubscribe();
this.scrollSubscription = fromEvent(window, "scroll")
.pipe(debounceTime(10))
.subscribe(() => this.updateActiveSection());
this.updateActiveSection();
}
private updateActiveSection(): void {
const offset = 120;
let activeId: string | null = null;
for (const id of this.sectionIds) {
const el = document.getElementById(id);
if (el) {
const rect = el.getBoundingClientRect();
if (rect.top <= offset) {
activeId = id;
}
}
}
if (activeId !== null && activeId !== this.activeSectionId()) {
this.activeSectionId.set(activeId);
this.activeParentSectionId.set(this.sectionToParentMap.get(activeId) ?? activeId);
}
}
scrollToSection(sectionId: string, parentId?: string): void {
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "start" });
this.activeSectionId.set(sectionId);
this.activeParentSectionId.set(parentId ?? sectionId);
}
}
isSectionExpanded(section: YourSectionType): boolean {
return this.activeParentSectionId() === `section-${section.ID}`;
}
Layout structure:
<div class="page-container">
<div class="dashboard-layout">
<!-- TOC Sidebar -->
<div class="toc-sidebar">
<div class="toc-title">Contents</div>
<nav class="toc-nav">
@for (section of sections; track section.id) {
<div class="toc-item">
<a href="javascript:void(0)"
[class.active]="activeSectionId() === section.id"
(click)="scrollToSection(section.id)">
{{ section.title }}
</a>
</div>
}
</nav>
</div>
<!-- Main Content -->
<div class="content-area">
@for (section of sections; track section.id) {
<section [id]="section.id" class="content-section">
<h2>{{ section.title }}</h2>
<!-- Section content -->
</section>
}
</div>
</div>
</div>
@use "/src/scss/abstracts" as *;
.dashboard-layout {
display: grid;
grid-template-columns: 250px 1fr;
gap: 1.5rem;
align-items: start;
@media (max-width: 992px) {
grid-template-columns: 1fr;
}
}
.toc-sidebar {
position: sticky;
top: 1rem;
@media (max-width: 992px) {
position: static;
}
.toc-title {
font-weight: bold;
margin-bottom: 0.5rem;
padding-left: 1rem;
}
.toc-nav {
.toc-item {
margin-bottom: 0.25rem;
> a {
display: block;
padding: 0.25rem 0.5rem 0.25rem 1rem;
color: var(--bs-gray-700);
text-decoration: none;
border-left: 2px solid transparent;
transition: all 0.2s;
&:hover {
color: var(--btn-primary-bg-color, var(--bs-primary));
border-left-color: var(--btn-primary-bg-color, var(--bs-primary));
}
&.active {
color: var(--btn-primary-bg-color, var(--bs-primary));
font-weight: bold;
border-left-color: var(--btn-primary-bg-color, var(--bs-primary));
}
}
.toc-children {
margin-left: 0.5rem;
.toc-child {
display: block;
padding: 0.25rem 0.5rem 0.25rem 1rem;
font-size: 0.875rem;
color: var(--bs-gray-600);
text-decoration: none;
border-left: 2px solid transparent;
transition: all 0.2s;
&:hover,
&.active {
color: var(--btn-primary-bg-color, var(--bs-primary));
border-left-color: var(--btn-primary-bg-color, var(--bs-primary));
}
&.active {
font-weight: bold;
}
}
}
}
}
}
.content-area {
.content-section {
margin-bottom: 1.5rem;
scroll-margin-top: 1rem;
h2 {
margin: 0 0 0.5rem;
font-size: 1.5rem;
}
}
}
OnInit and OnDestroyactiveSectionId (and activeParentSectionId if hierarchical)fromEvent(window, "scroll") with debounceTime(10)ngOnDestroyscroll-margin-top set on content sectionsposition: sticky with appropriate top valueCreate or flesh out a Jira story with structured sections for POs, Devs, and Claude. Accepts an optional Jira key to update an existing card.
Export Water Quality Management Plans for one jurisdiction from the local Neptune database to an Esri File Geodatabase in native projection. Use when a client requests WQMP spatial data for a specific city/jurisdiction.
Extract rework items from a Jira card's Acceptance Criteria and produce a fix plan. Auto-detects issue key from branch or accepts one as argument.
Fetch a Jira card by key (e.g. NPT-100) and produce a detailed implementation plan. Use this when starting work on a new story.
Build the SQL Server DACPAC and scaffold EF Core entities. Run this after finishing all SQL schema edits in Neptune.Database/.
Audit migration progress from legacy MVC to Angular SPA. Reports which entities are migrated, partial, or not started.