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> &amp;<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>) -&gt; <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) -&gt; 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 &amp; 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>