Mockup · desktop · destructive confirmation

Re-analyse confirmation — the shipped pixels

Every number below is transcribed from ReAnalyseConfirmSheet.swift and the strings from bristlenose/locales/en/desktop.json. This is a render of what the code produces, not a proposal for what it should be.

The intent drawing lives in analysis-lifecycle-states.html §“The confirmation — measured, not vague”. It was drawn in the web design system; the sheet is native. Where the two differ, this page shows the native side.

22 Aug 2026 — the buttons changed. Rendering the sheet is what surfaced it: Cancel held the default and the confirm button carried the destructive style, which the HIG contradicts on three counts. The artefacts below are the corrected sheet; the section directly after this one holds the before-and-after.

Page palette
Artefact — native macOS pixels. No labels that aren’t in the product. Commentary — hatched. Why it looks like that, and what to check. Note — the page palette switch themes this document, never the sheet: a native sheet follows the system appearance, not the report theme.

As it appears — light and dark

A project with fourteen sessions and all five kinds of curation present.

Project ▸ Re-analyse… · light appearance
Onboarding diaries

Re-analyse “Onboarding diaries”?

Bristlenose will transcribe and analyse all 14 interviews again from scratch.

Your work on this study will be discarded: 47 edited quotes 12 tags 8 starred quotes 3 renamed speakers 2 named themes
Project ▸ Re-analyse… · dark appearance
Onboarding diaries

Re-analyse “Onboarding diaries”?

Bristlenose will transcribe and analyse all 14 interviews again from scratch.

Your work on this study will be discarded: 47 edited quotes 12 tags 8 starred quotes 3 renamed speakers 2 named themes

What changed, 22 Aug 2026

Same geometry, same strings. Only which button is the default, and whether the other one is styled destructive.

before · shipped 19 Aug – 21 Aug
after · conformant
Commentary

Three departures, one cause

The sheet put .keyboardShortcut(.defaultAction) on Cancel, and on macOS the default button carries the accent fill — so the prominent blue control in a destructive confirmation was the way out, sitting to the left of a red-labelled Re-analyse. Apple’s alert guidance contradicts that three times over: don’t make Cancel the default; always place the default on the trailing side; and reserve the destructive style for actions people did not deliberately choose. All three follow from the first.

Empty Trash is Apple’s own example of this exact case. Because the person chose it, the alert does not apply the destructive style, and the convenience of Return-to-confirm outweighs restating that the action destroys. Re-analyse… is chosen from a menu the same way. The sheet’s old comment argued the opposite — Return must not finish the sentence for them — which is a real concern, but the HIG’s answer to it is give no button the default, never put the default on Cancel.

The house had already got this right, twice

Two other confirmations ship in this app and both conform. The Locate-error alert puts the default on its action; the Disconnect-account alert deliberately omits the destructive role, with a comment citing Empty Trash by name and noting that dropping it “also restores Return-to-confirm”. Both are native .alerts, so AppKit places their buttons. This sheet hand-rolls its row, which is how it drifted out of a convention it never opted into.

What the drawing had right all along

The intent drawing gave the trailing button the visual weight — it was only the wrong colour, red where the platform draws blue. The emphasis was never the disagreement; the hue was.

Type is 13/12, not the report scale

Native text styles: .headline is 13pt semibold and .callout is 12pt. The whole sheet therefore runs a little smaller and tighter than the drawing, which used --bn-text-heading (18px) for the title and the mono face for the counted lines. The counted lines here are the system face at 12pt, secondary, indented 12pt — a list, not a code block.

ElementSwiftRendered
Sheet.frame(width: 420) · .padding(22)420 × 314.5pt (SwiftUI), 376pt content
StackVStack(alignment:.leading, spacing:14)14pt between every block
Iconarrow.clockwise, .system(size:26), secondary26pt, ~50% label
Title.font(.headline)13pt semibold
Rebuild line.callout, secondary, .fixedSize(vertical:)12pt, wraps to 2 lines
Loss header.callout.weight(.medium)12pt medium, full label
Loss lines.callout, secondary, .padding(.leading,12)12pt, 3pt apart, 12pt indent
ButtonsCancel .cancelAction · Re-analyse .defaultAction22pt tall; default takes the accent fill, trailing

Heights, measured

Measured by SwiftUI: the canonical sheet is 420 × 314.5pt, and the long-name sheet 330.5pt — the title takes a second line and the sheet grows 16pt. The counted block is the only other part that varies; in the CSS renders here, four omitted lines take 72pt with them. Treat the CSS heights on this page as ~7.5pt light.

Two approximations, declared

The glyph is a stroked stand-in — the real one is the filled SF Symbol arrow.clockwise, which is slightly heavier and sits a touch lower on its baseline. And the gap between the two buttons is SwiftUI’s platform default rather than a number in the source; 10pt is drawn here. Neither changes a decision, but neither is measured.

