Compare commits

...
Author SHA1 Message Date
gronod 563f0e5d4a Merge pull request 'Milestone/m6 gamut stage0 cgats release' (#57) from milestone/m6-gamut-stage0-cgats-release into feat/31-about-help-chrome
Reviewed-on: #57
2026-09-09 18:31:47 +01:00
gronod cd4665e7a9 Merge pull request 'feat/30-cgats-interop' (#56) from feat/30-cgats-interop into milestone/m6-gamut-stage0-cgats-release 2026-09-09 17:20:48 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> f78da50a59 Implement #31: About dialog and global help overlays.
- Add ArtefactFiles.appInfo() build date from executable mtime.
- Add AboutView with version, build, build date, and close button.
- Add HelpOverlay view modifier for non-reflowing help badges.
- Wire About as a sheet and help toggle in SidebarView/RootView.
- Add AboutHelpUITests coverage.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-09 17:04:15 +01:00
6 changed files with 199 additions and 13 deletions
@@ -15,14 +15,26 @@ public enum ArtefactFiles {
try Data(contentsOf: url).base64EncodedString()
}
/// `get_app_info` version + build for the About dialog.
/// `get_app_info` version, build, and build date for the About dialog.
public static func appInfo(
bundle: Bundle = .main
) -> (version: String, build: String) {
) -> (version: String, build: String, buildDate: String) {
let info = bundle.infoDictionary ?? [:]
return (
info["CFBundleShortVersionString"] as? String ?? "0.0.0",
info["CFBundleVersion"] as? String ?? "0"
)
let version = info["CFBundleShortVersionString"] as? String ?? "0.0.0"
let build = info["CFBundleVersion"] as? String ?? "0"
let url = bundle.executableURL ?? bundle.bundleURL
let buildDate: String
if let values = try? url.resourceValues(forKeys: [.contentModificationDateKey]),
let date = values.contentModificationDate {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
buildDate = formatter.string(from: date)
} else {
buildDate = "Unknown"
}
return (version, build, buildDate)
}
}
+62
View File
@@ -0,0 +1,62 @@
import SwiftUI
import ICCeryCore
/// About dialog for ICCery (issue #31, docs/21 §Modals).
struct AboutView: View {
let onClose: () -> Void
private let info = ArtefactFiles.appInfo()
var body: some View {
VStack(spacing: 20) {
Image("ICCery-logo")
.resizable()
.scaledToFit()
.frame(height: 64)
Text("ICCery")
.font(.title)
.foregroundStyle(Theme.text)
VStack(alignment: .leading, spacing: 4) {
HStack {
Text("Version:")
.foregroundStyle(.secondary)
Text(info.version)
.foregroundStyle(Theme.text)
.accessibilityIdentifier("aboutVersion")
}
HStack {
Text("Build:")
.foregroundStyle(.secondary)
Text(info.build)
.foregroundStyle(Theme.text)
}
HStack {
Text("Build date:")
.foregroundStyle(.secondary)
Text(info.buildDate)
.foregroundStyle(Theme.text)
.accessibilityIdentifier("aboutBuildDate")
}
}
.font(.callout)
Text("Native macOS printer profiling workstation.")
.font(.caption)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
Button("Close") {
onClose()
}
.controlSize(.large)
.keyboardShortcut(.cancelAction)
.accessibilityIdentifier("closeAboutBtn")
}
.padding(32)
.frame(width: 360)
.background(Theme.panel)
.accessibilityIdentifier("aboutDialog")
}
}
+31
View File
@@ -0,0 +1,31 @@
import SwiftUI
/// Reusable help overlay badge that does not reflow layout (#171).
///
/// When `showing` is `true`, a small indicator is rendered as an overlay at the
/// top-trailing corner of the wrapped view. The native `.help` tooltip is always
/// available on hover, so the overlay is purely a visual cue in help mode.
struct HelpOverlay: ViewModifier {
let text: String
@Binding var showing: Bool
func body(content: Content) -> some View {
content
.help(text)
.overlay(alignment: .topTrailing) {
if showing {
Image(systemName: "questionmark.circle.fill")
.font(.system(size: 10, weight: .bold))
.foregroundStyle(Theme.accent)
.offset(x: 8, y: -8)
}
}
}
}
extension View {
/// Adds a non-reflowing help overlay to the view.
func helpOverlay(_ text: String, showing: Binding<Bool>) -> some View {
modifier(HelpOverlay(text: text, showing: showing))
}
}
+5 -5
View File
@@ -8,6 +8,7 @@ struct RootView: View {
@Bindable var workflow: TargetWorkflowViewModel
@State private var showingSettings = false
@State private var showingAbout = false
@State private var showingAllHelp = false
private var model: WizardViewModel { workflow.wizard }
@@ -16,7 +17,8 @@ struct RootView: View {
SidebarView(
workflow: workflow,
onOpenSettings: { showingSettings = true },
onOpenAbout: { showingAbout = true }
onOpenAbout: { showingAbout = true },
showingAllHelp: $showingAllHelp
)
Rectangle()
@@ -48,10 +50,8 @@ struct RootView: View {
.sheet(isPresented: $workflow.showingManagePresets) {
ManagePresetsDialog(workflow: workflow)
}
.alert("ICCery 2.0.0", isPresented: $showingAbout) {
Button("OK") {}
} message: {
Text("Native macOS printer profiling workstation.\nFull About dialog lands in issue #31.")
.sheet(isPresented: $showingAbout) {
AboutView { showingAbout = false }
}
}
+11 -2
View File
@@ -7,6 +7,7 @@ struct SidebarView: View {
@Bindable var workflow: TargetWorkflowViewModel
var onOpenSettings: () -> Void
var onOpenAbout: () -> Void
@Binding var showingAllHelp: Bool
private var model: WizardViewModel { workflow.wizard }
@@ -22,12 +23,20 @@ struct SidebarView: View {
Image(systemName: "gearshape")
}
.buttonStyle(.plain)
.help("Settings")
.helpOverlay("Open the Settings dialog.", showing: $showingAllHelp)
.accessibilityIdentifier("openSettingsBtn")
Button(action: onOpenAbout) {
Image(systemName: "info.circle")
}
.buttonStyle(.plain)
.help("About ICCery")
.helpOverlay("Open the About dialog.", showing: $showingAllHelp)
.accessibilityIdentifier("openAboutBtn")
Button(action: { showingAllHelp.toggle() }) {
Image(systemName: showingAllHelp ? "questionmark.circle.fill" : "questionmark.circle")
}
.buttonStyle(.plain)
.help("Toggle help overlays")
.accessibilityIdentifier("btnToggleAllHelp")
}
.padding(12)
@@ -0,0 +1,72 @@
import XCTest
/// About and help chrome UI tests (issue #31).
@MainActor
final class AboutHelpUITests: XCTestCase {
private var app: XCUIApplication!
override func setUp() async throws {
continueAfterFailure = false
app = XCUIApplication()
app.launchEnvironment = ["ICCERY_UI_TESTING": "1"]
}
override func tearDown() async throws {
app?.terminate()
app = nil
}
private func element(_ id: String) -> XCUIElement {
app.descendants(matching: .any)[id]
}
private func waitFor(_ id: String, timeout: TimeInterval = 10) -> XCUIElement {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
let el = element(id)
if el.exists { return el }
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
}
let el = element(id)
XCTAssertTrue(el.exists, "Expected element \(id)")
return el
}
func testAboutDialogShowsVersionAndBuildDate() throws {
app.launch()
app.activate()
let openAbout = app.buttons["openAboutBtn"]
XCTAssertTrue(openAbout.waitForExistence(timeout: 10))
openAbout.click()
_ = waitFor("aboutDialog", timeout: 10)
XCTAssertTrue(element("aboutVersion").exists)
XCTAssertTrue(element("aboutBuildDate").exists)
let close = app.buttons["closeAboutBtn"]
XCTAssertTrue(close.exists)
close.click()
XCTAssertFalse(element("aboutDialog").exists)
}
func testHelpOverlaysDoNotChangeSidebarHeight() throws {
app.launch()
app.activate()
let toggle = app.buttons["btnToggleAllHelp"]
XCTAssertTrue(toggle.waitForExistence(timeout: 10))
let sidebar = app.groups.containing(.button, identifier: "openSettingsBtn").element
let before = sidebar.frame
toggle.click()
let after = sidebar.frame
XCTAssertEqual(before.size.height, after.size.height,
"Toggling global help must not reflow the sidebar height.")
XCTAssertTrue(app.descendants(matching: .any)["openSettingsBtn"].exists)
}
}