Compare commits

...

9 Commits

Author SHA1 Message Date
b4a1097845 buncha stuff 2023-08-16 21:45:48 -07:00
adc582116b wip sidenotes post 2023-08-15 11:48:07 -07:00
7d5c696fa7 start working on sidenotes post 2023-08-15 11:10:22 -07:00
60eea00b88 misc tweaks 2023-08-15 11:07:11 -07:00
da72464f9a change font of drop caps 2023-08-15 11:06:54 -07:00
1a77127979 tweak positioning of sidenote counter 2023-08-15 11:05:30 -07:00
15e61d858d vue vs svelte post 2023-08-14 15:46:02 -07:00
1972c5714c drop cap component 2023-08-14 15:45:52 -07:00
ce411549ad css tweaks 2023-08-14 15:45:34 -07:00
11 changed files with 542 additions and 9 deletions

View File

@ -3,6 +3,7 @@
<head>
<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" />

42
src/lib/Dropcap.svelte Normal file
View File

@ -0,0 +1,42 @@
<script>
// Usage: <Dropcap word="Lorem">ipsum dolor sit amet...</Dropcap>
export let word;
const initial = word.slice(0, 1);
const remainder = word.slice(1);
// a few letters are narrower at the top, so we need more of a shift
const shiftValues = {
A: '-0.45em',
L: '-0.3em',
R: '-0.25em',
};
const shift = shiftValues[initial] || '0em';
</script>
<style>
.drop-cap {
text-transform: uppercase;
color: #8c0606;
/* box-sizing: border-box;*/
font-size: calc(var(--content-size) * var(--content-line-height) * 1.75);
float: left;
font-family: 'Baskerville';
line-height: 0.8;
margin-right: 0.1em;
display: block;
}
.first-word {
margin-left: var(--shift);
font-variant: petite-caps;
}
</style>
<p>
<span class="drop-cap">{initial}</span>
<span class="first-word" style:--shift={shift}>{remainder}</span>
<slot></slot>
</p>

View File

@ -13,6 +13,14 @@
export const draft = false;
</script>
<style>
.subtitle {
font-size: 0.9em;
font-style: italic;
margin-top: -0.5rem;
}
</style>
<svelte:head>
<title>{title}</title>
<link rel="stylesheet" href="/prism-dracula.css" />
@ -20,6 +28,6 @@
<div id="post">
<h1 id="{makeSlug(title)}">{title}</h1>
<p><em>{formatDate(date)}</em></p>
<p class="subtitle">{formatDate(date)}</p>
<slot></slot>
</div>

View File

