Skip to content

Add ledger and bands views, and a harness to inspect them - #4

Merged
Lenoux01 merged 4 commits into
mainfrom
feat/ledger-and-bands-views
Sep 9, 2026
Merged

Lenoux01 merged 4 commits into
mainfrom
feat/ledger-and-bands-views

Conversation

@Lenoux01

@Lenoux01 Lenoux01 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Two new view modes for the same projection, plus the surface to judge them on.

Views

  • ledger — one column, phase order intact, three fixed cells per item (glyph, title+summary, status) so status stops competing with the title for the eye. Shipped phases collapse to a single line carrying gets_you, which no view rendered until now despite every phase defining it.
  • bands — one full-width band per status in urgency order, each item tagged with the phase it came from. in-progress and blocked keep their band when empty, because an empty one is the news.

Both are additive. phases, list and compact emit byte-identical markup to before, pinned by a test that strips the is-view-* class and compares the trees.

view-model.ts gains the grouping (statusGroups, STATUS_ORDER) and a phase back-reference on ItemView, per the rule that the view model decides what to show; both renderers stay thin and mirror each other.

Harness

nextup serve now serves /harness: one combination of view × locale × audience × width × ground in an iframe, with the axes in a toolbar and the state in the query string, so a single combination is a link. It reuses the existing SSE stream and reloads only the frame.

It paid for itself immediately — ledger and bands were broken below ~640px, where the fixed grids do not reflow and the title, phase tag, status and links landed on top of each other. Both now collapse to glyph + stacked content, behind a container query rather than a media query: this component is a block on someone else's page and is as likely to be narrow in a desktop sidebar as on a phone.

Skins

The harness first showed nextup's own palette, which is what no consumer ships. nextup.config.yaml now takes an optional skins: list — css in declared order, the wrapper class a host scopes its tokens to, the host's dark class, and font links — and the harness gains a Skin axis defaulting to the first declared one.

build never reads skins: a projection carries no styling, and a skin must not be able to change what is published. Skin paths resolve against the repo root and are refused if they escape it.

Forcing a ground needed the dark palette outside its media query, so both palettes are declared once in styles.ts and emitted three ways — element defaults, prefers-color-scheme, and a data-nextup-theme attribute any host with its own toggle can use.

Checks

bun run lint, all three tsconfigs, 136 tests and bun run build green locally.

🤖 Generated with Claude Code

Lenoux01 and others added 4 commits September 8, 2026 16:26
Two modes for the same projection, both additive: `phases`, `list` and
`compact` emit byte-identical markup to before, pinned by a test.

- `ledger` — one column, phase order intact, three fixed cells per item
  (glyph, title+summary, status) so status stops competing with the title
  for the eye. Shipped phases collapse to a single line carrying gets_you,
  which no view rendered until now despite every phase defining it: for a
  prospect it is the payoff, and it says more in one line than a card of
  feature names.
- `bands` — one full-width band per status in urgency order, each item
  tagged with the phase it came from. `in-progress` and `blocked` keep
  their band when empty, because an empty one is the news.

`view-model.ts` gains the grouping (`statusGroups`, `STATUS_ORDER`) and a
phase back-reference on `ItemView`, per the rule that the view model
decides what to show; both renderers stay thin and mirror each other.

The status column is `max-content`, not a fixed width: German
"vorgeschlagen" overflows an 8rem cell, and a right-aligned nowrap cell
overflows leftward over its neighbour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…olumn

A roadmap has to hold up across five view modes, every locale the file
declares, both audiences, both grounds and any width. Nobody checks that
space by hand, so `nextup serve` now renders one combination in an iframe
with the axes in a toolbar above it. The state lives in the query string,
which makes a single combination a link — the useful currency when the
answer is "look at THIS one". It reuses the existing SSE stream and
reloads only the frame, so the toolbar keeps its scroll and focus.

The harness paid for itself immediately: `ledger` and `bands` were broken
below roughly 640px. Their fixed grids do not reflow, so on a phone the
title, the phase tag, the status word and the links all landed on top of
each other. Both now collapse to glyph + stacked content.

That breakpoint is a container query, not a media query: this component is
a block on someone else's page and is as likely to be narrow in a sidebar
on a desktop as it is on a phone.

Forcing a ground needed the dark palette to exist outside its media query,
so both palettes are now declared once in `styles.ts` and emitted three
ways — element defaults, `prefers-color-scheme`, and a `data-nextup-theme`
attribute any host with its own toggle can set. Declaring them once is
what stops the forced ground from drifting from the automatic one.

`parseHarnessState` validates every axis rather than trusting the query
string; an unknown view mode must never reach the renderer as a class
name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The harness showed nextup's own palette — green accent, blue "now", 10px
radius, system font — which is exactly what no consumer ships. lexilink's
look lives entirely in the app: a values-only theme.css, a roadmap.css
that maps those tokens onto --nextup-*, a wrapper class the mapping is
scoped to, and a `.dark` class instead of prefers-color-scheme. The
preview could not answer "how will this look in lexilink", which is the
only question worth asking of a view change.

`nextup.config.yaml` now takes an optional `skins:` list, and the harness
gains a Skin axis with the package defaults always offered alongside, so a
skin can be compared against them. All four parts matter: css in declared
order, the wrapper (a mapping scoped to a class does nothing without it),
the host's dark class, and font links.

serve-only, and deliberately: `build` never reads skins. A projection
carries no styling and a skin must not be able to change what is
published.

Skin paths come from a config file, so they resolve against the repo root
and are refused if they escape it — a dev server should not become a way
to read arbitrary files through a config someone pasted in. A skin naming
a moved file degrades to the defaults rather than 500ing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A bare /harness selected the package's own look, so with a skin declared
you still landed on exactly the design the skin exists to replace — the
picker was there, but you had to find it and switch. Previewing is done
for a host: the first declared skin wins the default, and nextup's own
palette stays selectable as a deliberate comparison.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Lenoux01
Lenoux01 merged commit da8cbf1 into main Sep 9, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant