oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1# Calls native design
2
3The first Oficina native design pass uses Calls' existing teal identity,
4quiet surfaces, clear type hierarchy and native controls. Apple's communication
5and productivity apps are references for interaction quality; Oficina retains
6its own artwork, wording and colour. Mac and phone share the joining ceremony
7while their call layouts suit the available space.
8
9## First pass
10
11- `CallsLobby` renders welcome, browser approval, ready-to-join and error states
12 on Mac, iPhone and iPad. The confirmation code and browser approval remain
13 part of the existing authentication flow. Signed out, the welcome is the
14 server's name, one prominent **Sign in** (the suite's label, exactly) and a
15 secondary **Join a call as a guest**; there is no address field until the
16 person is signed in. A link waiting on a sign-in names the server it is on.
17- `GuestJoinView` (a sheet on both platforms) is one invite-link field with a
18 Paste control, inline plain-words validation, Join and Cancel.
19- `ServerSettingsSection` is the **Calls server** setting: a section in the Mac's
20 Settings scene after the mini-window settings, and the whole of the iPhone's
21 settings sheet, reached by the home screen's gear.
22- `MacCallView` defaults to a remote participant filling the stage with a local
23 thumbnail. Speaker/Grid switches layouts (Focus until 0.4.0; see below); a
24 pin menu chooses what is on the stage in group calls. Video crops to fill while preserving its aspect ratio. Chat
25 starts hidden and can open as a sidebar or a separate window; its draft survives
26 moving and closing. The compact, coloured icon controls remain below the stage. On iPhone, chat remains a sheet and controls remain
27 in the bottom safe area.
28- Video names sit on an opaque dark label over the image. Microphone and camera
29 actions use both text and changing symbols, so colour is never the only cue.
30- `CallsAppearance` and `CallControl` live in CallsKit for both Apple clients.
31 These are Calls presentation choices. General native infrastructure belongs
32 in Rastrillo Native; Oficina's shared visual components belong in Estilo as
33 subsequent apps establish what they need in common.
34
35## Visual review
36
37After `swift build --package-path native/apple`, render the shipping lobby:
38
39```sh
40native/apple/.build/debug/CallsInterop --lobby-snapshots native/build/polish
41```
42
43This generates light and dark welcome, approval, home, ready, error, expired,
44link-on-another-server and visiting fixtures, the signed-out home and the
45settings sheet at iPhone width (`phone-*`), the guest screen empty and with an
46invalid link (`guest-*`), and the Mac Settings window (`settings-*`), without a
47server, real account or device permissions. Models read the server from a
48throwaway defaults suite. The off-screen window is never key, so prominent
49buttons draw untinted in these fixtures. The fixture entry point
50is only in CallsInterop, never the application.
51
52`TestNativeMacChat` also captures `native/build/call-focus.png`, `call-chat.png` and `call-grid.png` using the
53shipping Mac call view and messages received from a real browser participant.
54The off-screen bitmap capture does not capture Metal video frames; media
55continuity is verified separately by the existing decoded-frame/audio counters.
56
57The first pass was checked with Mac and iOS Simulator builds, the iPhone
58invalid-link UI test, and native/browser authentication, media, chat and sharing
59checks. Lobby review includes light/dark Mac states and iPhone at standard and
60accessibility text sizes. Physical-device call controls, VoiceOver, camera and
61microphone behaviour still need hands-on acceptance.
62
63## Next iterations
64
65Camera preview and device selection before joining, remote camera-off signalling,
66automatic speaker selection, and keyboard commands are follow-up product work.
67Those should be designed alongside the relevant media and lifecycle behaviour.
68
69Run `swift test --package-path native/apple` for chat-window lifecycle and draft
70retention checks. Window closure never invokes the meeting leave action.
71
72## iPhone and iPad call treatment · 2026-09-12
73
74The iOS call stage now defaults to remote focus with a local thumbnail, offers
75Focus/Grid and participant selection, and uses the same compact colored icon
76control vocabulary. Chat is a native sheet with unread count and a retained
77draft. iOS continues to end calls when backgrounded, except while sharing the screen;
78general background-call continuity remains follow-up media work.
79
80Safari member/guest sign-in and the real iPhone simulator native-media tests
81passed after this change. The signed development app was built and installed on
82Paul's paired iPhone. That installation does not substitute for a physical
83camera/microphone and interruption acceptance call.
84
85## Full-window call · 2026-09-25 (0.3.4)
86
87The call fills the window (Mac) or the screen (iPhone), with the controls over
88it on demand. **Call layout: Full window (new) / Classic** — Calls → Settings…
89on Mac, the gear's sheet on iPhone — keeps the previous layout one choice away;
90Full window is the default. The mini window and tab are unchanged.
91
92- **When the controls show** is `ControlsVisibility` (CallChrome.swift), one
93 state machine for both apps with the time passed in, so tests drive it. Mac:
94 pointer movement shows them, 3 s still hides them (`pointerDelay`; 1.5 s in
95 0.3.4 was too quick), leaving the window hides them 1.5 s later
96 (`pointerExitDelay`, never later than they were due anyway), and the pointer
97 hides with them over the picture (`NSCursor.setHiddenUntilMouseMoves`).
98 iPhone: a tap toggles, 3 s after the last touch hides (`touchDelay`). They stay up while the pointer is on the
99 bar, a menu, popover, sheet or panel is open (AppKit's menu-tracking and
100 window-sheet notifications cover the ones SwiftUI does not report), a bar
101 control has keyboard focus, VoiceOver is running, or an alert shows. Tab
102 reveals them. Space and ⌘⇧M work with them hidden. Fades are opacity only,
103 eased out: 0.35 s going, 0.2 s coming back; Reduce Motion shortens them to a
104 0.1 s opacity change and panels fade instead of sliding.
105- **The stage** is edge to edge: square tiles for Focus and a screen, a grid
106 that fits every tile without scrolling until they would be under 200 pt
107 (`GridFit`). On Mac there is no title bar while the call is full window
108 (0.3.5): the picture goes to the window's top edge, the traffic lights sit
109 over it and fade with the controls (not in full screen, where the system
110 shows them), a shade behind them keeps them legible, and a 32 pt strip
111 across the top drags the window and double-clicks as the person's title-bar
112 setting says (`WindowDragStrip`, `TitleBarDoubleClick`). Since 0.3.7 the
113 picture does too, as in FaceTime (`WindowMove`): a drag anywhere on the
114 stage that is not a control, a panel or the self view moves the window
115 once it has gone 4 pt (less is a click, which shows the controls), and a
116 double click there does what the title bar's would. Resizing is AppKit's
117 own, from every edge and corner, the top included. The title pill sits
118 to the right of the traffic lights and the notices in a column beneath it.
119 `CallWindowChrome` sets the window's title bar, watches it so SwiftUI cannot
120 quietly restore it mid-call, and puts it back for the lobby and Classic.
121 Names are small capsules with the muted mark (shape and word) inside, lifted
122 clear of the bar while it shows, visible while it does not.
123- **The self view** floats in Focus, draggable, settling in the corner it was
124 flicked towards (`PiPLayout`), kept clear of the bar, an open chat and the
125 camera strip beside a shared screen, and remembered. It always says Muted
126 when you are. VoiceOver has "Move self view to …" actions. In Grid you are a
127 tile, as before.
128- **Title and notices.** Title and status go with the controls. Reconnecting,
129 waiting to be let in, full, ended (with Join again), problems (dismissible),
130 "You are sharing your screen" and each person waiting with **Let in** stay
131 until resolved (`CallNotices`); the relay-floor line fades.
132- **Over the picture** (0.3.5): one dark system, whatever the system
133 appearance, because what is underneath is video (`CallOverlay`,
134 `CallOverlaySurface`, `CallOverlayButtonStyle`). Every surface — the bar,
135 the title pill, notices, name labels, the iPhone's round buttons — is black
136 at 70 % over a dark blur with a hairline edge; the black alone holds the
137 contrast, so white on it is at least 8.5:1 even over a white wall. Buttons
138 are white symbols on a light fill (5.5:1 worst case); a control that is on
139 (sharing, a panel open) is near-white with a dark symbol; microphone or
140 camera off is amber with the slashed symbol; Leave is a deeper red than the
141 system's (5.0:1 for its symbol). `CallOverlayContrastTests` holds the ratios
142 over white, black and grey. Classic keeps its own look.
143- **Mac bar**: a dark bar along the bottom — microphone, camera, share,
144 devices, layout (Focus/Grid and who is on the stage, in a popover), share link
145 — with chat and Leave on the right. Devices is a floating panel over the
146 stage above the bar, not a column that shrinks it.
147- **Mac chat** is an overlay sidebar sliding over the stage's trailing edge,
148 with the existing pop-out to a separate window kept. Chosen over the window
149 alone because the window is window management mid-call on a laptop screen,
150 while the sidebar is one click and leaves the picture its full size underneath;
151 anyone who wants it beside the call still has the window.
152- **iPhone bar**: a kebab (More controls) on the left and End call on the right.
153 The kebab opens a sheet: mute, camera, flip camera, share screen (the system
154 broadcast picker, which is why it is a sheet and not a menu), devices, Mic
155 Mode, layout, share link, chat and Reduce background noise. The status bar and
156 home indicator hide with the controls. `-CallsMicBesideMenu YES` puts a
157 microphone button beside the kebab, off by default.
158
159Snapshots: `CallsInterop --call-snapshots <dir>` (also part of
160`--lobby-snapshots`) renders the Mac call, both layouts, light and dark. The
161iPhone's come from `CallsPhoneUITests.testFullScreenCallSnapshots` against the
162debug build's `-CallsCallFixture` calls (`CallFixture`), written to
163`CALLS_SNAPSHOT_DIR` or `/tmp/immersive-call`.
164
165## Glass and the speaking glow · 2026-09-25 (0.3.7)
166
167Supersedes the hairline edges and button tiles of 0.3.5's **Over the picture**.
168
169- **One glass language, no hairlines.** Every surface over the picture — the
170 bar, the title pill, notices, name labels, the iPhone's round buttons and the
171 mini window's tab — is borderless glass: Liquid Glass on macOS 26 and iOS 26,
172 the ultra-thin material before, with black at 62 % on it
173 (`CallOverlay.scrim`). Rendered over white the glass measures #5B5B5B, so
174 white on it is 6.8:1 (6.2:1 from the black alone). The environment value
175 `callOverlayGlass` turns the glass off to show the fallback.
176- **Buttons are glyphs on the pane**: white, a light wash under the pointer,
177 and a capsule only for a state — near-white for on (sharing, a panel open),
178 amber with the slashed symbol for microphone or camera off, red for Leave.
179- **Devices** over the picture is opaque and dark (`CallOverlay.panelFill`)
180 with the dark appearance's labels: 16.7:1 primary and 5.1:1 secondary as
181 rendered. It holds the controls up while open and never fades with them.
182- **The speaking glow** replaces the white ring round a tile and the meter
183 beside the name, on Mac, iPhone, web and the mini window's tab: the accent's
184 hue lit up as a halo behind the name label, bright as the voice
185 (`SpeakingGlow`, and `judgeGlow`/`glowStyle` in room.js). The halo redraws
186 on a timeline only until it arrives. "No sound from …" and the microphone
187 warnings stay as words by the name; the Devices panel keeps a compact meter.
188 *The teal halo is superseded by the warm rim (0.4.3, below).*
189
190Snapshots: `CallsInterop --glass-snapshots <dir>` renders real windows below
191the desktop and captures them with `screencapture -l` (an off-screen bitmap
192draws no Liquid Glass); the iPhone's are
193`CallsPhoneUITests.testGlowAndGlassSnapshots` (`-CallsFixtureSpeaking`,
194`-CallsFixtureNoGlass`).
195
196
197## The warm rim · 2026-09-28 (0.4.3)
198
199Supersedes the teal halo of 0.3.7's **speaking glow**. After three rounds of
200HTML mock-ups (`speaking-indicator-options/`, served at
201calls-led-options.bds.oncarlos.com) Paul chose **T5, "Warm soft breath"**, and
202it is drawn exactly as that mock-up's box-shadow recipe, on Mac, iPhone, web
203and the mini window's tab (`WarmRim`, `TileSpeakingName`; `glowStyle` and
204`.calls-tile-name::after` on the web).
205
206Over the name's own glass and in its shape, where `on` is 0 or 1 and `lvl`
207the smoothed level 0…1 (-42 to -12 dBFS, no floor any more):
208
209| Layer | Drawn | Colour | Opacity |
210| --- | --- | --- | --- |
211| Rim | 1 pt inside the edge | hsl(38°, 80 %, 82 %) | on × (0.10 + lvl × 0.45) |
212| Top highlight | none in T5 | — | 0 |
213| Inner light | inset blur 3 + lvl × 6 pt | hsl(38°, 80 %, 72 %) | on × (0.06 + lvl × 0.30) |
214| Outer breath | blur 1 + lvl × 7 pt, outside only | hsl(38°, 80 %, 66 %) | on × lvl × 0.25 |
215
216Blurs are CSS box-shadow radii; SwiftUI draws them at half (its radius is the
217Gaussian's deviation). The timing is 0.3.7's: lit once speech has kept up
2180.15 s, 50 ms attack, 300 ms release. Once no longer lit, `on` holds until the
219level has fallen under 0.05 (`SpeakingGlow.outBelow`; on the web `--on` waits
220out the release), as the mock-up's does, so the rim fades rather than
221vanishing. Reduce Motion: a steady lvl of 0.6 while lit, and nothing moves.
222Only the overlay observes the level, and it redraws only until it arrives.
223
224Snapshots: `CallsInterop --glass-snapshots <dir>` writes `mac-rim-<level>.png`
225(`WarmRimGallery`: the mock-up's dark and bright tiles, white, and the busy
226frame); the iPhone's are `CallsPhoneUITests.testWarmRimSnapshots`
227(`-CallsFixtureRimGallery <level>`).
228
229## Speaker view and your own order · 2026-09-27 (0.4.0)
230
231Paul asked whether he could rearrange the people in Grid, and for a Speaker
232view: the speaker in the main window, everyone else underneath.
233
234- **Two layouts: Speaker and Grid.** Speaker replaced Focus. Focus was "one
235 person on the stage, chosen by hand", which is Speaker with that person
236 pinned, so keeping both would have been two names for one thing with a
237 worse default. Choosing a pin (from the layout menu, a tile's menu or a
238 click on a filmstrip tile) switches to Speaker. The choice of layout is now
239 kept (`VideoLayout.key`, `videoLayout` in UserDefaults; `calls.videoLayout`
240 in the browser's localStorage), and a kept `focus` reads as Speaker.
241 Nothing had kept one before 0.4.0 — every call opened in Focus — so the
242 migration is a rule in `VideoLayout.stored`/`storedLayout` rather than data
243 to move. The iPhone's ⋯ sheet says Speaker/Grid and "On the stage".
244- **The stage** is, in order: what you pinned while it is there; otherwise the
245 newest shared screen (as before, so a new screen takes the stage unless you
246 have pinned something); otherwise whoever has the floor; otherwise the first
247 other person in your order (`Stage.speaker`). You are on it only if you pin
248 yourself.
249- **Who has the floor** is `ActiveSpeaker`, fed five times a second from the
250 per-peer levels and speaking gate the glow already uses. Someone takes the
251 stage once they have been the loudest speaker for `dominance` (1.3 s), and
252 whoever is on it keeps it for at least `minimumHold` (3 s). A pause keeps
253 the speaker; an interjection shorter than 1.3 s never takes it; two people
254 trading sentences faster than that do not make it flap; someone who leaves
255 loses it at once. You are never the speaker. room.js's `speakerStep` is
256 the same rule with the same constants (`SPEAKER_DOMINANCE`, `SPEAKER_HOLD`),
257 and `room-layout.test.mjs` holds them equal.
258- **The filmstrip** holds everything not on the stage — other screens first,
259 then everyone in your order, you included. On the Mac it runs along the
260 bottom of the window, tiles a sixth of the window high (72–120 pt; five fit
261 across the smallest window, 760 × 540, and more scroll sideways), and rises
262 clear of the bar while the controls show, as the self view does; with the
263 controls hidden nothing lies over the stage. On iPhone it is a sideways
264 scroller of upright 3:4 tiles above the home indicator (4:3 in landscape),
265 rising with the bar in the same way. Tiles in it keep the glass name
266 labels, the Muted mark and the speaking glow.
267- **You in the filmstrip, not floating** — except in a call of two. The
268 order is one list shared by Grid and the filmstrip, and you are in it
269 like anyone else, so you can put yourself first or last; a floating self
270 view would have been the one tile that ignored the order. In a call of
271 two the filmstrip would hold nobody but you, so there you float in the
272 corner exactly as since 0.3.4 (`Stage.selfFloats`), and the full-window
273 look of the commonest call is unchanged.
274- **Pinning.** A click or tap on a filmstrip tile, or Pin in a tile's menu
275 (Mac context menu) or its accessibility actions, pins it. A pinned stage
276 carries a **Pinned** badge; pressing it, or Unpin, goes back to following
277 the speaker. On the Mac a single click on a filmstrip tile pins once the
278 double-click interval has passed (a double click zooms the window, as on
279 the picture).
280- **Your own order.** Drag a tile onto another's place — in Grid or in the
281 filmstrip — and the others make room; the dragged tile lifts (larger, with
282 a shadow and the accent edge) and follows the pointer. On iPhone and iPad a
283 long press picks it up. With Reduce Motion nothing grows or springs. Every
284 platform has a way without dragging: the Mac's tile context menu, and on
285 every platform the tile's accessibility actions (Pin/Unpin, Move earlier,
286 Move later, Move to first); the browser gives each tile those as buttons.
287 The order is yours alone and never sent. It is kept per meeting for the
288 rest of the app's run (`ParticipantOrders`; the browser's sessionStorage,
289 `calls.order.<meeting>`), under keys that survive a rejoin — "self" and the
290 person's name, since a peer id is new on every join. New arrivals go on the
291 end; someone who leaves is removed; a remembered person who comes back
292 takes their old place (`ParticipantOrder`).
293- **VoiceOver** hears the stage tile as "On stage: Tony" (", pinned" when
294 it is), and a pin, an unpin or a move is announced once, politely. An
295 automatic change of speaker is not announced: in a lively call it would
296 talk over the talking.
297- **The mini window** shows Speaker view's stage, whichever layout the call
298 window is in.
299
300Snapshots: `CallsInterop --speaker-snapshots <dir>` (Mac, real windows: the
301filmstrip with the controls up and down and at 760 × 540, a pin, a shared
302screen, a drag half way in Speaker and in Grid, Grid after a reorder, and a
303call of two); `CallsPhoneUITests.testSpeakerViewSnapshots` (iPhone, portrait
304and landscape, with `-CallsFixtureLayout`, `-CallsFixtureSpeaker`,
305`-CallsFixturePinned`, `-CallsFixtureLift` and `-CallsFixtureOrder`); and the
306browser gate's `speaker` scenario with `CALLS_SPEAKER_SNAPSHOTS=<dir>`.
307