Compare commits

..

19 Commits

Author SHA1 Message Date
453dad1e0d add feed link to app.html 2023-09-23 20:20:35 -07:00
7b3ae4dea8 hide TOC on narrow screens 2023-09-23 20:03:00 -07:00
ce4ddf5a17 tweak next/prev links 2023-09-23 19:23:40 -07:00
c1e82ffb2c finish feed 2023-09-05 17:16:39 -07:00
7fb1f05a1e initial feed implementation 2023-09-04 22:07:58 -07:00
a28ee8b2f0 get footer links working and get rid of unnecessary json route 2023-08-27 08:52:30 -07:00
1b2d55173a upgrade to sveltekit 1 2023-08-26 20:55:35 -07:00
3a59f45e58 rework toc, sidenote fade, and footer 2023-08-26 14:41:58 -07:00
0519291bda limit toc to two levels and vary style 2023-08-25 22:22:39 -07:00
d1aa23e7c7 tweak margin of toc 2023-08-23 07:50:38 -07:00
25ce1b2d85 only show header anchors on hover 2023-08-23 06:02:12 -07:00
5817d94043 rework layout and add table of contents 2023-08-21 22:16:17 -07:00
33d6838dc4 start work on table of contents 2023-08-20 22:04:21 -07:00
b1dc3ae0ea add heading anchors 2023-08-20 16:12:04 -07:00
6431267827 start sidenotes post 2023-08-19 13:11:17 -07:00
01fce255ac tweaks 2023-08-19 12:46:00 -07:00
8272a4bd43 rework rehype plugin 2023-08-19 12:45:02 -07:00
b68220fa2e conditionally render remainder of dropcap word 2023-08-19 12:16:23 -07:00
54bcec280d fix sidenote width and add partial nesting support 2023-08-19 12:12:12 -07:00
43 changed files with 2947 additions and 6423 deletions

6
.gitignore vendored

@ -3,3 +3,9 @@ node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
**/_test.*

2
.npmrc Normal file

@ -0,0 +1,2 @@
engine-strict=true
resolution-mode=highest

