Adopt nldd-app-view as the application shell #203

Closed
opened 2026-07-26 19:56:11 +00:00 by robbertbos · 1 comment
Owner

Waggle has never used nldd-app-view. That was not a decision, it just grew that way: every route hand-rolls a <div class="page-shell"> (routes/Inbox.vue, ReadingList.vue, Settings.vue). The NLDD skill calls it "de buitenste schil" and assumes it is present.

Two things during the typography work turned this from cosmetic into something with consequences.

What we are missing

  • Landmarks. nldd-page renders <main>, <header> and <footer> in its shadow DOM. We have no <main> anywhere in the app.
  • The body background. app-view forces its surface colour onto document.body, so iOS overscroll bounce and the status bar blend with the app instead of flashing the UA default white.
  • Overscroll policy. It manages overscroll-behavior on documentElement and body; we set neither.
  • NLDD's own resets. reset.css scopes its margin reset to html:has(nldd-app-view). Because we do not use the shell, that reset never applies, which is why #201 had to add a nldd-container > * { margin: 0 } rule of its own.

The part that needs care

nldd-page defaults to nested scroll mode: the page owns a scroller. Waggle scrolls the document today (useScrollLock locks document.body; TopNav and SearchRow are position: sticky against the viewport). Adopting the shell naively would change the scroll model.

app-view only derives a mode when a horizontal split view is present (_evaluateScrollMode), and Waggle is a bare-page app, so it defers to the inherited value - which defaults to nested. Keeping today's behaviour therefore means setting --context-scroll-mode: root ourselves.

Touches App.vue, the three route shells, useScrollLock, the sticky TopNav offset (--topnav-height) and the card-list scroll. Worth doing, but as its own change with a keyboard and mobile pass.

Waggle has never used `nldd-app-view`. That was not a decision, it just grew that way: every route hand-rolls a `<div class="page-shell">` (`routes/Inbox.vue`, `ReadingList.vue`, `Settings.vue`). The NLDD skill calls it "de buitenste schil" and assumes it is present. Two things during the typography work turned this from cosmetic into something with consequences. ### What we are missing - **Landmarks.** `nldd-page` renders `<main>`, `<header>` and `<footer>` in its shadow DOM. We have no `<main>` anywhere in the app. - **The body background.** `app-view` forces its surface colour onto `document.body`, so iOS overscroll bounce and the status bar blend with the app instead of flashing the UA default white. - **Overscroll policy.** It manages `overscroll-behavior` on `documentElement` and `body`; we set neither. - **NLDD's own resets.** `reset.css` scopes its margin reset to `html:has(nldd-app-view)`. Because we do not use the shell, that reset never applies, which is why #201 had to add a `nldd-container > * { margin: 0 }` rule of its own. ### The part that needs care `nldd-page` defaults to **nested** scroll mode: the page owns a scroller. Waggle scrolls the document today (`useScrollLock` locks `document.body`; `TopNav` and `SearchRow` are `position: sticky` against the viewport). Adopting the shell naively would change the scroll model. `app-view` only derives a mode when a horizontal split view is present (`_evaluateScrollMode`), and Waggle is a bare-page app, so it defers to the inherited value - which defaults to nested. Keeping today's behaviour therefore means setting `--context-scroll-mode: root` ourselves. Touches `App.vue`, the three route shells, `useScrollLock`, the sticky TopNav offset (`--topnav-height`) and the card-list scroll. Worth doing, but as its own change with a keyboard and mobile pass.
Author
Owner

One correction, found while closing the skip-link gap in #223 (PR #224).

This issue says nldd-page defaults to nested scroll for a bare-page app, and that keeping today's document scroll "means setting --context-scroll-mode: root ourselves". On 0.8.77 that is no longer true: _evaluateScrollMode in app-view.js resolves the absence of a split view to root, not to the inherited default.

// app-view.js:122-124
const next = splitView
  ? (splitView.isSingleColumn ? 'root' : 'nested')
  : 'root';

Measured in the running app (0.8.77), nldd-app-view > nldd-page[sticky-header] with no split view:

modeOnAppView:    "root"      // set by app-view itself, no declaration from us
dataScrollOnPage: "root"
headerPosition:   "sticky"    // against the document
headerTop:        "0px"       // = var(--context-layer-top, 0px)
pageOverflow:     "visible"   // the document scrolls
hasMain:          true

So the scroll model does not change and we would not have to set the variable. page.styles.js:118-127 is the branch that does it: in root mode the sticky header becomes a real position: sticky layer in normal flow, offset by --context-layer-top, and reserves its own space (no ResizeObserver padding). That also gives --topnav-height a DS-native replacement: SearchRow could publish itself as a layer instead of hardcoding the offset.

That removes what reads as the most expensive part of this issue. The rest still stands - App.vue, the route shells, useScrollLock and the card-list scroll, plus the keyboard and mobile pass.

Two things worth knowing before picking this up:

  • Sticky nav is justified and should survive the move. Measured on Leeslijst with 94 cards: 26347px over a 757px viewport = ~35 screens back to the only navigation in the app. sticky-header on nldd-page covers it.
  • <main> landed already. #224 added it via a shared PageShell.vue plus nldd-skip-link, without the shell, so the landmark and WCAG 2.4.1 are no longer arguments for adopting app-view - they are done. What is left here is the body background, the overscroll policy, NLDD's html:has(nldd-app-view) resets, and getting the sticky layer out of our own CSS (PageShell currently carries one position: sticky rule, moved off TopNav).

Related: the decision note from 2026-08-05 argued against adopting the shell partly on the scroll-mode cost. That part of the reasoning no longer holds.

One correction, found while closing the skip-link gap in #223 (PR #224). This issue says nldd-page defaults to nested scroll for a bare-page app, and that keeping today's document scroll "means setting `--context-scroll-mode: root` ourselves". On 0.8.77 that is no longer true: `_evaluateScrollMode` in `app-view.js` resolves the *absence* of a split view to `root`, not to the inherited default. ```js // app-view.js:122-124 const next = splitView ? (splitView.isSingleColumn ? 'root' : 'nested') : 'root'; ``` Measured in the running app (0.8.77), `nldd-app-view` > `nldd-page[sticky-header]` with no split view: ``` modeOnAppView: "root" // set by app-view itself, no declaration from us dataScrollOnPage: "root" headerPosition: "sticky" // against the document headerTop: "0px" // = var(--context-layer-top, 0px) pageOverflow: "visible" // the document scrolls hasMain: true ``` So the scroll model does not change and we would not have to set the variable. `page.styles.js:118-127` is the branch that does it: in root mode the sticky header becomes a real `position: sticky` layer in normal flow, offset by `--context-layer-top`, and reserves its own space (no ResizeObserver padding). That also gives `--topnav-height` a DS-native replacement: `SearchRow` could publish itself as a layer instead of hardcoding the offset. That removes what reads as the most expensive part of this issue. The rest still stands - App.vue, the route shells, `useScrollLock` and the card-list scroll, plus the keyboard and mobile pass. Two things worth knowing before picking this up: - **Sticky nav is justified and should survive the move.** Measured on Leeslijst with 94 cards: 26347px over a 757px viewport = ~35 screens back to the only navigation in the app. `sticky-header` on nldd-page covers it. - **`<main>` landed already.** #224 added it via a shared `PageShell.vue` plus `nldd-skip-link`, without the shell, so the landmark and WCAG 2.4.1 are no longer arguments for adopting app-view - they are done. What is left here is the body background, the overscroll policy, NLDD's `html:has(nldd-app-view)` resets, and getting the sticky layer out of our own CSS (PageShell currently carries one `position: sticky` rule, moved off TopNav). Related: the decision note from 2026-08-05 argued against adopting the shell partly on the scroll-mode cost. That part of the reasoning no longer holds.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
robbertbos/waggle#203
No description provided.