Skip to content

feat(component): table of contents and anchor links on docs pages#6

Merged
adrianschmidt merged 3 commits into
limefrom
feat/165-table-of-contents-component
Jun 18, 2026
Merged

feat(component): table of contents and anchor links on docs pages#6
adrianschmidt merged 3 commits into
limefrom
feat/165-table-of-contents-component

Conversation

@TommyLindh2

@TommyLindh2 TommyLindh2 commented Jun 17, 2026

Copy link
Copy Markdown

Summary

  • Adds a floating table of contents on each component docs page (top-right FAB that opens an overlay), covering Examples and all subsections (Properties / Events / Methods / Slots / Styles) with collapsible groups.
  • Adds a ¶ anchor link next to every heading — section headings, the example titles inside each playground, and every per-entry heading under Properties/Events/Methods/Slots/Styles. The anchor becomes visible on heading hover and highlights blue when the current URL matches its slug.
  • Clicking an active anchor removes the fragment from the URL (via history.replaceState) without scrolling.
  • Fork-side PR addressing Table of Contents on component pages jgroth/kompendium#165

Details

  • New kompendium-anchor component + URL helpers in src/components/component/anchors.ts.
  • New kompendium-toc component with explicit user-toggle state, defaultExpanded support, and auto-expand when the URL matches a child.
  • Playground now renders the example title (first docs line) in-place as an h5 so the anchor can sit next to the text; the rest of the docs still flow through kompendium-markdown.
  • Scroll fix in kompendium-component: the secondary-hash change now scrolls directly from the hashchange handler, since stencil-router's match doesn't re-render for fragment-only URL changes.
  • Example slugs are derived from the title text (e.g. "With icons" → #with-icons) so the URL matches what the user sees.

Test plan

  • Visit a component page — confirm the TOC FAB appears top-right.
  • Open the TOC — Examples should be expanded by default; Properties/Events/Methods/Slots/Styles should be collapsed and expandable via the chevron.
  • Click a TOC entry — URL gains #slug, page scrolls to that heading, anchor icon turns blue.
  • Hover any heading — ¶ icon fades in; hover away — fades out.
  • Click the blue (active) ¶ icon — URL fragment clears, icon goes back to hover-only, page does not scroll.
  • Open a URL like #/component/<tag>#basic-example directly — page scrolls to that example and its anchor highlights on load.
  • Old-style URLs like #/component/<tag>/examples/ still scroll to the Examples section (backward compatibility).

Demo video of how it works in Lime elements

On desktop size

Kompendium.-.Lime.elements.demo-20260422_143935-Meeting.Recording.mp4

On mobile size

Kompendium.-.Lime.elements.Mobile.view.-20260427_111702-Meeting.Recording.mp4

Summary by CodeRabbit

  • New Features

    • Anchor links added across component docs (Examples, Properties, Events, Methods, Slots, Styles) with stable per-item slugs for direct linking.
    • Floating, collapsible table-of-contents overlay with keyboard support, focus management, and auto-expansion to reveal the active section.
    • Playgrounds and headings now parse/display titles and support optional per-example anchors.
  • Style

    • Hover/focus styles reveal inline anchor controls for easier discovery.

Fork copy of the upstream PR jgroth#179, branched off main and targeting lime per FORK.md. Merging here releases @limetech/kompendium without waiting on upstream review.

@adrianschmidt

Copy link
Copy Markdown

Consolidated PR Review

🤖 Automated 7-agent review (backward-compat, code quality, architecture, security, observability, performance, Lime-platform). Posted by Adrian's review tooling.

PR Summary

Adds a floating table-of-contents FAB and ¶ heading anchor links to every component docs page. Introduces two new Stencil components (kompendium-anchor, kompendium-toc), URL/slug helpers in anchors.ts, and reworks the playground to render the example title in-place as a heading. 19 files, +1110 / −43. Closes jgroth#165.

Merge Readiness — MERGE WITH CAVEATS ⚠️

This PR is strictly better than the lime base branch: it ships a well-built feature, tightens the title escaping boundary (off the pre-existing raw-HTML markdown path onto auto-escaped JSX), and adds a filter(Boolean) guard that converts a previous render-crash on dangling @exampleComponent tags into graceful omission. No [High] findings; no regressions in any dimension.

  • Blockers: None. No dimension found a new bug, vulnerability, performance cliff, or backward-incompatible break.
  • Non-blocking but worth addressing: A handful of [Medium] follow-ups — missing tests for the riskiest new logic, slugId required-vs-optional inconsistency across the six *List templates, the dual id scheme, the duplicated slug computation, and the per-heading hashchange listener fan-out. See Top Recommendations.

1. Backward Compatibility — GOOD ✅

The PR is additive and preserves existing contracts.

  • [Low] Example titles now render as escaped plain text (<h3>{heading}</h3>) instead of flowing through kompendium-markdown (old '### ' + docs). Inline markdown in a title line (backticks, *emphasis*) will now show literally. Real-world impact tiny — titles are conventionally plain text — and the body still renders as markdown. playground.tsx:107-122.
  • [Low] Old-style section URLs verified still working: the sidebar menu still emits /component/<tag>/properties/ etc. and headings retain their legacy route-based id; scrollToElement resolves them. examples.spec.tsx:34 tests the legacy id.
  • Positives: anchor-scroll exports unchanged; the two externally-consumed *List helpers (PropertyList/MethodList, also used by the Type page) made slugId optional with guards; Playground.anchorSlug is additive; new components are purely additive — no public prop/event removed or renamed.

2. Code Quality — ACCEPTABLE ⚠️

Clean small modules with good JSDoc, but some inconsistency and a real test gap.

  • [Medium] slugId/id typed required vs optional inconsistently across the six *List templates, with matching guard inconsistency. props.tsx/methods.tsx declare slugId? and guard every use; events.tsx/slots.tsx/style.tsx/examples.tsx declare it required and render unconditionally — yet component.tsx:133-164 always passes a concrete value. events.tsx is even internally inconsistent (unguarded section span, guarded per-entry slug). With strictNullChecks off, nothing enforces either shape. Pick one contract (required, since the only caller always supplies it).
  • [Medium] No test coverage for the core new logic. Only examples.spec.tsx and playground.spec.tsx were touched. The tricky, pure, trivially-testable units are untested: slugify, uniqueExampleSlugs dedup, currentRoute/anchorHref hash parsing, firstLine, and toc.tsx's collectIds/findEntryById/findAncestorsOf/prune logic. These are the parts most likely to regress silently.
  • [Low] splitDocs (playground) and firstLine (anchors) are two near-duplicate first-line extractors that can drift; the TOC label and in-page heading derive the example title via different paths.
  • [Low] The section-anchor <span> + heading + <kompendium-anchor> triple is copy-pasted across all six templates; a shared AnchoredHeading helper would collapse it and prevent the guarded/unguarded drift already visible.
  • [Low] Three inline SVG render helpers in toc.tsx:1508-1551 repeat the same boilerplate; a single icon(d, size) helper would remove ~30 lines.
  • [Low] Active CodeRabbit thread (component.tsx ~213, "fallback title includes section prefix") does not reproduce in the shipped code — example child ids come from uniqueExampleSlugs(), which returns a bare slug (no examples- prefix), so the empty-title fallback prettifies a tag-derived slug. Safe to resolve; at most derive the fallback from example.tag directly to also drop the dedup-suffix number. (Confirmed independently by two agents.)
  • Positives: Pure, dependency-injection-friendly helpers; thoughtful uniqueExampleSlugs dedup, slotDisplayName default-slot normalization, the filter(Boolean) example narrowing, and onEntriesChange stale-toggle pruning; consistent listener add/remove pairing.

3. Architecture — GOOD ✅

Sound component boundaries; one accidental-complexity smell.

  • [Medium] Dual id scheme on every section is accidental complexity, not a clean separation. Each section emits both a legacy route-based heading id (component/<tag>/examples/) and a sibling zero-size <span class="section-anchor" id={slugId}> carrying the new slug, because the heading id is route-shaped and can't be a URL fragment slug. getScrollTargetId() now prefers the slug, leaving the route-based ids largely vestigial but still rendered everywhere. Worth a maintainer decision: migrate fully to slug ids and drop the route-based ones, or document why both must coexist. component.tsx:135, examples.tsx:25. (Code Quality flagged the same at [Low]; keeping the architectural framing canonical here.)
  • [Low] Three modules now independently parse the URL hash (anchor-scroll.ts, anchors.ts, inline in component.tsx); currentRoute() and getRoute().split('#')[0] are redundant implementations, and kompendium-anchor imports hash helpers from both modules. Consolidating into one location abstraction would reduce coupling. Pre-existing in spirit.
  • Positives: kompendium-toc is a generic, data-agnostic recursive TocEntry[] renderer while kompendium-component owns the domain mapping via buildTocEntries — the right boundary; shared SECTION_SLUGS keeps ids in sync; the TOC expand-state model (explicit-override-over-default, immutable Map updates, race-free prune/auto-expand ordering) is coherent and correct.

4. Security — GOOD ✅

No new attack surface; the PR slightly improves the escaping posture.

  • [Low] The example-title trust boundary moves toward more escaping: the old '### ' + docs path fed the first line through kompendium-markdown, which renders via rehype-raw + allowDangerousHtml into innerHTML (an unsanitized raw-HTML path that exists on the base branch). The new code renders the title as auto-escaped JSX text. The body still uses the unchanged pre-existing markdown path. No new injection introduced.
  • [Low] All id/href/aria-label values derive from slugify output ([a-z0-9-] only) or escaped JSX; anchorHref always returns a relative #fragment (never a scheme), and history.replaceState stays same-origin via a URL cloned from location.href. No javascript: URL, attribute breakout, open-redirect, or DOM-clobbering risk. Data is build-time, author-controlled JSON docs.
  • Positives: Consistent escaped JSX interpolation, relative-fragment-only hrefs, DOM-safe slugs; the only raw-HTML rendering is unchanged base-branch behavior.

5. Observability — GOOD ✅

Appropriate for a client-side docs renderer with essentially no logging infrastructure.

  • [Low] The new findExamples(...).filter(Boolean) (component.tsx:101-103) converts a broken-docs condition (a dangling @exampleComponent tag) from a render crash into a silent omission. Minor diagnosability reduction for an author-error condition; defensible as intentional robustness. The unknown-component-tag path still fails loudly as before.
  • Positives: No new silent swallowing of any failure the system depends on detecting; no existing console warning/error removed.

6. Performance — ACCEPTABLE ⚠️

No leaks or cliffs; two avoidable inefficiencies.

  • [Medium] One window hashchange listener per kompendium-anchor (anchor.tsx:78-85) — one anchor per section heading plus one per prop/event/method/slot/style entry plus one per example. A large component yields 60-100+ global listeners, each firing updateActive() on every hashchange. Not a leak (add/remove pair correctly on a stably-bound reference) and the per-event work is a cheap string compare, but it multiplies the codebase's existing one-listener-per-page-component pattern by member count. A single shared listener (or CSS :target/IntersectionObserver) would be O(1). (Architecture flagged the same at [Low]; the member-count multiplier justifies [Medium] as a worth-fixing inefficiency.)
  • [Medium] uniqueExampleSlugs(examples) is computed twice per render — once in ExampleList (examples.tsx:23) and once in buildTocEntries (component.tsx:393). Same result, same render pass. Compute once and pass down.
  • [Low] buildTocEntries + findExamples + per-entry slugify run on every render() though the source docs are static for the page; could be memoized on docs/match change.
  • [Low] Double scroll trigger on route change (componentDidUpdate + immediate scrollToAnchor) — idempotent and harmless, but redundant.
  • Positives: No listener leaks; userToggles growth is bounded and actively pruned; recursion is O(bounded API size) with small constants.

7. Lime Platform Issues — SAFE ✅

The lime-issue-detector catalog was fetched fresh and applied in full.

  • No applicable findings. The backend-runtime rule families (LPID-LO/EP/EH/TK/DB/CC/SEC) are structurally inapplicable — no Python, UoW, hooks, events, tasks, or lime_file I/O exist. The only relevant family, Frontend (LPID-FE-1…11), was checked against both new components and the templates; the PR is clean and notably gets the Stencil hazards right: listeners cleaned up in disconnectedCallback (FE-8), @State Maps rebuilt immutably (FE-6), no @State writes in render/post-render (FE-7), no innerHTML/eval/raw HTML sinks (FE-10).
  • Positives: Correct Stencil lifecycle and state-management discipline throughout the new components.

Overall Verdicts

Dimension Verdict
Backward Compatibility GOOD
Code Quality ACCEPTABLE
Architecture GOOD
Security GOOD
Observability GOOD
Performance ACCEPTABLE
Lime Platform Issues SAFE
Merge Readiness MERGE WITH CAVEATS

Top Recommendations

  1. [Medium from Code Quality] Add unit tests for the riskiest new pure logic: slugify, uniqueExampleSlugs (dedup suffixing), currentRoute/anchorHref hash parsing, firstLine, and toc.tsx's collectIds/findAncestorsOf/prune-and-expand. These are silent-regression-prone and trivially testable.
  2. [Medium from Code Quality] Make slugId consistently required across all six *List templates (the only caller always supplies it) and drop the now-dead slugId ? … : null guards — or, if the Type-page reuse of PropertyList/MethodList genuinely needs it optional, apply the same guarded pattern to all six. Either way, pick one contract.
  3. [Medium from Architecture] Decide on the dual id scheme: migrate fully to slug ids and drop the vestigial route-based heading ids (or the section-anchor spans), or add a comment explaining why both must coexist.
  4. [Medium from Performance] Compute uniqueExampleSlugs(examples) once in render() and pass the result to both ExampleList and buildTocEntries instead of recomputing in each.
  5. [Medium from Performance] Consider a single shared hashchange listener (on kompendium-component, broadcasting to anchors) or a CSS :target/IntersectionObserver approach instead of one global listener per kompendium-anchor, to avoid 60-100+ listeners on large component pages. Not a leak — purely a scaling cleanup.
  6. [Low from Code Quality] Resolve the remaining active CodeRabbit thread (component.tsx ~213): it does not reproduce in the shipped code, but deriving the empty-title fallback from example.tag directly would also drop the dedup-suffix number and let the thread close cleanly.
  7. [Low from Code Quality] Optional cleanups: share a first-line extractor between splitDocs and firstLine; extract a shared AnchoredHeading helper for the six repeated section-heading blocks; collapse the three SVG render helpers into one icon() helper.

@adrianschmidt

Copy link
Copy Markdown

Addressed review feedback

Thanks for the thorough review. I pushed 6 fixup commits plus one standalone test commit. Summary of what changed and what I consciously left alone.

Fixed

  • Tests for the new pure logic (test: commit). New anchors.spec.ts and toc.spec.tsx cover the silent-regression-prone units: slugify, firstLine, exampleAnchorId, entrySlug, uniqueExampleSlugs dedup, currentRoute/anchorHref hash parsing, and the TOC tree helpers (collectIds/findEntryById/findAncestorsOf) plus the prune-stale-toggles and auto-expand-active-section behavior. (To test the TOC helpers I exported them and made the auto-expand test drive a hashchange event, which is the same code path the real app uses.)

  • slugId contract made consistent (fixup → "skip section anchor when slugId is missing"). events/slots/style/examples now declare slugId? and guard every use, matching props/methods. I chose optional-with-guards rather than required because the Type page reuses PropertyList/MethodList without a slugId, so a required contract would be wrong for those two — one consistent shape across all six.

  • uniqueExampleSlugs computed once (fixup → "add table of contents…"). It's now computed a single time in render() and passed down to both ExampleList and buildTocEntries instead of being recomputed in each.

  • Empty-title example fallback (same fixup). The fallback now derives from example.tag directly, so it no longer carries the dedup-suffix number. This also resolves the open CodeRabbit thread (which did not actually reproduce, but this lets it close cleanly).

  • Dual id scheme documented (same fixup). Added a comment in component.tsx explaining why each section carries both a legacy route-based heading id and a short URL-fragment-safe slug id — they serve different purposes and the route id can't double as a hash fragment. Kept both rather than migrating, to avoid breaking existing sidebar links and scrollToElement in this PR.

  • Shared first-line extractor (fixup → "consolidate first-line extraction…"). splitDocs in the playground now reuses firstLine for its title, removing the near-duplicate.

  • Collapsed the three TOC SVG helpers into one icon(d, size) helper (fixup → "add floating kompendium-toc…"), removing the repeated boilerplate.

Consciously not changed

  • Per-kompendium-anchor hashchange fan-out. Left as-is. It's not a leak (add/remove pair correctly) and the per-event work is a cheap string compare; a shared listener / IntersectionObserver is the most invasive change in the set and was flagged non-blocking. Better as a follow-up than churn in this PR.

  • Shared AnchoredHeading helper. Declined. After making the slugId guarding consistent the drift is gone, and the six blocks differ enough (heading level, label, and the examples block renders playgrounds) that a shared helper adds indirection for modest gain.

  • Memoizing buildTocEntries on docs/match change and the redundant double scroll-trigger. Left alone — both are [Low], idempotent/harmless, and not worth the added state for a build-time-static docs page.

Lint and tests both pass (npm run lint, npm run test — 130 passing).

@adrianschmidt

Copy link
Copy Markdown

Consolidated PR Review — Re-review (iteration 2)

🤖 Automated 7-agent re-review after the fixups. Posted by Adrian's review tooling. Head b780744.

What this re-review covered

Verified the 7 commits pushed in response to the first review — that each fix is correct and that none introduced a regression — and re-assessed every prior finding at consistent (absolute) severity.

Merge Readiness — MERGE WITH CAVEATS ⚠️ (essentially clean)

The fixups land cleanly. All four actionable [Medium] findings from iteration 1 are resolved or consciously deferred, the new tests are meaningful, and no fixup introduced a new defect. The PR remains strictly better than lime.

  • Blockers: None. No regression in any dimension.
  • Only remaining [Medium]: the per-kompendium-anchor hashchange listener fan-out — the author explicitly deferred it as a non-blocking follow-up (not a leak; listeners pair correctly). Severity is held at [Medium] per re-review discipline, but it does not gate merge. Everything else is [Low].

Resolution of iteration-1 findings

Iteration-1 finding Severity Status
slugId required-vs-optional inconsistency across six templates [Medium] ✅ Fixed — all six now slugId? + guarded; verified consistent and the Type-page reuse still compiles
No tests for core new logic [Medium] ✅ Fixed — anchors.spec.ts + toc.spec.tsx with meaningful assertions (slugify/dedup/hash-parsing/tree helpers/prune/auto-expand)
uniqueExampleSlugs computed twice per render [Medium] ✅ Fixed — computed once in render(), threaded as slugs/exampleSlugs; verified no second call site
Dual id scheme [Medium] ✅ Documented — explanatory comment added; reasoning verified accurate & sound → drops to [Low] informational
Active CodeRabbit "fallback title prefix" [Low] ✅ Fixed — fallback now derives from example.tag
splitDocs/firstLine duplication [Low] ✅ Fixed — splitDocs reuses firstLine (behavior-preserving)
Three duplicated SVG helpers [Low] ✅ Fixed — collapsed into one icon(d, size)
Per-anchor hashchange listener fan-out [Medium] ⏸️ Consciously deferred (non-blocking, not a leak)
Section-anchor block copy-pasted across six templates [Low] ⏸️ Declined (drift risk gone after guard consistency)
buildTocEntries/findExamples run every render [Low] Carried (now strictly less work than iter 1)
Example titles render as escaped text not markdown [Low] Carried (by design)

Dimension verdicts (iteration 2)

Dimension Verdict Note
Backward Compatibility GOOD Every ExampleList caller passes the new required slugs; required→optional loosening breaks no caller; anchor-scroll surface untouched
Code Quality GOOD Fixups verified correct; logic hoisted into tested pure helpers. Residual [Low]: section-block duplication (declined), curried-renderer sectionSlug: string type is misleading under strictNullChecks off, splitDocs not directly unit-tested
Architecture GOOD exampleSlugs single-source-of-truth is a genuine data-flow improvement; dual-id decision documented accurately. Residual [Low]: three modules still parse the hash differently
Security GOOD No new sink; icon() args are static literals; escaping posture unchanged (still tighter than pre-PR)
Observability GOOD No new silent-failure path; optional-slugId guards can't hide a real misconfiguration (caller always passes constants)
Performance ACCEPTABLE ⚠️ Double-compute resolved; no new cost. Remaining: deferred listener fan-out [Medium], per-render TOC build [Low], double scroll trigger [Low]
Lime Platform Issues SAFE FE-6/7/8/10 re-verified clean on the refactored toc.tsx; exports are visibility-only
Merge Readiness MERGE WITH CAVEATS ⚠️ No blockers; one deferred non-blocking [Medium]

Remaining recommendations (all non-blocking)

  1. [Medium from Performance] (deferred) Replace the per-kompendium-anchor hashchange listener with a single shared listener on kompendium-component (or :target/IntersectionObserver) to avoid 60-100+ listeners on large component pages. Tracked as a follow-up.
  2. [Low from Code Quality] Optionally type the curried render* factories' sectionSlug as string | undefined to match the slugId? reality (cosmetic under strictNullChecks: off).
  3. [Low from Code Quality] Optionally add a direct splitDocs unit test for the empty-docs and title-only branches.
  4. [Low from Architecture] Optionally consolidate the three hash-parsing implementations (anchor-scroll.ts / anchors.ts / inline split('#')[0]) behind one location helper.

Nice work on the turnaround — the fixups are precise, the fixup-commit targeting is correct, and the new tests cover exactly the silent-regression-prone units that were flagged.

@adrianschmidt

Copy link
Copy Markdown

Iteration-2 polish

Thanks for the re-review. I took two of the four optional items and consciously declined the other two. Three more fixup commits pushed.

Fixed

  • Typed the curried render* factories' sectionSlug as string | undefined (fixup → "skip section anchor when slugId is missing"). This matches the slugId? reality across all five props/methods/events/slots/style factories. Cosmetic under strictNullChecks: off, but it documents the optionality at the type level. (The diff looks large only because prettier reindented the factory bodies when the arrow wrapped to two lines — no logic changed.)

  • Direct splitDocs unit tests (export fixup → the playground heading refactor; tests fixup → the "cover pure logic" test commit). splitDocs is now exported and covered for the empty-docs, blank-only, null/undefined, title-only, leading-blank-lines, and multi-line-body branches — the edge branches the component-level tests didn't exercise.

Consciously not changed

  • Per-kompendium-anchor hashchange listener fan-out ([Medium], still deferred). Unchanged. It's the most invasive item, it's not a leak (listeners pair correctly), and it doesn't gate merge. A shared listener / IntersectionObserver is the right shape but belongs in a focused follow-up rather than as churn on an approved PR.

  • Consolidating the three hash-parsing implementations ([Low]). Declined for this PR. getRoute/getAnchorId (anchor-scroll.ts), currentRoute (anchors.ts), and the inline split('#')[0] (component.tsx) are genuinely different operations, and two more sites (guide.tsx, navigation.tsx) parse the hash their own way too. A real consolidation is a small cross-cutting refactor touching well-tested scroll logic — more value as its own change than bundled here, where the risk/reward against an already-clean PR isn't worth it.

Lint and tests both pass (npm run lint; npm run test — 136 passing).

@adrianschmidt

Copy link
Copy Markdown

Consolidated PR Review — Verification pass (iteration 3)

🤖 Automated review, third pass. Posted by Adrian's review tooling. Head de3a31a.

Scope of this pass

The only change since the previous review (head b780744de3a31a, 3 fixup commits) is:

  1. Type widening — the five curried render* factories now declare sectionSlug: string | undefined instead of string (props/methods/events/slots/style). Resolves the iteration-2 [Low] "misleading type". The large line counts are prettier reindentation only — no logic change.
  2. export splitDocs (playground.tsx) + 6 new splitDocs unit tests (empty/blank/null/title-only/leading-blank/multi-line). Resolves the iteration-2 [Low] coverage gap.

This delta is runtime-inert: a TypeScript type annotation is type-erased on emit, exporting a function adds no runtime path, and the rest is test-only. The five runtime-oriented dimensions (Architecture, Security, Observability, Performance, Lime Platform) therefore carry their iteration-2 verdicts unchanged by construction — there is no new runtime surface for them to assess. The two dimensions where a type/test change could matter were re-reviewed in full:

  • Backward Compatibility — GOOD ✅. The type widening is type-erased (no emitted-JS difference) and makes the signature more correct (it now matches the slugId?: string call sites); export splitDocs is purely additive. No regression. (examples.tsx's factory was correctly left alone — it takes slugs: string[], not a sectionSlug.)
  • Code Quality — GOOD ✅. Both targeted iteration-2 [Low] items are fully and correctly resolved. The widening is applied consistently across exactly the right five factories and matches the sectionSlug ? … : null guards. All 6 new splitDocs assertions were traced against the implementation and are correct, with complete branch coverage (no-content / leading-blank-skip / multi-line-body). No new smell.

Resolution of iteration-2 items

Iteration-2 finding Severity Status
Misleading sectionSlug: string type vs slugId? reality [Low] ✅ Fixed — widened to string | undefined across all five factories
splitDocs lacked a direct unit test [Low] ✅ Fixed — exported + 6 branch-covering tests
Per-anchor hashchange listener fan-out [Medium] ⏸️ Deferred (non-blocking follow-up; not a leak)
Hash-parsing consolidation [Low] ⏸️ Declined (cross-cutting refactor, not worth the risk on an approved PR)
buildTocEntries section-block duplication [Low] ⏸️ Declined (readability trade-off)

Merge Readiness — MERGE WITH CAVEATS ⚠️ (clean)

No blockers, no regressions across all three passes. The PR is strictly better than lime. The only non-trivial open item is the consciously-deferred listener fan-out ([Medium], non-blocking); the remaining declined items are [Low] readability/refactor preferences. Tests: 136 passing, 0 failing.

Dimension Verdict
Backward Compatibility GOOD
Code Quality GOOD
Architecture GOOD ✅ (unchanged — no runtime delta)
Security GOOD ✅ (unchanged — no runtime delta)
Observability GOOD ✅ (unchanged — no runtime delta)
Performance ACCEPTABLE ⚠️ (unchanged — deferred listener fan-out)
Lime Platform Issues SAFE ✅ (unchanged — no runtime delta)
Merge Readiness MERGE WITH CAVEATS ⚠️

Nothing further is required to merge. The remaining recommendations are optional polish and a tracked performance follow-up.

@adrianschmidt

Copy link
Copy Markdown

Fixed the CI build failure

The CI and Build lime-elements docs jobs were failing because splitDocs (in playground.tsx) and collectIds/findEntryById/findAncestorsOf (in toc.tsx) had been exported to make them unit-testable — but Stencil forbids a @Component() module from exporting anything other than the component class itself (module-bundling docs). This is a build-time (stencil build --docs) rule that npm run test/lint don't enforce, which is why it slipped through.

Fix (fixup → b780744)

  • Moved the three TOC tree helpers to a new src/components/toc/toc.tree.ts; toc.tsx imports them.
  • Moved splitDocs to a new src/components/playground/split-docs.ts; playground.tsx imports it.
  • Updated both spec files to import from the new modules.
  • Fixed two latent TypeScript errors in toc.spec.tsx that the single-export error had been masking: spreading a NodeListOf (the project's lib has no dom.iterable) → Array.from(...), and accessing the protected onEntriesChange → cast through any. (The HTMLKompendiumTocElement "cannot find name" error was a cascade from these — it resolved once the program type-checks cleanly.)

Verified locally: npm run build ✅, npm run lint ✅, npm run test ✅ (136 passing). The helpers retain full direct unit-test coverage — they just live in plain modules now.

@adrianschmidt adrianschmidt force-pushed the feat/165-table-of-contents-component branch from e113560 to 3ad00a8 Compare June 18, 2026 10:27
@adrianschmidt

adrianschmidt commented Jun 18, 2026

Copy link
Copy Markdown

This is bloody fantastic @TommyLindh2! ❤️

I've added a bunch of minor fixups, that I'm about to squash now. If you want to have a look at them, the HEAD before squashing is at 3ad00a8 (you can then navigate to the parent commit, until you reach one of your own ones).

@adrianschmidt adrianschmidt force-pushed the feat/165-table-of-contents-component branch from 3ad00a8 to 1c2c376 Compare June 18, 2026 11:39
TommyLindh2 and others added 3 commits June 18, 2026 14:15
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>
@adrianschmidt adrianschmidt force-pushed the feat/165-table-of-contents-component branch from 1c2c376 to 64972f7 Compare June 18, 2026 12:15
@adrianschmidt adrianschmidt enabled auto-merge (rebase) June 18, 2026 12:16
@adrianschmidt adrianschmidt merged commit 36fbad2 into lime Jun 18, 2026
4 checks passed
@adrianschmidt adrianschmidt deleted the feat/165-table-of-contents-component branch June 18, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants