|
1 | 1 | <template> |
2 | 2 | <div class="els-steps"> |
3 | | - <div class="els-steps-body"> |
| 3 | + <div ref="bodyRef" class="els-steps-body"> |
4 | 4 | <slot /> |
5 | 5 | </div> |
6 | 6 | </div> |
7 | 7 | </template> |
8 | 8 |
|
| 9 | +<script setup lang="ts"> |
| 10 | +import { onMounted, ref } from 'vue'; |
| 11 | +import { slugify, uniqueId, collectUsedIds } from '../utils/slugify'; |
| 12 | +
|
| 13 | +const bodyRef = ref<HTMLElement | null>(null); |
| 14 | +
|
| 15 | +function anchorSteps() { |
| 16 | + const body = bodyRef.value; |
| 17 | + if (!body) return; |
| 18 | +
|
| 19 | + // Step ids are assigned at build time (els-structured-data markdown plugin). |
| 20 | + // We only read them here to attach the visual affordances; slugify is a |
| 21 | + // fallback for the rare case a build-time id is missing. |
| 22 | + const used = collectUsedIds(); |
| 23 | +
|
| 24 | + // Top-level steps only — leave nested sub-steps (ol ol > li) un-anchored. |
| 25 | + body.querySelectorAll<HTMLLIElement>(':scope > ol > li').forEach((li) => { |
| 26 | + if (li.dataset.anchored) return; // already decorated |
| 27 | + li.dataset.anchored = '1'; |
| 28 | +
|
| 29 | + const titleEl = li.querySelector('p'); |
| 30 | + let id = li.id; |
| 31 | + if (!id) { |
| 32 | + const title = titleEl?.textContent ?? li.textContent ?? ''; |
| 33 | + id = uniqueId(slugify(title) || 'step', used); |
| 34 | + li.id = id; |
| 35 | + } |
| 36 | +
|
| 37 | + const anchor = document.createElement('a'); |
| 38 | + anchor.className = 'header-anchor els-step-anchor'; |
| 39 | + anchor.setAttribute('href', `#${id}`); |
| 40 | + anchor.setAttribute('aria-hidden', 'true'); |
| 41 | + anchor.setAttribute('tabindex', '-1'); |
| 42 | + anchor.textContent = '#'; |
| 43 | + // On the left of the step title (prepended), vertically aligned with it. |
| 44 | + const target = titleEl ?? li; |
| 45 | + target.insertBefore(anchor, target.firstChild); |
| 46 | +
|
| 47 | + // Transparent overlay over the number badge so clicking the number also |
| 48 | + // navigates to the step's anchor and updates the URL. |
| 49 | + const numberLink = document.createElement('a'); |
| 50 | + numberLink.className = 'els-step-number'; |
| 51 | + numberLink.setAttribute('href', `#${id}`); |
| 52 | + numberLink.setAttribute('aria-hidden', 'true'); |
| 53 | + numberLink.setAttribute('tabindex', '-1'); |
| 54 | + li.appendChild(numberLink); |
| 55 | + }); |
| 56 | +} |
| 57 | +
|
| 58 | +onMounted(() => { |
| 59 | + // Run after VuePress has assigned heading ids. |
| 60 | + setTimeout(anchorSteps, 0); |
| 61 | +}); |
| 62 | +</script> |
| 63 | + |
9 | 64 | <style scoped> |
10 | 65 | .els-steps { |
11 | 66 | margin: 1.5rem 0; |
|
22 | 77 | .els-steps-body :deep(ol > li) { |
23 | 78 | counter-increment: step-counter; |
24 | 79 | position: relative; |
25 | | - padding-left: 1.5rem; |
| 80 | + padding-left: 1.2rem; |
26 | 81 | padding-bottom: 1.25rem; |
27 | 82 | margin-bottom: 0; |
28 | 83 | border-left: 2px solid #e0e3e8; |
29 | 84 | margin-left: 0.9rem; |
| 85 | + /* Land below the fixed navbar when navigated to via a step anchor. */ |
| 86 | + scroll-margin-top: 6rem; |
| 87 | +} |
| 88 | +
|
| 89 | +/* The anchor floats left at its natural position (no negative margin, so it |
| 90 | + never overlaps the number badge), with a fixed width and a minimal gap. |
| 91 | + font-size in rem (not em) keeps it identical to the prerequisites anchor. */ |
| 92 | +.els-steps-body :deep(ol > li > p:first-child > a.els-step-anchor) { |
| 93 | + opacity: 0; |
| 94 | + font-size: 1rem; |
| 95 | + width: 0.7rem; |
| 96 | + padding-right: 0; |
| 97 | + margin-left: 0; |
| 98 | + /* Inherit the title's 2rem line-height so the floated # centers at the same |
| 99 | + level as the (vertically centered) wording. No extra margin-top needed. */ |
| 100 | + line-height: inherit; |
| 101 | + margin-top: 0; |
| 102 | + transition: opacity 0.15s ease; |
| 103 | +} |
| 104 | +
|
| 105 | +.els-steps-body :deep(ol > li:hover > p:first-child > a.els-step-anchor) { |
| 106 | + opacity: 1; |
| 107 | +} |
| 108 | +
|
| 109 | +/* Indent the step's body content (everything below the title) by the anchor's |
| 110 | + reserved width so it aligns with the title wording. margin-left (not padding) |
| 111 | + so code blocks move as a whole and their background aligns too. Exclude the |
| 112 | + absolutely positioned number-overlay link. */ |
| 113 | +.els-steps-body :deep(ol > li > *:not(:first-child):not(a)) { |
| 114 | + margin-left: 0.7rem; |
| 115 | +} |
| 116 | +
|
| 117 | +/* Transparent overlay over the number badge — makes the number clickable. */ |
| 118 | +.els-steps-body :deep(ol > li > a.els-step-number) { |
| 119 | + position: absolute; |
| 120 | + left: -1rem; |
| 121 | + top: 0; |
| 122 | + width: 2rem; |
| 123 | + height: 2rem; |
| 124 | + border-radius: 50%; |
| 125 | + cursor: pointer; |
30 | 126 | } |
31 | 127 |
|
32 | 128 | .els-steps-body :deep(ol > li:last-child) { |
|
50 | 146 | align-items: center; |
51 | 147 | justify-content: center; |
52 | 148 | line-height: 1; |
| 149 | + transition: background 0.15s ease; |
| 150 | +} |
| 151 | +
|
| 152 | +/* Hover hint on the step: the number badge changes color. */ |
| 153 | +.els-steps-body :deep(ol > li:hover::before) { |
| 154 | + background: #0B5CAD; |
53 | 155 | } |
54 | 156 |
|
55 | 157 | .els-steps-body :deep(ol > li > p:first-child) { |
56 | 158 | font-weight: 600; |
57 | 159 | color: #0d1a26; |
58 | 160 | margin-top: 0; |
| 161 | + /* Match the number badge height so the wording sits at the badge's vertical |
| 162 | + middle (where the number is) instead of at its top. */ |
| 163 | + min-height: 2rem; |
| 164 | + line-height: 2rem; |
59 | 165 | } |
60 | 166 |
|
61 | 167 | .els-steps-body :deep(ol > li > p) { |
|
83 | 189 |
|
84 | 190 | .els-steps-body :deep(div[class*="language-"]), |
85 | 191 | .els-steps-body :deep(.code-with-copy) { |
86 | | - margin: 0.5rem 0; |
| 192 | + /* top/bottom only — left indent comes from the body-content rule above so |
| 193 | + the code block aligns with the step wording. */ |
| 194 | + margin-top: 0.5rem; |
| 195 | + margin-bottom: 0.5rem; |
87 | 196 | } |
88 | 197 |
|
89 | 198 | .els-steps-body :deep(:not(pre) > code) { |
|
0 commit comments