main
1<!DOCTYPE html>
2<html lang="en">
3 <head>
4 <meta name="viewport" content="width=device-width, initial-scale=1">
5 <meta charset="utf-8">
6 <meta name="viewport" content="width=device-width, initial-scale=1">
7 <title>
8 Flux — Design Sandbox
9 </title>
10 <meta name="author" content="Vincent Demeester">
11 <meta name="generator" content="Org Mode">
12 <script src="/flux.js" defer=""></script>
13 <link rel="stylesheet" href="/style.css" type="text/css">
14 </head>
15 <body>
16 <div class="site-controls">
17 <button class="theme-toggle" id="theme-toggle" title="Toggle dark/light mode">☀️</button>
18 </div>
19 <article id="content" class="content">
20 <header>
21 <h1 class="title">
22 Flux — Design Sandbox
23 </h1>
24 <p class="subtitle" role="doc-subtitle">
25 Exploring typography, rhythm, and stream design for vincent.demeester.fr
26 </p>
27 </header>
28 <section id="outline-container-typography" class="outline-2">
29 <h2 id="typography">
30 <a href="#typography" class="anchor">§</a>Typography<span class="tags"><a href="/flux/tags/design.html" class="tag">design</a><a href="/flux/tags/css.html" class="tag">css</a></span>
31 </h2>
32 <div id="text-typography" class="outline-text-2"></div>
33 <div id="outline-container-org7f16d57" class="outline-3">
34 <h3 id="dropcap">
35 <a href="#dropcap" class="anchor">§</a>Dropcap
36 </h3>
37 <div id="text-dropcap" class="outline-text-3">
38 <p class="dropcap">
39 Gardens can be very personal and full of whimsy or a garden can be a source of food and substance. This is my personal space on the World Wide Web. It is meant to be simple, <em>modest</em> and <strong>persistent</strong> — by persistent, it means that I am trying to not break URIs.
40 </p>
41 </div>
42 </div>
43 <div id="outline-container-org99c01cb" class="outline-3">
44 <h3 id="small-caps">
45 <a href="#small-caps" class="anchor">§</a>Small Caps
46 </h3>
47 <div id="text-small-caps" class="outline-text-3">
48 <p>
49 This website uses <span class="smallcaps">Tufte CSS</span> as its foundation, taking cues from <span class="smallcaps">Edward Tufte</span>'s principles. The <span class="smallcaps">HTML</span> is semantic and the <span class="smallcaps">CSS</span> is minimal.
50 </p>
51 </div>
52 </div>
53 </section>
54 <section id="outline-container-links" class="outline-2">
55 <h2 id="links">
56 <a href="#links" class="anchor">§</a>Links
57 </h2>
58 <div id="text-links" class="outline-text-2">
59 <p>
60 Links are styled following <a href="https://adactio.com/journal/22084">Adactio's guidance</a>: subtle underline offset, thin decoration, translucent color. Hover reveals full underline.
61 </p>
62 </div>
63 </section>
64 <section id="outline-container-blockquotes" class="outline-2">
65 <h2 id="blockquotes">
66 <a href="#blockquotes" class="anchor">§</a>Blockquotes
67 </h2>
68 <div id="text-blockquotes" class="outline-text-2"></div>
69 <div id="outline-container-orgee35afe" class="outline-3">
70 <h3 id="standard">
71 <a href="#standard" class="anchor">§</a>Standard
72 </h3>
73 <div id="text-standard" class="outline-text-3">
74 <blockquote>
75 <p>
76 While not everybody has or works in a dirt garden, we all share a familiarity with the idea of what a garden is. A garden is usually a place where things grow.
77 </p>
78 <p>
79 — Joel Hooks
80 </p>
81 </blockquote>
82 </div>
83 </div>
84 <div id="outline-container-org0ce0959" class="outline-3">
85 <h3 id="epigraph">
86 <a href="#epigraph" class="anchor">§</a>Epigraph
87 </h3>
88 <div id="text-epigraph" class="outline-text-3">
89 <p>
90 A blockquote with a <code>#+CAPTION</code> becomes an epigraph in ox-tufte:
91 </p>
92 <blockquote>
93 <p>
94 The phrase "digital garden" is a metaphor for thinking about writing and creating that focuses less on the resulting "content", and more on the process, care, and craft it takes to get there.
95 </p>
96 <footer>
97 Joel Hooks
98 </footer>
99 </blockquote>
100 </div>
101 </div>
102 </section>
103 <section id="outline-container-code-tonsky-style" class="outline-2">
104 <h2 id="code-tonsky-style">
105 <a href="#code-tonsky-style" class="anchor">§</a>Code — Tonsky-style<span class="tags"><a href="/flux/tags/tonsky.html" class="tag">tonsky</a><a href="/flux/tags/syntax.html" class="tag">syntax</a></span>
106 </h2>
107 <div id="text-code-tonsky-style" class="outline-text-2">
108 <p>
109 Following <a href="https://tonsky.me/blog/syntax-highlighting/">Tonsky's principles</a>: only 4 semantic categories get color.
110 </p>
111 </div>
112 <div id="outline-container-org9beed48" class="outline-3">
113 <h3 id="go">
114 <a href="#go" class="anchor">§</a>Go
115 </h3>
116 <div id="text-go" class="outline-text-3">
117 <div class="org-src-container">
118 <pre class="src src-go"><code><span class="org-comment">// Entry represents a single item in the flux stream.</span>
119<span class="org-comment">// It can be a GitHub PR, a bookmark, a note, etc.</span>
120<span class="org-keyword">type</span> <span class="org-type">Entry</span> <span class="org-keyword">struct</span> {
121 <span class="org-property-name">ID</span> <span class="org-type">string</span> <span class="org-string">`json:"id"`</span>
122 <span class="org-property-name">Kind</span> <span class="org-type">string</span> <span class="org-string">`json:"kind"`</span>
123 <span class="org-property-name">Title</span> <span class="org-type">string</span> <span class="org-string">`json:"title"`</span>
124 <span class="org-property-name">URL</span> <span class="org-type">string</span> <span class="org-string">`json:"url"`</span>
125 <span class="org-property-name">Date</span> <span class="org-type">time</span>.<span class="org-type">Time</span> <span class="org-string">`json:"date"`</span>
126 <span class="org-property-name">Tags</span> []<span class="org-type">string</span> <span class="org-string">`json:"tags"`</span>
127 <span class="org-property-name">Body</span> <span class="org-type">string</span> <span class="org-string">`json:"body,omitempty"`</span>
128} ¤ type Entry struct {
129
130<span class="org-keyword">func</span> <span class="org-function-name">NewEntry</span>(<span class="org-variable-name">kind</span> <span class="org-type">string</span>, <span class="org-variable-name">title</span> <span class="org-type">string</span>, <span class="org-variable-name">url</span> <span class="org-type">string</span>) *<span class="org-type">Entry</span> {
131 <span class="org-keyword">return</span> &<span class="org-type">Entry</span>{
132 ID: generateID(kind, url),
133 Kind: kind,
134 Title: title,
135 URL: url,
136 Date: time.Now(),
137 }
138} ¤ func NewEntry(kind string, title string, url string) *Entry {
139
140<span class="org-keyword">const</span> (
141 <span class="org-constant">KindGitHubPR</span> = <span class="org-string">"github-pr"</span>
142 <span class="org-constant">KindBookmark</span> = <span class="org-string">"bookmark"</span>
143 <span class="org-constant">MaxRetries</span> = <span class="org-number">3</span>
144 <span class="org-constant">DefaultTimeout</span> = <span class="org-number">30</span> * time.Second
145 <span class="org-constant">EnableCache</span> = <span class="org-constant">true</span>
146)
147</code></pre>
148 </div>
149 </div>
150 </div>
151 <div id="outline-container-orgde0f8b8" class="outline-3">
152 <h3 id="python">
153 <a href="#python" class="anchor">§</a>Python
154 </h3>
155 <div id="text-python" class="outline-text-3">
156 <div class="org-src-container">
157 <pre class="src src-python"><code><span class="org-keyword">import</span> json
158<span class="org-keyword">from</span> pathlib <span class="org-keyword">import</span> Path
159<span class="org-keyword">from</span> datetime <span class="org-keyword">import</span> datetime
160
161<span class="org-comment"># Tonsky: comments deserve bright color.</span>
162<span class="org-comment"># Good comments ADD to code.</span>
163
164<span class="org-keyword">class</span> <span class="org-type">FluxAggregator</span>:
165 <span class="org-doc">"""Merge entries from all sources into a single feed."""</span>
166
167 <span class="org-keyword">def</span> <span class="org-function-name">__init__</span>(<span class="org-keyword">self</span>, <span class="org-variable-name">cache_dir</span>: <span class="org-type">Path</span>, max_entries: <span class="org-type">int</span> = <span class="org-number">500</span>):
168 <span class="org-keyword">self</span>.<span class="org-variable-name">cache_dir</span> = cache_dir
169 <span class="org-keyword">self</span>.<span class="org-variable-name">max_entries</span> = max_entries
170 <span class="org-keyword">self</span>.<span class="org-variable-name">sources</span>: <span class="org-type">list</span>[<span class="org-type">Source</span>] = []
171 <span class="org-keyword">self</span>.<span class="org-variable-name">_last_run</span>: <span class="org-type">datetime</span> | <span class="org-type">None</span> = <span class="org-constant">None</span>
172
173 <span class="org-keyword">def</span> <span class="org-function-name">generate</span>(<span class="org-keyword">self</span>) -> <span class="org-type">list</span>[<span class="org-type">dict</span>]:
174 <span class="org-doc">"""Fetch from all sources, merge, deduplicate, sort."""</span>
175 <span class="org-variable-name">entries</span> = []
176 <span class="org-keyword">for</span> <span class="org-variable-name">source</span> <span class="org-keyword">in</span> <span class="org-keyword">self</span>.sources:
177 <span class="org-variable-name">new</span> = source.fetch(since=<span class="org-keyword">self</span>._last_run)
178 entries.extend(new)
179
180 <span class="org-comment"># Sort reverse-chronological, deduplicate by ID</span>
181 <span class="org-variable-name">seen</span> = <span class="org-builtin">set</span>()
182 <span class="org-variable-name">result</span> = []
183 <span class="org-keyword">for</span> <span class="org-variable-name">e</span> <span class="org-keyword">in</span> <span class="org-builtin">sorted</span>(entries, key=<span class="org-keyword">lambda</span> x: x[<span class="org-string">"date"</span>], reverse=<span class="org-constant">True</span>):
184 <span class="org-keyword">if</span> e[<span class="org-string">"id"</span>] <span class="org-keyword">not in</span> seen:
185 seen.add(e[<span class="org-string">"id"</span>])
186 result.append(e)
187
188 <span class="org-keyword">return</span> result[:<span class="org-number">500</span>] ¤ def generate(self) -> list[dict]:
189</code></pre>
190 </div>
191 </div>
192 </div>
193 <div id="outline-container-org194cf40" class="outline-3">
194 <h3 id="nix">
195 <a href="#nix" class="anchor">§</a>Nix
196 </h3>
197 <div id="text-nix" class="outline-text-3">
198 <div class="org-src-container">
199 <pre class="src src-nix"><code>{ pkgs, lib, config, ... }:
200
201<span class="org-nix-keyword">let</span>
202 <span class="org-comment"># Build the flux binary from source
203</span> <span class="org-nix-attribute">flux</span> = pkgs.buildGoModule <span class="org-nix-keyword">rec</span> {
204 <span class="org-nix-attribute">pname</span> = <span class="org-string">"flux"</span>;
205 <span class="org-nix-attribute">version</span> = <span class="org-string">"0.1.0"</span>;
206 <span class="org-nix-attribute">src</span> = <span class="org-nix-constant">./.</span>;
207 <span class="org-nix-attribute">vendorHash</span> = <span class="org-string">"sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="</span>;
208 };
209<span class="org-nix-keyword">in</span> {
210 <span class="org-nix-attribute">systemd.services.flux-generate</span> = {
211 <span class="org-nix-attribute">description</span> = <span class="org-string">"Generate flux stream"</span>;
212 <span class="org-nix-attribute">serviceConfig</span> = {
213 <span class="org-nix-attribute">Type</span> = <span class="org-string">"oneshot"</span>;
214 <span class="org-nix-attribute">ExecStart</span> = <span class="org-string">"</span><span class="org-nix-antiquote">${</span>flux<span class="org-nix-antiquote">}</span><span class="org-string">/bin/flux generate"</span>;
215 <span class="org-nix-attribute">User</span> = <span class="org-string">"www"</span>;
216 };
217 };
218
219 <span class="org-nix-attribute">systemd.timers.flux-generate</span> = {
220 <span class="org-nix-attribute">wantedBy</span> = [ <span class="org-string">"timers.target"</span> ];
221 <span class="org-nix-attribute">timerConfig.OnCalendar</span> = <span class="org-string">"hourly"</span>;
222 <span class="org-nix-attribute">timerConfig.Persistent</span> = <span class="org-nix-builtin">true</span>;
223 };
224}
225</code></pre>
226 </div>
227 </div>
228 </div>
229 </section>
230 <section id="outline-container-callouts" class="outline-2">
231 <h2 id="callouts">
232 <a href="#callouts" class="anchor">§</a>Callouts
233 </h2>
234 <div id="text-callouts" class="outline-text-2">
235 <p>
236 Callouts use raw HTML blocks for now (until we have a Soupault transform or org macro):
237 </p>
238 <div class="callout callout-note">
239 <div class="callout-title">
240 Note
241 </div>
242 <p>
243 A general note or annotation. Use for supplementary information.
244 </p>
245 </div>
246 <div class="callout callout-tip">
247 <div class="callout-title">
248 Tip
249 </div>
250 <p>
251 Use <code>flux generate --dry-run</code> to preview what entries would be added without writing any files.
252 </p>
253 </div>
254 <div class="callout callout-warning">
255 <div class="callout-title">
256 Warning
257 </div>
258 <p>
259 GitHub's search API is limited to 30 requests/minute even with authentication.
260 </p>
261 </div>
262 <div class="callout callout-danger">
263 <div class="callout-title">
264 Danger
265 </div>
266 <p>
267 Never commit your <code>GITHUB_TOKEN</code> to the repository.
268 </p>
269 </div>
270 </div>
271 </section>
272 <section id="outline-container-sidenotes-and-margin-notes" class="outline-2">
273 <h2 id="sidenotes-and-margin-notes">
274 <a href="#sidenotes-and-margin-notes" class="anchor">§</a>Sidenotes & Margin Notes<span class="tags"><a href="/flux/tags/tufte.html" class="tag">tufte</a></span>
275 </h2>
276 <div id="text-sidenotes-and-margin-notes" class="outline-text-2">
277 <p>
278 Edward Tufte's distinctive style places supplementary information in the margin rather than in footnotes at the bottom of the page<label id="fnr.1" for="fnr-in.1.1471703" class="margin-toggle sidenote-number"><sup class="numeral">1</sup></label><input type="checkbox" id="fnr-in.1.1471703" class="margin-toggle"><span class="sidenote"><sup class="numeral">1</sup>
279This is a numbered sidenote. On wide screens, it floats in the right margin. On narrow screens, it becomes an inline block. The approach follows <a href="https://gwern.net/sidenote">Gwern's sidenote analysis</a>.
280</span>. This keeps the reader's eye on the page instead of bouncing back and forth. The idea is simple: if something is worth saying, say it nearby.
281 </p>
282 <p>
283 Margin notes are similar but unnumbered — useful for brief asides or contextual links.
284 </p>
285 <label for="mn-auto.8101394" class="margin-toggle">⊕</label><input type="checkbox" id="mn-auto.8101394" class="margin-toggle">
286 <div class="marginnote" id="orga55bdd4">
287 <p>
288 This is an unnumbered margin note. Kenneth Reitz calls this "margins are for thinking." These work beautifully with Tufte CSS and ox-tufte.
289 </p>
290 </div>
291 <p>
292 They provide a visual rhythm to the page, filling what would otherwise be empty whitespace with relevant context.
293 </p>
294 <p>
295 A paragraph without sidenotes, for contrast. Notice how the text column stays at a comfortable reading width of about 640px (roughly 65 characters per line), which typographers consider optimal<label id="fnr.2" for="fnr-in.2.2061938" class="margin-toggle sidenote-number"><sup class="numeral">2</sup></label><input type="checkbox" id="fnr-in.2.2061938" class="margin-toggle"><span class="sidenote"><sup class="numeral">2</sup>
296Robert Bringhurst recommends 45–75 characters per line. We aim for ~65. This is also what Gwern targets.
297</span>.
298 </p>
299 </div>
300 </section>
301 <section id="outline-container-figures" class="outline-2">
302 <h2 id="figures">
303 <a href="#figures" class="anchor">§</a>Figures
304 </h2>
305 <div id="text-figures" class="outline-text-2">
306 <figure id="orgf718da5">
307 <img src="placeholder-wide.svg" alt="placeholder-wide.svg" class="org-svg">
308 <figcaption>
309 <span class="figure-number">Figure 1: </span>A standard figure occupies the full content width
310 </figcaption>
311 </figure>
312 <p>
313 A margin figure floats alongside text:
314 </p>
315 <label for="mn-auto.7408686" class="margin-toggle">⊕</label><input type="checkbox" id="mn-auto.7408686" class="margin-toggle">
316 <div class="marginnote" id="org0c2fec5">
317 <figure id="org0b22235">
318 <img src="placeholder-square.svg" alt="placeholder-square.svg" class="org-svg">
319 <figcaption>
320 <span class="figure-number">Figure 2: </span>A small margin figure
321 </figcaption>
322 </figure>
323 </div>
324 <p>
325 This paragraph has a margin figure floating next to it. Margin figures work especially well for small diagrams, icons, or thumbnail images that support but don't dominate the text.
326 </p>
327 </div>
328 <div id="outline-container-org72a3840" class="outline-3">
329 <h3 id="screenshot-style">
330 <a href="#screenshot-style" class="anchor">§</a>Screenshot style
331 </h3>
332 <div id="text-screenshot-style" class="outline-text-3">
333 <figure class="screenshot">
334 <img src="placeholder-wide.svg" alt="A screenshot example">
335 <figcaption>
336 <strong>Figure 3.</strong> Screenshots get a drop shadow and rounded corners.
337 </figcaption>
338 </figure>
339 </div>
340 </div>
341 </section>
342 <section id="outline-container-lists" class="outline-2">
343 <h2 id="lists">
344 <a href="#lists" class="anchor">§</a>Lists
345 </h2>
346 <div id="text-lists" class="outline-text-2">
347 <ul class="org-ul">
348 <li>
349 Item one with a <a href="https://example.com">link</a>
350 </li>
351 <li>
352 Item two with <code>inline code</code>
353 </li>
354 <li>
355 Item three with <b>bold</b> and <i>italic</i>
356 </li>
357 </ul>
358 <p>
359 Ordered:
360 </p>
361 <ol class="org-ol">
362 <li>
363 First step
364 </li>
365 <li>
366 Second step
367 </li>
368 <li>
369 Third step
370 </li>
371 </ol>
372 </div>
373 </section>
374 <section id="outline-container-table" class="outline-2">
375 <h2 id="table">
376 <a href="#table" class="anchor">§</a>Table
377 </h2>
378 <div id="text-table" class="outline-text-2">
379 <table>
380 <colgroup>
381 <col class="org-left">
382 <col class="org-left">
383 <col class="org-left">
384 </colgroup>
385 <thead>
386 <tr>
387 <th scope="col" class="org-left">
388 Reference
389 </th>
390 <th scope="col" class="org-left">
391 Key Takeaway
392 </th>
393 <th scope="col" class="org-left">
394 Applied Here
395 </th>
396 </tr>
397 </thead>
398 <tbody>
399 <tr>
400 <td class="org-left">
401 Gwern
402 </td>
403 <td class="org-left">
404 Aesthetically-pleasing minimalism
405 </td>
406 <td class="org-left">
407 Grayscale, dropcaps
408 </td>
409 </tr>
410 <tr>
411 <td class="org-left">
412 Tufte CSS
413 </td>
414 <td class="org-left">
415 Margins are for thinking
416 </td>
417 <td class="org-left">
418 Sidenotes in margin
419 </td>
420 </tr>
421 <tr>
422 <td class="org-left">
423 Crafting Interpreters
424 </td>
425 <td class="org-left">
426 Book-quality web typography
427 </td>
428 <td class="org-left">
429 Serif body, anchors
430 </td>
431 </tr>
432 <tr>
433 <td class="org-left">
434 Miessler
435 </td>
436 <td class="org-left">
437 Warm parchment, premium fonts
438 </td>
439 <td class="org-left">
440 Color palette
441 </td>
442 </tr>
443 <tr>
444 <td class="org-left">
445 Tietze
446 </td>
447 <td class="org-left">
448 CSS custom props for rhythm
449 </td>
450 <td class="org-left">
451 <code>--lh</code> system
452 </td>
453 </tr>
454 <tr>
455 <td class="org-left">
456 Larlet
457 </td>
458 <td class="org-left">
459 Seasonal list markers
460 </td>
461 <td class="org-left">
462 <code>body:has(time)</code>
463 </td>
464 </tr>
465 </tbody>
466 </table>
467 </div>
468 </section>
469 <section id="outline-container-prose-example" class="outline-2">
470 <h2 id="prose-example">
471 <a href="#prose-example" class="anchor">§</a>Prose Example<span class="tags"><a href="/flux/tags/writing.html" class="tag">writing</a><a href="/flux/tags/garden.html" class="tag">garden</a></span>
472 </h2>
473 <div id="text-prose-example" class="outline-text-2">
474 <p class="dropcap">
475 This section demonstrates how a full article or journal entry would look with this design system. The warm background, serif body text, and generous line-height create a reading experience closer to a book than a typical blog. Every decision — from the font choice to the margin width — serves readability.
476 </p>
477 <p>
478 As I said in "Random thoughts after 2 years," I've been inspired by <a href="https://joelhooks.com/digital-garden">Joel's digital garden</a> article. This space is inspired by <a href="https://www.la-grange.net/">a</a> <a href="https://larlet.fr/david/">lot</a> <a href="https://tomcritchlow.com/">of</a> <a href="https://joelhooks.com/">other</a> spaces, but adapted to my vision.
479 </p>
480 <p>
481 That's why the <i>flux</i> works alongside the garden: the garden holds evergreen pages; the flux captures the flow of time<label id="fnr.3" for="fnr-in.3.1650310" class="margin-toggle sidenote-number"><sup class="numeral">3</sup></label><input type="checkbox" id="fnr-in.3.1650310" class="margin-toggle"><span class="sidenote"><sup class="numeral">3</sup>
482Gwern makes a similar distinction with "tags" vs "essays." Some content is timeless reference, some is a snapshot of thinking at a moment.
483</span>.
484 </p>
485 <p>
486 The flux tool itself is simple Go. The core type is an <code>Entry</code> struct with a <code>Kind</code> discriminator. Each source implements a <code>Fetch</code> method:
487 </p>
488 <div class="org-src-container">
489 <pre class="src src-go"><code><span class="org-comment">// Source is the interface all flux providers implement.</span>
490<span class="org-keyword">type</span> <span class="org-type">Source</span> <span class="org-keyword">interface</span> {
491 <span class="org-function-name">Name</span>() <span class="org-type">string</span>
492 <span class="org-function-name">Fetch</span>(<span class="org-variable-name">ctx</span> <span class="org-type">context</span>.<span class="org-type">Context</span>, <span class="org-variable-name">since</span> <span class="org-type">time</span>.<span class="org-type">Time</span>) ([]<span class="org-type">Entry</span>, <span class="org-type">error</span>)
493}
494</code></pre>
495 </div>
496 <p>
497 The aggregator merges entries, sorts by date, deduplicates by <code>ID</code>, and feeds the result to renderers (JSON Feed, Atom, HTML template). No framework. No JavaScript. Just static files deployed via <code>rsync</code>.
498 </p>
499 <div class="callout callout-design">
500 <div class="callout-title">
501 Design Note
502 </div>
503 <p>
504 As Kenneth Reitz puts it: "If Fly.io disappears tomorrow, I can have this running on a Raspberry Pi in 10 minutes. Portability is a form of independence." Same principle here. The output is pure HTML+CSS that any web server can host. The Go binary is the only moving part.
505 </p>
506 </div>
507 </div>
508 </section>
509 </article>
510 </body>
511</html>