Compare commits

...
Author SHA1 Message Date
gronod 1d056c26bf docs: use PlantUML maps so Kroki fits a phone width
macOS CI / build-and-test (push) Skipped
macOS CI / build-and-test (pull_request) Waiting to run
macOS CI / package (pull_request) Blocked by required conditions
Class diagrams rendered 6556px wide; Gitea clipped that to a
black arrowhead and the UML C icon. Stacked maps are 560-780px
and render as labelled tables on public Kroki.
2026-09-14 15:04:22 +00:00
gronod c4f037535a docs: restore PlantUML UI maps; draw groups as rectangles
macOS CI / build-and-test (push) Skipped
macOS CI / build-and-test (pull_request) Waiting to run
macOS CI / package (pull_request) Blocked by required conditions
Revert the maps to 368ae56 and fix the Kroki render: default
package style is a folder, which Gitea scaled to a black tab.
Groups are rectangles on a white canvas; shape stereotypes
(<<Button>>, <<*.swift>>) are folded into the title so C4/sprites
cannot retarget the box.
2026-09-14 14:56:52 +00:00
gronod 191bcb0280 docs: replace hairball graphs with small overviews and tables
macOS CI / build-and-test (push) Skipped
macOS CI / build-and-test (pull_request) Waiting to run
macOS CI / package (pull_request) Blocked by required conditions
Gitea scaled the 50-node GraphViz maps to unreadably tiny black
boxes. Overviews are now ~12 nodes on an opaque white canvas;
enable/hide rules live in markdown tables that work in dark mode.
2026-09-14 14:44:59 +00:00
gronod 9c625ee21f docs: dark-mode GraphViz palette for Gitea Kroki
macOS CI / build-and-test (push) Skipped
macOS CI / build-and-test (pull_request) Waiting to run
macOS CI / package (pull_request) Blocked by required conditions
Unfilled nodes and default black type sat on a transparent SVG,
so dark mode showed only tiny black boxes. Opaque GitHub-dark
fills, light labels, larger type.
2026-09-14 14:40:28 +00:00
gronod 40cc4ad701 docs: drop PlantUML UI maps that Kroki 0.30.1 cannot run without AVX2
macOS CI / build-and-test (push) Skipped
macOS CI / build-and-test (pull_request) Waiting to run
macOS CI / package (pull_request) Blocked by required conditions
2026-09-14 14:25:52 +00:00
gronod 658d9813bd docs: render UI maps with GraphViz for Kroki 0.30.1
macOS CI / build-and-test (push) Skipped
macOS CI / package (pull_request) Blocked by required conditions
macOS CI / build-and-test (pull_request) In progress
Kroki 0.30.1 PlantUML is a GraalVM native image and requires AVX2.
This host has none, so the maps now use GraphViz (dot) — one
diagram per file, plus a markdown page with graphviz fences.
2026-09-14 14:25:35 +00:00
gronod 368ae56cf3 docs: split UI PlantUML map into one diagram per file
macOS CI / build-and-test (push) Skipped
macOS CI / build-and-test (pull_request) Failing after 40m37s
macOS CI / package (pull_request) Skipped
Kroki only renders the first @startuml block in a file.
2026-09-14 14:18:07 +00:00
gronod bd539df009 docs: PlantUML map of every interactive control
macOS CI / build-and-test (push) Skipped
macOS CI / build-and-test (pull_request) Failing after 3m58s
macOS CI / package (pull_request) Skipped
Identifiers, source files, and enable/hide/disable rules for the
wizard, sidebar, sheets, and File menu.
2026-09-14 14:03:26 +00:00
gronod 1b91b0a94e Merge pull request 'fix(ui): media Apply missing-printer notice visible in manage sheet (#170)' (#171) from fix/170-media-library-missing-printer-notice into develop
macOS CI / build-and-test (push) Failing after 42m49s
macOS CI / package (push) Skipped
fix(ui): media Apply missing-printer notice visible in manage sheet (#170)

Fixes #170
2026-09-14 14:37:05 +01:00
gronod 890e7281eb fix(ui): show media Apply failure inside the manage sheet (#170)
macOS CI / build-and-test (push) Skipped
macOS CI / package (pull_request) Canceled after 0s
macOS CI / build-and-test (pull_request) Canceled after 2m31s
Failed Apply kept the sheet open while the window banner sat
behind it, so Monterey XCTest never saw noticeText. Mirror the
failure string in the dialog (manageMediaNotice) and point the
UI test at that identifier.

Fixes #170
2026-09-14 13:36:59 +00:00
8 changed files with 382 additions and 25 deletions
+9
View File
@@ -220,6 +220,15 @@ struct ManageMediaDialog: View {
.accessibilityIdentifier("mediaLibraryList")
.frame(minHeight: 260)
if let notice = media.manageApplyNotice {
Text(notice)
.font(.callout)
.foregroundStyle(.orange)
.fixedSize(horizontal: false, vertical: true)
.accessibilityIdentifier("manageMediaNotice")
.accessibilityValue(notice)
}
HStack {
Button("Apply selected") {
if let id = selection,
+21 -24
View File
@@ -41,6 +41,9 @@ final class MediaLibraryViewModel: ObservableObject {
@Published var saveMediaApplyCal = false
/// Inline caption inside the capture sheet (no a11y id roster complete).
@Published var saveMediaError: String?
/// Last failed Apply while Manage is open. The window banner sits
/// behind the sheet on Monterey, so the dialog shows this too (#170).
@Published var manageApplyNotice: String?
/// Pure flow flag the manage sheet's "Capture current" asks the
/// sheet's `onDismiss` to open the capture sheet, avoiding a
@@ -120,24 +123,21 @@ final class MediaLibraryViewModel: ObservableObject {
/// with warning; the refusal is permanent so re-clicking can't help).
@discardableResult
func apply(_ recipe: MediaRecipe) async -> Bool {
manageApplyNotice = nil
guard let r = try? recipe.validated() else {
workflow.wizard.showNotice(
"Media recipe is invalid — not applied.", kind: .error)
return false
return failApply("Media recipe is invalid — not applied.", kind: .error)
}
guard let preset = environment.presetStore.all()
.first(where: { $0.id == r.presetID })
else {
workflow.wizard.showNotice(
return failApply(
"Preset \(r.presetID) no longer exists — recipe not applied.",
kind: .error)
return false
}
guard preset.colourSpace.lowercased() == r.colourSpace.lowercased() else {
workflow.wizard.showNotice(
return failApply(
"Recipe colour space does not match its preset — not applied.",
kind: .error)
return false
}
// Existing #82 mapping: presetSelect jumps, Stage 1/2/4 fields.
@@ -145,8 +145,6 @@ final class MediaLibraryViewModel: ObservableObject {
// Literal per issue: displayName, not the queue id.
workflow.wizard.printerName = r.printerDisplayName
var succeeded = true
// Queue: enumerate fresh via the session's serialized path
// listPrinters uses fixed process ids, so an overlapping
// enumeration would throw duplicateID. An empty result is a
@@ -156,16 +154,14 @@ final class MediaLibraryViewModel: ObservableObject {
workflow.print.selectedPrinter = r.printerID
await workflow.print.reloadSelectedCapabilities()
} else {
workflow.wizard.showNotice(
return failApply(
"Printer \(r.printerDisplayName) is not installed.",
kind: .warning)
succeeded = false
}
} else {
workflow.wizard.showNotice(
return failApply(
"Could not enumerate printers — queue left unchanged.",
kind: .warning)
succeeded = false
}
// Calibration the recipe is authoritative and runs after
@@ -195,10 +191,8 @@ final class MediaLibraryViewModel: ObservableObject {
guard FileManager.default.fileExists(atPath: calPath) else {
workflow.profile.applyCalibration = false
workflow.profile.calibrationFile = calPath
workflow.wizard.showNotice(
return failApply(
"Calibration file is missing: \(calPath)", kind: .error)
refreshStaleness()
return false
}
do {
let staleDays = environment.settingsStore.load().calibrationStaleDays
@@ -215,23 +209,26 @@ final class MediaLibraryViewModel: ObservableObject {
}
} catch {
workflow.profile.applyCalibration = false
workflow.wizard.showNotice(
return failApply(
"Could not load calibration: \(error.localizedDescription)",
kind: .error)
refreshStaleness()
return false
}
} else {
workflow.profile.applyCalibration = false
workflow.profile.calibrationFile = calPath
}
if succeeded {
selectedRecipeID = r.id
workflow.wizard.showNotice("Applied \(r.name)")
}
selectedRecipeID = r.id
workflow.wizard.showNotice("Applied \(r.name)")
refreshStaleness()
return succeeded
return true
}
private func failApply(_ text: String, kind: Notice.Kind) -> Bool {
manageApplyNotice = text
workflow.wizard.showNotice(text, kind: kind)
refreshStaleness()
return false
}
// MARK: - Capture
@@ -148,7 +148,9 @@ final class Milestone10MediaLibraryUITests: XCTestCase {
XCTAssertTrue(apply.waitForExistence(timeout: 10))
apply.click()
let notice = waitFor("noticeText")
// The window banner (`noticeText`) sits behind this sheet on
// Monterey (#170). Assert the in-sheet copy instead.
let notice = waitFor("manageMediaNotice", timeout: 15)
let text = (notice.value as? String) ?? notice.label
XCTAssertTrue(
text.contains("is not installed"),
+89
View File
@@ -0,0 +1,89 @@
@startuml ICCery-UI-sheets
title Sheets Settings, presets, media, Spot Read, Gamut, project
!theme plain
skinparam backgroundColor white
skinparam defaultFontColor black
skinparam defaultFontSize 13
skinparam mapBackgroundColor white
skinparam mapBorderColor #222222
skinparam mapFontColor black
skinparam arrowColor #222222
skinparam shadowing false
hide circle
top to bottom direction
map "SettingsView.swift" as set {
Argyll dir => TextField+Browse : empty = bundled
Default instrument => Picker : seeds Stage 3 and Spot Read
Enable i1Pro 2 LEDs => Toggle
settingsDeltaEGood => TextField
settingsDeltaEWarning => TextField : must be greater than Good
settingsCalStaleDays => TextField
Install location => Picker User or System
Ask before overwriting => Toggle
Open ColorSync after install => Toggle
Log level => Picker
Open log folder => Button
Cancel / Save => Save stays if validation fails
}
map "Presets PresetDialogs.swift" as pre {
savePresetName => TextField
savePresetDesc => TextField
btnCloseSavePresetDialog => Button
btnConfirmSavePreset => Button : disabled if name empty
presetRow-id => Row : built-in cannot delete
btnImportPreset => Button
btnExportActivePreset => Button : custom selected
btnCloseManagePresetsDialog => Button
}
map "Media MediaLibraryDialogs.swift" as media {
saveMediaName/Paper/Ink => TextField
saveMediaApplyCal => Toggle : disabled if CAL_ or missing file
btnConfirmSaveMedia => Button : name+paper+ink required
mediaLibraryList => List
btnMediaLibraryApply-id => Button per row
manageMediaNotice => Caption : failed Apply in-sheet
btnMediaLibraryApply => Button : disabled if no selection
btnCloseManageMediaDialog => Button
}
map "spotReadView SpotReadView.swift" as spot {
btnSpotDetectInstruments => Button
spotInstrumentSelect => Picker : disabled while running
spotSetDefault => Toggle
btnSpotStart => Button : needs sidecar, cwd, not chartread
btnSpotCalibrate => Button : calibrating
btnSpotTrigger => Button : Read, awaitingStrip
btnSpotStop => Button : while running
btnSpotCopyLab => Button : disabled if no sample
btnSpotExportCsv => Button : disabled if no history
btnCloseSpotRead => Button Esc : dismiss = Stop
}
map "gamutView GamutView.swift" as gam {
gamutLayer-srgb => Toggle : can hide, cannot remove
gamutLayer-profile => Toggle : disabled if no .gam
gamutLayer-compare => Toggle : disabled if no compare
btnGamutAddCompare => Menu
btnGamutRemoveCompare => Button
btnGamutSampleTiff => Button
btnResetGamutCamera => Button R
gamutLabEntryL/A/B => TextField
btnGamutInspectLab => Button
btnCloseGamut => Button Esc
}
map "Project alerts" as proj {
projectNewAlert => Alert : New Cancel / Confirm
Dirty save => Alert : Save / Don't Save / Cancel
projectRelocateSheet => Sheet : cwd missing on Open
}
set -[hidden]down- pre
pre -[hidden]down- media
media -[hidden]down- spot
spot -[hidden]down- gam
gam -[hidden]down- proj
@enduml
+81
View File
@@ -0,0 +1,81 @@
@startuml ICCery-UI-shell
title Shell, sidebar, stepper, File menu
!theme plain
skinparam backgroundColor white
skinparam defaultFontColor black
skinparam defaultFontSize 13
skinparam mapBackgroundColor white
skinparam mapBorderColor #222222
skinparam mapFontColor black
skinparam arrowColor #222222
skinparam shadowing false
hide circle
top to bottom direction
map "Window" as win {
WindowGroup => ICCeryApp.swift : 1280x800 min 1100x700
RootView => RootView.swift : hosts every sheet
noticeText => NoticeBanner : shown if wizard.notice set; auto-hide 6s
}
map "Sidebar header" as hdr {
openSettingsBtn => Button : always / Settings sheet
openAboutBtn => Button : always / About
btnToggleAllHelp => Button : toggles yellow help dots
}
map "Presets" as pre {
presetSelect => Picker.menu : none + preset id; none is not factory reset
btnSavePresetModal => Button : always
btnOpenPresetsDialog => Button : always
}
map "Media library" as med {
mediaSelect => Picker.menu : never reuses presetSelect
mediaRecipeStale => Caption : shown if staleReasons nonempty
btnMediaLibraryCapture => Button : disabled if no Stage 2 printer
btnMediaLibraryManage => Button : always
}
map "Studio (not stepper)" as stu {
btnCalibratePrinter => Button : always; Stage 0
btnViewGamut => Button : sidebar always on; same id as Stage 5
btnSpotRead => Button : disabled if no cwd or chartread running
}
map "Stepper 1-5 disk is truth" as stp {
Stage1 Generate => always
Stage2 Lay out => .ti1
Stage3 Measure => .ti1 AND .ti2
Stage4 Build => .ti3 (not .ti2 alone)
Stage5 Verify => .ti3 AND .icc/.icm
Calibrate => not a stepper row; always available
}
map "Project chip" as chip {
projectChipName => Text : bound name or No project
projectChipPath => Text : shown if bound
projectChipStale => Caption : diskBehindNotes
btnProjectReveal => Button : shown if bound
btnProjectSave => Button : canSave = bound + basename + cwd
btnProjectOpen => Button : shown if not bound
}
map "File menu" as menu {
menuProjectNew => Button Cmd-N
menuProjectOpen => Button Cmd-O
menuProjectRecents => Menu : disabled if recents empty
menuProjectSave => Button Cmd-S : !canSave
menuProjectSaveAs => Button Shift-Cmd-S : basename + cwd
menuProjectReport => Button : !canReport
menuProjectClose => Button : !isBound
}
win -[hidden]down- hdr
hdr -[hidden]down- pre
pre -[hidden]down- med
med -[hidden]down- stu
stu -[hidden]down- stp
stp -[hidden]down- chip
chip -[hidden]down- menu
@enduml
+83
View File
@@ -0,0 +1,83 @@
@startuml ICCery-UI-stage1-2
title Stages 1-2 Generate Target / Lay Out and Print
!theme plain
skinparam backgroundColor white
skinparam defaultFontColor black
skinparam defaultFontSize 13
skinparam mapBackgroundColor white
skinparam mapBorderColor #222222
skinparam mapFontColor black
skinparam arrowColor #222222
skinparam shadowing false
hide circle
top to bottom direction
map "stage-1 Stage1View.swift" as s1 {
colourSpace => Picker.segmented : RGB or CMYK
patchCountPreset => Picker
patchCountCustom => TextField : shown if preset is custom
whitePatches => Stepper 0-50
blackPatches => Stepper 0-50
targetBasename => TextField
btnBrowse => Button
btnSelectWorkDir => Button
btnOpenExisting => Button : .ti1 or .ti2
btn-import-dataset => Button : unlocks stage 4
selectedPathDisplay => Text
targenAdvancedDetails => DisclosureGroup : UI tests pre-expand
btnGenerate => Button : needs basename AND directory
targenLog => ProcessLogView
}
map "Stage 1 Advanced (inside disclosure)" as adv {
targenGreySteps => Toggle+field : field iff on
targenSingleChannelSteps => Toggle+field
targenNeutralSteps => Toggle+field
targenNeutralConcentration => Toggle+Slider 0-1
targenAdaptation => Toggle+Slider 0-1
targenPrecondProfile => TextField
btnBrowsePrecondProfile => Button
targenHighQuality => Toggle
targenAlgorithm => Picker
targenInkLimitGroup => Toggle+field : CMYK only
targenDarkEmphasis => Toggle+Slider 0-3
targenDevicePower => Toggle+Slider 0-3
}
map "stage-2 Stage2View.swift" as s2 {
cmWarningBanner => Banner : always on stage 2
instrumentSelect => Picker : chart code, not USB port
pageSizeSelect => Picker
customPageW / customPageH => TextField : pageSize custom
tiffDpi => Stepper 72-600
printtargLayoutOrder => Picker
printtargCustomSeed => TextField : custom seed order
btnToggleLabelEdit => Button
targetMetadataPrinter => TextField
targetMetadataInkSet => TextField
targetMetadataDriverPaper => TextField
targetMetadataActualPaper => TextField
targetLabelPreview => TextField or Text
btnCreateLayout => Button : disabled if no basename
tiffGallery => Grid : after printtarg
btnPrintPage-N => Button : disabled if no printer
}
map "rawPrintPanel" as prn {
printerSelect => Picker CUPS
printerStatusBadge => Caption
btnRefreshPrinters => Button
btnPrinterProperties => Button : disabled if no printer
printerTraySelect => Picker : if trays exist
printerMediaTypeSelect => Picker : if media types exist
btnOrientPortrait => Button
btnOrientLandscape => Button
btnPrintAll => Button : needs layout AND printer
btnAdvanceToStage3 => Button : needs .ti2 unlock
printNotificationText => Caption : if printNotice set
}
s1 -[hidden]down- adv
adv -[hidden]down- s2
s2 -[hidden]down- prn
@enduml
+84
View File
@@ -0,0 +1,84 @@
@startuml ICCery-UI-stage3-5-cal
title Stages 3-5 and Calibrate Printer
!theme plain
skinparam backgroundColor white
skinparam defaultFontColor black
skinparam defaultFontSize 13
skinparam mapBackgroundColor white
skinparam mapBorderColor #222222
skinparam mapFontColor black
skinparam arrowColor #222222
skinparam shadowing false
hide circle
top to bottom direction
map "stage-3 Stage3View.swift" as s3 {
btnDetectInstruments => Button : disabled while detecting
chartreadInstrumentSelect => Picker.menu
xyTableHint => Caption : if XY instrument
xyTablePanel => Place/Align/Scan/Remove
chartreadPrompt => Text
chartreadLastError => Caption : if notice set
btnStartRead => Button : shown if not running; needs basename+cwd
btnCalibrate => Button : running and calibrating
btnTrigger => Button : running and awaitingStrip
btnDoneReadEarly => Button : awaitingStrip
btnAccept => Button : place / align / continue / warning
btnRetry => Button : error state
btnDoneRead => Button : allStripsRead
btnCancel => Button : while running
swatchGrid => swatch-rowId-loc
btnMeasureAnotherSheet => Button : after a finished pass
btnFinishAndAverage => Button : finished AND at least one pass
}
map "stage-4 Stage4View.swift" as s4 {
colprofAlgorithm => Picker
colprofQuality => Picker
colprofFwa => Picker
colprofFwaCustomPath => TextField : custom spectrum
btnBrowseFwaSp => Button
colprofIlluminant => TextField
colprofObserver => TextField
colprofInputViewCond => TextField
colprofOutputViewCond => TextField
colprofDescription => TextField
colprofCopyright => TextField
colprofApplyCalibration => Toggle
colprofCalibrationFile => TextField : if applyCalibration
btnBrowseCalibrationFile => Button : if applyCalibration
btnCreateProfile => Button : basename + cwd, not running
}
map "stage-5 Stage5View.swift" as s5 {
btnVerifyProfile => Button : needs created profile
btnViewGamut => Button : disabled if no .gam (same id as sidebar)
btnInstallProfile => Button : needs created profile
driftPrinterFilter => Picker
btnExportHistory => Button
btnClearHistory => Button
verificationHistoryTable => Table
driftChart => not interactive
profileOverwriteBtn => Alert : install collision
profileRenameBtn => Alert
profileCancelCollisionBtn => Alert
}
map "stage-cal CalibrationView.swift" as cal {
Colour space => Picker.segmented : no a11y id
calSteps => TextField
White patches => TextField : no a11y id
calInkExplore => TextField : CMYK only
calNeutralEmphasis => Toggle
btnCalGenerate => Button : needs basename + cwd
btnCalLayout => Button : same
btnCalMeasure => Button : needs .ti3
btnCalCompute => Button : .ti3 and not computing
calApplyToggle => Toggle : after curves exist
btnCalReturn => Button Esc : restores original basename
}
s3 -[hidden]down- s4
s4 -[hidden]down- s5
s5 -[hidden]down- cal
@enduml
+12
View File
@@ -0,0 +1,12 @@
# Interactive UI map
PlantUML **maps** (not class diagrams). Class diagrams laid out 6500px wide, so Gitea on a phone showed a sliver of a black arrow and the UML “C” icon.
Each file is one stacked column (~560780px) that Kroki actually fits in the page.
| Diagram | File | Kroki size |
| --- | --- | --- |
| Shell, sidebar, stepper, File menu | [ui-interactive-map-shell.puml](ui-interactive-map-shell.puml) | 563 × 1367 |
| Stages 12 | [ui-interactive-map-stage1-2.puml](ui-interactive-map-stage1-2.puml) | 625 × 1366 |
| Stages 35 + Calibrate | [ui-interactive-map-stage3-5-cal.puml](ui-interactive-map-stage3-5-cal.puml) | 568 × 1386 |
| Sheets | [ui-interactive-map-sheets.puml](ui-interactive-map-sheets.puml) | 782 × 1502 |