Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The new tab

The new tab is a Svelte 5 app (extension/src/), built with Vite into extension/dist/. App.svelte is the root; everything under it is a Ct* component, styled with Tailwind utilities and nothing else (see Styling).

State

store.svelte.ts holds the whole state as one $state object, handed to every component through Svelte’s context (getContext("ct")):

{
  settings,     // what's saved, below
  scheme,       // the helper's scheme, or null
  helperError,  // why there's no helper, or null
  wallpaper,    // a blob: URL of the caelestia wallpaper, or null
  editing,      // the pen button
  panel,        // the settings panel is open
  focus,        // the editor the panel shows instead of its tabs, or null
}

The settings are one object under storage.local.settings:

{  font: "",                                        // the page's font-family; empty for the default
  panel: "CtSettings",                             // the component that draws settings
  background: { source: "wallpaper" | "colour" | "none", colour, dim, blur },  clock: { … }, toolbar: { side, … }, bookmarks: { items, … },  // each part's settings
  menu: { open, tab, tabs: { CtGitHub: { … }, CtMedia: { … }, … } },
  sites: { enabled, off: [styleId, …], accent, overrides },
  treeStyleTab: { source: "tint" | "wallpaper" | "none", colour, strength, dim, blur },
  css: "",                                         // custom CSS for the new tab
}

Each part’s settings, and each menu tab’s, are filled in from its component’s defaults when settings load, so a component that gains a setting needs no migration. Settings from when the page was a grid of widgets (layout, widgets) keep each part’s settings and drop the placement; the menu widget’s become the menu’s. Settings saved before the Svelte rewrite name a plugin (clock, bookmarks), which is mapped to the component.

Settings are local to the device: storage.local, not storage.sync, and, with the helper, ~/.config/caelestia-tab/settings.json too. The browser’s copy is lost when a temporary add-on is removed, which a browser restart does; the file isn’t, and can live in a dotfiles repo. The background saves every change to the file a second after it’s made (and, when there’s no file yet, the settings it has as soon as the helper connects), and takes the file’s settings when storage has none (a restart, a fresh install) or when the file changed after the last change in the browser (a hand edit), so an older file never rolls back newer edits. Settings, Advanced, All settings shows the whole object as JSON as well.

The page

The page is fixed (CtLayout): the menu’s section above the bookmarks. The section (CtMenu) has a bar along its top, with the menu button and the menu’s tabs on one side and the toolbar (CtToolbar: the bookmarks’ +, the pen, settings) on the other, and under the bar the clock. Opened, the menu is a panel over the whole section, growing out of the menu button (and shrinking back into it), with the open tab under the bar; the clock isn’t drawn while it’s covered. Switching tabs slides the new one in from the side its button is on. Whether the menu is open, and on which tab, is saved, so a new tab opens as the last one was left.

The toolbar’s side puts it on the right (the tabs on the left), the left (the tabs on the right, mirrored so the menu button stays at the edge) or in the middle (the tabs on the left). Ctrl+, opens settings without it.

font is the page’s font, inherited by every part unless it sets its own (the clock’s time and date each can). A font field autocompletes from the installed fonts, each shown in its own face, which the helper lists with fc-list, since a web page can’t; without the helper (or with one from before the fonts plugin) it takes a name typed in. Any text is accepted, a whole font-family list included, and the next font down always follows it: the page’s default after the page font, the page font after a part’s. So a name that isn’t a font, half-typed say, changes nothing. Font sizes are in pt; everything else is in rem.

Edit mode

The pen turns on edit mode, and each part shown gets CtEditOverlay: an outline and a chip with its name, which opens its fields in the side panel (CtPartEditor). The parts are the toolbar, the clock (while the menu is closed), the bookmarks and the open tab. Nothing covered can be edited, since nothing covered is drawn. The bookmarks also do more in edit mode through their editing prop: each tile’s controls.

Saving and syncing

Components change app.settings directly; nothing calls a save. An effect watches the settings and writes them to storage whenever they change, and every open tab’s storage.onChanged listener takes other tabs’ writes.

