diff --git a/Textream/Textream/BrowserServer.swift b/Textream/Textream/BrowserServer.swift index c58f481..0b365ed 100644 --- a/Textream/Textream/BrowserServer.swift +++ b/Textream/Textream/BrowserServer.swift @@ -330,9 +330,9 @@ class BrowserServer { -webkit-overflow-scrolling:touch;scroll-behavior:smooth} #prompter::-webkit-scrollbar{display:none} - /* Text: match ExternalDisplayView font sizing: max(48, min(96, width/14)) */ + /* Text: browser remote font sizing from BrowserFontSizePreset */ #text-container{ - font-size:clamp(48px,calc(100vw / 14),96px); + font-size:\(NotchSettings.shared.browserFontSizePreset.cssClamp); font-weight:600;line-height:1.4;word-wrap:break-word} .w{display:inline;transition:color .12s ease} .w.ann{font-style:italic} @@ -370,7 +370,7 @@ class BrowserServer { #prompter{padding:16px 5%} #bar{padding:10px 5% 20px} #waveform{width:160px;height:28px} - #text-container{font-size:clamp(28px,calc(100vw / 10),60px)} + #text-container{font-size:\(NotchSettings.shared.browserFontSizePreset.mobileCssClamp)} } diff --git a/Textream/Textream/NotchSettings.swift b/Textream/Textream/NotchSettings.swift index 1a02c9d..aaa39a7 100644 --- a/Textream/Textream/NotchSettings.swift +++ b/Textream/Textream/NotchSettings.swift @@ -165,6 +165,41 @@ enum CueBrightness: String, CaseIterable, Identifiable { } } +// MARK: - Browser Font Size Preset + +enum BrowserFontSizePreset: String, CaseIterable, Identifiable { + case sm, md, lg, xl + + var id: String { rawValue } + + var label: String { + switch self { + case .sm: return "SM" + case .md: return "MD" + case .lg: return "LG" + case .xl: return "XL" + } + } + + var cssClamp: String { + switch self { + case .sm: return "clamp(24px,calc(100vw / 22),48px)" + case .md: return "clamp(32px,calc(100vw / 18),54px)" + case .lg: return "clamp(40px,calc(100vw / 14),60px)" + case .xl: return "clamp(48px,calc(100vw / 12),64px)" + } + } + + var mobileCssClamp: String { + switch self { + case .sm: return "clamp(18px,calc(100vw / 16),36px)" + case .md: return "clamp(22px,calc(100vw / 13),42px)" + case .lg: return "clamp(28px,calc(100vw / 10),48px)" + case .xl: return "clamp(34px,calc(100vw / 8),56px)" + } + } +} + // MARK: - Overlay Mode enum OverlayMode: String, CaseIterable, Identifiable { @@ -419,6 +454,10 @@ class NotchSettings { didSet { UserDefaults.standard.set(Int(fullscreenScreenID), forKey: "fullscreenScreenID") } } + var fullscreenTopAnchor: Bool { + didSet { UserDefaults.standard.set(fullscreenTopAnchor, forKey: "fullscreenTopAnchor") } + } + var browserServerEnabled: Bool { didSet { UserDefaults.standard.set(browserServerEnabled, forKey: "browserServerEnabled") @@ -430,6 +469,10 @@ class NotchSettings { didSet { UserDefaults.standard.set(Int(browserServerPort), forKey: "browserServerPort") } } + var browserFontSizePreset: BrowserFontSizePreset { + didSet { UserDefaults.standard.set(browserFontSizePreset.rawValue, forKey: "browserFontSizePreset") } + } + var directorModeEnabled: Bool { didSet { UserDefaults.standard.set(directorModeEnabled, forKey: "directorModeEnabled") @@ -488,9 +531,11 @@ class NotchSettings { self.autoNextPageDelay = savedDelay > 0 ? savedDelay : 3 let savedFullscreenScreenID = UserDefaults.standard.integer(forKey: "fullscreenScreenID") self.fullscreenScreenID = UInt32(savedFullscreenScreenID) + self.fullscreenTopAnchor = UserDefaults.standard.object(forKey: "fullscreenTopAnchor") as? Bool ?? false self.browserServerEnabled = UserDefaults.standard.object(forKey: "browserServerEnabled") as? Bool ?? false let savedPort = UserDefaults.standard.integer(forKey: "browserServerPort") self.browserServerPort = savedPort > 0 ? UInt16(savedPort) : 7373 + self.browserFontSizePreset = BrowserFontSizePreset(rawValue: UserDefaults.standard.string(forKey: "browserFontSizePreset") ?? "") ?? .lg self.directorModeEnabled = UserDefaults.standard.object(forKey: "directorModeEnabled") as? Bool ?? false let savedDirectorPort = UserDefaults.standard.integer(forKey: "directorServerPort") self.directorServerPort = savedDirectorPort > 0 ? UInt16(savedDirectorPort) : 7575 diff --git a/Textream/Textream/SettingsView.swift b/Textream/Textream/SettingsView.swift index 581526b..781b926 100644 --- a/Textream/Textream/SettingsView.swift +++ b/Textream/Textream/SettingsView.swift @@ -867,6 +867,17 @@ struct SettingsView: View { RoundedRectangle(cornerRadius: 8) .fill(Color.primary.opacity(0.04)) ) + + Toggle(isOn: $settings.fullscreenTopAnchor) { + VStack(alignment: .leading, spacing: 2) { + Text("Lock Text to Top") + .font(.system(size: 13, weight: .medium)) + Text("Anchor the current line near the top of the screen instead of the center.") + .font(.system(size: 11)) + .foregroundStyle(.secondary) + } + } + .toggleStyle(.checkbox) } Divider() @@ -983,6 +994,17 @@ struct SettingsView: View { onRefresh: { refreshScreens() }, emptyMessage: "No external displays detected. Connect a display or enable Sidecar." ) + + Toggle(isOn: $settings.fullscreenTopAnchor) { + VStack(alignment: .leading, spacing: 2) { + Text("Lock Text to Top") + .font(.system(size: 13, weight: .medium)) + Text("Anchor the current line near the top of the screen instead of the center.") + .font(.system(size: 11)) + .foregroundStyle(.secondary) + } + } + .toggleStyle(.checkbox) } Spacer() } @@ -1050,6 +1072,18 @@ struct SettingsView: View { .fill(Color.accentColor.opacity(0.08)) ) + VStack(alignment: .leading, spacing: 6) { + Text("Remote Text Size") + .font(.system(size: 13, weight: .medium)) + Picker("", selection: $settings.browserFontSizePreset) { + ForEach(BrowserFontSizePreset.allCases) { preset in + Text(preset.label).tag(preset) + } + } + .pickerStyle(.segmented) + .labelsHidden() + } + DisclosureGroup("Advanced", isExpanded: $showAdvanced) { VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 6) { @@ -1330,6 +1364,7 @@ struct SettingsView: View { settings.glassOpacity = 0.15 settings.followCursorWhenUndocked = false settings.fullscreenScreenID = 0 + settings.fullscreenTopAnchor = false settings.externalDisplayMode = .off settings.externalScreenID = 0 settings.mirrorAxis = .horizontal @@ -1341,6 +1376,7 @@ struct SettingsView: View { settings.autoNextPageDelay = 3 settings.browserServerEnabled = false settings.browserServerPort = 7373 + settings.browserFontSizePreset = .lg settings.directorModeEnabled = false settings.directorServerPort = 7575 } diff --git a/docs/superpowers/plans/2026-03-22-browser-remote-text-size.md b/docs/superpowers/plans/2026-03-22-browser-remote-text-size.md new file mode 100644 index 0000000..d636069 --- /dev/null +++ b/docs/superpowers/plans/2026-03-22-browser-remote-text-size.md @@ -0,0 +1,212 @@ +# Browser Remote Text Size Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a four-preset text size picker (SM/MD/LG/XL) for the browser remote viewer, controlled from the Mac app's Settings > Remote tab. + +**Architecture:** New `BrowserFontSizePreset` enum in `NotchSettings.swift` with CSS clamp formulas as computed properties. Segmented picker in `SettingsView.swift` Remote tab. `BrowserServer.swift` reads the setting and injects the CSS values into the served HTML. + +**Tech Stack:** Swift, SwiftUI, AppKit (NSPanel), embedded HTML/CSS/JS + +**Spec:** `docs/superpowers/specs/2026-03-22-browser-remote-text-size-design.md` + +--- + +## File Map + +| File | Action | Responsibility | +|------|--------|---------------| +| `Textream/Textream/NotchSettings.swift` | Modify | Add `BrowserFontSizePreset` enum and `browserFontSizePreset` property | +| `Textream/Textream/SettingsView.swift` | Modify | Add segmented picker to Remote tab | +| `Textream/Textream/BrowserServer.swift` | Modify | Replace hardcoded font-size CSS with dynamic values from setting | + +--- + +### Task 1: Add `BrowserFontSizePreset` enum to `NotchSettings.swift` + +**Files:** +- Modify: `Textream/Textream/NotchSettings.swift:166` (insert new enum after `CueBrightness` closing brace at line 166, before `// MARK: - Overlay Mode` at line 168) + +- [ ] **Step 1: Add the enum definition** + +Insert after the closing `}` of `CueBrightness` at line 166, before `// MARK: - Overlay Mode` at line 168: + +```swift +// MARK: - Browser Font Size Preset + +enum BrowserFontSizePreset: String, CaseIterable, Identifiable { + case sm, md, lg, xl + + var id: String { rawValue } + + var label: String { + switch self { + case .sm: return "SM" + case .md: return "MD" + case .lg: return "LG" + case .xl: return "XL" + } + } + + var cssClamp: String { + switch self { + case .sm: return "clamp(24px,calc(100vw / 22),48px)" + case .md: return "clamp(32px,calc(100vw / 18),54px)" + case .lg: return "clamp(40px,calc(100vw / 14),60px)" + case .xl: return "clamp(48px,calc(100vw / 12),64px)" + } + } + + var mobileCssClamp: String { + switch self { + case .sm: return "clamp(18px,calc(100vw / 16),36px)" + case .md: return "clamp(22px,calc(100vw / 13),42px)" + case .lg: return "clamp(28px,calc(100vw / 10),48px)" + case .xl: return "clamp(34px,calc(100vw / 8),56px)" + } + } +} +``` + +- [ ] **Step 2: Add persisted property to `NotchSettings`** + +Add the property alongside the other browser settings (after `browserServerPort` around line 433-435): + +```swift +var browserFontSizePreset: BrowserFontSizePreset { + didSet { UserDefaults.standard.set(browserFontSizePreset.rawValue, forKey: "browserFontSizePreset") } +} +``` + +- [ ] **Step 3: Add initialization in `init()`** + +Add after line 498 (`self.browserServerPort = ...`) and before line 499 (`self.directorModeEnabled = ...`): + +```swift +self.browserFontSizePreset = BrowserFontSizePreset(rawValue: UserDefaults.standard.string(forKey: "browserFontSizePreset") ?? "") ?? .lg +``` + +- [ ] **Step 4: Build to verify no compile errors** + +Run: `xcodebuild -project Textream/Textream.xcodeproj -scheme Textream -configuration Debug build 2>&1 | tail -5` +Expected: `** BUILD SUCCEEDED **` + +- [ ] **Step 5: Commit** + +```bash +git add Textream/Textream/NotchSettings.swift +git commit -m "feat: add BrowserFontSizePreset enum and setting" +``` + +--- + +### Task 2: Add segmented picker to Settings > Remote tab + +**Files:** +- Modify: `Textream/Textream/SettingsView.swift:1073` (insert after the URL/copy-button row's closing background modifier, before the `DisclosureGroup("Advanced"...`) + +- [ ] **Step 1: Add the picker UI** + +Insert after the URL row block (after the `.background(RoundedRectangle...)` closing paren around line 1073) and before `DisclosureGroup("Advanced"` at line 1075: + +```swift +VStack(alignment: .leading, spacing: 6) { + Text("Remote Text Size") + .font(.system(size: 13, weight: .medium)) + Picker("", selection: $settings.browserFontSizePreset) { + ForEach(BrowserFontSizePreset.allCases) { preset in + Text(preset.label).tag(preset) + } + } + .pickerStyle(.segmented) + .labelsHidden() +} +``` + +- [ ] **Step 2: Build to verify no compile errors** + +Run: `xcodebuild -project Textream/Textream.xcodeproj -scheme Textream -configuration Debug build 2>&1 | tail -5` +Expected: `** BUILD SUCCEEDED **` + +- [ ] **Step 3: Commit** + +```bash +git add Textream/Textream/SettingsView.swift +git commit -m "feat: add remote text size picker to Settings Remote tab" +``` + +--- + +### Task 3: Wire up dynamic CSS in `BrowserServer.swift` + +**Files:** +- Modify: `Textream/Textream/BrowserServer.swift:333-335` (desktop font-size rule) +- Modify: `Textream/Textream/BrowserServer.swift:369-374` (mobile breakpoint) + +- [ ] **Step 1: Replace desktop font-size CSS** + +Find (around lines 333-335): +``` + /* Text: match ExternalDisplayView font sizing: max(48, min(96, width/14)) */ + #text-container{ + font-size:clamp(48px,calc(100vw / 14),96px); +``` + +Replace with: +``` + /* Text: browser remote font sizing from BrowserFontSizePreset */ + #text-container{ + font-size:\(NotchSettings.shared.browserFontSizePreset.cssClamp); +``` + +- [ ] **Step 2: Replace mobile breakpoint font-size CSS** + +Find (around line 373): +``` + #text-container{font-size:clamp(28px,calc(100vw / 10),60px)} +``` + +Replace with: +``` + #text-container{font-size:\(NotchSettings.shared.browserFontSizePreset.mobileCssClamp)} +``` + +- [ ] **Step 3: Verify the string interpolation context** + +Check that the HTML string containing these lines is already using Swift string interpolation (i.e., uses `\(...)` elsewhere). The BrowserServer HTML is built with string interpolation for colors and other dynamic values, so this pattern is consistent. + +- [ ] **Step 4: Build to verify no compile errors** + +Run: `xcodebuild -project Textream/Textream.xcodeproj -scheme Textream -configuration Debug build 2>&1 | tail -5` +Expected: `** BUILD SUCCEEDED **` + +- [ ] **Step 5: Commit** + +```bash +git add Textream/Textream/BrowserServer.swift +git commit -m "feat: use dynamic font size in browser remote HTML" +``` + +--- + +### Task 4: Manual verification + +- [ ] **Step 1: Launch the app** + +Run the app from Xcode (Cmd+R). + +- [ ] **Step 2: Verify Settings UI** + +Open Settings > Remote tab. Enable Remote Connection. Confirm the "Remote Text Size" segmented picker appears with SM / MD / LG / XL options below the URL row. Default should be LG (selected). + +- [ ] **Step 3: Verify browser remote renders correctly** + +Open the browser remote URL. Confirm text appears at the expected size. Change the preset in Settings, refresh the browser page, and confirm the text size changes. + +- [ ] **Step 4: Verify each preset** + +Cycle through all four presets (SM, MD, LG, XL), refreshing the browser each time. Confirm text scales from smallest (SM) to largest (XL). + +- [ ] **Step 5: Verify mobile breakpoint** + +Open browser dev tools, toggle responsive mode to a width under 768px. Confirm the mobile font sizes apply correctly for each preset. diff --git a/docs/superpowers/specs/2026-03-22-browser-remote-text-size-design.md b/docs/superpowers/specs/2026-03-22-browser-remote-text-size-design.md new file mode 100644 index 0000000..e27a242 --- /dev/null +++ b/docs/superpowers/specs/2026-03-22-browser-remote-text-size-design.md @@ -0,0 +1,67 @@ +# Browser Remote Text Size Setting + +## Summary + +Add a user-configurable text size setting for the browser remote viewer. Currently the browser remote hardcodes font size via `clamp(48px, calc(100vw/14), 96px)`. This feature adds a four-preset picker (SM/MD/LG/XL) in the Mac app's Settings > Remote tab, allowing the presenter to control how large text appears on the remote viewing device. + +## Motivation + +Different viewing distances and screen sizes benefit from different text sizes. A phone held in hand needs smaller text than a TV across the room. The current one-size-fits-all formula doesn't accommodate this. + +## Design + +### Data Model (`NotchSettings.swift`) + +Add a new enum `BrowserFontSizePreset: String, CaseIterable, Identifiable` (matching existing enum patterns) with four cases: + +| Case | Label | Desktop CSS `clamp()` | Mobile (`<=768px`) CSS `clamp()` | +|------|-------|----------------------|----------------------------------| +| `.sm` | SM | `clamp(24px, calc(100vw/22), 48px)` | `clamp(18px, calc(100vw/16), 36px)` | +| `.md` | MD | `clamp(32px, calc(100vw/18), 54px)` | `clamp(22px, calc(100vw/13), 42px)` | +| `.lg` | LG | `clamp(40px, calc(100vw/14), 60px)` | `clamp(28px, calc(100vw/10), 48px)` | +| `.xl` | XL | `clamp(48px, calc(100vw/12), 64px)` | `clamp(34px, calc(100vw/8), 56px)` | + +Add computed properties `cssClamp` and `mobileCssClamp` on the enum returning the formula strings (following the pattern of `FontColorPreset.cssColor`). + +Add a new persisted property on `NotchSettings`: + +```swift +var browserFontSizePreset: BrowserFontSizePreset // default: .lg +``` + +Default is `.lg` because the current hardcoded formula `clamp(48px, calc(100vw/14), 96px)` most closely matches the `.lg` preset's divisor (`100vw/14`), preserving a similar experience for existing users. The max value will be lower (60px vs 96px) per the user's requested size range (24-64px). + +Persisted to UserDefaults with key `"browserFontSizePreset"`, using the standard `didSet`/`init` pattern used by all other settings properties. + +### Settings UI (`SettingsView.swift`) + +- Add a segmented picker labeled "Remote Text Size" with options SM / MD / LG / XL +- Placement: in the Remote tab (`browserTab`), below the URL/copy-button row (after line 1073), above the `DisclosureGroup("Advanced", ...)` (line 1075) +- Only visible when `browserServerEnabled` is true +- Styled consistently with existing pickers in the app + +### Browser Remote (`BrowserServer.swift`) + +- Read `NotchSettings.shared.browserFontSizePreset` when generating the HTML page +- Use the enum's `cssClamp` property for the `#text-container` font-size rule (replacing current `clamp(48px,calc(100vw / 14),96px)`) +- Use the enum's `mobileCssClamp` property for the `@media(max-width:768px)` breakpoint rule (replacing current `clamp(28px,calc(100vw / 10),60px)`) +- Update the CSS comment on line 333 to reflect the dynamic sizing + +### Update Propagation + +The HTML is generated server-side. A change to the text size preset takes effect on the next page load or WebSocket reconnect from the browser viewer. No live push mechanism is needed for this setting. + +## Scope + +### In scope +- New `BrowserFontSizePreset` enum with `cssClamp`/`mobileCssClamp` computed properties +- New `browserFontSizePreset` property on `NotchSettings` with UserDefaults persistence +- Segmented picker in Settings > Remote tab +- CSS injection in `BrowserServer.swift` HTML generation +- Update stale CSS comment + +### Out of scope +- Text size control on the browser remote page itself (viewer-side control) +- Text size for Director mode +- Text size for the external display (NSPanel) +- Live-push of size changes without page reload