LifeLab Pro · design review
Read against the last three screenshots (Home with tabs, Lens Color Lab at depth 4, the block context menu and the Scheduled picker). Short version: the buffer survived the port intact — org gutter, * stars, :tag: notation, {filter} chips, mono body all still read as LifeLab. What broke is everything above the buffer: three stacked bars that each say “Home”, a toolbar written in mono but shaped like a native toolbar, and system controls arriving in Apple blue while the app is violet. Lens depth is the one content-level regression: colour alone can't carry nesting.
Set) lands in blue. Reads unbranded.Overall: a solid 6. Nothing here is a rebuild — it's four rules away from feeling deliberate. The trade you made (practicality + native surfaces over total mono purity) is the right trade; it just needs to be a stated trade with a boundary, instead of a gradient.
[home] in the nav row is a destination, the Home tab is a held page, * Home is the document's root block. Those are genuinely three different things — the UI currently presents them as three copies of the same word at three sizes.day and clicking the 2026-07-29 tab do nearly the same thing, with different persistence rules. Two rows, one job.edit affordance — they read like table rows dropped into a document, breaking the outline's left edge.{filter} everywhere, including the page title. An unfilled placeholder repeated 4× per screen reads as chrome noise, not as an invitation. Show it on hover/focus, or show it filled or not at all.find ⌘F · go ⌘P is mono, but it behaves like a toolbar; the Context sidebar header is sans-bold; the system menu is SF. The eye can't tell which parts of the app are content and which are controls — mono used to be that signal.The three Homes aren't a bug to delete — they're the app's actual structure poking through. Name it and the redundancy becomes the interface's spine:
“what do I hold open?”
Window state. Nouns — pages you're carrying. Pinned on the left (home, today), transient on the right. Persists across launches. This is the only place a page name is clickable furniture.
“what am I looking at, and through what?”
Active-tab state: path, lens/filter applied, counts, sync, verbs. Everything here changes when you switch tabs and describes a view, not a page. Emacs's modeline, keeping its old job.
“what is this thing?”
Content. * Home {filter} is the page's first block — editable, taggable, foldable, transcludable, exactly like its children. Not a title bar. Already true in your model; make it look true.
The move that pays for itself: home · day week month are not navigation, they are the pinned tabs. Merge the two rows. You lose 36px of chrome, you delete the duplicate concept, and “open” finally means one thing.
now · 3 bars, 2 of them overlapping
“Home” × 3. Nav row wins arguments it shouldn't: clicking day from a 6-tab session either hijacks a tab or silently spawns one.
proposed · tabs in the titlebar, modeline describes the view
day/week/month live as pinned tabs (◷ today re-points itself at midnight). The modeline now says something the tab bar can't: which lens is applied, how much matched. The title stops repeating and just... is the first block.
Variant worth prototyping: put the modeline at the bottom of the window, exactly like the old web build's -- NORMAL -- ? for keys strip. Top = tabs only (pure native), bottom = status + verbs + mode. It restores the one piece of Emacs identity the port dropped, costs nothing in native-ness, and gets the keyboard hints out of the reading area's line of sight.
Right now a tab is a page pointer. Make it page + lens + fold state + scroll + cursor and the whole app gets a second dimension for free — because that bundle is exactly what a LifeLab block can already store.
◷ today tab is the fastest possible “schedule this” — no picker, no menu.Lens Color Lab tab into the Home buffer and you've inserted a transclusion of it at the drop point. The inverse of “extract to subpage”.inbox, project/x, a lens. ⌘1–⌘9 jump to pin n; ⌘⇧[/⌘⇧] cycle.tabs.open(page, {lens=…}), tabs.pin, on_tab_focus, on_tab_close. A morning hook opens today + inbox + the active project; on_tab_focus refreshes an agenda lens. Tab lifecycle is a scriptable event stream.What to guard against: tabs make it cheap to accumulate 20 open pages, and a wiki that punishes you for browsing is a wiki you stop browsing. Two defences: transient tabs (single-click preview reuses one slot, double-click or edit promotes it to permanent — Sublime/VS Code's rule), and an auto-close-after-N-days-untouched setting for unpinned tabs. Pin = intent, tab = attention, neither = history.
You already discovered the ceiling — “nesting beyond three intentionally holds the deepest tone”, depth 4 · clamped at 15%. That's the correct conclusion from the wrong encoding. Tint is a categorical channel (this surface is a lens) and you're using it as an ordinal one (this lens is 3 deep). Split the two:
now · tint carries depth, full bleed, no indent
Four sibling bands at four near-identical greys. Nothing tells you 2 is inside 1 except reading the labels. At depth 3+ the tint difference is below the noise floor of a warm display.
proposed · indent + spine rule, tint clamps at depth 2
Same 16px step as the org gutter, so lenses indent on the same rhythm as headlines. The left rule marks the lens's extent (an org drawer's edge) and fades with depth — an ordinal channel that degrades gracefully instead of clamping.
Three supporting rules. (a) Stop full-bleeding the band — inset it to the content column so the outline's left edge stays a straight line; the lens is in the document, not a row of a table. (b) Move [−]/[+] into the star gutter, vertically aligned with the * of sibling blocks, so folds all live in one column. (c) Replace the right-aligned edit with the match count (always useful, and it's the one number a lens should always show); edit appears on hover in its place, or lives in the right-click menu you already have. And auto-fold lenses at depth ≥3 — nesting that deep is almost always for structure, not reading.
The port's implicit rule is “native where it's cheaper”. Make it an explicit one: anything about the app is native SF; anything about your content is mono. A menu is app. A tag inside a menu item is content. That single line resolves nearly every inconsistency in the current build.
29 selection and Set button are the loudest unbranded pixels in the app.Make TODO keeps TODO mono/red, since it's a content token.Context is sans-bold today) — it's a buffer, so mono it, or make the whole sidebar chrome-styled. Pick one.system default
tinted + mono numerals, org-aware footer
the menu, mostly as-is
Four edits, no redesign: shortcuts on every item (a menu is the best keyboard-shortcut teacher you have); tab verbs added — “open in new tab / split” is the menu's payoff for the new model; one destructive item, separated, at the bottom — Dismiss from page and Delete Block currently sit two rows apart and one is recoverable while the other isn't, so put dismiss up with Collapse where it belongs; content tokens in mono (TODO, tag names in the submenu). Trailing … on anything that opens a picker, as macOS expects.
{filter} unless the block is hovered, focused, or actually filtered. Replace the lens's edit with a match count.tabs.* Lua API + on_tab_focus hooks. Then ⌥click = split.Nothing above asks the app to be more mono than it is now. The port didn't lose the aesthetic — it lost the rule that generated the aesthetic, which was “mono means content”. Restate that rule, give the three bars three different jobs, and let position (not colour) carry depth, and the native build reads more intentional than the web one did.