What the counts do to it

Zero-count kinds are omitted, so the sheet’s height is a function of how much the researcher has done. Three real shapes.

singulars · one loss only · the shortest sheet

Re-analyse “Pilot”?

Bristlenose will transcribe and analyse 1 interview again from scratch.

Your work on this study will be discarded: 1 edited quote 1 tag 1 starred quote 1 renamed speaker 1 named theme
every _one form

Re-analyse “Ward rounds”?

Bristlenose will transcribe and analyse all 9 interviews again from scratch.

Your work on this study will be discarded: 6 starred quotes
stars only — four kinds omitted
Commentary

The one-line case is the one to look hardest at

With a single loss line the counted block is a header and one indented item, and the 14pt gaps above and below leave it floating in the middle of a sheet that is otherwise mostly air. It still reads, but it is the shape where “measured, not vague” buys least — six starred quotes is a smaller thing to weigh than the sheet’s own ceremony.

There is no zero case here, by construction

askToReAnalyse reads the counts first and starts the run without asking when they are all zero. So the sheet has a floor of one line and no empty state to draw.

Rendered by SwiftUI, not by this page

Everything else here is CSS standing in for macOS. This one is the layout chain itself — same VStack, same modifiers — put through ImageRenderer at 2×.

ImageRenderer · long project name · 420 × 330.5pt
The re-analyse sheet rendered by SwiftUI, long project name wrapping to two lines
Commentary

The title wraps — settled 22 Aug 2026

It had been an open question: the title carries no .fixedSize while the line beneath it does, and that asymmetry usually means something truncated once. It doesn’t. The long name takes a second line in full, no ellipsis, and the sheet grows 16pt to hold it.

The proof is that adding .fixedSize(horizontal: false, vertical: true) to the title changes nothing. Rendered both ways, the two PNGs are byte-identical — so the modifier would be a no-op here, and its absence on the title is not a latent bug. The one on the line below is doing no work either, on this layout.

And it corrects this page’s own numbers

The CSS renders above are ~7.5pt short of the real thing: 314.5pt for the canonical sheet, not 307, and 330.5pt for this one, not 323. The gap is the SF Symbol’s line box and the real push-button metrics, both of which the stand-ins under-measure. Widths are exact — 420pt is set, not derived. The figures quoted elsewhere on this page have been corrected to the SwiftUI measurements.

Two edges the drawing never had to answer

One the gate prevents, one it doesn’t. Drawn as the code would render them.

sessionCount nil (blocked upstream) · long project name

Re-analyse “Onboarding diaries”?

Bristlenose will transcribe and analyse all 0 interviews again from scratch.

Your work on this study will be discarded: 47 edited quotes 12 tags 8 starred quotes 3 renamed speakers 2 named themes
what ?? 0 would render — the gate prevents it

Re-analyse “Q3 Renewals — Enterprise Admin Diary Study (Wave 2)”?

Bristlenose will transcribe and analyse all 22 interviews again from scratch.

Your work on this study will be discarded: 118 edited quotes 64 tags 31 starred quotes 9 renamed speakers 7 named themes
a real client folder name
Commentary

“all 0 interviews” cannot happen — and the reason lives in another file

The sheet takes its count from projectIndex.unanalysed[project.id]?.sessionCount ?? 0, and that snapshot is nil until the folder watcher’s async scan lands. The curation counts come from a direct database read and do not wait for the watcher, so on the face of it the sheet could show five accurate loss lines above a sentence saying nothing will be rebuilt.

It can’t, because reAnalyseIsOffered ends with (data?.sessionCount ?? 0) > 0 — the command is unavailable in exactly the window where the sheet would say zero, and all three routes to askToReAnalyse pass that gate. So the drawing above is what the fallback would render, kept for the record.

What is worth naming is the coupling: the sheet’s second line is only ever true because a predicate in a different file happens to test the same value. Nothing in the sheet says so, and nothing would fail if the gate loosened.

One stale comment, same vintage

The menu-command case in ContentView.swift is commented “Ask, always … skipping it on a project that happens to carry no curation would make the ellipsis a lie”. The function it calls does exactly that skip, four lines into its own body, with the opposite rationale. Both landed in the same commit on 19 Aug, so this is a comment left behind by a decision rather than a live disagreement — but it argues a real point (a re-analysis re-spends on the provider even with nothing curated) against behaviour that has already shipped.

The long name wraps — confirmed against SwiftUI

Drawn wrapping here, and that is what the layout does; see the ImageRenderer section above. A researcher with two similarly-named waves keeps the word that tells them which study they are about to destroy, which matters because the title is the only place the project is named.

What is deliberately absent, and still is

No folder path, no --clean, no output directory, no “don’t ask again”. The cost is real every time and the count changes every time. The pixels keep that promise.