You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte/sites/svelte.dev/src/routes/tutorial/[slug]/+page.svelte

363 lines
8.0 KiB

<script>
import { browser } from '$app/environment';
import ScreenToggle from '$lib/components/ScreenToggle.svelte';
import Repl from '@sveltejs/repl';
import { theme } from '@sveltejs/site-kit/stores';
import { mapbox_setup, svelteUrl } from '../../../config.js';
import TableOfContents from './TableOfContents.svelte';
export let data;
/** @type {import('@sveltejs/repl').default} */
let repl;
let prev;
let scrollable;
/** @type {Map<string, {
* slug: string,
* section: import('$lib/server/tutorial/types').TutorialSection,
* chapter: import('$lib/server/tutorial/types').Tutorial,
* prev: { slug: string, section: import('$lib/server/tutorial/types').TutorialSection, chapter: import('$lib/server/tutorial/types').Tutorial }
* next?: { slug: string, section: import('$lib/server/tutorial/types').TutorialSection, chapter: import('$lib/server/tutorial/types').Tutorial }
* }>} */
const lookup = new Map();
let width = browser ? window.innerWidth : 1000;
let offset = 0;
data.tutorials_list.forEach((section) => {
section.tutorials.forEach((chapter) => {
const obj = {
slug: chapter.slug,
section,
chapter,
prev
};
lookup.set(chapter.slug, obj);
if (browser) {
// pending https://github.com/sveltejs/svelte/issues/2135
if (prev) prev.next = obj;
prev = obj;
}
});
});
// TODO is there a non-hacky way to trigger scroll when chapter changes?
$: if (scrollable) data.tutorial, scrollable.scrollTo(0, 0);
$: selected = lookup.get(data.slug);
$: improve_link = `https://github.com/sveltejs/svelte/tree/master/documentation/tutorial/${data.tutorial.dir}`;
const clone = (file) => ({
name: file.name.replace(/.\w+$/, ''),
type: file.type,
source: file.content
});
$: if (repl) {
completed = false;
repl.set({
files: data.tutorial.initial.map(clone)
});
}
$: mobile = width < 768;
function reset() {
repl.set({
files: data.tutorial.initial.map(clone)
});
//! BUG: Fix handleChange on REPL side, setting repl.set doesn't trigger it, and repl.update doesn't even work
completed = false;
}
function complete() {
repl.set({
files: data.tutorial.complete.map(clone)
});
completed = true;
}
let completed = false;
/** @param {import('svelte').ComponentEvents<Repl>['change']} event */
function handle_change(event) {
completed = event.detail.files.every((file, i) => {
const expected = data.tutorial.complete[i] && clone(data.tutorial.complete[i]);
return (
expected &&
file.name === expected.name &&
file.type === expected.type &&
file.source.trim().replace(/\s+$/gm, '') === expected.source.trim().replace(/\s+$/gm, '')
);
});
}
</script>
<svelte:head>
<title>{selected.section.title} / {selected.chapter.title} • Svelte Tutorial</title>
<meta name="twitter:title" content="Svelte tutorial" />
<meta name="twitter:description" content="{selected.section.title} / {selected.chapter.title}" />
<meta name="Description" content="{selected.section.title} / {selected.chapter.title}" />
</svelte:head>
<svelte:window bind:innerWidth={width} />
<div class="tutorial-outer">
<div class="viewport offset-{offset}">
<div class="tutorial-text">
<div class="table-of-contents">
<TableOfContents sections={data.tutorials_list} slug={data.slug} {selected} />
</div>
<div class="chapter-markup content" bind:this={scrollable}>
{@html data.tutorial.content}
<div class="controls">
{#if data.tutorial.complete.length}
<!-- TODO disable this button when the contents of the REPL
matches the expected end result -->
<button class="show" on:click={() => (completed ? reset() : complete())}>
{completed ? 'Reset' : 'Show me'}
</button>
{/if}
{#if selected.next}
<a class="next" href="/tutorial/{selected.next.slug}">Next</a>
{/if}
</div>
<div class="improve-chapter">
<a class="no-underline" href={improve_link}>Edit this chapter</a>
</div>
</div>
</div>
<div class="tutorial-repl">
{#if browser}
<Repl
bind:this={repl}
{svelteUrl}
orientation={mobile ? 'columns' : 'rows'}
fixed={mobile}
on:change={handle_change}
injectedJS={mapbox_setup}
relaxed
previewTheme={$theme.current}
/>
{/if}
</div>
</div>
{#if mobile}
<ScreenToggle bind:offset labels={['tutorial', 'input', 'output']} />
{/if}
</div>
<style>
.tutorial-outer {
position: relative;
height: calc(100vh - var(--sk-nav-height) - var(--sk-banner-bottom-height));
overflow: hidden;
padding: 0 0 42px 0;
box-sizing: border-box;
}
.viewport {
display: grid;
width: 300%;
height: 100%;
grid-template-columns: 33.333% 66.666%;
transition: transform 0.3s;
grid-auto-rows: 100%;
}
.offset-1 {
transform: translate(-33.333%, 0);
}
.offset-2 {
transform: translate(-66.666%, 0);
}
@media (min-width: 768px) {
.tutorial-outer {
padding: 0;
}
.viewport {
width: 100%;
height: 100%;
display: grid;
/* TODO */
grid-template-columns: minmax(33.333%, 48rem) auto;
grid-auto-rows: 100%;
transition: none;
}
.offset-1,
.offset-2 {
transform: none;
}
}
.tutorial-text {
display: flex;
flex-direction: column;
height: 100%;
border-right: 1px solid var(--sk-back-4);
background-color: var(--sk-back-3);
color: var(--sk-text-2);
}
.chapter-markup {
padding: 3.2rem 4rem;
overflow: auto;
flex: 1;
height: 0;
}
.chapter-markup :global(h2) {
margin: 4rem 0 1.6rem 0;
font-size: var(--sk-text-m);
line-height: 1;
font-weight: 400;
color: var(--sk-text-2);
}
.chapter-markup :global(h2:first-child) {
margin-top: 0.4rem;
}
.chapter-markup :global(a) {
transition: color 0.2s;
text-decoration: underline;
color: var(--sk-text-2);
}
.chapter-markup :global(a:hover) {
color: var(--sk-text-1);
}
.chapter-markup :global(ul) {
padding: 0 0 0 2em;
}
.chapter-markup :global(blockquote) {
background-color: rgba(0, 0, 0, 0.17);
color: var(--sk-text-2);
}
.chapter-markup::-webkit-scrollbar {
background-color: var(--sk-theme-2);
width: 8px;
}
.chapter-markup::-webkit-scrollbar-thumb {
background-color: var(--sk-scrollbar);
border-radius: 1em;
}
.chapter-markup :global(p) > :global(code),
.chapter-markup :global(ul) :global(code) {
color: var(--sk-code-base);
background: var(--sk-code-bg);
padding: 0.2em 0.4em 0.3em;
white-space: nowrap;
position: relative;
top: -0.1em;
}
.chapter-markup :global(code) {
/* padding: 0.4rem; */
margin: 0 0.2rem;
top: -0.1rem;
background: var(--sk-back-4);
}
.chapter-markup :global(pre) :global(code) {
padding: 0;
margin: 0;
top: 0;
background: transparent;
}
.chapter-markup :global(pre) {
margin: 0 0 2rem 0;
width: 100%;
max-width: var(--sk-line-max-width);
padding: 1rem 1rem;
box-shadow: inset 1px 1px 6px hsla(205.7, 63.6%, 30.8%, 0.06);
border-radius: 0.5rem;
--shiki-color-background: var(--sk-back-1);
}
.controls {
border-top: 1px solid rgba(255, 255, 255, 0.15);
padding: 1em 0 0 0;
display: flex;
align-items: center;
}
.show {
background: var(--sk-theme-1);
padding: 0.3em 0.7em;
border-radius: var(--sk-border-radius);
top: 0.1em;
position: relative;
font-size: var(--sk-text-s);
font-weight: 300;
color: rgba(255, 255, 255, 0.7);
}
.show:hover {
color: white;
}
a.next {
padding-right: 1.2em;
/* TODO */
/* background: no-repeat 100% 50% url(@sveltejs/site-kit/icons/arrow-right.svg); */
background-size: 1em 1em;
margin-left: auto;
}
.improve-chapter {
padding: 1em 0 0.5em 0;
}
.improve-chapter a {
color: var(--sk-text-2);
font-size: 14px;
text-decoration: none;
opacity: 0.6;
padding: 0 0.1em 0 1.2em;
}
.improve-chapter a::before {
content: '';
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 1em;
background: no-repeat 0 50% url(/icons/edit.svg);
background-size: 1em 1em;
}
@media (prefers-color-scheme: light) {
.improve-chapter a::before {
filter: invert(1);
}
}
.improve-chapter a:hover {
opacity: 1;
}
</style>