Files
iccery-v2-mac/Sources/ICCery/SidebarView.swift
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 34bef9a78e fix(stage0): implement printer calibration workflow and stabilise M6 UI tests.
- Add CalibrationTargenArgs, PrintcalArgs, CalibrationStore (stage 0).
- Add ArgyllRunner.runCalibrationTargen and runPrintcal.
- Add CalibrationView + CalibrationViewModel to the app shell.
- Wire calibration into RootView, SidebarView, Stage1View, TargetWorkflowViewModel.
- Remove mutable _uiTestRoot from AppPaths; derive isolated test roots from
  ICCERY_TEST_WORKDIR / process id.
- Fix Milestone6CGATSUITests to assert stage4TargetBasename instead of a
  VStack accessibility identifier.
- Fix Milestone6CalibrationUITests fixture binary path.

Refs: #29

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-09 22:27:02 +01:00

154 lines
5.6 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
import ICCeryCore
/// 270 pt sidebar (docs/21 §Shell): logo, settings/about buttons, preset
/// select, Calibrate Printer + status chip, and the 15 stepper.
struct SidebarView: View {
@Bindable var workflow: TargetWorkflowViewModel
var onOpenSettings: () -> Void
var onOpenAbout: () -> Void
@Binding var showingAllHelp: Bool
private var model: WizardViewModel { workflow.wizard }
var body: some View {
VStack(alignment: .leading, spacing: 0) {
HStack {
Image("ICCery-logo")
.resizable()
.scaledToFit()
.frame(height: 40)
Spacer()
Button(action: onOpenSettings) {
Image(systemName: "gearshape")
}
.buttonStyle(.plain)
.helpOverlay("Open the Settings dialog.", showing: $showingAllHelp)
.accessibilityIdentifier("openSettingsBtn")
Button(action: onOpenAbout) {
Image(systemName: "info.circle")
}
.buttonStyle(.plain)
.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)
Divider().overlay(Theme.border)
// Preset select (`#presetSelect`) — issue #11. Selection
// applies the preset immediately; names render via Text only.
Picker("Preset", selection: Binding(
get: { workflow.selectedPresetID },
set: { id in
if id == "none" {
workflow.selectedPresetID = "none"
} else if let preset = workflow.presets.first(where: { $0.id == id }) {
workflow.applyPreset(preset)
}
}
)) {
Text("No preset").tag("none")
ForEach(workflow.presets) { preset in
Text(preset.name).tag(preset.id)
}
}
.pickerStyle(.menu)
.accessibilityIdentifier("presetSelect")
.padding(.horizontal, 12)
.padding(.vertical, 8)
HStack(spacing: 8) {
Button("Save") { workflow.showingSavePreset = true }
.accessibilityIdentifier("btnSavePresetModal")
Button("Manage") { workflow.showingManagePresets = true }
.accessibilityIdentifier("btnOpenPresetsDialog")
Spacer()
}
.padding(.horizontal, 12)
.padding(.bottom, 8)
// Calibrate Printer (`#btnCalibratePrinter`).
Button(action: { model.enterCalibration() }) {
Label("Calibrate Printer", systemImage: "slider.horizontal.3")
.frame(maxWidth: .infinity)
}
.controlSize(.large)
.accessibilityIdentifier("btnCalibratePrinter")
.padding(.horizontal, 12)
Button(action: { model.openGamut(profileGamURL: workflow.profile.createdGamutURL) }) {
Label("View Gamut", systemImage: "view.3d")
.frame(maxWidth: .infinity)
}
.controlSize(.large)
.accessibilityIdentifier("btnViewGamut")
.padding(.horizontal, 12)
Divider().overlay(Theme.border)
.padding(.vertical, 8)
// Stepper 15.
VStack(alignment: .leading, spacing: 2) {
ForEach(WizardStage.stepperStages, id: \.self) { stage in
StepperRow(
stage: stage,
isActive: model.stage == stage,
// Artefact gating (issue #4) — disk is truth.
isEnabled: model.isUnlocked(stage)
) {
model.go(to: stage)
}
}
}
.padding(.horizontal, 6)
Spacer()
}
.frame(width: Theme.Metrics.sidebarWidth)
.background(Theme.panel)
}
}
private struct StepperRow: View {
let stage: WizardStage
let isActive: Bool
let isEnabled: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
HStack(spacing: 10) {
ZStack {
Circle()
.fill(isActive ? Theme.accent : Theme.border)
.frame(width: 26, height: 26)
Text("\(stage.stepperIndex ?? 0)")
.font(.callout.bold())
.foregroundStyle(isActive ? .white : Theme.text)
}
Label(stage.title, systemImage: stage.symbolName)
.font(.callout)
.foregroundStyle(isActive ? Theme.text : .secondary)
Spacer()
}
.padding(.horizontal, 8)
.padding(.vertical, 6)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.disabled(!isEnabled)
.opacity(isEnabled ? 1 : 0.45)
.background(
RoundedRectangle(cornerRadius: Theme.Metrics.cornerMedium)
.fill(isActive ? Theme.accent.opacity(0.15) : .clear)
)
}
}