Generated and pasted board backgrounds move into .backgrounds/ — the board root stops collecting the app's own pictures

New app-written background images (Theme tab ▸ Pattern, and Edit ▸ Paste
as Board Background) now land in a hidden `.backgrounds/` folder at
board root instead of beside index.md, matching the `.trash/` app-managed
pattern. `background.image` stores the qualified relative reference
(`.backgrounds/facets.png`); the resolver needed no change at all, since
it already accepted any relative path inside the board root — the same
mechanism that already resolved `art/backdrops/sunset.png` resolves the
new location for free. `BoardWriter.writeBoardImage` now creates its
destination folder if missing, since `.backgrounds/` won't exist until a
board's first generated or pasted background.

The Finder collision-ladder (`BoardStore.boardImageName`) is rescoped to
`.backgrounds/`'s own contents, and its overwrite-in-place check now
recognizes only the qualified form as "ours" — a legacy bare
`background.image: facets.png` from before this change is read as a
foreign reference rather than migrated, so a regeneration writes a fresh
`.backgrounds/` file and orphans the old one in place, per the no-migration
ruling. The Theme tab's Pattern/Solid mode-detection was updated to
recognize both the legacy and current spellings as the generator's own
output.

The board loader needed no change: `.backgrounds/` is a hidden,
non-UUID-shaped name, and `.skipsHiddenFiles` already keeps every hidden
entry off the lane walk before any name-based exclusion is consulted —
pinned with a new loader test. Deliberately did not add `.backgrounds` to
IntegrityRules' claimed-name/squatter-displacement table: that table
mirrors a specific existing DESIGN ruling this card doesn't amend.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-09 08:38:07 -04:00
parent 0a01405ced
commit d0c546179f
9 changed files with 270 additions and 88 deletions
@@ -145,7 +145,15 @@ struct BoardThemeTabView: View {
// Pattern, since that is the surface that made it; every other board no image, or a
// hand-placed one the app does not own opens on Solid color, which is also the mode that
// never overwrites a foreign picture by accident.
_mode = State(initialValue: store.snapshot.backgroundImage.value == FacetsGenerator.fileName ? .pattern : .solid)
//
// Two spellings answer "is it ours" since the `.backgrounds/` ruling (2026-08-09): the
// qualified reference every current write lands at (`BoardStore.boardImageName`), and the
// legacy bare name a board written before that ruling still carries at board root a board
// moved from neither location by this check, so both keep opening on Pattern.
let image = store.snapshot.backgroundImage.value
let generatesThisImage = image == FacetsGenerator.fileName
|| image == "\(BoardBackdrop.backgroundsFolderName)/\(FacetsGenerator.fileName)"
_mode = State(initialValue: generatesThisImage ? .pattern : .solid)
}
var body: some View {
+27
View File
@@ -69,6 +69,33 @@ enum BoardBackdrop {
return imageURL(named: path, inBoardRoot: root)
}
/// **Where the app writes its own background images** a hidden dot-name at board root, the
/// `.trash/` pattern applied one concept over (03-board-ui.md § Styling Capabilities; ruled
/// 2026-08-09: "New app-written background images go into an app-claimed folder").
///
/// `image:` still names a path *relative to the board root*, unchanged: a generated or pasted
/// picture is written under this folder and referenced by the qualified relative path
/// (`.backgrounds/facets.png`), which `imageURL(named:inBoardRoot:)` above already resolves
/// without any change of its own a subfolder was always a legal `image:` target
/// (`art/sunset.jpg` resolves exactly the same way, and the read-side coverage for it already
/// existed). The folder buys tidiness, not new resolution machinery.
///
/// **Not a claimed name, and deliberately so.** Nothing in the board loader ever has to be told
/// about it: `BoardLoader.directoryCandidates` skips hidden entries at every level it walks
/// (`.skipsHiddenFiles`) the same mechanism that already keeps `.trash` out of the lane walk
/// before that folder's own name-based exclusion is ever consulted so a hidden, non-UUID-shaped
/// `.backgrounds` is invisible to the tree walk by construction, with nothing further to add. A
/// file squatting the name simply fails the next background write like any other I/O error (the
/// same "the app degrades to correctness without it" posture `.gitignore` has); nothing needs the
/// name to exist for the app to keep working.
///
/// **Legacy images are not moved here.** A `background.image` written before this folder existed
/// or a hand-placed one anywhere else in the board keeps resolving exactly where it sits; only
/// a *new* app write (`BoardStore.boardImageName`) chooses this folder, and a regeneration over a
/// legacy reference leaves the old file orphaned in place rather than migrating it, the same
/// leftover an overwritten `facets.png` already could.
static let backgroundsFolderName = ".backgrounds"
/// Whether this board paints a background of its own **the window-chrome predicate**
/// (`BoardWindowHost`, `HostedWindowController.setExtendsContentUnderTitlebar`): a board with one
/// runs its content under a transparent title bar, and a board without one keeps the standard