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.