Skip to content

Commit 8d02c83

Browse files
author
Quang Phan
committed
feat(toc): complementary toclink action
1 parent 80730b4 commit 8d02c83

52 files changed

Lines changed: 946 additions & 452 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/cool-kangaroos-rhyme.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@svelte-put/toc': minor
3+
---
4+
5+
add idiomatic support with complementary action `toclink` for anchor tags that link to toc items (typically those in a table of contents)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@svelte-put/toc': patch
3+
---
4+
5+
undefined `anchor` & `observe` should correctly take default options

apps/docs/src/lib/client/components/BackToTopBtn/BackToTopBtn.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
transition:fly={{ y: 100, duration: 200 }}
2323
type="button"
2424
on:click={handleClick}
25-
class="grid place-items-center rounded-full bg-fg p-2 text-bg shadow hover:opacity-100 md:opacity-50 {cls}"
25+
class="grid place-items-center rounded-full bg-fg p-2 text-bg shadow transition-opacity duration-150 hover:opacity-100 md:opacity-50 {cls}"
2626
aria-label="Back to top"
2727
>
2828
<ArrowUp height="24" width="24" />

apps/docs/src/routes/(main)/+layout.svelte

Lines changed: 131 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<script lang="ts">
22
import { clickoutside } from '@svelte-put/clickoutside';
3-
import { toc, createTocStore } from '@svelte-put/toc';
3+
import { toc, toclink, createTocStore } from '@svelte-put/toc';
44
import { slide } from '@svelte-put/transitions';
55
import gruvboxDark from 'svelte-highlight/styles/gruvbox-dark-soft';
66
import gruvboxLight from 'svelte-highlight/styles/gruvbox-light-soft';
7-
import { fly, fade } from 'svelte/transition';
7+
import { fade } from 'svelte/transition';
88
99
import { browser } from '$app/environment';
1010
import { invalidate } from '$app/navigation';
@@ -49,20 +49,7 @@
4949
rightSidebarTogglerClick++;
5050
}
5151
52-
let tocChangePaused = false;
53-
let activeTocId: string;
5452
const tocStore = createTocStore();
55-
$: if (!tocChangePaused) activeTocId = $tocStore.activeItem?.id ?? '';
56-
57-
let intersectResetTimeoutId: ReturnType<typeof setTimeout>;
58-
function handleTocItemClick(id: string) {
59-
clearTimeout(intersectResetTimeoutId);
60-
activeTocId = id;
61-
tocChangePaused = true;
62-
intersectResetTimeoutId = setTimeout(() => {
63-
tocChangePaused = false;
64-
}, 800);
65-
}
6653
6754
$: rColorScheme =
6855
$page.data.colorScheme === 'system' && browser
@@ -130,57 +117,54 @@
130117
</div>
131118
</header>
132119

