|
4 | 4 | import type { Tutorial } from '$markdoc/layouts/Tutorial.svelte'; |
5 | 5 | import type { TocItem } from './DocsArticle.svelte'; |
6 | 6 | import Heading from '$markdoc/nodes/Heading.svelte'; |
7 | | - import { onMount } from 'svelte'; |
| 7 | + import { onMount, tick } from 'svelte'; |
| 8 | + import { page } from '$app/stores'; |
8 | 9 |
|
9 | 10 | export let toc: Array<TocItem>; |
10 | 11 | export let back: string; |
|
31 | 32 |
|
32 | 33 | let slotContent: HTMLElement | null = null; |
33 | 34 |
|
| 35 | + function scrollToElement(pageHash: string) { |
| 36 | + const element = document.getElementById(pageHash); |
| 37 | + if (element) { |
| 38 | + const offset = 50; |
| 39 | + const rect = element.getBoundingClientRect(); |
| 40 | + window.scroll({ top: window.scrollY + rect.top - offset }); |
| 41 | + } |
| 42 | + } |
| 43 | +
|
| 44 | + /** |
| 45 | + * Due to underlying logic with anchor links & the auto-scroll via hash values in the URL, |
| 46 | + * we have an issue where if the first item is not scrolled enough it isn't marked as `selected`. |
| 47 | + * |
| 48 | + * We do below workaround for the time being without breaking things to scroll to the first item. |
| 49 | + */ |
| 50 | + async function preSelectItemOnInit() { |
| 51 | + await tick(); |
| 52 | +
|
| 53 | + if (!$page.url.hash) return; |
| 54 | + const tocItem = toc.slice(1); |
| 55 | +
|
| 56 | + // no sub-items, return. |
| 57 | + if (!tocItem.length) return; |
| 58 | +
|
| 59 | + const pageHash = $page.url.hash.replace('#', ''); |
| 60 | + const tocItemHref = tocItem[0].href.replace('#', ''); |
| 61 | +
|
| 62 | + if (pageHash !== tocItemHref) return; |
| 63 | +
|
| 64 | + scrollToElement(pageHash); |
| 65 | + } |
| 66 | +
|
| 67 | + // same issue as above, only happens on the first item. |
| 68 | + function scrollToItem(parent: TocItem, index: number) { |
| 69 | + const tocItem = toc.slice(1); |
| 70 | +
|
| 71 | + if (!tocItem.length) return; |
| 72 | + const tocItemHref = parent.href.replace('#', ''); |
| 73 | +
|
| 74 | + const element = document.getElementById(tocItemHref); |
| 75 | +
|
| 76 | + if (index === 0) { |
| 77 | + scrollToElement(tocItemHref); |
| 78 | + } else { |
| 79 | + element?.scrollIntoView(); |
| 80 | + } |
| 81 | +
|
| 82 | + // because we used `preventDefault`. |
| 83 | + history.pushState(null, '', parent.href); |
| 84 | + } |
| 85 | +
|
34 | 86 | onMount(() => { |
35 | 87 | if (!slotContent) return; |
36 | 88 |
|
37 | 89 | // dynamically modify all `label` headers to `body`. |
38 | | - slotContent.querySelectorAll<HTMLHeadingElement>('h2.web-label').forEach((header) => { |
39 | | - header.classList.replace('web-label', 'web-main-body-500'); |
| 90 | + slotContent.querySelectorAll<HTMLHeadingElement>('h2.text-label').forEach((header) => { |
| 91 | + header.classList.replace('text-label', 'web-main-body-500'); |
40 | 92 | }); |
| 93 | +
|
| 94 | + preSelectItemOnInit(); |
41 | 95 | }); |
42 | 96 | </script> |
43 | 97 |
|
|
77 | 131 | /> |
78 | 132 | </a> |
79 | 133 | {/if} |
80 | | - <h1 class="web-title lg:-ml-5">{firstStepItem?.title}</h1> |
| 134 | + <h1 class="web-title {currentStep === 1 ? 'lg:-ml-5' : ''}"> |
| 135 | + {firstStepItem?.title} |
| 136 | + </h1> |
81 | 137 | </div> |
82 | 138 | </div> |
83 | 139 | <div class="web-article-header-end" /> |
|
87 | 143 | <section class="web-article-content-sub-section"> |
88 | 144 | <header class="web-article-content-header"> |
89 | 145 | <span class="web-numeric-badge">{currentStep}</span> |
90 | | - <Heading level={1} id={currentStepItem.href} step={currentStep}> |
91 | | - {getCorrectTitle(currentStepItem, 1)} |
92 | | - </Heading> |
| 146 | + <div class="tutorial-heading"> |
| 147 | + <Heading level={1} id={currentStepItem.href} step={currentStep}> |
| 148 | + {getCorrectTitle(currentStepItem, 1)} |
| 149 | + </Heading> |
| 150 | + </div> |
93 | 151 | </header> |
94 | 152 |
|
95 | | - <div class="u-padding-block-start-32" bind:this={slotContent}> |
| 153 | + <div class="web-u-padding-block-start-32" bind:this={slotContent}> |
96 | 154 | <slot /> |
97 | 155 | </div> |
98 | 156 |
|
99 | | - <div class="flex justify-between"> |
| 157 | + <div class="web-u-padding-block-start-32 flex justify-between"> |
100 | 158 | {#if prevStep} |
101 | 159 | <a href={prevStep.href} class="web-button is-text previous-step-anchor"> |
102 | 160 | <span class="icon-cheveron-left" aria-hidden="true" /> |
|
135 | 193 | <ol class="web-references-menu-list"> |
136 | 194 | {#each tutorials as tutorial, index} |
137 | 195 | {@const isCurrentStep = currentStep === tutorial.step} |
| 196 | + {@const absoluteToc = toc.slice(1)} |
138 | 197 | <li class="web-references-menu-item"> |
139 | 198 | <a |
140 | 199 | href={tutorial.href} |
|
148 | 207 | >{index === 0 ? 'Introduction' : tutorial.title}</span |
149 | 208 | > |
150 | 209 | </a> |
151 | | - {#if isCurrentStep && toc.length} |
| 210 | + {#if isCurrentStep && absoluteToc.length} |
152 | 211 | <ol |
153 | 212 | class="web-references-menu-list u-margin-block-start-16 u-margin-inline-start-32" |
154 | 213 | > |
155 | | - {#each toc.slice(1) as parent} |
| 214 | + {#each absoluteToc as parent, innerIndex} |
156 | 215 | <li class="web-references-menu-item"> |
157 | 216 | <a |
158 | 217 | href={parent.href} |
| 218 | + on:click|preventDefault={() => |
| 219 | + scrollToItem(parent, innerIndex)} |
159 | 220 | class="web-references-menu-link is-inner" |
160 | 221 | class:tutorial-scroll-indicator={parent.selected} |
161 | 222 | class:is-selected={parent.selected} |
|
208 | 269 | background: unset; |
209 | 270 | padding-inline-start: unset; |
210 | 271 | } |
| 272 | +
|
| 273 | + .u-margin-block-start-16 { |
| 274 | + margin-block-start: 1rem; |
| 275 | + } |
| 276 | +
|
| 277 | + .u-margin-inline-start-32 { |
| 278 | + margin-inline-start: 2rem; |
| 279 | + } |
| 280 | +
|
| 281 | + .web-references-menu-item:has(.is-selected)::before { |
| 282 | + /* maintains the distance correctly for the children items */ |
| 283 | + inset-inline-start: -3.55rem; |
| 284 | + } |
| 285 | +
|
| 286 | + /* Static slider: default slider for each selected link */ |
| 287 | + .web-references-menu-list > .web-references-menu-item > .is-selected::before { |
| 288 | + content: ' '; |
| 289 | + position: absolute; |
| 290 | + inset-block-start: 0; |
| 291 | + block-size: 1.375rem; |
| 292 | + inline-size: 0.0625rem; |
| 293 | + inset-inline-start: -1.3125rem; |
| 294 | + background-color: hsl(var(--p-references-menu-link-color-text)); |
| 295 | + } |
| 296 | +
|
| 297 | + /* Hide static slider if any child menu item is selected */ |
| 298 | + .web-references-menu-list |
| 299 | + > .web-references-menu-item:has(.web-references-menu-list .is-selected) |
| 300 | + > .is-selected::before { |
| 301 | + background-color: transparent; |
| 302 | + } |
| 303 | +
|
| 304 | + /* Transparent slider for selected child items because we use parent level */ |
| 305 | + .web-references-menu-list |
| 306 | + > .web-references-menu-item |
| 307 | + > .web-references-menu-list |
| 308 | + > .web-references-menu-item |
| 309 | + > .is-selected::before { |
| 310 | + content: ''; |
| 311 | + background-color: transparent; |
| 312 | + } |
| 313 | +
|
| 314 | + :global(.tutorial-heading h2) { |
| 315 | + margin-bottom: unset; |
| 316 | + } |
211 | 317 | </style> |
0 commit comments