@ -1,6 +1,6 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte);
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
## Creating a project
@ -8,14 +8,12 @@ If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm init svelte@next
npm create svelte@latest
# create a new project in my-app
npm init svelte@next my-app
npm create svelte@latest my-app
```
> Note: the `@next` is temporary
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
@ -29,10 +27,12 @@ npm run dev -- --open
## Building
Before creating a production version of your app, install an [adapter](https://kit.svelte.dev/docs#adapters) for your target environment. Then:
To create a production version of your app:
```bash
npm run build
```
> You can preview the built app with `npm run preview`, regardless of whether you installed an adapter. This should _not_ be used to serve your app in production.
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

@ -1,11 +0,0 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"$lib": ["src/lib"],
"$lib/*": ["src/lib/*"]
}
},
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
}

7834
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,18 +1,23 @@
{
"name": "blog",
"version": "0.0.1",
"scripts": {
"dev": "svelte-kit dev",
"build": "svelte-kit build",
"preview": "svelte-kit preview"
},
"devDependencies": {
"@sveltejs/adapter-static": "^1.0.0-next.21",
"@sveltejs/kit": "next",
"mdsvex": "^0.9.8",
"node-sass": "^6.0.1",
"svelte": "^3.42.6",
"svelte-preprocess": "^4.9.8"
},
"type": "module"
"name": "blog.jfmonty2.com",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-static": "^2.0.3",
"@sveltejs/kit": "^1.20.4",
"hast-util-to-text": "^4.0.0",
"mdast-util-to-string": "^4.0.0",
"mdsvex": "^0.11.0",
"svelte": "^4.0.5",
"unist-util-find": "^3.0.0",
"unist-util-visit": "^5.0.0",
"vite": "^4.4.2"
},
"type": "module"
}

@ -4,12 +4,13 @@
<meta charset="utf-8" />
<link rel="preload" href="/Tajawal-Regular.woff2" as="font" type="font/woff2" />
<link rel="preload" href="/Baskerville-Regular.woff2" as="font" type="font/woff2" />
<link rel="icon" href="/favicon.png" />
<link rel="stylesheet" href="/style.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%svelte.head%
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link rel="alternate" type="application/atom+xml" href="/feed">
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body>
<div id="svelte">%svelte.body%</div>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

@ -34,9 +34,8 @@
}
</style>
<p>
<span class="drop-cap">{initial}</span>
<span class="first-word" style:--shift={shift}>{remainder}</span>
<slot></slot>
</p>
<span class="drop-cap">{initial}</span>
{#if remainder.length}
<span class="first-word" style:--shift={shift}>{remainder}</span>
{/if}

67
src/lib/Heading.svelte Normal file

@ -0,0 +1,67 @@
<script>
export let level;
export let id = '';
const tag = `h${level}`;
</script>
<style>
a {
/* Works better to set the size here for line-height reasons */
font-size: 0.9em;
/* color: hsl(0, 0%, 25%); */
color: var(--accent-color);
}
a:hover {
border-bottom: 0.05em solid currentcolor;
}
svg {
width: 1em;
/* tiny tweak for optical alignment */
transform: translateY(2px);
}
.before {
display: none;
padding-right: 0.25em;
margin-left: -1.25em;
}
@media(min-width: 58rem) {
.before {
display: inline;
opacity: 0;
transition: opacity 150ms;
}
.h:hover .before, .before:hover {
opacity: 1;
}
.after {
display: none;
}
.h:hover {
cursor: default;
}
}
</style>
<svelte:element this={tag} {id} class="h">
<span class="before">
<a href="#{id}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
</svg></a></span><span> <!-- Looks ugly but necessary to get rid of spurious whitespace -->
<slot></slot>
</span>
<!-- Icon from https://heroicons.com/ -->
<a href="#{id}" class="after">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
</svg>
</a>
</svelte:element>

@ -18,7 +18,7 @@
{#if href.startsWith('/') || host(href) === $page.host}
<a sveltekit:prefetch {href}>
<a data-sveltekit-preload-data="hover" {href}>
<slot></slot>
</a>
{:else}

@ -1,8 +1,9 @@
<script context="module">
import { onMount } from 'svelte';
import { formatDate } from './datefmt.js';
import { makeSlug } from '$lib/slug.js';
import { makeSlug } from '$lib/utils.js';
import Toc from './Toc.svelte';
import Link from './Link.svelte';
export { Link as a };
</script>
@ -11,14 +12,82 @@
export let title, date;
export const description = '';
export const draft = false;
export let toc = null;
export let prev = null;
export let next = null;
</script>
<style>
.page {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, var(--content-width)) minmax(0, 1fr);
padding: 0 0.5rem;
}
.title {
grid-column: 2 / 3;
}
.left-gutter {
grid-column: 1 / 2;
justify-self: end;
}
.subtitle {
font-size: 0.9em;
font-style: italic;
margin-top: -0.5rem;
}
.post {
grid-column: 2 / 3;
}
.footer {
grid-column: 2 / 3;
margin-bottom: 2rem;
display: flex;
}
hr {
grid-column: 2 / 3;
width: 100%;
border-top: 1px solid hsl(0 0% 75%);
border-bottom: none;
margin: 2rem 0;
}
.footer a {
display: flex;
align-items: center;
gap: 0.45em;
font-size: 1rem;
color: var(--content-color-faded);
text-decoration: underline;
text-underline-offset: 0.25em;
text-decoration-color: transparent;
transition: 150ms;
}
.footer a:hover {
text-decoration-color: currentColor;
text-decoration: underline;
}
.footer svg {
width: 1em;
transition: 150ms;
}
a.prev:hover svg {
transform: translateX(-50%);
}
a.next:hover svg {
transform: translateX(50%);
}
</style>
<svelte:head>
@ -26,8 +95,42 @@
<link rel="stylesheet" href="/prism-dracula.css" />
</svelte:head>
<div id="post">
<h1 id="{makeSlug(title)}">{title}</h1>
<p class="subtitle">{formatDate(date)}</p>
<slot></slot>
<div class="page">
<div class="title">
<h1 id="{makeSlug(title)}">{title}</h1>
<p class="subtitle">{formatDate(date)}</p>
</div>
<div class="left-gutter">
{#if toc?.length !== 0}
<Toc items={toc} />
{/if}
</div>
<div class="post">
<slot></slot>
</div>
<hr>
<div class="footer">
{#if prev}
<a href="/{prev}" class="prev" data-sveltekit-preload-data="hover">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
</svg>
Previous
</a>
{/if}
{#if next}
<!-- we use margin-left rather than justify-content so it works regardless of whether the "previous" link exists -->
<a href="/{next}" class="next" style="margin-left: auto;" data-sveltekit-preload-data="hover">
Next
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
</svg>
</a>
{/if}
</div>
</div>

@ -1,4 +1,4 @@
<style lang="scss">
<style>
/* always applicable */
:global(body) {
counter-reset: sidenote;
@ -8,30 +8,30 @@
counter-increment: sidenote;
color: #444;
margin-left: 0.05rem;
}
&:after {
font-size: 0.75em;
position: relative;
bottom: 0.3rem;
color: #8c0606;
}
.counter:after {
font-size: 0.75em;
position: relative;
bottom: 0.3rem;
color: #8c0606;
}
.sidenote {
color: #555;
font-size: 0.8rem;
}
&:before {
content: counter(sidenote) " ";
/* absolute positioning puts it at the top-left corner of the sidenote, overlapping with the content
(because the sidenote is floated it counts as a positioned parent, I think) */
position: absolute;
/* translate moves it out to the left (and just a touch up to mimic the superscript efect)
-100% refers to the width of the element, so it pushes it out further if necessary (i.e. two digits instead of one) */
transform: translate(calc(-100% - 0.2rem), -0.15rem);
font-size: 0.75rem;
color: #8c0606;
}
.sidenote:before {
content: var(--sidenote-index, counter(sidenote)) " ";
/* absolute positioning puts it at the top-left corner of the sidenote, overlapping with the content
(because the sidenote is floated it counts as a positioned parent, I think) */
position: absolute;
/* translate moves it out to the left (and just a touch up to mimic the superscript efect)
-100% refers to the width of the element, so it pushes it out further if necessary (i.e. two digits instead of one) */
transform: translate(calc(-100% - 0.2rem), -0.15rem);
font-size: 0.75rem;
color: #8c0606;
}
.sidenote-toggle {
@ -46,16 +46,28 @@
.sidenote {
--gap: 2rem;
--sidenote-width: min(14rem, calc(50vw - var(--gap) - var(--content-width) / 2));
--sidenote-width: min(16rem, calc(50vw - var(--gap) - 1rem - var(--content-width) / 2));
width: var(--sidenote-width);
hyphens: auto;
position: relative;
float: right;
clear: right;
margin-right: calc(0rem - var(--sidenote-width) - var(--gap)); // gives us 2rem of space between content and sidenote
margin-right: calc(0rem - var(--sidenote-width) - var(--gap)); /* gives us 2rem of space between content and sidenote */
margin-bottom: 0.7rem;
}
/* fade-in animation */
.sidenote {
opacity: 0;
animation: fade-in 600ms ease-out;
animation-delay: 500ms;
animation-fill-mode: forwards;
}
@keyframes fade-in {
from {opacity: 0;}
to {opacity: 1;}
}
.nested.sidenote {
margin-right: 0;
margin-top: 0.7rem;
@ -107,26 +119,13 @@
font-size: 1.25rem;
color: #8c0606;
cursor: pointer;
&:hover {
transform: scale(1.1);
font-weight: 800;
}
}
.dismiss:hover {
transform: scale(1.1);
font-weight: 800;
}
}
// /* slight tweaks for in between state */
// @media (min-width: 52.5em) and (max-width: 70em) {
// .sidenote {
// padding-left: calc(50vw - 19rem);
// }
// }
// @media (max-width: 52.5em) {
// .sidenote {
// padding-left: 2rem;
// }
// }
</style>
<script context="module">

149
src/lib/Toc.svelte Normal file

@ -0,0 +1,149 @@
<script>
import { onMount } from 'svelte';
import { makeSlug } from '$lib/utils.js';
export let items;
items.forEach(i => i.slug = makeSlug(i.text));
let headings = [];
let currentHeadingSlug = null;
let currentSubheadingSlug = null;
function setCurrentHeading() {
for (const h of headings) {
const yPos = h.getBoundingClientRect().y;
if (yPos > (window.innerHeight / 3)) {
break;
}
if (h.tagName === 'H2') {
currentHeadingSlug = h.id;
currentSubheadingSlug = null;
}
if (h.tagName === 'H3') {
currentSubheadingSlug = h.id
}
}
}
onMount (() => {
// These shouldn't change over the life of the page, so we can cache them
headings = Array.from(document.querySelectorAll('h2[id], h3[id]'));
setCurrentHeading();
});
</script>
<svelte:window on:scroll={setCurrentHeading} />
<style>
#toc {
display: none;
position: sticky;
top: 1.5rem;
margin-left: 1rem;
margin-right: 2rem;
max-width: 14rem;
color: var(--content-color-faded);
opacity: 0;
animation: fade-in 600ms ease-out;
animation-delay: 500ms;
animation-fill-mode: forwards;
}
@media(min-width: 1300px) {
#toc { display: block }
}
@keyframes fade-in {
from {opacity: 0}
to {opacity: 1}
}
/* margin-left is to match the padding on the top-level list items,
but here it needs to be margin so that the border is also shifted */
h5 {
font-variant: petite-caps;
font-weight: 500;
max-width: fit-content;
margin-top: 0;
margin-bottom: 0.25em;
padding-bottom: 0.25em;
border-bottom: 1px solid currentcolor;
/* make the border stretch beyond the text just a bit, because I like the effect */
padding-right: 1.5rem;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
li {
position: relative;
font-size: 0.9rem;
}
li.depth-2 {
align-items: stretch;
margin-bottom: 0.2rem;
}
li.depth-3 {
align-items: center;
margin-bottom: 0.05rem;
}
.marker {
position: absolute;
left: -0.6rem;
}
.bar {
width: 0.1rem;
height: 100%;
}
.dot {
width: 0.15rem;
height: 0.15rem;
border-radius: 50%;
/* vertically center within its containing block */
top: 0;
bottom: 0;
margin: auto 0;
}
li.current, li:hover {
color: var(--content-color);
}
.current .marker, li:hover .marker {
background-color: var(--accent-color);
}
a {
color: inherit;
text-decoration: none;
}
</style>
<div id="toc">
<h5>
<span class="heading">Contents</span>
</h5>
<ul>
{#each items as item}
{#if item.depth === 2}
<li class="depth-2" class:current={item.slug === currentHeadingSlug} style:align-items="stretch">
<span class="marker bar"></span>
<a href="#{item.slug}">{item.text}</a>
</li>
{:else if item.depth === 3}
<li class="depth-3" class:current={item.slug === currentSubheadingSlug} style:align-items="center" style:margin-left="0.75em">
<span class="marker dot"></span>
<a href="#{item.slug}">{item.text}</a>
</li>
{/if}
{/each}
</ul>
</div>

@ -1,20 +0,0 @@
<script>
import Step from './Step.svelte';
import {onMount} from 'svelte';
let frame;
onMount(() => {
frame.setAttribute('srcdoc', frame.innerHTML);
})
</script>
<iframe bind:this={frame}>
<html>
<head></head>
<body>
<Step />
<p>Goodbye world!</p>
</body>
</html>
</iframe>

@ -1,7 +0,0 @@
<script>
let count = 0;
</script>
<p>hello world!</p>
<button on:click={() => count++}>Increment</button>
<p>The count is: {count}</p>

@ -1,50 +0,0 @@
const nonAlphaNum = /[^A-Za-z0-9\-]/g;
const space = /\s/g
export function makeSlug(text) {
return text
.toLowerCase()
.replace(space, '-')
.replace(nonAlphaNum, '')
}
function apply(node, types, fn) {
if (typeof types === 'string') {
types = new Set([types]);
}
else if (!(types instanceof Set)) {
types = new Set(types)
console.log(types)
}
if (types.has(node.type)) {
fn(node);
}
if ('children' in node) {
for (let child of node.children) {
apply(child, types, fn);
}
}
}
function getTextContent(node) {
let segments = [];
apply(node, 'text', textNode => {
// skip all-whitespace strings
if (textNode.value.match(/^\s+$/)) return;
segments.push(textNode.value.trim());
});
return segments.join(' ');
}
export default function slug() {
return (tree) => {
apply(tree, 'element', e => {
if (e.tagName.match(/h[1-6]/)) {
let text = getTextContent(e);
e.properties.id = makeSlug(text);
}
})
}
}

8
src/lib/utils.js Normal file

@ -0,0 +1,8 @@
const nonAlphaNum = /[^A-Za-z0-9\-]/g;
const space = /\s+/g;
export function makeSlug(text) {
return text
.toLowerCase()
.replace(space, '-')
.replace(nonAlphaNum, '');
}

94
src/lib/xml.js Normal file

@ -0,0 +1,94 @@
// const Node = {
// addChild(child) {
// this.children.push(child);
// return child;
// }
// }
export function tag(name, attrs, children) {
return {
type: 'tag',
tag: name,
attrs: attrs || {},
children: children || [],
addTag(name, attrs, children) {
const child = tag(name, attrs, children);
this.children.push(child);
return child;
},
};
}
export function text(content) {
return {
type: 'text',
text: content,
};
}
export function serialize(node, depth) {
if (!depth) {
depth = 0;
}
const indent = ' '.repeat(depth * 4);
let fragments = [];
// version tag, if this is the top level
if (depth === 0) {
fragments.push('<?xml version="1.0" encoding="UTF-8"?>\n')
}
fragments.push(`${indent}<${node.tag}`);
// this happens if there are multiple text nodes within the same parent
if (node.type === 'text') {
return `${indent}${escape(node.text)}`;
}
if (node.children === undefined) {
console.log(node);
}
// opening tag <element attr="value">
for (const attr in node.attrs) {
fragments.push(` ${attr}="${node.attrs[attr]}"`);
}
if (node.children.length === 0) {
fragments.push(' />');
return fragments.join('');
}
fragments.push('>');
// if the only child is a single text node, skip recursion and just dump contents directly
if (node.children.length === 1 && node.children[0].type === 'text') {
const text = escape(node.children[0].text);
fragments.push(text);
}
// otherwise, start a new line for each child node, then recurse
else {
for (const child of node.children) {
fragments.push('\n');
fragments.push(serialize(child, depth + 1));
}
// no need to verify that there were children, we already did that
fragments.push(`\n${indent}`);
}
fragments.push(`</${node.tag}>`);
return fragments.join('');
}
function escape(text) {
// we aren't going to bother with escaping attributes, so we won't worry about quotes
return text
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;');
}

90
src/plugins/rehype.js Normal file

@ -0,0 +1,90 @@
import { visit, CONTINUE, EXIT, SKIP, } from 'unist-util-visit';
import { find } from 'unist-util-find';
import { toText } from 'hast-util-to-text';
import { makeSlug } from '../lib/utils.js';
export function localRehype() {
let printed = false;
return (tree, vfile) => {
const needsDropcap = vfile.data.fm.dropcap !== false
let dropcapAdded = false;
let moduleScript;
let imports = new Set();
if (needsDropcap) {
imports.add("import Dropcap from '$lib/Dropcap.svelte';");
}
visit(tree, node => {
// add slugs to headings
if (isHeading(node)) {
processHeading(node);
imports.add("import Heading from '$lib/Heading.svelte';");
return SKIP;
}
// mdsvex adds a <script context="module"> so we just hijack that for our own purposes
if (isModuleScript(node)) {
moduleScript = node;
}
// convert first letter/word of first paragraph to <Dropcap word="{whatever}">
if (needsDropcap && !dropcapAdded && isParagraph(node)) {
addDropcap(node);
dropcapAdded = true;
return SKIP;
}
});
// insert our imports at the top of the `<script context="module">` tag
if (imports.size > 0) {
const script = moduleScript.value;
// split the script where the opening tag ends
const i = script.indexOf('>');
const openingTag = script.slice(0, i + 1);
const remainder = script.slice(i + 1);
// mdvsex uses tabs so we will as well
const importScript = Array.from(imports).join('\n\t');
moduleScript.value = `${openingTag}\n\t${importScript}${remainder}`;
}
}
}
function processHeading(node) {
const level = node.tagName.slice(1);
node.tagName = 'Heading';
node.properties.level = level;
node.properties.id = makeSlug(toText(node));
}
function addDropcap(par) {
let txtNode = find(par, {type: 'text'});
const i = txtNode.value.search(/\s/);
const firstWord = txtNode.value.slice(0, i);
const remainder = txtNode.value.slice(i);
par.children.unshift({
type: 'raw',
value: `<Dropcap word="${firstWord}" />`,
});
txtNode.value = remainder;
}
function isHeading(node) {
return node.type === 'element' && node.tagName.match(/h[1-6]/);
}
function isModuleScript(node) {
return node.type === 'raw' && node.value.match(/^<script context="module">/);
}
function isParagraph(node) {
return node.type === 'element' && node.tagName === 'p';
}

48
src/plugins/remark.js Normal file

@ -0,0 +1,48 @@
import { visit } from 'unist-util-visit';
import { toString } from 'mdast-util-to-string';
import fs from 'node:fs';
// build table of contents and inject into frontmatter
export function localRemark() {
return (tree, vfile) => {
let toc = [];
let description = null;
visit(tree, ['heading', 'paragraph'], node => {
// build table of contents and inject into frontmatter
if (node.type === 'heading') {
toc.push({
text: toString(node),
depth: node.depth,
});
}
// inject description (first 25 words of the first paragraph)
if (node.type === 'paragraph' && description === null) {
description = summarize(node);
}
});
vfile.data.fm.toc = toc;
vfile.data.fm.description = description;
}
}
// convert paragraph to single string after stripping everything between html tags
function summarize(par) {
let newChildren = [];
let push = true;
for (const child of par.children) {
if (child.type === 'html') {
push = !push;
continue;
}
if (push) {
newChildren.push(child);
}
}
return toString({type: 'paragraph', children: newChildren});
}

1
src/routes/+layout.js Normal file

@ -0,0 +1 @@
export const prerender = true;

37
src/routes/+layout.svelte Normal file

@ -0,0 +1,37 @@
<style>
.header {
background-color: #4f5f68;
}
nav {
max-width: 30rem;
margin: 0 auto;
display: flex;
justify-content: space-around;
}
nav a {
color: white;
width: 8rem;
min-width: 6rem;
font-size: 1.5rem;
text-decoration: none;
text-align: center;
padding: 0.25rem 0;
}
nav a:hover {
background-color: #00000025;
}
</style>
<div class="header">
<nav>
<a data-sveltekit-preload-data="hover" href="/">Home</a>
<a data-sveltekit-preload-data="hover" href="/posts">Posts</a>
<a data-sveltekit-preload-data="hover" href="/">About</a>
</nav>
</div>
<main>
<slot></slot>
</main>

7
src/routes/+page.js Normal file

@ -0,0 +1,7 @@
export async function load({ data }) {
let post = await import(`./_posts/${data.slug}.svx`);
post.metadata.next = data.next;
return {
post: post.default,
}
}

@ -0,0 +1,11 @@
import { postData, siblingPosts } from './_posts/all.js';
// this is in a "servserside" loader so that we don't end up embedding the metadata
// for every post into the final page
export function load() {
return {
slug: postData[0].slug,
next: postData[1].slug,
};
}

5
src/routes/+page.svelte Normal file

@ -0,0 +1,5 @@
<script>
export let data;
</script>
<svelte:component this={data.post} />

@ -1,24 +0,0 @@
<script context="module">
export async function load({ url, params }) {
try {
let post = await import(`./_posts/${params.slug}.svx`);
return {
props: {
post: post.default
}
}
}
catch (err) {
return {
status: 404,
error: `Not found: ${url.pathname}`,
}
}
}
</script>
<script>
export let post;
</script>
<svelte:component this={post} />

@ -0,0 +1,18 @@
import { error } from '@sveltejs/kit';
export async function load({ url, params, data }) {
try {
let post = await import(`../_posts/${params.slug}.svx`);
post.metadata.prev = data.prev;
post.metadata.next = data.next;
return {
post: post.default,
}
}
catch (err) {
// throw error(404, `Not found: ${url.pathname}`);
console.log(err);
throw err;
}
}

@ -0,0 +1,10 @@
import { postData } from '../_posts/all.js';
export function load({ params }) {
const i = postData.findIndex(p => p.slug === params.slug);
return {
prev: i > 0 ? postData[i - 1].slug : null,
next: i < postData.length - 1 ? postData[i + 1].slug : null,
};
}

@ -0,0 +1,5 @@
<script>
export let data;
</script>
<svelte:component this={data.post} />

@ -1,45 +0,0 @@
<style>
:global(main) {
--content-width: 42rem;
box-sizing: border-box;
max-width: var(--content-width);
margin: 0 auto;
padding: 0 15px;
}
#header {
background-color: #4f5f68;
}
#nav-main {
max-width: 30rem;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(3, minmax(6rem, 8rem));
justify-content: space-between;
}
#nav-main a {
font-size: 1.5rem;
color: white;
text-decoration: none;
text-align: center;
padding: 0.25rem 0;
}
#nav-main a:hover {
background-color: #00000025;
}
</style>
<div id="header">
<nav id="nav-main">
<a sveltekit:prefetch href="/">Home</a>
<a sveltekit:prefetch href="/posts">Posts</a>
<a sveltekit:prefetch href="/">About</a>
</nav>
</div>
<main>
<slot></slot>
</main>

@ -1,28 +1,26 @@
import { dev } from '$app/env';
const posts = import.meta.globEager('./_posts/*.svx');
import { dev } from '$app/environment';
const posts = import.meta.globEager('./*.svx');
export let postData = [];
let postData = [];
for (const path in posts) {
// skip draft posts in production mode
if (!dev && posts[path].metadata.draft) {
continue;
}
const slug = path.slice(9, -4)
// slice off the ./ and the .svx
const slug = path.slice(2, -4);
posts[path].metadata.slug = slug;
postData.push(posts[path].metadata);
}
postData.sort((a, b) => {
// sorting in reverse, so we flip the intuitive order
if (a.date > b.date) return -1;
if (a.date < b.date) return 1;
return 0;
})
export async function get() {
return {
body: {postData}
};
}
});
export { postData };

@ -3,6 +3,7 @@ title: Imagining A Passwordless Future
description: Can we replace passwords with something more user-friendly?
date: 2021-04-30
draft: true
dropcap: false
---
<script>
import Sidenote from '$lib/Sidenote.svelte';

@ -4,10 +4,8 @@ description: An entirely-too-detailed dive into how I implemented sidenotes for
date: 2023-08-14
---
<script>
import Dropcap from '$lib/Dropcap.svelte';
import Sidenote from '$lib/Sidenote.svelte';
import UnstyledSidenote from '$lib/UnstyledSidenote.svelte';
import Frame from '$lib/projects/sidenotes/Frame.svelte';
</script>
<style>
@ -64,7 +62,7 @@ date: 2023-08-14
}
</style>
<Dropcap word="One">of my major goals when building this blog was to have sidenotes. I've always been a fan of sidenotes on the web, because the most comfortable reading width for a column of text is <em>far</em> less than the absurd amounts of screen width we tend to have available, and what else are we going to use it for?<Sidenote>Some sites use it for ads, of course, which is yet another example of how advertising ruins everything.</Sidenote></Dropcap>
One of my major goals when building this blog was to have sidenotes. I've always been a fan of sidenotes on the web, because the most comfortable reading width for a column of text is <em>far</em> less than the absurd amounts of screen width we tend to have available, and what else are we going to use it for?<Sidenote>Some sites use it for ads, of course, which is yet another example of how advertising ruins everything.</Sidenote>
Footnotes don't really work on the web the way they do on paper, since the web doesn't have page breaks. You _can_ stick your footnotes in a floating box at the bottom of the page, so they're visible at the bottom of the text just like they would be on a printed page, but this sacrifices precious vertical space.<Sidenote>On mobile, it's _horizontal_ space that's at a premium, so I do use this approach there. Although I'm a pretty heavy user of sidenotes, so I have to make them toggleable as well or they'd fill up the entire screen.</Sidenote> Plus, you usually end up with the notes further away from the point of divergence than they would be as sidenotes anyway.

@ -4,12 +4,11 @@ description: They're more similar than they are different, but they say the most
date: 2023-06-29
---
<script>
import Dropcap from '$lib/Dropcap.svelte';
import Sidenote from '$lib/Sidenote.svelte';
</script>
<Dropcap word="Recently">I've had a chance to get to know Vue a bit. Since my frontend framework of choice has previously been Svelte (this blog is built in Svelte, for instance) I was naturally interested in how they compared.</Dropcap>
Recently I've had a chance to get to know Vue a bit. Since my frontend framework of choice has previously been Svelte (this blog is built in Svelte, for instance) I was naturally interested in how they compared.
Of course, this is only possible because Vue and Svelte are really much more similar than they are different. Even among frontend frameworks, they share a lot of the same basic ideas and high-level concepts, which means that we get to dive right into the nitpicky details and have fun debating `bind:attr={value}` versus `:attr="value"`. In the meantime, a lot of the building blocks are basically the same or at least have equivalents, such as:
* Single-file components with separate sections for markup, style, and logic

@ -0,0 +1,53 @@
import { tag, text, serialize } from '$lib/xml.js';
import { postData } from '../_posts/all.js';
export const prerender = true;
export function GET() {
return new Response(renderFeed(), {
headers: {'Content-Type': 'application/atom+xml'}
});
}
function renderFeed() {
const feed = tag('feed', {xmlns: 'http://www.w3.org/2005/Atom'});
feed.addTag('id', {}, [text('https://blog.jfmonty2.com/')])
feed.addTag('title', {}, [text("Joe's Blog")]);
feed.addTag('link', {rel: 'alternate', href: 'https://blog.jfmonty2.com/'});
feed.addTag('link', {rel: 'self', href: 'https://blog.jfmonty2.com/feed/'});
const lastUpdate = iso(postData[0].updated || postData[0].date);
feed.addTag('updated', {}, [text(lastUpdate)]);
const author = feed.addTag('author');
author.addTag('name', {}, [text('Joseph Montanaro')]);
for (const post of postData) {
const url = `https://blog.jfmonty2.com/${post.slug}`
const entry = feed.addTag('entry');
entry.addTag('title', {}, [text(post.title)]);
entry.addTag('link', {rel: 'alternate', href: url});
entry.addTag('id', {}, [text(url)]);
const publishedDate = iso(post.date);
entry.addTag('published', {}, [text(publishedDate)])
const updatedDate = iso(post.updated || post.date);
entry.addTag('updated', {}, [text(updatedDate)]);
entry.addTag('content', {type: 'html'}, [text(renderDescription(post))]);
}
return serialize(feed);
}
function renderDescription(post) {
return `<p>${post.description} <a href="https://blog.jfmonty2.com/${post.slug}">Read more</a></p>`;
}
function iso(datetimeStr) {
return new Date(datetimeStr).toISOString();
}

