feat(desktop): show the empty state on an empty workspace's Issues board

Merged
DA
11d ago
+93-20

Item "Empty board shows EmptyState" of the Improve Issues board plan. The developer settled the plan's open question: a workspace with no issues shows the app's empty state on the board too, instead of five empty columns.

Summary

  • issues-page.tsx: listState now gives COLLECTION_STATE.EMPTY to an empty workspace in both layouts, not only the List. CollectionPage then draws the same EmptyState ("No issues yet", ListTodoIcon) on the board. The board's reading blocks (IssueBoardSkeleton) are unchanged. A workspace with issues still stands all five columns, empty ones included.
  • The empty state's way on: a New issue button through EmptyState's action slot. It is wired to the toolbar's own onCompose, so it opens the same card (on Backlog). It is disabled whenever the toolbar's New issue is: no workspace listed, or the card already open. It matches the Inbox's NeedsYouEmpty, a label alone with no glyph. Factory and Automations have no action in their empty states. "New issue" is now one constant used by both buttons.
  • @luke/ui EmptyStateAction gains an optional disabled, passed to its Button. Without it, the empty state's button would vanish and reappear behind the card each time the card opened.
  • @luke/ui CollectionPage (CSS only): the notice wrapping an empty or failed state no longer adds its own 2:3 spacers around an EmptyState. The EmptyState already does that split itself, so the two stacked. That left the state a sixth of the room (about 115px on an 800px window) and clipped anything past the icon and line: this New issue, and a centred ErrorState's Retry. The rule is scoped to :has(> .luke-empty-state). With the spacers gone, a line-only state sits about 20px higher than before, at the 2:3 point of the whole room.

Shared component change

Two @luke/ui components owned by @charleslpan change.

  • EmptyStateAction.disabled (optional): passed straight to the action's Button. Every existing caller (NotFound, the Inbox's NeedsYouEmpty, ErrorState's Retry) leaves it out, so their buttons are unchanged.
  • CollectionPage's notice spacers: .luke-collection-page-notice stands its content at the room's 2:3 point with ::before/::after. EmptyState, and a centred ErrorState, which is drawn as one, already does the same split inside a flex: 1 1 0 box. Inside the notice, the two splits stacked: the state got a sixth of the room and scrolled what didn't fit. On main that already hides Factory's Retry when its tasks could not be read (shots below), and it would have hidden this New issue. The new rule drops the notice's spacers only where its direct child is .luke-empty-state. Its callers:
    • Factory (empty and failed): the line-only empty state sits 2px higher, and the failed state's Retry now shows. See the pair below.
    • Issues (empty and failed): this PR's change. The failed state's Retry now shows too.
    • Web admin (AdminPage, the accounts table, a day): these pass plain text, not an EmptyState, so :has(> .luke-empty-state) does not match and nothing moves.
    • Automations, the Inbox and Home do not draw on CollectionPage, so nothing changes there.
Before (main)After
Factory, nothing at work
Factory, tasks could not be read

Screenshots

These come from a throwaway, uncommitted jsdom test that rendered the real IssuesPage with an empty workspace. Headless Chromium then painted them at 1280×800 in .desktop-shell > main.desktop-main, with the sidebar folded, using the desktop stylesheet built from main (before) and from this branch (after). This is the same method as #3559 and #3642.

BeforeAfter
Board, empty workspace
List, empty workspace
Board, empty workspace, light
List, empty workspace, light

After filing the first issue, the board stands its five columns again:

Evidence

  • pnpm check:affected: typecheck, lint and knip pass, and the desktop and ui suites pass. discarded-effect ran out of memory inside turbo's parallel group in this sandbox. Run alone, it passes ("No discarded Effect, Stream, or Layer in 2342 files"). The first run's one desktop failure was app-menu.test.ts, because the sandbox lacked Electron's binary. It passes once the binary is installed.
  • issues-page.test.ts: the test "a workspace with no issue yet still stands the board's columns, each saying it holds none" is replaced by "a workspace with no issue yet says so on the board and in the list, its New issue opening the card, and the first issue filed stands the board's columns". The new test fails on main ("the empty state offers New issue"). The column-+ test now stands one issue first, since an empty workspace no longer draws column heads.
  • empty-state.test.ts: new test "a way on unavailable for now still stands, and a press does nothing". It fails without the disabled field.
  • Test count: +1. The issues-page test was replaced one for one, and empty-state gains one.

Invariants

  • ./scripts/check.sh green. (CI)
  • JSON Schema goldens unchanged.
  • Every behavior change has, in this PR, the test that fails without it; no change-detector test added.
  • Gateway envelope goldens unchanged.
  • No new as type assertion outside the allowlisted files.
  • No Effect.runPromise/runSync/runFork outside the runtime edges.
  • No new setTimeout/setInterval/new Promise/AbortController/fs.watch.
  • No Effect, Stream, or Layer written as a statement and dropped.
  • Test count delta listed above (+1).
  • No hand-rolled file replaced.
  • No CLAUDE.md/AGENTS.md sentence names a changed part.
  • PRIVACY.md unchanged.
  • Package deps unchanged.
  • Barrel/door rule unaffected.

Notes

  • No hosted-service change, so no run.sh --preview run is needed.
  • Blockers or follow-up verification: None

Made with Luke