feat: sticky table of contents with scrollspy for canvas dashboards#9693
feat: sticky table of contents with scrollspy for canvas dashboards#9693rohithreddykota wants to merge 3 commits into
Conversation
|
A couple of UXQA items,
What do you think @nishantmonu51 ? |
0ac3794 to
52255cd
Compare
| /* The rail itself: vertically centered, expanding to the full flyout on hover/focus (which | ||
| overlays the content to its right rather than reflowing it). `p-2` keeps the first and last bars | ||
| clear of the rounded corners so they aren't clipped. */ | ||
| .toc__panel { |
There was a problem hiding this comment.
Lets use toc-panel. The naming convention of double underscore is not standard for css. Same applies to double dash classes like toc__item--active.
There was a problem hiding this comment.
Done - renamed all classes to kebab-case (toc-panel, toc-item, toc-item-active, toc-bar, toc-text, toc-list, toc-compact) and updated every selector.
| : "smooth"; | ||
| } | ||
|
|
||
| function jumpTo(event: MouseEvent, entry: TocEntry) { |
There was a problem hiding this comment.
We can move this and watchScrollEnd to a separate class that has logic from loc and scrollspy. Also we should move to svelte5 way for using cross file state. Check web-common/src/lib/store-utils/types.svelte.ts for reference.
There was a problem hiding this comment.
Extracted everything into toc-controller.svelte.ts - a CanvasTocController class following the types.svelte.ts convention, with $state entries/activeId as the cross-file state. It absorbs the former scrollspy.ts (IntersectionObserver tracking + lock/unlock) along with refresh, the MutationObserver, jumpTo, and watchScrollEnd; slugify/deriveTocEntries stay as pure helpers in toc.ts. The component is now thin and runes-based, with an $effect owning the controller's lifecycle.
| behavior: this.scrollBehavior(), | ||
| block: "start", | ||
| }); | ||
| history.replaceState(history.state, "", `#${entry.id}`); |
There was a problem hiding this comment.
Do we need replace state here? Using goto will instead add support for back and forward navigation buttons. More of a product question @nishantmonu51
| // Re-derive on any content change: tab switch, async markdown resolution, edits, filters, or the | ||
| // `.row-container` appearing (e.g. after required filters are satisfied). Observing the scroll | ||
| // container rather than `.row-container` keeps this resilient to remounts. | ||
| this.mutationObserver = new MutationObserver(() => this.scheduleRefresh()); |
There was a problem hiding this comment.
This is a bit too aggressive. Lets fire events from web-common/src/features/canvas/components/markdown/Markdown.svelte on mount and destroy?
| return headings.map(({ el, text, level }) => { | ||
| let id = el.id || slugify(text); | ||
| // Dedupe collisions with a numeric suffix, e.g. "overview", "overview-2". | ||
| if (usedIds.has(id)) { |
There was a problem hiding this comment.
Lets use getName from web-common/src/features/entity-management/name-utils.ts . While it is not 1-1 it will suffice to give us a number appended id.
| }; | ||
|
|
||
| /** Turn arbitrary heading text into a URL-hash-safe slug. */ | ||
| export function slugify(text: string): string { |
There was a problem hiding this comment.
Lets merge this with web-common/src/lib/string-utils.ts::sanitizeSlug
Adds a persistent, minimap-style table of contents to canvas dashboards (view surfaces only; not the iframe embed).
h1–h3) rendered insidemarkdowncomponents of the active tab — no new section primitive; stays in sync as content/tabs change via a debouncedMutationObserver.IntersectionObserver(no scroll-event math); exactly one active section, with the highlight pinned during click-to-scroll so it doesn't flicker through intermediate sections.prefers-reduced-motion) and updates the URL hash; deep-linking on mount is supported. Accessible:<nav aria-label>, real links,aria-current="location", focus rings, full heading as the accessible name.Checklist:
Developed in collaboration with Claude Code
