File ▸ Share… stages a board as a zip and hands it to the system share sheet

Duplicate's own posture — a faithful copy, `.git` the sole exclusion, attachments/comments/trash
carried verbatim — staged to a temp directory (BoardShareStager, ditto-zipped via
DittoZipArchiver) and presented through NSSharingServicePicker (BoardSharePresentation), anchored
to the board window's toolbar or its center. Follows Duplicate/Save as Template's flush-then-
cancellable-copy sequence under the banner's in-progress row (ShareBoardCommand, AppCommands.swift),
menu-validated on focus alone rather than the read-only lock (a share is a read, Print's own
posture) with the one carve-out an open inline title editor still needs. WriteOperation gains
.shareBoard for the banner vocabulary; 11-command-nexus.md's File menu table gains the row.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-09 02:13:54 -04:00
parent 05bbf78926
commit da5d310673
10 changed files with 997 additions and 15 deletions
+11 -1
View File
@@ -3010,6 +3010,14 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
/// `title` is the board's display name.
case saveAsTemplate(title: String?)
/// File Share the whole board staged into a `.zip` for `NSSharingServicePicker`
/// (design ruling 2026-08-09, card 72691b11; `BoardShareStager`). Its own case beside
/// `.duplicateBoard` and `.saveAsTemplate`, on both their reasoning: a third command copies a
/// board wholesale for a third reason, and a banner saying the app could not "duplicate" or
/// "save as template" a board nobody asked to duplicate or template would name gestures that
/// never happened. `title` is the board's display name.
case shareBoard(title: String?)
case importAttachment(filename: String)
case listAttachments
@@ -3246,6 +3254,7 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
case .rename: .rename(title: title)
case .duplicateBoard: .duplicateBoard(title: title)
case .saveAsTemplate: .saveAsTemplate(title: title)
case .shareBoard: .shareBoard(title: title)
case .toggleTask: .toggleTask(title: title)
case .editBody: .editBody(title: title)
case .rawSource: .rawSource(title: title)
@@ -3289,7 +3298,7 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
// reading to be about and `.deleteComment`'s move into `comments/.trash/` stamps for the
// plain container reason its board-level twin does.
case .createBoard, .createLane, .createCard, .move, .copy, .delete, .purge, .migrateTombstone,
.style, .resize, .collapse, .expand, .rename, .duplicateBoard, .saveAsTemplate, .paste,
.style, .resize, .collapse, .expand, .rename, .duplicateBoard, .saveAsTemplate, .shareBoard, .paste,
.importAttachment,
.listAttachments, .removeAttachment, .relocateLooseFile, .agentGuide, .seedGitignore,
.mintBoardIndex, .stampSchema, .setBoardBackground,
@@ -3326,6 +3335,7 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
case let .rename(title): Self.phrase("rename", title)
case let .duplicateBoard(title): Self.phrase("duplicate board", title)
case let .saveAsTemplate(title): Self.phrase("save as template", title)
case let .shareBoard(title): Self.phrase("share board", title)
case let .importAttachment(filename): "import attachment '\(filename)'"
case .listAttachments: "list attachments"
case let .removeAttachment(filename): "move attachment '\(filename)' to the Trash"