# UI refresh: Werkator palette and mobile-first core screens Specification: [docs/plans/ui-refresh.md](../plans/ui-refresh.md). Selector contract: [docs/plans/ui-refresh-selectors.md](../plans/ui-refresh-selectors.md). Drafts: [docs/plans/mockups/ui-refresh.html](../plans/mockups/ui-refresh.html). This package changes appearance and operability only. No schema change, no migration, no new domain behaviour, and no change to sign-in, invitations, sessions or e-mail. The successor package [auth-flow-refresh.md](../plans/auth-flow-refresh.md) builds on this state. ## Design tokens `styles.css` and `static/login.css` now declare the same `--wj-*` token set and derive every Lumo variable from it, so there is a single palette instead of the three historical petrol tones. `offline.html` and the account deletion page reuse the same tokens; the legacy names (`--paper`, `--ink`, `--petrol`, …) remain as aliases because the deletion controller shares the login stylesheet. Three light tones were darkened against the values tabulated in the specification, because the tabulated ones missed WCAG AA as body text: `--wj-muted` `#6e7c7a` → `#677573`, `--wj-clay` `#b0563b` → `#a55137`, `--wj-amber` `#b8860b` → `#8c6608`. The dark values are unchanged. Measured ratios are listed under Verification. ## Shell The bottom navigation has four labelled destinations — Today, Work, Habits and More, the last one opening the drawer — with the icon in a pill that fills with `--wj-accent-soft` when the route is active. The active link also carries `aria-current="page"`; Today uses an exact highlight condition so it is not marked active on every route. Drawer entries carry icons and 48 px rows, and the guest badge moves into the drawer head below 600 px. Page headers are an uppercase eyebrow over the title instead of a single oversized heading. The More tab is a native button. A Vaadin button wraps its parts in an internal flex container, so the host's grid cannot stack icon over label. ## Today One hero card carries the day total in the accent colour, the record action, a chip row with per-customer totals, and this week's and this month's totals with a four-pixel track. The track scales against the busiest of the last twelve weeks or months, read by the new `WorkService.peaks`, which groups a single date-range query in memory and needs no schema change; without history the track stays empty. Today's entries are plain rows instead of a collapsible card list: customer and project on the first line, description ellipsised on the second, an inline stepper that saves immediately within the existing daily limits, and a chevron. The row text stays a link to `work?date=…&entry=…`. Habits appear as tiles, three per row, each vertical in itself: name, count, a short status line ("1 to go · Wk 6/5") and the plus above a smaller minus. A single habit keeps the full card. Both forms keep the same contract — an `H2` with the habit name, `.summary-value`, the indicator paragraphs with `role="status"`, `.habit-week-total` and a `vaadin-details` disclosure — so the browser suite sees no difference. ## Work The page header shows the selected date as the title, with previous/today/next pills and a compact date picker beneath it; the picker keeps an explicit `aria-label` because its visible label is hidden. The day card repeats Today's hero layout and folds the day-off switch into a toggle row, so the separate day-off card is gone. Entries use the same rows as Today; the row text and the trailing chevron both open the editor. The editor is a bottom sheet below 600 px and a 560 px dialog above. Its header carries the delete action and "Close and keep draft"; requesting deletion closes the sheet first, so only one dialog is ever open. Hours are chosen from twelve buttons covering the whole allowed range. The `IntegerField` stays in the form as the value that `work-drafts.js` and the acceptance assertions read — still `data-work-field="hours"` — but it is no longer rendered, because a number input beside the buttons only got in the way. Save and Discard sit in a sticky footer. Draft recovery and the conflict view became amber notes above the fields. ## Habits The habit chooser is a scrollable pill bar with create/edit/delete as quiet icon buttons. The month header gained three figures: this month's total, this week against its bound, and the current streak, computed by the new `HabitService.streak` from one date-range query using today's daily minimum. Calendar cells are square with the day number small and the count large; the background encodes the target status (`--wj-panel-2` for zero, `--wj-accent-soft` in target, `--wj-clay-soft` below the minimum, `--wj-amber-soft` above the maximum), future days stay dashed and disabled. The weekly total and limit paragraphs keep their position as immediate siblings of the week row, because WJ-006 addresses them by `following-sibling::p[1]` and `[2]`. ## Remaining pages Reports, customers and projects, settings, admin, login, offline and maintenance only adopt tokens, rhythm, card style, button hierarchy and the page header. Nothing was rearranged there. ## Corrections that made the styling take effect Three defects in the first token commit only surfaced in a real browser: - A descendant combinator after `::part()` is an invalid selector and voids the whole rule. `.journal-quick-nav, .journal-shell::part(navbar) .guest-badge, …` therefore never hid the desktop navigation on phones. The slotted elements are addressed as `.journal-shell > …` instead. - The dialog overlay now lives inside `vaadin-dialog`'s shadow root and forwards its parts. Every `vaadin-dialog-overlay::part(…)` rule was dead; the rules are `vaadin-dialog::part(…)` now, and `.wj-sheet` lands on the dialog host. - A single-column grid defaults to a `min-content` floor. With `white-space: nowrap` titles that made entry lists wider than the viewport, so the page scrolled sideways on a phone. Every such track is `minmax(0, 1fr)`, the text blocks clip their own overflow, and below 600 px the shell additionally clips stray horizontal overflow so a few excess pixels can never turn into a sideways scroll. Action labels are no longer hidden wholesale below 600 px. Icon-only is now an explicit `icon-action` per button, which keeps Save, Export and Create legible on a phone; accessible names were never affected because `actionButton` always sets `aria-label`. ## Test contract The inventory in [ui-refresh-selectors.md](../plans/ui-refresh-selectors.md) lists every selector, literal and attribute the suites bind to. All of them are preserved, including `data-work-field`, `data-work-submit`, `data-work-discard`, `data-work-recovery`, `data-work-entry`, `.work-entry-list`, `.work-entry-selected`, `.journal-hours`, the two `.journal-total-number` elements, `.journal-work-column`, `a.journal-entry`, `.summary-card` with its habit heading, `.summary-value` with `habit-warning`, `.habit-week-total`, `p.habit-indicator`, `vaadin-details-summary`, the calendar sibling paragraphs and every login id. `--wj-*` renames were confined to CSS. Two browser steps were adapted because the layout they described no longer exists: - `quickHourLimits` compared the entry card's text `· N Hours`. The row now carries its hours in the inline stepper, so the step asserts `.wj-count` instead. - `requestWorkDeletion` clicked Delete in the entry list. Deletion starts in the editor now, so the step opens the editor first and then clicks Delete in its header. Nothing else in `BrowserSteps.kt` changed, and no scenario was removed. ## Verification on 2026-09-12 Complete local pipeline on 0.1.107-SNAPSHOT with `JAVA_HOME=$HOME/opt/jdk25` (pinned Temurin 25 from `packaging/jdk.properties`): | Run | Result | |---|---| | `./gradlew test -Pproduction=true` | BUILD SUCCESSFUL, 245 tests, 43 skipped, 0 failures | | Chromium browser suite inside that run (`BrowserCucumberTest`) | 21 scenarios, every step passed | | `./gradlew firefoxAcceptance -Pproduction=true` | 21 scenarios, every step passed | | `./gradlew productionAcceptance -Pproduction=true` | 7 scenarios, 32 steps passed | | `./tools/test-frontend` | 18 tests passed | Two browser scenarios failed once during development and were fixed, not adapted: the default account already seeds two habits, so creating a third puts Today into the tile layout, and the first attempt had dropped the details disclosure from the tile. The tiles carry it again. One scenario failed in CI on the trunk and not locally. WJ-040 signs out and then opens an invitation link; sign-out already leaves the browser on `/login`, so adding only a fragment stays in the same document and submits the form that was rendered before the session changed. The application answers that correctly with its expired-form recovery page — the step was expecting the seamless path. A recipient opens such a link as a new document, so the step now leaves the page first; reloading is not an option because `login-invitation.js` strips the fragment from history immediately. Nothing about sessions, CSRF or sign-in was touched; the rough edge is recorded in [auth-flow-refresh.md](../plans/auth-flow-refresh.md), which owns that workflow. Manual inspection ran against `build/libs/werkjournal.jar` started with a file database and a fixture of three customers, four entries on the current day, three tracked habits with bounds, a month of habit counts and four months of work history — the populated state the specification asks for. Checked in Chromium device mode: Today, Work with the editor sheet open and the habit calendar at 360 × 780, 390 × 844 and 1280 × 900 in both themes; settings, reports and customers and projects at 390 × 844; the login page from a second instance with `guest-mode=false` at 390 × 844 in both themes. The habit calendar's card is capped at 480 px above 600 px, because square cells across the full content width made a month look like a wall. - Today at 390 × 844 shows everything without scrolling: the header, the hero card with chips and both tracks, four entry rows, and three habit tiles. The lowest tile ends at 719 px, the bottom navigation starts at 780 px. - No element exceeds the viewport at 360 × 780 or 390 × 844; long titles and descriptions ellipsise inside their row instead of widening the page. - Recording through Today → sheet → Save returns to Today with the new total; the habit steppers, a calendar day sheet, the day-off toggle, the appearance switch and the language switch were exercised by hand. - The only console errors are service-worker registration failures inside the embedded preview browser; `/sw.js` itself answers 200, and WJ-037 confirms a real service worker registers and controls the page in the Playwright Chromium run. Measured contrast, computed from the token values (WCAG 2.1 relative luminance): | Combination | light | dark | |---|---|---| | `--wj-text` on `--wj-bg` | 18.16 : 1 | 13.51 : 1 | | `--wj-text-2` on `--wj-panel` | 9.29 : 1 | 9.15 : 1 | | `--wj-muted` on `--wj-panel` | 4.61 : 1 | 5.56 : 1 | | `--wj-accent` on `--wj-panel` | 5.89 : 1 | 6.14 : 1 | | `--wj-accent` on `--wj-accent-soft` | 5.27 : 1 | 5.45 : 1 | | `--wj-on-accent` on `--wj-accent` | 6.14 : 1 | 7.41 : 1 | | `--wj-clay` on `--wj-clay-soft` | 4.62 : 1 | 5.38 : 1 | | `--wj-clay` on `--wj-panel` | 5.25 : 1 | 5.37 : 1 | | `--wj-amber` on `--wj-amber-soft` | 4.58 : 1 | 6.34 : 1 | | `--wj-crit` on `--wj-crit-soft` | 5.27 : 1 | 4.83 : 1 | | `--wj-ghost` on `--wj-panel` | 2.32 : 1 | 3.09 : 1 | CI: `.werkator.yml` now also builds every non-trunk branch under `werkator/branch`, so a feature branch is checked before it is merged; only the trunk job reaches `tools/ci-deploy`, which additionally refuses any branch but `main`. The branch job went green on `ui-refresh@branch` at 77df6e7 in 664 s. After the merge the trunk job went green at 839bff9 in 625 s and deployed: `{"result": "PASS", "commit": "839bff99…", "url": "https://werkjournal.javagil.de"}`. The live login page reports v0.1.107-SNAPSHOT. ## Deviations from the specification - Habit tiles expand their details inline instead of opening a bottom sheet. The suite requires a `vaadin-details-summary` inside the habit card, and a second surface beside it would have duplicated the same information. - The Habits page keeps its `habit.heading` title ("Small rituals.") rather than "Calendar", so it stays in the same voice as every other page header. - Per-customer chips wrap instead of scrolling horizontally. Wrapping keeps every customer visible without a hidden gesture. - The hours field is gone from the form rather than hidden behind a "more" chip: twelve pills already cover 1 to 12. The element remains in the DOM because the local draft format and five `hasValue` assertions read it; `editWork` now clicks a pill instead of filling the input. - `--wj-ghost` on `--wj-panel` reaches 2.32 : 1 in light and 3.09 : 1 in dark. It is used only for disabled controls and placeholders, which WCAG exempts; every enabled state uses `--wj-muted` or darker.