mirror of https://github.com/sveltejs/svelte
feat: Better docs nav (#8605)
* DocsNav * Push * Nav title on each page * Install jridgewell sourcemap codec. Why it breaking suddenly * Use theme store * Use $nav_title * use $page.data.nav_title * Disable global prerendering * Fix Suppprters section * use new method * Initially hidden nav functionality * Minor fixes * Simplify into one single nav * Accomodate to the bottom nav * Minor fixes * nit * Add selected to other pages as well * New way of passing to navbar * Code cleanup * Directly pass list instead of components * 14 days * Fix comment * Discord icon * Bump site-kit finallypull/8731/head
parent
ad9a672171
commit
aa5bb4a3db
@ -0,0 +1,11 @@
|
|||||||
|
/// <reference types="@sveltejs/kit" />
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
interface PageData {
|
||||||
|
nav_title: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
@ -0,0 +1,74 @@
|
|||||||
|
import { get_blog_data, get_blog_list } from '$lib/server/blog/index.js';
|
||||||
|
import { get_docs_data, get_docs_list } from '$lib/server/docs/index.js';
|
||||||
|
import { get_examples_data, get_examples_list } from '$lib/server/examples/index.js';
|
||||||
|
import { get_tutorial_data, get_tutorial_list } from '$lib/server/tutorial/index.js';
|
||||||
|
|
||||||
|
/** @param {URL} url */
|
||||||
|
function get_nav_title(url) {
|
||||||
|
const list = new Map([
|
||||||
|
[/^docs/, 'Docs'],
|
||||||
|
[/^repl/, 'REPL'],
|
||||||
|
[/^blog/, 'Blog'],
|
||||||
|
[/^faq/, 'FAQ'],
|
||||||
|
[/^tutorial/, 'Tutorial'],
|
||||||
|
[/^search/, 'Search'],
|
||||||
|
[/^examples/, 'Examples']
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const [regex, title] of list) {
|
||||||
|
if (regex.test(url.pathname.replace(/^\/(.+)/, '$1'))) {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get_nav_context_list() {
|
||||||
|
const docs_list = get_docs_list(get_docs_data());
|
||||||
|
const processed_docs_list = docs_list.map(({ title, pages }) => ({
|
||||||
|
title,
|
||||||
|
sections: pages.map(({ title, path }) => ({ title, path }))
|
||||||
|
}));
|
||||||
|
|
||||||
|
const blog_list = get_blog_list(get_blog_data());
|
||||||
|
const processed_blog_list = [
|
||||||
|
{
|
||||||
|
title: 'Blog',
|
||||||
|
sections: blog_list.map(({ title, slug, date }) => ({
|
||||||
|
title,
|
||||||
|
path: '/blog/' + slug,
|
||||||
|
// Put a NEW badge on blog posts that are less than 14 days old
|
||||||
|
badge: (+new Date() - +new Date(date)) / (1000 * 60 * 60 * 24) < 14 ? 'NEW' : undefined
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const tutorial_list = get_tutorial_list(get_tutorial_data());
|
||||||
|
const processed_tutorial_list = tutorial_list.map(({ title, tutorials }) => ({
|
||||||
|
title,
|
||||||
|
sections: tutorials.map(({ title, slug }) => ({ title, path: '/tutorial/' + slug }))
|
||||||
|
}));
|
||||||
|
|
||||||
|
const examples_list = get_examples_list(get_examples_data());
|
||||||
|
const processed_examples_list = examples_list
|
||||||
|
.map(({ title, examples }) => ({
|
||||||
|
title,
|
||||||
|
sections: examples.map(({ title, slug }) => ({ title, path: '/examples/' + slug }))
|
||||||
|
}))
|
||||||
|
.filter(({ title }) => title !== 'Embeds');
|
||||||
|
|
||||||
|
return {
|
||||||
|
docs: processed_docs_list,
|
||||||
|
blog: processed_blog_list,
|
||||||
|
tutorial: processed_tutorial_list,
|
||||||
|
examples: processed_examples_list
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const load = async ({ url }) => {
|
||||||
|
return {
|
||||||
|
nav_title: get_nav_title(url),
|
||||||
|
nav_context_list: get_nav_context_list()
|
||||||
|
};
|
||||||
|
};
|
@ -1,162 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { page } from '$app/stores';
|
|
||||||
import { SkipLink } from '@sveltejs/site-kit/components';
|
|
||||||
|
|
||||||
/** @type {ReturnType<typeof import('$lib/server/docs').get_docs_list>}*/
|
|
||||||
export let contents = [];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<SkipLink href="#docs-content">Skip to documentation</SkipLink>
|
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
|
||||||
<nav aria-label="Docs">
|
|
||||||
<ul class="sidebar">
|
|
||||||
{#each contents as section}
|
|
||||||
<li>
|
|
||||||
<span class="section">
|
|
||||||
{section.title}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{#each section.pages as { title, path }}
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
data-sveltekit-preload-data
|
|
||||||
class="page"
|
|
||||||
class:active={path === $page.url.pathname}
|
|
||||||
href={path}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
nav {
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
color: var(--sk-text-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
padding: var(--sk-page-padding-top) 0 var(--sk-page-padding-top) 3.2rem;
|
|
||||||
font-family: var(--sk-font);
|
|
||||||
height: 100%;
|
|
||||||
bottom: auto;
|
|
||||||
width: 100%;
|
|
||||||
columns: 2;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: block;
|
|
||||||
line-height: 1.2;
|
|
||||||
margin: 0;
|
|
||||||
margin-bottom: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
li:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
position: relative;
|
|
||||||
transition: color 0.2s;
|
|
||||||
border-bottom: none;
|
|
||||||
padding: 0;
|
|
||||||
color: var(--sk-text-3);
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
display: block;
|
|
||||||
padding-bottom: 0.8rem;
|
|
||||||
font-size: var(--sk-text-xs);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page {
|
|
||||||
display: block;
|
|
||||||
font-size: 1.6rem;
|
|
||||||
font-family: var(--sk-font);
|
|
||||||
padding-bottom: 0.6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active {
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--sk-text-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
ul ul,
|
|
||||||
ul ul li {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 600px) {
|
|
||||||
.sidebar {
|
|
||||||
columns: 2;
|
|
||||||
padding-left: var(--sk-page-padding-side);
|
|
||||||
padding-right: var(--sk-page-padding-side);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 700px) {
|
|
||||||
.sidebar {
|
|
||||||
columns: 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 832px) {
|
|
||||||
.sidebar {
|
|
||||||
columns: 1;
|
|
||||||
padding-left: 3.2rem;
|
|
||||||
padding-right: 0;
|
|
||||||
width: var(--sidebar-menu-width);
|
|
||||||
margin: 0 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
min-height: calc(100vh - var(--ts-toggle-height));
|
|
||||||
}
|
|
||||||
|
|
||||||
nav::after {
|
|
||||||
content: '';
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
bottom: var(--ts-toggle-height);
|
|
||||||
width: calc(var(--sidebar-width) - 1px);
|
|
||||||
height: 2em;
|
|
||||||
pointer-events: none;
|
|
||||||
background: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
hsla(var(--sk-back-3-hsl), 0) 0%,
|
|
||||||
hsla(var(--sk-back-3-hsl), 0.7) 50%,
|
|
||||||
hsl(var(--sk-back-3-hsl)) 100%
|
|
||||||
);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: calc(100% - 3rem) 100%; /* cover text but not scrollbar */
|
|
||||||
}
|
|
||||||
|
|
||||||
.active::after {
|
|
||||||
--size: 1rem;
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: var(--size);
|
|
||||||
height: var(--size);
|
|
||||||
top: -0.1rem;
|
|
||||||
right: calc(-0.5 * var(--size));
|
|
||||||
background-color: var(--sk-back-1);
|
|
||||||
border-left: 1px solid var(--sk-back-5);
|
|
||||||
border-bottom: 1px solid var(--sk-back-5);
|
|
||||||
transform: translateY(0.2rem) rotate(45deg);
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -0,0 +1,18 @@
|
|||||||
|
import { DocsMobileNav } from '@sveltejs/site-kit/docs';
|
||||||
|
|
||||||
|
export const load = async ({ data, parent }) => {
|
||||||
|
const contents = await parent();
|
||||||
|
|
||||||
|
return {
|
||||||
|
mobile_nav_start: {
|
||||||
|
label: 'Contents',
|
||||||
|
icon: 'contents',
|
||||||
|
component: DocsMobileNav,
|
||||||
|
props: {
|
||||||
|
contents: contents.sections,
|
||||||
|
pageContents: data.page
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
};
|
@ -1,163 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { afterNavigate } from '$app/navigation';
|
|
||||||
import { base } from '$app/paths';
|
|
||||||
import { page } from '$app/stores';
|
|
||||||
import { afterUpdate, onMount } from 'svelte';
|
|
||||||
|
|
||||||
/** @type {import('./$types').PageData['page']} */
|
|
||||||
export let details;
|
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
let hash = '';
|
|
||||||
|
|
||||||
/** @type {number} */
|
|
||||||
let height = 0;
|
|
||||||
|
|
||||||
/** @type {HTMLElement} */
|
|
||||||
let content;
|
|
||||||
|
|
||||||
/** @type {NodeListOf<HTMLElement>} */
|
|
||||||
let headings;
|
|
||||||
|
|
||||||
/** @type {number[]} */
|
|
||||||
let positions = [];
|
|
||||||
|
|
||||||
/** @type {HTMLElement} */
|
|
||||||
let containerEl;
|
|
||||||
|
|
||||||
let show_contents = false;
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await document.fonts.ready;
|
|
||||||
update();
|
|
||||||
highlight();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterNavigate(() => {
|
|
||||||
update();
|
|
||||||
highlight();
|
|
||||||
});
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
content = document.querySelector('.content');
|
|
||||||
const { top } = content.getBoundingClientRect();
|
|
||||||
headings = content.querySelectorAll('h2[id]');
|
|
||||||
positions = Array.from(headings).map((heading) => {
|
|
||||||
const style = getComputedStyle(heading);
|
|
||||||
return heading.getBoundingClientRect().top - parseFloat(style.scrollMarginTop) - top;
|
|
||||||
});
|
|
||||||
height = window.innerHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
function highlight() {
|
|
||||||
const { top, bottom } = content.getBoundingClientRect();
|
|
||||||
let i = headings.length;
|
|
||||||
while (i--) {
|
|
||||||
if (bottom - height < 50 || positions[i] + top < 100) {
|
|
||||||
const heading = headings[i];
|
|
||||||
hash = `#${heading.id}`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hash = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {URL} url */
|
|
||||||
function select(url) {
|
|
||||||
// belt...
|
|
||||||
setTimeout(() => {
|
|
||||||
hash = url.hash;
|
|
||||||
});
|
|
||||||
// ...and braces
|
|
||||||
window.addEventListener(
|
|
||||||
'scroll',
|
|
||||||
() => {
|
|
||||||
hash = url.hash;
|
|
||||||
},
|
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
afterUpdate(() => {
|
|
||||||
// bit of a hack — prevent sidebar scrolling if
|
|
||||||
// TOC is open on mobile, or scroll came from within sidebar
|
|
||||||
if (show_contents && window.innerWidth < 832) return;
|
|
||||||
const active = containerEl.querySelector('.active');
|
|
||||||
if (active) {
|
|
||||||
const { top, bottom } = active.getBoundingClientRect();
|
|
||||||
const min = 100;
|
|
||||||
const max = window.innerHeight - 100;
|
|
||||||
|
|
||||||
if (top > max) {
|
|
||||||
containerEl.scrollBy({
|
|
||||||
top: top - max,
|
|
||||||
left: 0,
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
} else if (bottom < min) {
|
|
||||||
containerEl.scrollBy({
|
|
||||||
top: bottom - min,
|
|
||||||
left: 0,
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:window on:scroll={highlight} on:resize={update} on:hashchange={() => select($page.url)} />
|
|
||||||
|
|
||||||
<aside class="on-this-page" bind:this={containerEl}>
|
|
||||||
<h2>On this page</h2>
|
|
||||||
<nav>
|
|
||||||
<ul>
|
|
||||||
<li><a href="{base}/docs/{details.slug}" class:active={hash === ''}>{details.title}</a></li>
|
|
||||||
{#each details.sections as { title, slug }}
|
|
||||||
<li><a href={`#${slug}`} class:active={`#${slug}` === hash}>{title}</a></li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.on-this-page {
|
|
||||||
display: var(--on-this-page-display);
|
|
||||||
position: fixed;
|
|
||||||
padding: var(--sk-page-padding-top) var(--sk-page-padding-side) 0 0;
|
|
||||||
width: min(280px, calc(var(--sidebar-width) - var(--sk-page-padding-side)));
|
|
||||||
height: calc(100vh - var(--sk-nav-height) - var(--sk-page-padding-top));
|
|
||||||
top: var(--sk-nav-height);
|
|
||||||
left: calc(100vw - (var(--sidebar-width)));
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 1.4rem !important;
|
|
||||||
font-weight: 400;
|
|
||||||
margin: 0 0 1rem 0 !important;
|
|
||||||
padding: 0 0 0 0.6rem;
|
|
||||||
color: var(--sk-text-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
display: block;
|
|
||||||
padding: 0.3rem 0.5rem;
|
|
||||||
color: var(--sk-text-3);
|
|
||||||
border-left: 2px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
background: var(--sk-back-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
a.active {
|
|
||||||
background: var(--sk-back-3);
|
|
||||||
border-left-color: var(--sk-theme-1);
|
|
||||||
}
|
|
||||||
</style>
|
|
Loading…
Reference in new issue