@ -10,7 +10,7 @@
margin-left: 0.05rem;
&:after {
font-size: 0.75rem;
font-size: 0.75em;
position: relative;
bottom: 0.3rem;
color: #8c0606;
@ -23,9 +23,13 @@
&:before {
content: counter(sidenote) " ";
position: relative;
/* 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;
bottom: 0.2rem;
color: #8c0606;
}
}
@ -43,7 +47,7 @@
.sidenote {
--gap: 2rem;
--sidenote-width: min(14rem, calc(50vw - var(--gap) - var(--content-width) / 2));
max-width: var(--sidenote-width);
width: var(--sidenote-width);
hyphens: auto;
position: relative;
float: right;
@ -52,6 +56,12 @@
margin-bottom: 0.7rem;
}
.nested.sidenote {
margin-right: 0;
margin-top: 0.7rem;
margin-bottom: 0;
}
.dismiss {
display: none;
}
@ -124,6 +134,21 @@
</script>
<script>
import { onMount } from 'svelte';
let noteBody;
let nested = false;
onMount(() => {
// check to see if the parent node is also a sidenote, if so move this one to the end
let parentNote = noteBody.parentElement.closest('span.sidenote');
if (parentNote) {
noteBody.remove();
parentNote.appendChild(noteBody);
nested = true;
}
});
const id = Math.random().toString().slice(2);
let toggle;
@ -143,7 +168,7 @@
<label for={id} on:click={toggleState} class="counter"></label>
<input {id} bind:this={toggle} type="checkbox" class="sidenote-toggle" />
<span class="sidenote">
<span class="sidenote" class:nested bind:this={noteBody}>
<label class="dismiss" for={id} on:click={toggleState}>&times;</label>
<slot></slot>
</span>

View File

@ -0,0 +1,48 @@
<script>
export let floatingCounter = true;
export let classes = '';
export {classes as class};
</script>
<style>
:global(body) {
counter-reset: sidenote unstyled-sidenote;
}
.counter {
counter-increment: unstyled-sidenote;
margin-left: 0.05rem;
}
.counter::after {
content: counter(unstyled-sidenote);
font-size: 0.75em;
position: relative;
bottom: 0.3em;
color: #0083c4;
}
.sidenote {
color: var(--content-color-faded);
font-size: 0.8rem;
}
.sidenote.floatingCounter::before {
content: counter(unstyled-sidenote);
font-size: 0.75rem;
color: #0083c4;
/* Since the sidenote is floated it counts as a positioned element,
so this would make the counter overlap the start of the text... */
position: absolute;
/* ...except that we move it out to the left and up a bit, so
it's hanging out in space. 100% refers to the width of this
pseudo-element, so we handle different-sized counters the same. */
transform: translate(
calc(-100% - 0.16em),
-0.12em
);
}
</style>
<span class="counter"></span>
<span class="sidenote {classes}" class:floatingCounter={floatingCounter}>
<slot></slot>
</span>

View File

@ -0,0 +1,20 @@
<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>

View File

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

View File

@ -0,0 +1,201 @@
---
title: Sidenotes
description: An entirely-too-detailed dive into how I implemented sidenotes for this blog.
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>
.counter {
counter-increment: sidenote;
}
.counter::before {
content: counter(sidenote);
color: var(--accent-color);
font-size: 0.75rem;
position: relative;
bottom: 0.2rem;
margin-left: 0.1rem;
}
.sidenote-absolute {
position: absolute;
left: calc(50% + min(100%, var(--content-width)) / 2 + 1rem);
max-width: 12rem;
font-size: 0.75rem;
}
.sidenote-absolute::before {
content: counter(sidenote);
color: var(--accent-color);
font-size: 0.75rem;
position: relative;
bottom: 0.2rem;
margin-right: 0.1rem;
}
:global(.sn-float) {
float: right;
}
:global(.sn-clear) {
float: right;
clear: right;
}
:global(.sn-gutter) {
float: right;
width: 14rem;
margin-right: -14rem;
}
:global(.sn-gap) {
float: right;
width: 14rem;
margin-right: -16rem;
}
:global(.sn-var-width) {
float: right;
--width: min(14rem, calc(50vw - var(--content-width) / 2) - 2rem);
width: var(--width);
margin-right: calc(0rem - var(--width) - 2rem);
}
</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>
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.
I'm also not a huge fan of show-on-hover/click for marginalia, because it requires an extra interaction--and often a fairly precise one, which is always annoying.<Sidenote>This is especially true on mobile, where I've found myself selecting text instead of showing/hiding a note because I didn't get my finger in quite the right place.</Sidenote> Admittedly this style _does_ get you the absolute minimum distance between the marginalia and the main content, but I think the extra interaction is too heavy a price to pay.<Sidenote>Except on mobile, as mentioned. Mobile displays just don't have _any_ extra space at all, so you're left choosing between various unappealing options.</Sidenote>
So we're left with sidenotes, which I consider the crème de la crème of web-based marginalia. So okay, sure, sidenotes are great and all, but how do we actually _do_ them? Well! _wipes imaginary sweat from brow_ It sure was tough, and for a while there I thought I'd never make it through, but I done did figgered it out in the end!<Sidenote>_Narrator:_ He had not figured it out. He had googled around until he found someone else who had figured it out, and then copied their solution.</Sidenote>
## The Suboptimal Solution: Absolute Positioning
I'm naturally lazy, so I wanted the authoring experience to be as low-friction as possible so that I wouldn't end up foregoing sidenotes just because they were too much of a pain to put in. Since I had already settled on [mdsvex](https://mdsvex.pngwn.io/docs) for authoring my posts, I wanted sidenotes to be just another component that I could throw in mid-stream whenever I had an ancillary thought to express. This meant that DOM-wise, the sidenotes were going to be mixed right in with the main body text. Since I was also hoping to do this in pure CSS,<Sidenote>Because as much as I claim not to care, I secretly fear the Hacker News anti-Javascript brigade and desperately crave their approval.</Sidenote> meant that I was going to have to do something that removed the sidenote from the normal document flow, such as `position: absolute`.
My first approach was something like this:
```css
.sidenote {
position: absolute;
/* 50% takes us to the midpoint of the page,
half of content-width gets out out to the gutter,
and the extra 1rem gives us some breathing room. */
left: calc(50% + var(--content-width) / 2 + 1rem);
max-width: 12rem;
font-size: 0.75rem;
}
```
And it worked! Sort of. Here's an example.<span class="counter"></span><span class="sidenote-absolute">My initial take on sidenotes. Seems to be working, right?</span> Unfortunately it has a major flaw: Absolute positioning removes an element from the document flow _entirely_, while I wanted sidenotes to still flow with _each other_, That doesn't happen with this solution--if you have multiple sidenotes too close together, they will overlap because absolute positioning Just Doesn't Care.<span class="counter"></span><span class="sidenote-absolute">Like this one.</span><span class="counter"><span class="sidenote-absolute" style="transform: translateY(0.2rem)">And this one, which I've moved down just a smidge to make the overlap more apparent.</span>
Obviously, it isn't that hard to just scan through the page looking for sidenotes, detect when they overlap, and then (since they're already absolutely positioned) adjust their `top` values appropriately to get rid of the overlap. But I didn't want to do this for a variety of reasons.
* I wanted to write this as a Svelte component, which means that's the obvious place to put this logic. But because there are many instances of the component and I only want to run the collision-detection logic once, it has to be coordinated across multiple instances of the same component, which is painful.
* Because we have to wait for the sidenote elements to _have_ concrete positions before we can detect whether they collide, we can't do this until they are mounted (i.e. inserted into the DOM). I was concerned that this would cause [FOUC](https://en.wikipedia.org/wiki/Flash_of_unstyled_content)-like problems, although in retrospect I don't actually recall it happening.<Sidenote>Possibly it was mitigated by the way Svelte batches DOM updates.</Sidenote>However, since I was always planning on static-rendering the site and letting SvelteKit do client-side hydration on page load, I don't think the possibility could ever be ruled out entirely.
* Anything that triggered a reflow could cause the text to move around, but the sidenotes might not follow suit.<Sidenote>Specifically: sidenotes that had been adjusted to get rid of overlap would stay where they were, because they would already have an explicit `top` property. Sidenotes that hadn't been adjusted would move up and down as text reflowed, but this meant they could end up overlapping again.</Sidenote> [There are a lot of things that can cause a reflow](https://gist.github.com/paulirish/5d52fb081b3570c81e3a),<Sidenote>And this is just the ones that come from Javascript! It doesn't even address stuff like resizing the window or expanding/collapsing a `<details>` element.</Sidenote> and I'd have to listen to all of them if I wanted this to be a fully general solution. Sure, I could just be aware of this problem and avoid using reflow-causing events where possible--but I wanted the freedom to be able to add as much interactivity as I felt like to any given blog post without having to worry.
None of these problems are _completely_ inaddressible, but it was all going to be very fiddly to fix properly, so I decided to do a bit more research before throwing in the towel. And boy am I glad that I did, because it turns out that with enough...
## CSS Wizardry
...anything is possible.
Eventually I ran across [this post](https://scripter.co/sidenotes-using-only-css/), which solved my problem almost perfectly. The basic idea is extremely straightforward:
1. Give your sidenotes a `float` and `clear` in the same direction, so that they are removed from the regular document flow _but_ (and this is crucual) _they will still take each other into account for layout purposes._
2. Give them a fixed width, and then:
3. Give them a negative margin equal to the max-width, so that they are pulled out of the body of the text and hang out in the gutter.
It's shockingly simple, to be honest--I would never have thought of it myself, but I'm glad somebody out there did.<Sidenote>It's worth noting that this same approach seems to be used by [Tufte CSS](https://edwardtufte.github.io/tufte-css/), which I had looked at previously but had failed to comprehend, possibly because it doesn't really go into detail about its sidenote mechanism.</Sidenote> The only problem is that you can't nest sidenotes, which is something I had hoped to support, but we'll get to that in a bit.
## Implementation
It took me quite a while (longer than it should have, probably) to really grok this, so I wanted to go through the implementation step-by-step and show the effect of each component part. For starters, let's just get the basic appearance out of the way:
```css
body {
counter-reset: sidenote;
}
.counter {
counter-increment: sidenote;
margin-left: 0.05rem;
}
.counter::after {
content: counter(unstyled-sidenote);
font-size: 0.75em;
position: relative;
bottom: 0.3em;
color: var(--accent-color);
}
.sidenote {
color: var(--content-color-faded);
font-size: 0.8rem;
}
.sidenote::before {
content: counter(unstyled-sidenote);
font-size: 0.75rem;
color: var(--accent-color);
/* Since the sidenote is floated it counts as a positioned element,
so this would make the counter overlap the start of the text... */
position: absolute;
/* ...except that we move it out to the left and up a bit, so
it's hanging out in space. 100% refers to the width of this
pseudo-element, so we handle different-sized counters the same. */
transform: translate(
calc(-100% - 0.16em),
-0.12em
);
}
```
This handles font size, color, and counters--CSS counters are very convenient for this, because they automatically adjust themselves whenever I go back and add or remove a sidenote earlier in the page. That gives us sidenote that looks like this:<UnstyledSidenote floatingCounter={false}>We're going to use a different color counter for these ones, so they can be more easily distinguished.</UnstyledSidenote>
It's still in flow, so our first change will be to remove it from the standard flow with `float: right`. Doing that moves it over to the side, like so.<UnstyledSidenote class="sn-float">The float also unmoors it from the text baseline.</UnstyledSidenote> Notice how it still takes up space in the body text, even though it's happening in a different place than its DOM location.
To keep it from doing that, we'll add a combination of a fixed width and a negative margin. The fixed width is primarily to give us a "target" number for the negative margin, since there isn't a good way to reference the width of the _current_ item when defining margins. (`margin-right: 100%` would move it by the width of the _containing_ block, which is not what we want.) With that in place, here's what we get.<UnstyledSidenote class="sn-gutter">Looking pretty good!</UnstyledSidenote> Unfortunately this example and subsequent ones don't work on mobile, since there are no gutters. Sorry about that! You'll have to view the desktop version to make them show up.
The next step is to keep the sidenotes from overlapping when there are multiple of them in quick succession, like these two.<UnstyledSidenote class="sn-gutter">This is one sidenote.</UnstyledSidenote><UnstyledSidenote class="sn-gutter">Another sidenote, which overlaps the first.</UnstyledSidenote> We do that with the `clear` property, which, when applied to a floated element, causes it to drop below any preceding floated elements on the specified side with which it would otherwise share a line.
This is easiest to show with an example, so let's do that. Here are two sidenotes with just `float: right` and no negative margin.<UnstyledSidenote class="sn-float">One.</UnstyledSidenote><UnstyledSidenote class="sn-float">Two.<span style="margin-right: 0.75rem"></span></UnstyledSidenote> [[Click here]] to animate the negative margin being applied to first the one, then the other. Applying negative margin to the first sidenote creates space for the other one to move to the side, since by nature floats want to form a horizontal row against the side of their containing block. Once we start applying negative margin to the second sidenote, though, normal flow rules don't apply, and they start to overlap.
This is fixed by `clear` because it changes the behavior of floats. Here are the same two sidenotes as above, but with `clear: right` applied to the second.<UnstyledSidenote class="sn-float">One.</UnstyledSidenote><UnstyledSidenote class="sn-clear">Two.</UnstyledSidenote> The `clear` property causes the second sidenote to drop below the first, which happens to be exactly the behavior that we want. All that's left is to apply the negative margin like so<UnstyledSidenote class="sn-clear sn-gutter">Three.</UnstyledSidenote><UnstyledSidenote class="sn-clear sn-gutter">Four.</UnstyledSidenote>and the whole stack will slide right over into the gutter.
It's smack up against the body text, though. In fact, since the floating counter hangs off to the left, it actually overlaps with the body text.(Depending on line wrapping, this may not be immediately apparent from the above.)
We can fix that in one of two ways. 1) We can increase the negative margin so that it's _greater_ than the width of the sidenote, or 2) We can just stick in some padding.<UnstyledSidenote class="sn-gap">Voila! Collision avoided.</UnstyledSidenote> I like the first option better, because it better reflects what we're actually doing here--margin is for creating caps _outside_ and _between_ elements, while padding is for gaps _inside_.
Here's what we have so far:
```css
.sidenote {
float: right;
width: 14rem;
margin-right: -16rem;
}
```
We still have a bit of a problem, though. Because we've assigned the sidenote a fixed width, it doesn't automatically shrink when the window gets too small for it. Obviously, of course, at _some_ point we're going to switch to the mobile version, which displays at the bottom of the screen and can be toggled on or off. But there are plenty of widths where sidenotes would still work perfectly well, just with a slightly narrower width than our initial `14rem`.
Fortunately, CSS `calc()` is widely supported and does exactly what we need.<UnstyledSidenote class="sn-var-width">Here we are! You may need to resize your window to get full effect.</UnstyledSidenote> Let's take a look:
```css
.sidenote {
float: right;
--width: min(
14rem,
calc( (100vw - var(--content-width) ) / 2 - 2rem )
);
width: var(--width);
margin-right: calc(0rem - var(--width) - 2rem);
}
```
To calculate the width, we take the full viewport (`100vw`) and subtract the width of the main column (`var(--content-width)`). This gives us the combined width of both gutters, but since we only want the width of a single gutter we divide by 2. Then we subtract a further `2rem` so that our width is a little less than the full width of ther gutter, to give us some breathing room.
For the margin, we just take the value we calculated for the width and subtract it from 0 (to make it negative), then subtract a further 2rem to pull the sidenote out by that much more to give us breathing room.

View File

@ -0,0 +1,165 @@
---
title: Thoughts on Vue vs Svelte
description: They're more similar than they are different, but they say the most bitter enemies are those who have the fewest differences.
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>
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
* Automatically reactive data bindings
* Two-way data binding (a point of almost religious contention in certain circles)
* An "HTML-first" mindset, as compared to the "Javascript-first" mindset found in React and its ilk. The best way I can describe this is by saying that in Vue and Svelte, the template<Sidenote>Or single-file component, anyway.</Sidenote> embeds the logic, whereas in React, the logic embeds the template.
I should also note that everything I say about Vue applies to the Options API unless otherwise noted, because that's all I've used. I've only seen examples of the Composition API (which looks even more like Svelte, to my eyes), I've never used it myself.
With that said, there are plenty of differences between the two, and naturally I find myself in possession of immediate and vehement Preferences.<Sidenote>I should also clarify that practically everything in this post is just that: a preference. While I obviously plan to explain my preferences and think it would be reasonable for other people to do the same, it's undeniably true that preferences can vary, and in a lot of cases are basically arbitrary. So if you find yourself disagreeing with all or most of what I say, consider it an opportunity to peer into the mindset of The Other Side.</Sidenote> Starting with:
## Template Syntax
Overall I think I favor Vue here. Both Vue and Svelte expect you to write most of your code in "single-file components", which are collections of markup, style, and logic<Sidenote>Much like a traditional HTML page.</Sidenote> that work together to describe the appearance and behavior of a component. But naturally, they do it slightly differently. Vue adds custom vue-specific attributes directly to the HTML elements, such as:
```markup
<div v-if="items.length">
<p>Please choose an item.</p>
<ul>
<li v-for="item in items">{{ item.name }}</li>
</ul>
</div>
<div v-else>
<p>There are no items available.</p>
</div>
```
While Svelte takes the more common approach of wrapping bits of markup in its own templating constructs:
```svelte
{#if items.length}
<div>
<p>Please choose an item</p>
<ul>
{#each items as item}
<li>{item.name}</li>
</ul>
</div>
{:else}
<div>
<p>There are no items available.</p>
</div>
```
While Vue's approach may be a tad unorthodox, I find that I actually prefer it in practice. It has the killer feature that, by embedding itself inside the existing HTML, it doesn't mess with my indentation - which is something that has always bugged me about Mustache, Liquid, Jinja, etc.<Sidenote>Maybe it's silly of me to spend time worrying<Sidenote>Nested<Sidenote>Doubly-nested sidenote!</Sidenote> sidenote!</Sidenote> about something so trivial,<Sidenote>Second nested sidenote.</Sidenote> but hey, this whole post is one big bikeshed anyway.</Sidenote>
Additionally (and Vue cites this as the primary advantage of its style, I think) the fact that Vue's custom attributes are all syntactically valid HTML means that you can actually embed Vue templates directly into your page source. Then, when you mount your app to an element containing Vue code, it will automatically figure out what to do with it.<Sidenote>AlpineJS also works this way, but this is the *only* way that it works - it doesn't have an equivalent for Vue's full-fat "app mode" as it were.</Sidenote> This strikes me as a fantastic way to ease the transition between "oh I just need a tiny bit of interactivity on this page, so I'll just sprinkle in some inline components" and "whoops it got kind of complex, guess I have to factor this out into its own app with a build step and all now."
Detractors of this approach might point out that it's harder to spot things like `v-if` and `v-for` when they're hanging out inside of existing HTML tags, but that seems like a problem that's easily solved with a bit of syntax highlighting.<Sidenote>I'm being unfair here. It's more than just a lack of syntax highlighting, it's a reversal of the typical order in which people are used to reading code, where the control flow is indicated before whatever it's controlling. So you end up with a sort of [garden-path-like](https://xkcd.com/2793/) problem where you have to mentally double back and re-read things in a different light. I still don't think it's a huge issue, though, because in every case I'm come across the control flow bits (so `v-if`, `v-for`, and `v-show`) are specified _immediately_ after the opening tag. So you don't really have to double back by an appreciable amount, and it doesn't take too long to get used to it.</Sidenote>
Continuing the exploration of template syntax, Vue has some cute shorthands for its most commonly-used directives, including `:` for `v-bind` and `@` for `v-on`. Svelte doesn't really have an equivalent for this, although it does allow you to shorten `attr={attr}` to `{attr}`, which can be convenient. Which might as well bring us to:
## Data Binding
I give this one to Svelte overall, although Vue has a few nice conveniences going for it.
Something that threw me a tiny bit when I first dug into Vue was that you need to use `v-bind` on any attribute that you want to have a dynamic value. So for instance, if you have a data property called `isDisabled` on your button component, you would do `<button v-bind:disabled="isDisabled">` (or the shorter `<button :disabled="isDisabled">`).
The reason this threw me is that Svelte makes the very intuitive decision that since we already have syntax for interpolating variables into the text contents of our markup, we can just reuse the same syntax for attributes. So the above would become `<button disabled={isDisabled}>`, which I find a lot more straightforward.<Sidenote>If your interpolation consists of a single expression you can even leave off the quote marks (as I did here), which is pleasant since you already have `{}` to act as visual delimiters.</Sidenote> I also find it simpler in cases where you want to compose a dynamic value out of some fixed and some variable parts, e.g. `<button title="Save {{itemsCount}} items">` vs. `<button :title="&#96;Save ${itemsCount} items&#96;">`.
Two-way bindings in Svelte are similarly straightforward, for example: `<input type="checkbox" bind:checked={isChecked}>` In Vue this would be `<input type="checkbox" v-model="isChecked">`, which when you first see it doesn't exactly scream that the value of `isChecked` is going to apply to the `checked` property of the checkbox. On the other hand, this does give Vue the flexibility of doing special things for e.g. the values of `<select>` elements: `<select v-model="selectedOption">` is doing quite a bit of work, since it has to interact with not only the `<select>` but the child `<option>`s as well. Svelte just throws in the towel here and tells you to do `<select bind:value={selectedOption}>`, which looks great until you realize that `value` isn't technically a valid attribute for a `<select>`. So Svelte's vaunted principle of "using the platform" does get a _little_ bent out of shape here.
Oh, and two-way bindings in Vue get _really_ hairy if it's another Vue component whose attribute you want to bind, rather than a builtin form input. Vue enforces that props be immutable from the inside, i.e. a component isn't supposed to mutate its own props. So from the parent component it doesn't look too bad:
```markup
<ChildComponent v-model="childValue" />`
```
But _inside_ the child component:
```js
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
methods: {
doThing() {
this.$emit('update:modelValue', newValue)
}
}
}
```
In Svelte, you just `bind:` on a prop of a child component, and then if the child updates the prop it will be reflected in the parent as well. I don't think there's any denying that's a lot simpler.<Sidenote>I think this is where the "two-way data binding" holy wars start to get involved, but I actually really like the way Svelte does things here. I think most of the furor about two-way data binding refers to bindings that are _implicitly_ two-way, i.e. the child can mutate state that the parent didn't expect or intend it to. In Svelte's case, though, this is only possible if you explicitly pass the state with `bind:`, which signifies that you _do_ want this state to be mutated by the child and that you have made provisions therefor. </Sidenote>
Vue does have some lovely convenience features for common cases, though. One of my favorites is binding an object to the `class` of an HTML element, for example: `<button :class="{btn: true, primary: false}">` Which doesn't look too useful on its own, but move that object into a data property and you can now toggle classes on the element extremely easily by just setting properties on the object. The closest Svelte comes is `<button class:btn={isBtn} class:primary={isPrimary}>`, which is a lot more verbose. Vue also lets you bind an array to `class` and the elements of the array will be treated as individual class names, which can be convenient in some cases if you have a big list of classes and you're toggling them all as a set.
The other area where I vastly prefer Vue's approach over Svelte's is in event handlers. Svelte requires that every event handler be a function, either named or inline, so with simple handlers you end up with a lot of `<button on:click={() => counter += 1}` situations. Vue takes the much more reasonable approach of letting you specify a plain statement as your event handler, e.g. `<button @click="counter += 1">`. For whatever reason this has always particularly annoyed me about Svelte, so Vue's take is very refreshing.
Admittedly, the Svelte approach does lead more gracefully into more complex scenarios where you need to capture the actual JS event: it just gets passed to the function. Vue kind of capitulates on consistency here and _also_ lets you pass the name of a function to an event handler, which is then called with the event as an argument. _Oooor_, you can reference the event via the special variable `$event`, which is convenient but feels a bit shoehorned in.
I'm ragging on Vue for its inconsistency here but I should note that I still do prefer the Vue approach, warts and all. "A foolish consistency is the hobgoblin of small minds," after all, and Vue's syntax is just so _convenient_. Besides, it optimizes for the 95% of the time I don't care about capturing the event, because realistically when am I going to want to do that? In both Vue and Svelte, all the traditional use cases for capturing an event are solved in other ways:
* You don't usually need `event.target`, because you can just give yourself a handle to the element directly (via `ref` in Vue, `bind:this=` in Svelte)
* You don't need to use it to get the value of an input (common with events like `change`), because you're just going to use a two-way binding for that
* In Vue, you don't even need it to check for modifier keys, because Vue gives you special syntax for this like `@event.shift`. (Svelte doesn't have an equivalent for this, so advantage Vue here again.)
You really only need to access the event when you're doing something more exotic, e.g. handling a bubbling event on a parent element and you need to check which child was actually the target, which does happen but again not the _majority_ of the time.
## Declaring Reactive Values
In Vue, reactive values (by which I mean "values that can automatically trigger a DOM update when they change") are either passed in as `props`, or declared in `data`. Or derived from either of those sources in `computed`. Then you reference them, either directly in your template or as properties of `this` in your logic. Which works fine, more or less, although you can run into problems if you're doing something fancy with nested objects or functions that get their own `this` scope.<Sidenote>It's worth noting that the Composition API avoids this, at the cost of having to call `ref()` on everything and reference `reactiveVar.value` rather than `reactiveVar` by itself.</Sidenote> The split between how you access something from the template and how you access it from logic was a touch surprising to me at first, though.
In Svelte, variables are just variables, you reference them the same way from everywhere, and if they need to be reactive it (mostly) just happens automagically. Svelte has a lot more freedom here because it's a compiler, rather than a library, so it can easily insert calls to its special `$$invalidate()` function after any update to a value that needs to be reactive.
Both frameworks allow you to either derive reactive values from other values, or just execute arbitrary code in response to data updates. In Vue these are two different concepts - derived reactive values are declared in `computed`, and reactive statements via the `watch` option. In Svelte they're just the same thing: Prefix any statement with `$:` (which is actually valid JS, as it turns out) and it will automatically be re-run any time one of the reactive values that it references gets updated. So both of the following:
```js
$: let fullname = `${firstname} ${lastname}`;
$: console.log(firstname, lastname);
```
would re-run any time `firstname` or `lastname` is updated, assuming those are reactive values to begin with.
Overall I tend to prefer the simplicity of Svelte's approach to reactivity, although I do find the `$:` syntax a little weird. It may be valid JS, but it's not valid JS that anybody actually _uses_. Moreover its official meaning doesn't have anything to do with what Svelte is using it for, so the fact that iT's vAliD jAVaSCriPt doesn't really do much for me. I think Vue's `computed` and `watch` options are much more obvious, if only from how they're named.
That said, I don't have any better ideas for marking reactive statements in Svelte, especially given that sometimes you _want_ a statement to ignore updates even if it does reference a value that might be updated. So maybe this is just one of those compromises you have to make.
## Code Structure
I go back and forth on this one, but I _think_ I have a slight preference for Svelte (at least, at the moment.) The major difference is that Vue<Sidenote>If you're using the Options API, at least.</Sidenote> enforces a lot more structure than Svelte: Data is in `props`/`data`/`computed`, logic is in `methods`, reactive stuff is in `watch`, etc. Svelte, by contrast, just lets you do basically whatever you want. It does require that you have only one `<script>` tag, so all your logic ends up being co-located, but that's pretty much it. Everything else is just a convention, like declaring props at the top of your script.
The advantage of Vue's approach is that it can make it easier to find things when you're jumping from template to logic: you see `someFunction(whatever)`, you know it's going to be under `methods`. With Svelte, `someFunction` could be defined anywhere in the script section.
On the other hand, this actually becomes a downside once your component gets a little bit complex. Separation of concerns is nice and all, but sometimes it just doesn't work very well to split a given component, and it ends up doing several unrelated or at least clearly distinct things. In Vue-land, the relevant bits of state, logic, etc. are all going to be scattered across `data`/`methods`/etc, meaning you can't really see "all the stuff that pertains to this one bit of functionality" in one place. It's also very clunky to split the logic for a single component across multiple JS files, which you might want to do as another way of managing the complexity of a large component. If you were to try, you'd end up with a big "skeleton" in your main component file, e.g.
```js
export default {
import {doThing, mungeData} from './otherfile.js';
// ...
computed: {
mungeData,
// ...
}
methods: {
doThing,
// ...
},
}
```
which doesn't seem very pleasant.
As a matter of fact, this was one of the primary [motivations](https://web.archive.org/web/20201109010309/https://composition-api.vuejs.org/#logic-reuse-code-organization) for the introduction of the Composition API in the first place.<Sidenote>Archive link, since that url now redirects to the [current Composition API FAQ](https://vuejs.org/guide/extras/composition-api-faq.html).</Sidenote> Unfortunately it also includes the downside that you have to call `ref()` on all your reactive values, and reference them by their `.value` property rather than just using the main variable. It's funny that this bothers me as much as it does, given that `this.someData` is hardly any more concise than `someData.value`, but there's no accounting for taste, I guess. Using `this` just feels more natural to me, although what feels most natural is Svelte's approach where you don't have to adjust how you reference reactive values at all.
Also, as long as we're harping on minor annoyances: For some reason I cannot for the life of me remember to put commas after all my function definitions in `computed`, `methods` etc. in my Vue components. It's such a tiny thing, but it's repeatedly bitten me because my workflow involves Vue automatically rebuilding my app every time I save the file, and I'm not always watching the console output because my screen real estate is in use elsewhere.<Sidenote>E.g. text editor on one screen with two columns of text, web page on one half of the other screen and dev tools on the other half. Maybe I need a third monitor?</Sidenote> So I end up forgetting a comma, the rebuild fails but I don't notice, and then I spend five minutes trying to figure out why my change isn't taking effect before I think to check for syntax errors.
It would be remiss of me, however, not to point out that one thing the Vue Options API enables<Sidenote>Kind of its initial _raison d'être_, from what I understand.</Sidenote> which is completely impossible with Svelte is at-runtime or "inline" components, where you just stick a blob of JS onto your page that defines a Vue component and where it should go, and Vue does the rest on page load. Svelte can't do this because it's a compiler, so naturally it has to compile your components into a usable form. This has many advantages, but sometimes you don't want to or even _can't_ add a build step, and in those cases Vue can really shine.
## Miscellany
### Performance
Performance isn't really a major concern for me when it comes to JS frameworks, since I don't tend to build the kind of extremely-complex apps where the overhead of the framework starts to make a difference. For what it's worth, though, the [Big Benchmark List](https://krausest.github.io/js-framework-benchmark/current.html) has Vue slightly ahead of Svelte when it comes to speed.<Sidenote>Although [recent rumors](https://twitter.com/Rich_Harris/status/1688581184018583558) put the next major version of Svelte _very_ close to that of un-framework'd vanilla JS, so this might change in the future.</Sidenote> I don't know how representative this benchmark is of a real-world workload.
As far as bundle size goes, it's highly dependent on how many components you're shipping - since Svelte compiles everything down to standalone JS and there's no shared framework, the minimum functional bundle can be quite small indeed. The flipside is that it grows faster with each component than Vue, again because there's no shared framework to rely on. So a Svelte app with 10 components will probably be a lot smaller than the equivalent Vue app, but scale that up to 1000 components and the advantage will most likely have flipped.
### Ecosystem
Vue has been around longer than Svelte, so it definitely has the advantage here. That said, Svelte has been growing pretty rapidly in recent years and there is a pretty decent ecosystem these days. This blog, for instance, uses [SvelteKit](https://kit.svelte.dev) and [mdsvex](https://mdsvex.pngwn.io/). But there are definitely gaps, e.g. I wasn't able to find an RSS feed generator when I went looking.<Sidenote>Arguably this is a lack in the SvelteKit ecosystem rather than the Svelte ecosystem, but I think it's fair to lump it together. SvelteKit is dependent on Svelte, so naturally it inherits all of Svelte's immaturity issues plus more of its own.</Sidenote> If I'd been using Vue/Nuxt it would have been available as a [first-party integration](https://content.nuxtjs.org/v1/community/integrations). All in all I'd say if a robust ecosystem is important to you then Vue is probably the better choice at this point.

Binary file not shown.

View File

@ -7,12 +7,28 @@
font-display: block;
}
@font-face {
font-family: 'Baskerville';
font-style: normal;
font-weight: 400;
src: url(/Baskerville-Regular.woff2) format('woff2');
font-display: block;
}
:root {
--content-size: 1.25rem;
--content-line-height: 1.3;
--content-color: #1e1e1e;
--content-color-faded: #555;
--accent-color: #8c0606;
}
html {
font-family: 'Tajawal', sans-serif;
font-size: 20px;
line-height: 1.3;
font-size: var(--content-size);
line-height: var(--content-line-height);
letter-spacing: -0.005em;
color: #1e1e1e;
color: var(--content-color);
}
body {