feat(component): table of contents and anchor links on docs pages#187
feat(component): table of contents and anchor links on docs pages#187adrianschmidt wants to merge 4 commits into
Conversation
The heading outline of the generated docs skipped levels (h1 -> h3 -> h5), which violates axe's heading-order rule and makes screen-reader navigation harder, and the debug route rendered examples with no heading context at all, so no example markup heading level could be correct on both pages. - The "Examples" section heading is now an h2 instead of an h3, but is still sized like an h3 (the other section headings are unchanged, since they only follow h3 or lower headings). - Example titles are now h3 instead of h5, but are still sized like h5 via custom properties that the playground sets on the markdown component. - The debug route now renders the same heading context as the component page: the component name as a small h2 and the example title as a small h3. With this, example markup using h4 captions is correct on both the component page and the debug page. fix: jgroth#184
Introduce a shared kompendium-anchor component that renders a ¶ paragraph link next to a heading and highlights persistently when its slug matches the current URL anchor. Clicking an active anchor removes the fragment via history.replaceState so the scroll position is preserved. Add URL/slug helpers in anchors.ts (slugify, firstLine, exampleAnchorId, currentRoute, anchorHref, entrySlug, uniqueExampleSlugs, SECTION_SLUGS) shared by the table of contents and the heading anchors, with unit tests. Co-authored-by: Adrian Schmidt <adrian.schmidt@lime.tech> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
New floating action button that opens an overlay listing the table of contents. Entries can be collapsible and optionally default-expanded; user toggles are tracked explicitly so a default-expanded section stays open on load but can still be closed, and the active URL anchor auto-expands its ancestor sections. Includes focus management on open/close, Escape/scrim dismissal, modifier-click handling, and pruning of stale toggle state when the entries change. Tree helpers live in toc.tree.ts so they can be unit tested without importing the component module. Co-authored-by: Adrian Schmidt <adrian.schmidt@lime.tech> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Render kompendium-toc on each component docs page with entries for Examples and the Properties/Events/Methods/Slots/Styles sections, and add a ¶ anchor next to every section heading, per-entry heading, and example title. Example slugs are derived from the title text and de-duplicated, computed once and shared between the TOC and the rendered ids so they stay in sync. The playground renders the example title in-place as a heading (split out via split-docs.ts) so the anchor can sit beside it, and the secondary-hash change now scrolls from the hashchange handler since stencil-router does not re-render for fragment-only URL changes. Closes jgroth#165. Co-authored-by: Adrian Schmidt <adrian.schmidt@lime.tech> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds slug-based deep-linking to every documentation section via a new ChangesAnchor deep-linking and floating TOC
Sequence Diagram(s)sequenceDiagram
participant User
participant Toc as kompendium-toc
participant Anchor as kompendium-anchor
participant anchorUtils as anchors.ts
participant history as Browser history
User->>Toc: clicks floating action button
Toc->>Toc: open = true, focus first link
User->>Toc: clicks TOC entry link
Toc->>Toc: handleLinkClick → close()
User->>Anchor: clicks paragraph-link (already active)
Anchor->>anchorUtils: currentRoute()
anchorUtils-->>Anchor: route string
Anchor->>history: replaceState(route + `#slug`)
Anchor->>Anchor: updateActive()
history->>Toc: hashchange event
Toc->>anchorUtils: getAnchorId()
anchorUtils-->>Toc: active anchor id
Toc->>Toc: expandSectionForActiveAnchor() → update userToggles
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/component-title.ts`:
- Around line 6-10: The getComponentTitle function crashes when a tag without
hyphens is passed because accessing title[0] on an empty string throws an error.
Add a guard check after the title variable assignment in getComponentTitle to
handle the case where the title is empty (which occurs when split('-').slice(1)
yields no results), either by returning a sensible default value or the original
tag value. Additionally, locate and remove the duplicate definition of
getComponentTitle mentioned to exist in the codebase to maintain code
maintainability.
In `@src/components/component/component.tsx`:
- Around line 120-176: The renderDocs method calls getComponentTitle(tag) where
tag originates from router parameters, and this can throw if the tag is
malformed or invalid. Verify that the fix to getComponentTitle itself handles
all edge cases and returns a safe default title. If getComponentTitle cannot be
modified, add defensive validation in renderDocs before calling
getComponentTitle(tag) to check whether the tag is valid, and provide a fallback
title value if validation fails to prevent runtime crashes.
In `@src/components/debug/debug.tsx`:
- Around line 80-92: The renderHeadings method calls getComponentTitle with
ownerComponent.tag without first verifying that ownerComponent is defined, which
can cause a crash if the example-to-owner relationship is missing in the docs.
Add a guard clause to check that ownerComponent exists and is defined before
accessing its tag property and passing it to getComponentTitle. This ensures the
method safely handles cases where ownerComponent may be undefined from the
find() operation at line 57.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7da52e0d-81a2-40c5-9636-e79de46934e6
📒 Files selected for processing (29)
src/components/anchor/anchor.scsssrc/components/anchor/anchor.tsxsrc/components/component-title.tssrc/components/component/anchors.spec.tssrc/components/component/anchors.tssrc/components/component/component.scsssrc/components/component/component.tsxsrc/components/component/slots.tssrc/components/component/templates/events.tsxsrc/components/component/templates/examples.spec.tsxsrc/components/component/templates/examples.tsxsrc/components/component/templates/methods.tsxsrc/components/component/templates/props.tsxsrc/components/component/templates/slots.tsxsrc/components/component/templates/style.tsxsrc/components/debug/debug.scsssrc/components/debug/debug.spec.tsxsrc/components/debug/debug.tsxsrc/components/markdown/markdown.scsssrc/components/playground/playground.scsssrc/components/playground/playground.spec.tsxsrc/components/playground/playground.tsxsrc/components/playground/split-docs.tssrc/components/toc/toc.scsssrc/components/toc/toc.spec.tsxsrc/components/toc/toc.tree.tssrc/components/toc/toc.tsxsrc/components/toc/toc.types.tssrc/style/global-layout-rules.scss
| export function getComponentTitle(tag: string): string { | ||
| const title = tag.split('-').slice(1).join(' '); | ||
|
|
||
| return title[0].toLocaleUpperCase() + title.slice(1); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for all call sites of getComponentTitle to check tag sources
rg -nP '\bgetComponentTitle\s*\(' --type=ts --type=tsx -A 2Repository: jgroth/kompendium
Length of output: 89
🏁 Script executed:
#!/bin/bash
# Search for all call sites of getComponentTitle (correct file type syntax)
rg -nP 'getComponentTitle\s*\(' --type ts -A 2
# Also check the file itself
echo "=== File content ==="
cat -n src/components/component-title.tsRepository: jgroth/kompendium
Length of output: 1641
🏁 Script executed:
#!/bin/bash
# Search for where tags originate and if they're validated
rg -n "tag" src/components/component-title.ts -B 3 -A 3Repository: jgroth/kompendium
Length of output: 438
🏁 Script executed:
#!/bin/bash
# Find where component.tag is defined/populated
rg -n "tag\s*[:=]" --type ts -B 2 | head -80
# Check if tags are validated anywhere
rg -n "tag" src/kompendium/menu.ts | grep -E "(validate|check|require|hyphen)"Repository: jgroth/kompendium
Length of output: 3630
🏁 Script executed:
#!/bin/bash
# Check the interface/type definition for component
rg -n "interface.*[Cc]omponent|type.*[Cc]omponent" --type ts -A 5 | head -100Repository: jgroth/kompendium
Length of output: 3617
🏁 Script executed:
#!/bin/bash
# Find all tag values used in the codebase
rg -n "\.tag\s*[=:]|tag:\s*['\"]" --type ts -A 1 | head -50Repository: jgroth/kompendium
Length of output: 2599
Add guard against empty title strings to improve robustness.
The crash scenario is theoretically valid: if a tag without hyphens (e.g., "button") reaches this function, split('-').slice(1) yields an empty array, join() returns "", and accessing title[0] will throw a runtime error. However, all current component tags follow the hyphen-delimited pattern (e.g., kompendium-type, my-button) from Stencil decorators, which are developer-controlled. The proposed guard is still good defensive programming to prevent future crashes if tag sources change or this function is reused.
Also note: getComponentTitle is duplicated in src/kompendium/menu.ts:151; consider removing one definition.
Proposed fix
export function getComponentTitle(tag: string): string {
const title = tag.split('-').slice(1).join(' ');
+ if (!title) {
+ return tag;
+ }
return title[0].toLocaleUpperCase() + title.slice(1);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function getComponentTitle(tag: string): string { | |
| const title = tag.split('-').slice(1).join(' '); | |
| return title[0].toLocaleUpperCase() + title.slice(1); | |
| } | |
| export function getComponentTitle(tag: string): string { | |
| const title = tag.split('-').slice(1).join(' '); | |
| if (!title) { | |
| return tag; | |
| } | |
| return title[0].toLocaleUpperCase() + title.slice(1); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/component-title.ts` around lines 6 - 10, The getComponentTitle
function crashes when a tag without hyphens is passed because accessing title[0]
on an empty string throws an error. Add a guard check after the title variable
assignment in getComponentTitle to handle the case where the title is empty
(which occurs when split('-').slice(1) yields no results), either by returning a
sensible default value or the original tag value. Additionally, locate and
remove the duplicate definition of getComponentTitle mentioned to exist in the
codebase to maintain code maintainability.
| private renderDocs( | ||
| tag: string, | ||
| component: JsonDocsComponent, | ||
| examples: JsonDocsComponent[], | ||
| exampleSlugs: string[], | ||
| ) { | ||
| const title = getComponentTitle(tag); | ||
| const tags = component.docsTags | ||
| .filter(negate(isTag('slot'))) | ||
| .filter(negate(isTag('exampleComponent'))); | ||
| const schema = this.schemas.find((s) => s.$id === tag); | ||
|
|
||
| // Each section carries two ids by design: a legacy route-based id on | ||
| // the heading (e.g. `component/<tag>/examples/`, kept so existing | ||
| // sidebar links and `scrollToElement` keep working) and a short, | ||
| // URL-fragment-safe `slugId` on a sibling section-anchor span (the | ||
| // target for the in-page TOC and ¶ anchors). The route-based id cannot | ||
| // double as a hash fragment, hence both coexist. | ||
| return [ | ||
| <h1 id={this.getId()}>{title}</h1>, | ||
| <kompendium-markdown text={component.docs} />, | ||
| <kompendium-taglist tags={tags} />, | ||
| <ExampleList | ||
| examples={examples} | ||
| slugs={exampleSlugs} | ||
| id={this.getId('examples')} | ||
| slugId={SECTION_SLUGS.examples} | ||
| schema={schema} | ||
| propsFactory={this.examplePropsFactory} | ||
| />, | ||
| <PropertyList | ||
| props={component.props} | ||
| id={this.getId('properties')} | ||
| slugId={SECTION_SLUGS.properties} | ||
| />, | ||
| <EventList | ||
| events={component.events} | ||
| id={this.getId('events')} | ||
| slugId={SECTION_SLUGS.events} | ||
| />, | ||
| <EventList events={component.events} id={this.getId('events')} />, | ||
| <MethodList | ||
| methods={component.methods} | ||
| id={this.getId('methods')} | ||
| slugId={SECTION_SLUGS.methods} | ||
| />, | ||
| <SlotList | ||
| slots={component.slots} | ||
| id={this.getId('slots')} | ||
| slugId={SECTION_SLUGS.slots} | ||
| />, | ||
| <StyleList | ||
| styles={component.styles} | ||
| id={this.getId('styles')} | ||
| slugId={SECTION_SLUGS.styles} | ||
| />, | ||
| <SlotList slots={component.slots} id={this.getId('slots')} />, | ||
| <StyleList styles={component.styles} id={this.getId('styles')} />, | ||
| ]; | ||
| } |
There was a problem hiding this comment.
Verify that malformed tags cannot crash getComponentTitle.
Line 126 calls getComponentTitle(tag) where tag comes from this.match.params.name. If the router provides a malformed or single-segment tag, this will throw due to the empty-title bug in getComponentTitle (see review comment on src/components/component-title.ts).
Ensure the fix to getComponentTitle is applied, or add defensive handling here:
🛡️ Defensive fallback (if getComponentTitle cannot be fixed)
- const title = getComponentTitle(tag);
+ const title = tag.includes('-') ? getComponentTitle(tag) : tag;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/component/component.tsx` around lines 120 - 176, The
renderDocs method calls getComponentTitle(tag) where tag originates from router
parameters, and this can throw if the tag is malformed or invalid. Verify that
the fix to getComponentTitle itself handles all edge cases and returns a safe
default title. If getComponentTitle cannot be modified, add defensive validation
in renderDocs before calling getComponentTitle(tag) to check whether the tag is
valid, and provide a fallback title value if validation fails to prevent runtime
crashes.
| private renderHeadings( | ||
| component: JsonDocsComponent, | ||
| ownerComponent: JsonDocsComponent, | ||
| ) { | ||
| const exampleTitle = component.docs?.split('\n')[0]; | ||
|
|
||
| return [ | ||
| <h2 class="context-heading"> | ||
| {getComponentTitle(ownerComponent.tag)} | ||
| </h2>, | ||
| !!exampleTitle && <h3 class="context-heading">{exampleTitle}</h3>, | ||
| ]; | ||
| } |
There was a problem hiding this comment.
Guard against undefined ownerComponent and malformed tags.
Line 88 calls getComponentTitle(ownerComponent.tag) without verifying that ownerComponent (from find() at line 57) is defined. If the example-to-owner relationship is missing in the docs, this will throw. Additionally, getComponentTitle itself will crash on malformed tags (see review on src/components/component-title.ts).
🛡️ Proposed guard
private renderHeadings(
component: JsonDocsComponent,
ownerComponent: JsonDocsComponent,
) {
+ if (!ownerComponent) {
+ return null;
+ }
const exampleTitle = component.docs?.split('\n')[0];
return [
<h2 class="context-heading">
{getComponentTitle(ownerComponent.tag)}
</h2>,
!!exampleTitle && <h3 class="context-heading">{exampleTitle}</h3>,
];
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/debug/debug.tsx` around lines 80 - 92, The renderHeadings
method calls getComponentTitle with ownerComponent.tag without first verifying
that ownerComponent is defined, which can cause a crash if the example-to-owner
relationship is missing in the docs. Add a guard clause to check that
ownerComponent exists and is defined before accessing its tag property and
passing it to getComponentTitle. This ensures the method safely handles cases
where ownerComponent may be undefined from the find() operation at line 57.
Re-open of #179
Based on #185, so that needs merging first (or it will be included when this PR is merged — see first commit).
fix: #165
Summary by CodeRabbit
Release Notes
New Features
Style
Tests