Native gate checks the app's own glow; docs for glass and the glow
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed,
+43
−3
native/DESIGN.md+32 −0native/README.md+6 −3native/apple/Sources/CallsKit/Call.swift+3 −0test/browser/native.mjs+2 −0
diff --git a/native/DESIGN.md b/native/DESIGN.md| index 1dab73a..4fa47b8 100644 |
| --- a/native/DESIGN.md |
| +++ b/native/DESIGN.md |
| @@ -156,3 +156,35 @@ Snapshots: `CallsInterop --call-snapshots <dir>` (also part of |
| iPhone's come from `CallsPhoneUITests.testFullScreenCallSnapshots` against the |
| debug build's `-CallsCallFixture` calls (`CallFixture`), written to |
| `CALLS_SNAPSHOT_DIR` or `/tmp/immersive-call`. |
| + |
| +## Glass and the speaking glow · 2026-09-25 (0.3.7) |
| + |
| +Supersedes the hairline edges and button tiles of 0.3.5's **Over the picture**. |
| + |
| +- **One glass language, no hairlines.** Every surface over the picture — the |
| + bar, the title pill, notices, name labels, the iPhone's round buttons and the |
| + mini window's tab — is borderless glass: Liquid Glass on macOS 26 and iOS 26, |
| + the ultra-thin material before, with black at 62 % on it |
| + (`CallOverlay.scrim`). Rendered over white the glass measures #5B5B5B, so |
| + white on it is 6.8:1 (6.2:1 from the black alone). The environment value |
| + `callOverlayGlass` turns the glass off to show the fallback. |
| +- **Buttons are glyphs on the pane**: white, a light wash under the pointer, |
| + and a capsule only for a state — near-white for on (sharing, a panel open), |
| + amber with the slashed symbol for microphone or camera off, red for Leave. |
| +- **Devices** over the picture is opaque and dark (`CallOverlay.panelFill`) |
| + with the dark appearance's labels: 16.7:1 primary and 5.1:1 secondary as |
| + rendered. It holds the controls up while open and never fades with them. |
| +- **The speaking glow** replaces the white ring round a tile and the meter |
| + beside the name, on Mac, iPhone, web and the mini window's tab: the accent's |
| + hue lit up (`CallsAppearance.glow`) as a halo behind the name label, bright |
| + as the voice (`SpeakingGlow`, and `judgeGlow`/`glowStyle` in room.js). The |
| + halo redraws on a timeline only until it arrives; on the web it is a |
| + registered `--glow` transitioned by CSS. "No sound from …" and the microphone |
| + warnings stay as words by the name; the Devices panel keeps a compact meter. |
| + |
| +Snapshots: `CallsInterop --glass-snapshots <dir>` renders real windows below |
| +the desktop and captures them with `screencapture -l` (an off-screen bitmap |
| +draws no Liquid Glass); the iPhone's are |
| +`CallsPhoneUITests.testGlowAndGlassSnapshots` (`-CallsFixtureSpeaking`, |
| +`-CallsFixtureNoGlass`). |
| + |
diff --git a/native/README.md b/native/README.md| index 7d8233e..805f593 100644 |
| --- a/native/README.md |
| +++ b/native/README.md |
| @@ -107,11 +107,14 @@ while sharing; the panel itself shows one warning line only when something is |
| wrong (a stretched or broken-up signal, echo cancellation off, noise reduction |
| bypassed, processing refused — `AudioWarning`). |
| -**Sound is measured, not inferred** (spec §15.20). On 2026-09-25 Tony, in a |
| +**Sound is measured, not inferred** (spec §15.20). Since 0.3.7 a tile shows it |
| +as the speaking glow behind the name (`SpeakingGlow`: -42 to -12 dBFS over a |
| +0.3 floor, lit once speech has kept up 0.15 s, 50 ms attack, 300 ms release, |
| +a steady 0.6 under Reduce Motion) in place of the white ring and the meter. On 2026-09-25 Tony, in a |
| browser, could not hear Paul on the Mac app, and Paul's panel looked fine |
| because nothing on it measured sound. Now: |
| -- **Your tile** and the panel's **Microphone level** meter what is sent — the |
| +- **Your tile's glow** and the panel's **Microphone level** meter what is sent — the |
| capture hook's level after noise reduction (`CaptureLevel`, read at 10 Hz). |
| Unmuted and capturing, with nothing reaching the hook above -90 dBFS |
| *before* the suppressor for five seconds, the tile says "Your microphone |
| @@ -120,7 +123,7 @@ because nothing on it measured sound. Now: |
| - **Everyone else's tile** meters what arrives from them: a renderer on their |
| remote audio track (`RemoteAudioTap`), which sees each decoded frame before |
| the output volume, read at 5 Hz. Muted, the tile shows the Muted badge and no |
| - meter. Reporting themselves unmuted, connected, with audio bytes still rising |
| + glow. Reporting themselves unmuted, connected, with audio bytes still rising |
| (inbound-rtp, one `statistics(for:)` per receiver a second) and the decoded |
| peak under -85 dBFS for five seconds, it says "No sound from Bea" instead, |
| and the app reports those people as `noSoundFrom` through the bridge. A thin |
diff --git a/native/apple/Sources/CallsKit/Call.swift b/native/apple/Sources/CallsKit/Call.swift| index c02bd4b..9f959a9 100644 |
| --- a/native/apple/Sources/CallsKit/Call.swift |
| +++ b/native/apple/Sources/CallsKit/Call.swift |
| @@ -769,6 +769,9 @@ private final class Events: NSObject, BridgeListenerProtocol { |
| micPeakSinceSample = 0 |
| values["noSoundFrom"] = UInt64(levels.noSoundFrom.count) |
| values["micSilent"] = levels.mic.silent ? 1 : 0 |
| + // The speaking glow behind your own name (0.3.7): lit while what is |
| + // sent is speech that has kept up. |
| + values["micGlow"] = levels.mic.glow.glow.lit ? 1 : 0 |
| return values |
| } |
| } |
diff --git a/test/browser/native.mjs b/test/browser/native.mjs| index abd8d74..d98bd65 100644 |
| --- a/test/browser/native.mjs |
| +++ b/test/browser/native.mjs |
| @@ -143,6 +143,8 @@ try { |
| await wait(() => sample?.stats.micPeakPermille > 10, "the app's own meter sees what it sends"); |
| assert.equal(sample.stats.micSilent, 0, "a live synthetic microphone is not reported as picking nothing up"); |
| console.log(`PASS the app's microphone meter reads what it sends, with no silence warning (noSoundFrom ${sample.stats.noSoundFrom})`); |
| + await wait(() => sample?.stats.micGlow === 1, "the app's own name glows while its synthetic voice speaks"); |
| + console.log('PASS the speaking glow behind the app\'s own name lights for its voice'); |
| } |
| // Choose the system default on the audio device module during the live |
| // call, then refuse an unavailable microphone. Both must leave media and |