133-
<div class="c-container relative flex w-full flex-1 items-stretch">
134-
{#key leftSidebarTogglerClick}
135-
<nav
136-
class="sidebar sidebar-left"
137-
transition:fly|local={{ x: -200, duration: 200 }}
138-
use:clickoutside={{ enabled: leftSidebarOpen && !isLg }}
139-
on:clickoutside={toggleLeftSidebar}
140-
data-open={leftSidebarOpen}
141-
aria-label="Pages"
142-
>
143-
<ul class="sidebar-content">
144-
<li>
145-
<a
146-
href={APP_ROUTE_TREE.docs.$.path()}
147-
data-current={data.pathname === APP_ROUTE_TREE.docs.$.path()}
148-
class="c-link block py-2 font-bold"
149-
on:click={closeLeftSidebar}
150-
data-sveltekit-preload-data="hover"
151-
>
152-
Introduction
153-
</a>
120+
<div class="relative flex w-full flex-1 items-stretch">
121+
<nav
122+
class="sidebar sidebar-left"
123+
use:clickoutside={{ enabled: leftSidebarOpen && !isLg }}
124+
on:clickoutside={toggleLeftSidebar}
125+
data-open={leftSidebarOpen}
126+
aria-label="Pages"
127+
>
128+
<ul class="sidebar-content">
129+
<li>
130+
<a
131+
href={APP_ROUTE_TREE.docs.$.path()}
132+
data-current={data.pathname === APP_ROUTE_TREE.docs.$.path()}
133+
class="c-link block py-2 font-bold"
134+
on:click={closeLeftSidebar}
135+
data-sveltekit-preload-data="hover"
136+
>
137+
Introduction
138+
</a>
139+
</li>
140+
{#each Object.entries(packagesByCategory) as [category, packages]}
141+
<li class="py-2">
142+
<p class="font-bold">{capitalize(category)}</p>
143+
<ul class="mt-2 space-y-1 border-l border-border">
144+
{#each packages as { path, status, id }}
145+
<li>
146+
<a
147+
data-sveltekit-preload-data="hover"
148+
href={path}
149+
data-current={data.pathname.includes(id)}
150+
class="c-link -ml-px block border-l border-transparent py-1 pl-3 data-current:border-primary"
151+
on:click={closeLeftSidebar}
152+
>
153+
<span class="h-full w-1 bg-primary" />
154+
{id}
155+
<sup>
156+
{#if status !== 'stable'}
157+
<StatusBadge {status} />
158+
{/if}
159+
</sup>
160+
</a>
161+
</li>
162+
{/each}
163+
</ul>
154164
</li>
155-
{#each Object.entries(packagesByCategory) as [category, packages]}
156-
<li class="py-2">
157-
<p class="font-bold">{capitalize(category)}</p>
158-
<ul class="mt-2 space-y-1 border-l border-border">
159-
{#each packages as { path, status, id }}
160-
<li>
161-
<a
162-
data-sveltekit-preload-data="hover"
163-
href={path}
164-
data-current={data.pathname.includes(id)}
165-
class="c-link -ml-px block border-l border-transparent py-1 pl-3 data-current:border-primary"
166-
on:click={closeLeftSidebar}
167-
>
168-
<span class="h-full w-1 bg-primary" />
169-
{id}
170-
<sup>
171-
{#if status !== 'stable'}
172-
<StatusBadge {status} />
173-
{/if}
174-
</sup>
175-
</a>
176-
</li>
177-
{/each}
178-
</ul>
179-
</li>
180-
{/each}
181-
</ul>
182-
</nav>
183-
{/key}
165+
{/each}
166+
</ul>
167+
</nav>
184168
{#if leftSidebarOpen}
185169
<div class="sidebar-backdrop" transition:fade|local={{ duration: 150 }} />
186170
{/if}
@@ -198,49 +182,48 @@
198182
</main>
199183
{/key}
200184

201-
{#key rightSidebarTogglerClick}
202-
<nav
203-
class="sidebar sidebar-right"
204-
transition:fly|local={{ x: 200, duration: 200 }}
205-
use:clickoutside={{ enabled: rightSidebarOpen && !isXl }}
206-
on:clickoutside={toggleRightSidebar}
207-
data-open={rightSidebarOpen}
208-
aria-label="Table of Contents"
209-
>
210-
<div class="sidebar-content text-sm">
211-
{#if Object.values($tocStore.items).length}
212-
<p class="py-2 font-bold uppercase">On This Page</p>
213-
<ul class="space-y-1 border-l border-border">
214-
{#each Object.values($tocStore.items) as { id, text, element }}
215-
{@const level = element.tagName.slice(1)}
216-
{@const current = id === activeTocId}
217-
<li>
218-
<a
219-
href={`#${id}`}
220-
data-current={current}
221-
class="c-link -ml-px block border-l border-transparent py-1 data-current:border-primary"
222-
class:pl-3={level === '2'}
223-
class:pl-5={level === '3'}
224-
class:pl-7={level === '4'}
225-
class:pl-9={level === '5'}
226-
class:pl-11={level === '6'}
227-
on:click={() => handleTocItemClick(id)}
228-
>
229-
{text}
230-
</a>
231-
</li>
232-
{/each}
233-
</ul>
234-
{/if}
235-
</div>
236-
</nav>
237-
{/key}
185+
<nav
186+
class="sidebar sidebar-right"
187+
use:clickoutside={{ enabled: rightSidebarOpen && !isXl }}
188+
on:clickoutside={toggleRightSidebar}
189+
data-open={rightSidebarOpen}
190+
aria-label="Table of Contents"
191+
>
192+
<div class="sidebar-content text-sm">
193+
{#if Object.values($tocStore.items).length}
194+
<p class="py-2 font-bold uppercase">On This Page</p>
195+
<ul class="space-y-1 border-l border-border">
196+
{#each Object.values($tocStore.items) as tocItem}
197+
{@const level = tocItem.element.tagName.slice(1)}
198+
<li>
199+
<!-- svelte-ignore a11y-missing-attribute -->
200+
<a
201+
use:toclink={{
202+
tocItem,
203+
store: tocStore,
204+
observe: {
205+
attribute: 'data-current',
206+
},
207+
}}
208+
class="c-link -ml-px block border-l border-transparent py-1 data-current:border-primary"
209+
class:pl-3={level === '2'}
210+
class:pl-5={level === '3'}
211+
class:pl-7={level === '4'}
212+
class:pl-9={level === '5'}
213+
class:pl-11={level === '6'}>{tocItem.text}</a
214+
>
215+
</li>
216+
{/each}
217+
</ul>
218+
{/if}
219+
</div>
220+
</nav>
238221
</div>
239222

240223
<footer class="border-t border-border bg-bg-soft py-6 text-center font-fira text-xs">
241224
<div class="c-container grid md:grid-cols-[auto,1fr,auto]">
242225
<div
243-
class="max-md:mb-4 max-md:flex max-md:items-center max-md:justify-center max-md:space-x-8 md:space-y-2 md:text-left"
226+
class="md:space-y-2 md:text-left max-md:mb-4 max-md:flex max-md:items-center max-md:justify-center max-md:space-x-8"
244227
>
245228
<p>
246229
<a href="/sitemap.xml" class="c-link" target="_blank">
@@ -287,34 +270,57 @@
287270
<style lang="postcss">
288271
.sidebar {
289272
flex-shrink: 0;
273+
290274
width: theme('spacing.sidebar');
275+
291276
font-size: theme('fontSize.sm');
277+
292278
border-color: theme('borderColor.border');
293279
280+
transition-timing-function: theme('transitionTimingFunction.DEFAULT');
281+
transition-duration: 200ms;
282+
transition-property: transform, opacity;
283+
294284
& .sidebar-content {
295285
position: sticky;
296286
top: theme('spacing.header');
287+
288+
overflow: auto;
289+
290+
width: 100%;
297291
padding-top: theme('spacing.10');
298292
}
299293
}
300294
301295
.sidebar-left {
302-
@media (max-width: theme('screens.lg')) {
296+
& .sidebar-content {
297+
max-height: calc(100vh - theme('spacing.header'));
298+
padding-right: theme('spacing.4');
299+
padding-left: theme('spacing.4');
300+
}
301+
302+
@media (max-width: theme('screens.max-md')) {
303303
position: fixed;
304304
z-index: theme('zIndex.sidebar');
305305
top: 0;
306306
bottom: 0;
307307
left: 0;
308+
transform: translateX(-100%);
308309
309-
display: none;
310-
justify-content: center;
310+
display: flex;
311311
312+
opacity: 0;
312313
background-color: theme('backgroundColor.bg.DEFAULT');
313314
border-right: theme('borderWidth.DEFAULT');
314315
box-shadow: theme('boxShadow.lg');
315316
316317
&[data-open='true'] {
317-
display: flex;
318+
transform: translateX(0);
319+
opacity: 1;
320+
}
321+
322+
& .sidebar-content {
323+
max-height: 100vh;
318324
}
319325
}
320326
}
@@ -332,33 +338,46 @@
332338
@screen lg {
333339
display: none;
334340
}
341+
342+
&::-webkit-scrollbar {
343+
display: none;
344+
}
335345
}
336346
337347
.sidebar-right {
338-
@media (max-width: theme('screens.xl')) {
348+
& .sidebar-content {
349+
max-height: calc(100vh - theme('spacing.header'));
350+
padding: theme('spacing.2');
351+
padding-top: theme('spacing.10');
352+
}
353+
354+
@media (max-width: theme('screens.max-lg')) {
339355
position: fixed;
340356
z-index: theme('zIndex.sidebar');
341357
top: theme('spacing.header');
342358
right: 0;
359+
transform: translateX(100%);
343360
344-
display: none;
361+
display: flex;
345362
justify-content: center;
346363
347364
width: auto;
348365
max-width: 80vw;
349-
padding: theme('spacing.4');
350366
367+
opacity: 0;
351368
background-color: theme('backgroundColor.bg.DEFAULT');
352369
border-bottom-right-radius: theme('borderRadius.DEFAULT');
353370
border-bottom-left-radius: theme('borderRadius.DEFAULT');
354371
box-shadow: theme('boxShadow.lg');
355372
356373
&[data-open='true'] {
357-
display: flex;
374+
transform: translateX(0);
375+
opacity: 1;
358376
}
359377
360378
& .sidebar-content {
361-
padding: theme('spacing.4');
379+
max-height: calc(95vh - theme('spacing.header'));
380+
padding: theme('spacing.8') theme('spacing.2') theme('spacing.2') theme('spacing.8');
362381
}
363382
}
364383
}

0 commit comments

Comments
 (0)