diff --git a/Sources/KitchenSyncApp/Design/KSTokens.swift b/Sources/KitchenSyncApp/Design/KSTokens.swift index c0149c9..b110d1b 100644 --- a/Sources/KitchenSyncApp/Design/KSTokens.swift +++ b/Sources/KitchenSyncApp/Design/KSTokens.swift @@ -47,7 +47,7 @@ enum KS { /// Text drawn ON lime. static let onInk = Color(hex: 0x0A0D07) static let amber = Color(hex: 0xFF9D3B) - /// The stopped label — salmon, not grey. + /// The stopped label — salmon, not gray. static let ember = Color(hex: 0xFF7A6B) static let ledFill = LinearGradient(colors: [Color(hex: 0xCAFF5A), Color(hex: 0x9BE32A)], @@ -75,7 +75,7 @@ enum KS { // (the BPM readout). All three are SIL OFL 1.1 and SHOULD be bundled to keep // the identity 1:1. They are not bundled yet — these are the documented // fallbacks. Note the design direction's rule: if DSEG7 is absent, the tempo -// glass must DROP its ghost layer, because a grey duplicate of a non-segmented +// glass must DROP its ghost layer, because a gray duplicate of a non-segmented // font reads as a rendering bug rather than unlit segments. extension Font { diff --git a/Sources/KitchenSyncApp/Discovery/DeviceMatch.swift b/Sources/KitchenSyncApp/Discovery/DeviceMatch.swift index c90785e..fb0a1cf 100644 --- a/Sources/KitchenSyncApp/Discovery/DeviceMatch.swift +++ b/Sources/KitchenSyncApp/Discovery/DeviceMatch.swift @@ -26,7 +26,7 @@ enum DeviceMatch { return matchesKnownHostname(serviceName) } - /// The FALLBACK. A heuristic, and deliberately labelled as one — it exists to keep un-updated + /// The FALLBACK. A heuristic, and deliberately labeled as one — it exists to keep un-updated /// units working, not because it is correct. /// /// The firmware advertises generic `_http._tcp` with no TXT record, so the hostname is all a diff --git a/Sources/KitchenSyncApp/Discovery/DiscoveryStatus.swift b/Sources/KitchenSyncApp/Discovery/DiscoveryStatus.swift index f6751e6..82ee692 100644 --- a/Sources/KitchenSyncApp/Discovery/DiscoveryStatus.swift +++ b/Sources/KitchenSyncApp/Discovery/DiscoveryStatus.swift @@ -30,7 +30,7 @@ enum DiscoveryStatus: Equatable { /// The browser died. `NWBrowser` does not come back from `.failed` on its own — /// somebody has to restart it. case failed - /// Cancelled, because we asked. + /// Canceled, because we asked. case stopped /// `kDNSServiceErr_PolicyDenied`. The one signal that means "the user has to act". @@ -42,7 +42,7 @@ enum DiscoveryStatus: Equatable { self = .starting case .ready: self = .browsing - case .cancelled: + case .cancelled: // spelling: ok (NWBrowser.State, Apple API) self = .stopped case .waiting(let error): // A permission denial ARRIVES AS A WAIT, which is precisely the trap: it looks diff --git a/Sources/KitchenSyncApp/Discovery/KitchenSyncDiscovery.swift b/Sources/KitchenSyncApp/Discovery/KitchenSyncDiscovery.swift index cbd169e..e1e5f8c 100644 --- a/Sources/KitchenSyncApp/Discovery/KitchenSyncDiscovery.swift +++ b/Sources/KitchenSyncApp/Discovery/KitchenSyncDiscovery.swift @@ -67,7 +67,7 @@ final class KitchenSyncDiscovery { } func stop() { - browser?.stateUpdateHandler = nil // cancelling is not news; don't report .stopped + browser?.stateUpdateHandler = nil // canceling is not news; don't report .stopped browser?.cancel() browser = nil } diff --git a/Sources/KitchenSyncApp/Discovery/SetupNetwork.swift b/Sources/KitchenSyncApp/Discovery/SetupNetwork.swift index 8ba2011..aab40b3 100644 --- a/Sources/KitchenSyncApp/Discovery/SetupNetwork.swift +++ b/Sources/KitchenSyncApp/Discovery/SetupNetwork.swift @@ -16,7 +16,7 @@ enum SetupNetwork { /// - `KitchenSync-Setup` — the P4 (`wifi_link.c` `AP_SSID`) /// - `KSTouch-Config` — the Touch (`KitchenSyncTouch.ino` `WiFi.softAP`) /// Matched as a **family of prefixes**, not one string, so a new product's setup AP is - /// recognised without an app change — the same reason `DeviceMatch` matches host prefixes. + /// recognized without an app change — the same reason `DeviceMatch` matches host prefixes. static let ssidPrefixes = ["kitchensync-setup", "kstouch-config"] /// True if `ssid` is one of our devices' setup APs. Case-insensitive: a WiFi SSID's case is diff --git a/Sources/KitchenSyncApp/Info.plist b/Sources/KitchenSyncApp/Info.plist index 4a46c60..c04aa35 100644 --- a/Sources/KitchenSyncApp/Info.plist +++ b/Sources/KitchenSyncApp/Info.plist @@ -18,6 +18,8 @@ $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) + ITSAppUsesNonExemptEncryption + NSBonjourServices _http._tcp @@ -26,5 +28,18 @@ KitchenSync finds your devices on the local network to show their status and control their MIDI clock. UILaunchScreen + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + diff --git a/Sources/KitchenSyncApp/Models/TransportLaunchState.swift b/Sources/KitchenSyncApp/Models/TransportLaunchState.swift index dec1bea..e6e4124 100644 --- a/Sources/KitchenSyncApp/Models/TransportLaunchState.swift +++ b/Sources/KitchenSyncApp/Models/TransportLaunchState.swift @@ -3,7 +3,7 @@ import Foundation /// Mirrors `TransportLaunchState` (`link-devices/X32Link/transport_launch.h`) as /// reported per output in `/status`'s `launch[4]` array. Quantized: Play arms, /// waits for the next bar line, then runs — Stop is immediate. That asymmetry is -/// deliberate (it's Ableton's own launch behaviour), so drive UI straight from +/// deliberate (it's Ableton's own launch behavior), so drive UI straight from /// this value rather than re-deriving an "armed" state on the client — the /// device already computes and reports it every poll. enum TransportLaunchState: Int, Decodable { diff --git a/Sources/KitchenSyncApp/ViewModels/DeviceDetailViewModel.swift b/Sources/KitchenSyncApp/ViewModels/DeviceDetailViewModel.swift index fd444fb..03eab62 100644 --- a/Sources/KitchenSyncApp/ViewModels/DeviceDetailViewModel.swift +++ b/Sources/KitchenSyncApp/ViewModels/DeviceDetailViewModel.swift @@ -29,7 +29,7 @@ final class DeviceDetailViewModel: ObservableObject { func start() { guard pollTask == nil else { return } pollTask = Task { [weak self] in - while !Task.isCancelled { + while !Task.isCancelled { // spelling: ok (Apple API) await self?.refreshStatus() try? await Task.sleep(for: self?.pollInterval ?? .seconds(1)) } diff --git a/Sources/KitchenSyncApp/ViewModels/DeviceListViewModel.swift b/Sources/KitchenSyncApp/ViewModels/DeviceListViewModel.swift index 94c4f20..a2e091e 100644 --- a/Sources/KitchenSyncApp/ViewModels/DeviceListViewModel.swift +++ b/Sources/KitchenSyncApp/ViewModels/DeviceListViewModel.swift @@ -64,7 +64,7 @@ final class DeviceListViewModel: ObservableObject { } discovery.start() pollTask = Task { - while !Task.isCancelled { + while !Task.isCancelled { // spelling: ok (Apple API) await refreshAll() try? await Task.sleep(for: .seconds(2)) } diff --git a/Sources/KitchenSyncApp/Views/DeviceListView.swift b/Sources/KitchenSyncApp/Views/DeviceListView.swift index 1012203..7892ff2 100644 --- a/Sources/KitchenSyncApp/Views/DeviceListView.swift +++ b/Sources/KitchenSyncApp/Views/DeviceListView.swift @@ -251,7 +251,7 @@ private struct DeviceRow: View { // "0 PEERS" for a device that isn't answering is a lie dressed as // a measurement. If there's no status, we don't know the peer count. // And "NO SIGNAL" is wrong for a device that's answering fine — it's - // signalling perfectly, in a dialect we can't read. + // signaling perfectly, in a dialect we can't read. Text(peersCaption) .font(.ksMono(10.5)) .tracking(1.6) diff --git a/Sources/KitchenSyncApp/Views/DeviceLiveSettingsSheet.swift b/Sources/KitchenSyncApp/Views/DeviceLiveSettingsSheet.swift index 9131624..38bff1b 100644 --- a/Sources/KitchenSyncApp/Views/DeviceLiveSettingsSheet.swift +++ b/Sources/KitchenSyncApp/Views/DeviceLiveSettingsSheet.swift @@ -10,7 +10,7 @@ import SwiftUI /// **No surface ever mixes live and reboot controls.** The device's own web page puts the metronome /// ENABLE toggle and the metro-accent toggle forty pixels apart, as the identical component, in the /// same box — one applies instantly and the other restarts the device mid-set, with nothing on -/// screen to tell them apart. Better labelling would not have fixed that. Not being adjacent does. +/// screen to tell them apart. Better labeling would not have fixed that. Not being adjacent does. /// /// Sections appear only for hardware the device actually reports (`link-devices` ESP-030): no /// speaker, no click section; no strip, no LED section. Solder a strip onto a Touch and flip one @@ -162,7 +162,7 @@ struct DeviceLiveSettingsSheet: View { } KSField(prefix: "BEAT") { - ColorPicker("Beat colour", selection: Binding( + ColorPicker("Beat color", selection: Binding( get: { Color(hex: l.beatColor) }, set: { onEdit(.ledBeatColor($0.rgb24)) } ), supportsOpacity: false) @@ -171,7 +171,7 @@ struct DeviceLiveSettingsSheet: View { } KSField(prefix: "ACCENT") { - ColorPicker("Accent colour", selection: Binding( + ColorPicker("Accent color", selection: Binding( get: { Color(hex: l.accentColor) }, set: { onEdit(.ledAccentColor($0.rgb24)) } ), supportsOpacity: false) diff --git a/Sources/KitchenSyncApp/Views/FirmwareUpdateSheet.swift b/Sources/KitchenSyncApp/Views/FirmwareUpdateSheet.swift index 39b7100..504a093 100644 --- a/Sources/KitchenSyncApp/Views/FirmwareUpdateSheet.swift +++ b/Sources/KitchenSyncApp/Views/FirmwareUpdateSheet.swift @@ -8,7 +8,7 @@ import UniformTypeIdentifiers /// the device — the firmware doesn't expose its chip target, so the app cannot check. /// A filename convention is not a safety mechanism, but an unlabelled "Flash" button /// is worse. So the confirmation names the device and requires an explicit -/// acknowledgement of the target. The proper fix is firmware-side (expose the target +/// acknowledgment of the target. The proper fix is firmware-side (expose the target /// in /status or /update) and belongs with T-011. /// /// The reassurance is real and worth stating plainly: dual-slot OTA (P4-017) means a @@ -40,7 +40,7 @@ struct FirmwareUpdateSheet: View { picker if binary != nil { - targetAcknowledgement + targetAcknowledgment } if let failure { @@ -124,7 +124,7 @@ struct FirmwareUpdateSheet: View { /// The app cannot verify the chip target, so the user must. Not a rubber stamp — /// it's the only gate that exists. - private var targetAcknowledgement: some View { + private var targetAcknowledgment: some View { HStack(alignment: .top, spacing: 10) { Toggle("", isOn: $acknowledgedTarget) .toggleStyle(KSSwitchStyle()) diff --git a/Sources/KitchenSyncApp/Views/KSControls.swift b/Sources/KitchenSyncApp/Views/KSControls.swift index 6c4f939..2ec3a2a 100644 --- a/Sources/KitchenSyncApp/Views/KSControls.swift +++ b/Sources/KitchenSyncApp/Views/KSControls.swift @@ -214,7 +214,7 @@ struct KSLiveSlider: View { debounce?.cancel() debounce = Task { try? await Task.sleep(for: .milliseconds(60)) - guard !Task.isCancelled else { return } + guard !Task.isCancelled else { return } // spelling: ok (Apple API) onCommit(Int(new.rounded())) } } @@ -234,7 +234,7 @@ struct KSLiveSlider: View { // Let the device win once it has answered — never leave a slider showing a // number the device doesn't have. .onChange(of: value) { _, new in - if debounce?.isCancelled ?? true { local = nil } + if debounce?.isCancelled ?? true { local = nil } // spelling: ok (Apple API) _ = new } .accessibilityLabel(label) diff --git a/Sources/KitchenSyncApp/Views/KSGlass.swift b/Sources/KitchenSyncApp/Views/KSGlass.swift index 52e4866..9ccd777 100644 --- a/Sources/KitchenSyncApp/Views/KSGlass.swift +++ b/Sources/KitchenSyncApp/Views/KSGlass.swift @@ -5,7 +5,7 @@ import SwiftUI /// The web UI draws a 7-segment display with an all-segments-lit "188.8" ghost /// behind the live value, imitating the unlit segments of a real LED display. /// That trick REQUIRES the DSEG7 Classic face. It isn't bundled yet, so per the -/// design direction the ghost layer is deliberately DROPPED: a grey duplicate of +/// design direction the ghost layer is deliberately DROPPED: a gray duplicate of /// a non-segmented font doesn't read as unlit segments, it reads as a rendering /// bug. Bundle DSEG7 and the ghost comes back — see `ghost` below. struct KSGlass: View { @@ -45,10 +45,10 @@ struct KSGlass: View { } } -/// The `.pwr` LED — breathes while the device is answering, goes dead grey when +/// The `.pwr` LED — breathes while the device is answering, goes dead gray when /// it isn't. The web page never needs an offline state; the app needs one /// constantly, and it's the first thing a user checks. (Proper unreachable -/// modelling — consecutive-failure thresholds — is T-010.) +/// modeling — consecutive-failure thresholds — is T-010.) struct KSPowerLED: View { let alive: Bool diff --git a/Tests/KitchenSyncAppTests/DeviceListViewModelTests.swift b/Tests/KitchenSyncAppTests/DeviceListViewModelTests.swift index 9e89fa5..4ad6ca2 100644 --- a/Tests/KitchenSyncAppTests/DeviceListViewModelTests.swift +++ b/Tests/KitchenSyncAppTests/DeviceListViewModelTests.swift @@ -113,7 +113,7 @@ final class DeviceListViewModelTests: XCTestCase { /// One dropped request on a busy LAN is NOISE, not a disconnection. At a 2s poll, /// flapping a red badge on every missed packet would be worse than the old - /// behaviour. A single miss keeps the device reachable and keeps its last status. + /// behavior. A single miss keeps the device reachable and keeps its last status. func test_a_single_missed_poll_does_not_declare_the_device_gone() async { StubURLProtocol.reset() StubURLProtocol.routes["GET /status"] = .init(body: Self.statusJSON) diff --git a/Tests/KitchenSyncAppTests/DeviceMatchTests.swift b/Tests/KitchenSyncAppTests/DeviceMatchTests.swift index eee753f..796eda9 100644 --- a/Tests/KitchenSyncAppTests/DeviceMatchTests.swift +++ b/Tests/KitchenSyncAppTests/DeviceMatchTests.swift @@ -21,7 +21,7 @@ final class DeviceMatchTests: XCTestCase { /// and X32Link use. hasPrefix is case-sensitive, so `"KitchenSync".hasPrefix( /// "kitchensync")` is false and the P4 was invisible. A Bonjour name's case is not /// meaningful; the match must be case-insensitive. - func test_the_p4_is_matched_despite_a_capitalised_instance_name() { + func test_the_p4_is_matched_despite_a_capitalized_instance_name() { XCTAssertTrue(DeviceMatch.isKitchenSync(serviceName: "KitchenSync", txt: nil)) XCTAssertTrue(DeviceMatch.isKitchenSync(serviceName: "KSTouch-DFD0", txt: nil)) } diff --git a/Tests/KitchenSyncAppTests/DiscoveryStatusTests.swift b/Tests/KitchenSyncAppTests/DiscoveryStatusTests.swift index e63322d..e378f1b 100644 --- a/Tests/KitchenSyncAppTests/DiscoveryStatusTests.swift +++ b/Tests/KitchenSyncAppTests/DiscoveryStatusTests.swift @@ -49,9 +49,9 @@ final class DiscoveryStatusTests: XCTestCase { XCTAssertEqual(DiscoveryStatus(browserState: .failed(.posix(.ENETDOWN))), .failed) } - func test_setup_and_cancelled_are_not_errors() { + func test_setup_and_canceled_are_not_errors() { XCTAssertEqual(DiscoveryStatus(browserState: .setup), .starting) - XCTAssertEqual(DiscoveryStatus(browserState: .cancelled), .stopped) + XCTAssertEqual(DiscoveryStatus(browserState: .cancelled), .stopped) // spelling: ok (NWBrowser.State, Apple API) } /// Only `permissionDenied` is the user's to fix. Everything else is ours or the diff --git a/Tests/KitchenSyncAppTests/RealTouchStatusTests.swift b/Tests/KitchenSyncAppTests/RealTouchStatusTests.swift index db114a5..9204d03 100644 --- a/Tests/KitchenSyncAppTests/RealTouchStatusTests.swift +++ b/Tests/KitchenSyncAppTests/RealTouchStatusTests.swift @@ -86,7 +86,7 @@ final class RealTouchStatusTests: XCTestCase { /// THE BUG, reduced to one assertion. /// /// `link_owns` is a LIVE claim. `ks_status.h`: "when link_owns is true the manual - /// PLAY/STOP buttons are ignored, so the UI greys them". The app obeys that contract + /// PLAY/STOP buttons are ignored, so the UI grays them". The app obeys that contract /// — `TransportAppearance.acceptsTap = !linkOwned` — and so it refused to even SEND /// the tap. /// @@ -107,7 +107,7 @@ final class RealTouchStatusTests: XCTestCase { linkOwned: s.linkOwnsTransport) XCTAssertTrue(button.acceptsTap, - "the device obeys play/stop — a UI that greys the button is lying to the user") + "the device obeys play/stop — a UI that grays the button is lying to the user") } /// ...and it must be able to stop it again. @@ -127,14 +127,14 @@ final class RealTouchStatusTests: XCTestCase { /// then `link_owns:true` is true, its manual buttons really are ignored, and greying /// them is correct: a button that does nothing is worse than a button that says so. /// The Touch's mistake was CLAIMING that while obeying anyway. - func test_a_device_that_really_does_defer_to_link_still_greys_the_button() throws { + func test_a_device_that_really_does_defer_to_link_still_grays_the_button() throws { let s = try JSONDecoder().decode(KsStatus.self, from: Self.lyingLinkOwnsJSON) let button = TransportAppearance.appearance(for: s.launch[0], linkOwned: s.linkOwnsTransport) XCTAssertFalse(button.acceptsTap, - "link_owns is a LIVE claim; honouring it is right. The fix belongs in whoever LIES.") + "link_owns is a LIVE claim; honoring it is right. The fix belongs in whoever LIES.") } /// The Touch has no Link-phase block (`xf*`), so `phaseHealth` must be nil rather diff --git a/Tests/KitchenSyncAppTests/SetupNetworkTests.swift b/Tests/KitchenSyncAppTests/SetupNetworkTests.swift index aa755e5..448007c 100644 --- a/Tests/KitchenSyncAppTests/SetupNetworkTests.swift +++ b/Tests/KitchenSyncAppTests/SetupNetworkTests.swift @@ -1,10 +1,10 @@ import XCTest @testable import KitchenSyncApp -/// T-026: a device in SoftAP setup mode isn't on the LAN, so the app recognises it by its +/// T-026: a device in SoftAP setup mode isn't on the LAN, so the app recognizes it by its /// setup SSID (to guide/join the AP) and reaches it at the fixed setup address. final class SetupNetworkTests: XCTestCase { - func test_the_fleet_setup_aps_are_recognised() { + func test_the_fleet_setup_aps_are_recognized() { XCTAssertTrue(SetupNetwork.isSetupAP(ssid: "KitchenSync-Setup")) // the P4 XCTAssertTrue(SetupNetwork.isSetupAP(ssid: "KSTouch-Config")) // the Touch } diff --git a/Tests/KitchenSyncAppTests/TransportAppearanceTests.swift b/Tests/KitchenSyncAppTests/TransportAppearanceTests.swift index 9efd70c..9ecd531 100644 --- a/Tests/KitchenSyncAppTests/TransportAppearanceTests.swift +++ b/Tests/KitchenSyncAppTests/TransportAppearanceTests.swift @@ -17,7 +17,7 @@ final class TransportAppearanceTests: XCTestCase { let a = TransportAppearance.appearance(for: .stopped, linkOwned: false) XCTAssertEqual(a.label, "STOPPED") - // Ember, not grey: grey is what DISABLED looks like, and a stopped + // Ember, not gray: gray is what DISABLED looks like, and a stopped // output is the opposite of disabled — it's loaded and ready to fire. XCTAssertEqual(a.face, .ember) XCTAssertFalse(a.blinks) @@ -27,7 +27,7 @@ final class TransportAppearanceTests: XCTestCase { /// The one that matters. Play is quantized — a tap arms the output and the /// bar line may be most of a bar away. The blink is the ONLY evidence the /// tap registered. The device's own web UI gets this wrong: its `.tgl.arming` - /// is a static amber with a 150ms colour transition and no motion at all. + /// is a static amber with a 150ms color transition and no motion at all. func test_armed_is_amber_and_blinks() { let a = TransportAppearance.appearance(for: .armed, linkOwned: false) diff --git a/Tests/KitchenSyncAppTests/UnsupportedDeviceTests.swift b/Tests/KitchenSyncAppTests/UnsupportedDeviceTests.swift index ed02e8e..4d8a7f8 100644 --- a/Tests/KitchenSyncAppTests/UnsupportedDeviceTests.swift +++ b/Tests/KitchenSyncAppTests/UnsupportedDeviceTests.swift @@ -72,7 +72,7 @@ final class UnsupportedDeviceTests: XCTestCase { XCTAssertEqual(vm.reachability(of: "kstouch-dfd0.local"), .unsupported) } - /// And the T-010 behaviour still holds: a device that genuinely stops answering IS unreachable. + /// And the T-010 behavior still holds: a device that genuinely stops answering IS unreachable. func test_a_device_that_stops_answering_is_still_unreachable() async { StubURLProtocol.routes["GET /status"] = .init(body: Self.realTouchStatusJSON) let vm = makeListVM() diff --git a/Tools/make_app_icon.swift b/Tools/make_app_icon.swift index ffa3563..484e1cb 100644 --- a/Tools/make_app_icon.swift +++ b/Tools/make_app_icon.swift @@ -110,7 +110,7 @@ ctx.setMiterLimit(10) // concentric banding, a contour map. Neither is a glow, because a glow FALLS OFF and // a stroke does not. // -// A shadow does. Zero offset + a blur radius + the LED colour is a real Gaussian +// A shadow does. Zero offset + a blur radius + the LED color is a real Gaussian // bloom around the path. Two passes, because one is too timid to see on charcoal. for (blur, alpha) in [(S * 0.075, 0.75), (S * 0.030, 0.85)] as [(Double, Double)] { ctx.saveGState() diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 52f0b0d..55e88ee 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -36,7 +36,7 @@ Models (pure) KsStatus, KsConfig, KsLiveEdit, TransportLaunchStat KsStatusTempoSource, TransportAppearance, DeviceMatch … ``` -- **Models are pure and testable.** State→appearance mapping, tempo-source labelling, +- **Models are pure and testable.** State→appearance mapping, tempo-source labeling, transport summary, and device-match logic are pure functions with unit tests — never buried in a view. - **The firmware is the source of truth for the models.** Field names, value grammars, diff --git a/docs/design/2026-07-14-view-layer-design-direction.md b/docs/design/2026-07-14-view-layer-design-direction.md index 1fe11af..7b03dcc 100644 --- a/docs/design/2026-07-14-view-layer-design-direction.md +++ b/docs/design/2026-07-14-view-layer-design-direction.md @@ -29,7 +29,7 @@ exactly the things this app exists to fix. ``` Eight colors, three faces. Near-black background, one lime LED, one amber, one warm off-white -ink, one grey. Everything else in both files is a tint of those. +ink, one gray. Everything else in both files is a tint of those. ### The signature moves, in order of how much they matter @@ -62,7 +62,7 @@ ten uses. Port it. .write:active{transform:translateY(4px);box-shadow:0 1px 0 #5e8a16} ``` -A key with 6px of travel that bottoms out when you press it. Labelled **Write & Reboot**. +A key with 6px of travel that bottoms out when you press it. Labeled **Write & Reboot**. **4. The transport toggle** (`ks_web.cpp:107-113`) — one control, three states, state *is* the label and the color: @@ -74,7 +74,7 @@ label and the color: .tgl:disabled{opacity:.45;cursor:not-allowed} ``` -**Stopped is not neutral grey — it's an ember**: dark maroon `#2A1512 → #1C0F0D` with salmon +**Stopped is not neutral gray — it's an ember**: dark maroon `#2A1512 → #1C0F0D` with salmon `#FF7A6B` text. That's a good decision. It reads "loaded, not firing," and it distinguishes stopped from disabled (which is just `opacity:.45`). @@ -148,7 +148,7 @@ All three faces are SIL OFL 1.1; bundle them and keep the identity 1:1. | Mono | DM Mono 400/500 | bundle; fallback `.system(design: .monospaced)` | every value, every field, every pill | | Segment | DSEG7 Classic | bundle — **required** for the ghost trick | the BPM readout only | -If DSEG7 can't be bundled, fall back to DM Mono heavy and **delete the ghost layer**. A grey +If DSEG7 can't be bundled, fall back to DM Mono heavy and **delete the ghost layer**. A gray duplicate of a non-segmented font doesn't read as unlit segments; it reads as a rendering bug. Two non-negotiables: **`.monospacedDigit()` on every number** (a 3-digit BPM must not reflow a @@ -191,8 +191,8 @@ in display 800 at 17pt, `.tracking(2)`, uppercase. **The three states, ported literally:** -- `.stopped` — `KS.emberFill`, 1pt `KS.line` stroke, `KS.ember` label. Keep the ember. Grey would - be the obvious choice and it's the wrong one: grey is what *disabled* looks like, and a stopped +- `.stopped` — `KS.emberFill`, 1pt `KS.line` stroke, `KS.ember` label. Keep the ember. Gray would + be the obvious choice and it's the wrong one: gray is what *disabled* looks like, and a stopped output is the opposite of disabled — it's cocked. - `.armed` — `KS.amberFill`, **1.5pt** `KS.amber` stroke, `KS.amber` label, **and it blinks.** - `.running` — `KS.ledOn`, `KS.ledEdge` stroke, `KS.onInk` label, plus @@ -552,7 +552,7 @@ press: .animation(.easeOut(duration: 0.06), value: pressed) ``` -Labelled, in the device's own words: **WRITE & REBOOT**. Behind an alert that **names the +Labeled, in the device's own words: **WRITE & REBOOT**. Behind an alert that **names the device** — `"Write & Reboot kitchensync-a4f2?"` — because you probably have three of them on the bench. @@ -609,11 +609,11 @@ genuinely dark, which is exactly what the hardware is doing), everything else di `.opacity(0.35)`, and a mono caption reads `REBOOTING…`. Polling continues. The instant `/status` answers, snap back and the beat dot starts flashing. This is precisely what the device's own page does — `send_result(..., reboot: true)` renders `ui_result_page` with poll-until-alive so the -browser isn't stranded on `/save` (`ks_web.cpp:560-563, 611`). Same behaviour, ported. +browser isn't stranded on `/save` (`ks_web.cpp:560-563, 611`). Same behavior, ported. **Firmware sheet (T-008), briefly:** same reboot vocabulary, same consequence band, plus the reassurance the task asks for — *"A failed flash does not brick the device. Dual-slot OTA means it -stays on the current firmware."* — and the target acknowledgement, since nothing in the client +stays on the current firmware."* — and the target acknowledgment, since nothing in the client validates the binary against the chip. Progress in `KS.ledOn` on `KS.panel`. `.fileImporter` limited to `.data` with a `.bin` extension check. diff --git a/docs/plans/2026-07-14-performance-settings-split-design.md b/docs/plans/2026-07-14-performance-settings-split-design.md index b3724bb..c94a38c 100644 --- a/docs/plans/2026-07-14-performance-settings-split-design.md +++ b/docs/plans/2026-07-14-performance-settings-split-design.md @@ -40,7 +40,7 @@ Applies instantly; the field pulses lime to prove it. - Per output: enable, CABLE, RATE, follow-Link - Click: volume, voice, accent — **only if a speaker is fitted** -- LED: brightness, mode, fade, colours — **only if a strip is wired** +- LED: brightness, mode, fade, colors — **only if a strip is wired** - Diagnostics (tick / phase health) - A door at the bottom → **Device Setup** @@ -64,7 +64,7 @@ this app exists to fix, and it is fixed *structurally*, not with labels: declared reboot-required. Merging everything into one Settings sheet would have re-created that adjacency with better -labelling. Better labelling is not the same as impossible. +labeling. Better labeling is not the same as impossible. ## Capability still drives visibility diff --git a/tasks/T-001.md b/tasks/T-001.md index 935c773..d92b5d2 100644 --- a/tasks/T-001.md +++ b/tasks/T-001.md @@ -47,7 +47,7 @@ Done — Xcode 26.6, Swift 6.3.3, xcodegen 2.45.4. **The scaffold compiled clean on the first real build.** Zero Swift errors, zero warnings, even under `SWIFT_STRICT_CONCURRENCY: complete` (verified applied via `-showBuildSettings`). None of -the three predicted failure modes above materialised — the Sendable diagnostics in particular +the three predicted failure modes above materialized — the Sendable diagnostics in particular never appeared. The only build failure was the expected missing `_main`. Added `Sources/KitchenSyncApp/KitchenSyncApp.swift`: `@main` + a deliberately inert `RootView` diff --git a/tasks/T-003.md b/tasks/T-003.md index 27964b7..7e78aba 100644 --- a/tasks/T-003.md +++ b/tasks/T-003.md @@ -37,7 +37,7 @@ wraps it with that output's config summary (cable, ppqn, phase, swing). ## Notes **Play is quantized; Stop is immediate.** A tap on `.stopped` arms the output, which waits for -the next bar line before it runs. That asymmetry is Ableton's own launch behaviour and it is +the next bar line before it runs. That asymmetry is Ableton's own launch behavior and it is deliberate. The blinking `.armed` state is therefore load-bearing UI, not decoration — it is the only feedback that the tap registered, and it may persist for most of a bar. diff --git a/tasks/T-007.md b/tasks/T-007.md index e3ceb04..7add965 100644 --- a/tasks/T-007.md +++ b/tasks/T-007.md @@ -39,7 +39,7 @@ device** — mid-set, that is a destructive act, and the UI has to say so. own web form, which won't echo a saved password back either. `WifiSlot` exposes only `passwordIsSet`. New passwords ride in `WifiCredentialEdit`, and `saveFormFields` omits the `wifi_pass` field entirely when the string is empty — that's `ks_config_set`'s documented no-op -for "keep current". So the empty-means-keep behaviour is already correct in the model layer; this +for "keep current". So the empty-means-keep behavior is already correct in the model layer; this task is about making sure the *user* understands it. `wifiEdits` is keyed by slot `id`, not array position. If the view filters or reorders the slots diff --git a/tasks/T-010.md b/tasks/T-010.md index 3e34997..e866d9d 100644 --- a/tasks/T-010.md +++ b/tasks/T-010.md @@ -35,7 +35,7 @@ default. Don't just surface the error — decide the threshold. At a 2s poll, one dropped request on a busy LAN is noise, not a disconnection; flapping a red badge on every missed packet is worse than the -current behaviour. Something like "3 consecutive failures" is the shape. +current behavior. Something like "3 consecutive failures" is the shape. This one is load-bearing for T-007 and T-008: both deliberately reboot the device, and neither has an honest way to show "it's coming back" until unreachability is a state the app can model. diff --git a/tasks/T-014.md b/tasks/T-014.md index 76d9543..b384eae 100644 --- a/tasks/T-014.md +++ b/tasks/T-014.md @@ -45,7 +45,7 @@ reflashed (`link-devices` ESP-031). Matching on TXT *only* would make every exis disappear — a worse bug than the one being fixed. So: TXT when present, hostname prefix otherwise, and widen the prefix set to include `kstouch-`. -The prefix fallback stays a heuristic and should be labelled as one in the code. It exists to keep +The prefix fallback stays a heuristic and should be labeled as one in the code. It exists to keep un-updated units working, not because it is correct. `NWBrowser` surfaces TXT via the result's `metadata` (`.bonjour(NWTXTRecord)`). Extract the decision diff --git a/tasks/T-016.md b/tasks/T-016.md index f07ea62..7823028 100644 --- a/tasks/T-016.md +++ b/tasks/T-016.md @@ -42,7 +42,7 @@ dead network) — this is the identical mistake, one endpoint over. - [ ] The list shows something honest for it — e.g. `UNSUPPORTED`, not `UNREACHABLE` - [ ] The detail screen explains the real cause: this device speaks a different `/status`, its firmware needs updating (`link-devices` ESP-029), and points at the OTA path -- [ ] A genuinely absent device still reads `UNREACHABLE` (T-010's behaviour is preserved) +- [ ] A genuinely absent device still reads `UNREACHABLE` (T-010's behavior is preserved) - [ ] A fixture built from the REAL Touch `/status` response is in the test suite - [ ] Written test-first per the /tdd rule diff --git a/tasks/T-018.md b/tasks/T-018.md index ebc67fc..47d396b 100644 --- a/tasks/T-018.md +++ b/tasks/T-018.md @@ -61,7 +61,7 @@ disconnection (T-010). Now: a lifetime counter is not a live fault. Every one of shape — **two different situations collapsed into one signal, and the user sent to fix the wrong thing.** -Worth generalising the rule rather than fixing it a fifth time: *before showing a warning, ask what +Worth generalizing the rule rather than fixing it a fifth time: *before showing a warning, ask what the user would DO about it. If two causes need different actions, they need different signals.* `maxStepMicros` is deliberately still un-warned — nobody has told us a threshold, and inventing one diff --git a/tasks/T-021.md b/tasks/T-021.md index 648439c..ea3f6c2 100644 --- a/tasks/T-021.md +++ b/tasks/T-021.md @@ -60,7 +60,7 @@ translations. This is the recurring bug of the codebase in structural form: the same decision, made in more than one place, drifts. Every one of T-009 / T-010 / T-016 / T-018 was "two situations collapsed into one -signal." Centralising the classification makes the fifth instance harder to write by accident. +signal." Centralizing the classification makes the fifth instance harder to write by accident. Source: architecture review 2026-07-14 (candidate 02). Pairs naturally with T-020 — both are "a decision spread across call sites, give it one home." Do T-020 first (higher leverage). diff --git a/tasks/T-024.md b/tasks/T-024.md index 43927de..5aa4863 100644 --- a/tasks/T-024.md +++ b/tasks/T-024.md @@ -55,7 +55,7 @@ out for a unit test, with one caller and an interface larger than its body, is s definition; naming that is the point of the review, even when the verdict is "leave it." No firmware/protocol risk here — `state == .stopped` is app-side transport intent, and the quantized -arm/fire behaviour is unaffected (this only decides which *request* a tap sends). +arm/fire behavior is unaffected (this only decides which *request* a tap sends). Source: architecture review 2026-07-14 (candidate 05).