A tab also hears its own writes come back, and not always in order: a slider writes on every step, and an early step’s echo can arrive after a later step was written. Taken for another tab’s change, it would roll the value back. So each tab remembers what it wrote and ignores those echoes (pending in store.svelte.ts).

A scheme change touches nothing but a <style> of --caelestia-* variables in the head; everything styled with them follows.

The wallpaper arrives as a data: URL, which the store turns into a blob URL once, so the page’s style doesn’t carry a copy of the image as text.

Settings

The settings are a panel docked to the window’s right edge, not a modal: the page moves over to stay in view beside it, so a change shows as it’s made. Every editor opens in the same panel, never in a pop-up: a part’s settings from the pen, a bookmark from its edit button or the +. edit(app, title, component, props) puts one there (app.focus), with a back arrow to the settings, and Escape steps back. Editors change the real settings, not a copy, so the page shows each change as it’s made and there’s nothing to save. A new bookmark is added before its editor opens, so it’s on the page while it’s filled in.

A part’s form is drawn from its component’s fields (see Writing a menu tab): the settings panel knows nothing about clocks or bookmarks. Settings itself has no per-part section: its General tab holds the page’s font, and everything about one part is edited from the page with the pen. The panel is a component named in settings (panel), so it can be replaced.

Styling

The page’s only global CSS is src/app.css: the --caelestia-* variables with fallbacks for before the helper answers, the bundled symbols font, form controls inheriting the page’s font, and Tailwind’s utilities without its reset. Tailwind’s colours are the scheme’s (bg-primary, text-on-surface, border-outline-variant …), declared with @theme inline, so every utility follows the live scheme. Tailwind’s own palette is left out.

The Ct* components use Tailwind classes only. A value only known at run time (a setting, the wallpaper) goes in as a CSS variable read by a class: style:--dim={…} with opacity-(--dim).

Each component also carries a stable ct-* class (ct-tile, ct-clock, ct-bookmarks …) for your custom CSS to target.

The bookmarks’ wrapper gets ct-edge-top, -bottom, -left and -right while it touches that edge of the window (edges in layout.ts, rechecked on resize and zoom), and squares its corners there with in-[.ct-edge-bottom]:rounded-b-none.

Bookmarks

CtBookmarks lays tiles out on a CSS grid whose grid-template-columns, grid-auto-rows, gap and grid-auto-flow are settings, typed as CSS. The Tiles and List presets only fill those fields in. A tile takes span <width> and span <height>, or any grid-column and grid-row value, which override the spans. A value the browser can’t parse is dropped, so a half-typed setting leaves the grid as it was.

With Even rows on (the Tiles default), the columns come from the narrowest tile width instead: as many as fit, then as few as still need that many rows, so five tiles that don’t fit on one row go 3 and 2, never 4 and 1 (evenColumns in layout.ts, tested in tests/extension.test.mjs).

A tile shows its image, or its colour when it has none: a scheme colour, which follows the scheme, or a fixed one from the browser’s colour picker. Text on a scheme colour uses its “on” colour (onPrimary on primary); on a fixed one, black or white, whichever reads better. The line under a tile takes the accent that goes with its colour (complement in scheme.ts: tertiary under primary, a container’s own colour under the container …), a colour of its own, or none, set per bookmark on its Look tab. Uploaded images are scaled to 512 px and stored in settings as WebP data: URLs, which is why the extension asks for unlimitedStorage.

A tile’s mark is either up to a few letters (an emoji works) or a Nerd Font glyph, from the vendored symbols-only font. The glyph field is a search over every glyph, showing all of them until you type; above them, it suggests glyphs from the bookmark’s address and name. The host’s labels, without www and the TLD, and the name’s words are matched against the 11 000 glyph names: a whole-word match (fa-github) ranks above a word inside a longer name (dev-githubactions). glyphs.ts does the matching.

In edit mode each tile also gets a bar under its content to move it earlier or later, drag it (HTML drag and drop, dropping on another tile’s position), edit it, or remove it.

Clock

CtClock shows the time and the date, each with its own separator, size (in pt) and font. The hour can be 12- or 24-hour, with or without a leading zero; the date comes in four styles. Its sizes shrink with a narrow window instead of overflowing it. It renders no time until it’s mounted, since a copy of the page rendered ahead of time can be hours old.