AppShell (apps/renderer/src/components/layout/AppShell.tsx) is the frame every route renders inside: the sidebar, the header, the page and the phone’s tab bar. It is composed almost entirely of @jeliroco-multimedia/blunt-ui’ Frame and Navigation (Jeliroco Components); what is Datler’s own is how the sidebar is hosted at each width, where the page scrolls, and the keyboard chords.
The regions
Frame lays out four regions, with corners="block" (the header spans every column, above the sidebar) and scroll="viewport" (the frame is one viewport tall and the document never scrolls):
| region | element | holds |
|---|---|---|
| block-start | <header>, the banner landmark | the sidebar button, the mark, Back and Forward, the page title, the day everything logs for, the open issues, and search |
| inline-start | the sidebar | the navigation, the trackables as the sidebar groups them, Settings and the profile switcher |
| center | the content | the demo banner, the bar of what is running, the host for an entry opened over the page (Inspect Without Leaving), then the page in its own scroller |
| block-end | the quick navigation, below md only | Home, Rundown, Trackables, Visualizations and Settings as tabs |
The header’s first control is the sidebar’s one control at every width: it collapses the rail on a desktop and opens the drawer on a phone, a sidebar glyph rather than a hamburger. Beside it is the mark (The Datler Mark) in a box of its own, so BrandMark reads that box’s width: the glyph alone on a phone, the wordmark from md. Back and Forward live here for every page, since the app has no back of its own, and each disables rather than pretends (HistoryNav). The title is the route’s, as an h1. Then the day everything logs for (DayControl, see The Moment), the open issues (Issues), and the search button, “Log or search anything…”, which opens the quick-add palette (Frictionless Capture); a phone has a search icon instead.
The navigation runs Home, Rundown, Schedules, Inbox, Issues, Trackables, Tags, Visualizations, Achievements (hidden while achievements are switched off, though its address keeps working), Catalogs, Calendar and Table, with counts beside Inbox and Issues. Below it the trackables are grouped as the sidebar’s Group by says (Tags): a tag’s name links to its page and its chevron, named “Collapse Fitness” or “Expand Fitness”, folds it. The rail is 210 px wide, or 56 px collapsed, which is remembered on the device; collapsed, the tree hides and the icons stay.
While the app is open, the shell also runs what logs for you: each day’s weather, the schedules that log themselves, and module updates reaching the trackables that follow them (WhileOpen; Weather from NASA POWER, Schedules, Modules).
The sidebar is one structure in two hosts
From md the sidebar is a static rail; below it, the same sidebar is in an off-canvas drawer. They are the same markup, the same navigation, tree and footer, rendered into one of two hosts, not two subtrees with one hidden. That is the opposite of Frame’s own choice for the tab bar, and deliberately: a rail and a bottom bar are different structures, a vertical list and a horizontal row, so Frame renders the tab bar only below md (compact: "only"). A second copy of the sidebar would run the tree’s queries twice, and would hold a live focus trap inside a display: none panel once the window crossed 768 px with the drawer open. So this is the one place the shell asks the breakpoint in JavaScript, a media query on the same 48rem every md: variant uses. Widening past it closes the drawer, so the next narrowing does not reopen it, and any change of address closes it too, a change of only ?id= included.
The drawer is a Sheet portalled into the sidebar region. Below md the drawer is the region, and it must stay in the document and in the tab order, since it holds the only way to the trackables at that width. Dialog.Popup cannot render without a Dialog.Portal, but a portal takes a container, so the Sheet portals into a node inside the region: the popup is the region’s descendant in document order and tab order, and Base UI brings the focus trap, Escape, focus restore and the inert background. Two things this makes load-bearing:
- The region is never
compact: "hide". That isdisplay: none, and a popup portalled into a hidden container is a focus trap nobody can see. Belowmdthe region is 0 px wide, with no border or surface, purely the drawer’s host. - The popup declares
@container.BrandMarkshows its wordmark above an anonymous@[7rem]query, which resolves to the nearest container ancestor, and everyFrameregion is one. The host region is 0 px wide belowmd, so without its own container the drawer would measure its host and show the logo alone in a 264 px panel.
One scroller for the whole rail
The rail scrolls as one, rather than a fixed navigation above a scrolling tree. The navigation is about 333 px tall and cannot shrink (min-height: auto on a flex item), so a scroller below it would absorb every pixel the window loses: at 500 px tall, it would leave the tree 8 px. The scroller is also the column’s one flex-1, so it fills the rail and Settings and the profile switcher rest on its floor; when the rail collapses, the tree inside it hides and the space stays. It never scrolls sideways: collapsed, the navigation is wider than the rail, so it is clipped.
The page scrolls inside the content
Frame would scroll the center region; here the region does not scroll, and an inner ScrollArea labelled “Page content” does, so the demo banner and the running bar stay put while the page moves under them. The page’s padding is set on the scroll area’s content slot, the part the library promises, never by a positional selector into its markup. Below md the page keeps room at its foot for the fixed tab bar.
Every width
Below md the bottom tab bar holds the top destinations, and the full navigation and the trackables are in the drawer. A page never scrolls sideways at 375, 768 or 1280 px wide, which the drive probe’s H-OVERFLOW check reports (scripts/drive/probe.ts; see Driving the App (CDP)). Fixed widths give way to the screen: the quick-add palette, for one, is min(620px, 100% less a margin) wide (components/capture/QuickAddPalette.tsx).
Pages reflow on the window’s breakpoints, and a component that lives at more than one width responds to its container instead: the library’s Card, so every Home widget fits its cell, and WidgetShell. Lists of cards are auto-fill grids with a minimum card width, so they go from one column to many with no per-breakpoint column spans. Home is the one surface arranged by hand, on its dashboard grid (Home); everywhere else the layout is intrinsic.
Keyboard
Three chords are records in the library’s shortcut registry, so the chord printed beside a control is read off the record that fires, and mod resolves to Ctrl here and ⌘ on a Mac:
| chord | does | from inside a dialog |
|---|---|---|
mod+k | opens the quick-add palette, “Log or search anything” | yes: it is global, since logging something else mid-edit is what a palette is for |
mod+b | collapses or expands the rail | no: it stays with the page |
mod+, | opens the appearance tweaks (Theming System) | no |
The search button’s keycap reads mod+k’s record, so it says Ctrl on Windows. The sidebar button’s name hardcodes ”(⌘B)” whatever the platform. Key hints are never shown in the Android build (KbdHints show="never" in apps/renderer/src/main.tsx); elsewhere the library shows them unless the device is a touch screen with no hover.
Escape is not in the registry. It belongs to whichever overlay is open, and a global binding would fire behind every dialog and popover in the app; the drawer’s Escape is Base UI’s.
Related
Page Specs · Jeliroco Components · Tags · Driving the App (CDP) · Theming System