chore: prepare legacy site deploy (#13987)

This basically removes everything but the home page and docs/tutorial. More specifically:

- add old tutorial to nav
- strips out implementations of repl and blog, these links go to the current blog/repl instead
- removes examples
- adds deprecation banner
- link to current site
pull/13988/head
Simon H 3 months ago committed by GitHub
parent 484588a923
commit 6d4c5cc870
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -5,9 +5,9 @@
"description": "Docs and examples for Svelte",
"type": "module",
"scripts": {
"dev": "node scripts/update.js && pnpm run generate && vite dev",
"build": "node scripts/update.js && pnpm run generate && vite build",
"generate": "node scripts/type-gen/index.js && node scripts/generate_examples.js",
"dev": "node scripts/update.js && vite dev",
"build": "node scripts/update.js && vite build",
"generate": "node scripts/type-gen/index.js",
"update": "node scripts/update.js --force=true",
"preview": "vite preview",
"start": "node build",

@ -1,16 +0,0 @@
import { fileURLToPath } from 'node:url';
import { get_examples_data } from '../src/lib/server/examples/index.js';
import { mkdir, writeFile } from 'node:fs/promises';
const examples_data = await get_examples_data(
fileURLToPath(new URL('../../../documentation/examples', import.meta.url))
);
try {
await mkdir(new URL('../src/lib/generated/', import.meta.url), { recursive: true });
} catch {}
writeFile(
new URL('../src/lib/generated/examples-data.js', import.meta.url),
`export default ${JSON.stringify(examples_data)}`
);

@ -1,84 +0,0 @@
// @ts-check
import { extractFrontmatter } from '@sveltejs/site-kit/markdown';
import { CONTENT_BASE_PATHS } from '../../../constants.js';
import { render_content } from '../renderer.js';
import { get_sections } from '../docs/index.js';
/**
* @param {import('./types').BlogData} blog_data
* @param {string} slug
*/
export async function get_processed_blog_post(blog_data, slug) {
for (const post of blog_data) {
if (post.slug === slug) {
return {
...post,
content: await render_content(post.file, post.content)
};
}
}
return null;
}
const BLOG_NAME_REGEX = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/;
/** @returns {Promise<import('./types').BlogData>} */
export async function get_blog_data(base = CONTENT_BASE_PATHS.BLOG) {
const { readdir, readFile } = await import('node:fs/promises');
/** @type {import('./types').BlogData} */
const blog_posts = [];
for (const file of (await readdir(base)).reverse()) {
if (!BLOG_NAME_REGEX.test(file)) continue;
const { date, date_formatted, slug } = get_date_and_slug(file);
const { metadata, body } = extractFrontmatter(await readFile(`${base}/${file}`, 'utf-8'));
const authors = metadata.author.split(',').map((author) => author.trim());
const authorUrls = metadata.authorURL.split(',').map((author) => author.trim());
blog_posts.push({
date,
date_formatted,
content: body,
description: metadata.description,
draft: metadata.draft === 'true',
slug,
title: metadata.title,
file,
authors: authors.map((author, i) => ({
name: author,
url: authorUrls[i]
})),
sections: await get_sections(body)
});
}
return blog_posts;
}
/** @param {import('./types').BlogData} blog_data */
export function get_blog_list(blog_data) {
return blog_data.map(({ slug, date, title, description, draft }) => ({
slug,
date,
title,
description,
draft
}));
}
/** @param {string} filename */
function get_date_and_slug(filename) {
const match = BLOG_NAME_REGEX.exec(filename);
if (!match) throw new Error(`Invalid filename for blog: '${filename}'`);
const [, date, slug] = match;
const [y, m, d] = date.split('-');
const date_formatted = `${months[+m - 1]} ${+d} ${y}`;
return { date, date_formatted, slug };
}
const months = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ');

@ -1,27 +0,0 @@
import type { Section } from '../docs/types';
export interface BlogPost {
title: string;
description: string;
date: string;
date_formatted: string;
slug: string;
file: string;
authors: {
name: string;
url?: string;
}[];
draft: boolean;
content: string;
sections: Section[];
}
export type BlogData = BlogPost[];
export interface BlogPostSummary {
slug: string;
title: string;
description: string;
date: string;
draft: boolean;
}

@ -1,98 +0,0 @@
import { CONTENT_BASE_PATHS } from '../../../constants.js';
/**
* @param {import('./types').ExamplesData} examples_data
* @param {string} slug
*/
export function get_example(examples_data, slug) {
for (const section of examples_data) {
for (const example of section.examples) {
if (example.slug === slug) {
return example;
}
}
}
return null;
}
/**
* @returns {Promise<import('./types').ExamplesData>}
*/
export async function get_examples_data(base = CONTENT_BASE_PATHS.EXAMPLES) {
const { readdir, stat, readFile } = await import('node:fs/promises');
const examples = [];
for (const subdir of await readdir(base)) {
/** @type {import('./types').ExamplesDatum} */
const section = {
title: '', // Initialise with empty
slug: subdir.split('-').slice(1).join('-'),
examples: []
};
if (!((await stat(`${base}/${subdir}`)).isDirectory() || subdir.endsWith('meta.json')))
continue;
if (!subdir.endsWith('meta.json'))
section.title =
JSON.parse(await readFile(`${base}/${subdir}/meta.json`, 'utf-8')).title ?? 'Embeds';
for (const section_dir of await readdir(`${base}/${subdir}`)) {
const match = /\d{2}-(.+)/.exec(section_dir);
if (!match) continue;
const slug = match[1];
const example_base_dir = `${base}/${subdir}/${section_dir}`;
// Get title for
const example_title = JSON.parse(
await readFile(`${example_base_dir}/meta.json`, 'utf-8')
).title;
/**
* @type {Array<{
* name: string;
* type: string;
* content: string;
* }>}
*/
const files = [];
for (const file of (await readdir(example_base_dir)).filter(
(file) => !file.endsWith('meta.json')
)) {
const type = file.split('.').at(-1);
if (!type) {
throw new Error(`Could not determine type from ${file}`);
}
files.push({
name: file,
type,
content: await readFile(`${example_base_dir}/${file}`, 'utf-8')
});
}
section.examples.push({ title: example_title, slug, files });
}
examples.push(section);
}
return examples;
}
/**
* @param {import('./types').ExamplesData} examples_data
* @returns {import('./types').ExamplesList}
*/
export function get_examples_list(examples_data) {
return examples_data.map((section) => ({
title: section.title,
examples: section.examples.map((example) => ({
title: example.title,
slug: example.slug
}))
}));
}

@ -1,27 +0,0 @@
export interface ExamplesDatum {
title: string;
slug: string;
examples: {
title: string;
slug: string;
files: {
content: string;
type: string;
name: string;
}[];
}[];
}
export type ExamplesData = ExamplesDatum[];
export interface Example {
title: string;
slug: string;
}
export interface ExampleSection {
title: string;
examples: Example[];
}
export type ExamplesList = ExampleSection[];

@ -1,12 +0,0 @@
import * as session from '$lib/db/session';
/** @type {import('@sveltejs/adapter-vercel').Config} */
export const config = {
runtime: 'nodejs18.x' // see https://github.com/sveltejs/svelte/pull/9136
};
export async function load({ request }) {
return {
user: await session.from_cookie(request.headers.get('cookie'))
};
}

@ -1,28 +0,0 @@
<script>
import { invalidateAll } from '$app/navigation';
import { setContext } from 'svelte';
setContext('app', {
login: () => {
const login_window = window.open(
`${window.location.origin}/auth/login`,
'login',
'width=600,height=400'
);
window.addEventListener('message', function handler(event) {
if (event.data.source !== 'svelte-auth') return;
login_window.close();
window.removeEventListener('message', handler);
invalidateAll();
});
},
logout: async () => {
const r = await fetch(`/auth/logout`);
if (r.ok) invalidateAll();
}
});
</script>
<slot />

@ -1,20 +0,0 @@
import * as gist from '$lib/db/gist';
export async function load({ url, parent }) {
let gists = [];
let next = null;
const search = url.searchParams.get('search');
const { user } = await parent();
if (user) {
const offset_param = url.searchParams.get('offset');
const offset = offset_param ? parseInt(offset_param) : 0;
const search = url.searchParams.get('search');
({ gists, next } = await gist.list(user, { offset, search }));
}
return { user, gists, next, search };
}

@ -1,328 +0,0 @@
<script>
import { getContext } from 'svelte';
import { Icon } from '@sveltejs/site-kit/components';
import { ago } from '$lib/time';
import { goto, invalidateAll } from '$app/navigation';
export let data;
const { login, logout } = getContext('app');
const format = /** @param {string} str */ (str) => ago(new Date(str));
let destroying = false;
async function destroy_selected() {
const confirmed = confirm(
`Are you sure you want to delete ${selected.length} ${
selected.length === 1 ? 'app' : 'apps'
}?`
);
if (!confirmed) return;
destroying = true;
const res = await fetch(`/apps/destroy`, {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
ids: selected
})
});
if (res.ok) {
selected = [];
await invalidateAll();
// this is a temporary fix because invalidation only works once
// TODO raise an issue
// location.reload();
} else {
alert('Deletion failed');
}
destroying = false;
}
let selected = [];
$: selecting = selected.length > 0;
</script>
<svelte:head>
<title>Your apps • Svelte</title>
</svelte:head>
<div class="apps">
{#if data.user}
<header>
<h1>Your apps</h1>
<div class="user">
<img
class="avatar"
alt="{data.user.github_name || data.user.github_login} avatar"
src={data.user.github_avatar_url}
/>
<span>
{data.user.github_name || data.user.github_login}
(<a on:click|preventDefault={logout} href="/auth/logout">log out</a>)
</span>
</div>
</header>
<div class="controls">
{#if selected.length > 0}
<button class="delete" on:click={() => destroy_selected()} disabled={destroying}>
<Icon name="delete" />
Delete {selected.length}
{selected.length === 1 ? 'app' : 'apps'}
</button>
<button on:click={() => (selected = [])}>Clear selection</button>
{:else}
<form
on:submit|preventDefault={(e) => {
const search = new FormData(/** @type {HTMLFormElement} */ (e.target)).get('search');
goto(search ? `/apps?search=${encodeURIComponent(search.toString())}` : '/apps');
}}
>
<input
type="search"
placeholder="Search"
aria-label="Search"
name="search"
value={data.search}
/>
</form>
{/if}
</div>
{#if data.gists.length > 0}
<ul class:selecting>
{#each data.gists as gist}
<li class:selected={selected.includes(gist.id)}>
<a href={selecting ? undefined : `/repl/${gist.id}`}>
<h2>{gist.name}</h2>
<span>updated {format(gist.updated_at || gist.created_at)}</span>
</a>
<label>
<input
aria-label="Select for delection"
type="checkbox"
bind:group={selected}
value={gist.id}
/>
</label>
</li>
{/each}
</ul>
<div class="pagination">
<!-- TODO more sophisticated pagination -->
{#if data.next !== null && !selecting}
<a
href="/apps?offset={data.next}{data.search
? `&search=${encodeURIComponent(data.search)}`
: ''}">Next page...</a
>
{/if}
</div>
{:else}
<p>No apps here. <a href="/repl">Go make one!</a></p>
{/if}
{:else}
<p>
Please <a on:click|preventDefault={login} href="/auth/login">log in</a> to see your saved apps.
</p>
{/if}
</div>
<style>
.apps {
padding: var(--sk-page-padding-top) var(--sk-page-padding-side) 6rem var(--sk-page-padding-side);
max-width: var(--sk-page-main-width);
margin: 0 auto;
}
header {
margin: 0 0 1em 0;
}
h1 {
font-size: 4rem;
font-weight: 400;
}
.user {
display: flex;
padding: 0 0 0 3.2rem;
position: relative;
margin: 1rem 0;
color: var(--sk-text-2);
}
.avatar {
position: absolute;
left: 0;
top: 0.1rem;
width: 2.4rem;
height: 2.4rem;
border: 1px solid rgba(0, 0, 0, 0.3);
border-radius: 0.2rem;
}
.controls {
position: sticky;
top: 1rem;
display: flex;
align-items: center;
width: 100%;
height: 4rem;
margin: 0 0 2rem 0;
font-size: 1.6rem;
z-index: 2;
justify-content: space-between;
outline: 1rem solid var(--sk-back-1);
}
.controls::after {
content: '';
position: absolute;
width: 100%;
bottom: -2rem;
height: 2rem;
background: linear-gradient(to bottom, var(--sk-back-1) 0%, var(--sk-back-1) 50%, transparent);
}
.controls form {
width: 100%;
height: 100%;
}
.controls input,
.controls button {
font-family: inherit;
font-size: inherit;
}
.controls input[type='search'] {
position: relative;
width: 100%;
height: 100%;
padding: 0.5rem 1rem;
line-height: 1;
display: flex;
border: 1px solid var(--sk-back-5);
border-radius: var(--sk-border-radius);
z-index: 2;
}
.controls button {
display: flex;
gap: 1rem;
padding: 0 1rem;
height: 100%;
border-radius: var(--sk-border-radius);
align-items: center;
}
.delete {
background-color: #da106e;
color: white;
}
ul {
list-style: none;
display: grid;
grid-gap: 1rem;
}
li {
position: relative;
overflow: hidden;
}
h2 {
color: var(--sk-text-2);
font-size: var(--sk-text-s);
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
}
li a {
display: block;
background: var(--sk-back-3);
padding: 1rem 3rem 1rem 1rem;
height: 100%;
line-height: 1;
border-radius: var(--sk-border-radius);
text-decoration: none;
}
li span {
font-size: 12px;
color: var(--sk-text-3);
}
li label {
position: absolute;
right: 0;
top: 0;
padding: 1rem;
}
li input {
display: block;
opacity: 0.2;
}
ul:not(.selecting) li:hover a {
background-color: var(--sk-theme-2);
color: white;
}
ul:not(.selecting) li:hover h2,
ul:not(.selecting) li:hover span {
color: white;
}
ul:not(.selecting) li:hover input {
opacity: 1;
}
li.selected {
filter: drop-shadow(1px 2px 4px hsla(205.7, 63.6%, 30.8%, 0.1));
}
li.selected input {
opacity: 1;
}
.selecting li:not(.selected) {
opacity: 0.4;
}
.selecting li:not(.selected):hover,
.selecting li:not(.selected):focus-within {
opacity: 1;
}
.pagination {
height: 4rem;
}
@media (min-width: 540px) {
ul {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 720px) {
ul {
grid-template-columns: repeat(3, 1fr);
}
}
</style>

@ -1,12 +0,0 @@
import * as session from '$lib/db/session';
import * as gist from '$lib/db/gist';
export async function POST({ request }) {
const user = await session.from_cookie(request.headers.get('cookie'));
if (!user) return new Response(undefined, { status: 401 });
const body = await request.json();
await gist.destroy(user.id, body.ids);
return new Response(undefined, { status: 204 });
}

@ -1,21 +0,0 @@
import { redirect } from '@sveltejs/kit';
export function load({ url }) {
const query = url.searchParams;
const gist = query.get('gist');
const example = query.get('example');
const version = query.get('version');
const vim = query.get('vim');
// redirect to v2 REPL if appropriate
if (version && /^[^>]?[12]/.test(version)) {
redirect(302, `https://v2.svelte.dev/repl?${query}`);
}
const id = gist || example || 'hello-world';
// we need to filter out null values
const q = new URLSearchParams();
if (version) q.set('version', version);
if (vim) q.set('vim', vim);
redirect(301, `/repl/${id}?${q}`);
}

@ -1,18 +0,0 @@
import { browser } from '$app/environment';
export function load({ data, url }) {
// initialize vim with the search param
const vim_search_params = url.searchParams.get('vim');
let vim = vim_search_params !== null && vim_search_params !== 'false';
// when in the browser check if there's a local storage entry and eventually override
// vim if there's not a search params otherwise update the local storage
if (browser) {
const vim_local_storage = window.localStorage.getItem('svelte:vim-enabled');
if (vim_search_params !== null) {
window.localStorage.setItem('svelte:vim-enabled', vim.toString());
} else if (vim_local_storage) {
vim = vim_local_storage !== 'false';
}
}
return { ...data, vim };
}

@ -1,16 +0,0 @@
import { error } from '@sveltejs/kit';
export async function load({ fetch, params, url }) {
const res = await fetch(`/repl/api/${params.id}.json`);
if (!res.ok) {
error(/** @type {import('@sveltejs/kit').NumericRange<400, 599>} */(res.status));
}
const gist = await res.json();
return {
gist,
version: url.searchParams.get('version') || '4'
};
}

@ -1,151 +0,0 @@
<script>
import { browser } from '$app/environment';
import { afterNavigate, goto, replaceState } from '$app/navigation';
import Repl from '@sveltejs/repl';
import { theme } from '@sveltejs/site-kit/stores';
import { onMount } from 'svelte';
import { mapbox_setup } from '../../../../config.js';
import AppControls from './AppControls.svelte';
export let data;
let version = data.version;
/** @type {import('@sveltejs/repl').default} */
let repl;
let name = data.gist.name;
let zen_mode = false;
let modified_count = 0;
function update_query_string(version) {
const params = [];
if (version !== 'latest') params.push(`version=${version}`);
const url =
params.length > 0 ? `/repl/${data.gist.id}?${params.join('&')}` : `/repl/${data.gist.id}`;
history.replaceState({}, 'x', url);
}
$: if (typeof history !== 'undefined') update_query_string(version);
onMount(() => {
if (data.version !== 'local') {
fetch(`https://unpkg.com/svelte@${data.version || '4'}/package.json`)
.then((r) => r.json())
.then((pkg) => {
version = pkg.version;
});
}
});
afterNavigate(() => {
repl?.set({
files: data.gist.components
});
});
function handle_fork(event) {
console.log('> handle_fork', event);
goto(`/repl/${event.detail.gist.id}?version=${version}`);
}
function handle_change(event) {
modified_count = event.detail.files.filter((c) => c.modified).length;
}
$: svelteUrl =
browser && version === 'local'
? `${location.origin}/repl/local`
: `https://unpkg.com/svelte@${version}`;
$: relaxed = data.gist.relaxed || (data.user && data.user.id === data.gist.owner);
$: vim = data.vim;
</script>
<svelte:head>
<title>{name} • REPL • Svelte</title>
<meta name="twitter:title" content="{data.gist.name} • REPL • Svelte" />
<meta name="twitter:description" content="Cybernetically enhanced web apps" />
<meta name="Description" content="Interactive Svelte playground" />
</svelte:head>
<div class="repl-outer {zen_mode ? 'zen-mode' : ''}">
<AppControls
user={data.user}
gist={data.gist}
{repl}
bind:name
bind:zen_mode
bind:modified_count
on:forked={handle_fork}
/>
{#if browser}
<Repl
bind:this={repl}
{svelteUrl}
{relaxed}
{vim}
injectedJS={mapbox_setup}
showModified
showAst
on:change={handle_change}
on:add={handle_change}
on:remove={handle_change}
previewTheme={$theme.current}
/>
{/if}
</div>
<style>
.repl-outer {
position: relative;
height: calc(100% - var(--sk-nav-height) - var(--sk-banner-bottom-height));
height: calc(100dvh - var(--sk-nav-height) - var(--sk-banner-bottom-height));
--app-controls-h: 5.6rem;
--pane-controls-h: 4.2rem;
overflow: hidden;
background-color: var(--sk-back-1);
padding: var(--app-controls-h) 0 0 0;
/* margin: 0 calc(var(--side-nav) * -1); */
box-sizing: border-box;
display: flex;
flex-direction: column;
}
/* temp fix for #2499 and #2550 while waiting for a fix for https://github.com/sveltejs/svelte-repl/issues/8 */
.repl-outer :global(.tab-content),
.repl-outer :global(.tab-content.visible) {
pointer-events: all;
opacity: 1;
}
.repl-outer :global(.tab-content) {
visibility: hidden;
}
.repl-outer :global(.tab-content.visible) {
visibility: visible;
z-index: 1;
}
.zen-mode {
position: fixed;
width: 100%;
height: 100%;
top: 0;
z-index: 111;
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
</style>

@ -1,339 +0,0 @@
<script>
import { createEventDispatcher, getContext } from 'svelte';
import UserMenu from './UserMenu.svelte';
import { Icon } from '@sveltejs/site-kit/components';
import * as doNotZip from 'do-not-zip';
import downloadBlob from './downloadBlob.js';
import { enter } from '$lib/utils/events.js';
import { isMac } from '$lib/utils/compat.js';
const dispatch = createEventDispatcher();
const { login } = getContext('app');
export let user;
/** @type {import('@sveltejs/repl').default} */
export let repl;
export let gist;
export let name;
export let zen_mode;
export let modified_count;
let saving = false;
let downloading = false;
let justSaved = false;
let justForked = false;
function wait(ms) {
return new Promise((f) => setTimeout(f, ms));
}
$: canSave = user && gist && gist.owner === user.id;
function handleKeydown(event) {
if (event.key === 's' && (isMac ? event.metaKey : event.ctrlKey)) {
event.preventDefault();
save();
}
}
async function fork(intentWasSave) {
saving = true;
const { files } = repl.toJSON();
try {
const r = await fetch(`/repl/create.json`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name,
files: files.map((file) => ({
name: `${file.name}.${file.type}`,
source: file.source
}))
})
});
if (r.status < 200 || r.status >= 300) {
const { error } = await r.json();
throw new Error(`Received an HTTP ${r.status} response: ${error}`);
}
const gist = await r.json();
dispatch('forked', { gist });
modified_count = 0;
repl.markSaved();
if (intentWasSave) {
justSaved = true;
await wait(600);
justSaved = false;
} else {
justForked = true;
await wait(600);
justForked = false;
}
} catch (err) {
if (navigator.onLine) {
alert(err.message);
} else {
alert(`It looks like you're offline! Find the internet and try again`);
}
}
saving = false;
}
async function save() {
if (!user) {
alert('Please log in before saving your app');
return;
}
if (saving) return;
if (!canSave) {
fork(true);
return;
}
saving = true;
try {
// Send all files back to API
// ~> Any missing files are considered deleted!
const { files } = repl.toJSON();
const r = await fetch(`/repl/save/${gist.id}.json`, {
method: 'PUT',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name,
files: files.map((file) => ({
name: `${file.name}.${file.type}`,
source: file.source
}))
})
});
if (r.status < 200 || r.status >= 300) {
const { error } = await r.json();
throw new Error(`Received an HTTP ${r.status} response: ${error}`);
}
modified_count = 0;
repl.markSaved();
justSaved = true;
await wait(600);
justSaved = false;
} catch (err) {
if (navigator.onLine) {
alert(err.message);
} else {
alert(`It looks like you're offline! Find the internet and try again`);
}
}
saving = false;
}
async function download() {
downloading = true;
const { files: components, imports } = repl.toJSON();
const files = await (await fetch('/svelte-app.json')).json();
if (imports.length > 0) {
const idx = files.findIndex(({ path }) => path === 'package.json');
const pkg = JSON.parse(files[idx].data);
const { devDependencies } = pkg;
imports.forEach((mod) => {
const match = /^(@[^/]+\/)?[^@/]+/.exec(mod);
devDependencies[match[0]] = 'latest';
});
pkg.devDependencies = devDependencies;
files[idx].data = JSON.stringify(pkg, null, ' ');
}
files.push(
...components.map((component) => ({
path: `src/${component.name}.${component.type}`,
data: component.source
}))
);
files.push({
path: `src/main.js`,
data: `import App from './App.svelte';
var app = new App({
target: document.body
});
export default app;`
});
downloadBlob(doNotZip.toBlob(files), 'svelte-app.zip');
downloading = false;
}
</script>
<svelte:window on:keydown={handleKeydown} />
<div class="app-controls">
<input
bind:value={name}
on:focus={(e) => e.target.select()}
use:enter={(e) => /** @type {HTMLInputElement} */ (e.target).blur()}
/>
<div class="buttons">
<button class="icon" on:click={() => (zen_mode = !zen_mode)} title="fullscreen editor">
{#if zen_mode}
<Icon name="close" />
{:else}
<Icon name="maximize" />
{/if}
</button>
<button class="icon" disabled={downloading} on:click={download} title="download zip file">
<Icon name="download" />
</button>
<button class="icon" disabled={saving || !user} on:click={() => fork(false)} title="fork">
{#if justForked}
<Icon name="check" />
{:else}
<Icon name="git-branch" />
{/if}
</button>
<button class="icon" disabled={saving || !user} on:click={save} title="save">
{#if justSaved}
<Icon name="check" />
{:else}
<Icon name="save" />
{#if modified_count}
<div class="badge">{modified_count}</div>
{/if}
{/if}
</button>
{#if user}
<UserMenu {user} />
{:else}
<button class="icon" on:click|preventDefault={login}>
<Icon name="log-in" />
<span>&nbsp;Log in to save</span>
</button>
{/if}
</div>
</div>
<style>
.app-controls {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: var(--app-controls-h);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.6rem var(--sk-page-padding-side);
background-color: var(--sk-back-4);
color: var(--sk-text-1);
white-space: nowrap;
flex: 0;
}
.buttons {
text-align: right;
margin-right: 0.4rem;
display: flex;
align-items: center;
gap: 0.2em;
}
.icon {
transform: translateY(0.1rem);
display: inline-block;
padding: 0.2em;
opacity: 0.7;
transition: opacity 0.3s;
font-family: var(--sk-font);
font-size: 1.6rem;
color: var(--sk-text-1);
line-height: 1;
}
.icon:hover,
.icon:focus-visible {
opacity: 1;
}
.icon:disabled {
opacity: 0.3;
}
.icon[title^='fullscreen'] {
display: none;
}
input {
background: transparent;
border: none;
color: currentColor;
font-family: var(--sk-font);
font-size: 1.6rem;
opacity: 0.7;
outline: none;
flex: 1;
margin: 0 0.2em 0 0.4rem;
padding-top: 0.2em;
border-bottom: 1px solid transparent;
}
input:hover {
border-bottom: 1px solid currentColor;
opacity: 1;
}
input:focus {
border-bottom: 1px solid currentColor;
opacity: 1;
}
button span {
display: none;
}
.badge {
background: #ff3e00;
border-radius: 100%;
font-size: 10px;
padding: 0;
width: 15px;
height: 15px;
line-height: 15px;
position: absolute;
top: 10px;
right: 0px;
}
@media (min-width: 600px) {
.icon[title^='fullscreen'] {
display: inline;
}
button span {
display: inline-block;
}
}
</style>

@ -1,134 +0,0 @@
<script>
import { getContext } from 'svelte';
import { Icon } from '@sveltejs/site-kit/components';
import { click_outside, focus_outside } from '@sveltejs/site-kit/actions';
const { logout } = getContext('app');
export let user;
let showMenu = false;
let name;
$: name = user.github_name || user.github_login;
</script>
<div
class="user"
use:focus_outside={() => (showMenu = false)}
use:click_outside={() => (showMenu = false)}
>
<button
on:click={() => (showMenu = !showMenu)}
aria-expanded={showMenu}
class="trigger"
aria-label={name}
>
<span class="name">{name}</span>
<img alt="" src={user.github_avatar_url} />
<Icon name={showMenu ? 'chevron-up' : 'chevron-down'} />
</button>
{#if showMenu}
<div class="menu">
<a href="/apps">Your saved apps</a>
<button on:click={logout}>Log out</button>
</div>
{/if}
</div>
<style>
.user {
position: relative;
display: inline-block;
padding: 0em 0 0 0.3rem;
z-index: 99;
}
.trigger {
display: flex;
align-items: center;
gap: 0.75rem;
outline-offset: 2px;
transform: translateY(0.1rem);
--opacity: 0.7;
}
.trigger:hover,
.trigger:focus-visible,
.trigger[aria-expanded='true'] {
--opacity: 1;
}
.name {
line-height: 1;
display: none;
font-family: var(--sk-font);
font-size: 1.6rem;
}
.name,
.trigger :global(.icon) {
display: none;
opacity: var(--opacity);
}
img {
width: 2.1rem;
height: 2.1rem;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 0.2rem;
transform: translateY(-0.1rem);
}
.menu {
position: absolute;
width: calc(100% + 1.6rem);
min-width: 10em;
top: 3rem;
right: -1.6rem;
background-color: var(--sk-back-2);
padding: 0.8rem 1.6rem;
z-index: 99;
text-align: left;
border-radius: 0.4rem;
display: flex;
flex-direction: column;
}
.menu button,
.menu a {
background-color: transparent;
font-family: var(--sk-font);
font-size: 1.6rem;
opacity: 0.7;
padding: 0.4rem 0;
text-decoration: none;
text-align: left;
border: none;
color: var(--sk-text-2);
}
.menu button:hover,
.menu button:focus-visible,
.menu a:hover,
.menu a:focus-visible {
opacity: 1;
color: inherit;
}
@media (min-width: 600px) {
.user {
padding: 0em 0 0 1.6rem;
}
img {
width: 2.4rem;
height: 2.4rem;
}
.name,
.trigger :global(.icon) {
display: inline-block;
}
}
</style>

@ -1,15 +0,0 @@
/**
* @param {Blob} blob
* @param {string} filename
*/
export default (blob, filename) => {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
URL.revokeObjectURL(url);
link.remove();
};

@ -1,16 +0,0 @@
import { error } from '@sveltejs/kit';
export async function load({ fetch, params, url }) {
const res = await fetch(`/repl/api/${params.id}.json`);
if (!res.ok) {
throw error(/** @type {any} */ (res.status));
}
const gist = await res.json();
return {
gist,
version: url.searchParams.get('version') || '4'
};
}

@ -1,92 +0,0 @@
<script>
import { browser } from '$app/environment';
import { afterNavigate } from '$app/navigation';
import { theme } from '@sveltejs/site-kit/stores';
import Repl from '@sveltejs/repl';
import { mapbox_setup } from '../../../../../config.js';
import { onMount } from 'svelte';
export let data;
let version = data.version;
/** @type {import('@sveltejs/repl').default} */
let repl;
function update_query_string(version) {
const params = [];
if (version !== 'latest') params.push(`version=${version}`);
const url =
params.length > 0
? `/repl/${data.gist.id}/embed?${params.join('&')}`
: `/repl/${data.gist.id}/embed`;
history.replaceState({}, 'x', url);
}
$: if (typeof history !== 'undefined') update_query_string(version);
onMount(() => {
if (data.version !== 'local') {
fetch(`https://unpkg.com/svelte@${data.version || '4'}/package.json`)
.then((r) => r.json())
.then((pkg) => {
version = pkg.version;
});
}
});
afterNavigate(() => {
repl?.set({
files: data.gist.components
});
});
$: svelteUrl =
browser && version === 'local'
? `${location.origin}/repl/local`
: `https://unpkg.com/svelte@${version}`;
$: relaxed = data.gist.relaxed || (data.user && data.user.id === data.gist.owner);
</script>
<svelte:head>
<title>{data.gist.name} • REPL • Svelte</title>
<meta name="twitter:title" content="{data.gist.name} • REPL • Svelte" />
<meta name="twitter:description" content="Cybernetically enhanced web apps" />
<meta name="Description" content="Interactive Svelte playground" />
</svelte:head>
<div class="repl-outer">
{#if browser}
<Repl
bind:this={repl}
{svelteUrl}
{relaxed}
injectedJS={mapbox_setup}
showModified
showAst
previewTheme={$theme.current}
embedded
/>
{/if}
</div>
<style>
.repl-outer {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--sk-back-1);
overflow: hidden;
box-sizing: border-box;
--pane-controls-h: 4.2rem;
display: flex;
flex-direction: column;
}
</style>

@ -1,93 +0,0 @@
import { dev } from '$app/environment';
import { client } from '$lib/db/client.js';
import * as gist from '$lib/db/gist.js';
import examples_data from '$lib/generated/examples-data.js';
import { get_example, get_examples_list } from '$lib/server/examples/index.js';
import { error, json } from '@sveltejs/kit';
export const prerender = 'auto';
const UUID_REGEX = /^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/;
/** @param {import('$lib/server/examples/types').ExamplesData[number]['examples'][number]['files'][number][]} files */
function munge(files) {
return files
.map((file) => {
const dot = file.name.lastIndexOf('.');
let name = file.name.slice(0, dot);
let type = file.name.slice(dot + 1);
if (type === 'html') type = 'svelte';
// @ts-expect-error what is file.source? by @PuruVJ
return { name, type, source: file.source ?? file.content ?? '' };
})
.sort((a, b) => {
if (a.name === 'App' && a.type === 'svelte') return -1;
if (b.name === 'App' && b.type === 'svelte') return 1;
if (a.type !== b.type) return a.type === 'svelte' ? -1 : 1;
return a.name < b.name ? -1 : 1;
});
}
export async function GET({ params }) {
// Currently, these pages(that are in examples/) are prerendered. To avoid making any FS requests,
// We prerender examples pages during build time. That means, when something like `/repl/hello-world.json`
// is accessed, this function won't be run at all, as it will be served from the filesystem
const example = get_example(examples_data, params.id);
if (example) {
return json({
id: params.id,
name: example.title,
owner: null,
relaxed: false, // TODO is this right? EDIT: It was example.relaxed before, which no example return to my knowledge. By @PuruVJ
components: munge(example.files)
});
}
if (dev && !client) {
// in dev with no local Supabase configured, proxy to production
// this lets us at least load saved REPLs
const res = await fetch(`https://svelte.dev/repl/api/${params.id}.json`);
// returning the response directly results in a bizarre
// content encoding error, so we create a new one
return new Response(await res.text(), {
status: res.status,
headers: {
'content-type': 'application/json'
}
});
}
if (!UUID_REGEX.test(params.id)) {
error(404);
}
const app = await gist.read(params.id);
if (!app) {
error(404, 'not found');
}
return json({
id: params.id,
name: app.name,
// @ts-ignore
owner: app.userid,
relaxed: false,
// @ts-expect-error app.files has a `source` property
components: munge(app.files)
});
}
export async function entries() {
const { get_examples_list } = await import('$lib/server/examples/index.js');
return get_examples_list(examples_data)
.map(({ examples }) => examples)
.flatMap((val) => val.map(({ slug }) => ({ id: slug })));
}

@ -1,18 +0,0 @@
import * as gist from '$lib/db/gist';
import * as session from '$lib/db/session';
import { error, json } from '@sveltejs/kit';
export async function POST({ request }) {
const user = await session.from_cookie(request.headers.get('cookie'));
if (!user) error(401);
const body = await request.json();
const result = await gist.create(user, body);
// normalize id
result.id = result.id.replace(/-/g, '');
return json(result, {
status: 201
});
}

@ -1,14 +0,0 @@
import { redirect } from '@sveltejs/kit';
export function load({ url }) {
if (!url.searchParams.has('gist')) {
throw redirect(301, '/repl/hello-world/embed');
} else {
const searchParamsWithoutGist = new URLSearchParams(url.searchParams);
searchParamsWithoutGist.delete('gist');
throw redirect(
301,
`/repl/${url.searchParams.get('gist')}/embed?${searchParamsWithoutGist.toString()}`
);
}
}

@ -1,15 +0,0 @@
import { env } from '$env/dynamic/private';
const local_svelte_path = env.LOCAL_SVELTE_PATH || '../../../svelte';
export async function GET({ params: { path } }) {
if (import.meta.env.PROD || ('/' + path).includes('/.')) {
return new Response(undefined, { status: 403 });
}
const { readFile } = await import('node:fs/promises');
return new Response(await readFile(`${local_svelte_path}/${path}`), {
headers: { 'Content-Type': 'text/javascript' }
});
}

@ -1,14 +0,0 @@
import * as gist from '$lib/db/gist';
import * as session from '$lib/db/session';
import { error } from '@sveltejs/kit';
// TODO reimplement as an action
export async function PUT({ params, request }) {
const user = await session.from_cookie(request.headers.get('cookie'));
if (!user) error(401, 'Unauthorized');
const body = await request.json();
await gist.update(user, params.id, body);
return new Response(undefined, { status: 204 });
}

@ -2,7 +2,7 @@
import '@sveltejs/site-kit/styles/index.css';
import { injectSpeedInsights } from '@vercel/speed-insights/sveltekit';
import { inject } from '@vercel/analytics'
import { inject } from '@vercel/analytics';
import { browser } from '$app/environment';
import { page } from '$app/stores';
import { Icon, Shell, Banners } from '@sveltejs/site-kit/components';
@ -58,9 +58,7 @@
</svelte:fragment>
<svelte:fragment slot="external-links">
<a href="https://learn.svelte.dev/">Tutorial</a>
<a href="https://kit.svelte.dev">SvelteKit</a>
<a href="https://svelte.dev">Current version</a>
<Separator />

@ -9,29 +9,15 @@ export const GET = async () => {
return json(
defineBanner([
{
id: 'advent2023',
id: 'deprecated',
start: new Date('1 Dec, 2023 00:00:00 UTC'),
end: new Date('24 Dec, 2023 23:59:59 UTC'),
end: new Date('24 Dec, 2050 23:59:59 UTC'),
arrow: true,
content: {
lg: 'Advent of Svelte 2023 is here!',
sm: 'Advent of Svelte'
lg: 'This documentation is for Svelte 3 and 4. Go to the latest docs.',
sm: 'These docs are for Svelte 3/4'
},
href: 'https://advent.sveltesociety.dev/'
},
// This one skips the blog post and just changes the link
{
id: 'advent2023-finished',
start: new Date('25 Dec, 2023 00:00:00 UTC'),
end: new Date('1 Jan, 2024 00:00:00 UTC'),
arrow: true,
content: {
lg: 'Advent of Svelte 2023 is over. See you next year!',
sm: 'Advent of Svelte 2023 is over!'
},
href: 'https://advent.sveltesociety.dev/'
// scope: ['svelte.dev, kit.svelte.dev'] // Dont show on learn.svelte.dev by not adding it to the array
href: 'https://svelte.dev/'
}
])
);

@ -1,9 +0,0 @@
import { get_blog_data, get_blog_list } from '$lib/server/blog/index.js';
export const prerender = true;
export async function load() {
return {
posts: get_blog_list(await get_blog_data())
};
}

@ -1,91 +0,0 @@
<script>
export let data;
</script>
<svelte:head>
<title>Blog • Svelte</title>
<link
rel="alternate"
type="application/rss+xml"
title="Svelte blog"
href="https://svelte.dev/blog/rss.xml"
/>
<meta name="twitter:title" content="Svelte blog" />
<meta name="twitter:description" content="Articles about Svelte and UI development" />
<meta name="Description" content="Articles about Svelte and UI development" />
</svelte:head>
<h1 class="visually-hidden">Blog</h1>
<div class="posts stretch">
{#each data.posts as post}
{#if !post.draft}
<article class="post" data-pubdate={post.date}>
<a class="no-underline" href="/blog/{post.slug}" title="Read the article »">
<h2>{post.title}</h2>
<p>{post.description}</p>
</a>
</article>
{/if}
{/each}
</div>
<style>
.posts {
grid-template-columns: 1fr 1fr;
grid-gap: 1em;
min-height: calc(100vh - var(--sk-nav-height) - var(--sk-banner-bottom-height));
padding: var(--sk-page-padding-top) var(--sk-page-padding-side) 6rem var(--sk-page-padding-side);
max-width: var(--sk-page-main-width);
margin: 0 auto;
}
h2 {
display: inline-block;
margin: 3.2rem 0 0.4rem 0;
color: var(--sk-text-2);
max-width: 18em;
font-size: var(--sk-text-m);
font-weight: 400;
}
.post:first-child {
margin: 0 0 2rem 0;
padding: 0 0 4rem 0;
border-bottom: var(--sk-thick-border-width) solid #6767785b; /* based on --second */
}
.post:first-child h2 {
font-size: 4rem;
font-weight: 400;
color: var(--sk-text-2);
}
.post:where(:first-child, :nth-child(2))::before {
content: 'Latest post • ' attr(data-pubdate);
color: var(--sk-theme-3);
font-size: var(--sk-text-xs);
font-weight: 400;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.post:nth-child(2)::before {
content: 'Older posts';
}
.post p {
font-size: var(--sk-text-s);
max-width: 30em;
color: var(--sk-text-3);
}
.post > a {
display: block;
}
.posts a:hover,
.posts a:hover > h2 {
color: var(--sk-theme-3);
}
</style>

@ -1,18 +0,0 @@
import { get_blog_data, get_processed_blog_post } from '$lib/server/blog/index.js';
import { error } from '@sveltejs/kit';
export const prerender = true;
export async function load({ params }) {
const post = await get_processed_blog_post(await get_blog_data(), params.slug);
if (!post) error(404);
// forgive me — terrible hack necessary to get diffs looking sensible
// on the `runes` blog post
post.content = post.content.replace(/( )+/gm, (match) => ' '.repeat(match.length / 4));
return {
post
};
}

@ -1,137 +0,0 @@
<script>
import { page } from '$app/stores';
import { copy_code_descendants } from '@sveltejs/site-kit/actions';
import { DocsOnThisPage, setupDocsHovers } from '@sveltejs/site-kit/docs';
export let data;
setupDocsHovers();
</script>
<svelte:head>
<title>{data.post.title}</title>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={data.post.title} />
<meta name="twitter:description" content={data.post.description} />
<meta name="Description" content={data.post.description} />
<meta name="twitter:image" content="https://svelte.dev/blog/{$page.params.slug}/card.png" />
<meta name="og:image" content="https://svelte.dev/blog/{$page.params.slug}/card.png" />
</svelte:head>
<div class="content">
<article class="post listify text" use:copy_code_descendants>
<h1>{data.post.title}</h1>
<p class="standfirst">{data.post.description}</p>
<p class="byline">
{#each data.post.authors as author, i}
{@const show_comma = data.post.authors.length > 2 && i < data.post.authors.length - 1}
{@const show_and = i === data.post.authors.length - 2}
<svelte:element this={author.url ? 'a' : 'span'} href={author.url}
>{author.name}</svelte:element
>{#if show_comma},&nbsp;{/if}
{#if show_and}and&nbsp;{/if}
{/each}
<time datetime={data.post.date}>{data.post.date_formatted}</time>
</p>
<DocsOnThisPage
details={{
content: '',
file: '',
path: `/blog/${data.post.slug}`,
sections: data.post.sections,
slug: data.post.slug,
title: data.post.title
}}
orientation="inline"
/>
{@html data.post.content}
</article>
</div>
<style>
.post {
padding: var(--sk-page-padding-top) var(--sk-page-padding-side) 6rem var(--sk-page-padding-side);
max-width: var(--sk-page-main-width);
margin: 0 auto;
}
h1 {
font-size: 4rem;
font-weight: 400;
}
.standfirst {
font-size: var(--sk-text-s);
color: var(--sk-text-3);
margin: 0 0 1em 0;
}
.byline {
margin: 0 0 1rem 0;
padding: 1.6rem 0 0 0;
border-top: var(--sk-thick-border-width) solid #6767785b;
font-size: var(--sk-text-xs);
text-transform: uppercase;
}
.post :global(figure) {
margin: 1.6rem 0 3.2rem 0;
}
.post :global(figure) :global(img) {
max-width: 100%;
}
.post :global(figcaption) {
color: var(--sk-theme-2);
text-align: left;
}
.post :global(video) {
width: 100%;
}
.post :global(aside) {
float: right;
margin: 0 0 1em 1em;
width: 16rem;
color: var(--sk-theme-2);
z-index: 2;
}
.post :global(.max) {
width: 100%;
}
.post :global(iframe) {
width: 100%;
height: 420px;
margin: 2em 0;
border-radius: var(--sk-border-radius);
border: 0.8rem solid var(--sk-theme-2);
}
@media (min-width: 910px) {
.post :global(.max) {
width: calc(100vw - 2 * var(--sk-page-padding-side));
margin: 0 calc(var(--sk-page-main-width) / 2 - 50vw);
text-align: center;
}
.post :global(.max) > :global(*) {
width: 100%;
max-width: 1200px;
}
.post :global(iframe) {
width: 100%;
max-width: 1100px;
margin: 2em auto;
}
}
</style>

@ -1,51 +0,0 @@
import { get_blog_data, get_processed_blog_post } from '$lib/server/blog/index.js';
import { Resvg } from '@resvg/resvg-js';
import { error } from '@sveltejs/kit';
import satori from 'satori';
import { html as toReactNode } from 'satori-html';
import Card from './Card.svelte';
import OverpassRegular from './Overpass-Regular.ttf';
const height = 630;
const width = 1200;
export const prerender = true;
export async function GET({ params }) {
const post = await get_processed_blog_post(await get_blog_data(), params.slug);
if (!post) error(404);
// @ts-ignore
const result = Card.render({ post });
const element = toReactNode(`${result.html}<style>${result.css.code}</style>`);
const svg = await satori(element, {
fonts: [
{
name: 'Overpass',
data: Buffer.from(OverpassRegular),
style: 'normal',
weight: 400
}
],
height,
width
});
const resvg = new Resvg(svg, {
fitTo: {
mode: 'width',
value: width
}
});
const image = resvg.render();
return new Response(image.asPng(), {
headers: {
'content-type': 'image/png',
'cache-control': 'public, max-age=600' // cache for 10 minutes
}
});
}

@ -1,58 +0,0 @@
<script>
export let post;
</script>
<div class="card">
<img src="https://sveltejs.github.io/assets/artwork/svelte-machine.png" alt="Svelte Machine" />
<div class="text">
<h1>{post.title}</h1>
<p class="date">{post.date_formatted}</p>
</div>
</div>
<style>
.card {
display: flex;
width: 100%;
height: 100%;
font-family: 'Overpass';
background: white;
}
img {
position: absolute;
width: 125%;
height: 100%;
top: 5%;
left: 0;
object-fit: cover;
}
.text {
display: flex;
position: absolute;
left: 80px;
width: 55%;
height: 80%;
display: flex;
flex-direction: column;
justify-content: center;
}
h1 {
font-size: 72px;
margin: 0;
color: #222;
font-weight: 400;
line-height: 80px;
margin: 0 0 0.5em 0;
}
.date {
font-size: 32px;
margin: 0;
color: #555;
text-transform: uppercase;
}
</style>

@ -1,72 +0,0 @@
import { get_blog_data, get_blog_list } from '$lib/server/blog/index.js';
export const prerender = true;
const months = ',Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(',');
/** @param {string} str */
function formatPubdate(str) {
const [y, m, d] = str.split('-');
return `${d} ${months[+m]} ${y} 12:00 +0000`;
}
/** @param {string} html */
function escapeHTML(html) {
/** @type {{ [key: string]: string }} */
const chars = {
'"': 'quot',
"'": '#39',
'&': 'amp',
'<': 'lt',
'>': 'gt'
};
return html.replace(/["'&<>]/g, (c) => `&${chars[c]};`);
}
/** @param {import('$lib/server/blog/types').BlogPostSummary[]} posts */
const get_rss = (posts) =>
`
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>Svelte blog</title>
<link>https://svelte.dev/blog</link>
<description>News and information about the magical disappearing UI framework</description>
<image>
<url>https://svelte.dev/favicon.png</url>
<title>Svelte</title>
<link>https://svelte.dev/blog</link>
</image>
${posts
.filter((post) => !post.draft)
.map(
(post) => `
<item>
<title>${escapeHTML(post.title)}</title>
<link>https://svelte.dev/blog/${post.slug}</link>
<description>${escapeHTML(post.description)}</description>
<pubDate>${formatPubdate(post.date)}</pubDate>
</item>
`
)
.join('')}
</channel>
</rss>
`
.replace(/>[^\S]+/gm, '>')
.replace(/[^\S]+</gm, '<')
.trim();
export async function GET() {
const posts = get_blog_list(await get_blog_data());
return new Response(get_rss(posts), {
headers: {
'Cache-Control': `max-age=${30 * 60 * 1e3}`,
'Content-Type': 'application/rss+xml'
}
});
}

@ -1,6 +0,0 @@
import { redirect } from '@sveltejs/kit';
import { dev } from '$app/environment';
export function load() {
redirect(dev ? 307 : 308, '/docs');
}

@ -1,7 +0,0 @@
import { redirect } from '@sveltejs/kit';
export const prerender = true;
export function load() {
redirect(301, 'examples/hello-world');
}

@ -1,15 +0,0 @@
import { get_example, get_examples_list } from '$lib/server/examples/index.js';
import examples_data from '$lib/generated/examples-data.js';
export const prerender = true;
export async function load({ params }) {
const examples_list = get_examples_list(examples_data);
const example = get_example(examples_data, params.slug);
return {
examples_list,
example,
slug: params.slug
};
}

@ -1,128 +0,0 @@
<!-- FIXME sometimes it adds a trailing slash when landing -->
<script>
// @ts-check
import { navigating } from '$app/stores';
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';
import TableOfContents from './TableOfContents.svelte';
export let data;
/** @type {number} */
let width;
let offset = 1;
/** @type {import('@sveltejs/repl').default} */
let repl;
const clone = (file) => ({
name: file.name.replace(/.\w+$/, ''),
type: file.type,
source: file.content
});
$: mobile = width < 768; // note: same as per media query below
/** @type {'columns' | 'rows'} */
$: replOrientation = mobile || width > 1080 ? 'columns' : 'rows';
$: repl && repl.set({ files: data.example.files.map(clone) });
</script>
<svelte:head>
<title>{data.example?.title} {data.example?.title ? '•' : ''} Svelte Examples</title>
<meta name="twitter:title" content="Svelte examples" />
<meta name="twitter:description" content="Cybernetically enhanced web apps" />
<meta name="Description" content="Interactive example Svelte apps" />
</svelte:head>
<h1 class="visually-hidden">Examples</h1>
<div class="examples-container" bind:clientWidth={width}>
<div class="viewport offset-{offset}">
<TableOfContents
sections={data.examples_list}
active_section={data.example?.slug}
isLoading={!!$navigating}
/>
<div class="repl-container" class:loading={$navigating}>
<Repl
bind:this={repl}
{svelteUrl}
orientation={replOrientation}
fixed={mobile}
relaxed
injectedJS={mapbox_setup}
previewTheme={$theme.current}
/>
</div>
</div>
{#if mobile}
<ScreenToggle bind:offset labels={['index', 'input', 'output']} />
{/if}
</div>
<style>
.examples-container {
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%;
}
.repl-container.loading {
opacity: 0.6;
}
/* temp fix for #2499 and #2550 while waiting for a fix for https://github.com/sveltejs/svelte-repl/issues/8 */
.repl-container :global(.tab-content),
.repl-container :global(.tab-content.visible) {
pointer-events: all;
opacity: 1;
}
.repl-container :global(.tab-content) {
visibility: hidden;
}
.repl-container :global(.tab-content.visible) {
visibility: visible;
}
.offset-1 {
transform: translate(-33.333%, 0);
}
.offset-2 {
transform: translate(-66.666%, 0);
}
@media (min-width: 768px) {
.examples-container {
padding: 0;
}
.viewport {
width: 100%;
height: 100%;
display: grid;
/* TODO */
grid-template-columns: 36rem auto;
grid-auto-rows: 100%;
transition: none;
}
.offset-1,
.offset-2 {
transform: none;
}
}
</style>

@ -1,123 +0,0 @@
<script>
import { onMount } from 'svelte';
export let sections = [];
export let active_section = null;
export let isLoading = false;
let active_el;
onMount(() => {
active_el.scrollIntoView({ block: 'center' });
});
</script>
<ul class="examples-toc">
{#each sections as section}
<!-- Avoid embeds -->
{#if section.title !== 'Embeds'}
<li>
<span class="section-title">{section.title}</span>
{#each section.examples as example}
<div class="row" class:active={example.slug === active_section} class:loading={isLoading}>
<a
href="/examples/{example.slug}"
class="row"
class:active={example.slug === active_section}
class:loading={isLoading}
>
<img
class="thumbnail"
alt="{example.title} thumbnail"
src="/examples/thumbnails/{example.slug}.jpg"
/>
<span>{example.title}</span>
</a>
{#if example.slug === active_section}
<a bind:this={active_el} href="/repl/{example.slug}" class="repl-link">REPL</a>
{/if}
</div>
{/each}
</li>
{/if}
{/each}
</ul>
<style>
.examples-toc {
overflow-y: auto;
height: 100%;
border-right: 1px solid var(--sk-back-4);
background-color: var(--sk-back-3);
color: var(--sk-text-2);
padding: 3rem 3rem 0 3rem;
margin: 0;
}
.examples-toc li {
display: block;
line-height: 1.2;
margin: 0 0 4.8rem 0;
}
.section-title {
display: block;
padding: 0 0 0.8rem 0;
font: 400 var(--sk-text-xs) var(--sk-font);
text-transform: uppercase;
letter-spacing: 0.12em;
font-weight: 700;
}
div {
display: flex;
flex-direction: row;
padding: 0.2rem 3rem;
margin: 0 -3rem;
}
div.active {
color: white;
}
div.active.loading {
background: rgba(0, 0, 0, 0.1) calc(100% - 3rem) 47% no-repeat url(/icons/loading.svg);
background-size: 1em 1em;
color: white;
}
a {
display: flex;
flex: 1 1 auto;
position: relative;
color: var(--sk-text-2);
border-bottom: none;
font-size: 1.6rem;
align-items: center;
justify-content: start;
padding: 0;
}
a:hover {
color: var(--sk-text-1);
}
.repl-link {
flex: 0 1 auto;
font-size: 1.2rem;
font-weight: 700;
margin-right: 2.5rem;
}
.thumbnail {
background-color: #fff;
object-fit: contain;
width: 5rem;
height: 5rem;
border-radius: 2px;
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.13);
margin: 0.2em 0.5em 0.2em 0;
}
</style>

@ -1,8 +0,0 @@
// @ts-check
import examples_data from '$lib/generated/examples-data.js';
import { get_examples_list } from '$lib/server/examples/index.js';
import { json } from '@sveltejs/kit';
export const GET = () => {
return json(get_examples_list(examples_data));
};

@ -1,26 +0,0 @@
import examples_data from '$lib/generated/examples-data.js';
import { get_example, get_examples_list } from '$lib/server/examples/index.js';
import { error, json } from '@sveltejs/kit';
export const prerender = true;
export const GET = ({ params }) => {
const examples = new Set(
get_examples_list(examples_data)
.map((category) => category.examples)
.flat()
.map((example) => example.slug)
);
if (!examples.has(params.slug)) error(404, 'Example not found');
return json(get_example(examples_data, params.slug));
};
export async function entries() {
const examples_list = get_examples_list(examples_data);
return examples_list
.map(({ examples }) => examples)
.flatMap((val) => val.map(({ slug }) => ({ slug })));
}

@ -1,7 +0,0 @@
import { redirect } from '@sveltejs/kit';
export const prerender = true;
export function GET() {
redirect(308, '/docs/faq');
}

@ -1,7 +1,5 @@
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_list } from '$lib/server/examples/index.js';
import examples_data from '$lib/generated/examples-data.js';
import { get_tutorial_list, get_tutorial_data } from '$lib/server/tutorial/index.js';
import { json } from '@sveltejs/kit';
export const prerender = true;
@ -14,9 +12,9 @@ export const GET = async () => {
* @returns {Promise<import('@sveltejs/site-kit').NavigationLink[]>}
*/
async function get_nav_list() {
const [docs_list, blog_list] = await Promise.all([
const [docs_list, tutorial_list] = await Promise.all([
get_docs_list(await get_docs_data()),
get_blog_list(await get_blog_data())
get_tutorial_list(await get_tutorial_data())
]);
const processed_docs_list = docs_list.map(({ title, pages }) => ({
@ -24,25 +22,10 @@ async function get_nav_list() {
sections: pages.map(({ title, path }) => ({ title, path }))
}));
const processed_blog_list = [
{
title: '',
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 examples_list = get_examples_list(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');
const processed_tutorial_list = tutorial_list.map(({ title, tutorials }) => ({
title,
sections: tutorials.map(({ title, slug }) => ({ title, path: '/tutorial/' + slug }))
}));
return [
{
@ -57,31 +40,25 @@ async function get_nav_list() {
]
},
{
title: 'Examples',
prefix: 'examples',
pathname: '/examples',
title: 'Tutorial',
prefix: 'tutorial',
pathname: '/tutorial',
sections: [
{
title: 'EXAMPLES',
sections: processed_examples_list
title: 'TUTORIAL',
sections: processed_tutorial_list
}
]
},
{
title: 'REPL',
prefix: 'repl',
pathname: '/repl'
pathname: 'https://svelte.dev/playground'
},
{
title: 'Blog',
prefix: 'blog',
pathname: '/blog',
sections: [
{
title: 'BLOG',
sections: processed_blog_list
}
]
pathname: 'https://svelte.dev/blog'
}
];
}

@ -1,8 +0,0 @@
import { redirect } from '@sveltejs/kit';
export function load() {
redirect(
307,
'https://docs.google.com/document/d/1IA9Z5rcIm_KRxvh_L42d2NDdYRHZ72MfszhyJrsmf5A'
);
}

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 358.464 235.952"><defs><style>.a{fill:#ff3e00;}.b{fill:#fff;}.c{fill:#1273ff;}.d{fill:#ffd815;}</style><symbol id="a" viewBox="0 0 93.224 112"><path class="a" d="M87.269,14.819C76.869-.066,56.328-4.478,41.477,4.984L15.4,21.608A29.921,29.921,0,0,0,1.876,41.651,31.514,31.514,0,0,0,4.984,61.882,30.006,30.006,0,0,0,.507,73.065,31.892,31.892,0,0,0,5.955,97.181c10.4,14.887,30.942,19.3,45.791,9.835L77.829,90.392A29.915,29.915,0,0,0,91.347,70.349a31.522,31.522,0,0,0-3.1-20.232,30.019,30.019,0,0,0,4.474-11.182,31.878,31.878,0,0,0-5.447-24.116"/><path class="b" d="M38.929,98.582a20.72,20.72,0,0,1-22.237-8.243,19.176,19.176,0,0,1-3.276-14.5,18.143,18.143,0,0,1,.623-2.435l.491-1.5,1.337.981a33.633,33.633,0,0,0,10.2,5.1l.969.294-.089.968A5.844,5.844,0,0,0,28,83.122a6.24,6.24,0,0,0,6.7,2.485,5.748,5.748,0,0,0,1.6-.7L62.382,68.281a5.43,5.43,0,0,0,2.451-3.631,5.794,5.794,0,0,0-.988-4.371,6.244,6.244,0,0,0-6.7-2.487,5.755,5.755,0,0,0-1.6.7l-9.953,6.345a19.06,19.06,0,0,1-5.3,2.326,20.719,20.719,0,0,1-22.237-8.243,19.171,19.171,0,0,1-3.277-14.5A17.992,17.992,0,0,1,22.915,32.37L49,15.747a19.03,19.03,0,0,1,5.3-2.329,20.72,20.72,0,0,1,22.237,8.243,19.176,19.176,0,0,1,3.277,14.5,18.453,18.453,0,0,1-.624,2.435l-.491,1.5-1.336-.979a33.616,33.616,0,0,0-10.2-5.1l-.97-.294.09-.968a5.859,5.859,0,0,0-1.052-3.878,6.241,6.241,0,0,0-6.7-2.485,5.748,5.748,0,0,0-1.6.7L30.842,43.719a5.421,5.421,0,0,0-2.449,3.63,5.79,5.79,0,0,0,.986,4.372,6.245,6.245,0,0,0,6.7,2.487,5.773,5.773,0,0,0,1.6-.7l9.952-6.342a18.978,18.978,0,0,1,5.3-2.328,20.718,20.718,0,0,1,22.236,8.243,19.171,19.171,0,0,1,3.277,14.5,18,18,0,0,1-8.13,12.054L44.229,96.253a19.017,19.017,0,0,1-5.3,2.329"/></symbol></defs><path class="c" d="M324.237,118.694c-59.509-36.573-119.018,36.573-178.526,0V61.45c59.508,36.573,119.017-36.573,178.526,0Z"/><path class="d" d="M324.237,175.74c-59.509-36.573-119.018,36.573-178.526,0V118.5c59.508,36.573,119.017-36.573,178.526,0Z"/><use width="93.224" height="112" transform="translate(34.228 29.267) scale(1.584)" xlink:href="#a"/></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

Loading…
Cancel
Save