@ -1,18 +0,0 @@
<script context="module">
export async function load({ fetch }) {
const resp = await fetch('/latest.json');
const postMeta = await resp.json();
const post = await import(`./_posts/${postMeta.slug}.svx`);
return {
props: {
post: post.default,
}
}
}
</script>
<script>
export let post;
</script>
<svelte:component this={post} />

@ -1,5 +0,0 @@
import { postData } from './posts.js';
export async function get() {
return {body: postData[0]};
}

@ -1,27 +1,20 @@
<script>
import { formatDate } from '$lib/datefmt.js';
export let postData;
import { postData } from '../_posts/all.js';
</script>
<style lang="scss">
<style>
#posts {
/*text-align: center;*/
max-width: 24rem;
// margin-top: 1.25rem;
margin-left: auto;
margin-right: auto;
max-width: var(--content-width);
margin: 0 auto;
}
.post {
border-bottom: 2px solid #eee;
margin-top: 1rem;
hr {
margin: 2rem 0;
border-color: #eee;
}
/* .post-title {
font-weight: bold;
font-size: 1.2rem;
}*/
.post-date {
color: #808080;
}
@ -44,9 +37,14 @@
text-decoration: underline;
}
h3 {
display: inline;
margin: 0;
h2 {
font-size: 1.25rem;
margin-top: 0.5rem;
margin-bottom: 0.75rem;
}
h2 a {
color: currentcolor;
}
</style>
@ -56,18 +54,22 @@
<div id="posts">
<h1 style:text-align="center">All Posts</h1>
{#each postData as post}
{#each postData as post, idx}
<div class="post">
<div class="post-date">{new Date(post.date).toISOString().split('T')[0]}</div>
<div>
<a sveltekit:prefetch class="post-link" href="/{post.slug}">
<h3>{post.title}<h3>
<h2>
<a data-sveltekit-preload-data="hover" class="post-link" href="/{post.slug}">
{post.title}
</a>
{#if post.draft}
<span class="draft-notice">Draft</span>
{/if}
</div>
</h2>
<p>{post.description}</p>
</div>
{#if idx < postData.length - 1}
<hr>
{/if}
{/each}
</div>

@ -29,10 +29,12 @@ html {
line-height: var(--content-line-height);
letter-spacing: -0.005em;
color: var(--content-color);
box-sizing: border-box;
}
body {
margin: 0;
--content-width: 42rem;
}
h1, h2, h3, h4, h5, h6 {
@ -72,10 +74,6 @@ p {
margin-bottom: 1rem;
}
/*ul, ol {
margin: 0.5rem 0;
}*/
code {
background: #eee;
border-radius: 0.2rem;
@ -87,5 +85,3 @@ code {
pre > code {
font-size: 0.8rem;
}
/* TESTING */

@ -1,24 +1,24 @@
import { mdsvex } from 'mdsvex';
import staticAdapter from '@sveltejs/adapter-static';
import svp from 'svelte-preprocess';
import slug from './src/lib/slug.js';
import { mdsvex } from 'mdsvex';
import { localRemark } from './src/plugins/remark.js';
import { localRehype } from './src/plugins/rehype.js';
/** @type {import('@sveltejs/kit').Config} */
const config = {
extensions: ['.svelte', '.svx'],
preprocess: [
mdsvex({
layout: './src/lib/Post.svelte',
rehypePlugins: [slug],
remarkPlugins: [localRemark],
rehypePlugins: [localRehype],
}),
svp.scss(),
],
kit: {
// hydrate the <div id="svelte"> element in src/app.html
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: staticAdapter(),
prerender: {
default: true,
},
}
};

@ -1,281 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<!-- Crane image created by macrovector on Freepik: https://www.freepik.com/free-vector/construction-icons-set_1537228.htm#query=crane&position=3&from_view=keyword -->
<svg
width="28.305676mm"
height="28.174238mm"
viewBox="0 0 28.305676 28.174238"
version="1.1"
id="svg1392"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
sodipodi:docname="crane.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1394"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="2.1089995"
inkscape:cx="-5.6899017"
inkscape:cy="78.710306"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1389" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-139.84716,-103.71933)">
<path
d="m 166.79605,124.82179 h 0.18627 v -20.83188 h -0.18627 v 20.83188"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path324" />
<path
d="m 166.25101,125.97184 h 1.27635 v -0.51893 c 0,-0.1323 -0.10724,-0.23919 -0.23918,-0.23919 h -0.79763 c -0.13229,0 -0.23954,0.10689 -0.23954,0.23919 v 0.51893"
style="fill:#e4731a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path326" />
<path
d="m 166.6934,125.21372 h 0.39193 v -0.64981 h -0.39193 v 0.64981"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path328" />
<path
d="m 165.62554,126.85626 c 0,0.69814 0.56585,1.26365 1.26365,1.26365 0.69779,0 1.26365,-0.56551 1.26365,-1.26365 0,-0.6978 -0.56586,-1.26365 -1.26365,-1.26365 -0.6978,0 -1.26365,0.56585 -1.26365,1.26365"
style="fill:#f9a727;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path330" />
<path
d="m 143.76637,124.8673 19.84057,-20.49675 -0.0935,-0.0903 -19.84093,20.49639 0.0938,0.0907"
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path332" />
<path
d="m 149.82885,128.1506 2.90478,-6.56802 10.82675,-17.25718 1.80869,1.13488 -10.81899,17.2466 -4.61751,5.50863 z m 13.67261,-24.08167 -10.93188,17.42652 -2.97638,6.72711 0.37782,0.23707 4.72899,-5.64338 10.92553,-17.41488 -2.12408,-1.33244"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path334" />
<path
d="m 154.67073,122.68783 -0.0988,0.1577 -1.96638,-1.23367 0.0988,-0.15769 1.96638,1.23366"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path336" />
<path
d="m 154.712,122.74604 -0.18132,0.0413 -0.71791,-3.13443 0.18168,-0.0416 0.71755,3.13478"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path338" />
<path
d="m 155.85042,120.93135 -3.13443,0.71755 -0.0416,-0.18132 3.13478,-0.71791 0.0413,0.18168"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path340" />
<path
d="m 155.87935,120.76167 -0.0991,0.15769 -1.96639,-1.23366 0.0991,-0.1577 1.96639,1.23367"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path342" />
<path
d="m 155.92062,120.81952 -0.18168,0.0416 -0.71755,-3.13443 0.18133,-0.0416 0.7179,3.13443"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path344" />
<path
d="m 157.05903,119.00483 -3.13478,0.71791 -0.0413,-0.18133 3.13443,-0.7179 0.0416,0.18132"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path346" />
<path
d="m 157.08761,118.8355 -0.0988,0.15769 -1.96638,-1.23366 0.0988,-0.15769 1.96638,1.23366"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path348" />
<path
d="m 157.12888,118.89336 -0.18132,0.0416 -0.71791,-3.13478 0.18168,-0.0413 0.71755,3.13443"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path350" />
<path
d="m 158.2673,117.07867 -3.13443,0.7179 -0.0416,-0.18168 3.13478,-0.71755 0.0413,0.18133"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path352" />
<path
d="m 158.29623,116.90898 -0.0991,0.15769 -1.96639,-1.23331 0.0991,-0.15804 1.96639,1.23366"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path354" />
<path
d="m 158.3375,116.96719 -0.18168,0.0416 -0.71755,-3.13479 0.18133,-0.0413 0.7179,3.13443"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path356" />
<path
d="m 159.47592,115.1525 -3.13479,0.7179 -0.0413,-0.18168 3.13443,-0.71755 0.0416,0.18133"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path358" />
<path
d="m 159.50449,114.98282 -0.0988,0.15804 -1.96638,-1.23366 0.0988,-0.15805 1.96638,1.23367"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path360" />
<path
d="m 159.54577,115.04102 -0.18133,0.0416 -0.71791,-3.13478 0.18169,-0.0413 0.71755,3.13443"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path362" />
<path
d="m 160.68418,113.22633 -3.13443,0.71791 -0.0416,-0.18168 3.13478,-0.71755 0.0413,0.18132"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path364" />
<path
d="m 160.71311,113.05665 -0.0991,0.15769 -1.96639,-1.23331 0.0991,-0.15805 1.96639,1.23367"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path366" />
<path
d="m 160.75438,113.11486 -0.18168,0.0416 -0.7179,-3.13478 0.18168,-0.0413 0.7179,3.13443"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path368" />
<path
d="m 161.8928,111.30017 -3.13479,0.7179 -0.0413,-0.18168 3.13443,-0.71755 0.0416,0.18133"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path370" />
<path
d="m 161.92137,111.13048 -0.0991,0.15805 -1.96603,-1.23367 0.0988,-0.15804 1.96638,1.23366"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path372" />
<path
d="m 161.96265,111.18869 -0.18133,0.0416 -0.71791,-3.13479 0.18169,-0.0416 0.71755,3.13478"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path374" />
<path
d="m 163.10106,109.374 -3.13443,0.7179 -0.0416,-0.18168 3.13478,-0.7179 0.0413,0.18168"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path376" />
<path
d="m 163.12999,109.20432 -0.0991,0.15769 -1.96639,-1.23367 0.0991,-0.15769 1.96639,1.23367"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path378" />
<path
d="m 163.17126,109.26252 -0.18168,0.0413 -0.71755,-3.13443 0.18133,-0.0416 0.7179,3.13478"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path380" />
<path
d="m 164.30968,107.44783 -3.13479,0.71755 -0.0413,-0.18132 3.13443,-0.71791 0.0416,0.18168"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path382" />
<path
d="m 164.33825,107.27815 -0.0988,0.15769 -1.96638,-1.23366 0.0988,-0.1577 1.96638,1.23367"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path384" />
<path
d="m 164.37953,107.33636 -0.18133,0.0413 -0.7179,-3.13443 0.18168,-0.0416 0.71755,3.13479"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path386" />
<path
d="m 165.51794,105.52167 -3.13443,0.71755 -0.0416,-0.18133 3.13443,-0.7179 0.0416,0.18168"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path388" />
<path
d="m 153.38944,124.32366 -0.18133,0.0416 -0.64382,-2.81128 0.18133,-0.0416 0.64382,2.81128"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path390" />
<path
d="m 153.34817,124.26581 -0.0988,0.15769 -1.48908,-0.93415 0.0991,-0.1577 1.48873,0.93416"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path392" />
<path
d="m 154.64215,122.85752 -2.81163,0.64382 -0.0413,-0.18168 2.81129,-0.64382 0.0416,0.18168"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path394" />
<path
d="m 152.06476,125.91399 -0.18556,0.012 -0.16228,-2.50931 0.18591,-0.0123 0.16193,2.50966"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path396" />
<path
d="m 152.02137,125.84096 -0.0988,0.15769 -0.98954,-0.62053 0.0991,-0.15769 0.98919,0.62053"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path398" />
<path
d="m 153.33405,124.43056 -2.31598,0.95461 -0.0709,-0.17215 2.31599,-0.95462 0.0709,0.17216"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path400" />
<path
d="m 167.06769,103.88196 c -0.20602,-0.12912 -0.44944,-0.1838 -0.69074,-0.15523 l -2.87549,0.3422 -0.0935,0.16122 2.09691,1.31551 1.58573,-1.32151 c 0.0522,-0.0434 0.0801,-0.10936 0.0759,-0.17709 -0.005,-0.0677 -0.0416,-0.12912 -0.0988,-0.1651"
style="fill:#e4731a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path402" />
<path
d="m 163.77733,104.24179 2.71922,-0.33902 c 0.13687,-0.0169 0.27551,0.0138 0.39264,0.0871 l 0.0395,0.0247 c 0.0162,0.0102 0.0265,0.0275 0.0279,0.0466 0.001,0.019 -0.007,0.0378 -0.0215,0.0501 l -1.44533,1.20474 -1.71239,-1.07421"
style="fill:#f9a727;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path404" />
<path
d="m 145.63997,128.61168 h 3.81459 v 0.62547 h -3.81459 v -0.62547"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path406" />
<path
d="m 140.21425,126.26888 c 0.0783,-0.35489 0.34149,-0.63994 0.68897,-0.74683 l 6.68832,-2.05246 -0.3549,5.14209 h -6.77756 c -0.18556,0 -0.3609,-0.084 -0.47696,-0.2286 -0.11606,-0.14429 -0.16051,-0.33373 -0.12065,-0.5147 l 0.35278,-1.5995"
style="fill:#f9a727;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path408" />
<path
d="m 151.90037,131.89357 c 0.75917,0 1.37689,-0.61771 1.37689,-1.37724 0,-0.75918 -0.61772,-1.3769 -1.37689,-1.3769 -0.0243,-0.002 -3.12667,-0.22013 -4.15961,-0.22013 -1.03293,0 -4.13526,0.21802 -4.1663,0.22049 h -7.1e-4 c -0.75212,0 -1.36948,0.61736 -1.36948,1.37654 0,0.75953 0.61771,1.37724 1.37689,1.37724 h 8.31921"
style="fill:#252529;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path410" />
<path
d="m 143.58116,131.69778 c -0.65158,0 -1.18145,-0.52987 -1.18145,-1.18145 0,-0.65123 0.52669,-1.1811 1.17475,-1.1811 l 0.0141,-7.1e-4 c 0.03,-0.002 3.12667,-0.21943 4.15219,-0.21943 1.03082,0 4.11551,0.21696 4.14338,0.21908 l 0.008,7e-4 h 0.008 c 0.65158,0 1.18145,0.53023 1.18145,1.18146 0,0.65158 -0.52987,1.18145 -1.18145,1.18145 h -8.31921"
style="fill:#e4731a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path412" />
<path
d="m 150.71891,130.51633 c 0,-0.65229 0.52882,-1.18146 1.18146,-1.18146 0.65263,0 1.18145,0.52917 1.18145,1.18146 0,0.65263 -0.52882,1.18145 -1.18145,1.18145 -0.65264,0 -1.18146,-0.52882 -1.18146,-1.18145"
style="fill:#b8bbb6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path414" />
<path
d="m 151.90037,131.19507 c 0.37429,0 0.67874,-0.30445 0.67874,-0.67874 0,-0.3743 -0.30445,-0.67875 -0.67874,-0.67875 -0.3743,0 -0.67875,0.30445 -0.67875,0.67875 0,0.37429 0.30445,0.67874 0.67875,0.67874"
style="fill:#252529;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path416" />
<path
d="m 145.53555,129.58146 c 0,-0.19014 0.15416,-0.34431 0.34431,-0.34431 0.19015,0 0.34431,0.15417 0.34431,0.34431 0,0.19015 -0.15416,0.34432 -0.34431,0.34432 -0.19015,0 -0.34431,-0.15417 -0.34431,-0.34432"
style="fill:#252529;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path418" />
<path
d="m 145.54331,131.35311 c 0,-0.19014 0.15416,-0.34431 0.34431,-0.34431 0.19015,0 0.34396,0.15417 0.34396,0.34431 0,0.19015 -0.15381,0.34432 -0.34396,0.34432 -0.19015,0 -0.34431,-0.15417 -0.34431,-0.34432"
style="fill:#252529;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path420" />
<path
d="m 149.25735,131.35311 c 0,-0.19014 0.15417,-0.34431 0.34432,-0.34431 0.19014,0 0.34431,0.15417 0.34431,0.34431 0,0.19015 -0.15417,0.34432 -0.34431,0.34432 -0.19015,0 -0.34432,-0.15417 -0.34432,-0.34432"
style="fill:#252529;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path422" />
<path
d="m 149.94598,129.58146 c 0,-0.19014 -0.15417,-0.34431 -0.34431,-0.34431 -0.19015,0 -0.34432,0.15417 -0.34432,0.34431 0,0.19015 0.15417,0.34432 0.34432,0.34432 0.19014,0 0.34431,-0.15417 0.34431,-0.34432"
style="fill:#252529;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path424" />
<path
d="m 144.76261,130.51633 c 0,-0.65229 -0.52881,-1.18146 -1.18145,-1.18146 -0.65264,0 -1.18145,0.52917 -1.18145,1.18146 0,0.65263 0.52881,1.18145 1.18145,1.18145 0.65264,0 1.18145,-0.52882 1.18145,-1.18145"
style="fill:#b8bbb6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path426" />
<path
d="m 143.58116,131.19507 c 0.3743,0 0.67874,-0.30445 0.67874,-0.67874 0,-0.3743 -0.30444,-0.67875 -0.67874,-0.67875 -0.3743,0 -0.67874,0.30445 -0.67874,0.67875 0,0.37429 0.30444,0.67874 0.67874,0.67874"
style="fill:#252529;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path428" />
<path
d="m 143.43652,131.0088 h 8.60848 v -0.98495 h -8.60848 v 0.98495"
style="fill:#f9a727;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path430" />
<path
d="m 147.23664,128.61168 h 3.55812 c 0.33761,0 0.61172,-0.27376 0.61172,-0.61172 v -0.73236 c 0,-0.72073 0.11465,-2.63737 -0.76377,-3.69429 -0.0744,-0.0896 -0.18485,-0.14147 -0.30092,-0.14147 h -2.60173 c -0.16158,0 -0.29704,0.12277 -0.31256,0.28364 l -0.19086,1.97943 v 2.91677"
style="fill:#e4731a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path432" />
<path
d="m 150.76301,128.2649 c 0.16969,0 0.30797,-0.13794 0.30797,-0.30762 v -0.7165 c 0,-0.066 7.1e-4,-0.14217 0.002,-0.22648 0.01,-0.76165 0.03,-2.34562 -0.68192,-3.20216 -0.0173,-0.0212 -0.0437,-0.0335 -0.0709,-0.0335 h -2.54529 c -0.008,0 -0.0155,0.006 -0.0162,0.0148 l -0.16404,1.69827 c -0.0141,0.14464 0.025,0.28928 0.10971,0.40711 l 1.52153,2.11702 c 0.11219,0.15628 0.29316,0.24906 0.48578,0.24906 h 1.05163"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path434" />
<path
d="m 141.47331,125.91928 h 2.36891 c 0.0723,0 0.13052,0.0924 0.13052,0.20637 h -0.13052 -0.13053 -2.10785 -0.13053 -0.13053 c 0,-0.11394 0.0582,-0.20637 0.13053,-0.20637"
style="fill:#e4731a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path436" />
<path
d="m 141.47331,126.34649 h 2.36891 c 0.0723,0 0.13052,0.0924 0.13052,0.20673 h -0.13052 -0.13053 -2.10785 -0.13053 -0.13053 c 0,-0.1143 0.0582,-0.20673 0.13053,-0.20673"
style="fill:#e4731a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path438" />
<path
d="m 141.47331,126.77406 h 2.36891 c 0.0723,0 0.13052,0.0924 0.13052,0.20637 h -0.13052 -0.13053 -2.10785 -0.13053 -0.13053 c 0,-0.11394 0.0582,-0.20637 0.13053,-0.20637"
style="fill:#e4731a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path440" />
</g>
</svg>

Before

(image error) Size: 17 KiB

@ -1,99 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Under Construction</title>
<style>
body {
margin: 0;
}
main {
background-color: #f2f2f2;
padding: 1rem;
height: 100vh;
width: 100vw;
justify-content: center;
align-content: center;
display: grid;
}
#hero {
padding: 4rem;
background-color: white;
border-radius: 100%;
}
#hero img {
width: 16rem;
}
p {
font-family: sans-serif;
margin-bottom: 2rem;
margin-top: 2rem;
font-size: 1.5rem;
text-align: center;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/luxon/3.0.4/luxon.min.js" integrity="sha512-XdACFfCJeqqfVU8mvvXReyFR130qjFvfv/PZOFGwVyBz0HC+57fNkSacMPF2Dyek5jqi4D7ykFrx/T7N6F2hwQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>
<main>
<p style="font-size:2.5rem;color:#505050">Coming Soon&trade;</p>
<div id="hero">
<img src="/crane.svg">
</div>
<p>
Under Construction for <br />
<span id="counter" style="margin-top:0.5rem"></span>
</p>
</main>
</body>
<script>
function u(v, unit) {
if (v === 1) {
return `${v} ${unit}`;
}
else {
return `${v} ${unit}s`;
}
}
function f(n) {
let s = n.toString();
if (s.length == 1) {
return '0' + s;
}
return s;
}
const start = luxon.DateTime.fromSeconds(1634529923);
function setDuration() {
var diff = luxon.DateTime.now().diff(start);
const years = Math.floor(diff.as('years'));
diff = diff.minus(luxon.Duration.fromObject({years}));
const months = Math.floor(diff.as('months'));
diff = diff.minus(luxon.Duration.fromObject({months}));
const days = Math.floor(diff.as('days'));
diff = diff.minus(luxon.Duration.fromObject({days}));
const hours = Math.floor(diff.as('hours'))
diff = diff.minus(luxon.Duration.fromObject({hours}));
const minutes = Math.floor(diff.as('minutes'));
diff = diff.minus(luxon.Duration.fromObject({minutes}));
const seconds = Math.floor(diff.as('seconds'));
diff = diff.minus(luxon.Duration.fromObject({seconds}));
const millis = diff.as('milliseconds');
const timeString = `${u(years, "year")}, ${u(months, "month")}, ${u(days, "day")}, ${f(hours)}:${f(minutes)}:${f(seconds)}.${Math.floor(millis / 100)}`;
document.getElementById('counter').innerHTML = timeString;
window.setTimeout(setDuration, 10);
}
setDuration();
</script>
</html>

6
vite.config.js Normal file

@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});