LifeLab Pro · design review

The native shell is right. The chrome hasn't decided what it is yet.

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.

1 · scorecard honest read, not encouragement

buffer typography
8
Mono body, star gutter, continuation indent, tag colour — all intact and dense in a good way. This is the part users came for.
tabs
8
Correct call. Clean, restrained, native-feeling, and the biggest new capability in the app. Underexploited so far (see §4).
chrome cohesion
4
Mono nav row + mono tab labels + sans sidebar header + native menus = four voices. Neither Emacs modeline nor macOS toolbar.
information layers
4
“Home” appears three times in 120px of chrome with no hierarchy telling you why. Nav row and tab bar overlap in purpose.
colour / native tint
5
Violet is a good pick and consistent in-app, but every system control (date picker selection, Set) lands in blue. Reads unbranded.
lens legibility
4
Depth encoded as tint only, clamped at 15%, full-bleed bands, no indent. Depth 1 and depth 4 are indistinguishable.
measure / line length
5
Mono runs the full window width; right-rail metadata floats a screen away from its row. Both get worse the wider the window.

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.

2 · what the Home screenshot is actually doing current state, annotated

LifeLab
Home ×2026-07-29 ×
*Home {filter}
*a block with metadata sss:more-tags:
*ayyy is this the rect:tags:
*a new block with ffiex and:key: :ayyy: :sada:
and
adad
*loading it takes quite long,:add:
[+]tags{filter}edit
[+]key{filter}edit
*english test
*hello:ayyy:
*TODOello
+ Add to Home…
1Three Homes, zero hierarchy. [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.
2The nav row and the tab bar compete. Clicking day and clicking the 2026-07-29 tab do nearly the same thing, with different persistence rules. Two rows, one job.
3The right rail is metadata debris. Tags right-align to the window edge, so at 2500px they detach from the block they describe and read as a second, phantom column. On the Day tab the rail is empty and the buffer looks unbalanced instead of clean.
4Lens rows are full-bleed with no indent and carry a right-aligned edit affordance — they read like table rows dropped into a document, breaking the outline's left edge.
5{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.
6Mono is doing two jobs. 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.

3 · the story: hold / show / is give each layer a different grammatical job

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:

tab bar

“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.

modeline

“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.

root block

“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

LifeLab
Home ×2026-07-29 ×Lens Color Lab ×
*Home {filter}
*a block with metadata sss

“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

⌂ home ×◷ todayLens Color Lab ×+
*Home
*a block with metadata sss

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.

4 · tabs open more than tabs a tab is a serialisable view, so make it a drop target and a data type

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.

drag block → tab
Move or link across pages without leaving. Drop = move, ⌥drop = link, ⇧drop = transclude. Dropping onto the ◷ today tab is the fastest possible “schedule this” — no picker, no menu.
drag tab → buffer
Transclusion by dragging. Pull the 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”.
⌥click tab
Split instead of switch — two tabs side by side in one window (your tmux-remix pane work, but the tab bar is the pane manager and shows which tabs are currently mounted).
lens → new tab
Promote a query to a workspace. “Open lens in tab” turns an inline lens into a durable filtered view with its own history; the ephemeral query buffer becomes a tab you can name and keep.
pinned tabs
Replaces the nav row (§3). Pins are user-editable: pin inbox, project/x, a lens. ⌘1⌘9 jump to pin n; ⌘⇧[/⌘⇧] cycle.
tab set = a block
The strongest one. “Save session to page” writes your open tabs into a block on any page. Reopen from the page, share it, diff it, template it — workspaces become notes, which is exactly the LifeLab thesis applied to its own chrome.
hooks + lua
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.
back/forward per tab
Each tab keeps its own history stack, so following a link no longer costs you your place — the thing modal navigation was working around.
tab overflow
Never scroll the bar. Past ~7 tabs, collapse to a count chip and open the tab list in the palette (⌘⇧T), searchable by page name and by applied lens.

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.

5 · lens depth needs indentation, not more tint tint says “lens”, position says “where”

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

*Lens Color Lab {filter}
[−]Lens Color Lab · Depth 1{−subpages −linked}edit
[−]Lens Color Lab · Depth 2{−subpages −linked}edit
[−]Lens Color Lab · Depth 3{−subpages −linked}edit
[−]Lens Color Lab · Depth 4{−subpages −linked}edit
*Depth 4 · clamped at 15%
This deeper container deliberately reuses the depth-3 colour.

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

*Lens Color Lab
[−]Depth 1{−subpages −linked}2
[−]Depth 2{−subpages −linked}3
[−]Depth 3{−subpages −linked}1
[+]Depth 4{−subpages −linked}0
*Depth 4 · folded by default
Depth is position. Tint just means “lens surface”.

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.

6 · native surfaces: keep, tint, reclaim draw the boundary explicitly

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.

keep native

  • Block right-click menu — grouping and verbs are already good; system menus give you submenus, keyboard repeat, accessibility for free.
  • Date pickers, colour pickers, file dialogs, window/tab management, text-field context menus.
  • Menu bar. Everything in the palette should also be a real menu item, discoverable without the palette.

tint / adjust

  • Set the app accent to violet so system controls stop shipping Apple blue — the 29 selection and Set button are the loudest unbranded pixels in the app.
  • Any content string inside a native menu (tag names, page names, block text in “Pin to…”) renders mono, at menu size.
  • Menu item labels stay sentence-case SF — Make TODO keeps TODO mono/red, since it's a content token.

reclaim as mono

  • The right sidebar header (Context is sans-bold today) — it's a buffer, so mono it, or make the whole sidebar chrome-styled. Pick one.
  • The nav row: it's a modeline, not a toolbar. Mono, small, low-contrast, aligned with the buffer's left edge, not the window's.
  • Toasts, sync status, counts — content-adjacent, mono.

system default

Jul 2026◀ ● ▶
SuMoTuWeThFrSa282930123456789101112131415161718192021222324252627282930311
Set

tinted + mono numerals, org-aware footer

Jul 2026◀ ● ▶
SuMoTuWeThFrSa282930123456789101112131415161718192021222324252627282930311
SCHEDULED: <2026-07-29 Wed>Set

the menu, mostly as-is

◯ Make TODO
↑ Add block above
↓ Add block below
⌕ Create Lens below… ⌘L
⌄ Collapse
⤢ Extract to subpage…
⧉ Open in new tab ⌘⏎
⧉ Open in split ⌥⌘⏎
⚲ Pin to…
☷ Scheduled… ⌘⇧S
☷ Deadline… ⌘⇧D
◌ Manage Tags…
🗑 Delete Block

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 bottomDismiss 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.

7 · punch list ordered by ratio of felt-improvement to work

now
Set the app accent to violet. One line, kills every stray blue in system controls.
now
Cap the buffer measure (~72–80ch) and attach the metadata rail to the content column, not the window. Fixes the phantom-column feel at every window size.
now
Indent nested lenses 16px per depth + fading left rule; clamp tint at depth 2; auto-fold depth ≥3.
now
Hide unfilled {filter} unless the block is hovered, focused, or actually filtered. Replace the lens's edit with a match count.
next
Merge nav row into the tab bar — home/day/week/month become pinned tabs; the second row becomes a real modeline (path · lens · counts · sync).
next
Transient tabs (single-click preview reuses a slot, edit promotes) before tab count becomes a problem.
next
Per-tab history + ⌘1⌘9 + ⌘⇧T tab list in the palette. Add tab verbs to the context menu.
next
Fix the mono/SF boundary: chrome = SF, content and content-derived tokens = mono, sidebar picks a side.
later
Drag block → tab (move/link/schedule) and drag tab → buffer (transclude). The interactions only tabs make possible.
later
Sessions as blocks + tabs.* Lua API + on_tab_focus hooks. Then ⌥click = split.
later
Prototype the bottom modeline against the top one. It's the cheapest way to get the old build's identity back without giving up a single native surface.

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.