원클릭으로
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 페이지를 검토하고 설치를 진행할 수 있습니다.
Create 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.
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 value