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